Implement get-else.

This commit is contained in:
Richard Newman 2016-08-27 15:36:12 -07:00
parent 38cd30a895
commit f225dbe734
3 changed files with 140 additions and 9 deletions

View file

@ -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

View file

@ -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]

View file

@ -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)))))