Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
(ns logseq.db.common.delete-blocks-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.common.util :as common-util]
[logseq.db :as ldb]
[logseq.db.common.delete-blocks :as delete-blocks]
[logseq.db.test.helper :as db-test]))
(deftest delete-blocks-removes-reactions
(testing "reactions targeting deleted blocks are retracted"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Block"}]}]})
block (db-test/find-block-by-content @conn "Block")
now (common-util/time-ms)
reaction {:block/uuid (random-uuid)
:block/created-at now
:block/updated-at now
:logseq.property.reaction/emoji-id "+1"
:logseq.property.reaction/target (:db/id block)}
_ (d/transact! conn [reaction])
reaction-entity (first (:logseq.property.reaction/_target (d/entity @conn (:db/id block))))
retracts [[:db/retractEntity (:db/id block)]]
extra (delete-blocks/update-refs-history @conn retracts {})]
(d/transact! conn (concat retracts extra))
(is (nil? (d/entity @conn (:db/id reaction-entity)))))))
(deftest delete-blocks-expands-property-value-children
(testing "delete-blocks retracts generated property value children"
(let [property-value-uuid (random-uuid)
conn (db-test/create-conn-with-blocks
{:properties {:user.property/cli-http-prop {:logseq.property/type :default}}
:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Parent"
:build/properties
{:user.property/cli-http-prop
{:build/property-value :block
:block/title "Property value"
:block/uuid property-value-uuid
:build/keep-uuid? true}}}]}]})
parent (db-test/find-block-by-content @conn "Parent")
property-value (d/entity @conn [:block/uuid property-value-uuid])
txs [[:db/retractEntity (:db/id parent)]]
expanded (delete-blocks/expand-delete-blocks-tx @conn txs {:outliner-op :delete-blocks})]
(is (= (:db/id parent) (:db/id (:block/parent property-value))))
(is (some #(= [:db/retractEntity (:db/id property-value)] %) expanded)))))
(deftest delete-blocks-removes-history-with-ref-value
(testing "property history entries referencing a deleted block are retracted"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}
{:block/title "Choice value"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
choice-value-block (db-test/find-block-by-content @conn "Choice value")
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/ref-value (:db/id choice-value-block)}])
history-entity (d/entity @conn [:block/uuid history-uuid])
retracts [[:db/retractEntity (:db/id choice-value-block)]]
extra (delete-blocks/update-refs-history @conn retracts {})]
(d/transact! conn (concat retracts extra))
(is (nil? (d/entity @conn (:db/id history-entity)))))))
(deftest property-history-block-updates-are-kept
(testing "history block updates do not delete the history entity"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}
{:block/title "Choice value"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
choice-value-block (db-test/find-block-by-content @conn "Choice value")
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/title "History entry"
:block/page (:db/id (:block/page target-block))
:block/parent (:db/id (:block/page target-block))
:block/order "a0"
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/ref-value (:db/id choice-value-block)}])
history-entity (d/entity @conn [:block/uuid history-uuid])
txs [[:db/retract (:db/id history-entity) :block/title "History entry"]
[:db/add (:db/id history-entity) :block/title "Updated history entry"]
[:db/retract (:db/id history-entity) :block/parent (:db/id (:block/page history-entity))]
[:db/add (:db/id history-entity) :block/parent (:db/id target-block)]
[:db/retract (:db/id history-entity) :block/order "a0"]
[:db/add (:db/id history-entity) :block/order "b0"]]
extra (delete-blocks/update-refs-history @conn txs {})]
(d/transact! conn (concat txs extra))
(let [updated-history (d/entity @conn (:db/id history-entity))]
(is (= {:block/title "Updated history entry"
:block/parent (:db/id target-block)
:block/order "b0"}
{:block/title (:block/title updated-history)
:block/parent (:db/id (:block/parent updated-history))
:block/order (:block/order updated-history)}))))))
(deftest remote-delete-blocks-removes-history-when-owner-ref-retracted
(testing "remote delete-blocks datoms fully retract property history blocks"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}
{:block/title "Choice value"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
choice-value-block (db-test/find-block-by-content @conn "Choice value")
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/ref-value (:db/id choice-value-block)}])
history-entity (d/entity @conn [:block/uuid history-uuid])
txs [[:db/retract (:db/id history-entity) :logseq.property.history/block (:db/id target-block)]]
extra (delete-blocks/update-refs-history @conn txs {:outliner-op :delete-blocks})]
(d/transact! conn (concat txs extra))
(is (nil? (d/entity @conn (:db/id history-entity)))))))
(deftest delete-blocks-removes-new-history-for-deleted-block
(testing "history blocks created in the same tx as their deleted owner are retracted"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}
{:block/title "Choice value"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
choice-value-block (db-test/find-block-by-content @conn "Choice value")
history-uuid (random-uuid)
now (common-util/time-ms)
txs [{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/ref-value (:db/id choice-value-block)}
[:db/retractEntity (:db/id target-block)]]
extra (delete-blocks/update-refs-history @conn txs {:outliner-op :delete-blocks})]
(d/transact! conn (concat txs extra))
(is (nil? (d/entity @conn [:block/uuid history-uuid]))))))
(deftest remote-delete-blocks-removes-new-normalized-history-when-owner-ref-retracted
(testing "remote normalized delete-blocks datoms fully retract new property history blocks"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}
{:block/title "Choice value"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
choice-value-block (db-test/find-block-by-content @conn "Choice value")
history-uuid (random-uuid)
history-eid 1000000
now (common-util/time-ms)
txs [[:db/add history-eid :block/uuid history-uuid]
[:db/add history-eid :block/created-at now]
[:db/add history-eid :block/updated-at now]
[:db/add history-eid :logseq.property.history/block (:db/id target-block)]
[:db/add history-eid :logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))]
[:db/add history-eid :logseq.property.history/ref-value (:db/id choice-value-block)]
[:db/retract history-eid :logseq.property.history/block (:db/id target-block)]]
extra (delete-blocks/update-refs-history @conn txs {:outliner-op :delete-blocks})]
(d/transact! conn (concat txs extra))
(is (nil? (d/entity @conn [:block/uuid history-uuid]))))))
(deftest delete-page-removes-history-with-ref-value
(testing "deleting a page retracts property history entries that referenced that page"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Journal"}
:blocks [{:block/title "b1"}]}
{:page {:block/title "Page to delete"}}]})
block (db-test/find-block-by-content @conn "b1")
page (db-test/find-page-by-title @conn "Page to delete")
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/ref-value (:db/id page)}])
history-entity (d/entity @conn [:block/uuid history-uuid])]
(ldb/transact! conn [[:db/retractEntity (:db/id page)]])
(is (nil? (d/entity @conn (:db/id history-entity)))))))
(deftest delete-property-removes-history-for-property
(testing "property history entries for a deleted property are retracted"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}]}]
:properties {:obsolete {:logseq.property/type :string}}})
block (db-test/find-block-by-content @conn "Target block")
property (d/entity @conn :user.property/obsolete)
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id block)
:logseq.property.history/property (:db/id property)
:logseq.property.history/scalar-value "old"}])
history-entity (d/entity @conn [:block/uuid history-uuid])
retracts [[:db/retractEntity (:db/id property)]]
extra (delete-blocks/update-refs-history @conn retracts {})]
(d/transact! conn (concat retracts extra))
(is (nil? (d/entity @conn (:db/id history-entity)))))))
(deftest delete-blocks-removes-history-for-corresponding-views
(testing "property history entries attached to a deleted block's view are retracted"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
view-uuid (random-uuid)
target-history-uuid (random-uuid)
view-history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid view-uuid
:block/title "Target view"
:block/created-at now
:block/updated-at now
:logseq.property/view-for (:db/id target-block)
:logseq.property.view/type :logseq.property.view/type.table
:logseq.property.view/feature-type :linked-references}
{:block/uuid target-history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/scalar-value "Todo"}
{:block/uuid view-history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block [:block/uuid view-uuid]
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/scalar-value "List"}])]
(ldb/transact! conn [[:db/retractEntity (:db/id target-block)]])
(is (nil? (d/entity @conn [:block/uuid view-uuid])))
(is (nil? (d/entity @conn [:block/uuid target-history-uuid])))
(is (nil? (d/entity @conn [:block/uuid view-history-uuid]))))))
(deftest delete-blocks-does-not-rewrite-title-for-deleted-view
(testing "reference title rewrite should skip generated views deleted by the same cleanup"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Asset target"}]}]})
target-block (db-test/find-block-by-content @conn "Asset target")
page (:block/page target-block)
view-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid view-uuid
:block/title "Unlinked references"
:block/raw-title "Unlinked references"
:block/created-at now
:block/updated-at now
:block/page (:db/id page)
:block/parent (:db/id page)
:block/order "cD66"
:block/refs (:db/id target-block)
:logseq.property/view-for (:db/id target-block)
:logseq.property.view/type :logseq.property.view/type.list
:logseq.property.view/feature-type :unlinked-references}])
view (d/entity @conn [:block/uuid view-uuid])
retracts [[:db/retractEntity (:db/id target-block)]]
extra (delete-blocks/update-refs-history @conn retracts {})]
(is (some #(= [:db/retractEntity (:db/id view)] %) extra))
(is (not-any? #(= [:db/add (:db/id view) :block/title "Unlinked references"] %) extra)))))
(deftest delete-blocks-does-not-rewrite-title-for-deleted-history
(testing "reference title rewrite should skip history entities deleted by the same cleanup"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Target block"}]}]})
target-block (db-test/find-block-by-content @conn "Target block")
page (:block/page target-block)
history-uuid (random-uuid)
now (common-util/time-ms)
_ (d/transact! conn [{:block/uuid history-uuid
:block/title "History entry"
:block/raw-title "History entry"
:block/created-at now
:block/updated-at now
:block/page (:db/id page)
:block/parent (:db/id page)
:block/order "a0"
:block/refs (:db/id target-block)
:logseq.property.history/block (:db/id target-block)
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
:logseq.property.history/scalar-value "Todo"}])
history (d/entity @conn [:block/uuid history-uuid])
retracts [[:db/retractEntity (:db/id target-block)]]
extra (delete-blocks/update-refs-history @conn retracts {})]
(is (some #(= [:db/retractEntity (:db/id history)] %) extra))
(is (not-any? #(= [:db/add (:db/id history) :block/title "History entry"] %) extra)))))

View File

@@ -0,0 +1,49 @@
(ns logseq.db.common.initial-data-refs-test
(:require [cljs.test :refer [deftest is]]
[datascript.core :as d]
[logseq.db.common.initial-data :as common-initial-data]
[logseq.db.test.helper :as db-test]))
(deftest get-block-refs-count-matches-get-block-refs-for-class-page-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "Ref A"}}
{:page {:block/title "Ref B"}}]})
topic-id (:db/id (d/entity @conn :user.class/Topic))
ref-a-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Ref A")
ref-b-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Ref B")
_ (d/transact! conn [[:db/add ref-a-id :block/refs topic-id]
[:db/add ref-b-id :block/refs topic-id]
[:db/add ref-b-id :logseq.property/hide? true]])
refs (common-initial-data/get-block-refs @conn topic-id)
count* (common-initial-data/get-block-refs-count @conn topic-id)]
(is (= (count refs) count*))
(is (= 1 count*))))
(deftest get-block-refs-count-page-without-db-ident-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Foo"}}
{:page {:block/title "Bar"}}]})
foo-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Foo")
bar-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Bar")
_ (d/transact! conn [[:db/add bar-id :block/refs foo-id]])
count* (common-initial-data/get-block-refs-count @conn foo-id)]
(is (= 1 count*))))

View File

@@ -0,0 +1,99 @@
(ns logseq.db.common.initial-data-test
"This ns is the only one to test against file based datascript connections.
These are useful integration tests"
(:require ["fs" :as fs]
["path" :as node-path]
[cljs.test :refer [deftest async use-fixtures is testing]]
[clojure.string :as string]
[datascript.core :as d]
[logseq.db.common.initial-data :as common-initial-data]
[logseq.db.frontend.property :as db-property]
[logseq.db.common.sqlite-cli :as sqlite-cli]
[logseq.db.sqlite.build :as sqlite-build]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.db.test.helper :as db-test]))
(use-fixtures
:each
;; Cleaning tmp/ before leaves last tmp/ after a test run for dev and debugging
{:before
#(async done
(if (fs/existsSync "tmp")
(fs/rm "tmp" #js {:recursive true} (fn [err]
(when err (js/console.log err))
(done)))
(done)))})
(defn- create-graph-dir
[dir db-name]
(fs/mkdirSync (node-path/join dir db-name) #js {:recursive true}))
(deftest get-initial-data
(testing "Fetches a defined block"
(create-graph-dir "tmp/graphs" "test-db")
(let [conn* (sqlite-cli/open-db! "tmp/graphs" "test-db")
_ (d/transact! conn* (sqlite-create-graph/build-db-initial-data "{}"))
blocks [{:file/path "logseq/config.edn"
:file/content "{:foo :bar}"}]
_ (d/transact! conn* blocks)
;; Simulate getting data from sqlite and restoring it for frontend
{:keys [schema initial-data]} (common-initial-data/get-initial-data @conn*)
conn (d/conn-from-datoms initial-data schema)]
(is (= blocks
(->> @conn
(d/q '[:find (pull ?b [:file/path :file/content])
:where [?b :file/content] [?b :file/path "logseq/config.edn"]])
(map first)))
"Correct file with content is found"))))
(deftest get-initial-data-includes-property-description-datoms
(create-graph-dir "tmp/graphs" "test-db")
(let [conn* (sqlite-cli/open-db! "tmp/graphs" "test-db")
_ (d/transact! conn* (sqlite-create-graph/build-db-initial-data "{}"))
{:keys [schema initial-data]} (common-initial-data/get-initial-data @conn*)
conn (d/conn-from-datoms initial-data schema)
description (-> (d/entity @conn :logseq.property/deadline)
:logseq.property/description)]
(is (string/includes? (str (db-property/property-value-content description))
"finish something"))))
(deftest restore-initial-data
(create-graph-dir "tmp/graphs" "test-db")
(let [conn* (sqlite-cli/open-db! "tmp/graphs" "test-db")
_ (d/transact! conn* (sqlite-create-graph/build-db-initial-data "{}"))
{:keys [init-tx]}
(sqlite-build/build-blocks-tx
{:pages-and-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "b1"}]}]})
_ (d/transact! conn* init-tx)
;; Simulate getting data from sqlite and restoring it for frontend
{:keys [schema initial-data]} (common-initial-data/get-initial-data @conn*)
conn (d/conn-from-datoms initial-data schema)]
(is (some? (db-test/find-page-by-title @conn "page1"))
"Restores recently updated page")))
(deftest get-block-and-children-has-children-flag
(testing "Top-level block with children has :block.temp/has-children? true"
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "test-page"}
:blocks [{:block/title "parent"
:build/children
[{:block/title "child1"}
{:block/title "child2"}]}]}])
parent-block (db-test/find-block-by-content @conn "parent")
result (common-initial-data/get-block-and-children
@conn (:block/uuid parent-block) {:children? false})]
(is (true? (:block.temp/has-children? (:block result)))
"Top-level block with children should have :block.temp/has-children? true")))
(testing "Top-level block without children has :block.temp/has-children? false"
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "test-page2"}
:blocks [{:block/title "leaf-block"}]}])
leaf-block (db-test/find-block-by-content @conn "leaf-block")
result (common-initial-data/get-block-and-children
@conn (:block/uuid leaf-block) {:children? false})]
(is (false? (:block.temp/has-children? (:block result)))
"Top-level block without children should have :block.temp/has-children? false"))))

View File

@@ -0,0 +1,303 @@
(ns logseq.db.common.view-test
(:require [cljs.test :refer [deftest is]]
[datascript.core :as d]
[logseq.db.common.view :as db-view]
[logseq.db.test.helper :as db-test]))
(defn- create-view-id
[conn feature-type & {:keys [view-for-id]}]
(let [tx (d/transact! conn [(cond-> {:db/id -100
:block/title "Test view"
:block/uuid (random-uuid)
:logseq.property.view/feature-type feature-type
:logseq.property.view/type :logseq.property.view/type.table}
view-for-id
(assoc :logseq.property/view-for view-for-id))])]
(get-in tx [:tempids -100])))
(deftest get-view-data-journals-returns-ordered-compact-index-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
(mapv (fn [journal-day]
{:page {:build/journal journal-day}
:blocks [{:block/title (str "Block " journal-day)}]})
[20260716 20260715 20260714 20260713 20260712])})
result (db-view/get-view-data @conn nil {:journals? true})
index (:data result)]
(is (= 5 (:count result)))
(is (= [20260716 20260715 20260714 20260713 20260712]
(mapv :block/journal-day index)))
(is (every? map? index))
(when (every? map? index)
(is (every? #(= #{:db/id :block/journal-day} (set (keys %))) index)
"The journal index should include only the identity and placeholder title data."))
(is (not (contains? result :selection-block-ids))
"Blocks are loaded only for visible journals.")))
(deftest get-view-data-all-pages-sorts-and-filters-hidden-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Alpha" :block/updated-at 10}}
{:page {:block/title "Beta" :block/updated-at 20}}
{:page {:block/title "Hidden" :block/updated-at 30 :logseq.property/hide? true}}
{:page {:block/title "Deleted" :block/updated-at 40 :logseq.property/deleted-at 1}}]})
view-id (create-view-id conn :all-pages)
result (db-view/get-view-data @conn view-id {:view-feature-type :all-pages
:sorting [{:id :block/updated-at :asc? false}]})
ids (:data result)
titles (map (fn [id] (:block/title (d/entity @conn id))) ids)]
(is (= 2 (:count result)))
(is (= ["Beta" "Alpha"] titles))))
(deftest get-view-data-all-pages-title-sort-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "gamma" :block/updated-at 1}}
{:page {:block/title "alpha" :block/updated-at 2}}
{:page {:block/title "beta" :block/updated-at 3}}]})
view-id (create-view-id conn :all-pages)
result (db-view/get-view-data @conn view-id {:view-feature-type :all-pages
:sorting [{:id :block/title :asc? true}]})
ids (:data result)
titles (map (fn [id] (:block/title (d/entity @conn id))) ids)]
(is (= ["alpha" "beta" "gamma"] titles))))
(deftest get-view-data-class-objects-sort-keeps-rows-with-missing-sort-value-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "With timestamp"
:block/updated-at 20
:build/tags [:Topic]}}
{:page {:block/title "Without timestamp"
:block/updated-at 10
:build/tags [:Topic]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
without-ts-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Without timestamp")
without-ts-value (:block/updated-at (d/entity @conn without-ts-id))
_ (d/transact! conn [[:db/retract without-ts-id :block/updated-at without-ts-value]])
view-id (create-view-id conn :class-objects :view-for-id class-id)
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id
:sorting [{:id :block/updated-at :asc? false}]})
titles (map (fn [id] (:block/title (d/entity @conn id))) (:data result))]
(is (= 2 (:count result)))
(is (= #{"With timestamp" "Without timestamp"} (set titles)))))
(deftest get-view-data-class-objects-simple-is-filter-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "A" :build/tags [:Topic]}}
{:page {:block/title "B" :build/tags [:Topic]}}
{:page {:block/title "C" :build/tags [:Topic]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id
:filters {:or? false
:filters [[:block/title :is #{"B"}]]}})
titles (map (fn [id] (:block/title (d/entity @conn id))) (:data result))]
(is (= 1 (:count result)))
(is (= ["B"] titles))))
(deftest get-view-data-class-objects-groups-by-title-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "A" :build/tags [:Topic]}}
{:page {:block/title "B" :build/tags [:Topic]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id :logseq.property.view/group-by-property :block/title]])
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id})
group-titles (map first (:data result))]
(is (= ["A" "B"] group-titles))))
(deftest get-view-data-class-objects-groups-by-many-values-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}
:SciFi {:block/title "Sci-Fi"}
:Drama {:block/title "Drama"}}
:pages-and-blocks
[{:page {:block/title "Movie A" :build/tags [:Topic :SciFi :Drama]}}
{:page {:block/title "Movie B" :build/tags [:Topic :SciFi]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id :logseq.property.view/group-by-property :block/tags]])
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id})
group->titles (into {}
(map (fn [[group rows]]
[(:block/title group)
(set (map (fn [id] (:block/title (d/entity @conn id))) rows))]))
(:data result))]
(is (= #{"Movie A" "Movie B"} (get group->titles "Sci-Fi")))
(is (= #{"Movie A"} (get group->titles "Drama")))))
(deftest get-view-data-list-view-keeps-one-row-shape-for-pages-and-blocks-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "Tagged page"
:build/tags [:Topic]}}
{:page {:block/title "Block page"}
:blocks [{:block/title "Tagged block"
:build/tags [:Topic]}]}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id
:logseq.property.view/group-by-property
:block/page]
[:db/add view-id
:logseq.property.view/type
:logseq.property.view/type.list]])
result (db-view/get-view-data @conn view-id
{:view-feature-type :class-objects
:view-for-id class-id})]
(is (= 2 (:count result)))
(is (every? (fn [[_group partitions]]
(every? (fn [[breadcrumb-uuid rows]]
(and (uuid? breadcrumb-uuid)
(sequential? rows)
(every? map? rows)))
partitions))
(:data result))
"A list view must not mix flat row IDs with nested partitions.")))
(deftest get-view-data-linked-references-page-view-does-not-crash-on-missing-db-ident-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Foo"}}
{:page {:block/title "Bar"}}]})
foo-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Foo")
bar-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Bar")
_ (d/transact! conn [[:db/add bar-id :block/refs foo-id]])
view-id (create-view-id conn :linked-references :view-for-id foo-id)
result (db-view/get-view-data @conn view-id {:view-feature-type :linked-references
:view-for-id foo-id})]
(is (number? (:count result)))
(is (contains? (set (:data result)) bar-id))))
(deftest get-view-data-groups-page-level-linked-references-under-the-referring-page-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Target"}}
{:page {:block/title "Referring page"}}]})
target-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Target")
referring-page-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Referring page")
target (d/entity @conn target-id)
referring-page (d/entity @conn referring-page-id)
view-id (create-view-id conn :linked-references
:view-for-id (:db/id target))
_ (d/transact! conn
[[:db/add (:db/id referring-page)
:block/refs
(:db/id target)]
[:db/add view-id
:logseq.property.view/type
:logseq.property.view/type.list]
[:db/add view-id
:logseq.property.view/group-by-property
:block/page]])
result (db-view/get-view-data
@conn view-id
{:view-feature-type :linked-references
:view-for-id (:db/id target)})
[[group partitions]] (:data result)]
(is (= (:block/uuid referring-page) (:block/uuid group)))
(is (= [[(:block/uuid referring-page)
[{:db/id (:db/id referring-page)
:block/parent nil}]]]
(mapv (fn [[breadcrumb rows]]
[breadcrumb (vec rows)])
partitions)))))
(deftest get-view-data-class-objects-ref-filter-fast-path-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "Page A"}
:blocks [{:block/title "Obj A"
:build/tags [:Topic]}]}
{:page {:block/title "Page B"}
:blocks [{:block/title "Obj B"
:build/tags [:Topic]}]}]})
obj-a-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Obj A")
obj-b-id (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Obj B")
page-a-uuid (:block/uuid (d/entity @conn (d/q '[:find ?e .
:in $ ?title
:where [?e :block/title ?title]]
@conn
"Page A")))
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
is-result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id
:filters {:or? false
:filters [[:block/page :is #{page-a-uuid}]]}})
is-titles (map (fn [id] (:block/title (d/entity @conn id))) (:data is-result))
is-not-result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id
:filters {:or? false
:filters [[:block/page :is-not #{page-a-uuid}]]}})
is-not-titles (set (map (fn [id] (:block/title (d/entity @conn id))) (:data is-not-result)))]
(is (= #{"Obj A"} (set is-titles)))
(is (= #{"Obj B"} is-not-titles))
(is (= #{obj-a-id} (set (:data is-result))))
(is (= #{obj-b-id} (set (:data is-not-result))))))
(deftest get-view-data-class-objects-groups-by-number-property-sorts-numerically-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:properties {:user.property/score {:logseq.property/type :number}}
:pages-and-blocks
[{:page {:block/title "A" :build/tags [:Topic]
:build/properties {:user.property/score 2}}}
{:page {:block/title "B" :build/tags [:Topic]
:build/properties {:user.property/score 10}}}
{:page {:block/title "C" :build/tags [:Topic]
:build/properties {:user.property/score 1}}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id :logseq.property.view/group-by-property :user.property/score]])
asc-groups (map first (:data (db-view/get-view-data @conn view-id
{:view-feature-type :class-objects
:view-for-id class-id})))
_ (d/transact! conn [[:db/add view-id :logseq.property.view/sort-groups-desc? true]])
desc-groups (map first (:data (db-view/get-view-data @conn view-id
{:view-feature-type :class-objects
:view-for-id class-id})))]
;; Number groups must sort numerically (1 2 10), not lexicographically (1 10 2)
(is (= [1 2 10] asc-groups))
;; "Sort groups order" (desc?) must reverse the numeric order
(is (= [10 2 1] desc-groups))))

View File

@@ -0,0 +1,59 @@
(ns logseq.db.frontend.class-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db.frontend.class :as db-class]
[logseq.db.test.helper :as db-test]))
(defn- parent-class-id [db]
(:db/id (d/entity db :user.class/Parent)))
(deftest get-class-objects-dedupes-inherited-tags-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Parent {:block/title "Parent"}
:Child {:block/title "Child"
:build/class-extends [:Parent]}}
:pages-and-blocks [{:page {:block/title "Object1"
:build/tags [:Parent :Child]}}]})
objects (db-class/get-class-objects @conn (parent-class-id @conn))
ids (map :db/id objects)]
(testing "an object tagged by both parent and child class is returned once"
(is (= 1 (count ids)))
(is (= 1 (count (distinct ids)))))))
(deftest get-class-objects-filters-hidden-objects-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Parent {:block/title "Parent"}
:Child {:block/title "Child"
:build/class-extends [:Parent]}}
:pages-and-blocks [{:page {:block/title "Visible"
:build/tags [:Child]}}
{:page {:block/title "Deleted"
:build/tags [:Child]
:logseq.property/deleted-at 1}}
{:page {:block/title "Hidden"
:build/tags [:Child]
:logseq.property/hide? true}}]})
titles (->> (db-class/get-class-objects @conn (parent-class-id @conn))
(map :block/title)
set)]
(is (= #{"Visible"} titles))))
(deftest get-class-objects-includes-hide-by-default-properties-test
(let [conn (db-test/create-conn-with-blocks
{:properties {:keywords {:logseq.property/type :default
:logseq.property/hide? true}
:author {:logseq.property/type :default}
:deleted-prop {:logseq.property/type :default}}})
deleted (d/entity @conn :user.property/deleted-prop)
_ (d/transact! conn [[:db/add (:db/id deleted) :logseq.property/deleted-at 1]])
property-class-id (:db/id (d/entity @conn :logseq.class/Property))
titles (->> (db-class/get-class-objects @conn property-class-id)
(map :block/title)
set)]
(testing "hide-by-default user properties still appear in the Property table"
(is (contains? titles "keywords"))
(is (contains? titles "author")))
(testing "deleted properties stay out of the Property table"
(is (not (contains? titles "deleted-prop"))))
(testing "private built-in properties stay out of the Property table"
(is (not (contains? titles "Property type"))))))

View File

@@ -0,0 +1,104 @@
(ns logseq.db.frontend.content-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.string :as string]
[logseq.db.frontend.content :as db-content]
[logseq.common.util.page-ref :as page-ref]))
(deftest replace-tags-with-page-refs
(testing "tags with overlapping names get replaced correctly"
(is (= "string [[foo]] string2 [[foo-bar]]"
(db-content/replace-tags-with-id-refs
"string #foo string2 #foo-bar"
[{:block/title "foo" :block/uuid "foo"}
{:block/title "foo-bar" :block/uuid "foo-bar"}])))))
(deftest title-ref->id-ref
(let [page-uuid (random-uuid)]
(is (= (str "some page ref " (page-ref/->page-ref page-uuid))
(db-content/title-ref->id-ref "some page ref [[page1]]" [{:block/title "page1" :block/uuid page-uuid}]))
"Replaces page ref name with uuid"))
(testing "uses original page name when journal title is normalized"
(let [journal-uuid #uuid "00000001-2026-0615-0000-000000000000"]
(is (= (str "some page ref " (page-ref/->page-ref journal-uuid))
(db-content/title-ref->id-ref
"some page ref [[2026-06-15]]"
[{:block/title "Jun 15th, 2026"
:block.temp/original-page-name "2026-06-15"
:block/uuid journal-uuid}])))))
(testing "ignores refs without uuid"
(is (= "some page ref [[2026-06-15]]"
(db-content/title-ref->id-ref
"some page ref [[2026-06-15]]"
[{:block/title "2026-06-15"}]))))
(testing "does replace tag with :replace-tag? true"
(is (= "some #[[5c6cd067-c602-4955-96b8-74b62e08113c]] tag"
(db-content/title-ref->id-ref "some #test tag"
[{:block/title "test" :block/uuid #uuid "5c6cd067-c602-4955-96b8-74b62e08113c"}]
{:replace-tag? true})))
(is (= "some #[[5c6cd067-c602-4955-96b8-74b62e08113c]] tag"
(db-content/title-ref->id-ref "some #[[another test]] tag"
[{:block/title "another test" :block/uuid #uuid "5c6cd067-c602-4955-96b8-74b62e08113c"}]
{:replace-tag? true}))))
(testing "does not replace tag with :replace-tag? false"
(is (= "some #test tag"
(db-content/title-ref->id-ref "some #test tag"
[{:block/title "test" :block/uuid (random-uuid)}]
{:replace-tag? false})))
(is (= "some #[[another test]] tag"
(db-content/title-ref->id-ref "some #[[another test]] tag"
[{:block/title "another test" :block/uuid (random-uuid)}]
{:replace-tag? false})))))
(deftest recur-replace-uuid-in-block-title-test
(let [uuid-a #uuid "11111111-1111-1111-1111-111111111111"
uuid-b #uuid "22222222-2222-2222-2222-222222222222"
uuid-c #uuid "33333333-3333-3333-3333-333333333333"
id-ref-a (page-ref/->page-ref uuid-a)
id-ref-b (page-ref/->page-ref uuid-b)
id-ref-c (page-ref/->page-ref uuid-c)]
(testing "replaces direct id-ref title"
(is (= "[[Direct Page]]"
(db-content/recur-replace-uuid-in-block-title
{:block/title id-ref-a
:block/refs [{:block/uuid uuid-a
:block/title "Direct Page"}]}))))
(testing "replaces recursively through nested refs"
(is (= "[[[[Leaf Page]]]]"
(db-content/recur-replace-uuid-in-block-title
{:block/title id-ref-a
:block/refs [{:block/uuid uuid-a
:block/title id-ref-b
:block/refs [{:block/uuid uuid-b
:block/title "Leaf Page"}]}]}))))
(testing "keeps hashtag style for simple page title"
(is (= "#simple-tag"
(db-content/recur-replace-uuid-in-block-title
{:block/title (str "#" id-ref-a)
:block/refs [{:block/uuid uuid-a
:block/title "simple-tag"}]}))))
(testing "keeps page-ref style hashtag when title contains spaces"
(is (= "#[[tag with space]]"
(db-content/recur-replace-uuid-in-block-title
{:block/title (str "#" id-ref-a)
:block/refs [{:block/uuid uuid-a
:block/title "tag with space"}]}))))
(testing "stops recursion when max-depth reached"
(let [result (db-content/recur-replace-uuid-in-block-title
{:block/title id-ref-a
:block/refs [{:block/uuid uuid-a
:block/title id-ref-b
:block/refs [{:block/uuid uuid-b
:block/title id-ref-c
:block/refs [{:block/uuid uuid-c
:block/title "Too Deep"}]}]}]}
1)]
(is (re-find db-content/id-ref-pattern result))
(is (not (string/includes? result "Too Deep")))))))

View File

@@ -0,0 +1,44 @@
(ns logseq.db.frontend.db-ident-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.edn :as edn]
[clojure.string :as string]
[logseq.db.frontend.db-ident :as db-ident]))
(defn- valid-edn-keyword?
"Like common-util/valid-edn-keyword? but with kw as argument"
[kw]
(try
(boolean (edn/read-string (str "{" kw " nil}")))
(catch :default _
false)))
;; These tests are copied to and should be kept in sync with logseq.db.misc-test
;; Tests edge cases as described in https://clojure.org/reference/reader#_symbols
(deftest create-db-ident-from-name
(testing "Symbols begin with a non-numeric character and can contain alphanumeric characters and *, +, !, -, _, ', ?, <, > and ="
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" "f@!{h[#"))
"Kw created from name containing invalid special characters is valid edn")
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" "foo*+!_'?<>=-"))
"Kw created from name with all valid special characters is valid edn")
(is (string/includes? (name (db-ident/create-db-ident-from-name "user.property" "foo*+!_'?<>=-"))
"*+!_'?<>=-")
"Kw created from name with all valid special characters contains all special characters")
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" "2ndCity"))
"Kw created from name starting with number is valid edn")
(is (string/starts-with? (name (db-ident/create-db-ident-from-name "user.property" "2ndCity"))
"NUM-2nd")
"Kw created from name starting with number does not start with that number"))
(testing "All other special characters"
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" ":name"))
"Kw created from name with ':' is valid edn")
(is (not (string/includes? (name (db-ident/create-db-ident-from-name "user.property" ":name")) ":"))
"Kw created from name with ':' doesn't contain ':'")
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" "foo/bar"))
"Kw created from name with '/' is valid edn")
(is (not (string/includes? (name (db-ident/create-db-ident-from-name "user.property" "foo/bar")) "/"))
"Kw created from name with '/' doesn't contain '/'")
(is (valid-edn-keyword? (db-ident/create-db-ident-from-name "user.property" "foo.bar"))
"Kw created from name with '.' is valid edn")
(is (not (string/includes? (name (db-ident/create-db-ident-from-name "user.property" "foo.bar")) "."))
"Kw created from name with '.' doesn't contain '.'")))

View File

@@ -0,0 +1,207 @@
(ns logseq.db.frontend.inputs-test
(:require [cljs-time.core :as t]
[cljs.test :refer [deftest is]]
[datascript.core :as d]
[logseq.common.util.date-time :as date-time-util]
[logseq.db.frontend.inputs :as db-inputs]
[logseq.db.frontend.rules :as rules]
[logseq.db.frontend.schema :as db-schema]
[logseq.db.sqlite.build :as sqlite-build]
[logseq.db.test.helper :as db-test]))
(defn- custom-query [db {:keys [inputs query input-options]}]
(let [q-args (cond-> (mapv #(db-inputs/resolve-input db % input-options) inputs)
(contains? (set query) '%)
(conj (rules/extract-rules rules/db-query-dsl-rules [:between])))]
(->> (apply d/q query db q-args)
(map first))))
(deftest resolve-input-for-page-and-block-inputs
(let [conn (d/create-conn db-schema/schema)
_ (d/transact! conn [{:db/ident :logseq.class/Page}])
_ (sqlite-build/create-blocks
conn
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children
[{:block/title "child 1"}
{:block/title "child 2"}]}]}])]
(is (= ["child 2" "child 1" "parent"]
(map :block/title
(custom-query @conn
{:inputs [:current-page]
:query '[:find (pull ?b [*])
:in $ ?current-page
:where [?b :block/page ?bp]
[?bp :block/name ?current-page]]
:input-options {:current-page-fn (constantly "page1")}})))
":current-page input resolves to current page name")
(is (= []
(map :block/title
(custom-query @conn
{:inputs [:current-page]
:query '[:find (pull ?b [*])
:in $ ?current-page
:where [?b :block/page ?bp]
[?bp :block/name ?current-page]]})))
":current-page input doesn't resolve when :current-page-fn not provided")
(is (= ["child 1" "child 2"]
(let [block-uuid (:block/uuid (db-test/find-block-by-content @conn "parent"))]
(map :block/title
(custom-query @conn
{:inputs [:current-block]
:query '[:find (pull ?b [*])
:in $ ?current-block
:where [?b :block/parent ?current-block]]
:input-options {:current-block-uuid block-uuid}}))))
":current-block input resolves to current block's :db/id")
(is (thrown-with-msg?
js/Error
#"Nothing found for entity"
(custom-query @conn
{:inputs [:current-block]
:query '[:find (pull ?b [*])
:in $ ?current-block
:where [?b :block/parent ?current-block]]
:input-options {:current-block-uuid nil}}))
":current-block input doesn't resolve and fails when :current-block-uuid is not provided")
(is (thrown-with-msg?
js/Error
#"Nothing found for entity"
(custom-query @conn
{:inputs [:current-block]
:query '[:find (pull ?b [*])
:in $ ?current-block
:where [?b :block/parent ?current-block]]
:input-options {:current-block-uuid :magic}}))
":current-block input doesn't resolve and fails when :current-block-uuid is invalid")
(is (= ["parent"]
(let [block-uuid (:block/uuid (db-test/find-block-by-content @conn "child 1"))]
(map :block/title
(custom-query @conn
{:inputs [:parent-block]
:query '[:find (pull ?parent-block [*])
:in $ ?parent-block
:where [?parent-block :block/parent]]
:input-options {:current-block-uuid block-uuid}}))))
":parent-block input resolves to parent of current blocks's :db/id")))
(deftest resolve-input-for-journal-date-inputs
(let [conn (db-test/create-conn-with-blocks
[{:page {:build/journal 20230101}
:blocks [{:block/title "b1"}]}
{:page {:build/journal 20230107}
:blocks [{:block/title "b2"}]}])]
(is (= ["b2"]
(with-redefs [t/today (constantly (t/date-time 2023 1 7))]
(map :block/title
(custom-query @conn
{:inputs [:3d-before :today]
:query '[:find (pull ?b [*])
:in $ ?start ?end %
:where (between ?b ?start ?end)]}))))
":Xd-before and :today resolve to correct journal range")
(is (= ["b1"]
(with-redefs [t/today (constantly (t/date-time 2022 12 31))]
(map :block/title
(custom-query @conn
{:inputs [:tomorrow :4d-after]
:query '[:find (pull ?b [*])
:in $ ?start ?end %
:where (between ?b ?start ?end)]}))))
":tomorrow and :Xd-after resolve to correct journal range")))
(defn- blocks-on-journal-page-from-block-with-content [db page-input block-content current-page-date]
(map :block/title
(custom-query db
{:inputs [page-input]
:query '[:find (pull ?b [*])
:in $ ?page
:where [?b :block/page ?e]
[?e :block/name ?page]]
:input-options
{:current-block-uuid (:block/uuid (db-test/find-block-by-content db block-content))
:current-page-fn (constantly
(date-time-util/int->journal-title (date-time-util/date->int current-page-date)
"MMM do, yyyy"))}})))
(deftest resolve-input-for-query-page
(let [current-date (t/date-time 2023 1 1)
conn (db-test/create-conn-with-blocks
[{:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
{:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
{:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}])
db @conn]
(is (= ["now"] (blocks-on-journal-page-from-block-with-content db :current-page "now" current-date))
":current-page resolves to the stateful page when called from a block on the stateful page")
(is (= ["now"] (blocks-on-journal-page-from-block-with-content db :query-page "now" current-date))
":query-page resolves to the stateful page when called from a block on the stateful page")
(is (= ["now"] (blocks-on-journal-page-from-block-with-content db :current-page "+1d" current-date))
":current-page resolves to the stateful page when called from a block on another page")
(is (= ["+1d"] (blocks-on-journal-page-from-block-with-content db :query-page "+1d" current-date))
":query-page resolves to the parent page when called from another page")))
(defn- blocks-journaled-between-inputs [db a b]
;; reverse is for sort order and may be brittle
(reverse
(map :block/title
(custom-query db
{:inputs [a b]
:query '[:find (pull ?b [*])
:in $ ?start ?end %
:where (between ?b ?start ?end)]}))))
(deftest resolve-input-for-relative-date-queries
(let [conn (db-test/create-conn-with-blocks
[{:page {:build/journal 20220101} :blocks [{:block/title "-1y"}]}
{:page {:build/journal 20221201} :blocks [{:block/title "-1m"}]}
{:page {:build/journal 20221225} :blocks [{:block/title "-1w"}]}
{:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
{:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
{:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}
{:page {:build/journal 20230108} :blocks [{:block/title "+1w"}]}
{:page {:build/journal 20230201} :blocks [{:block/title "+1m"}]}
{:page {:build/journal 20240101} :blocks [{:block/title "+1y"}]}])
db @conn]
(with-redefs [t/today (constantly (t/date-time 2023 1 1))]
(is (= ["now" "-1d" "-1w" "-1m" "-1y"] (blocks-journaled-between-inputs db :-365d :today))
":-365d and today resolve to correct journal range")
(is (= ["now" "-1d" "-1w" "-1m" "-1y"] (blocks-journaled-between-inputs db :-1y :today))
":-1y and today resolve to correct journal range")
(is (= ["now" "-1d" "-1w" "-1m"] (blocks-journaled-between-inputs db :-1m :today))
":-1m and today resolve to correct journal range")
(is (= ["now" "-1d" "-1w"] (blocks-journaled-between-inputs db :-1w :today))
":-1w and today resolve to correct journal range")
(is (= ["now" "-1d"] (blocks-journaled-between-inputs db :-1d :today))
":-1d and today resolve to correct journal range")
(is (= ["+1y" "+1m" "+1w" "+1d" "now"] (blocks-journaled-between-inputs db :today :+365d))
":+365d and today resolve to correct journal range")
(is (= ["+1y" "+1m" "+1w" "+1d" "now"] (blocks-journaled-between-inputs db :today :+1y))
":+1y and today resolve to correct journal range")
(is (= ["+1m" "+1w" "+1d" "now"] (blocks-journaled-between-inputs db :today :+1m))
":+1m and today resolve to correct journal range")
(is (= ["+1w" "+1d" "now"] (blocks-journaled-between-inputs db :today :+1w))
":+1w and today resolve to correct journal range")
(is (= ["+1d" "now"] (blocks-journaled-between-inputs db :today :+1d))
":+1d and today resolve to correct journal range")
(is (= ["+1d" "now"] (blocks-journaled-between-inputs db :today :today/+1d))
":today/+1d and today resolve to correct journal range"))))

View File

@@ -0,0 +1,42 @@
(ns logseq.db.frontend.property.type-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db.frontend.property.type :as db-property-type]
[logseq.db.test.helper :as db-test]))
(deftest asset-property-type-registered
(testing ":asset is exposed to users"
(is (some #{:asset} db-property-type/user-built-in-property-types)))
(testing ":asset is treated as a ref type"
(is (contains? db-property-type/user-ref-property-types :asset))
(is (contains? db-property-type/all-ref-property-types :asset)))
(testing ":asset validator requires a datascript db"
(is (contains? db-property-type/property-types-with-db :asset)))
(testing ":asset can be configured as multiple values"
(is (contains? db-property-type/cardinality-property-types :asset))
(is (not (contains? db-property-type/closed-value-property-types :asset))
":asset should not support closed values")))
(deftest asset-entity-validator
(testing "asset-entity? accepts blocks tagged :logseq.class/Asset and rejects others"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "plain-block"}
{:block/title "my-asset"
:build/tags [:logseq.class/Asset]}]}]})
db @conn
asset-block (db-test/find-block-by-content db "my-asset")
plain-block (db-test/find-block-by-content db "plain-block")
;; asset-entity? is private; reach through the registered schema
asset-fn (last (:asset db-property-type/built-in-validation-schemas))]
(is (some? asset-block))
(is (some? plain-block))
(testing "asset block validates"
(is (true? (boolean (asset-fn db (:db/id asset-block))))))
(testing "non-asset block does not validate"
(is (not (asset-fn db (:db/id plain-block)))))
(testing "nonexistent id does not validate"
(is (not (asset-fn db 9999999)))))))

View File

@@ -0,0 +1,89 @@
(ns logseq.db.frontend.property-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db.frontend.property :as db-property]))
(deftest sort-properties
(let [p1 {:db/id 1, :block/order "a", :block/uuid "uuid-a"}
p2 {:db/id 2, :block/order "b", :block/uuid "uuid-b"}
p3 {:db/id 3, :block/order nil, :block/uuid "uuid-d"}
p4 {:db/id 4, :block/order "b", :block/uuid "uuid-c"}
p5 {:db/id 5, :block/order nil, :block/uuid "uuid-e"}]
(is (= [p1 p2 p4 p3 p5]
(db-property/sort-properties [p3 p1 p5 p2 p4])))))
(deftest normalize-block-order-tx-data-test
(testing "Generate appropriate :block/order values for sorted-blocks with :block/order value = nil or duplicated"
(let [p1 {:db/id 1, :block/order "a0"}
p2 {:db/id 2, :block/order "bbb"}
p3 {:db/id 3, :block/order "bbb"}
p4 {:db/id 4, :block/order nil}
p5 {:db/id 5, :block/order nil}
sorted-entities [p1 p2 p3 p4 p5]
tx-data (db-property/normalize-sorted-entities-block-order sorted-entities)
;; apply tx-data to entities
tx-map (into {} (map (juxt :db/id identity) tx-data))
updated-entities (map (fn [ent]
(if-let [tx (get tx-map (:db/id ent))]
(merge ent tx)
ent))
sorted-entities)
;; sort again and test
final-sorted (db-property/sort-properties updated-entities)]
(is (= 5 (count final-sorted)))
;; Check that all orders are now strings
(is (every? string? (map :block/order final-sorted)))
;; Check that all orders are unique
(is (= 5 (count (set (map :block/order final-sorted)))))
;; Check that the final list is sorted correctly by the new orders
(is (= final-sorted (sort-by :block/order final-sorted)))))
(testing "No changes needed for already valid orders"
(let [p1 {:db/id 1, :block/order "b00"}
p2 {:db/id 2, :block/order "b01"}
sorted-entities [p1 p2]
tx-data (db-property/normalize-sorted-entities-block-order sorted-entities)]
(is (empty? tx-data)))))
(deftest reaction-built-in-properties
(let [props db-property/built-in-properties]
(testing "entries exist"
(is (contains? props :logseq.property.reaction/emoji-id))
(is (contains? props :logseq.property.reaction/target)))
(testing "schema types"
(is (= :string (get-in props [:logseq.property.reaction/emoji-id :schema :type])))
(is (= :node (get-in props [:logseq.property.reaction/target :schema :type]))))
(testing "internal visibility"
(is (= false (get-in props [:logseq.property.reaction/emoji-id :schema :public?])))
(is (= false (get-in props [:logseq.property.reaction/target :schema :public?])))
(is (= true (get-in props [:logseq.property.reaction/emoji-id :schema :hide?])))
(is (= true (get-in props [:logseq.property.reaction/target :schema :hide?]))))
(testing "logseq property namespace"
(is (db-property/logseq-property? :logseq.property.reaction/emoji-id))
(is (db-property/logseq-property? :logseq.property.reaction/target)))))
(deftest comments-built-in-properties
(let [props db-property/built-in-properties
property :logseq.property.comments/blocks]
(is (contains? props property))
(is (= "Commented blocks" (get-in props [property :title])))
(is (= :node (get-in props [property :schema :type])))
(is (= :many (get-in props [property :schema :cardinality])))
(is (= false (get-in props [property :schema :public?])))
(is (= true (get-in props [property :schema :hide?])))
(is (db-property/logseq-property? property))))
(deftest assignee-built-in-property
(let [property (get db-property/built-in-properties :logseq.property/assignee)]
(testing "schema"
(is (= "Assignee" (:title property)))
(is (= :node (get-in property [:schema :type])))
(is (= :many (get-in property [:schema :cardinality])))
(is (= true (get-in property [:schema :public?]))))
(testing "queryable built-in logseq property"
(is (= true (:queryable? property)))
(is (contains? db-property/public-built-in-properties :logseq.property/assignee))
(is (db-property/logseq-property? :logseq.property/assignee)))))

View File

@@ -0,0 +1,21 @@
(ns logseq.db.frontend.reaction-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.common.util :as common-util]
[logseq.db.frontend.validate :as db-validate]
[logseq.db.test.helper :as db-test]))
(deftest reaction-entity-valid
(testing "reaction entity passes db validation"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Block"}]}]})
block (db-test/find-block-by-content @conn "Block")
now (common-util/time-ms)
reaction {:block/uuid (random-uuid)
:block/created-at now
:logseq.property.reaction/emoji-id "+1"
:logseq.property.reaction/target (:db/id block)}]
(d/transact! conn [reaction])
(is (empty? (:errors (db-validate/validate-local-db! @conn)))))))

View File

@@ -0,0 +1,266 @@
(ns logseq.db.frontend.rules-test
(:require [cljs.test :refer [deftest is testing are]]
[datascript.core :as d]
[logseq.db.frontend.rules :as rules]
[logseq.db.test.helper :as db-test]))
(defn q-with-rules [query db]
;; query assumes no :in given
(d/q (into query [:in '$ '%])
db
(rules/extract-rules rules/db-query-dsl-rules)))
(deftest get-full-deps
(let [property-value-deps #{:ref->val :class-extends :object-has-class-property :property-missing-value :ref-property-value :ref-property-value-with-default}
property-deps (conj property-value-deps :ref-property-with-default)
task-deps (conj property-deps :task)
priority-deps (conj property-deps :priority)
task-priority-deps (into priority-deps task-deps)]
(are [x y] (= y (#'rules/get-full-deps x rules/rules-dependencies))
[:ref-property-value-with-default] property-value-deps
[:ref-property-with-default] property-deps
[:task] task-deps
[:priority] priority-deps
[:task :priority] task-priority-deps)))
(deftest has-property-rule
(let [conn (db-test/create-conn-with-blocks
{:properties {:foo {:logseq.property/type :default}
:foo2 {:logseq.property/type :default}}
:pages-and-blocks
[{:page {:block/title "Page1"
:build/properties {:foo "bar"}}}]})]
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo)]
@conn)
(map (comp :block/title first))))
"has-property returns result when block has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo2)]
@conn)
(map (comp :block/title first))))
"has-property returns no result when block doesn't have property")
(is (= [:block/tags :user.property/foo]
(q-with-rules '[:find [?p ...]
:where (has-property ?b ?p) [?b :block/title "Page1"]]
@conn))
"has-property can bind to property arg")))
(deftest ref-property-rule
(let [conn (db-test/create-conn-with-blocks
{:properties {:foo {:logseq.property/type :default}
:foo2 {:logseq.property/type :default}
:number-many {:logseq.property/type :number
:db/cardinality :many}
:page-many {:logseq.property/type :node
:db/cardinality :many}}
:pages-and-blocks
[{:page {:block/title "Page1"
:build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:build/page {:block/title "Page A"}]}}}}
{:page {:block/title "Page A"
:build/properties {:foo "bar A"}}}]})]
(testing "cardinality :one property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (ref-property ?b :user.property/foo "bar")]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (ref-property ?b :user.property/foo "baz")]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value")
(is (= #{:user.property/foo}
(->> (q-with-rules '[:find [?p ...]
:where (ref-property ?b ?p "bar") [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with bound property value"))
(testing "cardinality :many property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (ref-property ?b :user.property/number-many 5)]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (ref-property ?b :user.property/number-many 20)]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value")
(is (= #{:user.property/number-many}
(->> (q-with-rules '[:find [?p ...]
:where (ref-property ?b ?p 5) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with bound property value"))
;; NOTE: Querying a ref's name is different than before and requires more than just the rule
(testing ":ref property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where (ref-property ?b :user.property/page-many "Page A")]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value"))
(testing "bindings with property value"
(is (= #{:user.property/foo :user.property/number-many :user.property/page-many :block/tags}
(->> (q-with-rules '[:find [?p ...]
:where (ref-property ?b ?p _) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with unbound property value")
(is (= #{[:user.property/number-many 10]
[:user.property/number-many 5]
[:user.property/foo "bar"]
[:user.property/page-many "Page A"]
[:block/tags "Page"]}
(->> (q-with-rules '[:find ?p ?val
:where (ref-property ?b ?p ?val) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property and property value args")
(is (= #{"Page1"}
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where
[?b :user.property/page-many ?pv]
(ref-property ?pv :user.property/foo "bar A")]
@conn)
(map (comp :block/title first))
set))
"property can be used multiple times to query a property value's property"))))
(deftest property-rule
(let [conn (db-test/create-conn-with-blocks
{:properties {:foo {:logseq.property/type :default}
:foo2 {:logseq.property/type :default}
:number-many {:logseq.property/type :number
:db/cardinality :many}
:page-many {:logseq.property/type :node
:db/cardinality :many}}
:pages-and-blocks
[{:page {:block/title "Page1"
:build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:build/page {:block/title "Page A"}]}}}}
{:page {:block/title "Page A"
:build/properties {:foo "bar A"}}}]})]
(testing "cardinality :one property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "bar")]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "baz")]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value")
(is (= #{:user.property/foo}
(->> (q-with-rules '[:find [?p ...]
:where (property ?b ?p "bar") [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with bound property value"))
(testing "cardinality :many property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 5)]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 20)]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value")
(is (= #{:user.property/number-many}
(->> (q-with-rules '[:find [?p ...]
:where (property ?b ?p 5) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with bound property value"))
;; NOTE: Querying a ref's name is different than before and requires more than just the rule
(testing ":ref property"
(is (= ["Page1"]
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where (property ?b :user.property/page-many "Page A")]
@conn)
(map (comp :block/title first))))
"property returns result when page has property")
(is (= []
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
@conn)
(map (comp :block/title first))))
"property returns no result when page doesn't have property value"))
(testing "bindings with property value"
(is (= #{:user.property/foo :user.property/number-many :user.property/page-many :block/tags}
(->> (q-with-rules '[:find [?p ...]
:where (property ?b ?p _) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property arg with unbound property value")
(is (= #{[:user.property/number-many 10]
[:user.property/number-many 5]
[:user.property/foo "bar"]
[:user.property/page-many "Page A"]
[:block/tags "Page"]}
(->> (q-with-rules '[:find ?p ?val
:where (property ?b ?p ?val) [?b :block/title "Page1"]]
@conn)
set))
"property can bind to property and property value args")
(is (= #{"Page1"}
(->> (q-with-rules '[:find (pull ?b [:block/title])
:where
[?b :user.property/page-many ?pv]
(property ?pv :user.property/foo "bar A")]
@conn)
(map (comp :block/title first))
set))
"property can be used multiple times to query a property value's property"))))
(deftest tags-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:block/title "Page1"
:build/tags [:Person]}}
{:page {:block/title "Page2"
:build/tags [:Person]}}
{:page {:block/title "Page3"
:build/tags [:Employee]}}]})
person-eid (:db/id (d/entity @conn :user.class/Person))]
(d/transact! conn [{:db/ident :user.class/Employee
:logseq.property.class/extends :user.class/Person}])
(testing "tags query with eid"
(is (= #{"Page1" "Page2" "Page3"}
(->> (d/q
'[:find (pull ?b [:block/title])
:in $ % ?tag-ids
:where (tags ?b ?tag-ids)]
@conn
(rules/extract-rules rules/db-query-dsl-rules)
#{person-eid})
(map (comp :block/title first))
set))))
(testing "tags query with db/ident"
(is (= #{"Page1" "Page2" "Page3"}
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (tags ?b #{:user.class/Person})]
@conn)
(map (comp :block/title first))
set))))
(testing "tags query with block/title"
(is (= #{"Page1" "Page2" "Page3"}
(->> (q-with-rules '[:find (pull ?b [:block/title]) :where (tags ?b #{"Person"})]
@conn)
(map (comp :block/title first))
set))))))

View File

@@ -0,0 +1,342 @@
(ns logseq.db.sqlite.build-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.common.util.page-ref :as page-ref]
[logseq.db :as ldb]
[logseq.db.frontend.entity-util :as entity-util]
[logseq.db.frontend.property :as db-property]
[logseq.db.sqlite.build :as sqlite-build]
[logseq.db.sqlite.export :as sqlite-export]
[logseq.db.test.helper :as db-test]))
(deftest build-tags
(let [conn (db-test/create-conn)
_ (sqlite-build/create-blocks
conn
[{:page {:block/title "page1"}
:blocks [{:block/title "Jrue Holiday" :build/tags [:Person]}
{:block/title "some task" :build/tags [:logseq.class/Task]}]}
{:page {:block/title "Jayson Tatum" :build/tags [:Person]}}])]
(is (= [:user.class/Person]
(mapv :db/ident (:block/tags (db-test/find-block-by-content @conn "Jrue Holiday"))))
"Person class is created and correctly associated to a block")
(is (contains?
(set (map :db/ident (:block/tags (db-test/find-page-by-title @conn "Jayson Tatum"))))
:user.class/Person)
"Person class is created and correctly associated to a page")
(is (= [:logseq.class/Task]
(mapv :db/ident (:block/tags (db-test/find-block-by-content @conn "some task"))))
"Built-in class is associatedly correctly")))
(deftest build-properties-user
(let [conn (db-test/create-conn)
_ (sqlite-build/create-blocks
conn
[{:page {:block/title "page1"}
:blocks [{:block/title "Jrue Holiday" :build/properties {:description "Clutch defense"}}]}
{:page {:block/title "Jayson Tatum" :build/properties {:description "Awesome selfless basketball"}}}])]
(is (= "Clutch defense"
(->> (db-test/find-block-by-content @conn "Jrue Holiday")
:user.property/description
db-property/property-value-content))
"description property is created and correctly associated to a block")
(is (= "Awesome selfless basketball"
(->> (db-test/find-page-by-title @conn "Jayson Tatum")
:user.property/description
db-property/property-value-content))
"description property is created and correctly associated to a page")))
(deftest build-properties-built-in
(let [conn (db-test/create-conn)
_ (sqlite-build/create-blocks
conn
[{:page {:block/title "page1"}
:blocks [{:block/title "some todo"
:build/properties {:logseq.property/status :logseq.property/status.doing}}
{:block/title "rojo"
:build/properties {:logseq.property/background-color "red"}}]}])]
(is (= :logseq.property/status.doing
(->> (db-test/find-block-by-content @conn "some todo")
:logseq.property/status
:db/ident))
"built-in property with closed value is created and correctly associated to a block")
(is (= "red"
(->> (db-test/find-block-by-content @conn "rojo")
:logseq.property/background-color
db-property/property-value-content))
"built-in :default property is created and correctly associated to a block")))
(deftest build-properties-with-build-page
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"
:build/properties
{:date [:build/page {:build/journal 20250223}]
:page #{[:build/page {:block/title "page object"
:build/properties {:p1 "foo"
:date [:build/page {:build/journal 20250224}]}}]}}}}])]
(is (= "foo"
(->> (db-test/find-page-by-title @conn "page object")
:user.property/p1
db-property/property-value-content))
":build/page page can have a :default property")
(is (= 20250224
(->> (db-test/find-page-by-title @conn "page object")
:user.property/date
:block/journal-day))
":build/page page can have a :date property defined by another :build/page")))
(deftest build-for-existing-blocks
(let [conn (db-test/create-conn)
_ (sqlite-build/create-blocks
conn
{:properties {:p1 {}}
:classes {:MyClass {}}
:pages-and-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "block 1"}
{:block/title "block 2"}]}]})
block (db-test/find-block-by-content @conn "block 1")
block2 (db-test/find-block-by-content @conn "block 2")
page1 (db-test/find-page-by-title @conn "page1")
built-in-page (db-test/find-page-by-title @conn "Quick add")
{:keys [init-tx block-props-tx]}
(sqlite-build/build-blocks-tx
{:pages-and-blocks [{:page
{:block/uuid (:block/uuid built-in-page)
:build/keep-uuid? true
:build/properties {:logseq.property/description "foo"}
:block/title "Quick add"}
:blocks []}
{:page (select-keys (:block/page block) [:block/uuid])
:blocks [(merge {:block/title "imported task" :block/uuid (:block/uuid block)}
{:build/properties {:logseq.property/status :logseq.property/status.todo}
:build/tags [:logseq.class/Task]})]}]
:build-existing-tx? true})
_ (d/transact! conn init-tx)
_ (d/transact! conn block-props-tx)
updated-block (d/entity @conn [:block/uuid (:block/uuid block)])
{init-tx2 :init-tx block-props-tx2 :block-props-tx :as _tx}
(sqlite-build/build-blocks-tx
{:pages-and-blocks [{:page (select-keys (:block/page block2) [:block/uuid])
:blocks [(merge {:block/title "imported block" :block/uuid (:block/uuid block2)}
{:build/properties {:user.property/p1 "foo"}
:build/tags [:user.class/MyClass]})]}]
:properties {:user.property/p1 (select-keys (d/entity @conn :user.property/p1)
[:logseq.property/type :block/uuid])}
:build-existing-tx? true})
_ (d/transact! conn init-tx2)
_ (d/transact! conn block-props-tx2)
updated-block2 (d/entity @conn [:block/uuid (:block/uuid block2)])]
;; (cljs.pprint/pprint _tx)
(testing "existing page cases"
(is (= (:block/updated-at page1)
(:block/updated-at (db-test/find-page-by-title @conn "page1")))
"Existing page with no property changes didn't get updated")
(is (not= (:block/updated-at built-in-page)
(:block/updated-at (db-test/find-page-by-title @conn "Quick add")))
"Existing page with property changes does get updated"))
(testing "block with built-in properties and tags"
(is (= []
(filter #(and (not= (:block/uuid %) (:block/uuid built-in-page))
(or (:db/id %) (:db/ident %)))
(concat init-tx block-props-tx)))
"Tx doesn't try to create new blocks or modify existing idents")
(is (= "imported task" (:block/title updated-block)))
(is (= {:block/tags [:logseq.class/Task]
:logseq.property/status :logseq.property/status.todo}
(db-test/readable-properties updated-block))
"Block's properties and tags are updated"))
(testing "block with existing user properties and tags"
(is (= "imported block" (:block/title updated-block2)))
(is (= {:block/tags [:user.class/MyClass]
:user.property/p1 "foo"}
(db-test/readable-properties updated-block2))
"Block's properties and tags are updated"))))
(deftest build-blocks-with-refs
(let [block-uuid (random-uuid)
class-uuid (random-uuid)
page-uuid (random-uuid)
property-uuid (random-uuid)
conn (db-test/create-conn-with-blocks
{:classes {:C1 {:block/uuid class-uuid :build/keep-uuid? true}}
:properties {:p1 {:block/uuid property-uuid :build/keep-uuid? true}}
:build-existing-tx? true
:pages-and-blocks
[{:page {:block/title "page 1"}
:blocks [{:block/title "named page ref to [[named page]]"}
{:block/title (str "page ref to " (page-ref/->page-ref page-uuid))}
{:block/title (str "block ref to " (page-ref/->page-ref block-uuid))}
{:block/title (str "class ref to " (page-ref/->page-ref class-uuid))}
{:block/title (str "inline class ref to #" (page-ref/->page-ref class-uuid))}
{:block/title (str "property ref to " (page-ref/->page-ref property-uuid))}
{:block/title "hi" :block/uuid block-uuid :build/keep-uuid? true}]}
{:page {:block/title "another page" :block/uuid page-uuid :build/keep-uuid? true}}]})
block-with-named-page-ref (db-test/find-block-by-content @conn #"^named page ref")
block-with-page-ref (db-test/find-block-by-content @conn #"^page ref")
block-with-class-ref (db-test/find-block-by-content @conn #"^class ref")
block-with-inline-class-ref (db-test/find-block-by-content @conn #"^inline class ref")
block-with-property-ref (db-test/find-block-by-content @conn #"^property ref")
block-with-block-ref (db-test/find-block-by-content @conn #"^block ref")]
(is (ldb/internal-page? (db-test/find-page-by-title @conn "named page")))
(is (contains? (:block/refs block-with-named-page-ref) (db-test/find-page-by-title @conn "named page")))
(is (= page-uuid (:block/uuid (db-test/find-page-by-title @conn "another page"))))
(is (contains? (:block/refs block-with-page-ref) (db-test/find-page-by-title @conn "another page")))
(is (= class-uuid (:block/uuid (d/entity @conn :user.class/C1))))
(is (contains? (:block/refs block-with-class-ref) (d/entity @conn :user.class/C1)))
(is (contains? (:block/refs block-with-inline-class-ref) (d/entity @conn :user.class/C1)))
(is (= property-uuid (:block/uuid (d/entity @conn :user.property/p1))))
(is (contains? (:block/refs block-with-property-ref) (d/entity @conn :user.property/p1)))
(is (= block-uuid (:block/uuid (db-test/find-block-by-content @conn "hi"))))
(is (contains? (:block/refs block-with-block-ref) (db-test/find-block-by-content @conn "hi")))))
(deftest build-class-and-property-pages
(let [class-uuid (random-uuid)
property-uuid (random-uuid)
conn (db-test/create-conn-with-blocks
{:classes {:C1 {:block/uuid class-uuid :build/keep-uuid? true}}
:properties {:p1 {:block/uuid property-uuid :build/keep-uuid? true}}
:pages-and-blocks
[{:page {:block/uuid class-uuid}
:blocks [{:block/title "b1"
:build/children [{:block/title "b2"}]}]}
{:page {:block/uuid property-uuid}
:blocks [{:block/title "b3"
:build/children [{:block/title "b4"}]}]}]
:build-existing-tx? true})]
(is (= ["b1" "b2"]
(->> (d/q '[:find [?b ...] :in $ ?page-id :where [?b :block/page ?page-id]]
@conn [:block/uuid class-uuid])
(map #(:block/title (d/entity @conn %)))))
"Class page has correct blocks")
(is (= ["b3" "b4"]
(->> (d/q '[:find [?b ...] :in $ ?page-id :where [?b :block/page ?page-id]]
@conn [:block/uuid property-uuid])
(map #(:block/title (d/entity @conn %)))))
"Property page has correct blocks")))
(deftest build-class-extends-rejects-cycles
(testing "self cycle"
(is (thrown-with-msg?
js/Error
#"Cycle detected in :build/class-extends"
(sqlite-build/build-blocks-tx
{:classes {:user.class/A {:build/class-extends [:user.class/A]}}}))))
(testing "deprecated class parent self cycle"
(is (thrown-with-msg?
js/Error
#"Cycle detected in :build/class-extends"
(sqlite-build/build-blocks-tx
{:classes {:user.class/A {:build/class-parent :user.class/A}}}))))
(testing "deprecated class parent takes precedence"
(let [txs (sqlite-build/build-blocks-tx
{:classes {:user.class/A {:build/class-parent :user.class/B
:build/class-extends [:user.class/A]}
:user.class/B {}}})
class-a (some #(when (= :user.class/A (:db/ident %)) %) (:init-tx txs))
class-b (some #(when (= :user.class/B (:db/ident %)) %) (:init-tx txs))]
(is (= [(:db/id class-b)]
(:logseq.property.class/extends class-a)))))
(testing "multi-class cycle"
(is (thrown-with-msg?
js/Error
#"Cycle detected in :build/class-extends"
(sqlite-build/build-blocks-tx
{:classes {:user.class/A {:build/class-extends [:user.class/B]}
:user.class/B {:build/class-extends [:user.class/C]}
:user.class/C {:build/class-extends [:user.class/A]}}}))))
(testing "diamond inheritance without a cycle"
(is (map?
(sqlite-build/build-blocks-tx
{:classes {:user.class/A {}
:user.class/B {:build/class-extends [:user.class/A]}
:user.class/C {:build/class-extends [:user.class/A]}
:user.class/D {:build/class-extends [:user.class/B :user.class/C]}}})))))
(deftest property-value-with-properties-and-tags
(let [conn (db-test/create-conn-with-blocks
{:properties {:p1 {:logseq.property/type :default}}
:classes {:C1 {}}
:pages-and-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "block has pvalue with built-in tag"
:build/properties
{:p1 {:build/property-value :block
:block/title "t1"
:build/tags [:logseq.class/Task]}}}
{:block/title "block has pvalue with user tag"
:build/properties
{:p1 {:build/property-value :block
:block/title "u1"
:build/tags [:C1]}}}
{:block/title "Todo query",
:build/tags [:logseq.class/Query],
:build/properties
{:logseq.property/query
{:build/property-value :block
:block/title "{:query (task Todo)}"
:build/properties
{:logseq.property.code/lang "clojure"
:logseq.property.node/display-type :code}}}}]}]})]
(is (= {:logseq.property.node/display-type :code
:logseq.property.code/lang "clojure"}
(-> (db-test/find-block-by-content @conn "{:query (task Todo)}")
db-test/readable-properties
(dissoc :logseq.property/created-from-property))))
(is (= {:block/tags [:logseq.class/Task]}
(-> (db-test/find-block-by-content @conn "t1")
db-test/readable-properties
(dissoc :logseq.property/created-from-property))))
(is (= {:block/tags [:user.class/C1]}
(-> (db-test/find-block-by-content @conn "u1")
db-test/readable-properties
(dissoc :logseq.property/created-from-property))))))
(deftest build-ontology-with-multiple-namespaces
(let [conn (db-test/create-conn-with-blocks
{:properties {:user.property/p1 {:logseq.property/type :default}
:other.property/p1 {:logseq.property/type :default}}
:classes {:user.class/C1 {}
:other.class/C1 {}}})]
(is (entity-util/property? (d/entity @conn :user.property/p1)))
(is (entity-util/property? (d/entity @conn :other.property/p1)))
(is (entity-util/class? (d/entity @conn :user.class/C1)))
(is (entity-util/class? (d/entity @conn :other.class/C1)))))
(deftest build-preserves-class-property-ordering-for-export
(let [class-properties-c1 [:user.property/p2 :user.property/p1 :user.property/p3]
class-properties-c2 [:user.property/p4 :user.property/p2 :user.property/p3]
another-class-properties-c1 [:user.property/p5]
another-class-properties-c2 [:user.property/p6]
another-class-properties-c3 [:user.property/p6 :user.property/p5]
conn (db-test/create-conn-with-blocks
{:properties {:user.property/p1 {:logseq.property/type :default}
:user.property/p2 {:logseq.property/type :default}
:user.property/p3 {:logseq.property/type :default}
:user.property/p4 {:logseq.property/type :default}
:user.property/p5 {:logseq.property/type :default}
:user.property/p6 {:logseq.property/type :default}}
:classes {:user.class/C1 {:build/class-properties class-properties-c1}
:user.class/C2 {:build/class-properties class-properties-c2}
:user.class/AnotherC1 {:build/class-properties another-class-properties-c1}
:user.class/AnotherC2 {:build/class-properties another-class-properties-c2}
:user.class/AnotherC3 {:build/class-properties another-class-properties-c3}}})
export-map (sqlite-export/build-export @conn {:export-type :graph-ontology})]
(is (= class-properties-c1
(get-in export-map [:classes :user.class/C1 :build/class-properties])))
(is (= class-properties-c2
(get-in export-map [:classes :user.class/C2 :build/class-properties])))
(is (= another-class-properties-c3
(get-in export-map [:classes :user.class/AnotherC3 :build/class-properties]))
"Later class-level ordering constraint :p6 before :p5 is preserved")))

View File

@@ -0,0 +1,173 @@
(ns logseq.db.sqlite.create-graph-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.data :as data]
[clojure.set :as set]
[clojure.string :as string]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db.frontend.class :as db-class]
[logseq.db.frontend.property :as db-property]
[logseq.db.frontend.schema :as db-schema]
[logseq.db.frontend.validate :as db-validate]
[logseq.db.sqlite.build :as sqlite-build]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.db.test.helper :as db-test]))
(deftest new-graph-db-idents
(testing "a new graph follows :db/ident conventions for"
(let [conn (db-test/create-conn)
ident-ents (->> (d/q '[:find [?b ...]
:where [?b :db/ident]]
@conn)
(map (fn [id] (d/entity @conn id))))
default-idents (map :db/ident ident-ents)]
(is (> (count default-idents) 45)
"Approximate number of default idents is correct")
(testing "namespaces"
(is (= '() (remove namespace default-idents))
"All default :db/ident's have namespaces")
(is (= []
(->> (remove db-property/db-attribute-properties default-idents)
(keep namespace)
(remove #(string/starts-with? % "logseq"))))
"All default :db/ident namespaces start with logseq."))
(testing "closed values"
(let [closed-value-ents (filter #(string/includes? (name (:db/ident %)) ".") ident-ents)
closed-value-properties (->> closed-value-ents
(map :db/ident)
(map #(keyword (namespace %) (string/replace (name %) #".[^.]+$" "")))
set)]
(is (= []
(remove ldb/closed-value? closed-value-ents))
"All property names that contain a '.' are closed values")
(is (= #{}
(set/difference
(set (remove #{:logseq.property/color} closed-value-properties))
(set default-idents)))
"All closed values start with a prefix that is a property name"))))))
(deftest new-graph-marks-built-ins
(let [conn (db-test/create-conn)
idents (->> (d/q '[:find [(pull ?b [:db/ident :logseq.property/built-in?]) ...]
:where [?b :db/ident]]
@conn)
;; only kv's and empty property value aren't marked because
;; they aren't user facing
(remove #(or (= "logseq.kv" (namespace (:db/ident %)))
(= :logseq.property/empty-placeholder (:db/ident %)))))
pages (d/q '[:find [(pull ?b [:logseq.property/built-in? :block/title]) ...]
:where [?b :block/tags :logseq.class/Page]]
@conn)]
(is (= [] (remove :logseq.property/built-in? idents))
"All entities with :db/ident have built-in property (except for kv idents)")
(is (= [] (remove :logseq.property/built-in? pages))
"All default internal pages should have built-in property")))
(deftest new-graph-creates-class
(let [conn (db-test/create-conn)
task (d/entity @conn :logseq.class/Task)]
(is (ldb/class? task)
"Task class has correct type")
(is (= 4 (count (:logseq.property.class/properties task)))
"Has correct number of task properties")
(is (every? ldb/property? (:logseq.property.class/properties task))
"Each task property has correct type")))
(deftest new-graph-initializes-default-classes-correctly
(let [conn (db-test/create-conn)]
(is (= (set (keys db-class/built-in-classes))
(set (map #(:db/ident (d/entity @conn (:e %))) (d/datoms @conn :avet :block/tags :logseq.class/Tag))))
"All built-in classes are indexed by :block/tags with :logseq.class/Tag")
(is (= (count (dissoc db-class/built-in-classes :logseq.class/Root))
(count (->> (d/datoms @conn :avet :block/tags :logseq.class/Tag)
(map #(d/entity @conn (:e %)))
(mapcat :logseq.property.class/_extends)
set)))
"Reverse lookup of :logseq.property.class/extends correctly fetches number of child classes")))
(deftest new-graph-initializes-default-properties-correctly
(let [conn (db-test/create-conn)]
(is (= (set (keys db-property/built-in-properties))
(set (map #(:db/ident (d/entity @conn (:e %))) (d/datoms @conn :avet :block/tags :logseq.class/Property))))
"All built-in properties are indexed by :block/tags with :logseq.class/Property")
(is (= (set (keys db-property/built-in-properties))
(set (map #(:db/ident (d/entity @conn (:e %))) (d/datoms @conn :avet :logseq.property/type))))
"All built-in properties have and are indexed by :logseq.property/type")
(is (= #{}
(set/difference
(set (keys db-property/built-in-properties))
(set (map #(:db/ident (d/entity @conn (:e %))) (d/datoms @conn :avet :logseq.property/built-in?)))))
"All built-in properties have and are indexed by :logseq.property/built-in?")
;; testing :properties config
(testing "A built-in property that has"
(is (= :logseq.property/status.todo
(-> (d/entity @conn :logseq.property/status)
:logseq.property/default-value
:db/ident))
"A property with a :db/ident property value is created correctly")
(is (-> (d/entity @conn :logseq.property/deadline)
:logseq.property/description
db-property/property-value-content
str
(string/includes? "finish something"))
"A :default property is created correctly")
(is (= true
(-> (d/entity @conn :logseq.property/status)
:logseq.property/enable-history?))
"A :checkbox property is created correctly")
(is (= 1
(-> (d/entity @conn :logseq.property.repeat/recur-frequency)
:logseq.property/default-value
db-property/property-value-content))
"A numeric property is created correctly"))))
(deftest new-graph-is-valid
(let [conn (db-test/create-conn)
validation (db-validate/validate-local-db! @conn)]
;; For debugging
;; (println (count (:errors validation)) "errors of" (count (:entities validation)))
;; (cljs.pprint/pprint (:errors validation))
(is (empty? (map :entity (:errors validation)))
"New graph has no validation errors")))
(deftest property-types
(let [conn (d/create-conn db-schema/schema)
_ (d/transact! conn (sqlite-create-graph/build-db-initial-data
(pr-str {:macros {"docs-base-url" "https://docs.logseq.com/#/page/$1"}})))]
(testing ":url property"
(sqlite-build/create-blocks
conn
{:properties {:url {:logseq.property/type :url}}
:pages-and-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "b1" :build/properties {:url "https://logseq.com"}}
;; :url macros are used for consistently building urls with the same hostname e.g. docs graph
{:block/title "b2" :build/properties {:url "{{docs-base-url test}}"}}]}]})
(is (empty? (map :entity (:errors (db-validate/validate-local-db! @conn))))
"Graph with different :url blocks has no validation errors"))))
(deftest build-db-initial-data-test
(testing "idempotent initial-data"
(letfn [(remove-ignored-attrs&entities [init-data]
(let [ignored-idents #{:logseq.kv/graph-created-at :logseq.kv/graph-uuid :logseq.kv/local-graph-uuid}
init-data* (remove (fn [ent]
(contains? ignored-idents (:db/ident ent)))
init-data)]
(map (fn [ent] (dissoc ent
:block/created-at :block/updated-at
:file/last-modified-at :file/created-at
:block/order ;; TODO: block/order should be same as well
))
init-data*)))]
(let [[first-only second-only _common]
(data/diff (remove-ignored-attrs&entities (sqlite-create-graph/build-db-initial-data ""))
(remove-ignored-attrs&entities (sqlite-create-graph/build-db-initial-data "")))]
;; (pr-str [first-only second-only _common])
(is (and (every? nil? first-only)
(every? nil? second-only)))))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
(ns logseq.db.sqlite.gc-test
(:require ["fs" :as fs]
["path" :as node-path]
[cljs.test :refer [deftest async use-fixtures is testing]]
[datascript.core :as d]
[logseq.db.common.sqlite-cli :as sqlite-cli]
[logseq.db.sqlite.debug :as sqlite-debug]
[logseq.db.sqlite.gc :as sqlite-gc]))
(use-fixtures
:each
;; Cleaning tmp/ before leaves last tmp/ after a test run for dev and debugging
{:before
#(async done
(if (fs/existsSync "tmp")
(fs/rm "tmp" #js {:recursive true} (fn [err]
(when err (js/console.log err))
(done)))
(done)))})
(defn- create-graph-dir
[dir db-name]
(fs/mkdirSync (node-path/join dir db-name) #js {:recursive true}))
(deftest ^:long gc-kvs-table-test
(testing "Create a datascript db, gc it and ensure there's no missing addrs and garbage addrs"
(create-graph-dir "tmp/graphs" "test-db")
(let [{:keys [conn sqlite]} (sqlite-cli/open-sqlite-datascript! "tmp/graphs" "test-db")
tx-data (map (fn [i] {:block/uuid (random-uuid)
:block/title (str "title " i)})
(range 0 500000))]
(println "DB start transacting")
(d/transact! conn tx-data)
(println "DB transacted")
(let [non-ordered-tx (->> (shuffle tx-data)
(take 100000)
(map (fn [block] [:db/retractEntity [:block/uuid (:block/uuid block)]])))]
(d/transact! conn non-ordered-tx))
(println "gc time")
;; `true` to walk addresses and `false` to recursively run gc
(time (sqlite-gc/gc-kvs-table-node-version! sqlite false))
;; ensure there's no missing address (broken db)
(is (empty? (sqlite-debug/find-missing-addresses-node-version sqlite))
"Found missing addresses!")
(is (true? (sqlite-gc/ensure-no-garbage sqlite))
"Found garbage addresses!"))))

View File

@@ -0,0 +1,6 @@
(ns logseq.db.test-runner
"Test runner which enables https://github.com/PEZ/baldr by default"
(:require [nextjournal.test-runner :as next-runner]
[pez.baldr]))
(def -main next-runner/-main)

376
deps/db/test/logseq/db_test.cljs vendored Normal file
View File

@@ -0,0 +1,376 @@
(ns logseq.db-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[datascript.storage :as ds-storage :refer [IStorage]]
[logseq.db :as ldb]
[logseq.db.frontend.validate :as db-validate]
[logseq.db.test.helper :as db-test]))
(defrecord InMemoryStorage [*disk]
IStorage
(-store [_ addr+data-seq _delete-addrs]
(doseq [[addr data] addr+data-seq]
(vswap! *disk assoc addr data)))
(-restore [_ addr]
(get @*disk addr)))
(defn- make-storage []
(->InMemoryStorage (volatile! {})))
;;; datoms
;;; - 1 <----+
;;; - 2 |
;;; - 3 -+
(comment
;; TODO: throw error or fix broken data when cycle detected
(def broken-outliner-data-with-cycle
[{:db/id 1
:block/uuid #uuid"e538d319-48d4-4a6d-ae70-c03bb55b6fe4"
:block/parent 3}
{:db/id 2
:block/uuid #uuid"c46664c0-ea45-4998-adf0-4c36486bb2e5"
:block/parent 1}
{:db/id 3
:block/uuid #uuid"2b736ac4-fd49-4e04-b00f-48997d2c61a2"
:block/parent 2}]))
(def class-parents-data
[{:block/tags :logseq.class/Tag
:block/title "x"
:block/name "x"
:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"}
{:block/tags :logseq.class/Tag
:block/title "y"
:block/name "y"
:block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"
:logseq.property.class/extends [:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"]}
{:block/tags :logseq.class/Tag
:block/title "z"
:block/name "z"
:block/uuid #uuid "d95f2912-a7af-41b9-8ed5-28861f7fc0be"
:logseq.property.class/extends [:block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"]}])
(deftest get-class-extends
(let [conn (db-test/create-conn)]
(d/transact! conn class-parents-data)
(is (= #{"x" "y"}
(->> (ldb/get-class-extends (ldb/get-page @conn "z"))
(map :block/title)
set)))))
(deftest get-case-page
(let [conn (db-test/create-conn-with-blocks
{:properties
{:foo {:logseq.property/type :default}
:Foo {:logseq.property/type :default}}
:classes {:movie {} :Movie {}}})]
;; Case sensitive properties
(is (= "foo" (:block/title (ldb/get-case-page @conn "foo"))))
(is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo"))))
;; Case sensitive classes
(is (= "movie" (:block/title (ldb/get-case-page @conn "movie"))))
(is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie"))))))
(deftest get-journal-page-by-day
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks
[{:page {:build/journal 20260410}}
{:page {:build/journal 20260411}}]})]
(is (= "Apr 10th, 2026"
(:block/title (ldb/get-journal-page-by-day @conn 20260410))))
(is (= "Apr 11th, 2026"
(:block/title (ldb/get-journal-page-by-day @conn 20260411))))))
(deftest page-exists
(let [conn (db-test/create-conn-with-blocks
{:properties
{:foo {:logseq.property/type :default}
:Foo {:logseq.property/type :default}}
:classes {:movie {} :Movie {}}})]
(is (= ["foo"]
(map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "foo" #{:logseq.class/Property})))
"Property pages correctly found for given class")
(is (= nil
(ldb/page-exists? @conn "foo" #{:logseq.class/Tag}))
"Property pages correctly not found for given class")
(is (= ["movie"]
(map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "movie" #{:logseq.class/Tag})))
"Class pages correctly found for given class")
(is (= nil
(ldb/page-exists? @conn "movie" #{:logseq.class/Property}))
"Class pages correctly not found for given class")))
(deftest test-transact-with-multiple-tx-datoms
(testing "last write wins with same tx"
(let [conn (d/create-conn)]
(d/transact! conn [[:db/add -1 :property :v1]])
(let [tx (:max-tx @conn)]
(ldb/transact! conn
[(d/datom 1 :property :v1 (inc tx) false)
(d/datom 1 :property :v1 (inc tx) true)]))
(is (= :v1 (:property (d/entity @conn 1))))))
(testing "last write wins with different tx"
(let [conn (d/create-conn)]
(d/transact! conn [[:db/add -1 :property :v1]])
(let [tx (:max-tx @conn)]
(ldb/transact! conn
[(d/datom 1 :property :v1 (inc tx) false)
(d/datom 1 :property :v1 (+ tx 2) true)]))
(is (= :v1 (:property (d/entity @conn 1)))))))
(deftest test-batch-transact!
(testing "DB validation should be running after the whole transaction"
(let [conn (db-test/create-conn)]
(testing "#Task shouldn't be converted to property"
(is (thrown? js/Error
(with-out-str
(db-test/silence-stderr
(ldb/transact! conn [{:db/ident :logseq.class/Task
:block/tags :logseq.class/Property}]))))))
(ldb/batch-transact-with-temp-conn!
conn
{}
(fn [temp-conn]
(ldb/transact! temp-conn [{:db/ident :logseq.class/Task
:block/tags :logseq.class/Property}])
(ldb/transact! temp-conn [[:db/retract :logseq.class/Task :block/tags :logseq.class/Property]]))))))
(deftest batch-transact-with-temp-conn-preserves-retracts-test
(testing "temp conn batch replays retractions instead of treating every datom as an add"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page 1"}
:blocks [{:block/title "old"}]}]})
block (db-test/find-block-by-content @conn "old")
block-id (:db/id block)]
(ldb/batch-transact-with-temp-conn!
conn
{}
(fn [temp-conn]
(ldb/transact! temp-conn [[:db/retract [:block/uuid (:block/uuid block)] :block/title "old"]])
(ldb/transact! temp-conn [[:db/add [:block/uuid (:block/uuid block)] :block/title "new"]])))
(is (= "new" (:block/title (d/entity @conn block-id)))))))
(deftest batch-transact-with-temp-conn-preserves-cardinality-one-schema-test
(testing "temp conn emits replacement retractions for cardinality-one attrs"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page 1"}
:blocks [{:block/title "old"}]}]})
block (db-test/find-block-by-content @conn "old")
block-id (:db/id block)]
(ldb/transact! conn [[:db/add block-id :block/order "a0"]])
(ldb/batch-transact-with-temp-conn!
conn
{}
(fn [temp-conn]
(ldb/transact! temp-conn [[:db/add [:block/uuid (:block/uuid block)] :block/order "a1"]])))
(is (= "a1" (:block/order (d/entity @conn block-id)))))))
(deftest batch-transact-with-temp-conn-before-commit-can-abort-live-commit-test
(testing "before-commit runs after temp work and before the live conn is modified"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page 1"}
:blocks [{:block/title "old"}]}]})
block (db-test/find-block-by-content @conn "old")
block-id (:db/id block)]
(is (thrown? js/Error
(ldb/batch-transact-with-temp-conn!
conn
{}
(fn [temp-conn]
(ldb/transact! temp-conn [[:db/add [:block/uuid (:block/uuid block)] :block/title "new"]]))
{:before-commit #(throw (js/Error. "abort before commit"))})))
(is (= "old" (:block/title (d/entity @conn block-id)))))))
(deftest validated-transact-retries-when-live-conn-changes-before-commit-test
(testing "validated transact does not overwrite a concurrent live commit"
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page 1"}
:blocks [{:block/title "first"}
{:block/title "second"}]}]})
first-block (db-test/find-block-by-content @conn "first")
second-block (db-test/find-block-by-content @conn "second")
injected? (atom false)
original-pipeline @ldb/*transact-pipeline-fn]
(try
(ldb/register-transact-pipeline-fn!
(fn [tx-report]
(when (and (not @injected?)
(some (fn [datom]
(and (= :block/title (:a datom))
(= "first updated" (:v datom))))
(:tx-data tx-report)))
(reset! injected? true)
(ldb/transact! conn [[:db/add (:db/id second-block) :block/title "second updated"]]))
tx-report))
(ldb/transact! conn [[:db/add (:db/id first-block) :block/title "first updated"]])
(is (= "first updated" (:block/title (d/entity @conn (:db/id first-block)))))
(is (= "second updated" (:block/title (d/entity @conn (:db/id second-block)))))
(finally
(reset! ldb/*transact-pipeline-fn original-pipeline))))))
(deftest fix-db-transact-runs-pipeline-without-recursive-validation-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page 1"}
:blocks [{:block/title "before"}]}]})
block (db-test/find-block-by-content @conn "before")
block-id (:db/id block)
original-pipeline @ldb/*transact-pipeline-fn]
(try
(ldb/register-transact-pipeline-fn!
(fn [{:keys [db-before db-after tx-data tx-meta]}]
(let [stamp-report (d/with db-after
[{:db/id block-id
:block/tx-id 42}])]
(assoc stamp-report
:db-before db-before
:tx-data (concat tx-data (:tx-data stamp-report))
:tx-meta tx-meta))))
(with-redefs [db-validate/validate-tx-report
(fn [& _]
(throw (js/Error. "Repair transactions must not recursively validate")))]
(ldb/transact! conn
[{:db/id block-id
:block/title "after"}]
{:fix-db? true}))
(is (= "after" (:block/title (d/entity @conn block-id))))
(is (= 42 (:block/tx-id (d/entity @conn block-id)))
"Repair transactions commit canonical data produced by the pipeline.")
(finally
(reset! ldb/*transact-pipeline-fn original-pipeline)))))
(deftest test-batch-transact-clears-stale-tx-tail-before-next-store-tail
(let [block-uuid #uuid "00000001-2026-0421-0000-000000000000"
schema {:block/uuid {:db/unique :db.unique/identity}}
storage (make-storage)
conn (d/create-conn schema {:storage storage})]
;; Valid history: unique value moves to another entity.
(d/transact! conn [[:db/add 28446 :block/uuid block-uuid]])
(ldb/batch-transact!
conn
{}
(fn [batch-conn]
(d/transact! batch-conn [[:db/retract 28446 :block/uuid block-uuid]])
(d/transact! batch-conn [[:db/add 28447 :block/uuid block-uuid]])))
;; Next tx uses tail-only persistence.
(d/transact! conn [[:db/add 1 :filler "x"]])
(let [tail (get @(:*disk storage) @#'ds-storage/tail-addr)
stale? (some (fn [[e a v _tx]]
(and (= 28446 e)
(= :block/uuid a)
(= block-uuid v)))
(apply concat tail))
restored (d/restore-conn storage)]
(is (nil? stale?)
"stale pre-batch datoms should not leak into tail after batch-transact!")
(is (= [28447]
(mapv :e (d/datoms @restored :avet :block/uuid block-uuid)))))))
(deftest get-bidirectional-properties
(testing "disabled by default"
(let [conn (db-test/create-conn-with-blocks
{:properties {:friend {:logseq.property/type :node
:build/property-classes [:Person]}}
:classes {:Person {}
:Project {}}
:pages-and-blocks
[{:page {:block/title "Alice"
:build/tags [:Person]
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}
{:page {:block/title "Bob"}}
{:page {:block/title "Charlie"
:build/tags [:Project]
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}]})
target (db-test/find-page-by-title @conn "Bob")]
(is (empty? (ldb/get-bidirectional-properties @conn (:db/id target))))))
(testing "enabled per class"
(let [conn (db-test/create-conn-with-blocks
{:properties {:friend {:logseq.property/type :node
:build/property-classes [:Person]}}
:classes {:Person {:build/properties {:logseq.property.class/enable-bidirectional? true}}
:Project {}}
:pages-and-blocks
[{:page {:block/title "Alice"
:build/tags [:Person]
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}
{:page {:block/title "Bob"}}
{:page {:block/title "Charlie"
:build/tags [:Project]
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}]})
target (db-test/find-page-by-title @conn "Bob")
results (ldb/get-bidirectional-properties @conn (:db/id target))]
(is (= 1 (count results)))
(is (= "People" (:title (first results))))
(is (= ["Alice"]
(map :block/title (:entities (first results))))))))
(deftest get-bidirectional-properties-ignores-recycled-entities
(let [conn (db-test/create-conn-with-blocks
{:properties {:friend {:logseq.property/type :node
:build/property-classes [:Person]}}
:classes {:Person {:build/properties {:logseq.property.class/enable-bidirectional? true}}}
:pages-and-blocks
[{:page {:block/title "Alice"
:build/tags [:Person]
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}
{:page {:block/title "Bob"}}]})
alice (db-test/find-page-by-title @conn "Alice")
target (db-test/find-page-by-title @conn "Bob")]
(d/transact! conn [{:db/id (:db/id alice)
:logseq.property/deleted-at 1}])
(is (empty? (ldb/get-bidirectional-properties @conn (:db/id target))))))
(defn- bidirectional-perf-conn
[n property-titles]
(let [target-page {:page {:block/title "Target"}}
properties (into {}
(map (fn [property-title]
[property-title {:logseq.property/type :node
:build/property-classes [:Person]}]))
property-titles)
person-properties (into {}
(map (fn [property-title]
[property-title [:build/page {:block/title "Target"}]]))
property-titles)
pages (vec (concat [target-page]
(map (fn [i]
{:page {:block/title (str "Person " i)
:build/tags [:Person]
:build/properties person-properties}})
(range n))))]
(db-test/create-conn-with-blocks
{:properties properties
:classes {:Person {:build/properties {:logseq.property.class/enable-bidirectional? true}}}
:pages-and-blocks pages})))
(deftest ^:long get-bidirectional-properties-performance-single-property
(testing "attribute lookups scale with unique properties, not entities"
(let [conn (bidirectional-perf-conn 400 [:friend])
target-id (:db/id (db-test/find-page-by-title @conn "Target"))
original-entity d/entity
attr-lookups (atom 0)
results (with-redefs [d/entity (fn [db eid]
(when (keyword? eid)
(swap! attr-lookups inc))
(original-entity db eid))]
(ldb/get-bidirectional-properties @conn target-id))]
(is (= 1 (count results)))
(is (= 400 (count (:entities (first results)))))
(is (<= @attr-lookups 8)
(str "expected bounded attr lookups, got " @attr-lookups)))))
(deftest ^:long get-bidirectional-properties-performance-multi-property
(testing "attribute lookups stay bounded with multiple matching properties"
(let [conn (bidirectional-perf-conn 300 [:friend :colleague])
target-id (:db/id (db-test/find-page-by-title @conn "Target"))
original-entity d/entity
attr-lookups (atom 0)
results (with-redefs [d/entity (fn [db eid]
(when (keyword? eid)
(swap! attr-lookups inc))
(original-entity db eid))]
(ldb/get-bidirectional-properties @conn target-id))]
(is (= 1 (count results)))
(is (= 300 (count (:entities (first results)))))
(is (<= @attr-lookups 12)
(str "expected bounded attr lookups, got " @attr-lookups)))))