From a602a0b39d1427756374b758c0bb1b03281b0b00 Mon Sep 17 00:00:00 2001 From: Matthew Davidson Date: Fri, 22 Mar 2024 21:36:43 +0700 Subject: [PATCH] feat: Add SELECT * EXCEPT capability --- resources/inferenceql/query/base.bnf | 8 ++- src/inferenceql/query/base.cljc | 4 +- src/inferenceql/query/parser/tree.cljc | 2 +- src/inferenceql/query/plan.cljc | 73 ++++++++++++++++++++++++-- src/inferenceql/query/relation.cljc | 9 ++++ src/inferenceql/query/tuple.cljc | 9 +++- 6 files changed, 94 insertions(+), 11 deletions(-) diff --git a/resources/inferenceql/query/base.bnf b/resources/inferenceql/query/base.bnf index 2dbef04..a6701d8 100644 --- a/resources/inferenceql/query/base.bnf +++ b/resources/inferenceql/query/base.bnf @@ -76,12 +76,16 @@ distinct-clause ::= #'(?i)DISTINCT' select-clause ::= #'(?i)SELECT' (ws distinct-clause)? ws select-list -select-list ::= star +select-list ::= star-clause / selection (ws? ',' ws? selection)* / aggregation (ws? ',' ws? aggregation)* +star-clause ::= star (ws? except-clause)? + star ::= '*' +except-clause ::= #'(?i)EXCEPT' ws? '(' ws? identifier-list ws? ')' + selection ::= (scalar-expr | aggregation) (ws alias-clause)? alias-clause ::= #'(?i)AS' ws identifier @@ -149,7 +153,6 @@ insert-expr ::= #'(?i)INSERT' ws #'(?i)INTO' ws relation-expr ws relation-expr relation-value ::= '(' ws? identifier-list ws? ')' ws #'(?i)VALUES' ws value-lists -identifier-list ::= identifier (ws? ',' ws identifier)* value-list ::= '(' ws? value (ws? ',' ws? value)* ws? ')' value-lists ::= value-lists-full | value-lists-sparse value-lists-full ::= value-list (ws? ',' ws? value-list)* @@ -241,6 +244,7 @@ bool ::= #'true|false' float ::= #'-?\d+\.\d+(E-?\d+)?' int ::= #'-?\d+' nat ::= #'\d+' +identifier-list ::= identifier (ws? ',' ws? identifier)* identifier ::= simple-symbol | delimited-symbol simple-symbol ::= #'(?u)(?!G__)\p{L}[\p{L}\p{N}_\-\?\.]*' delimited-symbol ::= <'"'> #'[^\"]+' <'"'> diff --git a/src/inferenceql/query/base.cljc b/src/inferenceql/query/base.cljc index 59f2e16..0962d62 100644 --- a/src/inferenceql/query/base.cljc +++ b/src/inferenceql/query/base.cljc @@ -22,14 +22,14 @@ - parse - a parsing function" [s db parse] (let [node-or-failure (parse s)] + (tap> #:base.query{:node node-or-failure}) (cond (insta/failure? node-or-failure) (throw (error/parse node-or-failure)) (plan/relation-node? node-or-failure) (let [plan (plan/plan node-or-failure) env (db/env @db)] - (tap> #:base.query{:node node-or-failure - :plan plan + (tap> #:base.query{:plan plan :env env}) (plan/eval plan env {})) diff --git a/src/inferenceql/query/parser/tree.cljc b/src/inferenceql/query/parser/tree.cljc index f16c0cc..bc64b91 100644 --- a/src/inferenceql/query/parser/tree.cljc +++ b/src/inferenceql/query/parser/tree.cljc @@ -40,7 +40,7 @@ (string/blank? node)))) (defn children - "Returns `node`'s children." + "Returns `node`'s children. Removes whitespace nodes." [node] (into [] (clojure/remove whitespace?) diff --git a/src/inferenceql/query/plan.cljc b/src/inferenceql/query/plan.cljc index 07947eb..5438dd7 100644 --- a/src/inferenceql/query/plan.cljc +++ b/src/inferenceql/query/plan.cljc @@ -49,25 +49,32 @@ [node] (tree/match [node] [[:selection child]] (star? child) + [[:star-clause & _]] true [[:star & _]] true :else false)) ;;; plan (defn lookup + "A plan to retrieve a relation/table from the environment by name." [id] {::type :inferenceql.query.plan.type/lookup ::env/name id}) (defn select + "A plan to filters the tuples/rows that match the pred fn. + + NB: Used for the WHERE clause; unrelated to the SELECT clause." [op sexpr] {::type :inferenceql.query.plan.type/select ::sexpr sexpr ::plan op}) (defn extended-project + "An (extended) projection plan. Used for simple selections of existing + columns as well as derived columns." [op coll] - ;; `coll` is a sequence of (s-expression, attribute) pairs. + ;; `coll` is a sequence of (fn-to-eval-s-expression, attribute-name) pairs. (let [terms (mapv #(zipmap [::sexpr ::relation/attribute] %) coll)] {::type :inferenceql.query.plan.type/extended-project @@ -335,16 +342,66 @@ [[:scalar-expr & _]] true :else false)) +(defn ^:private select-star-plan + "Returns a plan for SELECT * and SELECT * EXCEPT ... clauses." + [node op] + (tap> {:star-node node}) + (tree/match [node] + [[:star-clause & children]] + (select-star-plan children op) ; trying recur breaks with CLJ-2808 + + ;; plain SELECT * + [[[:star _]]] + op + + ;; SELECT * EXCEPT (col1, col2, ...) + [[[:star & _] [:except-clause _except _lparen [:identifier-list & id-list] _rparen]]] + (loop [exclusions [] + [id & rest-ids] (filterv (tree/tag-pred :identifier) id-list)] + (if id + (recur (conj exclusions (literal/read id)) rest-ids) + {::type :inferenceql.query.plan.type/star-except + ::exclusions exclusions + ::plan op})) + + :else + (throw (ex-info "Can't generate selection plan - unknown * pattern" + {:star-node node + :partial-plan op})))) + +(comment + + (let [op :op + star-node [:star-clause [:star "*"]] + star-except-node [:star-clause + [:star "*"] + [:ws " "] + [:except-clause + "EXCEPT" + [:ws " "] + "(" + [:identifier-list + [:identifier [:simple-symbol "bob"]] + "," + [:ws " "] + [:identifier [:delimited-symbol "philh@rmonic"]]] + ")"]]] + (select-star-plan star-except-node op)) + + ) + (defn select-plan [select-node group-by-node op] (assert (= :select-clause (tree/tag select-node))) (assert (or (nil? group-by-node) (= :group-by-clause (tree/tag group-by-node)))) - (let [selections (selections select-node) + (let [selections (vec (selections select-node)) + _ (tap> {:select-node select-node}) + _ (tap> {:selections selections}) distinct-clause (tree/get-node select-node :distinct-clause) - plan (cond (and (= 1 (count selections)) - (star? (first selections))) - op + plan (cond (star? (first selections)) + (select-star-plan (first selections) op) + ;; FIXME?: Can't currently combine GROUP BY and SELECT *, which some dbs allow (or group-by-node (some aggregation? selections)) (aggregation-plan select-node group-by-node op) @@ -505,6 +562,12 @@ pred #(scalar/eval sexpr env bindings %)] (relation/select rel pred))) +(defmethod eval :inferenceql.query.plan.type/star-except + [plan env bindings] + (let [{::keys [exclusions plan]} plan + rel (eval plan env bindings)] + (relation/remove-attributes rel exclusions))) + (defmethod eval :inferenceql.query.plan.type/extended-project [plan env bindings] (let [{::keys [terms plan]} plan diff --git a/src/inferenceql/query/relation.cljc b/src/inferenceql/query/relation.cljc index c26b799..4f771f8 100644 --- a/src/inferenceql/query/relation.cljc +++ b/src/inferenceql/query/relation.cljc @@ -74,6 +74,7 @@ :attrs (mapv second coll)))) (defn select + "Selects/filters the tuples/rows that match the pred fn" [rel pred] (with-meta (filter pred (tuples rel)) (meta rel))) @@ -112,6 +113,14 @@ attr))] (relation rel :attrs attributes))) +(defn remove-attributes + "Remove attributes from a relation." + [rel attrs] + (let [attributes' (remove (set attrs) + (attributes rel))] + (-> (map #(apply dissoc % attrs) rel) + (relation :attrs attributes')))) + (defn group-by [rel f] (->> (tuples rel) diff --git a/src/inferenceql/query/tuple.cljc b/src/inferenceql/query/tuple.cljc index fa0327e..ed0925b 100644 --- a/src/inferenceql/query/tuple.cljc +++ b/src/inferenceql/query/tuple.cljc @@ -24,9 +24,16 @@ (defn select-attrs "Retrives tuple `tup` with only the attributes in `attrs`." [tup attrs] - (let [names (into #{} (map name) attrs)] + (let [names (into #{} (map name) attrs)] ; FIXME: should this be clojure.core/name (medley/filter-keys (comp names name) tup))) +;; +;;(defn remove-attrs +;; "Removes attributes in `attrs` from tuple `tup`." +;; [tup attrs] +;; (let [names (into #{} (map name) attrs)] +;; (medley/filter-keys (complement (comp names name)) +;; tup))) (defn ->map "Converts tuple `tup` to an immutable hash map."