Skip to content

Commit

Permalink
Support return maps (:keys/:syms/:strs) in query (closes #322, closes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
tonsky committed Jun 21, 2020
1 parent c1a0ee5 commit 5bc0b3e
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 33 deletions.
39 changes: 20 additions & 19 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
/target
/bench/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
*.iml
*.jar
.cljs_node_repl
.cpcache
.DS_Store
.idea
.vscode
/.lein-*
/.nrepl-port
/.repl*
web/out
web/*.js
release-js/datascript*.js
release-js/npm-*
web/target-cljs
.idea
*.iml
.vscode
.cpcache
/bench/target
/checkouts
/classes
/out
/target
dev/playground.clj
.DS_Store
TODO.txt
.cljs_node_repl
node_modules
package-lock.json
pom.xml
pom.xml.asc
reading
release-js/datascript*.js
release-js/npm-*
TODO.txt
web/*.js
web/out
web/target-cljs
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# WIP

- Support return maps (:keys/:syms/:strs) in query #322 #345

# 0.18.13

- Fix `empty?` builtin #349, thx @ash14
Expand Down
4 changes: 2 additions & 2 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
:extra-paths ["test"]
:extra-deps {
org.clojure/clojurescript {:mvn/version "1.10.520"}
lambdaisland/kaocha {:mvn/version "0.0-389"}
lambdaisland/kaocha-cljs {:mvn/version "0.0-21"}
lambdaisland/kaocha {:mvn/version "1.0.632"}
lambdaisland/kaocha-cljs {:mvn/version "0.0-71"}
}
}

Expand Down
37 changes: 37 additions & 0 deletions docs/queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Return maps

```
return-map = (return-keys | return-syms | return-strs)
return-keys = ':keys' symbol+
return-syms = ':syms' symbol+
return-strs = ':strs' symbol+
```

If you need a query to return a set of maps instead set of tuples, specify one of `:keys` / `:syms` / `:strs`, followed by a list of symbols:

```
[:find ?name ?age ?email, ...]
=> #{["Ivan" 10 "ivan@"]
["Oleg" 20 "oleg@"]
["Sergey" 30 "sergey@"]}
[:find ?name ?age ?email
:keys name A e-mail
...]
=> #{{:name "Ivan", :A 10, :e-mail "ivan@"}
{:name "Oleg", :A 20, :e-mail "oleg@"}
{:name "Sergey", :A 30, :e-mail "sergey@"}}
```

`:strs` forces map to have string keys, `:syms` for symbols.

Return maps are only compatible with normal find and tuple-returning find:

```
[:find [?name ?age ?email], ...]
=> {:name "Ivan", :A 10, :e-mail "ivan@"}
```

The amount of keys must match the amount of find elements.

Datomic docs: https://docs.datomic.com/on-prem/query.html#return-maps
5 changes: 5 additions & 0 deletions script/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/zsh -euo pipefail

cd "`dirname $0`/.."

npm i ws
5 changes: 5 additions & 0 deletions script/kaocha_clj.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/zsh -euo pipefail

cd "`dirname $0`/.."

clojure -A:test -m kaocha.runner clj --watch "$@"
5 changes: 5 additions & 0 deletions script/kaocha_cljs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/zsh -euo pipefail

cd "`dirname $0`/.."

clojure -A:test -m kaocha.runner cljs --watch "$@"
52 changes: 47 additions & 5 deletions src/datascript/parser.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,22 @@
{:error :parser/find, :fragment form})))


;; return-map = (return-keys | return-syms | return-strs)
;; return-keys = ':keys' symbol+
;; return-syms = ':syms' symbol+
;; return-strs = ':strs' symbol+

(deftrecord ReturnMap [type symbols])

(defn parse-return-map [type form]
(when (and (not (empty? form))
(every? symbol? form))
(case type
:keys (ReturnMap. type (mapv keyword form))
:syms (ReturnMap. type (vec form))
:strs (ReturnMap. type (mapv str form))
nil)))

;; with = [ variable+ ]

(defn parse-with [form]
Expand Down Expand Up @@ -677,7 +693,7 @@
(recur (update-in parsed [key] (fnil conj []) q) key (next qs)))
parsed)))

(defn validate-query [q form]
(defn validate-query [q form form-map]
(let [find-vars (set (collect-vars (:qfind q)))
with-vars (set (:qwith q))
in-vars (set (collect-vars (:qin q)))
Expand All @@ -687,10 +703,33 @@
shared (set/intersection find-vars with-vars)]
(when-not (empty? unknown)
(raise "Query for unknown vars: " (mapv :symbol unknown)
{:error :parser/query, :vars unknown, :form form}))
{:error :parser/query, :vars unknown, :form form}))
(when-not (empty? shared)
(raise ":find and :with should not use same variables: " (mapv :symbol shared)
{:error :parser/query, :vars shared, :form form})))
{:error :parser/query, :vars shared, :form form})))

(when-some [return-map (:qreturn-map q)]
(when (instance? FindScalar (:qfind q))
(raise (:type return-map) " does not work with single-scalar :find"
{:error :parser/query, :form form}))
(when (instance? FindColl (:qfind q))
(raise (:type return-map) " does not work with collection :find"
{:error :parser/query, :form form})))

(when-some [return-symbols (:symbols (:qreturn-map q))]
(let [find-elements (find-elements (:qfind q))]
(when-not (= (count return-symbols) (count find-elements))
(raise "Count of " (:type (:qreturn-map q)) " must match count of :find"
{:error :parser/query
:return-map (cons (:type (:qreturn-map q)) return-symbols)
:find find-elements
:form form}))))

(when (< 1 (->> [(:keys form-map) (:syms form-map) (:strs form-map)]
(filter some?)
(count)))
(raise "Only one of :keys/:syms/:strs must be present"
{:error :parser/query, :form form}))

(let [in-vars (collect-vars (:qin q))
in-sources (collect #(instance? SrcVar %) (:qin q))
Expand Down Expand Up @@ -731,7 +770,10 @@
{:qfind (parse-find (:find qm))
:qwith (when-let [with (:with qm)]
(parse-with with))
:qreturn-map (or (parse-return-map :keys (:keys qm))
(parse-return-map :syms (:syms qm))
(parse-return-map :strs (:strs qm)))
:qin (parse-in (:in qm ['$]))
:qwhere (parse-where (:where qm []))})]
(validate-query res q)
res))
(validate-query res q qm)
res))
36 changes: 30 additions & 6 deletions src/datascript/query.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -890,18 +890,42 @@
(for [[_ tuples] grouped]
(-aggregate find-elements context tuples))))

(defn map* [f xs]
(reduce #(conj %1 (f %2)) (empty xs) xs))

(defn tuples->return-map [return-map tuples]
(let [symbols (:symbols return-map)
idxs (range 0 (count symbols))]
(map*
(fn [tuple]
(reduce
(fn [m i] (assoc m (nth symbols i) (nth tuple i)))
{} idxs))
tuples)))

(defprotocol IPostProcess
(-post-process [find tuples]))
(-post-process [find return-map tuples]))

(extend-protocol IPostProcess
FindRel
(-post-process [_ tuples] tuples)
(-post-process [_ return-map tuples]
(if (nil? return-map)
tuples
(tuples->return-map return-map tuples)))

FindColl
(-post-process [_ tuples] (into [] (map first) tuples))
(-post-process [_ return-map tuples]
(into [] (map first) tuples))

FindScalar
(-post-process [_ tuples] (ffirst tuples))
(-post-process [_ return-map tuples]
(ffirst tuples))

FindTuple
(-post-process [_ tuples] (first tuples)))
(-post-process [_ return-map tuples]
(if (some? return-map)
(first (tuples->return-map return-map [(first tuples)]))
(first tuples))))

(defn- pull [find-elements context resultset]
(let [resolved (for [find find-elements]
Expand Down Expand Up @@ -952,4 +976,4 @@
(some dp/pull? find-elements)
(pull find-elements context)
true
(-post-process find))))
(-post-process find (:qreturn-map parsed-q)))))
5 changes: 4 additions & 1 deletion test/datascript/test/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@
(defn all-datoms [db]
(into #{} (map (juxt :e :a :v)) (d/datoms db :eavt)))

#?(:clj
(defn no-namespace-maps [t]
(binding [*print-namespace-maps* false]
(t)))
(t)))
:cljs
(def no-namespace-maps {:before #(set! *print-namespace-maps* false)}))

;; Core tests

Expand Down
50 changes: 50 additions & 0 deletions test/datascript/test/parser_return_map.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
(ns datascript.test.parser-return-map
(:require
#?(:cljs [cljs.test :as t :refer-macros [is are deftest testing]]
:clj [clojure.test :as t :refer [is are deftest testing]])
[datascript.core :as d]
[datascript.parser :as dp]
[datascript.db :as db]
[datascript.test.core :as tdc]))

#?(:cljs
(def Throwable js/Error))

(deftest test-parse-return-map
(is (= (:qreturn-map (dp/parse-query '[:find ?a ?b :keys x y :where [?a ?b]]))
(dp/->ReturnMap :keys [:x :y])))

(is (= (:qreturn-map (dp/parse-query '[:find ?a :syms x :where [?a]]))
(dp/->ReturnMap :syms ['x])))

(is (= (:qreturn-map (dp/parse-query '[:find ?a ?b ?c :strs x y z :where [?a ?b ?c]]))
(dp/->ReturnMap :strs ["x" "y" "z"])))


(testing "with find specs"
(is (= (:qreturn-map (dp/parse-query '[:find [?a ?b] :keys x y :where [?a ?b]]))
(dp/->ReturnMap :keys [:x :y])))

(is (thrown-msg? ":keys does not work with collection :find"
(dp/parse-query '[:find [?a ...] :keys x :where [?a]])))

(is (thrown-msg? ":keys does not work with single-scalar :find"
(dp/parse-query '[:find ?a . :keys x y :where [?a]]))))


(testing "errors"
(is (thrown-msg? "Only one of :keys/:syms/:strs must be present"
(dp/parse-query '[:find ?a ?b :keys x y :strs zt :where [?a ?b]])))

(is (thrown-msg? "Count of :keys must match count of :find"
(dp/parse-query '[:find ?a ?b :keys x y z :where [?a ?b]])))

(is (thrown-msg? "Count of :syms must match count of :find"
(dp/parse-query '[:find ?a ?b :syms x :where [?a ?b]])))

(is (thrown-msg? "Count of :strs must match count of :find"
(dp/parse-query '[:find ?a ?b :strs x :where [?a ?b]])))

(is (thrown-msg? "Count of :keys must match count of :find"
(dp/parse-query '[:find [?a ?b] :keys x :where [?a ?b]]))))
)
46 changes: 46 additions & 0 deletions test/datascript/test/query_return_map.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(ns datascript.test.query-return-map
(:require
#?(:cljs [cljs.test :as t :refer-macros [is are deftest testing]]
:clj [clojure.test :as t :refer [is are deftest testing]])
[datascript.core :as d]
[datascript.db :as db]
[datascript.test.core :as tdc]))

(def test-db
(d/db-with (d/empty-db)
[[:db/add 1 :name "Petr"]
[:db/add 1 :age 44]
[:db/add 2 :name "Ivan"]
[:db/add 2 :age 25]
[:db/add 3 :name "Sergey"]
[:db/add 3 :age 11]]))

(deftest test-find-specs
(is (= (d/q '[:find ?name ?age
:keys n a
:where [?e :name ?name]
[?e :age ?age]]
test-db)
#{{:n "Petr" :a 44} {:n "Ivan" :a 25} {:n "Sergey" :a 11}}))
(is (= (d/q '[:find ?name ?age
:syms n a
:where [?e :name ?name]
[?e :age ?age]]
test-db)
#{{'n "Petr" 'a 44} {'n "Ivan" 'a 25} {'n "Sergey" 'a 11}}))
(is (= (d/q '[:find ?name ?age
:strs n a
:where [?e :name ?name]
[?e :age ?age]]
test-db)
#{{"n" "Petr" "a" 44} {"n" "Ivan" "a" 25} {"n" "Sergey" "a" 11}}))

(is (= (d/q '[:find [?name ?age]
:keys n a
:where [?e :name ?name]
[(= ?name "Ivan")]
[?e :age ?age]]
test-db)
{:n "Ivan" :a 25})))


0 comments on commit 5bc0b3e

Please sign in to comment.