From 8abce7995b64d7faf5c6738a6a5eb25b2ee4335c Mon Sep 17 00:00:00 2001 From: metamben <103100869+metamben@users.noreply.github.com> Date: Tue, 28 Mar 2023 08:47:02 +0300 Subject: [PATCH] MLv2 new expression syntax (#29565) --- src/metabase/lib/common.cljc | 40 ++-- src/metabase/lib/core.cljc | 16 +- src/metabase/lib/dispatch.cljc | 7 +- src/metabase/lib/expression.cljc | 7 +- src/metabase/lib/filter.cljc | 163 +++++---------- src/metabase/lib/join.cljc | 8 + src/metabase/lib/schema/common.cljc | 6 + test/metabase/lib/aggregation_test.cljc | 47 +++-- test/metabase/lib/expression_test.cljc | 7 +- test/metabase/lib/filter_test.cljc | 264 ++++++++---------------- test/metabase/lib/join_test.cljc | 23 ++- test/metabase/lib/query_test.cljc | 2 +- test/metabase/lib/util_test.cljc | 2 +- 13 files changed, 248 insertions(+), 344 deletions(-) diff --git a/src/metabase/lib/common.cljc b/src/metabase/lib/common.cljc index 4877fe18ca7..e11b85e1760 100644 --- a/src/metabase/lib/common.cljc +++ b/src/metabase/lib/common.cljc @@ -4,10 +4,20 @@ [metabase.lib.field :as lib.field] #_{:clj-kondo/ignore [:unused-namespace]} [metabase.lib.options :as lib.options] + [metabase.lib.schema.common :as schema.common] #_{:clj-kondo/ignore [:unused-namespace]} [metabase.util.malli :as mu]) #?(:cljs (:require-macros [metabase.lib.common]))) +(mu/defn external-op :- [:maybe ::schema.common/external-op] + "Convert the internal operator `clause` to the external format." + [[operator options :as clause]] + (when clause + {:operator (cond-> operator + (keyword? operator) name) + :options options + :args (subvec clause 2)})) + (defmulti ->op-arg "Ensures that clause arguments are properly unwrapped" {:arglists '([query stage-number x])} @@ -15,15 +25,17 @@ (lib.dispatch/dispatch-value x))) (defmethod ->op-arg :default - [query stage-number x] - (if (vector? x) - (mapv #(->op-arg query stage-number %) x) - x)) + [_query _stage-number x] + x) (defmethod ->op-arg :metadata/field [query stage-number field-metadata] (lib.field/field query stage-number field-metadata)) +(defmethod ->op-arg :lib/external-op + [query stage-number {:keys [operator options args] :or {options {}}}] + (->op-arg query stage-number (lib.options/ensure-uuid (into [(keyword operator) options] args)))) + (defmethod ->op-arg :dispatch-type/fn [query stage-number f] (->op-arg query stage-number (f query stage-number))) @@ -38,26 +50,28 @@ (every? #(not-any? #{'query 'stage-number} %) argvecs)]} (let [fn-rename #(name (get {'/ 'div} % %))] `(do - (mu/defn ~(symbol (str (fn-rename op-name) "-clause")) :- ~(keyword "mbql.clause" (name op-name)) + (mu/defn ~(symbol (str (fn-rename op-name) "-clause")) :- :metabase.lib.schema.common/external-op ~(format "Create a standalone clause of type `%s`." (name op-name)) ~@(for [argvec argvecs :let [arglist-expr (if (contains? (set argvec) '&) (cons `list* (remove #{'&} argvec)) argvec)]] `([~'query ~'stage-number ~@argvec] - (-> (into [~(keyword op-name)] - (map (fn [~'arg] - (->op-arg ~'query ~'stage-number ~'arg))) - ~arglist-expr) - lib.options/ensure-uuid)))) + {:operator ~(keyword op-name) + :args (mapv (fn [~'arg] + (->op-arg ~'query ~'stage-number ~'arg)) + ~arglist-expr)}))) (mu/defn ~op-name :- fn? ~(format "Create a closure of clause of type `%s`." (name op-name)) ~@(for [argvec argvecs - :let [arglist-expr (if (contains? (set argvec) '&) + :let [varargs? (contains? (set argvec) '&) + arglist-expr (if varargs? (filterv (complement #{'&}) argvec) - (conj argvec []))]] + argvec)]] `([~@argvec] (fn ~(symbol (str (fn-rename op-name) "-closure")) [~'query ~'stage-number] - (apply ~(symbol (str (fn-rename op-name) "-clause")) ~'query ~'stage-number ~@arglist-expr))))))))) + ~(cond->> (concat [(symbol (str (fn-rename op-name) "-clause")) 'query 'stage-number] + arglist-expr) + varargs? (cons `apply)))))))))) diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index e202e9b0e99..a148d11410e 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -118,21 +118,7 @@ starts-with ends-with contains does-not-contain time-interval - segment - ->and - ->or - ->not - ->= ->!= - ->< -><= - ->> ->>= - ->between - ->inside - ->is-null ->not-null - ->is-empty ->not-empty - ->starts-with ->ends-with - ->contains ->does-not-contain - ->time-interval - ->segment] + segment] [lib.join join join-clause diff --git a/src/metabase/lib/dispatch.cljc b/src/metabase/lib/dispatch.cljc index 552c6fa305f..9987893bd4a 100644 --- a/src/metabase/lib/dispatch.cljc +++ b/src/metabase/lib/dispatch.cljc @@ -21,5 +21,10 @@ ;; of [[toucan2.core/model]]? (or (mbql-clause-type x) (when (map? x) - (:lib/type x)) + (if (and (or (keyword? (:operator x)) + (string? (:operator x))) + (vector? (:args x)) + (map? (:options x {}))) + :lib/external-op + (:lib/type x))) (u/dispatch-type-keyword x))) diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc index 556a2b5a100..e173572fefe 100644 --- a/src/metabase/lib/expression.cljc +++ b/src/metabase/lib/expression.cljc @@ -23,12 +23,17 @@ stage-number :- :int expression-name :- ::lib.schema.common/non-blank-string] (let [stage (lib.util/query-stage query stage-number)] - (or (get-in stage [:expressions expression-name]) + (or (some-> (get-in stage [:expressions expression-name]) + lib.common/external-op) (throw (ex-info (i18n/tru "No expression named {0}" (pr-str expression-name)) {:expression-name expression-name :query query :stage-number stage-number}))))) +(defmethod lib.schema.expression/type-of* :lib/external-op + [{:keys [operator options args] :or {options {}}}] + (lib.schema.expression/type-of* (into [(keyword operator) options] args))) + (defmethod lib.metadata.calculation/metadata :expression [query stage-number [_expression opts expression-name, :as expression-ref]] (let [expression (resolve-expression query stage-number expression-name)] diff --git a/src/metabase/lib/filter.cljc b/src/metabase/lib/filter.cljc index 289722ab9d0..b2ea06bdc3a 100644 --- a/src/metabase/lib/filter.cljc +++ b/src/metabase/lib/filter.cljc @@ -1,13 +1,12 @@ (ns metabase.lib.filter (:refer-clojure :exclude [filter and or not = < <= > ->> >= not-empty case]) (:require - [metabase.lib.dispatch :as lib.dispatch] - [metabase.lib.field :as lib.field] + [metabase.lib.common :as lib.common] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.options :as lib.options] [metabase.lib.schema] - [metabase.lib.schema.expression :as schema.expression] + [metabase.lib.schema.common :as schema.common] [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.util :as lib.util] [metabase.shared.util.i18n :as i18n] @@ -167,129 +166,70 @@ (lib.metadata.calculation/display-name query stage-number expr) (lib.temporal-bucket/interval->i18n n unit))) -(defmulti ^:private ->filter-arg - {:arglists '([query stage-number x])} - (fn [_query _stage-number x] - (lib.dispatch/dispatch-value x))) - -(defmethod ->filter-arg :default - [_query _stage-number x] - x) - -(defmethod ->filter-arg :metadata/field - [query stage-number field-metadata] - (lib.field/field query stage-number field-metadata)) - -(defmethod ->filter-arg :dispatch-type/fn - [query stage-number f] - (->filter-arg query stage-number (f query stage-number))) - -#?(:clj - (defmacro ^:private deffilter - [filter-name argvec] - {:pre [(symbol? filter-name) - (vector? argvec) (every? symbol? argvec) - (not-any? #{'query 'stage-number} argvec)]} - (let [filter-name-str (name filter-name) - vararg? (.contains ^java.util.Collection argvec '&) - args (remove #{'&} argvec) - arglist-expr (if vararg? - (cons 'list* args) - argvec)] - `(do - (mu/defn ~filter-name :- ~(keyword "mbql.clause" filter-name-str) - ~(format "Create a filter clause of type `%s`." filter-name-str) - [~'query ~'stage-number ~@argvec] - (-> (into [~(keyword filter-name)] - (map (fn [~'arg] - (->filter-arg ~'query ~'stage-number ~'arg))) - ~arglist-expr) - metabase.lib.options/ensure-uuid)) - - (mu/defn ~(symbol (str "->" filter-name-str)) :- fn? - ~(format "Return function creating a filter clause of type `%s`." filter-name-str) - ~argvec - (fn [~'query ~'stage-number] - ~(cond->> (concat [filter-name 'query 'stage-number] args) - vararg? (cons `apply)))))))) - -(metabase.lib.filter/deffilter and [x y & more]) -(metabase.lib.filter/deffilter or [x y & more]) -(metabase.lib.filter/deffilter not [x]) -(metabase.lib.filter/deffilter = [x y & more]) -(metabase.lib.filter/deffilter != [x y & more]) -(metabase.lib.filter/deffilter < [x y]) -(metabase.lib.filter/deffilter <= [x y]) -(metabase.lib.filter/deffilter > [x y]) -(metabase.lib.filter/deffilter >= [x y]) -(metabase.lib.filter/deffilter between [x lower upper]) -(metabase.lib.filter/deffilter inside [lat lon lat-max lon-min lat-min lon-max]) -(metabase.lib.filter/deffilter is-null [x]) -(metabase.lib.filter/deffilter not-null [x]) -(metabase.lib.filter/deffilter is-empty [x]) -(metabase.lib.filter/deffilter not-empty [x]) -(metabase.lib.filter/deffilter starts-with [whole part]) -(metabase.lib.filter/deffilter ends-with [whole part]) -(metabase.lib.filter/deffilter contains [whole part]) -(metabase.lib.filter/deffilter does-not-contain [whole part]) -(metabase.lib.filter/deffilter time-interval [x amount unit]) -(metabase.lib.filter/deffilter segment [segment-id]) - -(defmulti ^:private ->filter-clause - {:arglists '([query stage-number x])} - (fn [_query _stage-number x] - (lib.dispatch/dispatch-value x))) - -(defmethod ->filter-clause :default - [query stage-number x] - (if (vector? x) - (-> (mapv #(clojure.core/->> % - (->filter-arg query stage-number) - (->filter-clause query stage-number)) x) - lib.options/ensure-uuid) - x)) - -(defmethod ->filter-clause :dispatch-type/fn - [query stage-number f] - (->filter-clause query stage-number (f query stage-number))) +(lib.common/defop and [x y & more]) +(lib.common/defop or [x y & more]) +(lib.common/defop not [x]) +(lib.common/defop = [x y & more]) +(lib.common/defop != [x y & more]) +(lib.common/defop < [x y]) +(lib.common/defop <= [x y]) +(lib.common/defop > [x y]) +(lib.common/defop >= [x y]) +(lib.common/defop between [x lower upper]) +(lib.common/defop inside [lat lon lat-max lon-min lat-min lon-max]) +(lib.common/defop is-null [x]) +(lib.common/defop not-null [x]) +(lib.common/defop is-empty [x]) +(lib.common/defop not-empty [x]) +(lib.common/defop starts-with [whole part]) +(lib.common/defop ends-with [whole part]) +(lib.common/defop contains [whole part]) +(lib.common/defop does-not-contain [whole part]) +(lib.common/defop time-interval [x amount unit]) +(lib.common/defop segment [segment-id]) (mu/defn filter :- :metabase.lib.schema/query "Sets `boolean-expression` as a filter on `query`." - ([query boolean-expression] - (metabase.lib.filter/filter query -1 boolean-expression)) + ([query :- :metabase.lib.schema/query + boolean-expression] + (metabase.lib.filter/filter query nil boolean-expression)) - ([query stage-number boolean-expression] + ([query :- :metabase.lib.schema/query + stage-number :- [:maybe :int] + boolean-expression] (let [stage-number (clojure.core/or stage-number -1) - new-filter (->filter-clause query stage-number boolean-expression)] + new-filter (lib.common/->op-arg query stage-number boolean-expression)] (lib.util/update-query-stage query stage-number assoc :filter new-filter)))) (defn- and-clause? [clause] (clojure.core/and (vector? clause) (clojure.core/= (first clause) :and))) -(mu/defn current-filter :- [:maybe ::schema.expression/boolean] +(mu/defn current-filter :- [:maybe ::schema.common/external-op] "Returns the current filter in stage with `stage-number` of `query`. If `stage-number` is omitted, the last stage is used. See also [[metabase.lib.util/query-stage]]." - ([query :- :metabase.lib.schema/query] (current-filter query -1)) + ([query :- :metabase.lib.schema/query] (current-filter query nil)) ([query :- :metabase.lib.schema/query - stage-number :- :int] - (:filter (lib.util/query-stage query stage-number)))) + stage-number :- [:maybe :int]] + (-> (lib.util/query-stage query (clojure.core/or stage-number -1)) + :filter + lib.common/external-op))) -(mu/defn current-filters :- [:sequential ::schema.expression/boolean] +(mu/defn current-filters :- [:sequential ::schema.common/external-op] "Returns the current filters in stage with `stage-number` of `query`. If `stage-number` is omitted, the last stage is used. Logicaly, the filter attached to the query is the conjunction of the expressions in the returned list. If the returned list is empty, then there is no filter attached to the query. See also [[metabase.lib.util/query-stage]]." - ([query :- :metabase.lib.schema/query] (current-filters query -1)) + ([query :- :metabase.lib.schema/query] (current-filters query nil)) ([query :- :metabase.lib.schema/query - stage-number :- :int] - (if-let [existing-filter (:filter (lib.util/query-stage query stage-number))] + stage-number :- [:maybe :int]] + (if-let [existing-filter (:filter (lib.util/query-stage query (clojure.core/or stage-number -1)))] (if (and-clause? existing-filter) - (subvec existing-filter 2) - [existing-filter]) + (mapv lib.common/external-op (subvec existing-filter 2)) + [(lib.common/external-op existing-filter)]) []))) (defn- conjoin [existing-filter new-filter] @@ -304,23 +244,28 @@ yet, builds a conjunction with the current filter otherwise." ([query :- :metabase.lib.schema/query boolean-expression] - (metabase.lib.filter/add-filter query -1 boolean-expression)) + (metabase.lib.filter/add-filter query nil boolean-expression)) ([query :- :metabase.lib.schema/query - stage-number :- :int + stage-number :- [:maybe :int] boolean-expression] (let [stage-number (clojure.core/or stage-number -1) - new-filter (->filter-clause query stage-number boolean-expression)] + new-filter (lib.common/->op-arg query stage-number boolean-expression)] (lib.util/update-query-stage query stage-number update :filter conjoin new-filter)))) (mu/defn replace-filter :- :metabase.lib.schema/query "Replaces the expression with `target-uuid` with `boolean-expression` the filter of `query`." - ([query target-uuid boolean-expression] - (metabase.lib.filter/replace-filter query -1 target-uuid boolean-expression)) + ([query :- :metabase.lib.schema/query + target-uuid :- :string + boolean-expression] + (metabase.lib.filter/replace-filter query nil target-uuid boolean-expression)) - ([query stage-number target-uuid boolean-expression] + ([query :- :metabase.lib.schema/query + stage-number :- [:maybe :int] + target-uuid :- :string + boolean-expression] (let [stage-number (clojure.core/or stage-number -1) - new-filter (->filter-clause query stage-number boolean-expression)] + new-filter (lib.common/->op-arg query stage-number boolean-expression)] (lib.util/update-query-stage query stage-number update :filter lib.util/replace-clause target-uuid new-filter)))) diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc index 2d1476a61a9..ed067364cc8 100644 --- a/src/metabase/lib/join.cljc +++ b/src/metabase/lib/join.cljc @@ -97,6 +97,9 @@ [query stage-number f] (->join-clause query stage-number (f query stage-number))) +;; TODO this is basically the same as lib.common/->op-args, +;; but requiring lib.common leads to crircular dependencies: +;; join -> common -> field -> join. (defmulti ^:private ->join-condition {:arglists '([query stage-number x])} (fn [_query _stage-number x] @@ -106,6 +109,11 @@ [_query _stage-number x] x) +(defmethod ->join-condition :lib/external-op + [query stage-number {:keys [operator options args] :or {options {}}}] + (->join-condition query stage-number + (lib.options/ensure-uuid (into [operator options] args)))) + (defmethod ->join-condition :dispatch-type/fn [query stage-number f] (->join-condition query stage-number (f query stage-number))) diff --git a/src/metabase/lib/schema/common.cljc b/src/metabase/lib/schema/common.cljc index fc041acf381..6bf0afd9878 100644 --- a/src/metabase/lib/schema/common.cljc +++ b/src/metabase/lib/schema/common.cljc @@ -31,3 +31,9 @@ [:fn {:error/message "valid base type"} #(isa? % :type/*)]) + +(mr/def ::external-op + [:map + [:operator [:or :string :keyword]] + [:options {:optional true} ::options] + [:args [:sequential :any]]]) diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc index 75c120b7711..61efb32b310 100644 --- a/test/metabase/lib/aggregation_test.cljc +++ b/test/metabase/lib/aggregation_test.cljc @@ -3,6 +3,7 @@ [clojure.test :refer [are deftest is testing]] [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib] + [metabase.lib.field :as lib.field] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.query :as lib.query] @@ -15,8 +16,7 @@ (defn- is-fn? [op tag args expected-args] (let [f (apply op args)] (is (fn? f)) - (is (=? (into [tag {:lib/uuid string?}] - expected-args) + (is (=? {:operator tag, :args expected-args} (f {:lib/metadata meta/metadata} -1))))) (deftest ^:parallel aggregation-test @@ -24,9 +24,8 @@ venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID") venue-field-check [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]] (testing "count" - (testing "without query/stage-number, return a function for later resolution" - (is-fn? lib/count :count [] []) - (is-fn? lib/count :count [venues-category-id-metadata] [venue-field-check]))) + (is-fn? lib/count :count [] []) + (is-fn? lib/count :count [venues-category-id-metadata] [venue-field-check])) (testing "single arg aggregations" (doseq [[op tag] [[lib/avg :avg] [lib/max :max] @@ -35,8 +34,7 @@ [lib/sum :sum] [lib/stddev :stddev] [lib/distinct :distinct]]] - (testing "without query/stage-number, return a function for later resolution" - (is-fn? op tag [venues-category-id-metadata] [venue-field-check])))))) + (is-fn? op tag [venues-category-id-metadata] [venue-field-check]))))) (defn- aggregation-display-name [aggregation-clause] (lib.metadata.calculation/display-name lib.tu/venues-query -1 aggregation-clause)) @@ -173,14 +171,27 @@ [:expression {:base-type :type/Integer, :lib/uuid (str (random-uuid))} "double-price"]]))))) (deftest ^:parallel aggregate-test - (is (=? {:lib/type :mbql/query, - :database (meta/id) , - :type :pipeline, - :stages [{:lib/type :mbql.stage/mbql, - :source-table (meta/id :venues) , - :lib/options {:lib/uuid string?}, - :aggregation [[:sum {:lib/uuid string?} - [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]]]}]} - (-> (lib/query-for-table-name meta/metadata-provider "VENUES") - (lib/aggregate (lib/sum (lib/field "VENUES" "CATEGORY_ID"))) - (dissoc :lib/metadata))))) + (let [q (lib/query-for-table-name meta/metadata-provider "VENUES") + result-query + {:lib/type :mbql/query, + :database (meta/id) , + :type :pipeline, + :stages [{:lib/type :mbql.stage/mbql, + :source-table (meta/id :venues) , + :lib/options {:lib/uuid string?}, + :aggregation [[:sum {:lib/uuid string?} + [:field + {:base-type :type/Integer, :lib/uuid string?} + (meta/id :venues :category-id)]]]}]}] + + (testing "with helper function" + (is (=? result-query + (-> q + (lib/aggregate (lib/sum (lib/field "VENUES" "CATEGORY_ID"))) + (dissoc :lib/metadata))))) + (testing "with external format" + (is (=? result-query + (-> q + (lib/aggregate {:operator :sum + :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]}) + (dissoc :lib/metadata))))))) diff --git a/test/metabase/lib/expression_test.cljc b/test/metabase/lib/expression_test.cljc index 1f32dd4a46d..17fb916f3c6 100644 --- a/test/metabase/lib/expression_test.cljc +++ b/test/metabase/lib/expression_test.cljc @@ -7,6 +7,7 @@ [metabase.lib.expression :as lib.expression] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.schema :as lib.schema] + [metabase.lib.schema.common :as schema.common] [metabase.lib.schema.expression :as schema.expression] [metabase.lib.test-metadata :as meta] [metabase.lib.test-util :as lib.tu])) @@ -79,9 +80,11 @@ (lib/lower string-field) :type/Text])] (testing (str "expression: " (pr-str expr)) (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") - (lib/expression "myexpr" expr))] + (lib/expression "myexpr" expr)) + resolved (lib.expression/resolve-expression query 0 "myexpr")] (is (mc/validate ::lib.schema/query query)) - (is (= typ (schema.expression/type-of (lib.expression/resolve-expression query 0 "myexpr"))))))))) + (is (mc/validate ::schema.common/external-op resolved)) + (is (= typ (schema.expression/type-of resolved)))))))) (deftest ^:parallel col-info-expression-ref-test (is (=? {:base_type :type/Integer diff --git a/test/metabase/lib/filter_test.cljc b/test/metabase/lib/filter_test.cljc index 08fb8925b85..69d3c1e98b4 100644 --- a/test/metabase/lib/filter_test.cljc +++ b/test/metabase/lib/filter_test.cljc @@ -2,17 +2,14 @@ (:require [clojure.test :refer [deftest is testing]] [metabase.lib.core :as lib] + [metabase.lib.field :as lib.field] [metabase.lib.metadata :as lib.metadata] - [metabase.lib.options :as lib.options] [metabase.lib.test-metadata :as meta] #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) -(defn- test-clause [result-filter f ->f & args] - (testing "with query/stage-number, return clause right away" - (is (=? result-filter - (apply f {:lib/metadata meta/metadata} -1 args)))) - (testing "without query/stage-number, return a function for later resolution" - (let [f' (apply ->f args)] +(defn- test-clause [result-filter f & args] + (testing "return a function for later resolution" + (let [f' (apply f args)] (is (fn? f')) (is (=? result-filter (f' {:lib/metadata meta/metadata} -1)))))) @@ -28,79 +25,73 @@ categories-id-metadata (lib.metadata/stage-column q2 -1 "ID") checkins-date-metadata (lib.metadata/field q3 nil "CHECKINS" "DATE")] (testing "comparisons" - (doseq [[op f ->f] [[:= lib/= lib/->=] - [:!= lib/!= lib/->!=] - [:< lib/< lib/-><] - [:<= lib/<= lib/-><=] - [:> lib/> lib/->>] - [:>= lib/>= lib/->>=]]] + (doseq [[op f] [[:= lib/=] + [:!= lib/!=] + [:< lib/<] + [:<= lib/<=] + [:> lib/>] + [:>= lib/>=]]] (test-clause - [op - {:lib/uuid string?} - [:field {:lib/uuid string?} (meta/id :venues :category-id)] - [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]] - f ->f + {:operator op + :args [[:field {:lib/uuid string?} (meta/id :venues :category-id)] + [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]} + f venues-category-id-metadata categories-id-metadata))) (testing "between" (test-clause - [:between - {:lib/uuid string?} - [:field {:lib/uuid string?} (meta/id :venues :category-id)] - 42 - [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]] - lib/between lib/->between + {:operator :between + :args [[:field {:lib/uuid string?} (meta/id :venues :category-id)] + 42 + [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]} + lib/between venues-category-id-metadata 42 categories-id-metadata)) (testing "inside" (test-clause - [:inside - {:lib/uuid string?} - [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :latitude)] - [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :longitude)] - 42.7 13 4 27.3] - lib/inside lib/->inside + {:operator :inside + :args [[:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :latitude)] + [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :longitude)] + 42.7 13 4 27.3]} + lib/inside venues-latitude-metadata venues-longitude-metadata 42.7 13 4 27.3)) (testing "emptiness" - (doseq [[op f ->f] [[:is-null lib/is-null lib/->is-null] - [:not-null lib/not-null lib/->not-null] - [:is-empty lib/is-empty lib/->is-empty] - [:not-empty lib/not-empty lib/->not-empty]]] + (doseq [[op f] [[:is-null lib/is-null] + [:not-null lib/not-null] + [:is-empty lib/is-empty] + [:not-empty lib/not-empty]]] (test-clause - [op - {:lib/uuid string?} - [:field {:lib/uuid string?} (meta/id :venues :name)]] - f ->f + {:operator op + :args [[:field {:lib/uuid string?} (meta/id :venues :name)]]} + f venues-name-metadata))) (testing "string tests" - (doseq [[op f ->f] [[:starts-with lib/starts-with lib/->starts-with] - [:ends-with lib/ends-with lib/->ends-with] - [:contains lib/contains lib/->contains] - [:does-not-contain lib/does-not-contain lib/->does-not-contain]]] + (doseq [[op f] [[:starts-with lib/starts-with] + [:ends-with lib/ends-with] + [:contains lib/contains] + [:does-not-contain lib/does-not-contain]]] (test-clause - [op - {:lib/uuid string?} - [:field {:lib/uuid string?} (meta/id :venues :name)] - "part"] - f ->f + {:operator op + :args [[:field {:lib/uuid string?} (meta/id :venues :name)] + "part"]} + f venues-name-metadata "part"))) (testing "time-interval" (test-clause - [:time-interval - {:lib/uuid string?} - [:field {:base-type :type/Date, :lib/uuid string?} (meta/id :checkins :date)] - 3 - :day] - lib/time-interval lib/->time-interval + {:operator :time-interval + :args [[:field {:base-type :type/Date, :lib/uuid string?} (meta/id :checkins :date)] + 3 + :day]} + lib/time-interval checkins-date-metadata 3 :day)) @@ -108,16 +99,15 @@ (testing "segment" (doseq [id [7 "6"]] (test-clause - [:segment {:lib/uuid string?} id] - lib/segment lib/->segment + {:operator :segment + :args [id]} + lib/segment id))))) (deftest ^:parallel filter-test (let [q1 (lib/query-for-table-name meta/metadata-provider "CATEGORIES") q2 (lib/saved-question-query meta/metadata-provider meta/saved-question) venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID") - venues-name-metadata (lib.metadata/field q1 nil "VENUES" "NAME") - categories-id-metadata (lib.metadata/stage-column q2 -1 "ID") original-filter [:between {:lib/uuid string?} @@ -135,62 +125,26 @@ (testing "no filter" (is (nil? (lib/current-filter q1))) (is (= [] (lib/current-filters q2)))) - (testing "setting a simple filter" - (let [result-query - (lib/filter q1 (lib/between {:lib/metadata meta/metadata} -1 venues-category-id-metadata 42 100))] - (is (=? simple-filtered-query - (dissoc result-query :lib/metadata))) - (testing "and getting the current filter" - (is (=? original-filter - (lib/current-filter result-query))) - (is (=? [original-filter] - (lib/current-filters result-query)))))) - (testing "setting a simple filter thunk" - (is (=? simple-filtered-query - (-> q1 - (lib/filter (lib/->between venues-category-id-metadata 42 100)) - (dissoc :lib/metadata))))) + (testing "setting a simple filter via the helper function" + (let [result-query + (lib/filter q1 (lib/between venues-category-id-metadata 42 100)) + result-filter {:operator (-> original-filter first name) + :options (second original-filter) + :args (subvec original-filter 2)}] + (is (=? simple-filtered-query + (dissoc result-query :lib/metadata))) + (testing "and getting the current filter" + (is (=? result-filter + (lib/current-filter result-query))) + (is (=? [result-filter] + (lib/current-filters result-query)))))) (testing "setting a simple filter expression" (is (=? simple-filtered-query (-> q1 - (lib/filter [:between venues-category-id-metadata 42 100]) - (dissoc :lib/metadata))))) - - (testing "setting a nested filter expression" - (is (=? {:lib/type :mbql/query, - :database (meta/id), - :type :pipeline, - :stages - [{:lib/type :mbql.stage/mbql, - :source-table (meta/id :categories) - :lib/options #:lib{:uuid string?} - :filter - [:or - #:lib{:uuid string?} - [:between - #:lib{:uuid string?} - [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)] - 42 - 100] - [:and - #:lib{:uuid string?} - [:= - #:lib{:uuid string?} - [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)] - 242 - [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]] - [:contains - #:lib{:uuid string?} - [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)] - "part"]]]}]} - (-> q1 - (lib/filter [:or - [:between venues-category-id-metadata 42 100] - [:and - [:= venues-category-id-metadata 242 categories-id-metadata] - [:contains venues-name-metadata "part"]]]) + (lib/filter {:operator :between + :args [(lib.field/field q1 venues-category-id-metadata) 42 100]}) (dissoc :lib/metadata))))))) (deftest ^:parallel add-filter-test @@ -204,41 +158,46 @@ (meta/id :venues :category-id)] 42 100] + first-result-filter + {:operator (-> first-filter first name) + :options (second first-filter) + :args (subvec first-filter 2)} second-filter [:starts-with {:lib/uuid string?} [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)] "prefix"] + second-result-filter + {:operator (-> second-filter first name) + :options (second second-filter) + :args (subvec second-filter 2)} third-filter [:contains {:lib/uuid string?} [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)] "part"] + third-result-filter + {:operator (-> third-filter first name) + :options (second third-filter) + :args (subvec third-filter 2)} first-add (lib/add-filter simple-query - (lib/->between - (lib.metadata/field simple-query nil "VENUES" "CATEGORY_ID") + (lib/between + (lib/field "VENUES" "CATEGORY_ID") 42 100)) filtered-query (assoc-in simple-query [:stages 0 :filter] first-filter) second-add - (lib/add-filter first-add - (lib/starts-with - {:lib/metadata meta/metadata} - 0 - venues-name-metadata - "prefix")) + (lib/add-filter first-add {:operator "starts-with" + :args [(lib.field/field simple-query venues-name-metadata) "prefix"]}) and-query (assoc-in filtered-query [:stages 0 :filter] - [:and - {:lib/uuid string?} - first-filter - second-filter]) + [:and {:lib/uuid string?} first-filter second-filter]) third-add - (lib/add-filter second-add - (lib/->contains venues-name-metadata "part")) + (lib/add-filter second-add {:operator :contains + :args [(lib.field/field simple-query venues-name-metadata) "part"]}) extended-and-query (assoc-in filtered-query [:stages 0 :filter] @@ -252,15 +211,15 @@ "part"]])] (testing "adding an initial filter" (is (=? filtered-query first-add)) - (is (=? [first-filter] + (is (=? [first-result-filter] (lib/current-filters first-add)))) (testing "conjoining to filter" (is (=? and-query second-add)) - (is (=? [first-filter second-filter] + (is (=? [first-result-filter second-result-filter] (lib/current-filters second-add)))) (testing "conjoining to conjunction filter" (is (=? extended-and-query third-add)) - (is (=? [first-filter second-filter third-filter] + (is (=? [first-result-filter second-result-filter third-result-filter] (lib/current-filters third-add)))))) (deftest ^:parallel replace-filter-test @@ -269,9 +228,9 @@ venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID") simple-filtered-query (-> q1 - (lib/filter (lib/->between venues-category-id-metadata 42 100))) + (lib/filter (lib/between venues-category-id-metadata 42 100))) between-uuid (-> (lib/current-filter simple-filtered-query) - lib.options/options + :options :lib/uuid) result-query (assoc-in simple-filtered-query @@ -286,57 +245,16 @@ (is (=? result-query (lib/replace-filter simple-filtered-query between-uuid - (lib/starts-with - {:lib/metadata meta/metadata} - 0 - venues-name-metadata - "part"))))) + (lib/starts-with venues-name-metadata "part"))))) (testing "setting a simple filter thunk" (is (=? result-query (lib/replace-filter simple-filtered-query between-uuid - (lib/->starts-with venues-name-metadata "part"))))) + {:operator "starts-with" + :args [(lib.field/field q1 venues-name-metadata) "part"]})))) (testing "setting a simple filter expression" (is (=? result-query (lib/replace-filter simple-filtered-query between-uuid - [:starts-with venues-name-metadata "part"])))) - - (let [contains-uuid (str (random-uuid)) - nested-filtered-query - {:lib/type :mbql/query, - :database (meta/id), - :type :pipeline, - :stages - [{:lib/type :mbql.stage/mbql, - :source-table (meta/id :categories) - :lib/options {:lib/uuid (str (random-uuid))} - :filter - [:or - {:lib/uuid (str (random-uuid))} - [:between - {:lib/uuid (str (random-uuid))} - [:field {:base-type :type/Integer, :lib/uuid (str (random-uuid))} (meta/id :venues :category-id)] - 42 - 100] - [:and - {:lib/uuid (str (random-uuid))} - [:contains - {:lib/uuid contains-uuid} - [:field {:base-type :type/Text, :lib/uuid (str (random-uuid))} (meta/id :venues :name)] - "part"] - [:= - {:lib/uuid (str (random-uuid))} - [:field {:base-type :type/Integer, :lib/uuid (str (random-uuid))} (meta/id :venues :category-id)] - 242 - [:field {:base-type :type/BigInteger, :lib/uuid (str (random-uuid))} "ID"]]]]}]}] - (testing "setting a nested filter expression" - (is (=? (assoc-in nested-filtered-query - [:stages 0 :filter 3 2] - [:starts-with - {:lib/uuid string?} - [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)] - "part"]) - (lib/replace-filter nested-filtered-query - contains-uuid - [:starts-with venues-name-metadata "part"]))))))) + {:operator :starts-with + :args [(lib.field/field q1 venues-name-metadata) "part"]})))))) diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc index 793567922a2..c88c71f16c6 100644 --- a/test/metabase/lib/join_test.cljc +++ b/test/metabase/lib/join_test.cljc @@ -2,6 +2,7 @@ (:require [clojure.test :refer [deftest is testing]] [metabase.lib.core :as lib] + [metabase.lib.field :as lib.field] [metabase.lib.join :as lib.join] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] @@ -15,7 +16,7 @@ (let [query (lib/query meta/metadata-provider (meta/table-metadata :venues)) join-clause (-> ((lib/join-clause (meta/table-metadata :categories) - (lib/->= + (lib/= (lib/field (meta/id :venues :category-id)) (lib/with-join-alias (lib/field (meta/id :categories :id)) "CATEGORIES__via__CATEGORY_ID"))) query -1) @@ -41,11 +42,13 @@ {:lib/uuid string?} [:field {:lib/uuid string?} (meta/id :venues :category-id)] [:field {:lib/uuid string?} (meta/id :categories :id)]]}]}]} - (-> (lib/query-for-table-name meta/metadata-provider "VENUES") - (lib/join (lib/query-for-table-name meta/metadata-provider "CATEGORIES") - (lib/->= (lib/field "VENUES" "CATEGORY_ID") - (lib/field "CATEGORIES" "ID"))) - (dissoc :lib/metadata))))) + (let [q (lib/query-for-table-name meta/metadata-provider "VENUES")] + (-> q + (lib/join (lib/query-for-table-name meta/metadata-provider "CATEGORIES") + {:operator := + :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID")) + (lib.field/field q (lib.metadata/field q nil "CATEGORIES" "ID"))]}) + (dissoc :lib/metadata)))))) (deftest ^:parallel join-saved-question-test (is (=? {:lib/type :mbql/query @@ -65,8 +68,8 @@ [:field {:lib/uuid string?} (meta/id :categories :id)]]}]}]} (-> (lib/query-for-table-name meta/metadata-provider "CATEGORIES") (lib/join (lib/saved-question-query meta/metadata-provider meta/saved-question) - (lib/->= (lib/field "VENUES" "CATEGORY_ID") - (lib/field "CATEGORIES" "ID"))) + (lib/= (lib/field "VENUES" "CATEGORY_ID") + (lib/field "CATEGORIES" "ID"))) (dissoc :lib/metadata))))) (deftest ^:parallel join-condition-field-metadata-test @@ -76,7 +79,7 @@ venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID") categories-id-metadata (lib.metadata/stage-column q2 "ID")] (testing "lib/join-clause: return a function that can be resolved later" - (let [f (lib/join-clause q2 (lib/->= venues-category-id-metadata categories-id-metadata))] + (let [f (lib/join-clause q2 (lib/= venues-category-id-metadata categories-id-metadata))] (is (fn? f)) (is (=? {:lib/type :mbql/join :lib/options {:lib/uuid string?} @@ -98,7 +101,7 @@ [:field {:lib/uuid string?} (meta/id :venues :category-id)] [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]}]}]} (-> q1 - (lib/join q2 (lib/->= venues-category-id-metadata categories-id-metadata)) + (lib/join q2 (lib/= venues-category-id-metadata categories-id-metadata)) (dissoc :lib/metadata))))))) ;;; FIXME diff --git a/test/metabase/lib/query_test.cljc b/test/metabase/lib/query_test.cljc index f2d430a3574..642940c94b4 100644 --- a/test/metabase/lib/query_test.cljc +++ b/test/metabase/lib/query_test.cljc @@ -15,7 +15,7 @@ (lib/aggregate (lib/sum (lib/field (meta/id :venues :price))))) ;; wrong arity: there's a bug in our Kondo config, see https://metaboat.slack.com/archives/C04DN5VRQM6/p1679022185079739?thread_ts=1679022025.317059&cid=C04DN5VRQM6 query (-> #_{:clj-kondo/ignore [:invalid-arity]} - (lib/filter query (lib/= query -1 (lib/field (meta/id :venues :name)) "Toucannery")) + (lib/filter query (lib/= (lib/field (meta/id :venues :name)) "Toucannery")) (lib/breakout (lib/field (meta/id :venues :category-id))) (lib/order-by (lib/field (meta/id :venues :id))) (lib/limit 100))] diff --git a/test/metabase/lib/util_test.cljc b/test/metabase/lib/util_test.cljc index 6ef3efb1c45..aeb3da851b7 100644 --- a/test/metabase/lib/util_test.cljc +++ b/test/metabase/lib/util_test.cljc @@ -220,7 +220,7 @@ (deftest ^:parallel replace-clause-test (checking "can be called with anything" - [x gen/any] + [x gen/any-equatable] (is (identical? x (lib.util/replace-clause x (str (random-uuid)) (random-uuid))))) -- GitLab