DB graph: recycled (soft-deleted) pages/blocks still affect query evaluation until permanently deleted
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-result → ldb/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:
- Create a DB graph, a tag
Stream, and a:nodepropertyStream Of. - Create pages
parentandchild, both tagged#Stream; setchild'sStream Oftoparent. - Add the advanced query above. The result is
childonly. - Delete page
child. - The query now returns nothing. Expected:
parent. - Open the Recycle bin and permanently delete
child. The query now returnsparent.
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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