From df163979fd0a8466045d6319e8dbd9dfd7723bc5 Mon Sep 17 00:00:00 2001
From: metamben <103100869+metamben@users.noreply.github.com>
Date: Thu, 24 Aug 2023 21:06:00 +0300
Subject: [PATCH] Add filter-parts conversion helper (#33419)

---
 frontend/src/metabase-lib/filter.ts |  9 ++++++
 frontend/src/metabase-lib/types.ts  |  7 ++++
 src/metabase/lib/core.cljc          |  1 +
 src/metabase/lib/filter.cljc        | 50 ++++++++++++++++++++++++-----
 src/metabase/lib/js.cljs            |  9 ++++++
 test/metabase/lib/filter_test.cljc  | 28 ++++++++++++++++
 6 files changed, 96 insertions(+), 8 deletions(-)

diff --git a/frontend/src/metabase-lib/filter.ts b/frontend/src/metabase-lib/filter.ts
index 4f3ddc7bcb0..2cd69e732f0 100644
--- a/frontend/src/metabase-lib/filter.ts
+++ b/frontend/src/metabase-lib/filter.ts
@@ -7,6 +7,7 @@ import type {
   ExternalOp,
   FilterOperator,
   FilterClause,
+  FilterParts,
   Query,
 } from "./types";
 
@@ -50,3 +51,11 @@ export function filterOperator(
 ) {
   return ML.filter_operator(query, stageIndex, filterClause);
 }
+
+export function filterParts(
+  query: Query,
+  stageIndex: number,
+  filterClause: FilterClause,
+): FilterParts {
+  return ML.filter_parts(query, stageIndex, filterClause);
+}
diff --git a/frontend/src/metabase-lib/types.ts b/frontend/src/metabase-lib/types.ts
index be49cfc4da7..c7d28ce3e29 100644
--- a/frontend/src/metabase-lib/types.ts
+++ b/frontend/src/metabase-lib/types.ts
@@ -156,6 +156,13 @@ export type ExternalOp = {
   args: [ColumnMetadata, ...ExpressionArg[]];
 };
 
+export type FilterParts = {
+  operator: FilterOperator;
+  options: Record<string, unknown>;
+  column: ColumnWithOperators | null;
+  args: ExpressionArg[];
+};
+
 declare const Join: unique symbol;
 export type Join = unknown & { _opaque: typeof Join };
 
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index ae0befbcefe..6a3989a6468 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -158,6 +158,7 @@
   filterable-column-operators
   filter-clause
   filter-operator
+  filter-parts
   and
   or
   not
diff --git a/src/metabase/lib/filter.cljc b/src/metabase/lib/filter.cljc
index e84e207cfc2..101e10a8d94 100644
--- a/src/metabase/lib/filter.cljc
+++ b/src/metabase/lib/filter.cljc
@@ -15,6 +15,7 @@
    [metabase.lib.options :as lib.options]
    [metabase.lib.ref :as lib.ref]
    [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.expression :as lib.schema.expression]
    [metabase.lib.schema.filter :as lib.schema.filter]
    [metabase.lib.temporal-bucket :as lib.temporal-bucket]
@@ -189,6 +190,11 @@
   [filterable-column :- ColumnWithOperators]
   (:operators filterable-column))
 
+(defn- add-column-operators
+  [column]
+  (let [operators (lib.filter.operator/filter-operators column)]
+    (m/assoc-some column :operators (clojure.core/not-empty operators))))
+
 (mu/defn filterable-columns :- [:sequential ColumnWithOperators]
   "Get column metadata for all the columns that can be filtered in
   the stage number `stage-number` of the query `query`
@@ -209,15 +215,12 @@
 
   ([query        :- ::lib.schema/query
     stage-number :- :int]
-   (let [stage (lib.util/query-stage query stage-number)
-         columns (lib.metadata.calculation/visible-columns query stage-number stage)
-         with-operators (fn [column]
-                          (when-let [operators (clojure.core/not-empty (lib.filter.operator/filter-operators column))]
-                            (assoc column :operators operators)))]
+   (let [stage (lib.util/query-stage query stage-number)]
      (clojure.core/not-empty
-       (into []
-             (keep with-operators)
-             columns)))))
+      (into []
+            (comp (map add-column-operators)
+                  (clojure.core/filter :operators))
+            (lib.metadata.calculation/visible-columns query stage-number stage))))))
 
 (mu/defn filter-clause :- ::lib.schema.expression/boolean
   "Returns a standalone filter clause for a `filter-operator`,
@@ -246,3 +249,34 @@
      (clojure.core/or (m/find-first #(clojure.core/= (:short %) op)
                                     (lib.filter.operator/filter-operators (ref->col col-ref)))
                       (lib.filter.operator/operator-def op)))))
+
+(def ^:private FilterParts
+  [:map
+   [:lib/type [:= :mbql/filter-parts]]
+   [:operator ::lib.schema.filter/operator]
+   [:options ::lib.schema.common/options]
+   [:column [:maybe ColumnWithOperators]]
+   [:args [:sequential :any]]])
+
+(mu/defn filter-parts :- FilterParts
+  "Return the parts of the filter clause `a-filter-clause` in query `query` at stage `stage-number`.
+  Might obsolate [[filter-operator]]."
+  ([query a-filter-clause]
+   (filter-parts query -1 a-filter-clause))
+
+  ([query :- ::lib.schema/query
+    stage-number :- :int
+    a-filter-clause :- ::lib.schema.expression/boolean]
+   (let [[op options first-arg & rest-args] a-filter-clause
+         stage (lib.util/query-stage query stage-number)
+         columns (lib.metadata.calculation/visible-columns query stage-number stage)
+         ref->col (m/index-by lib.ref/ref columns)
+         col-ref (lib.equality/find-closest-matching-ref query first-arg (keys ref->col))
+         col (ref->col col-ref)]
+     {:lib/type :mbql/filter-parts
+      :operator (clojure.core/or (m/find-first #(clojure.core/= (:short %) op)
+                                               (lib.filter.operator/filter-operators col))
+                                 (lib.filter.operator/operator-def op))
+      :options  options
+      :column   (some-> col add-column-operators)
+      :args     (vec rest-args)})))
diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs
index ed78f6e11ca..31ceb5576dc 100644
--- a/src/metabase/lib/js.cljs
+++ b/src/metabase/lib/js.cljs
@@ -372,6 +372,15 @@
   [a-query stage-number a-filter-clause]
   (lib.core/filter-operator a-query stage-number a-filter-clause))
 
+(defn ^:export filter-parts
+  "Returns the parts (operator, args, and optionally, options) of `filter-clause`."
+  [a-query stage-number a-filter-clause]
+  (let [{:keys [operator options column args]} (lib.core/filter-parts a-query stage-number a-filter-clause)]
+    #js {:operator operator
+         :options (clj->js (select-keys options [:case-sensitive :include-current]))
+         :column column
+         :args (to-array args)}))
+
 (defn ^:export filter
   "Sets `boolean-expression` as a filter on `query`."
   [a-query stage-number boolean-expression]
diff --git a/test/metabase/lib/filter_test.cljc b/test/metabase/lib/filter_test.cljc
index a22042b57e3..838e95d77aa 100644
--- a/test/metabase/lib/filter_test.cljc
+++ b/test/metabase/lib/filter_test.cljc
@@ -318,6 +318,34 @@
         (is (= op
                (lib/filter-operator query filter-clause)))))))
 
+(deftest ^:parallel filter-parts-test
+  (let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :users))
+                  (lib/join (-> (lib/join-clause (meta/table-metadata :checkins)
+                                                 [(lib/=
+                                                   (meta/field-metadata :checkins :user-id)
+                                                   (meta/field-metadata :users :id))])
+                                (lib/with-join-fields :all)))
+                  (lib/join (-> (lib/join-clause (meta/table-metadata :venues)
+                                                 [(lib/=
+                                                   (meta/field-metadata :checkins :venue-id)
+                                                   (meta/field-metadata :venues :id))])
+                                (lib/with-join-fields :all))))]
+    (doseq [col (lib/filterable-columns query)
+            op (lib/filterable-column-operators col)
+            :let [filter-clause (case (:short op)
+                                  :between (lib/filter-clause op col 123 456)
+                                  (:contains :does-not-contain :starts-with :ends-with) (lib/filter-clause op col "123")
+                                  (:is-null :not-null :is-empty :not-empty) (lib/filter-clause op col)
+                                  :inside (lib/filter-clause op col 12 34 56 78 90)
+                                  (lib/filter-clause op col 123))]]
+      (testing (str (:short op) " with " (lib.types.isa/field-type col))
+        (is (=? {:lib/type :mbql/filter-parts
+                 :operator op
+                 :column {:lib/type :metadata/column
+                          :operators (comp vector? not-empty)}
+                 :args vector?}
+                (lib/filter-parts query filter-clause)))))))
+
 (deftest ^:parallel replace-filter-clause-test
   (testing "Make sure we are able to replace a filter clause using the lib functions for manipulating filters."
     (let [query           (lib/query meta/metadata-provider (meta/table-metadata :users))
-- 
GitLab