Skip to content

Commit 87ccef7

Browse files
authored
Merge pull request #5 from codeboost/cursor-impl
Implement XITDBCursor
2 parents 30b6652 + fdfe013 commit 87ccef7

File tree

6 files changed

+163
-40
lines changed

6 files changed

+163
-40
lines changed

src/xitdb/db.clj

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
;; Avoid extra require in your ns
1919
(def materialize common/materialize)
2020

21+
(defn open-database
22+
"Opens database `filename`.
23+
If `filename` is `:memory`, returns a memory based db.
24+
open-mode can be `r` or `rw`."
25+
[filename ^String open-mode]
26+
(let [core (if (= filename :memory)
27+
(CoreMemory. (RandomAccessMemory.))
28+
(CoreBufferedFile. (RandomAccessBufferedFile. (File. ^String filename) open-mode)))
29+
hasher (Hasher. (MessageDigest/getInstance "SHA-1"))]
30+
(Database. core hasher)))
31+
2132

2233
(defn ^WriteArrayList db-history [^Database db]
2334
(WriteArrayList. (.rootCursor db)))
@@ -42,17 +53,6 @@
4253
(append-context! history nil (fn [^WriteCursor cursor]
4354
(conversion/v->slot! cursor new-value))))
4455

45-
(defn open-database
46-
"Opens database `filename`.
47-
If `filename` is `:memory`, returns a memory based db.
48-
open-mode can be `r` or `rw`."
49-
[filename ^String open-mode]
50-
(let [core (if (= filename :memory)
51-
(CoreMemory. (RandomAccessMemory.))
52-
(CoreBufferedFile. (RandomAccessBufferedFile. (File. ^String filename) open-mode)))
53-
hasher (Hasher. (MessageDigest/getInstance "SHA-1"))]
54-
(Database. core hasher)))
55-
5656
(defn v->slot!
5757
"Converts a value to a slot which can be written to a cursor.
5858
For XITDB* types (which support ISlot), will return `-slot`,
@@ -63,35 +63,40 @@
6363
(conversion/v->slot! cursor v)))
6464

6565
(defn xitdb-swap!
66-
"Starts a new transaction and calls `f` with the value at root.
67-
`f` will receive a XITDBWrite* type (db) and `args`.
66+
"Starts a new transaction and calls `f` with the value at `base-keypath`.
67+
If `base-keypath` is nil, will use the root cursor.
68+
`f` will receive a XITDBWrite* type with the value at `base-keypath` and `args`.
6869
Actions on the XITDBWrite* type (like `assoc`) will mutate it.
69-
Return value of `f` is written at (root) cursor.
70+
Return value of `f` is written at `base-keypath` (or root) cursor.
7071
Returns the transaction history index."
71-
[db f & args]
72+
[db base-keypath f & args]
7273
(let [history (db-history db)
7374
slot (.getSlot history -1)]
7475
(append-context!
7576
history
7677
slot
7778
(fn [^WriteCursor cursor]
78-
(let [obj (xtypes/read-from-cursor cursor true)]
79+
(let [cursor (conversion/keypath-cursor cursor base-keypath)
80+
obj (xtypes/read-from-cursor cursor true)]
7981
(let [retval (apply f (into [obj] args))]
8082
(.write cursor (v->slot! cursor retval))))))))
8183

8284
(defn xitdb-swap-with-lock!
8385
"Performs the 'swap!' operation while locking `db.lock`.
8486
Returns the new value of the database.
8587
If the binding `*return-history?*` is true, returns
86-
`[current-history-index db-before db-after]`."
87-
[xitdb f & args]
88+
`[current-history-index db-before db-after]`.
89+
If `keypath` is not empty, the result of `f` will be written to the db at `keypath` rather
90+
than db root.
91+
Similarly, if `keypath` is not empty, the returned value will be the value at `keypath`."
92+
[xitdb base-keypath f & args]
8893
(let [^ReentrantLock lock (.-lock xitdb)]
8994
(when (.isHeldByCurrentThread lock)
9095
(throw (IllegalStateException. "swap! should not be called from swap! or reset!")))
9196
(try
9297
(.lock lock)
9398
(let [old-value (when *return-history?* (deref xitdb))
94-
index (apply xitdb-swap! (into [(-> xitdb .rwdb) f] args))
99+
index (apply xitdb-swap! (into [(-> xitdb .rwdb) base-keypath f] args))
95100
new-value (deref xitdb)]
96101
(if *return-history?*
97102
[index old-value new-value]
@@ -146,16 +151,16 @@
146151
(.unlock lock))))
147152

148153
(swap [this f]
149-
(xitdb-swap-with-lock! this f))
154+
(xitdb-swap-with-lock! this nil f))
150155

151156
(swap [this f a]
152-
(xitdb-swap-with-lock! this f a))
157+
(xitdb-swap-with-lock! this nil f a))
153158

154159
(swap [this f a1 a2]
155-
(xitdb-swap-with-lock! this f a1 a2))
160+
(xitdb-swap-with-lock! this nil f a1 a2))
156161

157162
(swap [this f x y args]
158-
(apply xitdb-swap-with-lock! (concat [this f x y] args))))
163+
(apply xitdb-swap-with-lock! (concat [this nil f x y] args))))
159164

160165
(defn xit-db
161166
"Returns a new XITDBDatabase which can be used to query and transact data.
@@ -178,4 +183,42 @@
178183
(->XITDBDatabase tldb rwdb (ReentrantLock.)))))
179184

180185

186+
(deftype XITDBCursor [xdb keypath]
187+
188+
java.io.Closeable
189+
(close [this])
190+
191+
clojure.lang.IDeref
192+
(deref [this]
193+
(let [v (deref xdb)]
194+
(get-in v keypath)))
195+
196+
clojure.lang.IAtom
197+
198+
(reset [this new-value]
199+
(xitdb-swap-with-lock! xdb keypath (constantly new-value)))
200+
201+
(swap [this f]
202+
(xitdb-swap-with-lock! xdb keypath f))
203+
204+
(swap [this f a]
205+
(xitdb-swap-with-lock! xdb keypath f a))
206+
207+
(swap [this f a1 a2]
208+
(xitdb-swap-with-lock! xdb keypath f a1 a2))
209+
210+
(swap [this f x y args]
211+
(apply xitdb-swap-with-lock! (concat [xdb keypath f x y] args))))
212+
213+
(defn xdb-cursor [xdb keypath]
214+
(cond
215+
(instance? XITDBCursor xdb)
216+
(XITDBCursor. (.-xdb xdb) (vec (concat (.-keypath xdb) keypath)))
217+
218+
(instance? XITDBDatabase xdb)
219+
(XITDBCursor. xdb keypath)
220+
221+
:else
222+
(throw (IllegalArgumentException. (str "xdb must be an instance of XITDBCursor or XITDBDatabase, got: " (type xdb))))))
223+
181224

src/xitdb/hash_map.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns xitdb.hash-map
22
(:require
33
[xitdb.common :as common]
4+
[xitdb.util.conversion :as conversion]
45
[xitdb.util.operations :as operations])
56
(:import
67
[io.github.radarroark.xitdb
@@ -157,7 +158,7 @@
157158
(let [cursor (operations/map-read-cursor whm key)]
158159
(if (nil? cursor)
159160
not-found
160-
(common/-read-from-cursor (operations/map-write-cursor whm key)))))
161+
(common/-read-from-cursor (conversion/map-write-cursor whm key)))))
161162

162163
clojure.lang.Seqable
163164
(seq [this]

src/xitdb/util/conversion.clj

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
(:import
55
[io.github.radarroark.xitdb
66
Database Database$Bytes Database$Float Database$Int
7-
ReadArrayList ReadCountedHashSet ReadCursor ReadHashMap ReadCountedHashMap
8-
ReadHashSet Slot Tag WriteArrayList WriteCountedHashSet WriteCursor WriteCountedHashMap
7+
ReadArrayList ReadCountedHashMap ReadCountedHashSet ReadCursor ReadHashMap
8+
ReadHashSet Slot Tag WriteArrayList WriteCountedHashMap WriteCountedHashSet WriteCursor
99
WriteHashMap WriteHashSet WriteLinkedArrayList]
1010
[java.io OutputStream OutputStreamWriter]
11-
[java.nio ByteBuffer]
1211
[java.security DigestOutputStream]))
1312

1413
(defn xit-tag->keyword
@@ -316,4 +315,65 @@
316315
nil
317316

318317
:else
319-
str)))
318+
str)))
319+
320+
(defn set-write-cursor
321+
[^WriteHashSet whs key]
322+
(let [hash-code (db-key-hash (-> whs .-cursor .-db) key)]
323+
(.putCursor whs hash-code)))
324+
325+
(defn map-write-cursor
326+
"Gets a write cursor for the specified key in a WriteHashMap.
327+
Creates the key if it doesn't exist."
328+
[^WriteHashMap whm key]
329+
(let [key-hash (db-key-hash (-> whm .cursor .db) key)]
330+
(.putCursor whm key-hash)))
331+
332+
(defn array-list-write-cursor
333+
"Returns a cursor to slot i in the array list.
334+
Throws if index is out of bounds."
335+
[^WriteArrayList wal i]
336+
(validation/validate-index-bounds i (.count wal) "Array list write cursor")
337+
(.putCursor wal i))
338+
339+
(defn linked-array-list-write-cursor
340+
[^WriteLinkedArrayList wlal i]
341+
(validation/validate-index-bounds i (.count wlal) "Linked array list write cursor")
342+
(.putCursor wlal i))
343+
344+
(defn write-cursor-for-key [cursor current-key]
345+
(let [value-tag (some-> cursor .slot .tag)]
346+
(cond
347+
(= value-tag Tag/HASH_MAP)
348+
(map-write-cursor (WriteHashMap. cursor) current-key)
349+
350+
(= value-tag Tag/COUNTED_HASH_MAP)
351+
(map-write-cursor (WriteCountedHashMap. cursor) current-key)
352+
353+
(= value-tag Tag/HASH_SET)
354+
(set-write-cursor (WriteHashSet. cursor) current-key)
355+
356+
(= value-tag Tag/COUNTED_HASH_SET)
357+
(set-write-cursor (WriteCountedHashSet. cursor) current-key)
358+
359+
(= value-tag Tag/ARRAY_LIST)
360+
(array-list-write-cursor (WriteArrayList. cursor) current-key)
361+
362+
(= value-tag Tag/LINKED_ARRAY_LIST)
363+
(linked-array-list-write-cursor (WriteLinkedArrayList. cursor) current-key)
364+
365+
:else
366+
(throw (IllegalArgumentException.
367+
(format "Cannot get cursor to key '%s' for value with tag '%s'" current-key (xit-tag->keyword value-tag)))))))
368+
369+
(defn keypath-cursor
370+
"Recursively goes to keypath and returns the write cursor"
371+
[^WriteCursor cursor keypath]
372+
(if (empty? keypath)
373+
cursor
374+
(loop [cursor cursor
375+
[current-key & remaining-keys] keypath]
376+
(let [new-cursor (write-cursor-for-key cursor current-key)]
377+
(if (empty? remaining-keys)
378+
new-cursor
379+
(recur new-cursor remaining-keys))))))

src/xitdb/util/operations.clj

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[xitdb.util.conversion :as conversion]
44
[xitdb.util.validation :as validation])
55
(:import
6-
[io.github.radarroark.xitdb ReadArrayList ReadCountedHashMap ReadCountedHashSet ReadHashMap ReadHashSet ReadLinkedArrayList Tag WriteArrayList WriteCursor WriteHashMap WriteHashSet WriteLinkedArrayList]))
6+
[io.github.radarroark.xitdb ReadArrayList ReadCountedHashMap ReadCountedHashSet ReadHashMap ReadHashSet ReadLinkedArrayList Tag WriteArrayList WriteCountedHashMap WriteCountedHashSet WriteCursor WriteHashMap WriteHashSet WriteLinkedArrayList]))
77

88
;; ============================================================================
99
;; Array List Operations
@@ -152,14 +152,6 @@
152152
(let [key-hash (conversion/db-key-hash (-> rhm .cursor .db) key)]
153153
(.getCursor rhm key-hash)))
154154

155-
156-
(defn map-write-cursor
157-
"Gets a write cursor for the specified key in a WriteHashMap.
158-
Creates the key if it doesn't exist."
159-
[^WriteHashMap whm key]
160-
(let [key-hash (conversion/db-key-hash (-> whm .cursor .db) key)]
161-
(.putCursor whm key-hash)))
162-
163155
;; ============================================================================
164156
;; Set Operations
165157
;; ============================================================================
@@ -286,4 +278,4 @@
286278
(if (reduced? new-result)
287279
@new-result
288280
(recur (inc i) new-result)))
289-
result))))
281+
result))))

src/xitdb/xitdb_types.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
[xitdb.array-list :as xarray-list]
44
[xitdb.common :as common]
55
[xitdb.hash-map :as xhash-map]
6-
[xitdb.linked-list :as xlinked-list]
76
[xitdb.hash-set :as xhash-set]
7+
[xitdb.linked-list :as xlinked-list]
88
[xitdb.util.conversion :as conversion])
99
(:import
10-
(io.github.radarroark.xitdb ReadCountedHashMap ReadCursor ReadHashMap Slot Tag WriteCursor WriteHashMap)))
10+
[io.github.radarroark.xitdb ReadCursor Slot Tag WriteCursor]))
1111

1212
(defn read-from-cursor
1313
"Reads the value at cursor and converts it to a Clojure type.

test/xitdb/cursor_test.clj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
(ns xitdb.cursor-test
2+
(:require
3+
[clojure.test :refer :all]
4+
[xitdb.db :as xdb]))
5+
6+
(deftest CursorTest
7+
(with-open [db (xdb/xit-db :memory)]
8+
(reset! db {:foo {:bar [1 2 3 {:hidden true} 5]}})
9+
(let [cursor1 (xdb/xdb-cursor db [:foo :bar])
10+
cursor2 (xdb/xdb-cursor db [:foo :bar 2])
11+
cursor3 (xdb/xdb-cursor db [:foo :bar 3 :hidden])]
12+
(testing "Cursors return the value at keypath"
13+
(is (= [1 2 3 {:hidden true} 5] (xdb/materialize @cursor1)))
14+
(is (= 3 @cursor2))
15+
(is (= true @cursor3)))
16+
17+
(testing "reset! on the cursor changes the underlying database"
18+
(reset! cursor3 :changed)
19+
(is (= :changed @cursor3))
20+
(is (= :changed (get-in @db [:foo :bar 3 :hidden])))
21+
(is (= [1 2 3 {:hidden :changed} 5]) (xdb/materialize @cursor1)))
22+
23+
(testing "swap! mutates the value at cursor"
24+
(swap! cursor1 assoc-in [3 :hidden] :changed-by-swap!)
25+
(is (= [1 2 3 {:hidden :changed-by-swap!} 5]) (xdb/materialize @cursor1))
26+
(is (= :changed-by-swap! @cursor3))
27+
(is (= 3 @cursor2))))))

0 commit comments

Comments
 (0)