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:
312
deps/db/test/logseq/db/common/delete_blocks_test.cljs
vendored
Normal file
312
deps/db/test/logseq/db/common/delete_blocks_test.cljs
vendored
Normal 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)))))
|
||||
49
deps/db/test/logseq/db/common/initial_data_refs_test.cljs
vendored
Normal file
49
deps/db/test/logseq/db/common/initial_data_refs_test.cljs
vendored
Normal 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*))))
|
||||
99
deps/db/test/logseq/db/common/initial_data_test.cljs
vendored
Normal file
99
deps/db/test/logseq/db/common/initial_data_test.cljs
vendored
Normal 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"))))
|
||||
303
deps/db/test/logseq/db/common/view_test.cljs
vendored
Normal file
303
deps/db/test/logseq/db/common/view_test.cljs
vendored
Normal 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))))
|
||||
59
deps/db/test/logseq/db/frontend/class_test.cljs
vendored
Normal file
59
deps/db/test/logseq/db/frontend/class_test.cljs
vendored
Normal 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"))))))
|
||||
104
deps/db/test/logseq/db/frontend/content_test.cljs
vendored
Normal file
104
deps/db/test/logseq/db/frontend/content_test.cljs
vendored
Normal 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")))))))
|
||||
44
deps/db/test/logseq/db/frontend/db_ident_test.cljc
vendored
Normal file
44
deps/db/test/logseq/db/frontend/db_ident_test.cljc
vendored
Normal 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 '.'")))
|
||||
207
deps/db/test/logseq/db/frontend/inputs_test.cljs
vendored
Normal file
207
deps/db/test/logseq/db/frontend/inputs_test.cljs
vendored
Normal 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"))))
|
||||
42
deps/db/test/logseq/db/frontend/property/type_test.cljs
vendored
Normal file
42
deps/db/test/logseq/db/frontend/property/type_test.cljs
vendored
Normal 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)))))))
|
||||
89
deps/db/test/logseq/db/frontend/property_test.cljs
vendored
Normal file
89
deps/db/test/logseq/db/frontend/property_test.cljs
vendored
Normal 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)))))
|
||||
21
deps/db/test/logseq/db/frontend/reaction_test.cljs
vendored
Normal file
21
deps/db/test/logseq/db/frontend/reaction_test.cljs
vendored
Normal 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)))))))
|
||||
266
deps/db/test/logseq/db/frontend/rules_test.cljs
vendored
Normal file
266
deps/db/test/logseq/db/frontend/rules_test.cljs
vendored
Normal 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))))))
|
||||
342
deps/db/test/logseq/db/sqlite/build_test.cljs
vendored
Normal file
342
deps/db/test/logseq/db/sqlite/build_test.cljs
vendored
Normal 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")))
|
||||
173
deps/db/test/logseq/db/sqlite/create_graph_test.cljs
vendored
Normal file
173
deps/db/test/logseq/db/sqlite/create_graph_test.cljs
vendored
Normal 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)))))))
|
||||
1469
deps/db/test/logseq/db/sqlite/export_test.cljs
vendored
Normal file
1469
deps/db/test/logseq/db/sqlite/export_test.cljs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
49
deps/db/test/logseq/db/sqlite/gc_test.cljs
vendored
Normal file
49
deps/db/test/logseq/db/sqlite/gc_test.cljs
vendored
Normal 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!"))))
|
||||
6
deps/db/test/logseq/db/test_runner.cljs
vendored
Normal file
6
deps/db/test/logseq/db/test_runner.cljs
vendored
Normal 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
376
deps/db/test/logseq/db_test.cljs
vendored
Normal 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)))))
|
||||
Reference in New Issue
Block a user