fix: validate and repair invalid db blocks

This commit is contained in:
Tienson Qin
2026-05-12 18:54:36 +08:00
parent 5638050cbb
commit 76a8a6d92c
9 changed files with 707 additions and 230 deletions

7
bb.edn
View File

@@ -114,6 +114,13 @@
"pnpm exec nbb-logseq script/dump_datoms.cljs"
*command-line-args*)}
dev:validate-and-fix
{:doc "Validate a DB graph and run shared invalid data repairs"
:requires ([babashka.fs :as fs])
:task (apply shell {:dir "deps/db" :extra-env {"ORIGINAL_PWD" (fs/cwd)}}
"pnpm exec nbb-logseq -cp src:../../src/main:../cli/src:../graph-parser/src:../outliner/src:../db-sync/src script/validate_and_fix.cljs"
*command-line-args*)}
dev:diff-datoms
logseq.tasks.dev/diff-datoms

84
deps/db/script/validate_and_fix.cljs vendored Normal file
View File

@@ -0,0 +1,84 @@
(ns validate-and-fix
"Validate a DB graph and run frontend.worker.db.validate-fix repairs."
(:require [babashka.cli :as cli]
[clojure.pprint :as pprint]
[frontend.worker.db.validate-fix :as validate-fix]
[logseq.db.common.sqlite-cli :as sqlite-cli]
[nbb.core :as nbb]))
(def spec
{:help {:alias :h
:desc "Print help"}
:no-fix {:desc "Only validate; do not run invalid data repairs"}
:verbose {:alias :v
:desc "Print invalid entity ids and final summary"}})
(defn- usage []
(str "Usage: bb dev:validate-and-fix GRAPH [OPTIONS]\n\n"
"GRAPH can be a DB graph name or a sqlite file path, e.g. ~/Downloads/db.sqlite.\n\n"
"Options:\n"
(cli/format-opts {:spec spec})))
(defn- expand-home
[path]
(if (.startsWith path "~/")
(str js/process.env.HOME (.slice path 1))
path))
(defn- print-result!
[graph {:keys [errors datom-count invalid-entity-ids]} options]
(if (seq errors)
(do
(println "Graph" (pr-str graph) "is still invalid.")
(println "Found" (count errors)
(if (= 1 (count errors)) "entity" "entities")
"with errors.")
(when (:verbose options)
(println "Invalid entity ids:")
(pprint/pprint invalid-entity-ids)
(println "Errors:")
(pprint/pprint errors))
(js/process.exit 1))
(do
(println "Valid!")
(when (:verbose options)
(println "Datoms:" datom-count)))))
(defn- open-graph!
[graph]
(apply sqlite-cli/open-sqlite-datascript! (sqlite-cli/->open-db-args graph)))
(defn- close-sqlite!
[sqlite]
(when sqlite
(.close sqlite)))
(defn- validate-open-graph!
[graph options]
(let [{:keys [sqlite conn]} (open-graph! graph)]
(try
(if (:no-fix options)
(let [{:keys [errors] :as result} (validate-fix/validate-db-result @conn)]
(validate-fix/log-validation-errors! errors)
result)
(validate-fix/validate-and-fix-invalid-blocks! conn))
(finally
(close-sqlite! sqlite)))))
(defn validate-and-fix-graph!
[graph options]
(print-result! graph
(validate-open-graph! (expand-home graph) options)
options))
(defn -main
[args]
(let [{options :opts args' :args} (cli/parse-args args {:spec spec})
graph (first args')]
(when (or (:help options) (nil? graph))
(println (usage))
(js/process.exit (if (:help options) 0 1)))
(validate-and-fix-graph! graph options)))
(when (= nbb/*file* (nbb/invoked-file))
(-main *command-line-args*))

View File

@@ -183,8 +183,14 @@
(update acc e update a (fnil conj #{}) v)
;; If there's already a val, don't clobber it and automatically start collecting it as a :many
(if-let [existing-val (get-in acc [e a])]
(if (set? existing-val)
(cond
(= existing-val v)
acc
(set? existing-val)
(update acc e assoc a (conj existing-val v))
:else
(update acc e assoc a #{existing-val v}))
(update acc e assoc a v))))
{}

View File

@@ -12,9 +12,17 @@
(def ^:private retention-ms (* 30 24 3600 1000))
(def gc-interval-ms (* 24 3600 1000))
(defn- has-attr?
[db entity attr]
(boolean
(some #(= attr (:a %))
(d/datoms db :eavt (:db/id entity)))))
(defn- recycled?
[entity]
(some? (:logseq.property/deleted-at entity)))
[db entity]
(or (ldb/recycled? entity)
(and (= recycle-page-title (:block/title (:block/page entity)))
(has-attr? db entity :logseq.property.recycle/original-page))))
(defn- build-recycle-page-tx
[db-id]
@@ -110,6 +118,22 @@
[db tx-data]
(distinct (concat tx-data (delete-blocks/update-refs-history db tx-data {}))))
(defn- retract-entity-tx-data
[db db-id]
(map (fn [datom]
[:db/retract (:e datom) (:a datom) (:v datom)])
(d/datoms db :eavt db-id)))
(defn- permanently-delete-entity-tx-data
[db root]
(->> (if (ldb/page? root)
(page-tree-ids db root)
(map :db/id (block-subtree db root)))
(mapcat #(retract-entity-tx-data db %))
seq
(with-delete-cleanup-tx db)
seq))
(defn recycle-blocks-tx-data
[db blocks {:keys [deleted-by-uuid now-ms]}]
(let [{:keys [page page-id tx-data]} (ensure-recycle-page db)
@@ -174,7 +198,7 @@
(let [original-parent (resolve-entity db (:logseq.property.recycle/original-parent root))
original-page (resolve-entity db (:logseq.property.recycle/original-page root))
parent-valid? (and original-parent
(not (recycled? original-parent))
(not (recycled? db original-parent))
(d/entity db (:db/id original-parent)))]
(cond
(ldb/page? root)
@@ -191,7 +215,7 @@
(and original-page
(d/entity db (:db/id original-page))
(not (recycled? original-page)))
(not (recycled? db original-page)))
{:parent original-page
:page original-page
:order (restore-order original-page)}
@@ -236,25 +260,28 @@
(defn ^:api permanently-delete-tx-data
[db root]
(when (and root (recycled? root))
(some->> (if (ldb/page? root)
(keep (fn [id]
(some-> (d/entity db id) :block/uuid))
(page-tree-ids db root))
(keep :block/uuid (block-subtree db root)))
(map (fn [block-uuid]
[:db/retractEntity [:block/uuid block-uuid]]))
distinct
seq
(with-delete-cleanup-tx db)
seq)))
(when (and root (recycled? db root))
(permanently-delete-entity-tx-data db root)))
(defn ^:api permanently-delete!
[conn root-uuid]
(when-let [root (d/entity @conn [:block/uuid root-uuid])]
(when-let [tx-data (permanently-delete-tx-data @conn root)]
(ldb/transact! conn tx-data {:outliner-op :recycle-delete-permanently})
true)))
(let [root-id (:db/id root)]
(loop [deleted? false]
(if-let [root' (d/entity @conn root-id)]
(let [datom-count-before (count (d/datoms @conn :eavt root-id))]
(if (zero? datom-count-before)
deleted?
(if-let [tx-data (permanently-delete-entity-tx-data @conn root')]
(do
(ldb/transact! conn tx-data {:outliner-op :recycle-delete-permanently
:skip-validate-db? true})
(when (>= (count (d/datoms @conn :eavt root-id)) datom-count-before)
(throw (ex-info "Failed to make progress while permanently deleting recycled block"
{:root-id root-id})))
(recur true))
deleted?)))
deleted?)))))
(defn- gc-tx-data
[db {:keys [now-ms] :or {now-ms (common-util/time-ms)}}]
@@ -267,7 +294,7 @@
[(<= ?deleted-at ?cutoff)]]
db cutoff)
(map #(d/entity db %))
(filter recycled?)
(filter #(recycled? db %))
(mapcat (fn [entity]
(if (ldb/page? entity)
(map (fn [id] [:db/retractEntity id]) (page-tree-ids db entity))

View File

@@ -4,188 +4,12 @@
[datascript.core :as d]
[datascript.impl.entity :as de]
[frontend.worker.db.migrate :as db-migrate]
[frontend.worker.db.validate-fix :as validate-fix]
[frontend.worker.shared-service :as shared-service]
[logseq.db :as ldb]
[logseq.db-sync.checksum :as sync-checksum]
[logseq.db.frontend.class :as db-class]
[logseq.db.frontend.validate :as db-validate]))
(defn- get-property-by-title
[db title]
(when title
(some->> (first (ldb/page-exists? db title [:logseq.class/Property]))
(d/entity db))))
(defn- block-missing-uuid?
[entity]
(and (nil? (:block/uuid entity))
(string? (:block/title entity))
(:block/page entity)
(:block/parent entity)
(string? (:block/order entity))
(int? (:block/created-at entity))
(int? (:block/updated-at entity))))
(defn- normal-page-missing-updated-at?
[entity dispatch-key]
(and (= dispatch-key :normal-page)
(nil? (:block/updated-at entity))
(int? (:block/created-at entity))))
(defn- ^:large-vars/cleanup-todo fix-invalid-blocks!
[conn errors]
(let [db @conn
fix-tx-data (mapcat
(fn [{:keys [entity dispatch-key]}]
(let [entity (d/entity db (:db/id entity))]
(cond
(some? (:logseq.property/parent entity))
[[:db/retract (:db/id entity) :logseq.property/parent]]
(some? (:hide? entity))
[[:db/retract (:db/id entity) :hide?]]
(some? (:public? entity))
[[:db/retract (:db/id entity) :public?]]
(some? (:block/pre-block? entity))
[[:db/retract (:db/id entity) :block/pre-block?]]
(some? (:logseq.property.embedding/hnsw-label entity))
[[:db/retract (:db/id entity) :logseq.property.embedding/hnsw-label]]
(some? (:logseq.property.embedding/hnsw-label-updated-at entity))
[[:db/retract (:db/id entity) :logseq.property.embedding/hnsw-label-updated-at]]
(normal-page-missing-updated-at? entity dispatch-key)
[[:db/add (:db/id entity) :block/updated-at (:block/created-at entity)]]
(and (= "External URL" (:block/title entity))
(nil? (:block/tags entity)))
[[:db/retractEntity (:db/id entity)]]
(and (ldb/property? entity)
(some #(= (:db/ident %) :logseq.class/Tag) (:block/tags entity)))
[[:db/retract (:db/id entity) :block/tags :logseq.class/Tag]]
(and (:db/ident entity)
(db-class/user-class-namespace? (str (:db/ident entity)))
(not (:logseq.property/built-in? entity))
(not (ldb/class? entity)))
[[:db/add (:db/id entity) :block/tags :logseq.class/Tag]
[:db/retract (:db/id entity) :block/tags :logseq.class/Page]]
(and (ldb/class? entity) (:kv/value entity))
[[:db/retract (:db/id entity) :kv/value]]
(and (ldb/property? entity)
(:logseq.property.class/extends entity))
(mapv (fn [class]
[:db/retract (:db/id entity) :logseq.property.class/extends (:db/id class)])
(:logseq.property.class/extends entity))
(:block/level entity)
[[:db/retract (:db/id entity) :block/level]]
;; missing :db/ident
(and (ldb/class? entity) (nil? (:db/ident entity)) (:block/title entity))
[[:db/add (:db/id entity) :db/ident (db-class/create-user-class-ident-from-name db (:block/title entity))]]
(and
(= (:block/title (:logseq.property/created-from-property entity)) "description")
(nil? (:block/page entity)))
(let [property-id (:db/id (:logseq.property/created-from-property entity))]
[[:db/add (:db/id entity) :block/page property-id]
[:db/add (:db/id entity) :block/parent property-id]])
(and (:db/ident entity)
(:logseq.property/built-in? entity)
(:block/parent entity))
[[:db/retract (:db/id entity) :block/parent]]
(:block/format entity)
[[:db/retract (:db/id entity) :block/format]]
(= :whiteboard-shape (:logseq.property/ls-type entity))
[[:db/retractEntity (:db/id entity)]]
(and (:block/page entity) (not (:block/parent entity)))
[[:db/add (:db/id entity) :block/parent (:db/id (:block/page entity))]]
(and (:logseq.property/created-by-ref entity)
(not (de/entity? (:logseq.property/created-by-ref entity))))
[[:db/retractEntity (:db/id entity)]]
(block-missing-uuid? entity)
[[:db/add (:db/id entity) :block/uuid (random-uuid)]]
(vector? (:logseq.property/value entity))
[[:db/retractEntity (:db/id entity)]]
(and (:block/tx-id entity) (nil? (:block/title entity)))
[[:db/retractEntity (:db/id entity)]]
(and (:block/title entity) (nil? (:block/page entity)) (nil? (:block/parent entity)) (nil? (:block/name entity)))
[[:db/retractEntity (:db/id entity)]]
(= :block/path-refs (:db/ident entity))
(try
(db-migrate/remove-block-path-refs db)
(catch :default _e
nil))
(not-every? (fn [e] (ldb/class? e)) (:block/tags entity))
(let [non-tags (remove ldb/class? (:block/tags entity))]
(map (fn [tag]
[:db/retract (:db/id entity) :block/tags (:db/id tag)]) non-tags))
(and (= dispatch-key :normal-page) (:block/page entity))
[[:db/retract (:db/id entity) :block/page]]
(and (= dispatch-key :block) (nil? (:block/title entity)))
[[:db/retractEntity (:db/id entity)]]
(and (= dispatch-key :block) (nil? (:block/page entity)))
(let [latest-journal-id (:db/id (first (ldb/get-latest-journals db)))
page-id (:db/id (:block/page (:block/parent entity)))]
(cond
page-id
[[:db/add (:db/id entity) :block/page page-id]]
latest-journal-id
[[:db/add (:db/id entity) :block/page latest-journal-id]
[:db/add (:db/id entity) :block/parent latest-journal-id]]
:else
(js/console.error (str "Don't know where to put the block " (:db/id entity)))))
(and (= dispatch-key :block)
(some (fn [k] (= "user.class" (namespace k))) (keys (:logseq.property.table/sized-columns entity))))
(let [new-value (->> (keep (fn [[k v]]
(if (= "user.class" (namespace k))
(when-let [property (get-property-by-title db (:block/title (d/entity db k)))]
[(:db/ident property) v])
[k v]))
(:logseq.property.table/sized-columns entity))
(into {}))]
[[:db/add (:db/id entity) :logseq.property.table/sized-columns new-value]])
(some (fn [k] (= "block.temp" (namespace k))) (keys entity))
(let [ks (filter (fn [k] (= "block.temp" (namespace k))) (keys entity))]
(mapv (fn [k] [:db/retract (:db/id entity) k]) ks))
(and (not (:block/page entity)) (not (:block/parent entity)) (not (:block/name entity)))
[[:db/retractEntity (:db/id entity)]]
(and (= dispatch-key :property-value-block) (:block/title entity))
[[:db/retract (:db/id entity) :block/title]]
(and (ldb/class? entity) (not (:logseq.property.class/extends entity))
(not= (:db/ident entity) :logseq.class/Root))
[[:db/add (:db/id entity) :logseq.property.class/extends :logseq.class/Root]]
(and (or (ldb/class? entity) (ldb/property? entity)) (ldb/internal-page? entity))
[[:db/retract (:db/id entity) :block/tags :logseq.class/Page]]
(and (:logseq.property.asset/remote-metadata entity) (nil? (:logseq.property.asset/type entity)))
[[:db/retractEntity (:db/id entity)]]
:else
nil)))
errors)
class-as-properties (concat
(mapcat
(fn [ident]
(->> (d/datoms db :avet ident)
(mapcat (fn [d]
(let [entity (d/entity db (:v d))]
(when (ldb/class? entity)
(if-let [property (get-property-by-title db (:block/title entity))]
[[:db/retract (:e d) (:a d) (:v d)]
[:db/add (:e d) (:a d) (:db/id property)]]
[[:db/retract (:e d) (:a d) (:v d)]])))))))
[:logseq.property.view/group-by-property :logseq.property.table/pinned-columns])
(->> (d/datoms db :eavt)
(filter (fn [d] (= (namespace (:a d)) "user.class")))
(mapcat (fn [d]
(let [class-title (:block/title (d/entity db (:a d)))
property (get-property-by-title db class-title)]
(if property
[[:db/retract (:e d) (:a d) (:v d)]
[:db/add (:e d) (:db/ident property) (:v d)]]
[[:db/retract (:e d) (:a d) (:v d)]]))))))
tx-data (concat fix-tx-data
class-as-properties)]
(when (seq tx-data)
(let [tx-report (d/transact! conn tx-data {:fix-db? true})]
(seq (:tx-data tx-report))))))
(defn- fix-num-prefix-db-idents!
"Fix invalid db/ident keywords for both classes and properties"
[conn]
@@ -261,30 +85,6 @@
:db/index true}]
{:fix-db? true})))
(defn- validate-db-result
[db]
(let [{:keys [errors datom-count entities]} (db-validate/validate-db db)
invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))]
{:errors errors
:datom-count datom-count
:entities entities
:invalid-entity-ids invalid-entity-ids}))
(defn- log-validation-errors!
[errors]
(doseq [error errors]
(prn :debug
:entity (:entity error)
:error (dissoc error :entity))))
(defn- validate-and-fix-invalid-blocks!
[conn]
(loop [{:keys [errors] :as result} (validate-db-result @conn)]
(log-validation-errors! errors)
(if (and (seq errors) (fix-invalid-blocks! conn errors))
(recur (validate-db-result @conn))
result)))
(defn validate-db
[conn & {:keys [fix] :or {fix true}}]
(when fix
@@ -294,11 +94,14 @@
(fix-non-closed-values! conn)
(fix-num-prefix-db-idents! conn))
(let [{:keys [errors datom-count entities invalid-entity-ids]}
(let [{:keys [errors datom-count entities invalid-entity-ids]
:as result}
(if fix
(validate-and-fix-invalid-blocks! conn)
(let [{:keys [errors] :as result} (validate-db-result @conn)]
(log-validation-errors! errors)
(validate-fix/validate-and-fix-invalid-blocks!
conn
{:remove-block-path-refs-fn db-migrate/remove-block-path-refs})
(let [{:keys [errors] :as result} (validate-fix/validate-db-result @conn)]
(validate-fix/log-validation-errors! errors)
result))
db @conn]
@@ -316,9 +119,10 @@
(shared-service/broadcast-to-clients! :notification
[(str "Your graph is valid! " (assoc (db-validate/graph-counts db entities) :datoms datom-count))
:success false]))
{:errors errors
:datom-count datom-count
:invalid-entity-ids invalid-entity-ids}))
(assoc result
:errors errors
:datom-count datom-count
:invalid-entity-ids invalid-entity-ids)))
(defn recompute-checksum-diagnostics
[_repo conn {:keys [local-checksum remote-checksum] :as _sync-diagnostics}]

View File

@@ -0,0 +1,330 @@
(ns frontend.worker.db.validate-fix
"Script-safe DB validation repairs shared by worker validation and dev tasks."
(:require [clojure.string :as string]
[datascript.core :as d]
[datascript.impl.entity :as de]
[logseq.db :as ldb]
[logseq.db.frontend.class :as db-class]
[logseq.db.frontend.validate :as db-validate]))
(defn- get-property-by-title
[db title]
(when title
(some->> (first (ldb/page-exists? db title [:logseq.class/Property]))
(d/entity db))))
(defn- block-missing-uuid?
[entity]
(and (nil? (:block/uuid entity))
(string? (:block/title entity))
(:block/page entity)
(:block/parent entity)
(string? (:block/order entity))
(int? (:block/created-at entity))
(int? (:block/updated-at entity))))
(defn- normal-page-missing-updated-at?
[entity dispatch-key]
(and (= dispatch-key :normal-page)
(nil? (:block/updated-at entity))
(int? (:block/created-at entity))))
(defn- on-recycle-page?
[entity]
(= "recycle" (:block/name (:block/page entity))))
(defn- has-attr?
[db entity attr]
(boolean
(some #(= attr (:a %))
(d/datoms db :eavt (:db/id entity)))))
(defn- invalid-recycled-block?
[db entity dispatch-key]
(and (= dispatch-key :block)
(or (ldb/recycled? entity)
(and (on-recycle-page? entity)
(has-attr? db entity :logseq.property.recycle/original-page)))))
(defn- resolve-entity-id
[db entity]
(letfn [(valid-entity-id [id]
(when (and (int? id)
(<= id 2147483647))
id))]
(or (valid-entity-id (:db/id entity))
(when (de/entity? entity)
(valid-entity-id (.-eid ^js entity)))
(some (fn [[property value]]
(when-let [attr (:db/ident property)]
(:e (first (d/datoms db :avet attr value)))))
(:block/properties entity))
(some->> (re-find #":db/id (\d+)" (pr-str entity))
second
parse-long
valid-entity-id))))
(defn- invalid-normal-page-alias-datoms
[db entity dispatch-key]
(let [eid (resolve-entity-id db entity)]
(when (and (= dispatch-key :normal-page)
eid)
(seq
(remove
(fn [datom]
(ldb/page? (d/entity db (:v datom))))
(d/datoms db :eavt eid :block/alias))))))
(defn- invalid-recycle-fragment?
[entity dispatch-key]
(and (nil? dispatch-key)
(or (:logseq.property.recycle/original-order entity)
(:logseq.property.recycle/original-page entity)
(:logseq.property.recycle/original-parent entity)
(:logseq.property/deleted-at entity)
(string/includes? (pr-str entity) ":logseq.property.recycle/"))))
(defn- invalid-orphan-fragment?
[entity dispatch-key]
(and (nil? dispatch-key)
(not (block-missing-uuid? entity))
(or (:logseq.property.embedding/hnsw-label-updated-at entity)
(:logseq.property/deleted-at entity)
(:block/uuid entity)
(string/includes? (pr-str entity) ":logseq.property.embedding/hnsw-label-updated-at")
(string/includes? (pr-str entity) ":logseq.property/deleted-at"))))
(defn- invalid-fragment-error?
[{:keys [entity dispatch-key]}]
(or (invalid-recycle-fragment? entity dispatch-key)
(invalid-orphan-fragment? entity dispatch-key)))
(defn invalid-fragment-ids
[db errors]
(distinct
(keep (fn [{error-entity-id :entity-id :as error}]
(when (invalid-fragment-error? error)
(or error-entity-id (resolve-entity-id db (:entity error)))))
errors)))
(defn- remove-block-path-refs
[db]
(if (d/entity db :block/path-refs)
(let [remove-datoms (->> (d/datoms db :avet :block/path-refs)
(map :e)
(distinct)
(mapv (fn [id]
[:db/retract id :block/path-refs])))]
(conj remove-datoms [:db/retractEntity :block/path-refs]))
(map (fn [eid]
[:db/retract eid :block/path-refs])
(d/q '[:find [?e ...]
:where
[?e :block/path-refs ?v]]
db))))
(defn fix-invalid-blocks!
([conn errors]
(fix-invalid-blocks! conn errors nil))
([conn errors {:keys [remove-block-path-refs-fn]}]
(let [db @conn
fix-tx-data (mapcat
(fn [{error-entity-id :entity-id
:keys [entity dispatch-key]}]
(when-let [entity-id (or error-entity-id (resolve-entity-id db entity))]
(let [error-entity entity
entity (d/entity db entity-id)
invalid-alias-datoms (invalid-normal-page-alias-datoms db entity dispatch-key)]
(cond
(invalid-recycle-fragment? error-entity dispatch-key)
[[:db/retractEntity entity-id]]
(invalid-orphan-fragment? error-entity dispatch-key)
[[:db/retractEntity entity-id]]
(invalid-recycled-block? db entity dispatch-key)
[[:db/retractEntity entity-id]]
(some? (:logseq.property/parent entity))
[[:db/retract entity-id :logseq.property/parent]]
(some? (:hide? entity))
[[:db/retract entity-id :hide?]]
(some? (:public? entity))
[[:db/retract entity-id :public?]]
(some? (:block/pre-block? entity))
[[:db/retract entity-id :block/pre-block?]]
(some? (:logseq.property.embedding/hnsw-label entity))
[[:db/retract entity-id :logseq.property.embedding/hnsw-label]]
(some? (:logseq.property.embedding/hnsw-label-updated-at entity))
[[:db/retract entity-id :logseq.property.embedding/hnsw-label-updated-at]]
(normal-page-missing-updated-at? entity dispatch-key)
[[:db/add entity-id :block/updated-at (:block/created-at entity)]]
(and (= "External URL" (:block/title entity))
(nil? (:block/tags entity)))
[[:db/retractEntity entity-id]]
(and (ldb/property? entity)
(some #(= (:db/ident %) :logseq.class/Tag) (:block/tags entity)))
[[:db/retract entity-id :block/tags :logseq.class/Tag]]
(and (:db/ident entity)
(db-class/user-class-namespace? (str (:db/ident entity)))
(not (:logseq.property/built-in? entity))
(not (ldb/class? entity)))
[[:db/add entity-id :block/tags :logseq.class/Tag]
[:db/retract entity-id :block/tags :logseq.class/Page]]
(and (ldb/class? entity) (:kv/value entity))
[[:db/retract entity-id :kv/value]]
(and (ldb/property? entity)
(:logseq.property.class/extends entity))
(mapv (fn [class]
[:db/retract entity-id :logseq.property.class/extends (:db/id class)])
(:logseq.property.class/extends entity))
invalid-alias-datoms
(mapv (fn [datom]
[:db/retract (:e datom) (:a datom) (:v datom)])
invalid-alias-datoms)
(:block/level entity)
[[:db/retract entity-id :block/level]]
(and (ldb/class? entity) (nil? (:db/ident entity)) (:block/title entity))
[[:db/add entity-id :db/ident (db-class/create-user-class-ident-from-name db (:block/title entity))]]
(and
(= (:block/title (:logseq.property/created-from-property entity)) "description")
(nil? (:block/page entity)))
(let [property-id (:db/id (:logseq.property/created-from-property entity))]
[[:db/add entity-id :block/page property-id]
[:db/add entity-id :block/parent property-id]])
(and (:db/ident entity)
(:logseq.property/built-in? entity)
(:block/parent entity))
[[:db/retract entity-id :block/parent]]
(:block/format entity)
[[:db/retract entity-id :block/format]]
(= :whiteboard-shape (:logseq.property/ls-type entity))
[[:db/retractEntity entity-id]]
(and (:block/page entity) (not (:block/parent entity)))
[[:db/add entity-id :block/parent (:db/id (:block/page entity))]]
(and (:logseq.property/created-by-ref entity)
(not (de/entity? (:logseq.property/created-by-ref entity))))
[[:db/retractEntity entity-id]]
(block-missing-uuid? entity)
[[:db/add entity-id :block/uuid (random-uuid)]]
(vector? (:logseq.property/value entity))
[[:db/retractEntity entity-id]]
(and (:block/tx-id entity) (nil? (:block/title entity)))
[[:db/retractEntity entity-id]]
(and (:block/title entity) (nil? (:block/page entity)) (nil? (:block/parent entity)) (nil? (:block/name entity)))
[[:db/retractEntity entity-id]]
(= :block/path-refs (:db/ident entity))
(try
((or remove-block-path-refs-fn remove-block-path-refs) db)
(catch :default _e
nil))
(not-every? (fn [e] (ldb/class? e)) (:block/tags entity))
(let [non-tags (remove ldb/class? (:block/tags entity))]
(map (fn [tag]
[:db/retract entity-id :block/tags (:db/id tag)]) non-tags))
(and (= dispatch-key :normal-page) (:block/page entity))
[[:db/retract entity-id :block/page]]
(and (= dispatch-key :block) (nil? (:block/title entity)))
[[:db/retractEntity entity-id]]
(and (= dispatch-key :block) (nil? (:block/page entity)))
(let [latest-journal-id (:db/id (first (ldb/get-latest-journals db)))
page-id (:db/id (:block/page (:block/parent entity)))]
(cond
page-id
[[:db/add entity-id :block/page page-id]]
latest-journal-id
[[:db/add entity-id :block/page latest-journal-id]
[:db/add entity-id :block/parent latest-journal-id]]
:else
(js/console.error (str "Don't know where to put the block " entity-id))))
(and (= dispatch-key :block)
(some (fn [k] (= "user.class" (namespace k))) (keys (:logseq.property.table/sized-columns entity))))
(let [new-value (->> (keep (fn [[k v]]
(if (= "user.class" (namespace k))
(when-let [property (get-property-by-title db (:block/title (d/entity db k)))]
[(:db/ident property) v])
[k v]))
(:logseq.property.table/sized-columns entity))
(into {}))]
[[:db/add entity-id :logseq.property.table/sized-columns new-value]])
(some (fn [k] (= "block.temp" (namespace k))) (keys entity))
(let [ks (filter (fn [k] (= "block.temp" (namespace k))) (keys entity))]
(mapv (fn [k] [:db/retract entity-id k]) ks))
(and (not (:block/page entity)) (not (:block/parent entity)) (not (:block/name entity)))
[[:db/retractEntity entity-id]]
(and (= dispatch-key :property-value-block) (:block/title entity))
[[:db/retract entity-id :block/title]]
(and (ldb/class? entity) (not (:logseq.property.class/extends entity))
(not= (:db/ident entity) :logseq.class/Root))
[[:db/add entity-id :logseq.property.class/extends :logseq.class/Root]]
(and (or (ldb/class? entity) (ldb/property? entity)) (ldb/internal-page? entity))
[[:db/retract entity-id :block/tags :logseq.class/Page]]
(and (:logseq.property.asset/remote-metadata entity) (nil? (:logseq.property.asset/type entity)))
[[:db/retractEntity entity-id]]
:else
nil))))
errors)
class-as-properties (concat
(mapcat
(fn [ident]
(->> (d/datoms db :eavt)
(filter (fn [d] (= ident (:a d))))
(mapcat (fn [d]
(let [entity (d/entity db (:v d))]
(when (ldb/class? entity)
(if-let [property (get-property-by-title db (:block/title entity))]
[[:db/retract (:e d) (:a d) (:v d)]
[:db/add (:e d) (:a d) (:db/id property)]]
[[:db/retract (:e d) (:a d) (:v d)]])))))))
[:logseq.property.view/group-by-property :logseq.property.table/pinned-columns])
(->> (d/datoms db :eavt)
(filter (fn [d] (= (namespace (:a d)) "user.class")))
(mapcat (fn [d]
(let [class-title (:block/title (d/entity db (:a d)))
property (get-property-by-title db class-title)]
(if-let [property-ident (:db/ident property)]
[[:db/retract (:e d) (:a d) (:v d)]
[:db/add (:e d) property-ident (:v d)]]
[[:db/retract (:e d) (:a d) (:v d)]]))))))
tx-data (concat fix-tx-data
class-as-properties)]
(when (seq tx-data)
(let [tx-report (d/transact! conn tx-data {:fix-db? true})]
(seq (:tx-data tx-report)))))))
(defn validate-db-result
[db]
(let [{:keys [errors datom-count entities]} (db-validate/validate-db db)
errors' (map (fn [error]
(assoc error :entity-id (resolve-entity-id db (:entity error))))
errors)
invalid-entity-ids (distinct (map :entity-id errors'))]
{:errors errors'
:datom-count datom-count
:entities entities
:invalid-entity-ids invalid-entity-ids}))
(defn log-validation-errors!
[errors]
(doseq [error errors]
(prn :debug
:entity (:entity error)
:error (dissoc error :entity))))
(defn validate-and-fix-invalid-blocks!
([conn]
(validate-and-fix-invalid-blocks! conn nil))
([conn opts]
(loop [{:keys [errors] :as result} (validate-db-result @conn)
seen-validation-states #{}]
(log-validation-errors! errors)
(let [validation-state (hash (pr-str errors))
fix-progress? (and (seq errors)
(not (contains? seen-validation-states validation-state))
(fix-invalid-blocks! conn errors opts))]
(if fix-progress?
(recur (validate-db-result @conn)
(conj seen-validation-states validation-state))
result)))))

View File

@@ -2,10 +2,12 @@
(:require [cljs.test :refer [deftest is]]
[datascript.core :as d]
[frontend.worker.db.validate :as worker-db-validate]
[frontend.worker.db.validate-fix :as validate-fix]
[frontend.worker.shared-service :as shared-service]
[logseq.db.frontend.schema :as db-schema]
[logseq.db.frontend.validate :as db-validate]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]))
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.outliner.recycle :as recycle]))
(defn- create-db-graph-conn
[]
@@ -80,3 +82,143 @@
(is (nil? (:logseq.property.class/extends property)))
(is (nil? (:kv/value class)))
(is (empty? (:errors (worker-db-validate/validate-db conn))))))))
(deftest validate-db-deletes-invalid-recycled-blocks
(let [conn (create-db-graph-conn)
page-id (get (:tempids
(d/transact! conn [{:db/id "page"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 1
:block/name "test page"
:block/title "Test Page"
:block/tags :logseq.class/Page}]))
"page")
block-id (get (:tempids
(d/transact! conn [{:db/id "block"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 2
:block/page page-id
:block/parent page-id
:block/order "a0"
:block/title "Deleted block"}]))
"block")
block (d/entity @conn block-id)]
(d/transact! conn (recycle/recycle-blocks-tx-data @conn [block] {}) {:outliner-op :delete-blocks})
(d/transact! conn [[:db/add block-id :block/pre-block? true]])
(is (seq (:errors (db-validate/validate-db @conn))))
(with-redefs [shared-service/broadcast-to-clients! (fn [& _args] nil)]
(is (empty? (:errors (worker-db-validate/validate-db conn))))
(is (nil? (d/entity @conn block-id))))))
(deftest validate-db-deletes-invalid-recycled-fragments
(let [conn (create-db-graph-conn)
tempids (:tempids
(d/transact! conn [{:db/id "recycle-fragment"
:logseq.property.embedding/hnsw-label-updated-at 0
:logseq.property.recycle/original-order "a2b"}
{:db/id "orphan-fragment"
:block/uuid (random-uuid)}]))
recycle-fragment-id (get tempids "recycle-fragment")
orphan-fragment-id (get tempids "orphan-fragment")]
(let [errors (:errors (db-validate/validate-db @conn))]
(is (seq errors))
(is (= #{recycle-fragment-id}
(set (validate-fix/invalid-fragment-ids @conn errors)))))
(let [result (validate-fix/validate-and-fix-invalid-blocks! conn)]
(is (empty? (:errors result)))
(is (nil? (d/entity @conn recycle-fragment-id)))
(is (nil? (d/entity @conn orphan-fragment-id)))
(is (empty? (:errors (validate-fix/validate-and-fix-invalid-blocks! conn)))))))
(deftest validate-db-repairs-normal-page-alias-to-non-page
(let [conn (create-db-graph-conn)
page-id (get (:tempids
(d/transact! conn [{:db/id "page"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 1
:block/name "object oriented programming"
:block/title "Object Oriented Programming"
:block/tags :logseq.class/Page}]))
"page")
block-id (get (:tempids
(d/transact! conn [{:db/id "block"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 2
:block/page page-id
:block/parent page-id
:block/order "a0"
:block/title "Not a Page"}]))
"block")]
(d/transact! conn [[:db/add page-id :block/alias block-id]])
(is (seq (:errors (db-validate/validate-db @conn))))
(with-redefs [shared-service/broadcast-to-clients! (fn [& _args] nil)]
(let [result (worker-db-validate/validate-db conn)
page (d/entity @conn page-id)]
(is (empty? (:errors result)))
(is (empty? (:block/alias page)))
(is (empty? (:errors (worker-db-validate/validate-db conn))))))))
(deftest validate-db-repairs-invalid-raw-block-tag
(let [conn (create-db-graph-conn)
page-id (get (:tempids
(d/transact! conn [{:db/id "page"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 1
:block/name "test page"
:block/title "Test Page"
:block/tags :logseq.class/Page}]))
"page")
block-id (get (:tempids
(d/transact! conn [{:db/id "block"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 2
:block/page page-id
:block/parent page-id
:block/order "a0"
:block/title "Tagged block"}]))
"block")
corrupt-db (d/init-db (conj (vec (d/datoms @conn :eavt))
(d/datom block-id :block/tags :missing.ident/tag 1 true))
(:schema @conn))
corrupt-conn (d/conn-from-db corrupt-db)]
(is (seq (:errors (db-validate/validate-db @corrupt-conn))))
(with-redefs [shared-service/broadcast-to-clients! (fn [& _args] nil)]
(validate-fix/fix-invalid-blocks! corrupt-conn (:errors (db-validate/validate-db @corrupt-conn)))
(is (empty? (remove #(= :logseq.class/Page (:db/ident %))
(:block/tags (d/entity @corrupt-conn block-id))))))))
(deftest validate-db-does-not-add-class-property-with-missing-ident
(let [conn (create-db-graph-conn)
block-id (get (:tempids
(d/transact! conn [{:db/id "class"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 1
:block/name "topic"
:block/title "Topic"
:block/tags :logseq.class/Tag
:db/ident :user.class/topic}
{:db/id "property"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 1
:block/name "topic"
:block/title "Topic"
:block/tags :logseq.class/Property}
{:db/id "block"
:block/uuid (random-uuid)
:block/created-at 1
:block/updated-at 2
:block/title "Block"
:user.class/topic "value"}]))
"block")]
(validate-fix/fix-invalid-blocks! conn [])
(let [block (d/entity @conn block-id)]
(is (nil? (:user.class/topic block)))
(is (nil? (get block nil))))))

View File

@@ -0,0 +1,17 @@
(ns logseq.db.frontend.malli-schema-test
(:require [cljs.test :refer [deftest is]]
[logseq.db.frontend.malli-schema :as db-malli-schema]))
(deftest datoms->entity-maps-ignores-duplicate-identical-cardinality-one-datoms
(is (= {1 {:block/title "Title"
:block/created-at 1}}
(db-malli-schema/datoms->entity-maps
[{:e 1 :a :block/title :v "Title"}
{:e 1 :a :block/title :v "Title"}
{:e 1 :a :block/created-at :v 1}]))))
(deftest datoms->entity-maps-keeps-conflicting-cardinality-one-values
(is (= {1 {:block/title #{"Old" "New"}}}
(db-malli-schema/datoms->entity-maps
[{:e 1 :a :block/title :v "Old"}
{:e 1 :a :block/title :v "New"}]))))

View File

@@ -0,0 +1,60 @@
(ns logseq.outliner.recycle-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]))
(deftest permanently-delete-recycled-block-removes-uuid-missing-descendants
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}]}])
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")
parent-uuid (:block/uuid parent)
child-id (:db/id child)]
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
(d/transact! conn [[:db/retract child-id :block/uuid (:block/uuid child)]])
(is (true? (recycle/permanently-delete! conn parent-uuid)))
(is (nil? (d/entity @conn child-id)))))
(deftest permanently-delete-recycled-descendant
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}]}])
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")
child-uuid (:block/uuid child)]
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid child-uuid]))))
(is (true? (recycle/permanently-delete! conn child-uuid)))
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
(deftest permanently-delete-block-stuck-on-recycle-page
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}]}])
page (ldb/get-page @conn "page1")
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")
child-uuid (:block/uuid child)]
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
(d/transact! conn [[:db/retract (:db/id parent) :logseq.property/deleted-at]
[:db/add (:db/id child) :logseq.property.recycle/original-page (:db/id page)]])
(is (false? (ldb/recycled? (d/entity @conn [:block/uuid child-uuid]))))
(is (true? (recycle/permanently-delete! conn child-uuid)))
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
(deftest permanently-delete-invalid-recycled-block
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"}]}])
parent (db-test/find-block-by-content @conn "parent")
parent-uuid (:block/uuid parent)]
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
(d/transact! conn [[:db/add (:db/id parent) :block/pre-block? true]])
(is (true? (recycle/permanently-delete! conn parent-uuid)))
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))))