Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ When something is added, it's typically marked *Experimental*. When the API cont

### Notable fixes

- **Transactons: history purge type error on Node** – While porting database purge tests to CLJS, it was discovered that :db.history.purge/before relied on a JVM-specific Date method, causing a type error on Node.js. The implementation was refactored to use a shared cross-platform helper ([#870])
- **Query planner: variable-attribute cross-source Cartesian product** — a multi-source query with a variable-attribute pattern `[$b ?e ?a ?v]` (the attribute is a logic var — e.g. a join attribute retrieved from the data itself) whose value was produced by a function and reached through two or more linked entity-groups on the driving source was mis-ordered: the value-producing op ran *after* the variable-attribute scan, so the scan ran unconstrained and the cross-source join collapsed onto the non-selective attribute variable, yielding a Cartesian product. Such scans are now recognized as correlated joins and ordered after their attribute/value producers, so the join is selective. The legacy (relational) engine crashed outright on the same shape (`No matching clause: N` in `resolve-pattern-lookup-ref-at-index` — a relation-tuple position index exceeded the datom's five slots on a wide multi-source relation); such positions now resolve to the value unchanged. As a bonus, a variable-attribute scan whose attribute and value are both bound upstream now AVET point-seeks per `(attr, value)` pair instead of full-scanning — the dynamic-attribute join over a large target database drops from O(all datoms) to O(pairs · log n) (≈8000× on a 100k-datom benchmark). ([#865])
- **Query planner: as-of card-one merge skipped older visible values** — a date/tx `as-of` query whose timepoint fell between an attribute's initial and updated `:db/txInstant` could drop the older-but-still-visible card-one value (e.g. an entity's original `:age`): the fused merge found the current value, rejected it as too new, and stopped instead of looking back through temporal history. The merge now falls back to the assembled temporal slice. Surfaced by the native babashka-pod test suite; reproducible on JVM and JS. ([#863])
- **Query planner: `get-else` over `d/history` enumerated every version** — `[(get-else $ ?e :attr default) ?v]` is single-valued (the legacy engine returns one value or the default per entity), but on a `HistoricalDB` the planner forced the merge card-many and emitted one row per historical version (and, for card-many attributes, one row per value). `get-else` merges are now single-valued regardless of temporal type or attribute cardinality. ([#863])
Expand Down
4 changes: 2 additions & 2 deletions src/datahike/db/transaction.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,9 @@
(log/raise "Can’t modify tuple attrs directly: " op-vec
{:error :transact/syntax, :tx-data op-vec}))))

(defn- filter-before [datoms ^Date before-date db]
(defn- filter-before [datoms before-date db]
(let [before-pred (fn [^Datom d]
(.before ^Date (.-v d) before-date))
(bp/date-before? (.-v d) before-date))
filtered-tx-ids (dbu/filter-txInstant datoms before-pred db)]
(filter
(fn [^Datom d]
Expand Down
2 changes: 2 additions & 0 deletions test/datahike/test/nodejs_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[datahike.test.cljs-tiered-storage-test]
[datahike.test.cljs-pattern-scan-test]
[datahike.test.optimistic-test]
[datahike.test.purge-test]
[datahike.test.reference-test]
[datahike.test.valid-time-test]
;; Portable query suites — exercise the query-engine paths that were
Expand Down Expand Up @@ -376,6 +377,7 @@
'datahike.test.cljs-tiered-storage-test
'datahike.test.cljs-pattern-scan-test
'datahike.test.optimistic-test
'datahike.test.purge-test
'datahike.test.reference-test
'datahike.test.valid-time-test
'datahike.test.time-variance-test
Expand Down
75 changes: 42 additions & 33 deletions test/datahike/test/purge_test.cljc
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
(ns datahike.test.purge-test
(:require
#?(:cljs [cljs.test :as t :refer-macros [is are deftest testing]]
:clj [clojure.test :as t :refer [is are deftest testing]])
#?(:cljs [cljs.test :as t :refer-macros [is are testing]]
:clj [clojure.test :as t :refer [is are testing]])
[clojure.core.async :refer [<!]]
[datahike.api :as d]
[datahike.test.async #?(:clj :refer :cljs :refer-macros) [deftest-async]]
[datahike.test.utils :as tu]))

#?(:cljs (def Throwable js/Error))
Expand Down Expand Up @@ -36,81 +38,87 @@
(into #{}
(d/q '[:find [(pull ?e [:name :age]) ...] :where [?e :name _]] db)))

(deftest test-purge
(let [conn (tu/setup-db (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000001"))]
(deftest-async test-purge
(let [cfg (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000001")
conn (<! (tu/setup-db-async cfg))]
(testing "retract datom, data is removed from current db and found in history"
(let [name "Alice"]
(d/transact conn [[:db/retract [:name name] :age 25]])
(<! (d/transact! conn [[:db/retract [:name name] :age 25]]))
(are [x y] (= x y)
true (nil? (find-age @conn name))
25 (find-age (d/history @conn) name))))
(testing "purge datom from current index and from history"
(let [name "Bob"]
(d/transact conn [[:db/purge [:name name] :age 35]])
(<! (d/transact! conn [[:db/purge [:name name] :age 35]]))
(are [x y] (= x y)
true (nil? (find-age @conn name))
true (nil? (find-age (d/history @conn) name)))))
(testing "purge retracted datom"
(let [name "Alice"]
(d/transact conn [[:db/purge [:name name] :age 25]])
(<! (d/transact! conn [[:db/purge [:name name] :age 25]]))
(are [x y] (= x y)
nil (find-age @conn name)
nil (find-age (d/history @conn) name))))
(d/release conn)))

(deftest test-purge-attribute
(let [conn (tu/setup-db (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000002"))]
(deftest-async test-purge-attribute
(let [cfg (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000002")
conn (<! (tu/setup-db-async cfg))]
(testing "purge attribute from current index"
(let [name "Alice"]
(d/transact conn [[:db.purge/attribute [:name name] :age]])
(<! (d/transact! conn [[:db.purge/attribute [:name name] :age]]))
(are [x y] (= x y)
true (nil? (find-age @conn name))
true (nil? (find-age (d/history @conn) name))
#{["Alice"] ["Bob"]} (d/q '[:find ?n :where [_ :name ?n]] @conn))))
(testing "retract attribute from current index and purge from history"
(let [name "Bob"]
(testing "retracting from current index"
(d/transact conn [[:db.fn/retractAttribute [:name name] :age]])
(<! (d/transact! conn [[:db.fn/retractAttribute [:name name] :age]]))
(are [x y] (= x y)
true (nil? (find-age @conn name))
35 (find-age (d/history @conn) name)))
(testing "purging from history"
(d/transact conn [[:db.purge/entity [:name name] :age]])
(<! (d/transact! conn [[:db.purge/entity [:name name] :age]]))
(are [x y] (= x y)
true (nil? (find-age @conn name))
true (nil? (find-age (d/history @conn) name))))))
(d/release conn)))

(deftest test-purge-entity
(let [conn (tu/setup-db (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000003"))]
(deftest-async test-purge-entity
(let [cfg (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000003")
conn (<! (tu/setup-db-async cfg))]
(testing "purge entity from current index"
(is (= #{{:name "Alice" :age 25} {:name "Bob" :age 35}} (find-entities @conn)))
(d/transact conn [[:db.purge/entity [:name "Alice"]]])
(<! (d/transact! conn [[:db.purge/entity [:name "Alice"]]]))
(is (= #{{:name "Bob" :age 35}} (find-entities @conn)))
(is (= #{{:name "Bob" :age 35}} (find-entities (d/history @conn)))))
(testing "retract entity from current index and purge from history"
(let [name "Bob"]
(testing "retracting from current index"
(d/transact conn [[:db/retractEntity [:name name]]])
(<! (d/transact! conn [[:db/retractEntity [:name name]]]))
(is (= #{} (find-entities @conn)))
(is (= #{{:name "Bob" :age 35}} (find-entities (d/history @conn)))))
(testing "purging from history"
(d/transact conn [[:db.purge/entity [:name name]]])
(<! (d/transact! conn [[:db.purge/entity [:name name]]]))
(is (= #{} (find-entities @conn)))
(is (= #{} (find-entities (d/history @conn)))))))

(testing "purge something that is not present in the database"
(is (thrown-with-msg? Throwable
#"Can't find entity with ID \[:name \"Alice\"\] to be purged"
(d/transact conn [[:db.purge/entity [:name "Alice"]]]))))
(d/release conn)))
(is (tu/error-msg-satisfies?
#(re-find #"Can't find entity with ID \[:name \"Alice\"\] to be purged" %)
(<! (d/transact! conn [[:db.purge/entity [:name "Alice"]]])))))
(when conn (d/release conn))))

(deftest test-purge-non-temporal-database
(let [conn (tu/setup-db (-> (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000004")
(assoc :keep-history? false)))]
(deftest-async test-purge-non-temporal-database
(let [cfg (-> (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000004")
(assoc :keep-history? false))
conn (<! (tu/setup-db-async cfg))]
(testing "purge data in non temporal database"
(is (thrown-with-msg? Throwable #"Purge entity is only available in temporal databases\."
(d/transact conn [[:db.purge/entity [:name "Alice"]]]))))
(d/release conn)))
(is (tu/error-msg-satisfies?
#(re-find #"Purge entity is only available in temporal databases\." %)
(<! (d/transact! conn [[:db.purge/entity [:name "Alice"]]])))))
(when conn (d/release conn))))

(defn find-ages [db name]
(d/q '[:find ?a ?op
Expand All @@ -121,24 +129,25 @@
db
name))

(deftest test-history-purge-before
(let [conn (tu/setup-db (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000005"))
(deftest-async test-history-purge-before
(let [cfg (assoc-in cfg-template [:store :id] #uuid "09000000-0000-0000-0000-000000000005")
conn (<! (tu/setup-db-async cfg))
name "Alice"]
(testing "remove all historical data before date"
(is (= #{[25 true]}
(find-ages @conn name)))
(let [upsert-date (java.util.Date.)]
(d/transact conn [{:db/id [:name name] :age 30}])
(let [upsert-date #?(:clj (java.util.Date.) :cljs (js/Date.))]
(<! (d/transact! conn [{:db/id [:name name] :age 30}]))
(is (= #{[30 true]}
(find-ages @conn name)))
(is (= #{[25 true] [25 false] [30 true]}
(find-ages (d/history @conn) name)))
(d/transact conn [[:db.history.purge/before upsert-date]])
(<! (d/transact! conn [[:db.history.purge/before upsert-date]]))
(is (= #{[30 true]}
(find-ages @conn name)))
(is (= #{[25 false] [30 true]}
(find-ages (d/history @conn) name)))
(d/transact conn [[:db.history.purge/before (java.util.Date.)]])
(<! (d/transact! conn [[:db.history.purge/before #?(:clj (java.util.Date.) :cljs (js/Date.))]]))
(is (= #{[30 true]}
(find-ages (d/history @conn) name)))))
(d/release conn)))
6 changes: 6 additions & 0 deletions test/datahike/test/utils.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,9 @@

(defn conn-id []
(str (get-time)))

(defn error-msg-satisfies? [pred err]
(and err
(pred
#?(:clj (ex-message err)
:cljs (or (ex-message err) (.-message err))))))