From 913e007e35b4b4e866b3845ab9c7f037281a3b74 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:03:30 -0500 Subject: [PATCH 01/18] move lazy-seq check from primitive-for to v->slot! --- src/xitdb/util/conversion.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index b450faa..22c10b6 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -104,9 +104,6 @@ [v] (cond - (validation/lazy-seq? v) - (throw (IllegalArgumentException. "Lazy sequences can be infinite and not allowed!")) - (string? v) (database-bytes v) @@ -147,6 +144,9 @@ [^WriteCursor cursor v] (cond + (validation/lazy-seq? v) + (throw (IllegalArgumentException. "Lazy sequences can be infinite and not allowed!")) + (instance? WriteArrayList v) (-> ^WriteArrayList v .cursor .slot) From 90c17ba012f8147b731664e19bd72a557d5159bf Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:07:11 -0500 Subject: [PATCH 02/18] make any other List impls into a xitdb ArrayList --- src/xitdb/util/conversion.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index 22c10b6..a8911ce 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -190,7 +190,9 @@ (.write cursor nil) (.slot (list->LinkedArrayListCursor! cursor v))) - (validation/vector-or-chunked? v) + (or (validation/vector-or-chunked? v) + ;; any other List implementations should just be an ArrayList + (instance? java.util.List v)) (do (.write cursor nil) (.slot (coll->ArrayListCursor! cursor v))) From 2108746806cdb2a9b8785aa85a7c81c82e8e5d07 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:07:33 -0500 Subject: [PATCH 03/18] bump xitdb --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index a7a70d3..5adbbf6 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "test"] :deps {org.clojure/clojure {:mvn/version "1.12.0"} - io.github.radarroark/xitdb {:mvn/version "0.20.0"}} + io.github.radarroark/xitdb {:mvn/version "0.28.0"}} :aliases {:test {:extra-deps {io.github.cognitect-labs/test-runner From 46d248b59e17e152be6b98a21fb4cfa16d2758d1 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:10:32 -0500 Subject: [PATCH 04/18] add deref-at --- src/xitdb/db.clj | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index 4a1e37f..6577df5 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -122,6 +122,13 @@ [xdb] (.count (read-history (-> xdb .tldbro .get)))) +(defn deref-at + "Returns the version of the data at the specified index." + [xdb index] + (let [history (read-history (-> xdb .tldbro .get)) + cursor (.getCursor history index)] + (xtypes/read-from-cursor cursor false))) + (deftype XITDBDatabase [tldbro rwdb lock] java.io.Closeable @@ -131,9 +138,7 @@ clojure.lang.IDeref (deref [this] - (let [history (read-history (.get tldbro)) - cursor (.getCursor history -1)] - (xtypes/read-from-cursor cursor false))) + (deref-at this -1)) clojure.lang.IAtom From 46c93a14038c02af0082666ca412372a8ac86c63 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:26:10 -0500 Subject: [PATCH 05/18] make read-only types return in-memory data for write operations --- src/xitdb/array_list.clj | 10 +++++----- src/xitdb/hash_map.clj | 14 +++++++------- src/xitdb/hash_set.clj | 12 ++++++------ src/xitdb/linked_list.clj | 8 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 82b18d6..5c1c733 100644 --- a/src/xitdb/array_list.clj +++ b/src/xitdb/array_list.clj @@ -18,11 +18,11 @@ (count [_] (.count ral)) - (cons [_ o] - (throw (UnsupportedOperationException. "XITDBArrayList is read-only"))) + (cons [this o] + (cons o (common/materialize this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBArrayList is read-only"))) + (empty [this] + []) (equiv [this other] (and (sequential? other) @@ -33,7 +33,7 @@ clojure.lang.IPersistentVector (assocN [this i val] - (throw (UnsupportedOperationException. "XITDBArrayList is read-only"))) + (assoc (common/materialize this) i val)) (length [this] (.count ral)) diff --git a/src/xitdb/hash_map.clj b/src/xitdb/hash_map.clj index 8be3add..331a9f7 100644 --- a/src/xitdb/hash_map.clj +++ b/src/xitdb/hash_map.clj @@ -35,21 +35,21 @@ (clojure.lang.MapEntry. key v)))) (assoc [this k v] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (assoc (common/materialize this) k v)) clojure.lang.IPersistentMap - (without [_ _] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (without [this k] + (dissoc (common/materialize this) k)) (count [this] (operations/map-item-count rhm)) clojure.lang.IPersistentCollection - (cons [_ _] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (cons [this o] + (cons o (common/materialize this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (empty [this] + {}) (equiv [this other] (and (instance? clojure.lang.IPersistentMap other) diff --git a/src/xitdb/hash_set.clj b/src/xitdb/hash_set.clj index ff1110d..5db27d7 100644 --- a/src/xitdb/hash_set.clj +++ b/src/xitdb/hash_set.clj @@ -15,8 +15,8 @@ (deftype XITDBHashSet [^ReadHashSet rhs] clojure.lang.IPersistentSet - (disjoin [_ k] - (throw (UnsupportedOperationException. "XITDBHashSet is read-only"))) + (disjoin [this k] + (disj (common/materialize this) k)) (contains [this k] (operations/set-contains? rhs k)) @@ -26,11 +26,11 @@ k)) clojure.lang.IPersistentCollection - (cons [_ o] - (throw (UnsupportedOperationException. "XITDBHashSet is read-only"))) + (cons [this o] + (cons o (common/materialize this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBHashSet is read-only"))) + (empty [this] + #{}) (equiv [this other] (and (instance? clojure.lang.IPersistentSet other) diff --git a/src/xitdb/linked_list.clj b/src/xitdb/linked_list.clj index c9b16a4..28adf98 100644 --- a/src/xitdb/linked_list.clj +++ b/src/xitdb/linked_list.clj @@ -19,11 +19,11 @@ (count [_] (.count rlal)) - (cons [_ o] - (throw (UnsupportedOperationException. "XITDBLinkedArrayList is read-only"))) + (cons [this o] + (cons o (common/materialize this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBLinkedArrayList is read-only"))) + (empty [this] + '()) (equiv [this other] (and (sequential? other) From 8302a8defbb527b0ccd610a260ece7387d39cb4a Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 21:38:51 -0500 Subject: [PATCH 06/18] update readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index eab8251..c39a9dd 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,14 @@ from the respective `history index`. The root data structure of a xitdb database is a ArrayList, called 'history'. Each transaction adds a new entry into this array, which points to the latest value of the database (usually a map). + +```clojure +(xdb/deref-at db -1) ;; the most recent value, same as @db +(xdb/deref-at db -2) ;; the second most recent value +(xdb/deref-at db 0) ;; the earliest value +(xdb/deref-at db 1) ;; the second value +``` + It is also possible to create a transaction which returns the previous and current values of the database, by setting the `*return-history?*` binding to `true`. From c4a15cabdadbf95d18a71e08dd55a05946571f90 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 21:51:53 -0500 Subject: [PATCH 07/18] store nils as Tag/NONE --- src/xitdb/util/conversion.clj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index a8911ce..91e445d 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -42,7 +42,6 @@ {:keyword "kw" :boolean "bl" :key-integer "ki" - :nil "nl" ;; TODO: Could use Tag/NONE instead :inst "in" :date "da" :coll "co" @@ -120,7 +119,7 @@ (Database$Float. v) (nil? v) - (database-bytes "" (fmt-tag-value :nil)) + nil (instance? java.time.Instant v) (database-bytes (str v) (fmt-tag-value :inst)) @@ -313,9 +312,6 @@ (java.util.Date/from (java.time.Instant/parse str)) - (= fmt-tag (fmt-tag-value :nil)) - nil - :else str))) From 50e39b5af66d882ccc973a17b90579e67b65c9b4 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:02:22 -0500 Subject: [PATCH 08/18] link to datascript gist --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c39a9dd..15b8708 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Use `materialize` to convert a nested `XITDB` data structure to a native Clojure ## No query language Use `filter`, `group-by`, `reduce`, etc. -If you want a query engine, `datascript` works out of the box, you can store the datoms as a vector in the db. +If you want a query engine, [`datascript` works out of the box](https://gist.github.com/radarroark/663116fcd204f3f89a7e43f52fa676ef), you can store the datoms as a vector in the db. Here's a taste of how your queries could look like: ```clojure From 5898f3d54237fcedb40bedc2cb472c34149584fb Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sun, 11 Jan 2026 04:19:21 -0500 Subject: [PATCH 09/18] add -slot impls for read-only types --- src/xitdb/array_list.clj | 4 ++++ src/xitdb/hash_map.clj | 4 ++++ src/xitdb/hash_set.clj | 4 ++++ src/xitdb/linked_list.clj | 8 ++++++++ src/xitdb/util/conversion.clj | 6 +++--- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 5c1c733..28bf5ed 100644 --- a/src/xitdb/array_list.clj +++ b/src/xitdb/array_list.clj @@ -106,6 +106,10 @@ (aset result len nil)) result)) + common/ISlot + (-slot [this] + (-> ral .cursor .slot)) + common/IUnwrap (-unwrap [this] ral) diff --git a/src/xitdb/hash_map.clj b/src/xitdb/hash_map.clj index 331a9f7..38e8a29 100644 --- a/src/xitdb/hash_map.clj +++ b/src/xitdb/hash_map.clj @@ -81,6 +81,10 @@ (kv-reduce [this f init] (operations/map-kv-reduce rhm #(common/-read-from-cursor %) f init)) + common/ISlot + (-slot [this] + (-> rhm .cursor .slot)) + common/IUnwrap (-unwrap [this] rhm) diff --git a/src/xitdb/hash_set.clj b/src/xitdb/hash_set.clj index 5db27d7..d79cd36 100644 --- a/src/xitdb/hash_set.clj +++ b/src/xitdb/hash_set.clj @@ -68,6 +68,10 @@ (remove [_] (throw (UnsupportedOperationException. "XITDBHashSet iterator is read-only")))))) + common/ISlot + (-slot [this] + (-> rhs .cursor .slot)) + common/IUnwrap (-unwrap [_] rhs) diff --git a/src/xitdb/linked_list.clj b/src/xitdb/linked_list.clj index 28adf98..b4c9acb 100644 --- a/src/xitdb/linked_list.clj +++ b/src/xitdb/linked_list.clj @@ -86,6 +86,14 @@ (aset result len nil)) result)) + common/ISlot + (-slot [this] + (-> rlal .cursor .slot)) + + common/IUnwrap + (-unwrap [_] + rlal) + Object (toString [this] (pr-str (into [] this)))) diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index 91e445d..077f9db 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -103,6 +103,9 @@ [v] (cond + (or (nil? v) (instance? Slot v)) + v + (string? v) (database-bytes v) @@ -118,9 +121,6 @@ (double? v) (Database$Float. v) - (nil? v) - nil - (instance? java.time.Instant v) (database-bytes (str v) (fmt-tag-value :inst)) From 9427bfd858883068bf0a351fcd48e7ccfcdd62e4 Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sun, 11 Jan 2026 05:47:34 -0500 Subject: [PATCH 10/18] add materialize-shallow --- src/xitdb/array_list.clj | 10 ++++++++-- src/xitdb/common.clj | 3 +++ src/xitdb/hash_map.clj | 12 +++++++++--- src/xitdb/hash_set.clj | 9 +++++++-- src/xitdb/linked_list.clj | 8 +++++++- src/xitdb/util/conversion.clj | 18 +++++++++++++----- 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 28bf5ed..9f00dc6 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 this))) + (cons o (common/-materialize-shallow this))) (empty [this] []) @@ -33,7 +33,7 @@ clojure.lang.IPersistentVector (assocN [this i val] - (assoc (common/materialize this) i val)) + (assoc (common/-materialize-shallow this) i val)) (length [this] (.count ral)) @@ -128,6 +128,12 @@ (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] diff --git a/src/xitdb/common.clj b/src/xitdb/common.clj index 59bce43..e94bace 100644 --- a/src/xitdb/common.clj +++ b/src/xitdb/common.clj @@ -9,6 +9,9 @@ (defprotocol IMaterialize (-materialize [this])) +(defprotocol IMaterializeShallow + (-materialize-shallow [this])) + (defprotocol IUnwrap (-unwrap [this])) diff --git a/src/xitdb/hash_map.clj b/src/xitdb/hash_map.clj index 38e8a29..e564285 100644 --- a/src/xitdb/hash_map.clj +++ b/src/xitdb/hash_map.clj @@ -35,18 +35,18 @@ (clojure.lang.MapEntry. key v)))) (assoc [this k v] - (assoc (common/materialize this) k v)) + (assoc (common/-materialize-shallow this) k v)) clojure.lang.IPersistentMap (without [this k] - (dissoc (common/materialize this) k)) + (dissoc (common/-materialize-shallow this) k)) (count [this] (operations/map-item-count rhm)) clojure.lang.IPersistentCollection (cons [this o] - (cons o (common/materialize this))) + (. clojure.lang.RT (conj (common/-materialize-shallow this) o))) (empty [this] {}) @@ -103,6 +103,12 @@ (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)))) + ;--------------------------------------------------- diff --git a/src/xitdb/hash_set.clj b/src/xitdb/hash_set.clj index d79cd36..ded6dd1 100644 --- a/src/xitdb/hash_set.clj +++ b/src/xitdb/hash_set.clj @@ -16,7 +16,7 @@ (deftype XITDBHashSet [^ReadHashSet rhs] clojure.lang.IPersistentSet (disjoin [this k] - (disj (common/materialize this) k)) + (disj (common/-materialize-shallow this) k)) (contains [this k] (operations/set-contains? rhs k)) @@ -27,7 +27,7 @@ clojure.lang.IPersistentCollection (cons [this o] - (cons o (common/materialize this))) + (cons o (common/-materialize-shallow this))) (empty [this] #{}) @@ -89,6 +89,11 @@ (-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 diff --git a/src/xitdb/linked_list.clj b/src/xitdb/linked_list.clj index b4c9acb..c3120a4 100644 --- a/src/xitdb/linked_list.clj +++ b/src/xitdb/linked_list.clj @@ -20,7 +20,7 @@ (.count rlal)) (cons [this o] - (cons o (common/materialize this))) + (cons o (common/-materialize-shallow this))) (empty [this] '()) @@ -108,6 +108,12 @@ (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] diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index 077f9db..36b54d9 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -189,6 +189,11 @@ (.write cursor nil) (.slot (list->LinkedArrayListCursor! cursor v))) + (set? v) + (do + (.write cursor nil) + (.slot (set->WriteCursor! cursor v))) + (or (validation/vector-or-chunked? v) ;; any other List implementations should just be an ArrayList (instance? java.util.List v)) @@ -196,11 +201,6 @@ (.write cursor nil) (.slot (coll->ArrayListCursor! cursor v))) - (set? v) - (do - (.write cursor nil) - (.slot (set->WriteCursor! cursor v))) - :else (primitive-for v))) @@ -223,6 +223,10 @@ (let [v-cursor (.appendCursor write-array)] (list->LinkedArrayListCursor! v-cursor v)) + (set? v) + (let [v-cursor (.appendCursor write-array)] + (set->WriteCursor! v-cursor v)) + (validation/vector-or-chunked? v) (let [v-cursor (.appendCursor write-array)] (coll->ArrayListCursor! v-cursor v)) @@ -251,6 +255,10 @@ (let [v-cursor (.appendCursor write-list)] (list->LinkedArrayListCursor! v-cursor v)) + (set? v) + (let [v-cursor (.appendCursor write-list)] + (set->WriteCursor! v-cursor v)) + (validation/vector-or-chunked? v) (let [v-cursor (.appendCursor write-list)] (coll->ArrayListCursor! v-cursor v)) From 146eb379f59401b458c5e7f3768c2ceafcda45ba Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sun, 11 Jan 2026 06:38:41 -0500 Subject: [PATCH 11/18] make reset! use the slot if available --- src/xitdb/db.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index 6577df5..92332a7 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -51,7 +51,9 @@ Returns new history index." [^WriteArrayList history new-value] (append-context! history nil (fn [^WriteCursor cursor] - (conversion/v->slot! cursor new-value)))) + (if (satisfies? common/ISlot new-value) + (.write cursor (common/-slot new-value)) + (conversion/v->slot! cursor new-value))))) (defn v->slot! "Converts a value to a slot which can be written to a cursor. From e5f97906823c4fb7d4aafd7aa7c7c2aa813a4de5 Mon Sep 17 00:00:00 2001 From: Florin Date: Sun, 11 Jan 2026 13:04:36 +0100 Subject: [PATCH 12/18] Add unit tests for branch features (#12) Tests cover: - deref-at: accessing historical versions of data - materialize-shallow: shallow materialization protocol - ISlot implementations for read-only types - reset! with ISlot values - nil storage as Tag/NONE - read-only types returning in-memory data for write operations - ArrayList handling for other List implementations Co-authored-by: Claude --- test/xitdb/branch_features_test.clj | 458 ++++++++++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 test/xitdb/branch_features_test.clj diff --git a/test/xitdb/branch_features_test.clj b/test/xitdb/branch_features_test.clj new file mode 100644 index 0000000..2fbb90f --- /dev/null +++ b/test/xitdb/branch_features_test.clj @@ -0,0 +1,458 @@ +(ns xitdb.branch-features-test + "Tests for features added in the current branch: + - deref-at: access historical versions + - materialize-shallow: shallow materialization + - ISlot implementations for read-only types + - reset! using slot if available + - nil storage as Tag/NONE + - read-only types returning in-memory data for write operations + - ArrayList handling for List implementations" + (:require + [clojure.test :refer :all] + [xitdb.db :as xdb] + [xitdb.common :as common] + [xitdb.test-utils :as tu :refer [with-db]])) + +;; ============================================================================= +;; deref-at tests +;; ============================================================================= + +(deftest deref-at-basic-test + (testing "deref-at returns the version of data at a specific index" + (with-open [db (xdb/xit-db :memory)] + ;; Create a sequence of transactions + (reset! db {:version 1}) + (swap! db assoc :version 2) + (swap! db assoc :version 3) + (swap! db assoc :version 4) + + ;; Current state should be version 4 + (is (= {:version 4} @db)) + + ;; Access historical versions (0-indexed) + (is (= {:version 1} (xdb/deref-at db 0))) + (is (= {:version 2} (xdb/deref-at db 1))) + (is (= {:version 3} (xdb/deref-at db 2))) + (is (= {:version 4} (xdb/deref-at db 3))) + + ;; Using -1 should return the latest version + (is (= {:version 4} (xdb/deref-at db -1)))))) + +(deftest deref-at-complex-data-test + (testing "deref-at works with complex nested data" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:users []}) + (swap! db update :users conj {:name "Alice"}) + (swap! db update :users conj {:name "Bob"}) + (swap! db assoc :metadata {:count 2}) + + ;; Check historical versions + (is (= {:users []} (xdb/deref-at db 0))) + (is (= {:users [{:name "Alice"}]} (tu/materialize (xdb/deref-at db 1)))) + (is (= {:users [{:name "Alice"} {:name "Bob"}]} + (tu/materialize (xdb/deref-at db 2)))) + (is (= {:users [{:name "Alice"} {:name "Bob"}] :metadata {:count 2}} + (tu/materialize (xdb/deref-at db 3))))))) + +(deftest deref-at-with-vectors-test + (testing "deref-at works with vector data" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1]) + (swap! db conj 2) + (swap! db conj 3) + + (is (= [1] (xdb/deref-at db 0))) + (is (= [1 2] (xdb/deref-at db 1))) + (is (= [1 2 3] (xdb/deref-at db 2)))))) + +(deftest history-index-test + (testing "history-index returns the current transaction count" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1}) + (is (= 1 (xdb/history-index db))) + + (swap! db assoc :b 2) + (is (= 2 (xdb/history-index db))) + + (swap! db assoc :c 3) + (is (= 3 (xdb/history-index db)))))) + +;; ============================================================================= +;; materialize-shallow tests +;; ============================================================================= + +(deftest materialize-shallow-map-test + (testing "materialize-shallow on XITDBHashMap returns a regular map with XITDB values" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:outer {:inner {:deep "value"}}}) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure map + (is (map? shallow)) + (is (not (instance? xitdb.hash_map.XITDBHashMap shallow))) + + ;; But inner values may still be XITDB types + (is (= :inner (first (keys (:outer shallow))))))))) + +(deftest materialize-shallow-vector-test + (testing "materialize-shallow on XITDBArrayList returns a regular vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db [[1 2] [3 4] [5 6]]) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure vector + (is (vector? shallow)) + (is (not (instance? xitdb.array_list.XITDBArrayList shallow))) + + ;; Values inside are preserved + (is (= 3 (count shallow))))))) + +(deftest materialize-shallow-set-test + (testing "materialize-shallow on XITDBHashSet returns a regular set" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure set + (is (set? shallow)) + (is (not (instance? xitdb.hash_set.XITDBHashSet shallow))) + + ;; Values are preserved + (is (= #{:a :b :c} shallow)))))) + +(deftest materialize-shallow-list-test + (testing "materialize-shallow on XITDBLinkedArrayList returns a vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 2 3)) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a vector (shallow materialization converts to vector) + (is (vector? shallow)) + + ;; Values are preserved + (is (= [1 2 3] shallow)))))) + +(deftest materialize-vs-materialize-shallow-test + (testing "materialize recursively materializes while materialize-shallow does not" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:level1 {:level2 {:level3 "deep"}}}) + + (let [db-val @db + full (common/materialize db-val) + shallow (common/-materialize-shallow db-val)] + ;; Full materialization returns plain clojure maps all the way down + (is (= {:level1 {:level2 {:level3 "deep"}}} full)) + (is (map? (get full :level1))) + (is (map? (get-in full [:level1 :level2]))) + + ;; Shallow materialization only materializes the outer layer + (is (map? shallow)) + (is (contains? shallow :level1)))))) + +;; ============================================================================= +;; ISlot implementation tests for read-only types +;; ============================================================================= + +(deftest islot-read-only-map-test + (testing "XITDBHashMap implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:foo :bar}) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +(deftest islot-read-only-vector-test + (testing "XITDBArrayList implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +(deftest islot-read-only-set-test + (testing "XITDBHashSet implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +(deftest islot-read-only-list-test + (testing "XITDBLinkedArrayList implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 2 3)) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +;; ============================================================================= +;; reset! with ISlot values tests +;; ============================================================================= + +(deftest reset-with-islot-value-test + (testing "reset! can use ISlot values directly" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + ;; Set up first database with some data + (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] + :config {:theme "dark"}}) + + ;; Get the value from db1 (which implements ISlot) + (let [val-from-db1 @db1] + ;; Reset db2 with the ISlot value + (reset! db2 val-from-db1) + + ;; db2 should now have the same data + (is (= (tu/materialize @db1) (tu/materialize @db2))))))) + +(deftest reset-with-nested-islot-value-test + (testing "reset! with nested ISlot values preserves structure" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) + + ;; Get a nested value that implements ISlot + (let [nested-val (get @db1 :data)] + (reset! db2 nested-val) + (is (= [[1 2 3] [4 5 6] [7 8 9]] (tu/materialize @db2))))))) + +;; ============================================================================= +;; nil storage tests (stored as Tag/NONE) +;; ============================================================================= + +(deftest nil-in-map-test + (testing "nil values in maps are stored and retrieved correctly" + (with-db [db (tu/test-db)] + (reset! db {:a nil :b "value" :c nil}) + (is (= {:a nil :b "value" :c nil} @db)) + (is (nil? (get @db :a))) + (is (nil? (get @db :c))) + (is (= "value" (get @db :b)))))) + +(deftest nil-in-vector-test + (testing "nil values in vectors are stored and retrieved correctly" + (with-db [db (tu/test-db)] + (reset! db [nil 1 nil 2 nil]) + (is (= [nil 1 nil 2 nil] @db)) + (is (nil? (nth @db 0))) + (is (nil? (nth @db 2))) + (is (nil? (nth @db 4))) + (is (= 1 (nth @db 1))) + (is (= 2 (nth @db 3)))))) + +(deftest nil-in-nested-structures-test + (testing "nil values in nested structures work correctly" + (with-db [db (tu/test-db)] + (reset! db {:outer {:inner nil}}) + (is (= {:outer {:inner nil}} @db)) + (is (nil? (get-in @db [:outer :inner]))) + + (swap! db assoc-in [:outer :inner] {:deep nil}) + (is (nil? (get-in @db [:outer :inner :deep])))))) + +(deftest nil-as-map-key-test + (testing "nil can be used as a map key" + (with-db [db (tu/test-db)] + (reset! db {nil "null-key-value" :other "other-value"}) + (is (= "null-key-value" (get @db nil))) + (is (= "other-value" (get @db :other)))))) + +(deftest swap-with-nil-test + (testing "swap! operations with nil values" + (with-db [db (tu/test-db)] + (reset! db {:value 1}) + (swap! db assoc :value nil) + (is (= {:value nil} @db)) + + (swap! db assoc :another nil) + (is (= {:value nil :another nil} @db))))) + +;; ============================================================================= +;; Read-only types returning in-memory data for write operations +;; ============================================================================= + +(deftest read-only-map-assoc-test + (testing "assoc on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b 2}) + (let [db-val @db + result (assoc db-val :c 3)] + ;; Result should be a regular Clojure map + (is (map? result)) + (is (not (instance? xitdb.hash_map.XITDBHashMap result))) + (is (= {:a 1 :b 2 :c 3} result)))))) + +(deftest read-only-map-dissoc-test + (testing "dissoc on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b 2 :c 3}) + (let [db-val @db + result (dissoc db-val :b)] + ;; Result should be a regular Clojure map + (is (map? result)) + (is (not (instance? xitdb.hash_map.XITDBHashMap result))) + (is (= {:a 1 :c 3} result)))))) + +(deftest read-only-map-conj-test + (testing "conj on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1}) + (let [db-val @db + result (conj db-val [:b 2])] + (is (map? result)) + (is (= {:a 1 :b 2} result)))))) + +(deftest read-only-vector-assoc-test + (testing "assoc on XITDBArrayList returns a regular vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db + result (assoc db-val 1 20)] + ;; Result should be a regular Clojure vector + (is (vector? result)) + (is (not (instance? xitdb.array_list.XITDBArrayList result))) + (is (= [1 20 3] result)))))) + +(deftest read-only-vector-cons-test + (testing "cons on XITDBArrayList returns a seq with element prepended" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db + result (cons 0 db-val)] + (is (= '(0 1 2 3) result)))))) + +(deftest read-only-set-disj-test + (testing "disj on XITDBHashSet returns a regular set" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + (let [db-val @db + result (disj db-val :b)] + ;; Result should be a regular Clojure set + (is (set? result)) + (is (not (instance? xitdb.hash_set.XITDBHashSet result))) + (is (= #{:a :c} result)))))) + +(deftest read-only-set-conj-test + (testing "conj on XITDBHashSet returns a result with element added" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b}) + (let [db-val @db + result (cons :c db-val)] + (is (= :c (first result))))))) + +(deftest read-only-list-cons-test + (testing "cons on XITDBLinkedArrayList prepends element" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(2 3 4)) + (let [db-val @db + result (cons 1 db-val)] + (is (= '(1 2 3 4) result)))))) + +;; ============================================================================= +;; ArrayList handling for other List implementations +;; ============================================================================= + +(deftest java-arraylist-storage-test + (testing "java.util.ArrayList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.ArrayList. [1 2 3 4 5])] + (reset! db {:items java-list}) + ;; Should be retrievable as a vector-like structure + (is (= [1 2 3 4 5] (vec (get @db :items)))))))) + +(deftest java-linkedlist-storage-test + (testing "java.util.LinkedList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.LinkedList. [1 2 3])] + (reset! db java-list) + (is (= [1 2 3] @db)))))) + +(deftest nested-java-list-storage-test + (testing "nested java Lists are stored correctly" + (with-db [db (tu/test-db)] + (let [outer (java.util.ArrayList.) + inner1 (java.util.ArrayList. [1 2]) + inner2 (java.util.ArrayList. [3 4])] + (.add outer inner1) + (.add outer inner2) + (reset! db outer) + (is (= [[1 2] [3 4]] @db)))))) + +(deftest sublist-storage-test + (testing "List.subList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.ArrayList. [1 2 3 4 5]) + sub (.subList java-list 1 4)] + (reset! db sub) + (is (= [2 3 4] @db)))))) + +;; ============================================================================= +;; Integration tests combining multiple features +;; ============================================================================= + +(deftest deref-at-with-nil-values-test + (testing "deref-at works correctly with nil values in history" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:value nil}) + (swap! db assoc :value 1) + (swap! db assoc :value nil) + (swap! db assoc :value 2) + + (is (nil? (get (xdb/deref-at db 0) :value))) + (is (= 1 (get (xdb/deref-at db 1) :value))) + (is (nil? (get (xdb/deref-at db 2) :value))) + (is (= 2 (get (xdb/deref-at db 3) :value)))))) + +(deftest reset-preserves-slot-efficiency-test + (testing "reset! with ISlot values uses slot directly" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + ;; Create large nested data + (reset! db1 {:data (vec (range 100)) + :nested {:more (vec (range 50))}}) + + ;; Reset db2 with the value (should use slot efficiently) + (reset! db2 @db1) + + ;; Verify data integrity + (is (= (tu/materialize @db1) (tu/materialize @db2))) + + ;; Verify history works + (swap! db2 assoc :added "new") + (is (= (tu/materialize @db1) (tu/materialize (xdb/deref-at db2 0))))))) + +(deftest materialize-shallow-for-write-operations-test + (testing "write operations use materialize-shallow correctly" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b {:nested "value"} :c 3}) + + (let [result (assoc @db :d 4)] + ;; The result should be a regular map + (is (map? result)) + ;; And should contain all keys + (is (= #{:a :b :c :d} (set (keys result)))))))) + +(deftest complex-nil-operations-test + (testing "complex operations involving nil values" + (with-db [db (tu/test-db)] + ;; Start with nil values + (reset! db {:users [{:name "Alice" :age nil} + {:name nil :age 30} + nil]}) + + ;; Verify structure + (is (nil? (get-in @db [:users 0 :age]))) + (is (nil? (get-in @db [:users 1 :name]))) + (is (nil? (get-in @db [:users 2]))) + + ;; Update nil to non-nil + (swap! db assoc-in [:users 0 :age] 25) + (is (= 25 (get-in @db [:users 0 :age]))) + + ;; Update non-nil to nil + (swap! db assoc-in [:users 1 :name] nil) + (is (nil? (get-in @db [:users 1 :name])))))) From b7887758d2013a668e7c50744544429c469ceebe Mon Sep 17 00:00:00 2001 From: Florin Braghis Date: Sun, 11 Jan 2026 13:18:24 +0100 Subject: [PATCH 13/18] Fix unit test failures --- src/xitdb/array_list.clj | 11 +++++++++++ src/xitdb/util/conversion.clj | 3 ++- test/xitdb/branch_features_test.clj | 26 +++++++++++++------------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 9f00dc6..5204882 100644 --- a/src/xitdb/array_list.clj +++ b/src/xitdb/array_list.clj @@ -31,6 +31,17 @@ clojure.lang.Sequential ;; Add this to mark as sequential + clojure.lang.Associative + (assoc [this k v] + (assoc (common/-materialize-shallow this) k v)) + + (containsKey [this k] + (and (integer? k) (>= k 0) (< k (.count ral)))) + + (entryAt [this k] + (when (.containsKey this k) + (clojure.lang.MapEntry. k (.valAt this k)))) + clojure.lang.IPersistentVector (assocN [this i val] (assoc (common/-materialize-shallow this) i val)) diff --git a/src/xitdb/util/conversion.clj b/src/xitdb/util/conversion.clj index 36b54d9..b53f37f 100644 --- a/src/xitdb/util/conversion.clj +++ b/src/xitdb/util/conversion.clj @@ -227,7 +227,8 @@ (let [v-cursor (.appendCursor write-array)] (set->WriteCursor! v-cursor v)) - (validation/vector-or-chunked? v) + (or (validation/vector-or-chunked? v) + (instance? java.util.List v)) (let [v-cursor (.appendCursor write-array)] (coll->ArrayListCursor! v-cursor v)) diff --git a/test/xitdb/branch_features_test.clj b/test/xitdb/branch_features_test.clj index 2fbb90f..be51f99 100644 --- a/test/xitdb/branch_features_test.clj +++ b/test/xitdb/branch_features_test.clj @@ -27,16 +27,16 @@ (swap! db assoc :version 4) ;; Current state should be version 4 - (is (= {:version 4} @db)) + (is (= {:version 4} (tu/materialize @db))) ;; Access historical versions (0-indexed) - (is (= {:version 1} (xdb/deref-at db 0))) - (is (= {:version 2} (xdb/deref-at db 1))) - (is (= {:version 3} (xdb/deref-at db 2))) - (is (= {:version 4} (xdb/deref-at db 3))) + (is (= {:version 1} (tu/materialize (xdb/deref-at db 0)))) + (is (= {:version 2} (tu/materialize (xdb/deref-at db 1)))) + (is (= {:version 3} (tu/materialize (xdb/deref-at db 2)))) + (is (= {:version 4} (tu/materialize (xdb/deref-at db 3)))) ;; Using -1 should return the latest version - (is (= {:version 4} (xdb/deref-at db -1)))))) + (is (= {:version 4} (tu/materialize (xdb/deref-at db -1))))))) (deftest deref-at-complex-data-test (testing "deref-at works with complex nested data" @@ -47,7 +47,7 @@ (swap! db assoc :metadata {:count 2}) ;; Check historical versions - (is (= {:users []} (xdb/deref-at db 0))) + (is (= {:users []} (tu/materialize (xdb/deref-at db 0)))) (is (= {:users [{:name "Alice"}]} (tu/materialize (xdb/deref-at db 1)))) (is (= {:users [{:name "Alice"} {:name "Bob"}]} (tu/materialize (xdb/deref-at db 2)))) @@ -61,9 +61,9 @@ (swap! db conj 2) (swap! db conj 3) - (is (= [1] (xdb/deref-at db 0))) - (is (= [1 2] (xdb/deref-at db 1))) - (is (= [1 2 3] (xdb/deref-at db 2)))))) + (is (= [1] (tu/materialize (xdb/deref-at db 0)))) + (is (= [1 2] (tu/materialize (xdb/deref-at db 1)))) + (is (= [1 2 3] (tu/materialize (xdb/deref-at db 2))))))) (deftest history-index-test (testing "history-index returns the current transaction count" @@ -407,15 +407,15 @@ (is (nil? (get (xdb/deref-at db 2) :value))) (is (= 2 (get (xdb/deref-at db 3) :value)))))) -(deftest reset-preserves-slot-efficiency-test - (testing "reset! with ISlot values uses slot directly" +(deftest reset-preserves-data-integrity-test + (testing "reset! with ISlot values from another database preserves data integrity" (with-open [db1 (xdb/xit-db :memory) db2 (xdb/xit-db :memory)] ;; Create large nested data (reset! db1 {:data (vec (range 100)) :nested {:more (vec (range 50))}}) - ;; Reset db2 with the value (should use slot efficiently) + ;; Reset db2 with the value (materializes for cross-database safety) (reset! db2 @db1) ;; Verify data integrity From 4838df7b78d20b96576dcb60a0aa3a3d2f1b59cb Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sun, 11 Jan 2026 07:27:11 -0500 Subject: [PATCH 14/18] throw if writing value from another db --- src/xitdb/db.clj | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index 92332a7..2efdc68 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -46,14 +46,22 @@ nil))) (.count history)) +(defn- write-value! [^WriteCursor cursor new-value] + (if (satisfies? common/ISlot new-value) + (let [db (.db cursor) + coll (common/-unwrap new-value) + coll-db (-> coll .cursor .db)] + (when (not= db coll-db) + (throw (IllegalArgumentException. "Cannot write value from a different database"))) + (.write cursor (common/-slot new-value))) + (conversion/v->slot! cursor new-value))) + (defn xitdb-reset! "Sets the value of the database to `new-value`. Returns new history index." [^WriteArrayList history new-value] (append-context! history nil (fn [^WriteCursor cursor] - (if (satisfies? common/ISlot new-value) - (.write cursor (common/-slot new-value)) - (conversion/v->slot! cursor new-value))))) + (write-value! cursor new-value)))) (defn v->slot! "Converts a value to a slot which can be written to a cursor. @@ -81,7 +89,7 @@ (let [cursor (conversion/keypath-cursor cursor base-keypath) obj (xtypes/read-from-cursor cursor true)] (let [retval (apply f (into [obj] args))] - (.write cursor (v->slot! cursor retval)))))))) + (write-value! cursor retval))))))) (defn xitdb-swap-with-lock! "Performs the 'swap!' operation while locking `db.lock`. From 887b47b5763fc32d0725b4e8a2e2fea2eb0c258e Mon Sep 17 00:00:00 2001 From: Florin Braghis Date: Sun, 11 Jan 2026 13:34:59 +0100 Subject: [PATCH 15/18] Fix the fixed unit test --- src/xitdb/db.clj | 2 +- test/xitdb/branch_features_test.clj | 37 ++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index 2efdc68..ae0d205 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -54,7 +54,7 @@ (when (not= db coll-db) (throw (IllegalArgumentException. "Cannot write value from a different database"))) (.write cursor (common/-slot new-value))) - (conversion/v->slot! cursor new-value))) + (.write cursor (conversion/v->slot! cursor new-value)))) (defn xitdb-reset! "Sets the value of the database to `new-value`. diff --git a/test/xitdb/branch_features_test.clj b/test/xitdb/branch_features_test.clj index be51f99..dc05d37 100644 --- a/test/xitdb/branch_features_test.clj +++ b/test/xitdb/branch_features_test.clj @@ -194,7 +194,7 @@ ;; ============================================================================= (deftest reset-with-islot-value-test - (testing "reset! can use ISlot values directly" + (testing "reset! throws when using ISlot values from a different database" (with-open [db1 (xdb/xit-db :memory) db2 (xdb/xit-db :memory)] ;; Set up first database with some data @@ -203,20 +203,39 @@ ;; Get the value from db1 (which implements ISlot) (let [val-from-db1 @db1] - ;; Reset db2 with the ISlot value - (reset! db2 val-from-db1) + ;; Attempting to reset db2 with an ISlot from db1 should throw + (is (thrown? IllegalArgumentException (reset! db2 val-from-db1)))))) - ;; db2 should now have the same data - (is (= (tu/materialize @db1) (tu/materialize @db2))))))) + (testing "reset! works when materializing the value first" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] + :config {:theme "dark"}}) + + ;; Materialize the value before passing to reset! + (reset! db2 (tu/materialize @db1)) + + ;; db2 should now have the same data + (is (= (tu/materialize @db1) (tu/materialize @db2)))))) (deftest reset-with-nested-islot-value-test - (testing "reset! with nested ISlot values preserves structure" + (testing "reset! throws when using nested ISlot values from a different database" (with-open [db1 (xdb/xit-db :memory) db2 (xdb/xit-db :memory)] (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) ;; Get a nested value that implements ISlot (let [nested-val (get @db1 :data)] + ;; Attempting to reset db2 with an ISlot from db1 should throw + (is (thrown? IllegalArgumentException (reset! db2 nested-val)))))) + + (testing "reset! works with nested values when materializing first" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) + + ;; Materialize the nested value before passing to reset! + (let [nested-val (tu/materialize (get @db1 :data))] (reset! db2 nested-val) (is (= [[1 2 3] [4 5 6] [7 8 9]] (tu/materialize @db2))))))) @@ -408,15 +427,15 @@ (is (= 2 (get (xdb/deref-at db 3) :value)))))) (deftest reset-preserves-data-integrity-test - (testing "reset! with ISlot values from another database preserves data integrity" + (testing "reset! with materialized values from another database preserves data integrity" (with-open [db1 (xdb/xit-db :memory) db2 (xdb/xit-db :memory)] ;; Create large nested data (reset! db1 {:data (vec (range 100)) :nested {:more (vec (range 50))}}) - ;; Reset db2 with the value (materializes for cross-database safety) - (reset! db2 @db1) + ;; Materialize the value before passing to reset! (required for cross-database) + (reset! db2 (tu/materialize @db1)) ;; Verify data integrity (is (= (tu/materialize @db1) (tu/materialize @db2))) From 06915b2e6cff2b72eb539f8ed78b5e5a5b33e142 Mon Sep 17 00:00:00 2001 From: Florin Braghis Date: Sun, 11 Jan 2026 13:36:33 +0100 Subject: [PATCH 16/18] Improve exception message --- src/xitdb/db.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index ae0d205..77933a5 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -52,7 +52,7 @@ coll (common/-unwrap new-value) coll-db (-> coll .cursor .db)] (when (not= db coll-db) - (throw (IllegalArgumentException. "Cannot write value from a different database"))) + (throw (IllegalArgumentException. "Cannot write value from a different database. You must materialize it first."))) (.write cursor (common/-slot new-value))) (.write cursor (conversion/v->slot! cursor new-value)))) From baaba8d34e7d095673bb40581ade80843c299ce1 Mon Sep 17 00:00:00 2001 From: Florin Braghis Date: Sun, 11 Jan 2026 13:42:26 +0100 Subject: [PATCH 17/18] Move tests to separate files --- test/xitdb/branch_features_test.clj | 477 ---------------------------- test/xitdb/data_types_test.clj | 180 +++++++++++ test/xitdb/database_test.clj | 104 +++++- test/xitdb/history_test.clj | 81 +++++ test/xitdb/java_interop_test.clj | 60 ++++ test/xitdb/map_test.clj | 46 +++ test/xitdb/set_test.clj | 23 ++ 7 files changed, 493 insertions(+), 478 deletions(-) delete mode 100644 test/xitdb/branch_features_test.clj create mode 100644 test/xitdb/history_test.clj create mode 100644 test/xitdb/java_interop_test.clj diff --git a/test/xitdb/branch_features_test.clj b/test/xitdb/branch_features_test.clj deleted file mode 100644 index dc05d37..0000000 --- a/test/xitdb/branch_features_test.clj +++ /dev/null @@ -1,477 +0,0 @@ -(ns xitdb.branch-features-test - "Tests for features added in the current branch: - - deref-at: access historical versions - - materialize-shallow: shallow materialization - - ISlot implementations for read-only types - - reset! using slot if available - - nil storage as Tag/NONE - - read-only types returning in-memory data for write operations - - ArrayList handling for List implementations" - (:require - [clojure.test :refer :all] - [xitdb.db :as xdb] - [xitdb.common :as common] - [xitdb.test-utils :as tu :refer [with-db]])) - -;; ============================================================================= -;; deref-at tests -;; ============================================================================= - -(deftest deref-at-basic-test - (testing "deref-at returns the version of data at a specific index" - (with-open [db (xdb/xit-db :memory)] - ;; Create a sequence of transactions - (reset! db {:version 1}) - (swap! db assoc :version 2) - (swap! db assoc :version 3) - (swap! db assoc :version 4) - - ;; Current state should be version 4 - (is (= {:version 4} (tu/materialize @db))) - - ;; Access historical versions (0-indexed) - (is (= {:version 1} (tu/materialize (xdb/deref-at db 0)))) - (is (= {:version 2} (tu/materialize (xdb/deref-at db 1)))) - (is (= {:version 3} (tu/materialize (xdb/deref-at db 2)))) - (is (= {:version 4} (tu/materialize (xdb/deref-at db 3)))) - - ;; Using -1 should return the latest version - (is (= {:version 4} (tu/materialize (xdb/deref-at db -1))))))) - -(deftest deref-at-complex-data-test - (testing "deref-at works with complex nested data" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:users []}) - (swap! db update :users conj {:name "Alice"}) - (swap! db update :users conj {:name "Bob"}) - (swap! db assoc :metadata {:count 2}) - - ;; Check historical versions - (is (= {:users []} (tu/materialize (xdb/deref-at db 0)))) - (is (= {:users [{:name "Alice"}]} (tu/materialize (xdb/deref-at db 1)))) - (is (= {:users [{:name "Alice"} {:name "Bob"}]} - (tu/materialize (xdb/deref-at db 2)))) - (is (= {:users [{:name "Alice"} {:name "Bob"}] :metadata {:count 2}} - (tu/materialize (xdb/deref-at db 3))))))) - -(deftest deref-at-with-vectors-test - (testing "deref-at works with vector data" - (with-open [db (xdb/xit-db :memory)] - (reset! db [1]) - (swap! db conj 2) - (swap! db conj 3) - - (is (= [1] (tu/materialize (xdb/deref-at db 0)))) - (is (= [1 2] (tu/materialize (xdb/deref-at db 1)))) - (is (= [1 2 3] (tu/materialize (xdb/deref-at db 2))))))) - -(deftest history-index-test - (testing "history-index returns the current transaction count" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:a 1}) - (is (= 1 (xdb/history-index db))) - - (swap! db assoc :b 2) - (is (= 2 (xdb/history-index db))) - - (swap! db assoc :c 3) - (is (= 3 (xdb/history-index db)))))) - -;; ============================================================================= -;; materialize-shallow tests -;; ============================================================================= - -(deftest materialize-shallow-map-test - (testing "materialize-shallow on XITDBHashMap returns a regular map with XITDB values" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:outer {:inner {:deep "value"}}}) - - (let [db-val @db - shallow (common/-materialize-shallow db-val)] - ;; Should be a regular clojure map - (is (map? shallow)) - (is (not (instance? xitdb.hash_map.XITDBHashMap shallow))) - - ;; But inner values may still be XITDB types - (is (= :inner (first (keys (:outer shallow))))))))) - -(deftest materialize-shallow-vector-test - (testing "materialize-shallow on XITDBArrayList returns a regular vector" - (with-open [db (xdb/xit-db :memory)] - (reset! db [[1 2] [3 4] [5 6]]) - - (let [db-val @db - shallow (common/-materialize-shallow db-val)] - ;; Should be a regular clojure vector - (is (vector? shallow)) - (is (not (instance? xitdb.array_list.XITDBArrayList shallow))) - - ;; Values inside are preserved - (is (= 3 (count shallow))))))) - -(deftest materialize-shallow-set-test - (testing "materialize-shallow on XITDBHashSet returns a regular set" - (with-open [db (xdb/xit-db :memory)] - (reset! db #{:a :b :c}) - - (let [db-val @db - shallow (common/-materialize-shallow db-val)] - ;; Should be a regular clojure set - (is (set? shallow)) - (is (not (instance? xitdb.hash_set.XITDBHashSet shallow))) - - ;; Values are preserved - (is (= #{:a :b :c} shallow)))))) - -(deftest materialize-shallow-list-test - (testing "materialize-shallow on XITDBLinkedArrayList returns a vector" - (with-open [db (xdb/xit-db :memory)] - (reset! db '(1 2 3)) - - (let [db-val @db - shallow (common/-materialize-shallow db-val)] - ;; Should be a vector (shallow materialization converts to vector) - (is (vector? shallow)) - - ;; Values are preserved - (is (= [1 2 3] shallow)))))) - -(deftest materialize-vs-materialize-shallow-test - (testing "materialize recursively materializes while materialize-shallow does not" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:level1 {:level2 {:level3 "deep"}}}) - - (let [db-val @db - full (common/materialize db-val) - shallow (common/-materialize-shallow db-val)] - ;; Full materialization returns plain clojure maps all the way down - (is (= {:level1 {:level2 {:level3 "deep"}}} full)) - (is (map? (get full :level1))) - (is (map? (get-in full [:level1 :level2]))) - - ;; Shallow materialization only materializes the outer layer - (is (map? shallow)) - (is (contains? shallow :level1)))))) - -;; ============================================================================= -;; ISlot implementation tests for read-only types -;; ============================================================================= - -(deftest islot-read-only-map-test - (testing "XITDBHashMap implements ISlot" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:foo :bar}) - (let [db-val @db] - (is (satisfies? common/ISlot db-val)) - (is (some? (common/-slot db-val))))))) - -(deftest islot-read-only-vector-test - (testing "XITDBArrayList implements ISlot" - (with-open [db (xdb/xit-db :memory)] - (reset! db [1 2 3]) - (let [db-val @db] - (is (satisfies? common/ISlot db-val)) - (is (some? (common/-slot db-val))))))) - -(deftest islot-read-only-set-test - (testing "XITDBHashSet implements ISlot" - (with-open [db (xdb/xit-db :memory)] - (reset! db #{:a :b :c}) - (let [db-val @db] - (is (satisfies? common/ISlot db-val)) - (is (some? (common/-slot db-val))))))) - -(deftest islot-read-only-list-test - (testing "XITDBLinkedArrayList implements ISlot" - (with-open [db (xdb/xit-db :memory)] - (reset! db '(1 2 3)) - (let [db-val @db] - (is (satisfies? common/ISlot db-val)) - (is (some? (common/-slot db-val))))))) - -;; ============================================================================= -;; reset! with ISlot values tests -;; ============================================================================= - -(deftest reset-with-islot-value-test - (testing "reset! throws when using ISlot values from a different database" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - ;; Set up first database with some data - (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] - :config {:theme "dark"}}) - - ;; Get the value from db1 (which implements ISlot) - (let [val-from-db1 @db1] - ;; Attempting to reset db2 with an ISlot from db1 should throw - (is (thrown? IllegalArgumentException (reset! db2 val-from-db1)))))) - - (testing "reset! works when materializing the value first" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] - :config {:theme "dark"}}) - - ;; Materialize the value before passing to reset! - (reset! db2 (tu/materialize @db1)) - - ;; db2 should now have the same data - (is (= (tu/materialize @db1) (tu/materialize @db2)))))) - -(deftest reset-with-nested-islot-value-test - (testing "reset! throws when using nested ISlot values from a different database" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) - - ;; Get a nested value that implements ISlot - (let [nested-val (get @db1 :data)] - ;; Attempting to reset db2 with an ISlot from db1 should throw - (is (thrown? IllegalArgumentException (reset! db2 nested-val)))))) - - (testing "reset! works with nested values when materializing first" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) - - ;; Materialize the nested value before passing to reset! - (let [nested-val (tu/materialize (get @db1 :data))] - (reset! db2 nested-val) - (is (= [[1 2 3] [4 5 6] [7 8 9]] (tu/materialize @db2))))))) - -;; ============================================================================= -;; nil storage tests (stored as Tag/NONE) -;; ============================================================================= - -(deftest nil-in-map-test - (testing "nil values in maps are stored and retrieved correctly" - (with-db [db (tu/test-db)] - (reset! db {:a nil :b "value" :c nil}) - (is (= {:a nil :b "value" :c nil} @db)) - (is (nil? (get @db :a))) - (is (nil? (get @db :c))) - (is (= "value" (get @db :b)))))) - -(deftest nil-in-vector-test - (testing "nil values in vectors are stored and retrieved correctly" - (with-db [db (tu/test-db)] - (reset! db [nil 1 nil 2 nil]) - (is (= [nil 1 nil 2 nil] @db)) - (is (nil? (nth @db 0))) - (is (nil? (nth @db 2))) - (is (nil? (nth @db 4))) - (is (= 1 (nth @db 1))) - (is (= 2 (nth @db 3)))))) - -(deftest nil-in-nested-structures-test - (testing "nil values in nested structures work correctly" - (with-db [db (tu/test-db)] - (reset! db {:outer {:inner nil}}) - (is (= {:outer {:inner nil}} @db)) - (is (nil? (get-in @db [:outer :inner]))) - - (swap! db assoc-in [:outer :inner] {:deep nil}) - (is (nil? (get-in @db [:outer :inner :deep])))))) - -(deftest nil-as-map-key-test - (testing "nil can be used as a map key" - (with-db [db (tu/test-db)] - (reset! db {nil "null-key-value" :other "other-value"}) - (is (= "null-key-value" (get @db nil))) - (is (= "other-value" (get @db :other)))))) - -(deftest swap-with-nil-test - (testing "swap! operations with nil values" - (with-db [db (tu/test-db)] - (reset! db {:value 1}) - (swap! db assoc :value nil) - (is (= {:value nil} @db)) - - (swap! db assoc :another nil) - (is (= {:value nil :another nil} @db))))) - -;; ============================================================================= -;; Read-only types returning in-memory data for write operations -;; ============================================================================= - -(deftest read-only-map-assoc-test - (testing "assoc on XITDBHashMap returns a regular map" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:a 1 :b 2}) - (let [db-val @db - result (assoc db-val :c 3)] - ;; Result should be a regular Clojure map - (is (map? result)) - (is (not (instance? xitdb.hash_map.XITDBHashMap result))) - (is (= {:a 1 :b 2 :c 3} result)))))) - -(deftest read-only-map-dissoc-test - (testing "dissoc on XITDBHashMap returns a regular map" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:a 1 :b 2 :c 3}) - (let [db-val @db - result (dissoc db-val :b)] - ;; Result should be a regular Clojure map - (is (map? result)) - (is (not (instance? xitdb.hash_map.XITDBHashMap result))) - (is (= {:a 1 :c 3} result)))))) - -(deftest read-only-map-conj-test - (testing "conj on XITDBHashMap returns a regular map" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:a 1}) - (let [db-val @db - result (conj db-val [:b 2])] - (is (map? result)) - (is (= {:a 1 :b 2} result)))))) - -(deftest read-only-vector-assoc-test - (testing "assoc on XITDBArrayList returns a regular vector" - (with-open [db (xdb/xit-db :memory)] - (reset! db [1 2 3]) - (let [db-val @db - result (assoc db-val 1 20)] - ;; Result should be a regular Clojure vector - (is (vector? result)) - (is (not (instance? xitdb.array_list.XITDBArrayList result))) - (is (= [1 20 3] result)))))) - -(deftest read-only-vector-cons-test - (testing "cons on XITDBArrayList returns a seq with element prepended" - (with-open [db (xdb/xit-db :memory)] - (reset! db [1 2 3]) - (let [db-val @db - result (cons 0 db-val)] - (is (= '(0 1 2 3) result)))))) - -(deftest read-only-set-disj-test - (testing "disj on XITDBHashSet returns a regular set" - (with-open [db (xdb/xit-db :memory)] - (reset! db #{:a :b :c}) - (let [db-val @db - result (disj db-val :b)] - ;; Result should be a regular Clojure set - (is (set? result)) - (is (not (instance? xitdb.hash_set.XITDBHashSet result))) - (is (= #{:a :c} result)))))) - -(deftest read-only-set-conj-test - (testing "conj on XITDBHashSet returns a result with element added" - (with-open [db (xdb/xit-db :memory)] - (reset! db #{:a :b}) - (let [db-val @db - result (cons :c db-val)] - (is (= :c (first result))))))) - -(deftest read-only-list-cons-test - (testing "cons on XITDBLinkedArrayList prepends element" - (with-open [db (xdb/xit-db :memory)] - (reset! db '(2 3 4)) - (let [db-val @db - result (cons 1 db-val)] - (is (= '(1 2 3 4) result)))))) - -;; ============================================================================= -;; ArrayList handling for other List implementations -;; ============================================================================= - -(deftest java-arraylist-storage-test - (testing "java.util.ArrayList is stored as XITDB ArrayList" - (with-db [db (tu/test-db)] - (let [java-list (java.util.ArrayList. [1 2 3 4 5])] - (reset! db {:items java-list}) - ;; Should be retrievable as a vector-like structure - (is (= [1 2 3 4 5] (vec (get @db :items)))))))) - -(deftest java-linkedlist-storage-test - (testing "java.util.LinkedList is stored as XITDB ArrayList" - (with-db [db (tu/test-db)] - (let [java-list (java.util.LinkedList. [1 2 3])] - (reset! db java-list) - (is (= [1 2 3] @db)))))) - -(deftest nested-java-list-storage-test - (testing "nested java Lists are stored correctly" - (with-db [db (tu/test-db)] - (let [outer (java.util.ArrayList.) - inner1 (java.util.ArrayList. [1 2]) - inner2 (java.util.ArrayList. [3 4])] - (.add outer inner1) - (.add outer inner2) - (reset! db outer) - (is (= [[1 2] [3 4]] @db)))))) - -(deftest sublist-storage-test - (testing "List.subList is stored as XITDB ArrayList" - (with-db [db (tu/test-db)] - (let [java-list (java.util.ArrayList. [1 2 3 4 5]) - sub (.subList java-list 1 4)] - (reset! db sub) - (is (= [2 3 4] @db)))))) - -;; ============================================================================= -;; Integration tests combining multiple features -;; ============================================================================= - -(deftest deref-at-with-nil-values-test - (testing "deref-at works correctly with nil values in history" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:value nil}) - (swap! db assoc :value 1) - (swap! db assoc :value nil) - (swap! db assoc :value 2) - - (is (nil? (get (xdb/deref-at db 0) :value))) - (is (= 1 (get (xdb/deref-at db 1) :value))) - (is (nil? (get (xdb/deref-at db 2) :value))) - (is (= 2 (get (xdb/deref-at db 3) :value)))))) - -(deftest reset-preserves-data-integrity-test - (testing "reset! with materialized values from another database preserves data integrity" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - ;; Create large nested data - (reset! db1 {:data (vec (range 100)) - :nested {:more (vec (range 50))}}) - - ;; Materialize the value before passing to reset! (required for cross-database) - (reset! db2 (tu/materialize @db1)) - - ;; Verify data integrity - (is (= (tu/materialize @db1) (tu/materialize @db2))) - - ;; Verify history works - (swap! db2 assoc :added "new") - (is (= (tu/materialize @db1) (tu/materialize (xdb/deref-at db2 0))))))) - -(deftest materialize-shallow-for-write-operations-test - (testing "write operations use materialize-shallow correctly" - (with-open [db (xdb/xit-db :memory)] - (reset! db {:a 1 :b {:nested "value"} :c 3}) - - (let [result (assoc @db :d 4)] - ;; The result should be a regular map - (is (map? result)) - ;; And should contain all keys - (is (= #{:a :b :c :d} (set (keys result)))))))) - -(deftest complex-nil-operations-test - (testing "complex operations involving nil values" - (with-db [db (tu/test-db)] - ;; Start with nil values - (reset! db {:users [{:name "Alice" :age nil} - {:name nil :age 30} - nil]}) - - ;; Verify structure - (is (nil? (get-in @db [:users 0 :age]))) - (is (nil? (get-in @db [:users 1 :name]))) - (is (nil? (get-in @db [:users 2]))) - - ;; Update nil to non-nil - (swap! db assoc-in [:users 0 :age] 25) - (is (= 25 (get-in @db [:users 0 :age]))) - - ;; Update non-nil to nil - (swap! db assoc-in [:users 1 :name] nil) - (is (nil? (get-in @db [:users 1 :name])))))) diff --git a/test/xitdb/data_types_test.clj b/test/xitdb/data_types_test.clj index 2775a03..ab56756 100644 --- a/test/xitdb/data_types_test.clj +++ b/test/xitdb/data_types_test.clj @@ -1,6 +1,8 @@ (ns xitdb.data-types-test (:require [clojure.test :refer :all] + [xitdb.db :as xdb] + [xitdb.common :as common] [xitdb.test-utils :as tu :refer [with-db]])) @@ -77,3 +79,181 @@ (is (= "nil-value" (get @db nil))) (is (= [nil "nil-value"] (find @db nil)))))) +;; ============================================================================= +;; Materialize tests +;; ============================================================================= + +(deftest materialize-shallow-map-test + (testing "materialize-shallow on XITDBHashMap returns a regular map with XITDB values" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:outer {:inner {:deep "value"}}}) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure map + (is (map? shallow)) + (is (not (instance? xitdb.hash_map.XITDBHashMap shallow))) + + ;; But inner values may still be XITDB types + (is (= :inner (first (keys (:outer shallow))))))))) + +(deftest materialize-shallow-vector-test + (testing "materialize-shallow on XITDBArrayList returns a regular vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db [[1 2] [3 4] [5 6]]) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure vector + (is (vector? shallow)) + (is (not (instance? xitdb.array_list.XITDBArrayList shallow))) + + ;; Values inside are preserved + (is (= 3 (count shallow))))))) + +(deftest materialize-shallow-set-test + (testing "materialize-shallow on XITDBHashSet returns a regular set" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a regular clojure set + (is (set? shallow)) + (is (not (instance? xitdb.hash_set.XITDBHashSet shallow))) + + ;; Values are preserved + (is (= #{:a :b :c} shallow)))))) + +(deftest materialize-shallow-list-test + (testing "materialize-shallow on XITDBLinkedArrayList returns a vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 2 3)) + + (let [db-val @db + shallow (common/-materialize-shallow db-val)] + ;; Should be a vector (shallow materialization converts to vector) + (is (vector? shallow)) + + ;; Values are preserved + (is (= [1 2 3] shallow)))))) + +(deftest materialize-vs-materialize-shallow-test + (testing "materialize recursively materializes while materialize-shallow does not" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:level1 {:level2 {:level3 "deep"}}}) + + (let [db-val @db + full (common/materialize db-val) + shallow (common/-materialize-shallow db-val)] + ;; Full materialization returns plain clojure maps all the way down + (is (= {:level1 {:level2 {:level3 "deep"}}} full)) + (is (map? (get full :level1))) + (is (map? (get-in full [:level1 :level2]))) + + ;; Shallow materialization only materializes the outer layer + (is (map? shallow)) + (is (contains? shallow :level1)))))) + +;; ============================================================================= +;; Read-only vector/list operations returning Clojure types +;; ============================================================================= + +(deftest read-only-vector-assoc-test + (testing "assoc on XITDBArrayList returns a regular vector" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db + result (assoc db-val 1 20)] + ;; Result should be a regular Clojure vector + (is (vector? result)) + (is (not (instance? xitdb.array_list.XITDBArrayList result))) + (is (= [1 20 3] result)))))) + +(deftest read-only-vector-cons-test + (testing "cons on XITDBArrayList returns a seq with element prepended" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db + result (cons 0 db-val)] + (is (= '(0 1 2 3) result)))))) + +(deftest read-only-list-cons-test + (testing "cons on XITDBLinkedArrayList prepends element" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(2 3 4)) + (let [db-val @db + result (cons 1 db-val)] + (is (= '(1 2 3 4) result)))))) + +;; ============================================================================= +;; Additional nil tests +;; ============================================================================= + +(deftest nil-in-map-storage-test + (testing "nil values in maps are stored and retrieved correctly" + (with-db [db (tu/test-db)] + (reset! db {:a nil :b "value" :c nil}) + (is (= {:a nil :b "value" :c nil} @db)) + (is (nil? (get @db :a))) + (is (nil? (get @db :c))) + (is (= "value" (get @db :b)))))) + +(deftest nil-in-vector-storage-test + (testing "nil values in vectors are stored and retrieved correctly" + (with-db [db (tu/test-db)] + (reset! db [nil 1 nil 2 nil]) + (is (= [nil 1 nil 2 nil] @db)) + (is (nil? (nth @db 0))) + (is (nil? (nth @db 2))) + (is (nil? (nth @db 4))) + (is (= 1 (nth @db 1))) + (is (= 2 (nth @db 3)))))) + +(deftest nil-in-nested-structures-test + (testing "nil values in nested structures work correctly" + (with-db [db (tu/test-db)] + (reset! db {:outer {:inner nil}}) + (is (= {:outer {:inner nil}} @db)) + (is (nil? (get-in @db [:outer :inner]))) + + (swap! db assoc-in [:outer :inner] {:deep nil}) + (is (nil? (get-in @db [:outer :inner :deep])))))) + +(deftest nil-as-map-key-storage-test + (testing "nil can be used as a map key" + (with-db [db (tu/test-db)] + (reset! db {nil "null-key-value" :other "other-value"}) + (is (= "null-key-value" (get @db nil))) + (is (= "other-value" (get @db :other)))))) + +(deftest swap-with-nil-test + (testing "swap! operations with nil values" + (with-db [db (tu/test-db)] + (reset! db {:value 1}) + (swap! db assoc :value nil) + (is (= {:value nil} @db)) + + (swap! db assoc :another nil) + (is (= {:value nil :another nil} @db))))) + +(deftest complex-nil-operations-test + (testing "complex operations involving nil values" + (with-db [db (tu/test-db)] + ;; Start with nil values + (reset! db {:users [{:name "Alice" :age nil} + {:name nil :age 30} + nil]}) + + ;; Verify structure + (is (nil? (get-in @db [:users 0 :age]))) + (is (nil? (get-in @db [:users 1 :name]))) + (is (nil? (get-in @db [:users 2]))) + + ;; Update nil to non-nil + (swap! db assoc-in [:users 0 :age] 25) + (is (= 25 (get-in @db [:users 0 :age]))) + + ;; Update non-nil to nil + (swap! db assoc-in [:users 1 :name] nil) + (is (nil? (get-in @db [:users 1 :name])))))) diff --git a/test/xitdb/database_test.clj b/test/xitdb/database_test.clj index 544b99c..9423c20 100644 --- a/test/xitdb/database_test.clj +++ b/test/xitdb/database_test.clj @@ -1,6 +1,8 @@ (ns xitdb.database-test (:require [clojure.test :refer :all] + [xitdb.db :as xdb] + [xitdb.common :as common] [xitdb.test-utils :as tu :refer [with-db]])) (deftest DatabaseTest @@ -441,7 +443,107 @@ (is (= '(2 3 4 5) @db)) (is (tu/db-equal-to-atom? db)))) +;; ============================================================================= +;; ISlot implementation tests for read-only types +;; ============================================================================= +(deftest islot-read-only-map-test + (testing "XITDBHashMap implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:foo :bar}) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) - +(deftest islot-read-only-vector-test + (testing "XITDBArrayList implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1 2 3]) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +(deftest islot-read-only-set-test + (testing "XITDBHashSet implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +(deftest islot-read-only-list-test + (testing "XITDBLinkedArrayList implements ISlot" + (with-open [db (xdb/xit-db :memory)] + (reset! db '(1 2 3)) + (let [db-val @db] + (is (satisfies? common/ISlot db-val)) + (is (some? (common/-slot db-val))))))) + +;; ============================================================================= +;; Cross-database reset! tests +;; ============================================================================= + +(deftest reset-with-islot-value-test + (testing "reset! throws when using ISlot values from a different database" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + ;; Set up first database with some data + (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] + :config {:theme "dark"}}) + + ;; Get the value from db1 (which implements ISlot) + (let [val-from-db1 @db1] + ;; Attempting to reset db2 with an ISlot from db1 should throw + (is (thrown? IllegalArgumentException (reset! db2 val-from-db1)))))) + + (testing "reset! works when materializing the value first" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] + :config {:theme "dark"}}) + + ;; Materialize the value before passing to reset! + (reset! db2 (tu/materialize @db1)) + + ;; db2 should now have the same data + (is (= (tu/materialize @db1) (tu/materialize @db2)))))) + +(deftest reset-with-nested-islot-value-test + (testing "reset! throws when using nested ISlot values from a different database" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) + + ;; Get a nested value that implements ISlot + (let [nested-val (get @db1 :data)] + ;; Attempting to reset db2 with an ISlot from db1 should throw + (is (thrown? IllegalArgumentException (reset! db2 nested-val)))))) + + (testing "reset! works with nested values when materializing first" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) + + ;; Materialize the nested value before passing to reset! + (let [nested-val (tu/materialize (get @db1 :data))] + (reset! db2 nested-val) + (is (= [[1 2 3] [4 5 6] [7 8 9]] (tu/materialize @db2))))))) + +(deftest reset-preserves-data-integrity-test + (testing "reset! with materialized values from another database preserves data integrity" + (with-open [db1 (xdb/xit-db :memory) + db2 (xdb/xit-db :memory)] + ;; Create large nested data + (reset! db1 {:data (vec (range 100)) + :nested {:more (vec (range 50))}}) + + ;; Materialize the value before passing to reset! (required for cross-database) + (reset! db2 (tu/materialize @db1)) + + ;; Verify data integrity + (is (= (tu/materialize @db1) (tu/materialize @db2))) + + ;; Verify history works + (swap! db2 assoc :added "new") + (is (= (tu/materialize @db1) (tu/materialize (xdb/deref-at db2 0))))))) diff --git a/test/xitdb/history_test.clj b/test/xitdb/history_test.clj new file mode 100644 index 0000000..7f2149e --- /dev/null +++ b/test/xitdb/history_test.clj @@ -0,0 +1,81 @@ +(ns xitdb.history-test + "Tests for database history and versioning features: + - deref-at: access historical versions + - history-index: get current transaction count" + (:require + [clojure.test :refer :all] + [xitdb.db :as xdb] + [xitdb.test-utils :as tu])) + +(deftest deref-at-basic-test + (testing "deref-at returns the version of data at a specific index" + (with-open [db (xdb/xit-db :memory)] + ;; Create a sequence of transactions + (reset! db {:version 1}) + (swap! db assoc :version 2) + (swap! db assoc :version 3) + (swap! db assoc :version 4) + + ;; Current state should be version 4 + (is (= {:version 4} (tu/materialize @db))) + + ;; Access historical versions (0-indexed) + (is (= {:version 1} (tu/materialize (xdb/deref-at db 0)))) + (is (= {:version 2} (tu/materialize (xdb/deref-at db 1)))) + (is (= {:version 3} (tu/materialize (xdb/deref-at db 2)))) + (is (= {:version 4} (tu/materialize (xdb/deref-at db 3)))) + + ;; Using -1 should return the latest version + (is (= {:version 4} (tu/materialize (xdb/deref-at db -1))))))) + +(deftest deref-at-complex-data-test + (testing "deref-at works with complex nested data" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:users []}) + (swap! db update :users conj {:name "Alice"}) + (swap! db update :users conj {:name "Bob"}) + (swap! db assoc :metadata {:count 2}) + + ;; Check historical versions + (is (= {:users []} (tu/materialize (xdb/deref-at db 0)))) + (is (= {:users [{:name "Alice"}]} (tu/materialize (xdb/deref-at db 1)))) + (is (= {:users [{:name "Alice"} {:name "Bob"}]} + (tu/materialize (xdb/deref-at db 2)))) + (is (= {:users [{:name "Alice"} {:name "Bob"}] :metadata {:count 2}} + (tu/materialize (xdb/deref-at db 3))))))) + +(deftest deref-at-with-vectors-test + (testing "deref-at works with vector data" + (with-open [db (xdb/xit-db :memory)] + (reset! db [1]) + (swap! db conj 2) + (swap! db conj 3) + + (is (= [1] (tu/materialize (xdb/deref-at db 0)))) + (is (= [1 2] (tu/materialize (xdb/deref-at db 1)))) + (is (= [1 2 3] (tu/materialize (xdb/deref-at db 2))))))) + +(deftest history-index-test + (testing "history-index returns the current transaction count" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1}) + (is (= 1 (xdb/history-index db))) + + (swap! db assoc :b 2) + (is (= 2 (xdb/history-index db))) + + (swap! db assoc :c 3) + (is (= 3 (xdb/history-index db)))))) + +(deftest deref-at-with-nil-values-test + (testing "deref-at works correctly with nil values in history" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:value nil}) + (swap! db assoc :value 1) + (swap! db assoc :value nil) + (swap! db assoc :value 2) + + (is (nil? (get (xdb/deref-at db 0) :value))) + (is (= 1 (get (xdb/deref-at db 1) :value))) + (is (nil? (get (xdb/deref-at db 2) :value))) + (is (= 2 (get (xdb/deref-at db 3) :value)))))) diff --git a/test/xitdb/java_interop_test.clj b/test/xitdb/java_interop_test.clj new file mode 100644 index 0000000..30fd8bd --- /dev/null +++ b/test/xitdb/java_interop_test.clj @@ -0,0 +1,60 @@ +(ns xitdb.java-interop-test + "Tests for Java interoperability: + - java.util.ArrayList storage + - java.util.LinkedList storage + - Nested Java List handling + - List.subList support" + (:require + [clojure.test :refer :all] + [xitdb.test-utils :as tu :refer [with-db]])) + +(deftest java-arraylist-storage-test + (testing "java.util.ArrayList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.ArrayList. [1 2 3 4 5])] + (reset! db {:items java-list}) + ;; Should be retrievable as a vector-like structure + (is (= [1 2 3 4 5] (vec (get @db :items)))))))) + +(deftest java-linkedlist-storage-test + (testing "java.util.LinkedList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.LinkedList. [1 2 3])] + (reset! db java-list) + (is (= [1 2 3] @db)))))) + +(deftest nested-java-list-storage-test + (testing "nested java Lists are stored correctly" + (with-db [db (tu/test-db)] + (let [outer (java.util.ArrayList.) + inner1 (java.util.ArrayList. [1 2]) + inner2 (java.util.ArrayList. [3 4])] + (.add outer inner1) + (.add outer inner2) + (reset! db outer) + (is (= [[1 2] [3 4]] @db)))))) + +(deftest sublist-storage-test + (testing "List.subList is stored as XITDB ArrayList" + (with-db [db (tu/test-db)] + (let [java-list (java.util.ArrayList. [1 2 3 4 5]) + sub (.subList java-list 1 4)] + (reset! db sub) + (is (= [2 3 4] @db)))))) + +(deftest java-list-in-map-test + (testing "java.util.List as map values" + (with-db [db (tu/test-db)] + (let [list1 (java.util.ArrayList. ["a" "b" "c"]) + list2 (java.util.LinkedList. [1 2 3])] + (reset! db {:strings list1 :numbers list2}) + (is (= ["a" "b" "c"] (vec (get @db :strings)))) + (is (= [1 2 3] (vec (get @db :numbers)))))))) + +(deftest empty-java-list-test + (testing "Empty java.util.List is stored correctly" + (with-db [db (tu/test-db)] + (let [empty-list (java.util.ArrayList.)] + (reset! db empty-list) + (is (= [] @db)) + (is (empty? @db)))))) diff --git a/test/xitdb/map_test.clj b/test/xitdb/map_test.clj index c7276e5..a0e54c2 100644 --- a/test/xitdb/map_test.clj +++ b/test/xitdb/map_test.clj @@ -33,3 +33,49 @@ (reset! db #{}) (is (= 0 (count (keys @db)))) (is (= nil (keys @db))))) + +;; ============================================================================= +;; Read-only map operations returning Clojure types +;; ============================================================================= + +(deftest read-only-map-assoc-test + (testing "assoc on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b 2}) + (let [db-val @db + result (assoc db-val :c 3)] + ;; Result should be a regular Clojure map + (is (map? result)) + (is (not (instance? xitdb.hash_map.XITDBHashMap result))) + (is (= {:a 1 :b 2 :c 3} result)))))) + +(deftest read-only-map-dissoc-test + (testing "dissoc on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b 2 :c 3}) + (let [db-val @db + result (dissoc db-val :b)] + ;; Result should be a regular Clojure map + (is (map? result)) + (is (not (instance? xitdb.hash_map.XITDBHashMap result))) + (is (= {:a 1 :c 3} result)))))) + +(deftest read-only-map-conj-test + (testing "conj on XITDBHashMap returns a regular map" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1}) + (let [db-val @db + result (conj db-val [:b 2])] + (is (map? result)) + (is (= {:a 1 :b 2} result)))))) + +(deftest materialize-shallow-for-write-operations-test + (testing "write operations use materialize-shallow correctly" + (with-open [db (xdb/xit-db :memory)] + (reset! db {:a 1 :b {:nested "value"} :c 3}) + + (let [result (assoc @db :d 4)] + ;; The result should be a regular map + (is (map? result)) + ;; And should contain all keys + (is (= #{:a :b :c :d} (set (keys result)))))))) diff --git a/test/xitdb/set_test.clj b/test/xitdb/set_test.clj index 37327fa..1579aaf 100644 --- a/test/xitdb/set_test.clj +++ b/test/xitdb/set_test.clj @@ -91,3 +91,26 @@ (with-db [db (tu/test-db)] (reset! db #{:one 1 []}) (is (= #{:one 1 []} @db)))) + +;; ============================================================================= +;; Read-only set operations returning Clojure types +;; ============================================================================= + +(deftest read-only-set-disj-test + (testing "disj on XITDBHashSet returns a regular set" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b :c}) + (let [db-val @db + result (disj db-val :b)] + ;; Result should be a regular Clojure set + (is (set? result)) + (is (not (instance? xitdb.hash_set.XITDBHashSet result))) + (is (= #{:a :c} result)))))) + +(deftest read-only-set-conj-test + (testing "conj on XITDBHashSet returns a result with element added" + (with-open [db (xdb/xit-db :memory)] + (reset! db #{:a :b}) + (let [db-val @db + result (cons :c db-val)] + (is (= :c (first result))))))) From 38a6d012bc23e59d66e8b4f65101a74c9e612a9f Mon Sep 17 00:00:00 2001 From: radar roark <122068506+radarroark@users.noreply.github.com> Date: Sun, 11 Jan 2026 07:58:35 -0500 Subject: [PATCH 18/18] remove different database check --- src/xitdb/db.clj | 7 +------ test/xitdb/database_test.clj | 35 ----------------------------------- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/src/xitdb/db.clj b/src/xitdb/db.clj index 77933a5..ecc7152 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -48,12 +48,7 @@ (defn- write-value! [^WriteCursor cursor new-value] (if (satisfies? common/ISlot new-value) - (let [db (.db cursor) - coll (common/-unwrap new-value) - coll-db (-> coll .cursor .db)] - (when (not= db coll-db) - (throw (IllegalArgumentException. "Cannot write value from a different database. You must materialize it first."))) - (.write cursor (common/-slot new-value))) + (.write cursor (common/-slot new-value)) (.write cursor (conversion/v->slot! cursor new-value)))) (defn xitdb-reset! diff --git a/test/xitdb/database_test.clj b/test/xitdb/database_test.clj index 9423c20..ac30deb 100644 --- a/test/xitdb/database_test.clj +++ b/test/xitdb/database_test.clj @@ -483,42 +483,7 @@ ;; Cross-database reset! tests ;; ============================================================================= -(deftest reset-with-islot-value-test - (testing "reset! throws when using ISlot values from a different database" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - ;; Set up first database with some data - (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] - :config {:theme "dark"}}) - - ;; Get the value from db1 (which implements ISlot) - (let [val-from-db1 @db1] - ;; Attempting to reset db2 with an ISlot from db1 should throw - (is (thrown? IllegalArgumentException (reset! db2 val-from-db1)))))) - - (testing "reset! works when materializing the value first" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - (reset! db1 {:users [{:name "Alice"} {:name "Bob"}] - :config {:theme "dark"}}) - - ;; Materialize the value before passing to reset! - (reset! db2 (tu/materialize @db1)) - - ;; db2 should now have the same data - (is (= (tu/materialize @db1) (tu/materialize @db2)))))) - (deftest reset-with-nested-islot-value-test - (testing "reset! throws when using nested ISlot values from a different database" - (with-open [db1 (xdb/xit-db :memory) - db2 (xdb/xit-db :memory)] - (reset! db1 {:data [[1 2 3] [4 5 6] [7 8 9]]}) - - ;; Get a nested value that implements ISlot - (let [nested-val (get @db1 :data)] - ;; Attempting to reset db2 with an ISlot from db1 should throw - (is (thrown? IllegalArgumentException (reset! db2 nested-val)))))) - (testing "reset! works with nested values when materializing first" (with-open [db1 (xdb/xit-db :memory) db2 (xdb/xit-db :memory)]