diff --git a/README.md b/README.md index 15b8708..15b265b 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ It is a Clojure interface for [xitdb-java](https://github.com/radarroark/xitdb-j - All heavy lifting done by the bare-to-the-jvm java library. - Database files can be used from other languages, via [xitdb Java library](https://github.com/radarroark/xitdb-java) or the [xitdb Zig library](https://github.com/radarroark/xitdb) -## Quickstart +## Quick Start Add the dependency to your project, start a REPL. @@ -59,27 +59,6 @@ For the programmer, a `xitdb` database is like a Clojure atom. (get-in @db [:users "alice" :age]) ;; => 31 ``` -One important distinction from the Clojure atom is that inside a transaction (eg. a `swap!`), -'change' operations on the received `db` argument are mutating the underlying data structure. - -```clojure -(with-db [db (xdb/xit-db :memory)] - (reset! db {}) - (swap! db (fn [db] - (let [db1 (assoc db :foo :bar)] - (println "db1:" db1) - (println "db:" db))))) -``` -prints -``` -db1: {:foo :bar} -db: {:foo :bar} -``` -As you can see, `(assoc db :foo :bar)` changed the value of `db`, in contrast -to how it works with a Clojure persistent map. This is because, inside `swap!`, -`db` is referencing a WriteCursor, which writes the value to the underlying -ArrayList or HashMap objects inside `xit-db-java`. -The value will actually be commited to the database when the `swap!` function returns. ## Data structures are read lazily from the database @@ -105,7 +84,7 @@ using Clojure functions. Use `materialize` to convert a nested `XITDB` data structure to a native Clojure data structure: ```clojure -(materialize (get-in @db [:users "alice"])) ;; => {:name "Alice" :age 31} +(xdb/materialize (get-in @db [:users "alice"])) ;; => {:name "Alice" :age 31} ``` ## No query language @@ -154,6 +133,42 @@ values of the database, by setting the `*return-history?*` binding to `true`. (println "new value:" new-value))) ``` +## Freezing + +One important distinction from the Clojure atom is that inside a transaction (eg. a `swap!`), the data is temporarily mutable. This is exactly like Clojure's transients, and it is a very important optimization. However, this can lead to a surprising behavior: + +```clojure +(swap! db (fn [moment] + (let [moment (assoc moment :fruits ["apple" "pear" "grape"]) + moment (assoc moment :food (:fruits moment)) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + +;; => + +{:fruits ["apple" "pear" "grape" "eggs" "rice" "fish"] + :food ["apple" "pear" "grape" "eggs" "rice" "fish"]} + +;; the fruits vector was mutated! +``` + +If you want to prevent data from being mutated within a transaction, you must `freeze!` it: + +```clojure +(swap! db (fn [moment] + (let [moment (assoc moment :fruits ["apple" "pear" "grape"]) + moment (assoc moment :food (xdb/freeze! (:fruits moment))) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + +;; => + +{:fruits ["apple" "pear" "grape"] + :food ["apple" "pear" "grape" "eggs" "rice" "fish"]} +``` + +Note that this is not doing an expensive copy of the fruits vector. We are benefitting from structural sharing, just like in-memory Clojure data. The reason we have to `freeze!` is because the default is different than Clojure; in Clojure, you must opt-in to temporary mutability by using transients, whereas in xitdb you must opt-out of it. + ### Architecture `xitdb-clj` builds on [xitdb-java](https://github.com/radarroark/xitdb-java) which implements: diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 5204882..c014e2a 100644 --- a/src/xitdb/array_list.clj +++ b/src/xitdb/array_list.clj @@ -19,7 +19,7 @@ (.count ral)) (cons [this o] - (cons o (common/-materialize-shallow this))) + (. clojure.lang.RT (conj (common/-materialize-shallow this) o))) (empty [this] []) @@ -29,7 +29,7 @@ (= (count this) (count other)) (every? identity (map = this other)))) - clojure.lang.Sequential ;; Add this to mark as sequential + clojure.lang.Sequential clojure.lang.Associative (assoc [this k v] @@ -125,6 +125,16 @@ (-unwrap [this] ral) + common/IMaterialize + (-materialize [this] + (reduce (fn [a v] + (conj a (common/materialize v))) [] (seq this))) + + common/IMaterializeShallow + (-materialize-shallow [this] + (reduce (fn [a v] + (conj a v)) [] (seq this))) + Object (toString [this] (pr-str (into [] this)))) @@ -133,18 +143,6 @@ (.write w "#XITDBArrayList") (print-method (into [] o) w)) -(extend-protocol common/IMaterialize - XITDBArrayList - (-materialize [this] - (reduce (fn [a v] - (conj a (common/materialize v))) [] (seq this)))) - -(extend-protocol common/IMaterializeShallow - XITDBArrayList - (-materialize-shallow [this] - (reduce (fn [a v] - (conj a v)) [] (seq this)))) - ;;----------------------------------------------- (deftype XITDBWriteArrayList [^WriteArrayList wal] @@ -184,6 +182,8 @@ (length [this] (.count wal)) + clojure.lang.Sequential + clojure.lang.Associative (assoc [this k v] (when-not (integer? k) @@ -248,10 +248,18 @@ (-unwrap [this] wal) + common/IReadOnly + (-read-only [this] + (XITDBArrayList. wal)) + Object (toString [this] (str "XITDBWriteArrayList"))) +(defmethod print-method XITDBWriteArrayList [o ^java.io.Writer w] + (.write w "#XITDBWriteArrayList") + (print-method (into [] (common/-read-only o)) w)) + ;; Constructors (defn xwrite-array-list [^WriteCursor write-cursor] diff --git a/src/xitdb/common.clj b/src/xitdb/common.clj index e94bace..8a34780 100644 --- a/src/xitdb/common.clj +++ b/src/xitdb/common.clj @@ -15,6 +15,8 @@ (defprotocol IUnwrap (-unwrap [this])) +(defprotocol IReadOnly + (-read-only [this])) (defn materialize [v] (cond diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index ecc7152..38c0622 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -231,4 +231,15 @@ :else (throw (IllegalArgumentException. (str "xdb must be an instance of XITDBCursor or XITDBDatabase, got: " (type xdb)))))) +(defn freeze! + "Prevents all data written in the current transaction from + being mutated by any remaining changes. Throws if called + outside of a transaction. Returns a read-only version of the + given writeable data structure." + [x] + (when-not (satisfies? common/IReadOnly x) + (throw (IllegalArgumentException. + (str "freeze! requires a writeable XITDB data structure, got: " (type x))))) + (-> x common/-unwrap .cursor .db .freeze) + (common/-read-only x)) diff --git a/src/xitdb/hash_map.clj b/src/xitdb/hash_map.clj index e564285..db90b98 100644 --- a/src/xitdb/hash_map.clj +++ b/src/xitdb/hash_map.clj @@ -89,6 +89,16 @@ (-unwrap [this] rhm) + common/IMaterialize + (-materialize [this] + (reduce (fn [m [k v]] + (assoc m k (common/materialize v))) {} (seq this))) + + common/IMaterializeShallow + (-materialize-shallow [this] + (reduce (fn [m [k v]] + (assoc m k v)) {} (seq this))) + Object (toString [this] (str (into {} this)))) @@ -97,18 +107,6 @@ (.write w "#XITDBHashMap") (print-method (into {} o) w)) -(extend-protocol common/IMaterialize - XITDBHashMap - (-materialize [this] - (reduce (fn [m [k v]] - (assoc m k (common/materialize v))) {} (seq this)))) - -(extend-protocol common/IMaterializeShallow - XITDBHashMap - (-materialize-shallow [this] - (reduce (fn [m [k v]] - (assoc m k v)) {} (seq this)))) - ;--------------------------------------------------- @@ -186,10 +184,17 @@ (-unwrap [this] whm) + common/IReadOnly + (-read-only [this] + (XITDBHashMap. whm)) + Object (toString [this] (str "XITDBWriteHashMap"))) +(defmethod print-method XITDBWriteHashMap [o ^java.io.Writer w] + (.write w "#XITDBWriteHashMap") + (print-method (into {} (common/-read-only o)) w)) (defn xwrite-hash-map [^WriteCursor write-cursor] (->XITDBWriteHashMap (WriteHashMap. write-cursor))) diff --git a/src/xitdb/hash_set.clj b/src/xitdb/hash_set.clj index ded6dd1..f0a453e 100644 --- a/src/xitdb/hash_set.clj +++ b/src/xitdb/hash_set.clj @@ -27,7 +27,7 @@ clojure.lang.IPersistentCollection (cons [this o] - (cons o (common/-materialize-shallow this))) + (. clojure.lang.RT (conj (common/-materialize-shallow this) o))) (empty [this] #{}) @@ -76,6 +76,14 @@ (-unwrap [_] rhs) + common/IMaterialize + (-materialize [this] + (into #{} (map common/materialize (seq this)))) + + common/IMaterializeShallow + (-materialize-shallow [this] + (into #{} (seq this))) + Object (toString [this] (str (into #{} this)))) @@ -84,16 +92,6 @@ (.write w "#XITDBHashSet") (print-method (into #{} o) w)) -(extend-protocol common/IMaterialize - XITDBHashSet - (-materialize [this] - (into #{} (map common/materialize (seq this))))) - -(extend-protocol common/IMaterializeShallow - XITDBHashSet - (-materialize-shallow [this] - (into #{} (seq this)))) - ;; Writable version of the set (deftype XITDBWriteHashSet [^WriteHashSet whs] clojure.lang.IPersistentSet @@ -146,10 +144,18 @@ (-unwrap [_] whs) + common/IReadOnly + (-read-only [this] + (XITDBHashSet. whs)) + Object (toString [_] (str "XITDBWriteHashSet"))) +(defmethod print-method XITDBWriteHashSet [o ^java.io.Writer w] + (.write w "#XITDBWriteHashSet") + (print-method (into #{} (common/-read-only o)) w)) + ;; Constructor functions (defn xwrite-hash-set [^WriteCursor write-cursor] (->XITDBWriteHashSet (WriteHashSet. write-cursor))) diff --git a/src/xitdb/linked_list.clj b/src/xitdb/linked_list.clj index c3120a4..163335c 100644 --- a/src/xitdb/linked_list.clj +++ b/src/xitdb/linked_list.clj @@ -7,8 +7,8 @@ [io.github.radarroark.xitdb ReadCursor ReadLinkedArrayList WriteCursor WriteLinkedArrayList])) (defn array-seq - [^ReadLinkedArrayList rlal] "The cursors used must implement the IReadFromCursor protocol." + [^ReadLinkedArrayList rlal] (operations/linked-array-seq rlal #(common/-read-from-cursor %))) (deftype XITDBLinkedArrayList [^ReadLinkedArrayList rlal] @@ -20,7 +20,7 @@ (.count rlal)) (cons [this o] - (cons o (common/-materialize-shallow this))) + (. clojure.lang.RT (conj (common/-materialize-shallow this) o))) (empty [this] '()) @@ -30,7 +30,7 @@ (= (count this) (count other)) (every? identity (map = this other)))) - clojure.lang.Sequential ;; Mark as sequential + clojure.lang.Sequential clojure.lang.Indexed (nth [_ i] @@ -94,6 +94,18 @@ (-unwrap [_] rlal) + common/IMaterialize + (-materialize [this] + (apply list + (reduce (fn [a v] + (conj a (common/materialize v))) [] (seq this)))) + + common/IMaterializeShallow + (-materialize-shallow [this] + (apply list + (reduce (fn [a v] + (conj a v)) [] (seq this)))) + Object (toString [this] (pr-str (into [] this)))) @@ -102,18 +114,6 @@ (.write w "#XITDBLinkedArrayList") (print-method (into [] o) w)) -(extend-protocol common/IMaterialize - XITDBLinkedArrayList - (-materialize [this] - (reduce (fn [a v] - (conj a (common/materialize v))) [] (seq this)))) - -(extend-protocol common/IMaterializeShallow - XITDBLinkedArrayList - (-materialize-shallow [this] - (reduce (fn [a v] - (conj a v)) [] (seq this)))) - ;; ----------------------------------------------------------------- (deftype XITDBWriteLinkedArrayList [^WriteLinkedArrayList wlal] @@ -122,7 +122,6 @@ (.count wlal)) (cons [this o] - ;; TODO: This should insert at position 0 (operations/linked-array-list-insert-value! wlal 0 (common/unwrap o)) this) @@ -139,6 +138,14 @@ (range (count this)))) false)) + clojure.lang.IPersistentVector + (assocN [this i val] + (operations/linked-array-list-assoc-value! wlal i (common/unwrap val)) + this) + + (length [this] + (.count wlal)) + clojure.lang.Indexed (nth [this i] (.nth this i nil)) @@ -148,6 +155,27 @@ (common/-read-from-cursor (.putCursor wlal i)) not-found)) + clojure.lang.Sequential + + clojure.lang.Associative + (assoc [this k v] + (when-not (integer? k) + (throw (IllegalArgumentException. "Key must be integer"))) + (operations/linked-array-list-assoc-value! wlal k (common/unwrap v)) + this) + + (containsKey [this k] + (and (integer? k) (>= k 0) (< k (.count wlal)))) + + (entryAt [this k] + (when (.containsKey this k) + (clojure.lang.MapEntry. k (.valAt this k)))) + + clojure.lang.IPersistentMap + (without [this key] + (operations/linked-array-list-remove-value! wlal key) + this) + clojure.lang.ILookup (valAt [this k] (.valAt this k nil)) @@ -202,16 +230,17 @@ (-unwrap [this] wlal) + common/IReadOnly + (-read-only [this] + (XITDBLinkedArrayList. wlal)) + Object (toString [this] (str "XITDBWriteLinkedArrayList"))) -(extend-protocol common/IMaterialize - XITDBLinkedArrayList - (-materialize [this] - (apply list - (reduce (fn [a v] - (conj a (common/materialize v))) [] (seq this))))) +(defmethod print-method XITDBWriteLinkedArrayList [o ^java.io.Writer w] + (.write w "#XITDBWriteLinkedArrayList") + (print-method (into [] (common/-read-only o)) w)) ;; Constructors diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index b53f37f..8934686 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -4,7 +4,7 @@ (:import [io.github.radarroark.xitdb Database Database$Bytes Database$Float Database$Int - ReadArrayList ReadCountedHashMap ReadCountedHashSet ReadCursor ReadHashMap + ReadArrayList ReadLinkedArrayList ReadCountedHashMap ReadCountedHashSet ReadCursor ReadHashMap ReadHashSet Slot Tag WriteArrayList WriteCountedHashMap WriteCountedHashSet WriteCursor WriteHashMap WriteHashSet WriteLinkedArrayList] [java.io OutputStream OutputStreamWriter] @@ -146,39 +146,27 @@ (validation/lazy-seq? v) (throw (IllegalArgumentException. "Lazy sequences can be infinite and not allowed!")) - (instance? WriteArrayList v) - (-> ^WriteArrayList v .cursor .slot) + ;; we only need to check for the read-only data structures, + ;; because the writeable data structures inherit from them! - (instance? WriteLinkedArrayList v) - (-> ^WriteLinkedArrayList v .cursor .slot) + (instance? ReadArrayList v) + (-> ^ReadArrayList v .cursor .slot) - (instance? WriteHashMap v) - (-> ^WriteHashMap v .cursor .slot) + (instance? ReadLinkedArrayList v) + (-> ^ReadLinkedArrayList v .cursor .slot) (instance? ReadHashMap v) (-> ^ReadHashMap v .cursor .slot) - (instance? ReadCountedHashMap v) - (-> ^ReadCountedHashMap v .cursor .slot) - - (instance? WriteCountedHashMap v) - (-> ^WriteCountedHashMap v .cursor .slot) - - (instance? ReadArrayList v) - (-> ^ReadArrayList v .cursor .slot) - (instance? ReadHashSet v) (-> ^ReadHashSet v .cursor .slot) + (instance? ReadCountedHashMap v) + (-> ^ReadCountedHashMap v .cursor .slot) + (instance? ReadCountedHashSet v) (-> ^ReadCountedHashSet v .cursor .slot) - (instance? WriteHashSet v) - (-> ^WriteHashSet v .cursor .slot) - - (instance? WriteCountedHashSet v) - (-> ^WriteCountedHashSet v .cursor .slot) - (map? v) (do (.write cursor nil) diff --git a/src/xitdb/util/operations.clj b/src/xitdb/util/operations.clj index 848c858..9b11e74 100644 --- a/src/xitdb/util/operations.clj +++ b/src/xitdb/util/operations.clj @@ -62,12 +62,36 @@ (.write cursor (conversion/v->slot! cursor v)) wlal)) +(defn ^WriteLinkedArrayList linked-array-list-assoc-value! + "Associates a value at index i in a WriteLinkedArrayList. + Appends the value if the index equals the current count. + Replaces the value at the specified index otherwise. + Throws an IllegalArgumentException if the index is out of bounds." + [^WriteLinkedArrayList wlal i v] + + (assert (= Tag/LINKED_ARRAY_LIST (-> wlal .cursor .slot .tag))) + (assert (number? i)) + + (validation/validate-index-bounds i (.count wlal) "Array list assoc") + + (let [cursor (if (= i (.count wlal)) + (.appendCursor wlal) + (.putCursor wlal i))] + (.write cursor (conversion/v->slot! cursor v))) + wlal) + (defn linked-array-list-insert-value! "Inserts a value at position pos in a WriteLinkedArrayList. Converts the value to an appropriate XitDB representation using v->slot!." [^WriteLinkedArrayList wlal pos v] - (let [cursor (-> wlal .cursor)] - (.insert wlal pos (conversion/v->slot! cursor v))) + (let [cursor (.insertCursor wlal pos)] + (.write cursor (conversion/v->slot! cursor v))) + wlal) + +(defn linked-array-list-remove-value! + "Removes a value at position pos in a WriteLinkedArrayList." + [^WriteLinkedArrayList wlal pos] + (.remove wlal pos) wlal) (defn linked-array-list-pop! diff --git a/test/xitdb/data_types_test.clj b/test/xitdb/data_types_test.clj index ab56756..8d2df88 100644 --- a/test/xitdb/data_types_test.clj +++ b/test/xitdb/data_types_test.clj @@ -132,8 +132,8 @@ (let [db-val @db shallow (common/-materialize-shallow db-val)] - ;; Should be a vector (shallow materialization converts to vector) - (is (vector? shallow)) + ;; Should be a list + (is (list? shallow)) ;; Values are preserved (is (= [1 2 3] shallow)))))) diff --git a/test/xitdb/freeze_test.clj b/test/xitdb/freeze_test.clj new file mode 100644 index 0000000..075b3e0 --- /dev/null +++ b/test/xitdb/freeze_test.clj @@ -0,0 +1,180 @@ +(ns xitdb.freeze-test + "Tests for the `freeze!` function" + (:require + [clojure.test :refer :all] + [xitdb.db :as xdb] + [xitdb.test-utils :as tu])) + +(deftest freeze-array-list-test + (testing "without freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits ["apple" "pear" "grape"]) + moment (assoc moment :food (:fruits moment)) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits ["apple" "pear" "grape" "eggs" "rice" "fish"] + :food ["apple" "pear" "grape" "eggs" "rice" "fish"]} + (xdb/materialize @db))))) + + (testing "with freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits ["apple" "pear" "grape"]) + moment (assoc moment :food (xdb/freeze! (:fruits moment))) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits ["apple" "pear" "grape"] + :food ["apple" "pear" "grape" "eggs" "rice" "fish"]} + (xdb/materialize @db))))) + + (testing "with freeze and modifying return value" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits ["apple" "pear" "grape"]) + moment (assoc moment :food (conj (xdb/freeze! (:fruits moment)) + "eggs" "rice" "fish"))] + moment))) + + (is (= {:fruits ["apple" "pear" "grape"] + :food ["apple" "pear" "grape" "eggs" "rice" "fish"]} + (xdb/materialize @db)))))) + +(deftest freeze-linked-array-list-test + (testing "without freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits '("apple" "pear" "grape")) + moment (assoc moment :food (:fruits moment)) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits ["fish" "rice" "eggs" "apple" "pear" "grape"] + :food ["fish" "rice" "eggs" "apple" "pear" "grape"]} + (xdb/materialize @db))))) + + (testing "with freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits '("apple" "pear" "grape")) + moment (assoc moment :food (xdb/freeze! (:fruits moment))) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits ["apple" "pear" "grape"] + :food ["fish" "rice" "eggs" "apple" "pear" "grape"]} + (xdb/materialize @db))))) + + (testing "with freeze and modifying return value" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits '("apple" "pear" "grape")) + moment (assoc moment :food (conj (xdb/freeze! (:fruits moment)) + "eggs" "rice" "fish"))] + moment))) + + (is (= {:fruits ["apple" "pear" "grape"] + :food ["fish" "rice" "eggs" "apple" "pear" "grape"]} + (xdb/materialize @db)))))) + +(deftest freeze-hash-map-test + (testing "without freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits {:names ["apple" "pear" "grape"]}) + moment (assoc moment :food (:fruits moment)) + moment (update-in moment [:food :names] conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits {:names ["apple" "pear" "grape" "eggs" "rice" "fish"]} + :food {:names ["apple" "pear" "grape" "eggs" "rice" "fish"]}} + (xdb/materialize @db))))) + + (testing "with freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits {:names ["apple" "pear" "grape"]}) + moment (assoc moment :food (xdb/freeze! (:fruits moment))) + moment (update-in moment [:food :names] conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits {:names ["apple" "pear" "grape"]} + :food {:names ["apple" "pear" "grape" "eggs" "rice" "fish"]}} + (xdb/materialize @db))))) + + (testing "with freeze and modifying return value" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits {:names ["apple" "pear" "grape"]}) + moment (assoc moment :food (update (xdb/freeze! (:fruits moment)) + :names conj + "eggs" "rice" "fish"))] + moment))) + + (is (= {:fruits {:names ["apple" "pear" "grape"]} + :food {:names ["apple" "pear" "grape" "eggs" "rice" "fish"]}} + (xdb/materialize @db)))))) + +(deftest freeze-hash-set-test + (testing "without freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits #{"apple" "pear" "grape"}) + moment (assoc moment :food (:fruits moment)) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits #{"apple" "pear" "grape" "eggs" "rice" "fish"} + :food #{"apple" "pear" "grape" "eggs" "rice" "fish"}} + (xdb/materialize @db))))) + + (testing "with freeze" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits #{"apple" "pear" "grape"}) + moment (assoc moment :food (xdb/freeze! (:fruits moment))) + moment (update moment :food conj "eggs" "rice" "fish")] + moment))) + + (is (= {:fruits #{"apple" "pear" "grape"} + :food #{"apple" "pear" "grape" "eggs" "rice" "fish"}} + (xdb/materialize @db))))) + + (testing "with freeze and modifying return value" + (with-open [db (xdb/xit-db :memory)] + (reset! db {}) + + (swap! db (fn [moment] + (let [moment (assoc moment :fruits #{"apple" "pear" "grape"}) + moment (assoc moment :food (conj (xdb/freeze! (:fruits moment)) + "eggs" "rice" "fish"))] + moment))) + + (is (= {:fruits #{"apple" "pear" "grape"} + :food #{"apple" "pear" "grape" "eggs" "rice" "fish"}} + (xdb/materialize @db)))))) + diff --git a/test/xitdb/linked_list_test.clj b/test/xitdb/linked_list_test.clj new file mode 100644 index 0000000..4153991 --- /dev/null +++ b/test/xitdb/linked_list_test.clj @@ -0,0 +1,51 @@ +(ns xitdb.linked-list-test + (:require + [clojure.test :refer :all] + [xitdb.db :as xdb])) + +(deftest LinkedListTest + (testing "Linked list works" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 2 3 4 5)) + (swap! db conj 6) + (swap! db assoc (count @db) 7) + + (is (= [6 1 2 3 4 5 7] @db)) + (is (= 7 (count @db))))) + + (testing "Basic operations" + (with-open [db (xdb/xit-db :memory)] + (testing "Creation" + (reset! db '(1 2 3 4 5)) + (is (= '(1 2 3 4 5) @db))) + + (testing "Membership" + (is (= 4 (get @db 3)))) + + (testing "Adding/Removing" + (swap! db conj 7) + (is (= '(7 1 2 3 4 5) @db)) + (swap! db dissoc 0) + (is (= '(1 2 3 4 5) @db))) + + (testing "Adding to read-only list" + (is (= [6 1 2 3 4 5] + (conj @db 6)))) + + (testing "Emptying" + (swap! db empty) + (is (= '() @db)) + (is (= 0 (count @db))))))) + +(deftest DataTypes + (testing "Supports nested types" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 {:foo :bar} [1 2 3 4] (7 89))) + (is (= '(1 {:foo :bar} [1 2 3 4] (7 89)) + (xdb/materialize @db))) + + (testing "Adding a list to the list" + (swap! db conj '(1 [3 4] {:new :map})) + (is (= '((1 [3 4] {:new :map}) 1 {:foo :bar} [1 2 3 4] (7 89)) + (xdb/materialize @db))))))) +