Implement type-aware querying. Fixes #14.
* Alter how clauses are concatenated. They now preserve order more accurately. * Track mappings between vars and extracted type columns. * Generate type code constraints. * Push known types down into :not. * Push known types down into :or. * Tests and test fixes.
This commit is contained in:
parent
2529378725
commit
1c6244db5b
11 changed files with 459 additions and 103 deletions
|
@ -159,6 +159,7 @@
|
|||
(defn datoms-source [db]
|
||||
(source/map->DatomsSource
|
||||
{:table :datoms
|
||||
:schema (:schema db)
|
||||
:fulltext-table :fulltext_values
|
||||
:fulltext-view :all_datoms
|
||||
:columns [:e :a :v :tx :added]
|
||||
|
|
|
@ -91,10 +91,11 @@
|
|||
(let [{:keys [find in with where]} find] ; Destructure the Datalog query.
|
||||
(validate-with with)
|
||||
(validate-in in)
|
||||
(let [external-bindings (in->bindings in)]
|
||||
(let [external-bindings (in->bindings in)
|
||||
known-types {}]
|
||||
(assoc context
|
||||
:elements (:elements find)
|
||||
:cc (clauses/patterns->cc (:default-source context) where external-bindings)))))
|
||||
:cc (clauses/patterns->cc (:default-source context) where known-types external-bindings)))))
|
||||
|
||||
(defn find->sql-clause
|
||||
"Take a parsed `find` expression and turn it into a structured SQL
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:refer [attribute-in-source
|
||||
constant-in-source]]
|
||||
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str cond-let]]
|
||||
[honeysql.core :as sql]
|
||||
[datascript.parser :as dp
|
||||
#?@(:cljs
|
||||
[:refer
|
||||
|
@ -52,27 +53,96 @@
|
|||
;;
|
||||
;; `from` is a list of [source alias] pairs, suitable for passing to honeysql.
|
||||
;; `bindings` is a map from var to qualified columns.
|
||||
;; `known-types` is a map from var to type keyword.
|
||||
;; `extracted-types` is a mapping, similar to `bindings`, but used to pull
|
||||
;; type tags out of the store at runtime.
|
||||
;; `wheres` is a list of fragments that can be joined by `:and`.
|
||||
(defrecord ConjoiningClauses [source from external-bindings bindings wheres])
|
||||
(defrecord ConjoiningClauses
|
||||
[source
|
||||
from ; [[:datoms 'datoms123]]
|
||||
external-bindings ; {?var0 (sql/param :foobar)}
|
||||
bindings ; {?var1 :datoms123.v}
|
||||
known-types ; {?var1 :db.type/integer}
|
||||
extracted-types ; {?var2 :datoms123.value_type_tag}
|
||||
wheres ; [[:= :datoms123.v 15]]
|
||||
])
|
||||
|
||||
(defn bind-column-to-var [cc variable col]
|
||||
(let [var (:symbol variable)]
|
||||
(util/conj-in cc [:bindings var] col)))
|
||||
(defn bind-column-to-var [cc variable table position]
|
||||
(let [var (:symbol variable)
|
||||
col (sql/qualify table (name position))
|
||||
bound (util/append-in cc [:bindings var] col)]
|
||||
(if (or (not (= position :v))
|
||||
(contains? (:known-types cc) var)
|
||||
(contains? (:extracted-types cc) var))
|
||||
;; Type known; no need to accumulate a type-binding.
|
||||
bound
|
||||
(let [tag-col (sql/qualify table :value_type_tag)]
|
||||
(assoc-in bound [:extracted-types var] tag-col)))))
|
||||
|
||||
(defn constrain-column-to-constant [cc col position value]
|
||||
(util/conj-in cc [:wheres]
|
||||
[:= col (if (= :a position)
|
||||
(attribute-in-source (:source cc) value)
|
||||
(constant-in-source (:source cc) value))]))
|
||||
(defn constrain-column-to-constant [cc table position value]
|
||||
(let [col (sql/qualify table (name position))]
|
||||
(util/append-in cc
|
||||
[:wheres]
|
||||
[:= col (if (= :a position)
|
||||
(attribute-in-source (:source cc) value)
|
||||
(constant-in-source (:source cc) value))])))
|
||||
|
||||
(defn augment-cc [cc from bindings wheres]
|
||||
(defprotocol ITypeTagged (->tag-codes [x]))
|
||||
|
||||
(extend-protocol ITypeTagged
|
||||
#?@(:cljs
|
||||
[string (->tag-codes [x] #{4 10 11 12})
|
||||
Keyword (->tag-codes [x] #{13}) ; TODO: what about idents?
|
||||
boolean (->tag-codes [x] #{1})
|
||||
number (->tag-codes [x]
|
||||
(if (integer? x)
|
||||
#{0 4 5} ; Could be a ref or a number or a date.
|
||||
#{4 5}))]) ; Can't be a ref.
|
||||
#?@(:clj
|
||||
[String (->tag-codes [x] #{10})
|
||||
clojure.lang.Keyword (->tag-codes [x] #{13}) ; TODO: what about idents?
|
||||
Boolean (->tag-codes [x] #{1})
|
||||
Integer (->tag-codes [x] #{0 5}) ; Could be a ref or a number.
|
||||
Long (->tag-codes [x] #{0 5}) ; Could be a ref or a number.
|
||||
Float (->tag-codes [x] #{5})
|
||||
Double (->tag-codes [x] #{5})
|
||||
java.util.UUID (->tag-codes [x] #{11})
|
||||
java.util.Date (->tag-codes [x] #{4})
|
||||
java.net.URI (->tag-codes [x] #{12})]))
|
||||
|
||||
(defn constrain-value-column-to-constant
|
||||
"Constrain a `v` column. Note that this can contribute *two*
|
||||
constraints: one for the column itself, and one for the type tag.
|
||||
We don't need to do this if the attribute is known and thus
|
||||
constrains the type."
|
||||
[cc table-alias value]
|
||||
(let [possible-type-codes (->tag-codes value)
|
||||
aliased (sql/qualify table-alias (name :value_type_tag))
|
||||
clauses (map
|
||||
(fn [code] [:= aliased code])
|
||||
possible-type-codes)]
|
||||
(util/concat-in cc [:wheres]
|
||||
;; Type checks then value checks.
|
||||
[(case (count clauses)
|
||||
0 (raise-str "Unexpected number of clauses.")
|
||||
1 (first clauses)
|
||||
(cons :or clauses))
|
||||
[:= (sql/qualify table-alias (name :v))
|
||||
(constant-in-source (:source cc) value)]])))
|
||||
|
||||
(defn augment-cc [cc from bindings extracted-types wheres]
|
||||
(assoc cc
|
||||
:from (concat (:from cc) from)
|
||||
:bindings (merge-with concat (:bindings cc) bindings)
|
||||
:extracted-types (merge (:extracted-types cc) extracted-types)
|
||||
:wheres (concat (:wheres cc) wheres)))
|
||||
|
||||
(defn merge-ccs [left right]
|
||||
(augment-cc left (:from right) (:bindings right) (:wheres right)))
|
||||
(augment-cc left
|
||||
(:from right)
|
||||
(:bindings right)
|
||||
(:extracted-types right)
|
||||
(:wheres right)))
|
||||
|
||||
(defn- bindings->where
|
||||
"Take a bindings map like
|
||||
|
@ -115,9 +185,9 @@
|
|||
(impose-external-bindings
|
||||
(assoc cc :wheres
|
||||
;; Note that the order of clauses here means that cross-pattern var bindings
|
||||
;; come first. That's OK: the SQL engine considers these altogether.
|
||||
(concat (bindings->where (:bindings cc))
|
||||
(:wheres cc)))))
|
||||
;; come last That's OK: the SQL engine considers these altogether.
|
||||
(concat (:wheres cc)
|
||||
(bindings->where (:bindings cc))))))
|
||||
|
||||
(defn binding-for-symbol-or-throw [cc symbol]
|
||||
(let [internal-bindings (symbol (:bindings cc))
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
[datomish.query.cc :as cc]
|
||||
[datomish.query.functions :as functions]
|
||||
[datomish.query.source
|
||||
:refer [attribute-in-source
|
||||
:refer [pattern->schema-value-type
|
||||
attribute-in-source
|
||||
constant-in-source
|
||||
source->from
|
||||
source->constraints]]
|
||||
[datomish.schema :as schema]
|
||||
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str cond-let]]
|
||||
[datascript.parser :as dp
|
||||
#?@(:cljs
|
||||
|
@ -50,18 +52,48 @@
|
|||
Not->NotJoinClause not-join->where-fragment
|
||||
simple-or? simple-or->cc)
|
||||
|
||||
(defn- check-or-apply-value-type [cc value-type pattern-part]
|
||||
(if (nil? value-type)
|
||||
cc
|
||||
(condp instance? pattern-part
|
||||
Placeholder
|
||||
cc
|
||||
|
||||
Variable
|
||||
(let [var-sym (:symbol pattern-part)]
|
||||
(if-let [existing-type (var-sym (:known-types cc))]
|
||||
(if (= existing-type value-type)
|
||||
cc
|
||||
(raise "Var " var-sym " already has type " existing-type "; this pattern wants " value-type
|
||||
{:pattern pattern-part :value-type value-type}))
|
||||
(assoc-in cc [:known-types var-sym] value-type)))
|
||||
|
||||
Constant
|
||||
(do
|
||||
(or (and (= :db.type/ref value-type)
|
||||
(or (keyword? (:value pattern-part)) ; ident
|
||||
(integer? (:value pattern-part)))) ; entid
|
||||
(schema/ensure-value-matches-type value-type (:value pattern-part)))
|
||||
cc))))
|
||||
|
||||
(defn- apply-pattern-clause-for-alias
|
||||
"This helper assumes that `cc` has already established a table association
|
||||
for the provided alias."
|
||||
[cc alias pattern]
|
||||
(let [places (map vector
|
||||
(:pattern pattern)
|
||||
(:columns (:source cc)))]
|
||||
(let [pattern (:pattern pattern)
|
||||
columns (:columns (:source cc))
|
||||
places (map vector pattern columns)
|
||||
value-type (pattern->schema-value-type (:source cc) pattern)] ; Optional; e.g., :db.type/string
|
||||
(reduce
|
||||
(fn [cc
|
||||
[pattern-part ; ?x, :foo/bar, 42
|
||||
position]] ; :a
|
||||
(let [col (sql/qualify alias (name position))] ; :datoms123.a
|
||||
(let [cc (case position
|
||||
;; TODO: we should be able to constrain :e and :a to be
|
||||
;; entities... but the type checker expects that to be an int.
|
||||
:v (check-or-apply-value-type cc value-type pattern-part)
|
||||
:e (check-or-apply-value-type cc :db.type/ref pattern-part)
|
||||
cc)]
|
||||
(condp instance? pattern-part
|
||||
;; Placeholders don't contribute any bindings, nor do
|
||||
;; they constrain the query -- there's no need to produce
|
||||
|
@ -70,10 +102,16 @@
|
|||
cc
|
||||
|
||||
Variable
|
||||
(cc/bind-column-to-var cc pattern-part col)
|
||||
(cc/bind-column-to-var cc pattern-part alias position)
|
||||
|
||||
Constant
|
||||
(cc/constrain-column-to-constant cc col position (:value pattern-part))
|
||||
(if (and (nil? value-type)
|
||||
(= position :v))
|
||||
;; If we don't know the type, but we have a constant, generate
|
||||
;; a :wheres clause constraining the accompanying value_type_tag
|
||||
;; column.
|
||||
(cc/constrain-value-column-to-constant cc alias (:value pattern-part))
|
||||
(cc/constrain-column-to-constant cc alias position (:value pattern-part)))
|
||||
|
||||
(raise "Unknown pattern part." {:part pattern-part :clause pattern}))))
|
||||
|
||||
|
@ -105,7 +143,7 @@
|
|||
(apply-pattern-clause-for-alias
|
||||
|
||||
;; Record the new table mapping.
|
||||
(util/conj-in cc [:from] [table alias])
|
||||
(util/append-in cc [:from] [table alias])
|
||||
|
||||
;; Use the new alias for columns.
|
||||
alias
|
||||
|
@ -124,7 +162,7 @@
|
|||
(raise-str "Unknown function " (:fn predicate)))
|
||||
|
||||
(let [args (map (partial cc/argument->value cc) (:args predicate))]
|
||||
(util/conj-in cc [:wheres] (cons f args)))))
|
||||
(util/append-in cc [:wheres] (cons f args)))))
|
||||
|
||||
(defn apply-not-clause [cc not]
|
||||
(when-not (instance? Not not)
|
||||
|
@ -136,13 +174,19 @@
|
|||
;; fragment, and include the external bindings so that they match up.
|
||||
;; Otherwise, we need to delay. Right now we're lazy, so we just fail:
|
||||
;; reorder your query yourself.
|
||||
(util/conj-in cc [:wheres]
|
||||
(not-join->where-fragment
|
||||
(Not->NotJoinClause (:source cc)
|
||||
(merge-with concat
|
||||
(:external-bindings cc)
|
||||
(:bindings cc))
|
||||
not))))
|
||||
;;
|
||||
;; Note that we don't extract and reuse any types established inside
|
||||
;; the `not` clause: perhaps those won't make sense outside. But it's
|
||||
;; a filter, so we push the external types _in_.
|
||||
(util/append-in cc
|
||||
[:wheres]
|
||||
(not-join->where-fragment
|
||||
(Not->NotJoinClause (:source cc)
|
||||
(:known-types cc)
|
||||
(merge-with concat
|
||||
(:external-bindings cc)
|
||||
(:bindings cc))
|
||||
not))))
|
||||
|
||||
(defn apply-or-clause [cc orc]
|
||||
(when-not (instance? Or orc)
|
||||
|
@ -163,6 +207,7 @@
|
|||
|
||||
(if (simple-or? orc)
|
||||
(cc/merge-ccs cc (simple-or->cc (:source cc)
|
||||
(:known-types cc)
|
||||
(merge-with concat
|
||||
(:external-bindings cc)
|
||||
(:bindings cc))
|
||||
|
@ -200,12 +245,14 @@
|
|||
[cc patterns]
|
||||
(reduce apply-clause cc patterns))
|
||||
|
||||
(defn patterns->cc [source patterns external-bindings]
|
||||
(defn patterns->cc [source patterns known-types external-bindings]
|
||||
(cc/expand-where-from-bindings
|
||||
(expand-pattern-clauses
|
||||
(cc/map->ConjoiningClauses
|
||||
{:source source
|
||||
:from []
|
||||
:known-types (or known-types {})
|
||||
:extracted-types {}
|
||||
:external-bindings (or external-bindings {})
|
||||
:bindings {}
|
||||
:wheres []})
|
||||
|
@ -230,13 +277,12 @@
|
|||
;; that a declared variable list is valid for the clauses given.
|
||||
(defrecord NotJoinClause [unify-vars cc])
|
||||
|
||||
(defn make-not-join-clause [source external-bindings unify-vars patterns]
|
||||
(->NotJoinClause unify-vars (patterns->cc source patterns external-bindings)))
|
||||
|
||||
(defn Not->NotJoinClause [source external-bindings not]
|
||||
(defn Not->NotJoinClause [source known-types external-bindings not]
|
||||
(when-not (instance? DefaultSrc (:source not))
|
||||
(raise "Non-default sources are not supported in `not` clauses." {:clause not}))
|
||||
(make-not-join-clause source external-bindings (:vars not) (:clauses not)))
|
||||
(map->NotJoinClause
|
||||
{:unify-vars (:vars not)
|
||||
:cc (patterns->cc source (:clauses not) known-types external-bindings)}))
|
||||
|
||||
(defn not-join->where-fragment [not-join]
|
||||
[:not
|
||||
|
@ -288,15 +334,17 @@
|
|||
|
||||
(defn simple-or->cc
|
||||
"The returned CC has not yet had bindings expanded."
|
||||
[source external-bindings orc]
|
||||
[source known-types 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.
|
||||
;; column names and known types.
|
||||
(let [cc (cc/map->ConjoiningClauses
|
||||
{:source source
|
||||
:from []
|
||||
:known-types (or known-types {})
|
||||
:extracted-types {}
|
||||
:external-bindings (or external-bindings {})
|
||||
:bindings {}
|
||||
:wheres []})
|
||||
|
@ -307,6 +355,9 @@
|
|||
;; That was easy.
|
||||
primary
|
||||
|
||||
;; Note that for a simple `or` clause, the same template is used for each,
|
||||
;; so we can simply use the `extracted-types` bindings from `primary`.
|
||||
;; A complex `or` is much harder to handle.
|
||||
(let [template (assoc primary :wheres [])
|
||||
alias (second (first (:from template)))
|
||||
ccs (map (partial apply-pattern-clause-for-alias template alias)
|
||||
|
@ -315,7 +366,8 @@
|
|||
;; 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
|
||||
(assoc primary
|
||||
:wheres
|
||||
[(cons :or
|
||||
(reduce (fn [acc cc]
|
||||
(let [w (:wheres cc)]
|
||||
|
|
|
@ -35,13 +35,26 @@
|
|||
@param context A Context, containing elements.
|
||||
@return a sequence of pairs."
|
||||
[context]
|
||||
(def foo context)
|
||||
(let [elements (:elements context)]
|
||||
(let [elements (:elements context)
|
||||
cc (:cc context)
|
||||
known-types (:known-types cc)
|
||||
extracted-types (:extracted-types cc)]
|
||||
|
||||
(when-not (every? #(instance? Variable %1) elements)
|
||||
(raise-str "Unable to :find non-variables."))
|
||||
(map (fn [elem]
|
||||
(let [var (:symbol elem)]
|
||||
[(lookup-variable (:cc context) var) (util/var->sql-var var)]))
|
||||
|
||||
;; If the type of a variable isn't explicitly known, we also select
|
||||
;; its type column so we can transform it.
|
||||
(mapcat (fn [elem]
|
||||
(let [var (:symbol elem)
|
||||
lookup-var (lookup-variable cc var)
|
||||
projected-var (util/var->sql-var var)
|
||||
var-projection [lookup-var projected-var]]
|
||||
(if (or (contains? known-types var)
|
||||
(not (contains? extracted-types var)))
|
||||
[var-projection]
|
||||
[var-projection [(get extracted-types var)
|
||||
(util/var->sql-type-var var)]])))
|
||||
elements)))
|
||||
|
||||
(defn row-pair-transducer [context]
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
(ns datomish.query.source
|
||||
(:require
|
||||
[datomish.query.transforms :as transforms]
|
||||
[datomish.schema :as schema]
|
||||
[datascript.parser
|
||||
#?@(:cljs
|
||||
[:refer [Variable Constant Placeholder]])])
|
||||
|
@ -39,6 +40,7 @@
|
|||
(source->fulltext-from [source]
|
||||
"Returns a pair, `[table alias]` for querying the source's fulltext index.")
|
||||
(source->constraints [source alias])
|
||||
(pattern->schema-value-type [source pattern])
|
||||
(attribute-in-source [source attribute])
|
||||
(constant-in-source [source constant]))
|
||||
|
||||
|
@ -48,6 +50,7 @@
|
|||
fulltext-table ; Typically :fulltext_values
|
||||
fulltext-view ; Typically :all_datoms
|
||||
columns ; e.g., [:e :a :v :tx]
|
||||
schema ; An ISchema instance.
|
||||
|
||||
;; `attribute-transform` is a function from attribute to constant value. Used to
|
||||
;; turn, e.g., :p/attribute into an interned integer.
|
||||
|
@ -88,6 +91,19 @@
|
|||
(when-let [f (:make-constraints source)]
|
||||
(f alias)))
|
||||
|
||||
(pattern->schema-value-type [source pattern]
|
||||
(let [[_ a v _] pattern
|
||||
schema (:schema (:schema source))]
|
||||
(when (instance? Constant a)
|
||||
(let [val (:value a)]
|
||||
(if (keyword? val)
|
||||
;; We need to find the entid for the keyword attribute,
|
||||
;; because the schema stores attributes by ID.
|
||||
(let [id (attribute-in-source source val)]
|
||||
(get-in schema [id :db/valueType]))
|
||||
(when (integer? val)
|
||||
(get-in schema [val :db/valueType])))))))
|
||||
|
||||
(attribute-in-source [source attribute]
|
||||
((:attribute-transform source) attribute))
|
||||
|
||||
|
|
|
@ -105,12 +105,26 @@
|
|||
:db.type/string { :valid? string? }
|
||||
:db.type/boolean { :valid? #?(:clj #(instance? Boolean %) :cljs #(= js/Boolean (type %))) }
|
||||
:db.type/long { :valid? integer? }
|
||||
:db.type/uuid { :valid? #?(:clj #(instance? java.util.UUID %) :cljs string?) }
|
||||
:db.type/instant { :valid? #?(:clj #(instance? java.util.Date %) :cljs #(= js/Date (type %))) }
|
||||
:db.type/uri { :valid? #?(:clj #(instance? java.net.URI %) :cljs string?) }
|
||||
:db.type/double { :valid? #?(:clj float? :cljs number?) }
|
||||
})
|
||||
|
||||
(defn #?@(:clj [^Boolean ensure-value-matches-type]
|
||||
:cljs [^boolean ensure-value-matches-type]) [type value]
|
||||
(if-let [valid? (get-in value-type-map [type :valid?])]
|
||||
(when-not (valid? value)
|
||||
(raise "Invalid value for type " type "; got " value
|
||||
{:error :schema/valueType, :type type, :value value}))
|
||||
(raise "Unknown valueType " type ", expected one of " (sorted-set (keys value-type-map))
|
||||
{:error :schema/valueType, :type type})))
|
||||
|
||||
;; There's some duplication here so we get better error messages.
|
||||
(defn #?@(:clj [^Boolean ensure-valid-value]
|
||||
:cljs [^boolean ensure-valid-value]) [schema attr value]
|
||||
{:pre [(schema? schema)]}
|
||||
{:pre [(schema? schema)
|
||||
(integer? attr)]}
|
||||
(let [schema (.-schema schema)]
|
||||
(if-let [valueType (get-in schema [attr :db/valueType])]
|
||||
(if-let [valid? (get-in value-type-map [valueType :valid?])]
|
||||
|
@ -123,7 +137,8 @@
|
|||
{:error :schema/valueType, :attribute attr}))))
|
||||
|
||||
(defn ->SQLite [schema attr value]
|
||||
{:pre [(schema? schema)]}
|
||||
{:pre [(schema? schema)
|
||||
(integer? attr)]}
|
||||
(let [schema (.-schema schema)]
|
||||
(if-let [valueType (get-in schema [attr :db/valueType])]
|
||||
(if-let [valid? (get-in value-type-map [valueType :valid?])]
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
(case tag
|
||||
0 value ; ref.
|
||||
1 (= value 1) ; boolean
|
||||
4 (new Date value) ; instant
|
||||
4 (js/Date. value) ; instant
|
||||
13 (keyword (subs value 1)) ; keyword
|
||||
; 12 value ; URI
|
||||
; 11 value ; UUID
|
||||
|
|
|
@ -30,6 +30,14 @@
|
|||
~expr
|
||||
(cond-let ~@rest)))))
|
||||
|
||||
(defn var->sql-type-var
|
||||
"Turns '?xyz into :_xyz_type_tag."
|
||||
[x]
|
||||
(if (and (symbol? x)
|
||||
(str/starts-with? (name x) "?"))
|
||||
(keyword (str "_" (subs (name x) 1) "_type_tag"))
|
||||
(throw (ex-info (str x " is not a Datalog var.") {}))))
|
||||
|
||||
(defn var->sql-var
|
||||
"Turns '?xyz into :xyz."
|
||||
[x]
|
||||
|
@ -38,18 +46,6 @@
|
|||
(keyword (subs (name x) 1))
|
||||
(throw (ex-info (str x " is not a Datalog var.") {}))))
|
||||
|
||||
(defn conj-in
|
||||
"Associates a value into a sequence in a nested associative structure, where
|
||||
ks is a sequence of keys and v is the new value, and returns a new nested
|
||||
structure.
|
||||
If any levels do not exist, hash-maps will be created. If the destination
|
||||
sequence does not exist, a new one is created."
|
||||
{:static true}
|
||||
[m [k & ks] v]
|
||||
(if ks
|
||||
(assoc m k (conj-in (get m k) ks v))
|
||||
(assoc m k (conj (get m k) v))))
|
||||
|
||||
(defn concat-in
|
||||
{:static true}
|
||||
[m [k & ks] vs]
|
||||
|
@ -57,6 +53,17 @@
|
|||
(assoc m k (concat-in (get m k) ks vs))
|
||||
(assoc m k (concat (get m k) vs))))
|
||||
|
||||
(defn append-in
|
||||
"Associates a value into a sequence in a nested associative structure, where
|
||||
ks is a sequence of keys and v is the new value, and returns a new nested
|
||||
structure.
|
||||
Always puts the value last.
|
||||
If any levels do not exist, hash-maps will be created. If the destination
|
||||
sequence does not exist, a new one is created."
|
||||
{:static true}
|
||||
[m path v]
|
||||
(concat-in m path [v]))
|
||||
|
||||
(defmacro while-let [binding & forms]
|
||||
`(loop []
|
||||
(when-let ~binding
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[datomish.query.source :as source]
|
||||
[datomish.query.transforms :as transforms]
|
||||
[datomish.query :as query]
|
||||
[datomish.schema :as schema]
|
||||
#?@(:clj
|
||||
[
|
||||
[honeysql.core :as sql :refer [param]]
|
||||
|
@ -12,7 +13,9 @@
|
|||
[
|
||||
[honeysql.core :as sql :refer-macros [param]]
|
||||
[cljs.test :as t :refer-macros [is are deftest testing]]])
|
||||
))
|
||||
)
|
||||
#?(:clj
|
||||
(:import [clojure.lang ExceptionInfo])))
|
||||
|
||||
(defn- fgensym [s c]
|
||||
(symbol (str s c)))
|
||||
|
@ -25,7 +28,18 @@
|
|||
([s]
|
||||
(fgensym s (dec (swap! counter inc)))))))
|
||||
|
||||
(defn mock-source [db]
|
||||
(def simple-schema
|
||||
{:db/txInstant {:db/ident :db/txInstant
|
||||
:db/valueType :long
|
||||
:db/cardinality :db.cardinality/one}
|
||||
:foo/int {:db/ident :foo/int
|
||||
:db/valueType :db.type/integer
|
||||
:db/cardinality :db.cardinality/one}
|
||||
:foo/str {:db/ident :foo/str
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/many}})
|
||||
|
||||
(defn mock-source [db schema]
|
||||
(source/map->DatomsSource
|
||||
{:table :datoms
|
||||
:fulltext-table :fulltext_values
|
||||
|
@ -34,39 +48,105 @@
|
|||
:attribute-transform transforms/attribute-transform-string
|
||||
:constant-transform transforms/constant-transform-default
|
||||
:table-alias (comp (make-predictable-gensym) name)
|
||||
:schema (schema/map->Schema
|
||||
{:schema schema
|
||||
:rschema nil})
|
||||
:make-constraints nil}))
|
||||
|
||||
(defn- expand [find]
|
||||
(let [context (context/->Context (mock-source nil) nil nil)
|
||||
(defn- expand [find schema]
|
||||
(let [context (context/->Context (mock-source nil schema) nil nil)
|
||||
parsed (query/parse find)]
|
||||
(query/find->sql-clause context parsed)))
|
||||
|
||||
(deftest test-basic-join
|
||||
(is (= {:select '([:datoms1.v :timestampMicros] [:datoms0.e :page]),
|
||||
:modifiers [:distinct],
|
||||
:from '[[:datoms datoms0]
|
||||
[:datoms datoms1]],
|
||||
:where (list
|
||||
:and
|
||||
[:= :datoms1.e :datoms0.tx]
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.v 1]
|
||||
[:= :datoms1.a "db/txInstant"]
|
||||
[:not
|
||||
(list :and (list :> :datoms1.e (sql/param :latest)))])}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
[?t :db/txInstant ?timestampMicros]
|
||||
(not [(> ?t ?latest)])]))))
|
||||
(defn- populate [find schema]
|
||||
(let [context (context/->Context (mock-source nil schema) nil nil)
|
||||
parsed (query/parse find)]
|
||||
(query/find-into-context context parsed)))
|
||||
|
||||
(deftest test-pattern-not-join
|
||||
(is (= '{:select ([:datoms1.v :timestampMicros] [:datoms0.e :page]),
|
||||
(deftest test-type-extraction
|
||||
(testing "Variable entity."
|
||||
(is (= (:known-types (:cc (populate '[:find ?e ?v :in $ :where [?e :foo/int ?v]] simple-schema)))
|
||||
{'?v :db.type/integer
|
||||
'?e :db.type/ref})))
|
||||
(testing "Numeric entid."
|
||||
(is (= (:known-types (:cc (populate '[:find ?v :in $ :where [6 :foo/int ?v]] simple-schema)))
|
||||
{'?v :db.type/integer})))
|
||||
(testing "Keyword entity."
|
||||
(is (= (:known-types (:cc (populate '[:find ?v :in $ :where [:my/thing :foo/int ?v]] simple-schema)))
|
||||
{'?v :db.type/integer}))))
|
||||
|
||||
(deftest test-value-constant-constraint-descends-into-not-and-or
|
||||
(testing "Elision of types inside a join."
|
||||
(is (= '{:select ([:datoms0.e :e]
|
||||
[:datoms0.v :v]),
|
||||
:modifiers [:distinct],
|
||||
:from [[:datoms datoms0]],
|
||||
:where (:and
|
||||
[:= :datoms0.a "foo/int"]
|
||||
[:not
|
||||
[:exists
|
||||
{:select [1],
|
||||
:from [[:all_datoms all_datoms1]],
|
||||
:where (:and
|
||||
[:= :all_datoms1.e 15]
|
||||
[:= :datoms0.v :all_datoms1.v])}]])}
|
||||
(expand
|
||||
'[:find ?e ?v :in $ :where
|
||||
[?e :foo/int ?v]
|
||||
(not [15 ?a ?v])]
|
||||
simple-schema))))
|
||||
|
||||
(testing "Type collisions inside :not."
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"\?v already has type :db\.type\/integer"
|
||||
(expand
|
||||
'[:find ?e ?v :in $ :where
|
||||
[?e :foo/int ?v]
|
||||
(not [15 :foo/str ?v])]
|
||||
simple-schema))))
|
||||
(testing "Type collisions inside :or"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"\?v already has type :db\.type\/integer"
|
||||
(expand
|
||||
'[:find ?e ?v :in $ :where
|
||||
[?e :foo/int ?v]
|
||||
(or
|
||||
[15 :foo/str ?v]
|
||||
[10 :foo/int ?v])]
|
||||
simple-schema)))))
|
||||
|
||||
(deftest test-type-collision
|
||||
(let [find '[:find ?e ?v :in $
|
||||
:where
|
||||
[?e :foo/int ?v]
|
||||
[?x :foo/str ?v]]]
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"\?v already has type :db\.type\/integer"
|
||||
(populate find simple-schema)))))
|
||||
|
||||
(deftest test-value-constant-constraint
|
||||
(is (= {:select '([:all_datoms0.e :foo]),
|
||||
:modifiers [:distinct],
|
||||
:from '[[:all_datoms all_datoms0]],
|
||||
:where (list :and
|
||||
(list :or
|
||||
[:= :all_datoms0.value_type_tag 0]
|
||||
[:= :all_datoms0.value_type_tag 5])
|
||||
[:= :all_datoms0.v 99])}
|
||||
(expand
|
||||
'[:find ?foo :in $ :where
|
||||
[?foo _ 99]]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-value-constant-constraint-elided-using-schema
|
||||
(testing "There's no need to produce value_type_tag constraints when the attribute is specified."
|
||||
(is
|
||||
(= '{:select ([:datoms1.v :timestampMicros] [:datoms0.e :page]),
|
||||
:modifiers [:distinct],
|
||||
:from [[:datoms datoms0]
|
||||
[:datoms datoms1]],
|
||||
:where (:and
|
||||
[:= :datoms1.e :datoms0.tx]
|
||||
;; We don't need a type check on the range of page/starred...
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.v 1]
|
||||
[:= :datoms1.a "db/txInstant"]
|
||||
|
@ -76,12 +156,65 @@
|
|||
:from [[:datoms datoms2]],
|
||||
:where (:and
|
||||
[:= :datoms2.a "foo/bar"]
|
||||
[:= :datoms0.e :datoms2.e])}]])}
|
||||
[:= :datoms0.e :datoms2.e])}]]
|
||||
[:= :datoms0.tx :datoms1.e])}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
[?t :db/txInstant ?timestampMicros]
|
||||
(not [?page :foo/bar _])]))))
|
||||
(not [?page :foo/bar _])]
|
||||
|
||||
(merge
|
||||
simple-schema
|
||||
{:page/starred {:db/valueType :db.type/boolean
|
||||
:db/ident :page/starred
|
||||
:db/cardinality :db.cardinality/one}}))))))
|
||||
|
||||
(deftest test-basic-join
|
||||
(is (= {:select '([:datoms1.v :timestampMicros] [:datoms0.e :page]),
|
||||
:modifiers [:distinct],
|
||||
:from '[[:datoms datoms0]
|
||||
[:datoms datoms1]],
|
||||
:where (list
|
||||
:and
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.value_type_tag 1] ; boolean
|
||||
[:= :datoms0.v 1]
|
||||
[:= :datoms1.a "db/txInstant"]
|
||||
[:not
|
||||
(list :and (list :> :datoms0.tx (sql/param :latest)))]
|
||||
[:= :datoms0.tx :datoms1.e])}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
[?t :db/txInstant ?timestampMicros]
|
||||
(not [(> ?t ?latest)])]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-pattern-not-join
|
||||
(is (= '{:select ([:datoms1.v :timestampMicros] [:datoms0.e :page]),
|
||||
:modifiers [:distinct],
|
||||
:from [[:datoms datoms0]
|
||||
[:datoms datoms1]],
|
||||
:where (:and
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.value_type_tag 1] ; boolean
|
||||
[:= :datoms0.v 1]
|
||||
[:= :datoms1.a "db/txInstant"]
|
||||
[:not
|
||||
[:exists
|
||||
{:select [1],
|
||||
:from [[:datoms datoms2]],
|
||||
:where (:and
|
||||
[:= :datoms2.a "foo/bar"]
|
||||
[:= :datoms0.e :datoms2.e])}]]
|
||||
[:= :datoms0.tx :datoms1.e])}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
[?t :db/txInstant ?timestampMicros]
|
||||
(not [?page :foo/bar _])]
|
||||
simple-schema))))
|
||||
|
||||
;; Note that clause ordering is not directly correlated to the output: cross-bindings end up
|
||||
;; at the front. The SQL engine will do its own analysis. See `clauses/expand-where-from-bindings`.
|
||||
|
@ -92,17 +225,20 @@
|
|||
[:datoms datoms1]],
|
||||
:where (list
|
||||
:and
|
||||
[:= :datoms1.e :datoms0.tx]
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.value_type_tag 1] ; boolean
|
||||
[:= :datoms0.v 1]
|
||||
[:not
|
||||
(list :and (list :> :datoms0.tx (sql/param :latest)))]
|
||||
[:= :datoms1.a "db/txInstant"])}
|
||||
[:= :datoms1.a "db/txInstant"]
|
||||
[:= :datoms0.tx :datoms1.e]
|
||||
)}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
(not [(> ?t ?latest)])
|
||||
[?t :db/txInstant ?timestampMicros]]))))
|
||||
[?t :db/txInstant ?timestampMicros]]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-pattern-not-join-ordering-preserved
|
||||
(is (= '{:select ([:datoms2.v :timestampMicros] [:datoms0.e :page]),
|
||||
|
@ -110,8 +246,8 @@
|
|||
:from [[:datoms datoms0]
|
||||
[:datoms datoms2]],
|
||||
:where (:and
|
||||
[:= :datoms2.e :datoms0.tx]
|
||||
[:= :datoms0.a "page/starred"]
|
||||
[:= :datoms0.value_type_tag 1] ; boolean
|
||||
[:= :datoms0.v 1]
|
||||
[:not
|
||||
[:exists
|
||||
|
@ -121,48 +257,77 @@
|
|||
[:= :datoms1.a "foo/bar"]
|
||||
[:= :datoms0.e :datoms1.e])}]]
|
||||
[:= :datoms2.a "db/txInstant"]
|
||||
[:= :datoms0.tx :datoms2.e]
|
||||
)}
|
||||
(expand
|
||||
'[:find ?timestampMicros ?page :in $ ?latest :where
|
||||
[?page :page/starred true ?t]
|
||||
(not [?page :foo/bar _])
|
||||
[?t :db/txInstant ?timestampMicros]]))))
|
||||
[?t :db/txInstant ?timestampMicros]]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-single-or
|
||||
(is (= '{:select ([:datoms1.e :page]),
|
||||
(is (= '{:select ([:datoms0.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.value_type_tag 10]
|
||||
[:= :datoms0.v "http://example.com/"]
|
||||
[:= :datoms1.a "page/title"]
|
||||
[:= :datoms2.a "page/loves"])}
|
||||
[:= :datoms2.a "page/loves"]
|
||||
[:= :datoms0.e :datoms1.e]
|
||||
[:= :datoms0.e :datoms2.v])}
|
||||
(expand
|
||||
'[:find ?page :in $ ?latest :where
|
||||
[?page :page/url "http://example.com/"]
|
||||
[?page :page/title ?title]
|
||||
(or
|
||||
[?entity :page/loves ?page])]))))
|
||||
[?entity :page/loves ?page])]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-simple-or
|
||||
(is (= '{:select ([:datoms1.e :page]),
|
||||
(is (= '{:select ([:datoms0.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.value_type_tag 10]
|
||||
[:= :datoms0.v "http://example.com/"]
|
||||
[:= :datoms1.a "page/title"]
|
||||
(:or
|
||||
[:= :datoms2.a "page/likes"]
|
||||
[:= :datoms2.a "page/loves"]))}
|
||||
[:= :datoms2.a "page/loves"])
|
||||
[:= :datoms0.e :datoms1.e]
|
||||
[:= :datoms0.e :datoms2.v])}
|
||||
(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])]))))
|
||||
[?entity :page/loves ?page])]
|
||||
simple-schema))))
|
||||
|
||||
(deftest test-tag-projection
|
||||
(is (= '{:select ([:datoms0.e :page]
|
||||
[:datoms1.v :thing]
|
||||
[:datoms1.value_type_tag :_thing_type_tag]),
|
||||
:modifiers [:distinct],
|
||||
:from ([:datoms datoms0]
|
||||
[:datoms datoms1]),
|
||||
:where (:and
|
||||
[:= :datoms0.a "page/url"]
|
||||
[:= :datoms0.value_type_tag 10]
|
||||
[:= :datoms0.v "http://example.com/"]
|
||||
(:or
|
||||
[:= :datoms1.a "page/likes"]
|
||||
[:= :datoms1.a "page/loves"])
|
||||
[:= :datoms0.e :datoms1.e])}
|
||||
(expand
|
||||
'[:find ?page ?thing :in $ ?latest :where
|
||||
[?page :page/url "http://example.com/"]
|
||||
(or
|
||||
[?page :page/likes ?thing]
|
||||
[?page :page/loves ?thing])]
|
||||
simple-schema))))
|
||||
|
|
|
@ -9,6 +9,22 @@
|
|||
(is (= :x (util/var->sql-var '?x)))
|
||||
(is (= :XX (util/var->sql-var '?XX))))
|
||||
|
||||
#?(:cljs
|
||||
(deftest test-integer?-js
|
||||
(is (integer? 0))
|
||||
(is (integer? 5))
|
||||
(is (integer? 50000000000))
|
||||
(is (integer? 5.00)) ; Because JS.
|
||||
(is (not (integer? 5.1)))))
|
||||
|
||||
#?(:clj
|
||||
(deftest test-integer?-clj
|
||||
(is (integer? 0))
|
||||
(is (integer? 5))
|
||||
(is (integer? 50000000000))
|
||||
(is (not (integer? 5.00)))
|
||||
(is (not (integer? 5.1)))))
|
||||
|
||||
#?(:cljs
|
||||
(deftest test-raise
|
||||
(let [caught
|
||||
|
|
Loading…
Reference in a new issue