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>
267 lines
13 KiB
Clojure
267 lines
13 KiB
Clojure
(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))))))
|