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
|
||||
[honeysql.format :as fmt]
|
||||
[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]]
|
||||
[datascript.parser :as dp
|
||||
#?@(:cljs
|
||||
|
@ -136,9 +141,95 @@
|
|||
|
||||
(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
|
||||
;; 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
|
||||
"Either returns an application of `function` to `cc`, or nil to
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
(:require
|
||||
[datomish.query.transforms :as transforms]
|
||||
[datomish.schema :as schema]
|
||||
[datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str]]
|
||||
[datascript.parser
|
||||
#?@(:cljs
|
||||
[:refer [Variable Constant Placeholder]])])
|
||||
|
@ -68,15 +69,31 @@
|
|||
Source
|
||||
|
||||
(source->from [source attribute]
|
||||
(let [table
|
||||
(if (and (instance? Constant attribute)
|
||||
;; TODO: look in the DB schema to see if `attribute` is known to not be
|
||||
;; a fulltext attribute.
|
||||
true)
|
||||
(:table source)
|
||||
(let [schema (:schema source)
|
||||
int->table (fn [a]
|
||||
(if (schema/fulltext? schema a)
|
||||
(:fulltext-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.
|
||||
(:fulltext-view source))]
|
||||
(:fulltext-view source)
|
||||
|
||||
true
|
||||
(raise "Unknown source->from attribute " attribute {:attribute attribute}))]
|
||||
[table ((:table-alias source) table)]))
|
||||
|
||||
(source->non-fulltext-from [source]
|
||||
|
|
|
@ -494,3 +494,26 @@
|
|||
[:= :datoms0.e :datoms1.e])}}
|
||||
:from [:preag]}
|
||||
(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