diff --git a/src/datomish/db.cljc b/src/datomish/db.cljc index d99f2b62..7621928f 100644 --- a/src/datomish/db.cljc +++ b/src/datomish/db.cljc @@ -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] diff --git a/src/datomish/query.cljc b/src/datomish/query.cljc index 2279c996..7c27277a 100644 --- a/src/datomish/query.cljc +++ b/src/datomish/query.cljc @@ -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 diff --git a/src/datomish/query/cc.cljc b/src/datomish/query/cc.cljc index 324bc3d3..38f8abcc 100644 --- a/src/datomish/query/cc.cljc +++ b/src/datomish/query/cc.cljc @@ -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)) diff --git a/src/datomish/query/clauses.cljc b/src/datomish/query/clauses.cljc index 26d55dee..001cd2a5 100644 --- a/src/datomish/query/clauses.cljc +++ b/src/datomish/query/clauses.cljc @@ -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)] diff --git a/src/datomish/query/projection.cljc b/src/datomish/query/projection.cljc index f71a3ec6..3301a36d 100644 --- a/src/datomish/query/projection.cljc +++ b/src/datomish/query/projection.cljc @@ -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] diff --git a/src/datomish/query/source.cljc b/src/datomish/query/source.cljc index 4a054e53..cc5a7562 100644 --- a/src/datomish/query/source.cljc +++ b/src/datomish/query/source.cljc @@ -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)) diff --git a/src/datomish/schema.cljc b/src/datomish/schema.cljc index 7c4ad286..785d375b 100644 --- a/src/datomish/schema.cljc +++ b/src/datomish/schema.cljc @@ -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?])] diff --git a/src/datomish/sqlite_schema.cljc b/src/datomish/sqlite_schema.cljc index 0429a8e3..b865cbbd 100644 --- a/src/datomish/sqlite_schema.cljc +++ b/src/datomish/sqlite_schema.cljc @@ -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 diff --git a/src/datomish/util.cljc b/src/datomish/util.cljc index 31e25bb6..8e1da980 100644 --- a/src/datomish/util.cljc +++ b/src/datomish/util.cljc @@ -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 diff --git a/test/datomish/test/query.cljc b/test/datomish/test/query.cljc index e16f3723..4fde079a 100644 --- a/test/datomish/test/query.cljc +++ b/test/datomish/test/query.cljc @@ -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)))) diff --git a/test/datomish/test/util.cljc b/test/datomish/test/util.cljc index 4bf83d15..c8315f75 100644 --- a/test/datomish/test/util.cljc +++ b/test/datomish/test/util.cljc @@ -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