Implement get-else.
This commit is contained in:
parent
38cd30a895
commit
f225dbe734
3 changed files with 140 additions and 9 deletions
|
@ -6,7 +6,12 @@
|
||||||
(:require
|
(:require
|
||||||
[honeysql.format :as fmt]
|
[honeysql.format :as fmt]
|
||||||
[datomish.query.cc :as cc]
|
[datomish.query.cc :as cc]
|
||||||
[datomish.query.source :as source]
|
[datomish.schema :as schema]
|
||||||
|
[datomish.sqlite-schema :refer [->tag ->SQLite]]
|
||||||
|
[datomish.query.source
|
||||||
|
:as source
|
||||||
|
:refer [attribute-in-source
|
||||||
|
constant-in-source]]
|
||||||
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise 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
|
||||||
|
@ -136,9 +141,95 @@
|
||||||
|
|
||||||
(cc/augment-cc cc from bindings wheres)))
|
(cc/augment-cc cc from bindings wheres)))
|
||||||
|
|
||||||
|
;; get-else is how Datalog handles optional attributes.
|
||||||
|
;;
|
||||||
|
;; It consists of:
|
||||||
|
;; * A bound entity
|
||||||
|
;; * A cardinality-one attribute
|
||||||
|
;; * A var to bind the value
|
||||||
|
;; * A default value.
|
||||||
|
;;
|
||||||
|
;; We model this as:
|
||||||
|
;; * A check against known bindings for the entity.
|
||||||
|
;; * A check against the schema for cardinality-one.
|
||||||
|
;; * Generating a COALESCE expression with a query inside the projection itself.
|
||||||
|
;;
|
||||||
|
;; Note that this will be messy for queries like:
|
||||||
|
;;
|
||||||
|
;; [:find ?page ?title :in $
|
||||||
|
;; :where [?page :page/url _]
|
||||||
|
;; [(get-else ?page :page/title "<empty>") ?title]
|
||||||
|
;; [_ :foo/quoted ?title]]
|
||||||
|
;;
|
||||||
|
;; or
|
||||||
|
;; [(some-function ?title)]
|
||||||
|
;;
|
||||||
|
;; -- we aren't really establishing a binding, so the subquery will be
|
||||||
|
;; repeated. But this will do for now.
|
||||||
|
(defn apply-get-else-clause [cc function]
|
||||||
|
(let [{:keys [source bindings external-bindings]} cc
|
||||||
|
schema (:schema source)
|
||||||
|
|
||||||
|
{:keys [args binding]} function
|
||||||
|
[src e a default-val] args]
|
||||||
|
|
||||||
|
(when-not (instance? BindScalar binding)
|
||||||
|
(raise-str "Expected scalar binding."))
|
||||||
|
(when-not (instance? Variable (:variable binding))
|
||||||
|
(raise-str "Expected variable binding."))
|
||||||
|
(when-not (instance? Constant a)
|
||||||
|
(raise-str "Expected constant attribute."))
|
||||||
|
(when-not (instance? Constant default-val)
|
||||||
|
(raise-str "Expected constant default value."))
|
||||||
|
(when-not (and (instance? SrcVar src)
|
||||||
|
(= "$" (name (:symbol src))))
|
||||||
|
(raise "Non-default sources not supported." {:arg src}))
|
||||||
|
|
||||||
|
(let [a (attribute-in-source source (:value a))
|
||||||
|
a-type (get-in (:schema schema) [a :db/valueType])
|
||||||
|
a-tag (->tag a-type)
|
||||||
|
|
||||||
|
default-val (:value default-val)
|
||||||
|
var (:variable binding)]
|
||||||
|
|
||||||
|
;; Schema check.
|
||||||
|
(when-not (and (integer? a)
|
||||||
|
(not (datomish.schema/multival? schema a)))
|
||||||
|
(raise-str "Attribute " a " is not cardinality-one."))
|
||||||
|
|
||||||
|
;; TODO: type-check the default value.
|
||||||
|
|
||||||
|
(condp instance? e
|
||||||
|
Variable
|
||||||
|
(let [e (:symbol e)
|
||||||
|
e-binding (cc/binding-for-symbol-or-throw cc e)]
|
||||||
|
|
||||||
|
(let [[table _] (source/source->from source a) ; We don't need to alias: single pattern.
|
||||||
|
;; These :limit values shouldn't be needed, but sqlite will
|
||||||
|
;; appreciate them.
|
||||||
|
;; Note that we don't extract type tags here: the attribute
|
||||||
|
;; must be known!
|
||||||
|
subquery {:select
|
||||||
|
[(sql/call
|
||||||
|
:coalesce
|
||||||
|
{:select [:v]
|
||||||
|
:from [table]
|
||||||
|
:where [:and
|
||||||
|
[:= 'a a]
|
||||||
|
[:= 'e e-binding]]
|
||||||
|
:limit 1}
|
||||||
|
(->SQLite default-val))]
|
||||||
|
:limit 1}]
|
||||||
|
(->
|
||||||
|
(assoc-in cc [:known-types (:symbol var)] a-type)
|
||||||
|
(util/append-in [:bindings (:symbol var)] subquery))))
|
||||||
|
|
||||||
|
(raise-str "Can't handle entity" e)))))
|
||||||
|
|
||||||
(def sql-functions
|
(def sql-functions
|
||||||
;; Future: versions of this that uses snippet() or matchinfo().
|
;; Future: versions of this that uses snippet() or matchinfo().
|
||||||
{"fulltext" apply-fulltext-clause})
|
{"fulltext" apply-fulltext-clause
|
||||||
|
"get-else" apply-get-else-clause})
|
||||||
|
|
||||||
(defn apply-sql-function
|
(defn apply-sql-function
|
||||||
"Either returns an application of `function` to `cc`, or nil to
|
"Either returns an application of `function` to `cc`, or nil to
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
(:require
|
(:require
|
||||||
[datomish.query.transforms :as transforms]
|
[datomish.query.transforms :as transforms]
|
||||||
[datomish.schema :as schema]
|
[datomish.schema :as schema]
|
||||||
|
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str]]
|
||||||
[datascript.parser
|
[datascript.parser
|
||||||
#?@(:cljs
|
#?@(:cljs
|
||||||
[:refer [Variable Constant Placeholder]])])
|
[:refer [Variable Constant Placeholder]])])
|
||||||
|
@ -68,15 +69,31 @@
|
||||||
Source
|
Source
|
||||||
|
|
||||||
(source->from [source attribute]
|
(source->from [source attribute]
|
||||||
(let [table
|
(let [schema (:schema source)
|
||||||
(if (and (instance? Constant attribute)
|
int->table (fn [a]
|
||||||
;; TODO: look in the DB schema to see if `attribute` is known to not be
|
(if (schema/fulltext? schema a)
|
||||||
;; a fulltext attribute.
|
(:fulltext-table source)
|
||||||
true)
|
(:table source)))
|
||||||
(:table source)
|
table
|
||||||
|
(cond
|
||||||
|
(integer? attribute)
|
||||||
|
(int->table attribute)
|
||||||
|
|
||||||
|
(instance? Constant attribute)
|
||||||
|
(let [a (:value attribute)
|
||||||
|
id (if (keyword? a)
|
||||||
|
(attribute-in-source source a)
|
||||||
|
a)]
|
||||||
|
(int->table id))
|
||||||
|
|
||||||
|
;; TODO: perhaps we know an external binding already?
|
||||||
|
(or (instance? Variable attribute)
|
||||||
|
(instance? Placeholder attribute))
|
||||||
;; It's variable. We must act as if it could be a fulltext datom.
|
;; It's variable. We must act as if it could be a fulltext datom.
|
||||||
(:fulltext-view source))]
|
(:fulltext-view source)
|
||||||
|
|
||||||
|
true
|
||||||
|
(raise "Unknown source->from attribute " attribute {:attribute attribute}))]
|
||||||
[table ((:table-alias source) table)]))
|
[table ((:table-alias source) table)]))
|
||||||
|
|
||||||
(source->non-fulltext-from [source]
|
(source->non-fulltext-from [source]
|
||||||
|
|
|
@ -494,3 +494,26 @@
|
||||||
[:= :datoms0.e :datoms1.e])}}
|
[:= :datoms0.e :datoms1.e])}}
|
||||||
:from [:preag]}
|
:from [:preag]}
|
||||||
(query/context->sql-clause context)))))
|
(query/context->sql-clause context)))))
|
||||||
|
|
||||||
|
(deftest-db test-get-else conn
|
||||||
|
(let [attrs (<? (<initialize-with-schema conn page-schema))]
|
||||||
|
(is (= {:select (list
|
||||||
|
[:datoms0.e :page]
|
||||||
|
[{:select [(sql/call
|
||||||
|
:coalesce
|
||||||
|
{:select [:v],
|
||||||
|
:from [:datoms],
|
||||||
|
:where [:and
|
||||||
|
[:= 'a 65540]
|
||||||
|
[:= 'e :datoms0.e]],
|
||||||
|
:limit 1}
|
||||||
|
"No title")],
|
||||||
|
:limit 1} :title]),
|
||||||
|
:modifiers [:distinct],
|
||||||
|
:from '([:datoms datoms0]),
|
||||||
|
:where (list :and [:= :datoms0.a 65539])}
|
||||||
|
(expand '[:find ?page ?title :in $
|
||||||
|
:where
|
||||||
|
[?page :page/url _]
|
||||||
|
[(get-else $ ?page :page/title "No title") ?title]]
|
||||||
|
conn)))))
|
||||||
|
|
Loading…
Reference in a new issue