"Validate graph" silently deletes user data: fix defaults to true, logs only at :debug, doesn't bump updated-at, and doesn't refresh the UI
@tiensonqin is already working on this.
Since Aug 18, 2026.
- Dominant language
- No language data
- Stars
- 28
- Forks
- 2
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
`validate-db` runs a series of mutating fix-up passes whenever its `fix`
argument is true, **which is its default**. One of those passes,
`fix-non-closed-values!`, retracts property values. When it does, the user is
told nothing:
1. No notification, and no mention in the validation report.
2. The retraction is printed only at `:debug` to worker stdout.
3. `:block/updated-at` on the affected block is **not** bumped, so no external
tooling or sync can detect the change.
4. **The UI does not refresh.** The block keeps displaying the value it no
longer has until the app is manually reloaded.
Point 4 is the one I'd most like to draw attention to. While reproducing this I
first concluded validation was *not* the cause — because the value was still on
screen after validating. It had already been deleted from the database. A user
who validates, glances at the page, and sees their data intact has been told
something false by the application.
I lost data to this twice, six days apart, and only noticed when a build reading
the property started failing. The second loss removed a value I had manually
restored after the first.
## Version
Logseq DB version, desktop macOS arm64 (build 2287).
`db-worker-node` build-time `2026-07-28T15:15:45Z`, revision `9a11243-dirty`.
## Steps to reproduce
Any state that causes a fix pass to retract will do. The one I hit:
1. Create a property with type **Text** and two closed values.
2. Change its type to **Node** (see companion issue — the closed values are left
behind and become invisible).
3. Set the property on a block to a node value.
4. Run **Validate graph**.
5. Look at the block. **The value is still displayed.**
6. Reload the app. The value is gone.
Querying DataScript directly after step 4 confirms the value is already absent
at that point. Restoring the value and validating again deletes it again, so
this is not a one-time migration artifact.
The only trace:
```
:debug :fix-non-closed-values ([:db/retract 7171 :my.property/fig-source 7014])
```
## Relevant source
From `frontend.worker.db.validate` in the shipped bundle:
```clojure
(defn validate-db
[conn & {:keys [fix] :or {fix true}}] ; fix defaults TRUE
(when fix
(fix-extends-cardinality! conn)
(fix-icon-wrong-type! conn)
(db-migrate/ensure-built-in-data-exists! conn)
(fix-non-closed-values! conn)
(fix-num-prefix-db-idents! conn))
(let [{:keys [errors ...]}
(if fix
(validate-and-fix-invalid-blocks! conn)
...)]
...))
```
Two observations on the shape of this:
**`fix` defaults to `true`.** A command named "Validate graph" mutates the graph
unless a caller opts out. The CLI gets this the other way round — `logseq graph
validate` exposes `-f, --fix` as opt-in — so the two surfaces disagree about
whether validation is a read operation.
**Several call sites take the default without saying so.** `:dev/validate-db`
is one. `export-repo-as-db-edn!` is another, and it validates *before* writing
the file — so a failed or cancelled export still runs the fix passes, and leaves
no artifact to show for it.
**Some passes are considerably broader than their names suggest.**
`fix-icon-wrong-type!` retracts **every** `:logseq.property/icon` datom in the
database if the icon property's `:db/valueType` is `ref`:
```clojure
(defn- fix-icon-wrong-type!
[conn]
(let [icon (d/entity @conn :logseq.property/icon)]
(when (= :db.type/ref (:db/valueType icon))
(let [datoms (d/datoms @conn :avet :logseq.property/icon)
tx-data (cons
[:db/retract (:db/id icon) :db/valueType]
(map (fn [d] [:db/retract (:e d) (:a d)]) datoms))]
(d/transact! conn tx-data {:fix-db? true})))))
```
That may well be correct recovery behaviour. But combined with the silence
above, a user has no way to learn it happened.
## Suggested fixes, in priority order
1. **Notify on any fix pass that retracts user data**, and log above `:debug`.
Include the count in the validation report the user already sees. Silent
deletion is what turned a configuration inconsistency into lost work.
2. **Refresh affected blocks after the fix passes run.** Displaying a value that
has just been deleted is worse than displaying nothing — it actively
convinces the user that nothing happened.
3. **Reconsider `fix` defaulting to `true`** for a user-facing command called
"Validate graph", or rename the command to reflect that it repairs. Matching
the CLI's opt-in `--fix` would make the two surfaces consistent.
4. **Bump `:block/updated-at`** when a fix pass modifies a block, so sync and
external tooling can detect the change.
5. Have `fix-non-closed-values!` skip properties whose current
`logseq.property/type` no longer supports closed values (see companion
issue) — it selects purely on the presence of `:block/_closed-value-property`
without checking the type.
## Impact
A build pipeline reading the affected property failed with 5 unresolved
bindings, roughly twenty hours after an identical run had resolved 34 bindings
cleanly. Nothing changed in between except this retraction, and because
`:block/updated-at` was untouched there was no signal that the graph had changed
at all.
## Related
Companion issue: logseq/db-test#1069 — changing a property's type away from Text leaves its
closed values behind, invisible but still enforced. That is the state that
triggered the deletion here.
## Attachments
Two `db-worker-node-YYYYMMDD.log` files covering both occurrences, available on
request — small, and containing no page content.
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.
Assessment
This issue has not been assessed yet.