diff --git a/README.md b/README.md index eab8251..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 @@ -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`. 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 diff --git a/src/xitdb/array_list.clj b/src/xitdb/array_list.clj index 82b18d6..5204882 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-shallow this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBArrayList is read-only"))) + (empty [this] + []) (equiv [this other] (and (sequential? other) @@ -31,9 +31,20 @@ 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] - (throw (UnsupportedOperationException. "XITDBArrayList is read-only"))) + (assoc (common/-materialize-shallow this) i val)) (length [this] (.count ral)) @@ -106,6 +117,10 @@ (aset result len nil)) result)) + common/ISlot + (-slot [this] + (-> ral .cursor .slot)) + common/IUnwrap (-unwrap [this] ral) @@ -124,6 +139,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/db.clj b/src/xitdb/db.clj index 4a1e37f..ecc7152 100644 --- a/src/xitdb/db.clj +++ b/src/xitdb/db.clj @@ -46,12 +46,17 @@ nil))) (.count history)) +(defn- write-value! [^WriteCursor cursor new-value] + (if (satisfies? common/ISlot new-value) + (.write cursor (common/-slot new-value)) + (.write cursor (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] - (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. @@ -79,7 +84,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`. @@ -122,6 +127,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 +143,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 diff --git a/src/xitdb/hash_map.clj b/src/xitdb/hash_map.clj index 8be3add..e564285 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-shallow this) k v)) clojure.lang.IPersistentMap - (without [_ _] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (without [this k] + (dissoc (common/-materialize-shallow this) k)) (count [this] (operations/map-item-count rhm)) clojure.lang.IPersistentCollection - (cons [_ _] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (cons [this o] + (. clojure.lang.RT (conj (common/-materialize-shallow this) o))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBHashMap is read-only"))) + (empty [this] + {}) (equiv [this other] (and (instance? clojure.lang.IPersistentMap other) @@ -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) @@ -99,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 ff1110d..ded6dd1 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-shallow 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-shallow this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBHashSet is read-only"))) + (empty [this] + #{}) (equiv [this other] (and (instance? clojure.lang.IPersistentSet other) @@ -68,6 +68,10 @@ (remove [_] (throw (UnsupportedOperationException. "XITDBHashSet iterator is read-only")))))) + common/ISlot + (-slot [this] + (-> rhs .cursor .slot)) + common/IUnwrap (-unwrap [_] rhs) @@ -85,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 c9b16a4..c3120a4 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-shallow this))) - (empty [_] - (throw (UnsupportedOperationException. "XITDBLinkedArrayList is read-only"))) + (empty [this] + '()) (equiv [this other] (and (sequential? other) @@ -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)))) @@ -100,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 b450faa..b53f37f 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" @@ -104,8 +103,8 @@ [v] (cond - (validation/lazy-seq? v) - (throw (IllegalArgumentException. "Lazy sequences can be infinite and not allowed!")) + (or (nil? v) (instance? Slot v)) + v (string? v) (database-bytes v) @@ -122,9 +121,6 @@ (double? v) (Database$Float. v) - (nil? v) - (database-bytes "" (fmt-tag-value :nil)) - (instance? java.time.Instant v) (database-bytes (str v) (fmt-tag-value :inst)) @@ -147,6 +143,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) @@ -190,15 +189,17 @@ (.write cursor nil) (.slot (list->LinkedArrayListCursor! cursor v))) - (validation/vector-or-chunked? v) + (set? v) (do (.write cursor nil) - (.slot (coll->ArrayListCursor! cursor v))) + (.slot (set->WriteCursor! cursor v))) - (set? 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 (set->WriteCursor! cursor v))) + (.slot (coll->ArrayListCursor! cursor v))) :else (primitive-for v))) @@ -222,7 +223,12 @@ (let [v-cursor (.appendCursor write-array)] (list->LinkedArrayListCursor! v-cursor v)) - (validation/vector-or-chunked? v) + (set? v) + (let [v-cursor (.appendCursor write-array)] + (set->WriteCursor! v-cursor v)) + + (or (validation/vector-or-chunked? v) + (instance? java.util.List v)) (let [v-cursor (.appendCursor write-array)] (coll->ArrayListCursor! v-cursor v)) @@ -250,6 +256,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)) @@ -311,9 +321,6 @@ (java.util.Date/from (java.time.Instant/parse str)) - (= fmt-tag (fmt-tag-value :nil)) - nil - :else str))) 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..ac30deb 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,72 @@ (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-nested-islot-value-test + (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)))))))