logseq / logseq/db-test

DB graph: recycled (soft-deleted) pages/blocks still affect query evaluation until permanently deleted

Open
#1,208 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
No language data
Stars
28
Forks
2
PR merge metrics
No merged PRs in 30d

Description

Search first
  • I searched and no similar issues were found
What Happened?

In a DB graph, deleting a page or block moves it to the Recycle page (a soft delete), but it still counts in advanced (Datalog) queries until it is permanently deleted from the Recycle bin.

recycle-page-tx-data / recycle-blocks-tx-data (deps/outliner/src/logseq/outliner/recycle.cljs) only change :block/parent, :block/page, :block/order and add :logseq.property/deleted-at. Every other attribute stays on the entity, including user :node property values that point at other nodes.

Custom queries run as a plain d/q against the full DB (execute-custom-query in src/main/frontend/worker/handler/query.cljs). Recycled rows are removed from the output afterwards (filter-block-query-resultldb/hidden? in render_resource/query.cljs), but recycled entities still take part inside the query: in joins, not, or, aggregates, etc.

So any query that asks "is X referenced by something?" gives wrong results after a delete, and only becomes correct once the item is permanently deleted.

Concrete case: a "leaf nodes" query over a hierarchy built with a :node property (Stream Of):

{:title "All leaf streams"
 :query
 [:find (pull ?Stream [*])
  :where
  [?Stream-tag :block/title "Stream"]
  [?Stream :block/tags ?Stream-tag]
  [?parent-property :block/title "Stream Of"]
  [?parent-property :db/ident ?parent-attribute]
  ;; No stream has this stream as its parent
  (not [?child ?parent-attribute ?Stream])]}

Deleting the only child of a stream should make the parent a leaf. It doesn't: the recycled child still has Stream Of → parent, so the not clause excludes the parent. The recycled child is also hidden from the output, so the parent is simply missing from the results.

Reproduce the Bug

UI:

  1. Create a DB graph, a tag Stream, and a :node property Stream Of.
  2. Create pages parent and child, both tagged #Stream; set child's Stream Of to parent.
  3. Add the advanced query above. The result is child only.
  4. Delete page child.
  5. The query now returns nothing. Expected: parent.
  6. Open the Recycle bin and permanently delete child. The query now returns parent.

Minimal failing test on current master (66c1a434b7), runnable from deps/outliner:

(ns logseq.outliner.recycle-query-repro-test
  (:require [cljs.test :refer [deftest is]]
            [datascript.core :as d]
            [logseq.db :as ldb]
            [logseq.db.test.helper :as db-test]
            [logseq.outliner.recycle :as recycle]))

(def leaf-query
  '[:find [?title ...]
    :where
    [?s :block/tags ?tag]
    [?tag :block/title "stream"]
    [?s :block/title ?title]
    (not [?child :user.property/streamOf ?s])])

(deftest recycled-child-still-blocks-leaf-query
  (let [conn (db-test/create-conn-with-blocks
              {:properties {:streamOf {:logseq.property/type :node}}
               :classes {:stream {}}
               :pages-and-blocks
               [{:page {:block/title "parent" :build/tags [:stream]}}
                {:page {:block/title "child" :build/tags [:stream]
                        :build/properties {:streamOf [:build/page {:block/title "parent"}]}}}]})
        child (ldb/get-page @conn "child")]
    (ldb/transact! conn (recycle/recycle-page-tx-data @conn child {}) {:outliner-op :delete-page})
    (is (ldb/recycled? (ldb/get-page @conn "child")))
    (is (contains? (set (d/q leaf-query @conn)) "parent"))))

Output:

child recycled? true streamOf still set? true
after recycle: (child)
FAIL: expected (contains? leaves "parent"), actual: (not (contains? #{"child"} "parent"))
Expected Behavior

Recycled (soft-deleted) pages and blocks, including descendants of a recycled root, should not affect query evaluation. A query should give the same result as if the item had been permanently deleted.

Additional Context

Workaround for users: add a guard to each clause that could match recycled entities, e.g.

(not [?child ?parent-attribute ?Stream]
     [(missing? $ ?child :logseq.property/deleted-at)])

This only covers recycled roots. Descendants of a recycled block don't carry deleted-at themselves.

Possible fix: run custom/DSL queries against a filtered DB (d/filter) that drops datoms whose entity is recycled, and ref datoms whose value is a recycled entity. The set of recycled ids (roots with deleted-at plus their subtrees) can be computed once per query so the predicate stays a set lookup. The trade-offs are the cost of filtered-DB queries and keeping query watch/invalidation consistent. Happy to open a PR along these lines if this direction works for maintainers.

Are you willing to submit a PR? If you know how to fix the bug.
  • I'm willing to submit a PR (Thank you!)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the minimal failing test from deps/outliner, then read deps/outliner/src/logseq/outliner/recycle.cljs and execute-custom-query in src/main/frontend/worker/handler/query.cljs. Compare query evaluation with the post-query filtering in render_resource/query.cljs. Done means recycled pages, blocks, and descendants no longer affect joins, negations, or other query clauses, matching permanent deletion behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
clojure
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.