From e89544bebac42a4c24fc4a5cd3a94c9da1dbf7d7 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Thu, 6 Oct 2016 18:26:40 -0700 Subject: [PATCH] Implement all four find specs. Fixes #38. r=nalexander --- src/common/datomish/db.cljc | 64 ++++++---- src/common/datomish/query.cljc | 62 ++++++++-- src/common/datomish/query/context.cljc | 18 ++- src/common/datomish/query/projection.cljc | 55 +++++++-- test/datomish/test/query.cljc | 140 ++++++++++++++++++++++ test/datomish/tofinoish_test.cljc | 29 ++--- 6 files changed, 304 insertions(+), 64 deletions(-) diff --git a/src/common/datomish/db.cljc b/src/common/datomish/db.cljc index c41381a8..3fe7107d 100644 --- a/src/common/datomish/db.cljc +++ b/src/common/datomish/db.cljc @@ -800,29 +800,6 @@ :schema entid-schema }))) -;; TODO: factor this into the overall design. -(defn db - query-context - (query/options-into-context limit order-by) - (query/find-into-context parsed)) - row-pair-transducer (projection/row-pair-transducer context) - sql (query/context->sql-string context inputs) - chan (chan 50 row-pair-transducer)] - - (s/ db + query-context + (query/options-into-context limit order-by) + (query/find-into-context parsed)) + + ;; We turn each row into either an array of values or an unadorned + ;; value. The row-pair-transducer does this work. + ;; The only thing to do to handle the full suite of find specs + ;; is to decide if we're then returning an array of transduced rows + ;; or just the first result. + row-pair-transducer (projection/row-pair-transducer context) + sql (query/context->sql-string context inputs) + + first-only (context/scalar-or-tuple-query? context) + buffer-size (if first-only + 1 + default-result-buffer-size) + chan (chan buffer-size row-pair-transducer)] + + ;; Fill the channel. + (s/partial-subquery inner-projection (:cc context))) limit (:limit context) @@ -134,6 +143,19 @@ (raise "Invalid limit " limit {:limit limit})) (assoc context :limit limit :order-by-vars order-by)) +(defn find-spec->elements [find-spec] + (condp instance? find-spec + FindRel (:elements find-spec) + FindTuple (:elements find-spec) + FindScalar [(:element find-spec)] + FindColl [(:element find-spec)] + (raise "Unable to handle find spec." {:find-spec find-spec}))) + +(defn find-spec->limit [find-spec] + (when (or (instance? FindScalar find-spec) + (instance? FindTuple find-spec)) + 1)) + (defn find-into-context "Take a parsed `find` expression and return a fully populated Context. You'll want this so you can get access to the @@ -142,15 +164,37 @@ (let [{:keys [find in with where]} find] ; Destructure the Datalog query. (validate-with with) (validate-in in) + + ;; A find spec can be: + ;; + ;; * FindRel containing :elements. Returns an array of arrays. + ;; * FindColl containing :element. This is like mapping (fn [row] (aget row 0)) + ;; over the result set. Returns an array of homogeneous values. + ;; * FindScalar containing :element. Returns a single value. + ;; * FindTuple containing :elements. This is just like :limit 1 + ;; on FindColl, returning the first item of the result array. Returns an + ;; array of heterogeneous values. + ;; + ;; The code to handle these is: + ;; - Just above, unifying a variable list in find-spec->elements. + ;; - In context.cljc, checking whether a single value or collection is returned. + ;; - In projection.cljc, transducing according to whether a single column or + ;; multiple columns are assembled into the output. + ;; - In db.cljc, where we finally take rows and decide what to push into an + ;; output channel. + (let [external-bindings (in->bindings in) - elements (:elements find) + elements (find-spec->elements find) known-types {} group-by-vars (projection/extract-group-by-vars elements with)] - (assoc context - :elements elements - :group-by-vars group-by-vars - :has-aggregates? (not (nil? group-by-vars)) - :cc (clauses/patterns->cc (:default-source context) where known-types external-bindings))))) + (util/assoc-if + (assoc context + :find-spec find + :elements elements + :group-by-vars group-by-vars + :has-aggregates? (not (nil? group-by-vars)) + :cc (clauses/patterns->cc (:default-source context) where known-types external-bindings)) + :limit (find-spec->limit find))))) (defn find->sql-clause "Take a parsed `find` expression and turn it into a structured SQL diff --git a/src/common/datomish/query/context.cljc b/src/common/datomish/query/context.cljc index 8a21d8cb..2d7a3e3c 100644 --- a/src/common/datomish/query/context.cljc +++ b/src/common/datomish/query/context.cljc @@ -4,12 +4,19 @@ ;; A context, very simply, holds on to a default source and some knowledge ;; needed for aggregation. -(ns datomish.query.context) +(ns datomish.query.context + (:require + [datascript.parser :as dp + #?@(:cljs [:refer [FindRel FindColl FindTuple FindScalar]])]) + #?(:clj + (:import + [datascript.parser FindRel FindColl FindTuple FindScalar]))) (defrecord Context [ default-source - elements ; The :find list itself. + find-spec ; The parsed find spec. Used to decide how to process rows. + elements ; A list of Element instances, drawn from the :find-spec itself. has-aggregates? group-by-vars ; A list of variables from :find and :with, used to generate GROUP BY. order-by-vars ; A list of projected variables and directions, e.g., [:date :asc], [:_max_timestamp :desc]. @@ -17,5 +24,10 @@ cc ; The main conjoining clause. ]) +(defn scalar-or-tuple-query? [context] + (when-let [find-spec (:find-spec context)] + (or (instance? FindScalar find-spec) + (instance? FindTuple find-spec)))) + (defn make-context [source] - (->Context source nil false nil nil nil nil)) + (->Context source nil nil false nil nil nil nil)) diff --git a/src/common/datomish/query/projection.cljc b/src/common/datomish/query/projection.cljc index eb0978a6..1740bd66 100644 --- a/src/common/datomish/query/projection.cljc +++ b/src/common/datomish/query/projection.cljc @@ -9,10 +9,28 @@ [datomish.sqlite-schema :as ss] [datomish.util :as util #?(:cljs :refer-macros :clj :refer) [raise raise-str cond-let]] [datascript.parser :as dp - #?@(:cljs [:refer [Aggregate Pattern DefaultSrc Variable Constant Placeholder PlainSymbol]])] + #?@(:cljs [:refer + [Aggregate + Constant + DefaultSrc + FindRel FindColl FindTuple FindScalar + Pattern + Placeholder + PlainSymbol + Variable + ]])] ) - #?(:clj (:import [datascript.parser Aggregate Pattern DefaultSrc Variable Constant Placeholder PlainSymbol])) - ) + #?(:clj (:import + [datascript.parser + Aggregate + Constant + DefaultSrc + FindRel FindColl FindTuple FindScalar + Pattern + Placeholder + PlainSymbol + Variable + ]))) (defn lookup-variable [cc variable] (or (-> cc :bindings variable first) @@ -251,18 +269,37 @@ elements)))) (defn row-pair-transducer [context] - (let [{:keys [elements cc]} context + (let [{:keys [find-spec elements cc]} context {:keys [source known-types extracted-types]} cc ;; We know the projection will fail above if these aren't simple variables or aggregates. projectors - (make-projectors-for-columns elements known-types extracted-types)] + (make-projectors-for-columns elements known-types extracted-types) + + single-column-find-spec? + (or (instance? FindScalar find-spec) + (instance? FindColl find-spec))] (map - (fn [[row err]] - (if err - [row err] - [(map (fn [projector] (projector row)) projectors) nil]))))) + (if single-column-find-spec? + ;; We're only grabbing one result from each row. + (let [projector (first projectors)] + (when (second projectors) + (raise "Single-column find spec used, but multiple projectors present." + {:elements elements + :projectors projectors + :find-spec find-spec})) + + (fn [[row err]] + (if err + [nil err] + [(projector row) nil]))) + + ;; Otherwise, collect each column into a sequence. + (fn [[row err]] + (if err + [nil err] + [(map (fn [projector] (projector row)) projectors) nil])))))) (defn extract-group-by-vars "Take inputs to :find and, if any aggregates exist in `elements`, diff --git a/test/datomish/test/query.cljc b/test/datomish/test/query.cljc index ce601bea..c2cf5ae2 100644 --- a/test/datomish/test/query.cljc +++ b/test/datomish/test/query.cljc @@ -684,3 +684,143 @@ "something") '[[?save]]]]} conn))))) + +(deftest-db test-find-specs-expansion conn + (let [attrs (