574 lines
22 KiB
Clojure
574 lines
22 KiB
Clojure
(ns pyramid.core-test
|
|
(:require
|
|
[pyramid.core :as p]
|
|
[pyramid.ident :as ident]
|
|
[clojure.test :as t]))
|
|
|
|
|
|
(t/deftest normalization
|
|
(t/is (= {:person/id {0 {:person/id 0}}}
|
|
(p/db [{:person/id 0}]))
|
|
"a single entity")
|
|
(t/is (= {:person/id {0 {:person/id 0
|
|
:person/name "asdf"}
|
|
1 {:person/id 1
|
|
:person/name "jkl"}}}
|
|
(p/db [{:person/id 0
|
|
:person/name "asdf"}
|
|
{:person/id 1
|
|
:person/name "jkl"}]))
|
|
"multiple entities with attributes")
|
|
(t/is (= {:person/id {0 {:person/id 0
|
|
:person/name "asdf"}
|
|
1 {:person/id 1
|
|
:person/name "jkl"}}
|
|
:people [[:person/id 0]
|
|
[:person/id 1]]}
|
|
(p/db [{:people [{:person/id 0
|
|
:person/name "asdf"}
|
|
{:person/id 1
|
|
:person/name "jkl"}]}]))
|
|
"nested under a key")
|
|
(t/is (= {:person/id {0 {:person/id 0
|
|
:some-data {1 "hello"
|
|
3 "world"}}}}
|
|
(p/db [{:person/id 0
|
|
:some-data {1 "hello"
|
|
3 "world"}}]))
|
|
"Map with numbers as keys")
|
|
(t/is (= {:a/id {1 {:a/id 1
|
|
:b [{:c [:d/id 1]}]}}
|
|
:d/id {1 {:d/id 1
|
|
:d/txt "a"}}}
|
|
(p/db [{:a/id 1
|
|
:b [{:c {:d/id 1
|
|
:d/txt "a"}}]}]))
|
|
"Collections of non-entities still get normalized")
|
|
(t/is (= {:person/id
|
|
{123
|
|
{:person/id 123,
|
|
:person/name "Will",
|
|
:contact {:phone "000-000-0001"},
|
|
:best-friend [:person/id 456],
|
|
:friends
|
|
[[:person/id 9001]
|
|
[:person/id 456]
|
|
[:person/id 789]
|
|
[:person/id 1000]]},
|
|
456
|
|
{:person/id 456,
|
|
:person/name "Jose",
|
|
:account/email "asdf@jkl",
|
|
:best-friend [:person/id 123]},
|
|
9001 #:person{:id 9001, :name "Georgia"},
|
|
789 #:person{:id 789, :name "Frank"},
|
|
1000 #:person{:id 1000, :name "Robert"}}}
|
|
(p/db [{:person/id 123
|
|
:person/name "Will"
|
|
:contact {:phone "000-000-0001"}
|
|
:best-friend
|
|
{:person/id 456
|
|
:person/name "Jose"
|
|
:account/email "asdf@jkl"}
|
|
:friends
|
|
[{:person/id 9001
|
|
:person/name "Georgia"}
|
|
{:person/id 456
|
|
:person/name "Jose"}
|
|
{:person/id 789
|
|
:person/name "Frank"}
|
|
{:person/id 1000
|
|
:person/name "Robert"}]}
|
|
{:person/id 456
|
|
:best-friend {:person/id 123}}]))
|
|
"refs"))
|
|
|
|
|
|
(t/deftest non-entities
|
|
(t/is (= {:foo ["bar"]} (p/db [{:foo ["bar"]}])))
|
|
(t/is (= {:person/id {0 {:person/id 0
|
|
:foo ["bar"]}}}
|
|
(p/db [{:person/id 0
|
|
:foo ["bar"]}]))))
|
|
|
|
|
|
(t/deftest custom-schema
|
|
(t/is (= {:color {"red" {:color "red" :hex "#ff0000"}}}
|
|
(p/db [{:color "red" :hex "#ff0000"}]
|
|
(ident/by-keys :color)))
|
|
"ident/by-keys")
|
|
(t/is (= {:color {"red" {:color "red" :hex "#ff0000"}}}
|
|
(p/db [^{:db/ident :color}
|
|
{:color "red" :hex "#ff0000"}]))
|
|
"local schema")
|
|
(t/testing "complex schema"
|
|
(let [db (p/db [{:type "person"
|
|
:id "1234"
|
|
:purchases [{:type "item"
|
|
:id "1234"}]}
|
|
{:type "item"
|
|
:id "5678"}
|
|
{:type "foo"}
|
|
{:id "bar"}]
|
|
(fn [entity]
|
|
(let [{:keys [type id]} entity]
|
|
(when (and (some? type) (some? id))
|
|
[(keyword type "id") id]))))]
|
|
(t/is (= {:person/id
|
|
{"1234" {:type "person", :id "1234", :purchases [[:item/id "1234"]]}},
|
|
:item/id
|
|
{"1234" {:type "item", :id "1234"}, "5678" {:type "item", :id "5678"}},
|
|
:type "foo",
|
|
:id "bar"}
|
|
db)
|
|
"correctly identifies entities")
|
|
(t/is (= {[:person/id "1234"]
|
|
{:type "person", :id "1234", :purchases [{:type "item", :id "1234"}]}}
|
|
(p/pull db [{[:person/id "1234"] [:type :id {:purchases [:type :id]}]}]))
|
|
"pull"))))
|
|
|
|
|
|
(t/deftest add
|
|
(t/is (= {:person/id {0 {:person/id 0}}}
|
|
(p/add {} {:person/id 0})))
|
|
(t/is (= {:person/id {0 {:person/id 0 :person/name "Gill"}
|
|
1 {:person/id 1}}}
|
|
(p/add
|
|
{}
|
|
{:person/id 0}
|
|
{:person/id 1}
|
|
{:person/id 0 :person/name "Gill"}))))
|
|
|
|
|
|
(t/deftest add-report
|
|
(t/is (= {:db {:person/id {0 {:person/id 0}}}
|
|
:entities #{[:person/id 0]}}
|
|
(p/add-report {} {:person/id 0})))
|
|
(t/is (= {:db {:person/id {0 {:person/id 0
|
|
:person/name "Gill"
|
|
:best-friend [:person/id 1]}
|
|
1 {:person/id 1
|
|
:person/name "Uma"}}
|
|
:me [:person/id 0]}
|
|
:entities #{[:person/id 0]
|
|
[:person/id 1]}}
|
|
(p/add-report {} {:me {:person/id 0
|
|
:person/name "Gill"
|
|
:best-friend {:person/id 1
|
|
:person/name "Uma"}}})))
|
|
#_(t/is (= {:db {:person/id {0 {:person/id 0 :person/name "Gill"}
|
|
1 {:person/id 1}}}
|
|
:entities #{{:person/id 0 :person/name "Gill"}
|
|
{:person/id 1}}}
|
|
(p/add-report
|
|
{}
|
|
{:person/id 0}
|
|
{:person/id 1}
|
|
{:person/id 0 :person/name "Gill"}))))
|
|
|
|
|
|
(def data
|
|
{:people/all [{:person/id 0
|
|
:person/name "Alice"
|
|
:person/age 25
|
|
:best-friend {:person/id 1}
|
|
:person/favorites
|
|
{:favorite/ice-cream "vanilla"}}
|
|
{:person/id 1
|
|
:person/name "Bob"
|
|
:person/age 23}]})
|
|
|
|
|
|
(def db
|
|
(p/db [data]))
|
|
|
|
|
|
(t/deftest pull
|
|
(t/is (= #:people{:all [{:person/id 0} {:person/id 1}]}
|
|
(p/pull db [:people/all]))
|
|
"simple key")
|
|
(t/is (= {:people/all [{:person/name "Alice"
|
|
:person/id 0}
|
|
{:person/name "Bob"
|
|
:person/id 1}]}
|
|
(p/pull db [{:people/all [:person/name :person/id]}]))
|
|
"basic join + prop")
|
|
(t/is (= #:people{:all [{:person/name "Alice"
|
|
:person/id 0
|
|
:best-friend #:person{:name "Bob", :id 1 :age 23}}
|
|
#:person{:name "Bob", :id 1}]}
|
|
(p/pull db [#:people{:all [:person/name :person/id :best-friend]}]))
|
|
"join + prop + join ref lookup")
|
|
(t/is (= #:people{:all [{:person/name "Alice"
|
|
:person/id 0
|
|
:best-friend #:person{:name "Bob"}}
|
|
#:person{:name "Bob", :id 1}]}
|
|
(p/pull db [#:people{:all [:person/name
|
|
:person/id
|
|
{:best-friend [:person/name]}]}]))
|
|
"join + prop, ref as prop resolver")
|
|
(t/is (= {[:person/id 1] #:person{:id 1, :name "Bob", :age 23}}
|
|
(p/pull db [[:person/id 1]]))
|
|
"ident acts as ref lookup")
|
|
(t/is (= {[:person/id 0] {:person/id 0
|
|
:person/name "Alice"
|
|
:person/age 25
|
|
:best-friend {:person/id 1}
|
|
:person/favorites #:favorite{:ice-cream "vanilla"}}}
|
|
(p/pull db [[:person/id 0]]))
|
|
"ident does not resolve nested refs")
|
|
(t/is (= {[:person/id 0] #:person{:id 0
|
|
:name "Alice"
|
|
:favorites #:favorite{:ice-cream "vanilla"}}}
|
|
(p/pull db [{[:person/id 0] [:person/id
|
|
:person/name
|
|
:person/favorites]}]))
|
|
"join on ident")
|
|
(t/is (= {:people/all [{:person/name "Alice"
|
|
:person/id 0
|
|
:best-friend #:person{:name "Bob", :id 1 :age 23}}
|
|
#:person{:name "Bob", :id 1}]
|
|
[:person/id 1] #:person{:age 23}}
|
|
(p/pull db [{:people/all [:person/name :person/id :best-friend]}
|
|
{[:person/id 1] [:person/age]}]))
|
|
"multiple joins")
|
|
|
|
(t/testing "includes params"
|
|
(t/is (= #:people{:all [#:person{:name "Bob", :id 1}]}
|
|
(p/pull (-> db
|
|
(p/add {'(:people/all {:with "params"}) [[:person/id 1]]}))
|
|
'[{(:people/all {:with "params"})
|
|
[:person/name :person/id]}])))
|
|
(t/is (= '{:person/foo {:person/id 1
|
|
:person/name "Bob"}}
|
|
(p/pull (-> db
|
|
(p/add {'(:person/foo {:person/id 2})
|
|
{:person/id 1}}))
|
|
'[{(:person/foo {:person/id 2})
|
|
[:person/name :person/id]}]))
|
|
"params that include an entity-looking thing should not be normalized")
|
|
(t/is (= {}
|
|
(p/pull db '[([:person/id 1] {:with "params"})])))
|
|
(t/is (= {}
|
|
(p/pull db '[{(:people/all {:with "params"})
|
|
[:person/name :person/id]}]))))
|
|
|
|
(t/testing "union"
|
|
(let [data {:chat/entries
|
|
[{:message/id 0
|
|
:message/text "foo"
|
|
:chat.entry/timestamp "1234"}
|
|
{:message/id 1
|
|
:message/text "bar"
|
|
:chat.entry/timestamp "1235"}
|
|
{:audio/id 0
|
|
:audio/url "audio://asdf.jkl"
|
|
:audio/duration 1234
|
|
:chat.entry/timestamp "4567"}
|
|
{:photo/id 0
|
|
:photo/url "photo://asdf_10x10.jkl"
|
|
:photo/height 10
|
|
:photo/width 10
|
|
:chat.entry/timestamp "7890"}]}
|
|
db1 (p/db [data])
|
|
query [{:chat/entries
|
|
{:message/id
|
|
[:message/id :message/text :chat.entry/timestamp]
|
|
|
|
:audio/id
|
|
[:audio/id :audio/url :audio/duration :chat.entry/timestamp]
|
|
|
|
:photo/id
|
|
[:photo/id :photo/url :photo/width :photo/height :chat.entry/timestamp]
|
|
|
|
:asdf/jkl [:asdf/jkl]}}]]
|
|
(t/is (= #:chat{:entries [{:message/id 0
|
|
:message/text "foo"
|
|
:chat.entry/timestamp "1234"}
|
|
{:message/id 1
|
|
:message/text "bar"
|
|
:chat.entry/timestamp "1235"}
|
|
{:audio/id 0
|
|
:audio/url "audio://asdf.jkl"
|
|
:audio/duration 1234
|
|
:chat.entry/timestamp "4567"}
|
|
{:photo/id 0
|
|
:photo/url "photo://asdf_10x10.jkl"
|
|
:photo/width 10
|
|
:photo/height 10
|
|
:chat.entry/timestamp "7890"}]}
|
|
(p/pull db1 query)))))
|
|
|
|
(t/testing "not found"
|
|
(t/is (= {} (p/pull {} [:foo])))
|
|
(t/is (= {} (p/pull {} [:foo :bar :baz])))
|
|
(t/is (= {} (p/pull {} [:foo {:bar [:asdf]} :baz])))
|
|
|
|
(t/is (= {:foo "bar"}
|
|
(p/pull {:foo "bar"} [:foo {:bar [:asdf]} :baz])))
|
|
(t/is (= {:bar {:asdf 123}}
|
|
(p/pull
|
|
{:bar {:asdf 123}}
|
|
[:foo {:bar [:asdf :jkl]} :baz])))
|
|
(t/is (= {:bar {}}
|
|
(p/pull
|
|
(p/db [{:bar {:bar/id 0}}
|
|
{:bar/id 0
|
|
:qwerty 1234}])
|
|
[:foo {:bar [:asdf :jkl]} :baz])))
|
|
(t/is (= {:bar {:asdf "jkl"}}
|
|
(p/pull
|
|
(p/db [{:bar {:bar/id 0}}
|
|
{:bar/id 0
|
|
:asdf "jkl"}])
|
|
[:foo {:bar [:asdf :jkl]} :baz])))
|
|
(t/is (= {:bar {}}
|
|
(p/pull
|
|
(p/db [{:bar {:bar/id 0}}
|
|
{:bar/id 1
|
|
:asdf "jkl"}])
|
|
[:foo {:bar [:asdf :jkl]} :baz])))
|
|
|
|
(t/is (= {:foo [{:bar/id 1
|
|
:bar/name "asdf"}
|
|
{:baz/id 1
|
|
:baz/name "jkl"}]}
|
|
(p/pull
|
|
(p/db [{:foo [{:bar/id 1
|
|
:bar/name "asdf"}
|
|
{:baz/id 1
|
|
:baz/name "jkl"}]}])
|
|
[{:foo {:bar/id [:bar/id :bar/name]
|
|
:baz/id [:baz/id :baz/name]}}])))
|
|
|
|
(t/is (= {:foo [{:bar/id 1
|
|
:bar/name "asdf"}
|
|
{:bar/id 2}
|
|
{:baz/id 1
|
|
:baz/name "jkl"}]}
|
|
(p/pull
|
|
(p/db [{:foo [{:bar/id 1
|
|
:bar/name "asdf"}
|
|
{:bar/id 2}
|
|
{:baz/id 1
|
|
:baz/name "jkl"}]}])
|
|
[{:foo {:bar/id [:bar/id :bar/name]
|
|
:baz/id [:baz/id :baz/name]}}]))))
|
|
|
|
(t/testing "bounded recursion"
|
|
(let [data {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders
|
|
[{:entry/id "asdf"
|
|
:entry/folders
|
|
[{:entry/id "qwerty"}]}
|
|
{:entry/id "jkl"
|
|
:entry/folders
|
|
[{:entry/id "uiop"}]}]}]}}
|
|
db (p/db [data])]
|
|
(t/is (= {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[]}}
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders 0}]}])))
|
|
(t/is (= {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders []}]}}
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders 1}]}])))
|
|
(t/is (= {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders
|
|
[{:entry/id "asdf"
|
|
:entry/folders []}
|
|
{:entry/id "jkl"
|
|
:entry/folders []}]}]}}
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders 2}]}])))
|
|
(t/is (= {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders
|
|
[{:entry/id "asdf"
|
|
:entry/folders
|
|
[{:entry/id "qwerty"}]}
|
|
{:entry/id "jkl"
|
|
:entry/folders
|
|
[{:entry/id "uiop"}]}]}]}}
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders 3}]}])))
|
|
(t/is (= {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders
|
|
[{:entry/id "asdf"
|
|
:entry/folders
|
|
[{:entry/id "qwerty"}]}
|
|
{:entry/id "jkl"
|
|
:entry/folders
|
|
[{:entry/id "uiop"}]}]}]}}
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders 10}]}])))))
|
|
|
|
(t/testing "infinite recursion"
|
|
(let [data {:entries
|
|
{:entry/id "foo"
|
|
:entry/folders
|
|
[{:entry/id "bar"}
|
|
{:entry/id "baz"
|
|
:entry/folders
|
|
[{:entry/id "asdf"
|
|
:entry/folders
|
|
[{:entry/id "qwerty"}]}
|
|
{:entry/id "jkl"
|
|
:entry/folders
|
|
[{:entry/id "uiop"}]}]}]}}
|
|
db (p/db [data])]
|
|
(t/is (= data
|
|
(p/pull db '[{:entries [:entry/id
|
|
{:entry/folders ...}]}])))))
|
|
|
|
(t/testing "query metadata"
|
|
(t/is (-> db
|
|
(p/pull ^:foo [])
|
|
(meta)
|
|
(:foo))
|
|
"root")
|
|
(t/is (-> db
|
|
(p/pull [^:foo {[:person/id 0] [:person/name]}])
|
|
(get [:person/id 0])
|
|
(meta)
|
|
(:foo))
|
|
"join")
|
|
(let [data {:chat/entries
|
|
[{:message/id 0
|
|
:message/text "foo"
|
|
:chat.entry/timestamp "1234"}
|
|
{:message/id 1
|
|
:message/text "bar"
|
|
:chat.entry/timestamp "1235"}
|
|
{:audio/id 0
|
|
:audio/url "audio://asdf.jkl"
|
|
:audio/duration 1234
|
|
:chat.entry/timestamp "4567"}
|
|
{:photo/id 0
|
|
:photo/url "photo://asdf_10x10.jkl"
|
|
:photo/height 10
|
|
:photo/width 10
|
|
:chat.entry/timestamp "7890"}]}
|
|
db1 (p/db [data])
|
|
query ^:foo [^:bar
|
|
{:chat/entries
|
|
{:message/id
|
|
[:message/id :message/text :chat.entry/timestamp]
|
|
|
|
:audio/id
|
|
[:audio/id :audio/url :audio/duration :chat.entry/timestamp]
|
|
|
|
:photo/id
|
|
[:photo/id :photo/url :photo/width :photo/height :chat.entry/timestamp]
|
|
|
|
:asdf/jkl [:asdf/jkl]}}]
|
|
result (p/pull db1 query)]
|
|
(t/is (= #:chat{:entries [{:message/id 0
|
|
:message/text "foo"
|
|
:chat.entry/timestamp "1234"}
|
|
{:message/id 1
|
|
:message/text "bar"
|
|
:chat.entry/timestamp "1235"}
|
|
{:audio/id 0
|
|
:audio/url "audio://asdf.jkl"
|
|
:audio/duration 1234
|
|
:chat.entry/timestamp "4567"}
|
|
{:photo/id 0
|
|
:photo/url "photo://asdf_10x10.jkl"
|
|
:photo/width 10
|
|
:photo/height 10
|
|
:chat.entry/timestamp "7890"}]}
|
|
result))
|
|
(t/is (-> result meta :foo))
|
|
(t/is (every? #(:bar (meta %)) (get result :chat/entries)))))
|
|
(t/testing "dangling entities"
|
|
(t/is (= {[:id 0] {:friends [{:id 1} {:id 2}]}}
|
|
(p/pull
|
|
{:id {0 {:id 0 :name "asdf" :friends [[:id 1] [:id 2]]}
|
|
1 {:id 1 :name "jkl"}}}
|
|
[{[:id 0] [:friends]}]))
|
|
"dangling entity shows up in queries that do not select any props")
|
|
;; BB-TEST-PATCH: NullPointerException: Cannot invoke "clojure.lang.IObj.withMeta(clojure.lang.IPersistentMap)"
|
|
#_(t/is (= {[:id 0] {:friends [{:id 1, :name "jkl"} {:id 2}]}}
|
|
(p/pull
|
|
{:id {0 {:id 0 :name "asdf" :friends [[:id 1] [:id 2]]}
|
|
1 {:id 1 :name "jkl"}}}
|
|
[{[:id 0] [{:friends [:id :name]}]}]))
|
|
"dangling entity shows up in queries that include ID")
|
|
;; BB-TEST-PATCH: NullPointerException: Cannot invoke "clojure.lang.IObj.withMeta(clojure.lang.IPersistentMap)"
|
|
#_(t/is (= {[:id 0] {:friends [{:name "jkl"}]}}
|
|
(p/pull
|
|
{:id {0 {:id 0 :name "asdf" :friends [[:id 1] [:id 2]]}
|
|
1 {:id 1 :name "jkl"}}}
|
|
[{[:id 0] [{:friends [:name]}]}]))
|
|
"dangling entity does not show up in queries that do not include ID")))
|
|
|
|
|
|
(t/deftest pull-report
|
|
(t/is (= {:data {:people/all [{:person/name "Alice"}
|
|
{:person/name "Bob"}]}
|
|
:entities #{[:person/id 0] [:person/id 1]}}
|
|
(p/pull-report db [{:people/all [:person/name]}]))
|
|
"basic join + prop")
|
|
(t/is (= {:data #:people{:all [{:person/name "Alice"
|
|
:best-friend #:person{:name "Bob", :id 1 :age 23}}
|
|
#:person{:name "Bob"}]}
|
|
:entities #{[:person/id 0] [:person/id 1]}}
|
|
(p/pull-report db [#:people{:all [:person/name :best-friend]}]))
|
|
"join + prop + join ref lookup")
|
|
(t/is (= {:data {[:person/id 1] #:person{:id 1, :name "Bob", :age 23}}
|
|
:entities #{[:person/id 1]}}
|
|
(p/pull-report db [[:person/id 1]]))
|
|
"ident acts as ref lookup")
|
|
(t/is (= {:data {[:person/id 0] {:person/id 0
|
|
:person/name "Alice"
|
|
:person/age 25
|
|
:best-friend {:person/id 1}
|
|
:person/favorites #:favorite{:ice-cream "vanilla"}}}
|
|
:entities #{[:person/id 0]}}
|
|
(p/pull-report db [[:person/id 0]]))
|
|
"ident does not resolve nested refs"))
|
|
|
|
|
|
(t/deftest delete
|
|
(t/is (= {:people/all [[:person/id 0]]
|
|
:person/id {0 {:person/id 0
|
|
:person/name "Alice"
|
|
:person/age 25
|
|
:person/favorites #:favorite{:ice-cream "vanilla"}}}}
|
|
(p/delete db [:person/id 1]))))
|
|
|
|
|
|
(t/deftest data->query
|
|
(t/is (= [:a]
|
|
(p/data->query {:a 42})))
|
|
(t/is (= [{:a [:b]}]
|
|
(p/data->query {:a {:b 42}})))
|
|
(t/is (= [{:a [:b :c]}]
|
|
(p/data->query {:a [{:b 42} {:c :d}]})))
|
|
(t/is (= [{[:a 42] [:b]}]
|
|
(p/data->query {[:a 42] {:b 33}}))))
|
|
|
|
(comment
|
|
(t/run-tests))
|