Implement simple 'or' clauses. r=nalexander
This commit is contained in:
parent
1ad67a03eb
commit
8a77dcd8f0
4 changed files with 221 additions and 30 deletions
|
@ -8,7 +8,7 @@
|
||||||
[datomish.query.context :as context]
|
[datomish.query.context :as context]
|
||||||
[datomish.query.projection :as projection]
|
[datomish.query.projection :as projection]
|
||||||
[datomish.query.transforms :as transforms]
|
[datomish.query.transforms :as transforms]
|
||||||
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise-str cond-let]]
|
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str cond-let]]
|
||||||
[datascript.parser :as dp
|
[datascript.parser :as dp
|
||||||
#?@(:cljs
|
#?@(:cljs
|
||||||
[:refer [
|
[:refer [
|
||||||
|
@ -61,11 +61,11 @@
|
||||||
|
|
||||||
(defn- validate-in [in]
|
(defn- validate-in [in]
|
||||||
(when (nil? in)
|
(when (nil? in)
|
||||||
(raise-str ":in expression cannot be nil."))
|
(raise ":in expression cannot be nil." {:binding in}))
|
||||||
(when-not (= "$" (name (-> in first :variable :symbol)))
|
(when-not (= "$" (name (-> in first :variable :symbol)))
|
||||||
(raise-str "Non-default sources not supported."))
|
(raise "Non-default sources not supported." {:binding in}))
|
||||||
(when-not (every? (partial instance? BindScalar) (rest in))
|
(when-not (every? (partial instance? BindScalar) (rest in))
|
||||||
(raise-str "Non-scalar bindings not supported.")))
|
(raise "Non-scalar bindings not supported." {:binding in})))
|
||||||
|
|
||||||
(defn in->bindings
|
(defn in->bindings
|
||||||
"Take an `:in` list and return a bindings map suitable for use
|
"Take an `:in` list and return a bindings map suitable for use
|
||||||
|
@ -127,3 +127,16 @@
|
||||||
(not [(> ?t ?latest)]) ])
|
(not [(> ?t ?latest)]) ])
|
||||||
{:latest 5})
|
{:latest 5})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#_
|
||||||
|
(datomish.query/find->sql-string
|
||||||
|
(datomish.query.context/->Context (datomish.query.source/datoms-source nil) nil nil)
|
||||||
|
(datomish.query/parse
|
||||||
|
'[:find ?page :in $ ?latest :where
|
||||||
|
[?page :page/url "http://example.com/"]
|
||||||
|
[?page :page/title ?title]
|
||||||
|
(or
|
||||||
|
[?entity :page/likes ?page]
|
||||||
|
[?entity :page/loves ?page])
|
||||||
|
])
|
||||||
|
{})
|
||||||
|
|
|
@ -13,14 +13,14 @@
|
||||||
[datascript.parser :as dp
|
[datascript.parser :as dp
|
||||||
#?@(:cljs
|
#?@(:cljs
|
||||||
[:refer
|
[:refer
|
||||||
[PlainSymbol Predicate Not Pattern DefaultSrc Variable Constant Placeholder]])]
|
[PlainSymbol Predicate Not Or Pattern DefaultSrc Variable Constant Placeholder]])]
|
||||||
[honeysql.core :as sql]
|
[honeysql.core :as sql]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
)
|
)
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import
|
(:import
|
||||||
[datascript.parser
|
[datascript.parser
|
||||||
PlainSymbol Predicate Not Pattern DefaultSrc Variable Constant Placeholder])))
|
PlainSymbol Predicate Not Or Pattern DefaultSrc Variable Constant Placeholder])))
|
||||||
|
|
||||||
;; A ConjoiningClauses (CC) is a collection of clauses that are combined with JOIN.
|
;; A ConjoiningClauses (CC) is a collection of clauses that are combined with JOIN.
|
||||||
;; The topmost form in a query is a ConjoiningClauses.
|
;; The topmost form in a query is a ConjoiningClauses.
|
||||||
|
@ -62,6 +62,12 @@
|
||||||
(attribute-in-source (:source cc) value)
|
(attribute-in-source (:source cc) value)
|
||||||
(constant-in-source (:source cc) value))]))
|
(constant-in-source (:source cc) value))]))
|
||||||
|
|
||||||
|
(defn merge-ccs [left right]
|
||||||
|
(assoc left
|
||||||
|
:from (concat (:from left) (:from right))
|
||||||
|
:bindings (merge-with concat (:bindings left) (:bindings right))
|
||||||
|
:wheres (concat (:wheres left) (:wheres right))))
|
||||||
|
|
||||||
(defn- bindings->where
|
(defn- bindings->where
|
||||||
"Take a bindings map like
|
"Take a bindings map like
|
||||||
{?foo [:datoms12.e :datoms13.v :datoms14.e]}
|
{?foo [:datoms12.e :datoms13.v :datoms14.e]}
|
||||||
|
@ -108,24 +114,15 @@
|
||||||
(:wheres cc)))))
|
(:wheres cc)))))
|
||||||
|
|
||||||
;; Pattern building is recursive, so we need forward declarations.
|
;; Pattern building is recursive, so we need forward declarations.
|
||||||
(declare Not->NotJoinClause not-join->where-fragment)
|
(declare
|
||||||
|
Not->NotJoinClause not-join->where-fragment
|
||||||
|
simple-or? simple-or->cc)
|
||||||
|
|
||||||
;; Accumulates a pattern into the CC. Returns a new CC.
|
(defn- apply-pattern-clause-for-alias
|
||||||
(defn apply-pattern-clause
|
"This helper assumes that `cc` has already established a table association
|
||||||
"Transform a DataScript Pattern instance into the parts needed
|
for the provided alias."
|
||||||
to build a SQL expression.
|
[cc alias pattern]
|
||||||
|
(let [places (map vector
|
||||||
@arg cc A CC instance.
|
|
||||||
@arg pattern The pattern instance.
|
|
||||||
@return an augmented CC"
|
|
||||||
[cc pattern]
|
|
||||||
(when-not (instance? Pattern pattern)
|
|
||||||
(raise-str "Expected to be called with a Pattern instance." pattern))
|
|
||||||
(when-not (instance? DefaultSrc (:source pattern))
|
|
||||||
(raise-str "Non-default sources are not supported in patterns. Pattern: " pattern))
|
|
||||||
|
|
||||||
(let [[table alias] (source->from (:source cc)) ; e.g., [:datoms :datoms123]
|
|
||||||
places (map vector
|
|
||||||
(:pattern pattern)
|
(:pattern pattern)
|
||||||
(:columns (:source cc)))]
|
(:columns (:source cc)))]
|
||||||
(reduce
|
(reduce
|
||||||
|
@ -148,10 +145,32 @@
|
||||||
|
|
||||||
(raise-str "Unknown pattern part " pattern-part))))
|
(raise-str "Unknown pattern part " pattern-part))))
|
||||||
|
|
||||||
|
cc
|
||||||
|
places)))
|
||||||
|
|
||||||
|
;; Accumulates a pattern into the CC. Returns a new CC.
|
||||||
|
(defn apply-pattern-clause
|
||||||
|
"Transform a DataScript Pattern instance into the parts needed
|
||||||
|
to build a SQL expression.
|
||||||
|
|
||||||
|
@arg cc A CC instance.
|
||||||
|
@arg pattern The pattern instance.
|
||||||
|
@return an augmented CC"
|
||||||
|
[cc pattern]
|
||||||
|
(when-not (instance? Pattern pattern)
|
||||||
|
(raise-str "Expected to be called with a Pattern instance." pattern))
|
||||||
|
(when-not (instance? DefaultSrc (:source pattern))
|
||||||
|
(raise-str "Non-default sources are not supported in patterns. Pattern: " pattern))
|
||||||
|
|
||||||
|
(let [[table alias] (source->from (:source cc))] ; e.g., [:datoms :datoms123]
|
||||||
|
(apply-pattern-clause-for-alias
|
||||||
|
|
||||||
;; Record the new table mapping.
|
;; Record the new table mapping.
|
||||||
(util/conj-in cc [:from] [table alias])
|
(util/conj-in cc [:from] [table alias])
|
||||||
|
|
||||||
places)))
|
;; Use the new alias for columns.
|
||||||
|
alias
|
||||||
|
pattern)))
|
||||||
|
|
||||||
(defn- plain-symbol->sql-predicate-symbol [fn]
|
(defn- plain-symbol->sql-predicate-symbol [fn]
|
||||||
(when-not (instance? PlainSymbol fn)
|
(when-not (instance? PlainSymbol fn)
|
||||||
|
@ -205,18 +224,48 @@
|
||||||
(:bindings cc))
|
(:bindings cc))
|
||||||
not))))
|
not))))
|
||||||
|
|
||||||
|
(defn apply-or-clause [cc orc]
|
||||||
|
(when-not (instance? Or orc)
|
||||||
|
(raise "Expected to be called with a Or instance." {:clause orc}))
|
||||||
|
(when-not (instance? DefaultSrc (:source orc))
|
||||||
|
(raise "Non-default sources are not supported in `or` clauses." {:clause orc}))
|
||||||
|
|
||||||
|
;; A simple `or` is something like:
|
||||||
|
;;
|
||||||
|
;; (or [?foo :foo/bar ?baz]
|
||||||
|
;; [?foo :foo/noo ?baz])
|
||||||
|
;;
|
||||||
|
;; This can be converted into a single join and an `or` :where expression.
|
||||||
|
;;
|
||||||
|
;; Otherwise -- perhaps each leg of the `or` binds different variables, which
|
||||||
|
;; is acceptable for an `or-join` form -- we need to turn this into a joined
|
||||||
|
;; subquery.
|
||||||
|
|
||||||
|
(if (simple-or? orc)
|
||||||
|
(merge-ccs cc (simple-or->cc (:source cc)
|
||||||
|
(merge-with concat
|
||||||
|
(:external-bindings cc)
|
||||||
|
(:bindings cc))
|
||||||
|
orc))
|
||||||
|
|
||||||
|
;; TODO: handle And within the Or patterns.
|
||||||
|
(raise "Non-simple `or` clauses not yet supported." {:clause orc})))
|
||||||
|
|
||||||
;; We're keeping this simple for now: a straightforward type switch.
|
;; We're keeping this simple for now: a straightforward type switch.
|
||||||
(defn apply-clause [cc it]
|
(defn apply-clause [cc it]
|
||||||
(condp instance? it
|
(condp instance? it
|
||||||
|
Or
|
||||||
|
(apply-or-clause cc it)
|
||||||
|
|
||||||
Not
|
Not
|
||||||
(apply-not-clause cc it)
|
(apply-not-clause cc it)
|
||||||
|
|
||||||
Predicate
|
Predicate
|
||||||
(apply-predicate-clause cc it)
|
(apply-predicate-clause cc it)
|
||||||
|
|
||||||
Pattern
|
Pattern
|
||||||
(apply-pattern-clause cc it)
|
(apply-pattern-clause cc it)
|
||||||
|
|
||||||
(raise "Unknown clause." {:clause it})))
|
(raise "Unknown clause." {:clause it})))
|
||||||
|
|
||||||
(defn expand-pattern-clauses
|
(defn expand-pattern-clauses
|
||||||
|
@ -227,7 +276,12 @@
|
||||||
(defn patterns->cc [source patterns external-bindings]
|
(defn patterns->cc [source patterns external-bindings]
|
||||||
(expand-where-from-bindings
|
(expand-where-from-bindings
|
||||||
(expand-pattern-clauses
|
(expand-pattern-clauses
|
||||||
(->ConjoiningClauses source [] (or external-bindings {}) {} [])
|
(map->ConjoiningClauses
|
||||||
|
{:source source
|
||||||
|
:from []
|
||||||
|
:external-bindings (or external-bindings {})
|
||||||
|
:bindings {}
|
||||||
|
:wheres []})
|
||||||
patterns)))
|
patterns)))
|
||||||
|
|
||||||
(defn cc->partial-subquery
|
(defn cc->partial-subquery
|
||||||
|
@ -254,8 +308,7 @@
|
||||||
|
|
||||||
(defn Not->NotJoinClause [source external-bindings not]
|
(defn Not->NotJoinClause [source external-bindings not]
|
||||||
(when-not (instance? DefaultSrc (:source not))
|
(when-not (instance? DefaultSrc (:source not))
|
||||||
(raise-str "Non-default sources are not supported in patterns. Pattern: "
|
(raise "Non-default sources are not supported in `not` clauses." {:clause not}))
|
||||||
not))
|
|
||||||
(make-not-join-clause source external-bindings (:vars not) (:clauses not)))
|
(make-not-join-clause source external-bindings (:vars not) (:clauses not)))
|
||||||
|
|
||||||
(defn not-join->where-fragment [not-join]
|
(defn not-join->where-fragment [not-join]
|
||||||
|
@ -268,3 +321,81 @@
|
||||||
|
|
||||||
;; If it does establish bindings, then it has to be a subquery.
|
;; If it does establish bindings, then it has to be a subquery.
|
||||||
[:exists (merge {:select [1]} (cc->partial-subquery (:cc not-join)))])])
|
[:exists (merge {:select [1]} (cc->partial-subquery (:cc not-join)))])])
|
||||||
|
|
||||||
|
|
||||||
|
;; A simple Or clause is one in which each branch can be evaluated against
|
||||||
|
;; a single pattern match. That means that all the variables are in the same places.
|
||||||
|
;; We can produce a ConjoiningClauses in that case -- the :wheres will suffice
|
||||||
|
;; for alternation.
|
||||||
|
|
||||||
|
(defn validate-or-clause [orc]
|
||||||
|
(when-not (instance? DefaultSrc (:source orc))
|
||||||
|
(raise "Non-default sources are not supported in `or` clauses." {:clause orc}))
|
||||||
|
(when-not (nil? (:required (:rule-vars orc)))
|
||||||
|
(raise "We've never seen required rule-vars before." {:clause orc})))
|
||||||
|
|
||||||
|
(defn simple-or? [orc]
|
||||||
|
(let [clauses (:clauses orc)]
|
||||||
|
(and
|
||||||
|
;; Every pattern is a Pattern.
|
||||||
|
(every? (partial instance? Pattern) clauses)
|
||||||
|
|
||||||
|
(or
|
||||||
|
(= 1 (count clauses))
|
||||||
|
|
||||||
|
;; Every pattern has the same source, and every place is either the
|
||||||
|
;; same var or a fixed value. We ignore placeholders for now.
|
||||||
|
(let [template (first clauses)
|
||||||
|
template-source (:source template)]
|
||||||
|
(every? (fn [c]
|
||||||
|
(and (= (:source c) template-source)
|
||||||
|
(util/every-pair?
|
||||||
|
(fn [left right]
|
||||||
|
(condp instance? left
|
||||||
|
Variable (= left right)
|
||||||
|
Constant (instance? Constant right)
|
||||||
|
|
||||||
|
false))
|
||||||
|
(:pattern c) (:pattern template))))
|
||||||
|
(rest clauses)))))))
|
||||||
|
|
||||||
|
(defn simple-or->cc
|
||||||
|
"The returned CC has not yet had bindings expanded."
|
||||||
|
[source external-bindings orc]
|
||||||
|
(validate-or-clause orc)
|
||||||
|
|
||||||
|
;; We 'fork' a CC for each pattern, then union them together.
|
||||||
|
;; We need to build the first in order that the others use the same
|
||||||
|
;; column names.
|
||||||
|
(let [cc (map->ConjoiningClauses
|
||||||
|
{:source source
|
||||||
|
:from []
|
||||||
|
:external-bindings (or external-bindings {})
|
||||||
|
:bindings {}
|
||||||
|
:wheres []})
|
||||||
|
primary (apply-pattern-clause cc (first (:clauses orc)))
|
||||||
|
remainder (rest (:clauses orc))]
|
||||||
|
|
||||||
|
(if (empty? remainder)
|
||||||
|
;; That was easy.
|
||||||
|
primary
|
||||||
|
|
||||||
|
(let [template (assoc primary :wheres [])
|
||||||
|
alias (second (first (:from template)))
|
||||||
|
ccs (map (partial apply-pattern-clause-for-alias template alias)
|
||||||
|
remainder)]
|
||||||
|
|
||||||
|
;; Because this is a simple clause, we know that the first pattern established
|
||||||
|
;; any necessary bindings.
|
||||||
|
;; Take any new :wheres from each CC and combine them with :or.
|
||||||
|
(assoc primary :wheres
|
||||||
|
[(cons :or
|
||||||
|
(reduce (fn [acc cc]
|
||||||
|
(let [w (:wheres cc)]
|
||||||
|
(case (count w)
|
||||||
|
0 acc
|
||||||
|
1 (conj acc (first w))
|
||||||
|
|
||||||
|
(conj acc (cons :and w)))))
|
||||||
|
[]
|
||||||
|
(cons primary ccs)))])))))
|
||||||
|
|
|
@ -62,3 +62,11 @@
|
||||||
(when-let ~binding
|
(when-let ~binding
|
||||||
~@forms
|
~@forms
|
||||||
(recur))))
|
(recur))))
|
||||||
|
|
||||||
|
(defn every-pair? [f xs ys]
|
||||||
|
(or (and (empty? xs) (empty? ys))
|
||||||
|
(and (not (empty? xs))
|
||||||
|
(not (empty? ys))
|
||||||
|
(f (first xs) (first ys))
|
||||||
|
(recur f (rest xs) (rest ys)))))
|
||||||
|
|
||||||
|
|
|
@ -125,3 +125,42 @@
|
||||||
[?page :page/starred true ?t]
|
[?page :page/starred true ?t]
|
||||||
(not [?page :foo/bar _])
|
(not [?page :foo/bar _])
|
||||||
[?t :db/txInstant ?timestampMicros]]))))
|
[?t :db/txInstant ?timestampMicros]]))))
|
||||||
|
|
||||||
|
(deftest test-single-or
|
||||||
|
(is (= '{:select ([:datoms1.e :page]),
|
||||||
|
:modifiers [:distinct],
|
||||||
|
:from ([:datoms datoms0] [:datoms datoms1] [:datoms datoms2]),
|
||||||
|
:where (:and
|
||||||
|
[:= :datoms1.e :datoms0.e]
|
||||||
|
[:= :datoms1.e :datoms2.v]
|
||||||
|
[:= :datoms0.a "page/url"]
|
||||||
|
[:= :datoms0.v "http://example.com/"]
|
||||||
|
[:= :datoms1.a "page/title"]
|
||||||
|
[:= :datoms2.a "page/loves"])}
|
||||||
|
(expand
|
||||||
|
'[:find ?page :in $ ?latest :where
|
||||||
|
[?page :page/url "http://example.com/"]
|
||||||
|
[?page :page/title ?title]
|
||||||
|
(or
|
||||||
|
[?entity :page/loves ?page])]))))
|
||||||
|
|
||||||
|
(deftest test-simple-or
|
||||||
|
(is (= '{:select ([:datoms1.e :page]),
|
||||||
|
:modifiers [:distinct],
|
||||||
|
:from ([:datoms datoms0] [:datoms datoms1] [:datoms datoms2]),
|
||||||
|
:where (:and
|
||||||
|
[:= :datoms1.e :datoms0.e]
|
||||||
|
[:= :datoms1.e :datoms2.v]
|
||||||
|
[:= :datoms0.a "page/url"]
|
||||||
|
[:= :datoms0.v "http://example.com/"]
|
||||||
|
[:= :datoms1.a "page/title"]
|
||||||
|
(:or
|
||||||
|
[:= :datoms2.a "page/likes"]
|
||||||
|
[:= :datoms2.a "page/loves"]))}
|
||||||
|
(expand
|
||||||
|
'[:find ?page :in $ ?latest :where
|
||||||
|
[?page :page/url "http://example.com/"]
|
||||||
|
[?page :page/title ?title]
|
||||||
|
(or
|
||||||
|
[?entity :page/likes ?page]
|
||||||
|
[?entity :page/loves ?page])]))))
|
||||||
|
|
Loading…
Reference in a new issue