diff --git a/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs b/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs index 0670ac5aad..dc4c140b74 100644 --- a/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs +++ b/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs @@ -1,5 +1,6 @@ (ns logseq.db-sync.worker.handler.sync - (:require [clojure.string :as string] + (:require [clojure.set :as set] + [clojure.string :as string] [datascript.core :as d] [lambdaisland.glogi :as log] [logseq.db :as ldb] @@ -296,10 +297,32 @@ (defn- sanitize-tx [db outliner-op tx-data] - (if (= outliner-op :fix) - (remove (fn [[_ id]] - (nil? (d/entity db id))) tx-data) - tx-data)) + (let [retract-op? (fn [item] + (and (vector? item) + (= 2 (count item)) + (contains? #{:db/retractEntity :db.fn/retractEntity} (first item)))) + ->eid (fn [entity-ref] + (some-> (d/entity db entity-ref) :db/id)) + retract-eids (->> tx-data + (keep (fn [item] + (when (retract-op? item) + (->eid (second item))))) + set) + descendant-retract-eids (->> retract-eids + (mapcat (fn [eid] + (let [entity (d/entity db eid)] + (when (:block/uuid entity) + (ldb/get-block-full-children-ids db eid))))) + (remove nil?) + set) + missing-retract-eids (sort (set/difference descendant-retract-eids retract-eids)) + tx-data' (cond-> (vec tx-data) + (seq missing-retract-eids) + (into (map (fn [eid] [:db/retractEntity eid]) missing-retract-eids)))] + (if (= outliner-op :fix) + (remove (fn [[_ id]] + (nil? (d/entity db id))) tx-data') + tx-data'))) (defn- apply-tx-entry! [conn {:keys [tx outliner-op]}] diff --git a/deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs b/deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs index 3d005ac3bd..8fb59d8691 100644 --- a/deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs +++ b/deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs @@ -234,6 +234,83 @@ (is (nil? (:data response))) (is (= 2 @apply-calls))))) +(defn- seed-page-with-block-tree! + [conn] + (let [page-uuid (random-uuid) + parent-uuid (random-uuid) + child-a-uuid (random-uuid) + child-b-uuid (random-uuid) + now 1775549093572] + (d/transact! conn [{:block/uuid page-uuid + :block/name "sync-repro-page" + :block/title "sync-repro-page" + :block/created-at now + :block/updated-at now} + {:block/uuid parent-uuid + :block/title "parent" + :block/parent [:block/uuid page-uuid] + :block/page [:block/uuid page-uuid] + :block/order "a0" + :block/created-at now + :block/updated-at now} + {:block/uuid child-a-uuid + :block/title "child-a" + :block/parent [:block/uuid parent-uuid] + :block/page [:block/uuid page-uuid] + :block/order "a1" + :block/created-at now + :block/updated-at now} + {:block/uuid child-b-uuid + :block/title "child-b" + :block/parent [:block/uuid parent-uuid] + :block/page [:block/uuid page-uuid] + :block/order "a2" + :block/created-at now + :block/updated-at now}]) + {:page-uuid page-uuid + :parent-uuid parent-uuid + :child-a-uuid child-a-uuid + :child-b-uuid child-b-uuid})) + +(deftest tx-batch-stale-retract-block-includes-current-descendants-test + (testing "stale block retract should still delete descendants attached in current db" + (let [sql (test-sql/make-sql) + conn (storage/open-conn sql) + self #js {:sql sql + :conn conn + :schema-ready true} + {:keys [parent-uuid child-a-uuid child-b-uuid]} (seed-page-with-block-tree! conn) + t-before (storage/get-t sql) + stale-delete-entry {:tx (protocol/tx->transit [[:db/retractEntity [:block/uuid parent-uuid]]]) + :outliner-op :delete-blocks} + response (with-redefs [ws/broadcast! (fn [& _] nil)] + (sync-handler/handle-tx-batch! self nil [stale-delete-entry] t-before))] + (is (= "tx/batch/ok" (:type response))) + (is (number? (:t response))) + (is (nil? (d/entity @conn [:block/uuid parent-uuid]))) + (is (nil? (d/entity @conn [:block/uuid child-a-uuid]))) + (is (nil? (d/entity @conn [:block/uuid child-b-uuid])))))) + +(deftest tx-batch-stale-retract-page-includes-current-page-tree-test + (testing "stale page retract should still delete page tree to avoid orphan blocks" + (let [sql (test-sql/make-sql) + conn (storage/open-conn sql) + self #js {:sql sql + :conn conn + :schema-ready true} + {:keys [page-uuid parent-uuid child-a-uuid child-b-uuid]} (seed-page-with-block-tree! conn) + t-before (storage/get-t sql) + stale-delete-entry {:tx (protocol/tx->transit [[:db/retractEntity [:block/uuid page-uuid]]]) + :outliner-op :delete-page} + response (with-redefs [ws/broadcast! (fn [& _] nil)] + (sync-handler/handle-tx-batch! self nil [stale-delete-entry] t-before))] + (is (= "tx/batch/ok" (:type response))) + (is (number? (:t response))) + (is (nil? (d/entity @conn [:block/uuid page-uuid]))) + (is (nil? (d/entity @conn [:block/uuid parent-uuid]))) + (is (nil? (d/entity @conn [:block/uuid child-a-uuid]))) + (is (nil? (d/entity @conn [:block/uuid child-b-uuid])))))) + (deftest sync-pull-is-blocked-when-graph-is-not-ready-for-use-test (async done (let [self #js {:env #js {"DB" :db}