(ns frontend.worker.local-tx-log-test (:require [cljs.test :refer [deftest is testing]] [frontend.worker.local-tx-log :as local-tx-log] [frontend.worker.state :as worker-state] [logseq.db.common.sqlite :as common-sqlite] [logseq.db.sqlite.gc :as sqlite-gc])) (defn- new-memory-db [] (let [Database (js/require "better-sqlite3")] (new Database ":memory:"))) (defn- with-client-ops-db [repo f] (let [db (new-memory-db) prev-client-ops-conns @worker-state/*client-ops-conns] (reset! worker-state/*client-ops-conns {repo db}) (try (f db) (finally (.close db) (reset! worker-state/*client-ops-conns prev-client-ops-conns))))) (defn- sqlite-count [^js db sql & args] (let [^js stmt (.prepare db sql) ^js row (if (seq args) (.apply (.-get stmt) stmt (to-array args)) (.get stmt))] (if row (or (aget row "c") (aget row "count")) 0))) (deftest sqlite-local-tx-roundtrip-test (let [repo "repo-1"] (with-client-ops-db repo (fn [_db] (local-tx-log/update-local-tx repo 9) (local-tx-log/update-local-tx repo 12) (is (= 12 (local-tx-log/get-local-tx repo))))))) (deftest cleanup-finished-history-ops-removes-only-unreferenced-finished-txs-test (let [repo "repo-cleanup" keep-tx-id (random-uuid) remove-tx-id (random-uuid) pending-tx-id (random-uuid)] (with-client-ops-db repo (fn [db] (local-tx-log/update-local-tx repo 99) (local-tx-log/upsert-local-tx-entry! repo {:tx-id keep-tx-id :created-at 1 :pending? false :failed? false :normalized-tx-data [] :reversed-tx-data []}) (local-tx-log/upsert-local-tx-entry! repo {:tx-id remove-tx-id :created-at 2 :pending? false :failed? false :normalized-tx-data [] :reversed-tx-data []}) (local-tx-log/upsert-local-tx-entry! repo {:tx-id pending-tx-id :created-at 3 :pending? true :failed? false :normalized-tx-data [] :reversed-tx-data []}) (is (= 1 (local-tx-log/cleanup-finished-history-ops! repo #{keep-tx-id}))) (is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str keep-tx-id)))) (is (= 0 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str remove-tx-id)))) (is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str pending-tx-id)))) (is (= 99 (local-tx-log/get-local-tx repo))))))) (deftest cleanup-finished-history-ops-no-conn-is-noop-test (let [repo "repo-no-conn" prev-client-ops-conns @worker-state/*client-ops-conns] (reset! worker-state/*client-ops-conns {}) (try (testing "cleanup should be safe when client-ops conn is missing" (is (= 0 (local-tx-log/cleanup-finished-history-ops! repo #{})))) (finally (reset! worker-state/*client-ops-conns prev-client-ops-conns))))) (deftest gc-on-client-ops-db-preserves-local-tx-test "Reproduces the bug where GC on client-ops db could reset local-tx to 0. The client-ops db has a kvs table (created by create-kvs-table!) but it's empty because client-ops doesn't store Datascript data. Running GC on an empty kvs table is a no-op with better-sqlite3 but can crash or misbehave with WASM sqlite (browser), leading to db corruption and local-tx reset." (let [repo "repo-gc-test"] (with-client-ops-db repo (fn [db] ;; client-ops db gets a kvs table during db open, but it's empty (common-sqlite/create-kvs-table! db) (local-tx-log/ensure-sqlite-schema! db) (local-tx-log/update-local-tx repo 42) (is (= 42 (local-tx-log/get-local-tx repo)) "local-tx should be 42 before GC") ;; GC on client-ops db: kvs table is empty because client-ops db ;; does not store Datascript data. gc-kvs-table! expects a valid ;; Datascript schema at kvs addr 0, which doesn't exist here. ;; This causes transit-js to throw "Expected first argument to be ;; a string" because it receives undefined instead of a transit ;; string. This is why gc-sqlite-dbs! must NOT include client-ops db. (testing "gc-kvs-table! crashes on client-ops db with empty kvs table" (is (thrown-with-msg? js/Error #"Expected first argument to be a string" (sqlite-gc/gc-kvs-table! db {:full-gc? false})))) ;; After GC, local-tx should still be intact (is (= 42 (local-tx-log/get-local-tx repo)) "local-tx should still be 42 after GC") ;; VACUUM should also be safe (.exec db "VACUUM") (is (= 42 (local-tx-log/get-local-tx repo)) "local-tx should still be 42 after VACUUM")))))