Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
273 lines
12 KiB
Clojure
273 lines
12 KiB
Clojure
(ns logseq.db-sync.storage-test
|
|
(:require ["better-sqlite3" :as sqlite3]
|
|
[clojure.string :as string]
|
|
[cljs.test :refer [deftest is testing]]
|
|
[datascript.core :as d]
|
|
[logseq.db :as ldb]
|
|
[logseq.db-sync.common :as common]
|
|
[logseq.db-sync.storage :as storage]
|
|
[logseq.db.common.normalize :as db-normalize]
|
|
[logseq.db-sync.test-sql :as test-sql]))
|
|
|
|
(def sqlite (if (find-ns 'nbb.core) (aget sqlite3 "default") sqlite3))
|
|
|
|
(defn- select-sql?
|
|
[sql]
|
|
(string/starts-with? (-> sql string/trim string/lower-case) "select"))
|
|
|
|
(defn- run-sql
|
|
[^js stmt args]
|
|
(.apply (.-run stmt) stmt (to-array args)))
|
|
|
|
(defn- all-sql
|
|
[^js stmt args]
|
|
(.apply (.-all stmt) stmt (to-array args)))
|
|
|
|
(defn- with-memory-sql
|
|
[f]
|
|
(let [db (new sqlite ":memory:" nil)
|
|
sql #js {:exec (fn [sql-str & args]
|
|
(let [stmt (.prepare db sql-str)]
|
|
(if (select-sql? sql-str)
|
|
(all-sql stmt args)
|
|
(do
|
|
(run-sql stmt args)
|
|
nil))))
|
|
:close (fn []
|
|
(.close db))}]
|
|
(try
|
|
(f sql)
|
|
(finally
|
|
(.close sql)))))
|
|
|
|
(defn- seeded-rng
|
|
[seed0]
|
|
(let [state (atom (bit-or (long seed0) 0))]
|
|
(fn []
|
|
(let [s (swap! state
|
|
(fn [x]
|
|
(let [x (bit-xor x (bit-shift-left x 13))
|
|
x (bit-xor x (bit-shift-right x 17))
|
|
x (bit-xor x (bit-shift-left x 5))]
|
|
(bit-or x 0))))]
|
|
(/ (double (unsigned-bit-shift-right s 0)) 4294967296.0)))))
|
|
|
|
(defn- rand-int*
|
|
[rng n]
|
|
(js/Math.floor (* (rng) n)))
|
|
|
|
(defn- pick-rand
|
|
[rng coll]
|
|
(when (seq coll)
|
|
(nth coll (rand-int* rng (count coll)))))
|
|
|
|
(defn- normal-block-uuids
|
|
[db]
|
|
(->> (d/datoms db :avet :block/uuid)
|
|
(map :e)
|
|
distinct
|
|
(keep (fn [eid]
|
|
(let [ent (d/entity db eid)]
|
|
(when (and (uuid? (:block/uuid ent))
|
|
(not (ldb/built-in? ent))
|
|
(nil? (:block/name ent))
|
|
(some? (:block/page ent)))
|
|
(:block/uuid ent)))))
|
|
vec))
|
|
|
|
(deftest t-meta-test
|
|
(let [sql (test-sql/make-sql)]
|
|
(storage/init-schema! sql)
|
|
(is (= 0 (storage/get-t sql)))
|
|
(storage/set-t! sql 1)
|
|
(is (= 1 (storage/get-t sql)))
|
|
(storage/set-t! sql 2)
|
|
(is (= 2 (storage/get-t sql)))))
|
|
|
|
(deftest tx-log-test
|
|
(let [sql (test-sql/make-sql)]
|
|
(storage/init-schema! sql)
|
|
(storage/append-tx! sql 1 "tx-1" 100 :save-block)
|
|
(storage/append-tx! sql 2 "tx-2" 200 :move-blocks)
|
|
(storage/append-tx! sql 3 "tx-3" 300 nil)
|
|
(let [result (storage/fetch-tx-since sql 1)]
|
|
(is (= [{:t 2 :tx "tx-2" :outliner-op :move-blocks}
|
|
{:t 3 :tx "tx-3" :outliner-op nil}]
|
|
result)))))
|
|
|
|
(deftest stale-checksum-no-op-transact-does-not-throw-test
|
|
(testing "a no-op tx should not throw and should keep incremental checksum state"
|
|
(with-memory-sql
|
|
(fn [sql]
|
|
(let [stale-checksum "f4b78e83776d45fb"]
|
|
(storage/init-schema! sql)
|
|
(storage/set-checksum! sql stale-checksum)
|
|
(let [conn (storage/open-conn sql)
|
|
result (try
|
|
(d/transact! conn
|
|
[]
|
|
{:outliner-op :rebase})
|
|
:ok
|
|
(catch :default e
|
|
e))]
|
|
(is (= :ok result))
|
|
(is (= stale-checksum
|
|
(storage/get-checksum sql)))))))))
|
|
|
|
(deftest initial-checksum-does-not-overwrite-existing-checksum-test
|
|
(testing "snapshot checksum initialization must not replace an existing incremental checksum"
|
|
(let [sql (test-sql/make-sql)
|
|
existing-checksum "aaaaaaaaaaaaaaaa"
|
|
snapshot-checksum "bbbbbbbbbbbbbbbb"]
|
|
(storage/init-schema! sql)
|
|
(storage/set-initial-checksum! sql existing-checksum)
|
|
(let [result (try
|
|
(storage/set-initial-checksum! sql snapshot-checksum)
|
|
:ok
|
|
(catch :default error
|
|
error))]
|
|
(is (not= :ok result))
|
|
(is (= existing-checksum (storage/get-checksum sql)))))))
|
|
|
|
(deftest initial-checksum-rejects-non-empty-tx-log-test
|
|
(testing "snapshot checksum initialization must not run after tx history already advanced"
|
|
(let [sql (test-sql/make-sql)
|
|
snapshot-checksum "bbbbbbbbbbbbbbbb"]
|
|
(storage/init-schema! sql)
|
|
(storage/append-tx! sql 1 "tx-1" 100 :save-block)
|
|
(storage/set-t! sql 1)
|
|
(let [result (try
|
|
(storage/set-initial-checksum! sql snapshot-checksum)
|
|
:ok
|
|
(catch :default error
|
|
error))]
|
|
(is (not= :ok result))
|
|
(is (nil? (storage/get-checksum sql)))))))
|
|
|
|
(deftest stale-checksum-transact-keeps-kvs-and-tx-log-consistent-test
|
|
(testing "stale checksum should not fail transact; kvs and tx_log/t should advance together"
|
|
(with-memory-sql
|
|
(fn [sql]
|
|
(storage/init-schema! sql)
|
|
(let [conn (storage/open-conn sql)
|
|
stale-checksum "ffffffffffffffff"
|
|
page-uuid (random-uuid)]
|
|
;; Use a stale checksum and ensure append path remains consistent.
|
|
(storage/set-checksum! sql stale-checksum)
|
|
(let [result (try
|
|
(d/transact! conn [{:block/uuid page-uuid
|
|
:block/name "repro-kvs-ahead-page"}])
|
|
:ok
|
|
(catch :default e
|
|
e))]
|
|
(is (= :ok result))
|
|
(is (= 1 (storage/get-t sql)))
|
|
(is (= 1 (count (storage/fetch-tx-since sql 0))))
|
|
(is (not= stale-checksum (storage/get-checksum sql)))
|
|
(let [restored-conn (storage/open-conn sql)]
|
|
(is (= page-uuid
|
|
(:block/uuid (d/entity @restored-conn [:block/uuid page-uuid])))))))))))
|
|
|
|
(deftest tx-log-append-failure-does-not-advance-checksum-test
|
|
(testing "server checksum and t should not advance when the tx log append fails"
|
|
(with-memory-sql
|
|
(fn [sql]
|
|
(storage/init-schema! sql)
|
|
(let [conn (storage/open-conn sql)
|
|
page-uuid (random-uuid)
|
|
failed-block-uuid (random-uuid)]
|
|
(d/transact! conn [{:block/uuid page-uuid
|
|
:block/name "tx-log-failure-repro"}])
|
|
(let [checksum-before (storage/get-checksum sql)
|
|
t-before (storage/get-t sql)
|
|
original-sql-exec common/sql-exec
|
|
result (with-redefs [common/sql-exec
|
|
(fn [sql sql-str & args]
|
|
(if (string/includes? sql-str "insert into tx_log")
|
|
(throw (js/Error. "tx log append failed"))
|
|
(apply original-sql-exec sql sql-str args)))]
|
|
(try
|
|
(d/transact! conn [{:block/uuid failed-block-uuid
|
|
:block/title "failed append"
|
|
:block/page [:block/uuid page-uuid]
|
|
:block/parent [:block/uuid page-uuid]}])
|
|
:ok
|
|
(catch :default error
|
|
error)))]
|
|
(is (not= :ok result))
|
|
(is (= t-before (storage/get-t sql)))
|
|
(is (= checksum-before (storage/get-checksum sql)))))))))
|
|
|
|
(deftest normalize-drop-can-hide-kvs-mutation-from-tx-log-test
|
|
(testing "if normalize drops tx payload, tx_log can miss persisted kvs state changes"
|
|
(with-memory-sql
|
|
(fn [sql]
|
|
(storage/init-schema! sql)
|
|
(let [conn (storage/open-conn sql)
|
|
page-uuid (random-uuid)]
|
|
(with-redefs [db-normalize/normalize-tx-data (fn [_db-after _db-before _tx-data]
|
|
[])]
|
|
(d/transact! conn [{:block/uuid page-uuid
|
|
:block/name "normalize-drop-repro"}]))
|
|
(is (= 1 (storage/get-t sql)))
|
|
(let [entries (storage/fetch-tx-since sql 0)]
|
|
(is (= 1 (count entries)))
|
|
(is (= []
|
|
(common/read-transit (:tx (first entries)))))
|
|
(let [restored-conn (storage/open-conn sql)]
|
|
(is (= page-uuid
|
|
(:block/uuid (d/entity @restored-conn [:block/uuid page-uuid])))))))))))
|
|
|
|
(deftest randomized-normal-block-retract-recreate-does-not-throw-checksum-mismatch-test
|
|
(testing "normal block retract/recreate patterns should not naturally trigger server checksum mismatch"
|
|
(with-memory-sql
|
|
(fn [sql]
|
|
(storage/init-schema! sql)
|
|
(let [conn (storage/open-conn sql)
|
|
rng (seeded-rng 424242)
|
|
page-uuid (random-uuid)
|
|
_ (d/transact! conn [{:block/uuid page-uuid
|
|
:block/name "repro-page"
|
|
:block/title "repro-page"}])
|
|
_ (d/transact! conn (mapv (fn [idx]
|
|
{:block/uuid (random-uuid)
|
|
:block/title (str "seed-" idx)
|
|
:block/page [:block/uuid page-uuid]
|
|
:block/parent [:block/uuid page-uuid]
|
|
:block/order (str "a" idx)})
|
|
(range 5)))
|
|
*mismatch (atom nil)]
|
|
(dotimes [step 500]
|
|
(when-not @*mismatch
|
|
(let [db @conn
|
|
blocks (normal-block-uuids db)
|
|
target (pick-rand rng blocks)
|
|
sibling (pick-rand rng (remove #(= % target) blocks))
|
|
add-fresh? (< (rng) 0.3)
|
|
tx-data (cond-> [[:db/retractEntity [:block/uuid target]]
|
|
[:db/add -1 :block/uuid target]
|
|
[:db/add -1 :block/title (str "rr-" step)]
|
|
[:db/add -1 :block/page [:block/uuid page-uuid]]
|
|
[:db/add -1 :block/parent [:block/uuid page-uuid]]
|
|
[:db/add -1 :block/order (str "z" (mod step 7))]]
|
|
sibling
|
|
(conj [:db/add [:block/uuid sibling] :block/title (str "sib-" step)])
|
|
add-fresh?
|
|
(into [[:db/add -2 :block/uuid (random-uuid)]
|
|
[:db/add -2 :block/title (str "fresh-" step)]
|
|
[:db/add -2 :block/page [:block/uuid page-uuid]]
|
|
[:db/add -2 :block/parent [:block/uuid page-uuid]]
|
|
[:db/add -2 :block/order (str "x" (mod step 9))]]))]
|
|
(try
|
|
(d/transact! conn tx-data)
|
|
(catch :default e
|
|
(let [message (or (ex-message e) (some-> e .-message) (str e))]
|
|
(if (string/includes? message "server checksum doesn't match")
|
|
(reset! *mismatch {:step step
|
|
:tx-data tx-data
|
|
:error message})
|
|
;; tx can be invalid due random -2 fresh fields; ignore non-checksum failures
|
|
nil)))))))
|
|
(is (nil? @*mismatch)
|
|
(str "found checksum mismatch repro: " (pr-str @*mismatch))))))))
|