From 7866f7e20be21cb8d80db70b6ce7c583c97c6b68 Mon Sep 17 00:00:00 2001
From: Ngoc Khuat <qn.khuat@gmail.com>
Date: Wed, 3 Apr 2024 22:27:25 +0700
Subject: [PATCH] Fix unable to search field values from partitioned tables
 (#40767)

---
 .../driver/bigquery_cloud_sdk_test.clj        | 101 ++++++++++++++++++
 src/metabase/api/field.clj                    |  25 +----
 src/metabase/db/metadata_queries.clj          |  94 ++++++++++------
 .../driver/common/parameters/dates.clj        |   3 +-
 .../driver/common/parameters/operators.clj    |   4 +-
 src/metabase/legacy_mbql/util.cljc            |  30 ++++++
 src/metabase/models/field_values.clj          |   7 +-
 src/metabase/models/params.clj                |  34 +-----
 src/metabase/models/params/chain_filter.clj   |   7 +-
 src/metabase/models/table.clj                 |  26 +++--
 .../middleware/check_features.clj             |  10 +-
 .../middleware/parameters/mbql.clj            |   7 +-
 .../sync/analyze/fingerprint/insights.clj     |   3 +-
 src/metabase/sync/util.clj                    |   3 +-
 test/metabase/db/metadata_queries_test.clj    |  53 ++++++++-
 test/metabase/models/params_test.clj          |   3 +-
 .../query_processor_test/aggregation_test.clj |   2 +-
 17 files changed, 282 insertions(+), 130 deletions(-)

diff --git a/modules/drivers/bigquery-cloud-sdk/test/metabase/driver/bigquery_cloud_sdk_test.clj b/modules/drivers/bigquery-cloud-sdk/test/metabase/driver/bigquery_cloud_sdk_test.clj
index 9626e5a5ef1..fb00db41876 100644
--- a/modules/drivers/bigquery-cloud-sdk/test/metabase/driver/bigquery_cloud_sdk_test.clj
+++ b/modules/drivers/bigquery-cloud-sdk/test/metabase/driver/bigquery_cloud_sdk_test.clj
@@ -12,6 +12,7 @@
    [metabase.query-processor :as qp]
    [metabase.query-processor.compile :as qp.compile]
    [metabase.query-processor.pipeline :as qp.pipeline]
+   [metabase.query-processor.test-util :as qp.test-util]
    [metabase.sync :as sync]
    [metabase.test :as mt]
    [metabase.test.data.bigquery-cloud-sdk :as bigquery.tx]
@@ -419,6 +420,106 @@
            (finally
             (drop-table-if-exists! table-name))))))))
 
+(deftest search-field-from-table-requires-a-filter-test
+  (testing "#40673"
+    (mt/test-driver :bigquery-cloud-sdk
+      (mt/with-model-cleanup [:model/Table]
+        (let [partitioned-table "fv_partitioned_table"]
+          (try
+           (doseq [sql [(format "CREATE TABLE %s (id INT64, category STRING)
+                                PARTITION BY _PARTITIONDATE
+                                OPTIONS (require_partition_filter = TRUE);"
+                                (fmt-table-name partitioned-table))
+                        (format "INSERT INTO %s (id, category)
+                                VALUES (1, \"coffee\"), (2, \"tea\"), (3, \"matcha\");"
+                                (fmt-table-name partitioned-table))]]
+             (bigquery.tx/execute! sql))
+           (sync/sync-database! (mt/db) {:scan :schema})
+           (let [category-field-id (mt/id :fv_partitioned_table :category)]
+             (t2/update! :model/Field category-field-id {:has_field_values :search})
+             (t2/delete! :model/FieldValues :field_id category-field-id)
+             (= [["coffee"]]
+                (mt/user-http-request :crowberto :get 200 (format "/field/%d/search/%d" category-field-id category-field-id)
+                                      :value "co")))
+           (finally
+            (drop-table-if-exists! partitioned-table))))))))
+
+(deftest chain-filter-with-fields-from-table-requires-a-filter-test
+  (testing "#40673"
+    (mt/test-driver :bigquery-cloud-sdk
+      (binding [qp.test-util/*enable-fk-support-for-disabled-drivers-in-tests* true]
+        (mt/with-model-cleanup [:model/Table]
+          (let [category-table-name "cf_category"
+                product-table-name  "cf_product"]
+            (try
+              (doseq [sql [(format "CREATE TABLE %s (id INT64, category STRING, PRIMARY KEY(id) NOT ENFORCED)
+                                   PARTITION BY _PARTITIONDATE
+                                   OPTIONS (require_partition_filter = TRUE);"
+                                   (fmt-table-name category-table-name))
+                           (format "INSERT INTO %s (id, category)
+                                   VALUES (1, \"coffee\"), (2, \"tea\");"
+                                   (fmt-table-name category-table-name))
+                           (format "CREATE TABLE %s (id INT64, category_id INT64, name STRING)
+                                   PARTITION BY _PARTITIONDATE
+                                   OPTIONS (require_partition_filter = TRUE);"
+                                   (fmt-table-name product-table-name))
+                           (format "ALTER TABLE %1$s
+                                   ADD CONSTRAINT fk_product_category_id FOREIGN KEY (category_id)
+                                   REFERENCES %2$s(id) NOT ENFORCED;"
+                                   (fmt-table-name product-table-name)
+                                   (fmt-table-name category-table-name))
+                           (format "INSERT INTO %s (id, category_id, name)
+                                   VALUES (1, 1, \"Americano\"), (2, 1, \"Cold brew\"), (3, 2, \"Herbal\"), (4, 2, \"Oolong\");"
+                                   (fmt-table-name product-table-name))]]
+                (bigquery.tx/execute! sql))
+              (sync/sync-database! (mt/db) {:scan :schema})
+              ;; Fake fk relationship for bigquery because apparently fk on bigquery is not a thing.
+              ;; We want this to test whether chain filter add a filter on partitioned fields from joned tables.
+              (t2/update! :model/Field (mt/id :cf_product :category_id) {:fk_target_field_id (mt/id :cf_category :id)})
+              (mt/with-temp
+                [:model/Card          card-category {:database_id   (mt/id)
+                                                     :table_id      (mt/id :cf_category)
+                                                     :dataset_query (mt/mbql-query cf_category)}
+                 :model/Card          card-product  {:database_id   (mt/id)
+                                                     :table_id      (mt/id :cf_product)
+                                                     :dataset_query (mt/mbql-query cf_product)}
+                 :model/Dashboard     dashboard     {:parameters [{:name "Category"
+                                                                   :slug "category"
+                                                                   :id   "_CATEGORY_"
+                                                                   :type :string/=}
+                                                                  {:name "Product Name"
+                                                                   :slug "Product name"
+                                                                   :id   "_NAME_"
+                                                                   :type :string/=}]}
+                 :model/DashboardCard _dashcard     {:card_id            (:id card-category)
+                                                     :dashboard_id       (:id dashboard)
+                                                     :parameter_mappings [{:parameter_id "_CATEGORY_"
+                                                                           :card_id      (:id card-category)
+                                                                           :target       [:dimension (mt/$ids $cf_category.category)]}]}
+                 :model/DashboardCard _dashcard     {:card_id            (:id card-product)
+                                                     :dashboard_id       (:id dashboard)
+                                                     :parameter_mappings [{:parameter_id "_NAME_"
+                                                                           :card_id      (:id card-product)
+                                                                           :target       [:dimension (mt/$ids $cf_product.name)]}]}]
+
+                (testing "chained filter works"
+                  (is (= {:has_more_values false
+                          :values          [["Americano"] ["Cold brew"]]}
+                         (mt/user-http-request :crowberto :get 200 (format "/dashboard/%d/params/%s/values?%s=%s"
+                                                                           (:id dashboard) "_NAME_" "_CATEGORY_" "coffee")))))
+                (testing "getting values works"
+                  (is (= {:has_more_values false
+                          :values          [["Americano"] ["Cold brew"] ["Herbal"] ["Oolong"]]}
+                         (mt/user-http-request :crowberto :get 200 (format "/dashboard/%d/params/%s/values" (:id dashboard) "_NAME_")))))
+                (testing "searching values works"
+                  (is (= {:has_more_values false
+                          :values          [["Oolong"]]}
+                         (mt/user-http-request :crowberto :get 200 (format "/dashboard/%d/params/%s/search/oo" (:id dashboard) "_NAME_"))))))
+
+             (finally
+               (doseq [table-name [product-table-name category-table-name]]
+                 (drop-table-if-exists! table-name))))))))))
+
 (deftest query-integer-pk-or-fk-test
   (mt/test-driver :bigquery-cloud-sdk
     (testing "We should be able to query a Table that has a :type/Integer column marked as a PK or FK"
diff --git a/src/metabase/api/field.clj b/src/metabase/api/field.clj
index 6d6743f29bf..330f15b8831 100644
--- a/src/metabase/api/field.clj
+++ b/src/metabase/api/field.clj
@@ -351,26 +351,6 @@
     (t2/select-one Field :id fk-target-field-id)
     field))
 
-(defn- search-values-query
-  "Generate the MBQL query used to power FieldValues search in [[search-values]] below. The actual query generated
-  differs slightly based on whether the two Fields are the same Field.
-
-  Note: the generated MBQL query assume that both `field` and `search-field` are from the same table."
-  [field search-field value limit]
-  {:database (db-id field)
-   :type     :query
-   :query    {:source-table (table-id field)
-              :filter       (when (some? value)
-                              [:contains [:field (u/the-id search-field) nil] value {:case-sensitive false}])
-              ;; if both fields are the same then make sure not to refer to it twice in the `:breakout` clause.
-              ;; Otherwise this will break certain drivers like BigQuery that don't support duplicate
-              ;; identifiers/aliases
-              :breakout     (if (= (u/the-id field) (u/the-id search-field))
-                              [[:field (u/the-id field) nil]]
-                              [[:field (u/the-id field) nil]
-                               [:field (u/the-id search-field) nil]])
-              :limit        limit}})
-
 (mu/defn search-values :- [:maybe ms/FieldValuesList]
   "Search for values of `search-field` that contain `value` (up to `limit`, if specified), and return pairs like
 
@@ -398,9 +378,8 @@
    (try
     (let [field        (follow-fks field)
           search-field (follow-fks search-field)
-          limit        (or maybe-limit default-max-field-search-limit)
-          results      (qp/process-query (search-values-query field search-field value limit))]
-      (get-in results [:data :rows]))
+          limit        (or maybe-limit default-max-field-search-limit)]
+      (metadata-queries/search-values-query field search-field value limit))
     (catch Throwable e
       (log/error e (trs "Error searching field values"))
       []))))
diff --git a/src/metabase/db/metadata_queries.clj b/src/metabase/db/metadata_queries.clj
index 25c852a7b3d..286268e0de7 100644
--- a/src/metabase/db/metadata_queries.clj
+++ b/src/metabase/db/metadata_queries.clj
@@ -8,7 +8,6 @@
    [metabase.driver.util :as driver.u]
    [metabase.legacy-mbql.schema :as mbql.s]
    [metabase.legacy-mbql.schema.helpers :as helpers]
-   [metabase.models.table :as table]
    [metabase.query-processor :as qp]
    [metabase.query-processor.interface :as qp.i]
    [metabase.util :as u]
@@ -16,7 +15,8 @@
    [metabase.util.malli.schema :as ms]
    [toucan2.core :as t2]))
 
-(defn- qp-query [db-id mbql-query]
+(defn- qp-query
+  [db-id mbql-query]
   {:pre [(integer? db-id)]}
   (-> (binding [qp.i/*disable-qp-logging* true]
         (qp/process-query
@@ -36,39 +36,44 @@
       :type/Date     [:> field-form "0001-01-01"]
       :type/DateTime [:> field-form "0001-01-01T00:00:00"])))
 
-(defn- query-with-default-partitioned-field-filter
-  [query table-id]
-  (let [;; In bigquery, range or datetime partitioned table can have only one partioned field,
-        ;; Ingestion time partitioned table can use either _PARTITIONDATE or _PARTITIONTIME as
-        ;; partitioned field
-        partition-field (or (t2/select-one :model/Field
-                                           :table_id table-id
-                                           :database_partitioned true
-                                           :active true
-                                           ;; prefer _PARTITIONDATE over _PARTITIONTIME for ingestion time query
-                                           {:order-by [[:name :asc]]})
-                            (throw (ex-info (format "No partitioned field found for table: %d" table-id)
-                                            {:table_id table-id})))
-        filter-form     (partition-field->filter-form partition-field)]
-    (update query :filter (fn [existing-filter]
-                            (if (some? existing-filter)
-                              [:and existing-filter filter-form]
-                              filter-form)))))
+(defn add-required-filters-if-needed
+  "Add a dummy filter for tables that require filters.
+  Look into tables from source tables and all the joins.
+  Currently this only apply to partitioned tables on bigquery that requires a partition filter.
+  In the future we probably want this to be dispatched by database engine or handled by QP."
+  [query]
+  (let [table-ids              (->> (conj (keep :source-table (:joins query)) (:source-table query))
+                                    (filter pos-int?))
+        required-filter-fields (when (seq table-ids)
+                                 (t2/select :model/Field {:select    [:f.*]
+                                                          :from      [[:metabase_field :f]]
+                                                          :left-join [[:metabase_table :t] [:= :t.id :f.table_id]]
+                                                          :where     [:and
+                                                                      [:= :f.active true]
+                                                                      [:= :f.database_partitioned true]
+                                                                      [:= :t.active true]
+                                                                      [:= :t.database_require_filter true]
+                                                                      [:in :t.id table-ids]]}))
+        update-query-filter-fn (fn [existing-filter new-filter]
+                                (if (some? existing-filter)
+                                  [:and existing-filter new-filter]
+                                  new-filter))]
+    (case (count required-filter-fields)
+      0
+      query
+      1
+      (update query :filter update-query-filter-fn (partition-field->filter-form (first required-filter-fields)))
+      ;; > 1
+      (update query :filter update-query-filter-fn (into [:and] (map partition-field->filter-form required-filter-fields))))))
 
 (defn- field-mbql-query
   [table mbql-query]
-  (cond-> mbql-query
-    true
-    (assoc :source-table (:id table))
-
-    ;; Some table requires a filter to be able to query the data
-    ;; Currently this only applied to Partitioned table in bigquery where the partition field
-    ;; is required as a filter.
-    ;; In the future we probably want this to be dispatched by database engine type
-    (:database_require_filter table)
-    (query-with-default-partitioned-field-filter (:id table))))
-
-(defn- field-query [{table-id :table_id} mbql-query]
+  (-> mbql-query
+      (assoc :source-table (:id table))
+      add-required-filters-if-needed))
+
+(defn- field-query
+  [{table-id :table_id} mbql-query]
   {:pre [(integer? table-id)]}
   (let [table (t2/select-one :model/Table :id table-id)]
     (qp-query (:db_id table)
@@ -104,6 +109,25 @@
    (field-query field {:breakout [[:field (u/the-id field) nil]]
                        :limit    (min max-results absolute-max-distinct-values-limit)})))
 
+;; I'm not sure whether this field-distinct-values and field-distinct-values belong to this namespace
+;; maybe it's better to keep this in metabase.models.field or metabase.models.field-values
+(defn search-values-query
+ "Generate the MBQL query used to power FieldValues search in [[metabase.api.field/search-values]]. The actual query generated
+  differs slightly based on whether the two Fields are the same Field.
+
+  Note: the generated MBQL query assume that both `field` and `search-field` are from the same table."
+ [field search-field value limit]
+ (field-query field {:filter   (when (some? value)
+                                 [:contains [:field (u/the-id search-field) nil] value {:case-sensitive false}])
+                     ;; if both fields are the same then make sure not to refer to it twice in the `:breakout` clause.
+                     ;; Otherwise this will break certain drivers like BigQuery that don't support duplicate
+                     ;; identifiers/aliases
+                     :breakout (if (= (u/the-id field) (u/the-id search-field))
+                                 [[:field (u/the-id field) nil]]
+                                 [[:field (u/the-id field) nil]
+                                  [:field (u/the-id search-field) nil]])
+                     :limit    limit}))
+
 (defn field-distinct-count
   "Return the distinct count of `field`."
   [field & [limit]]
@@ -149,7 +173,7 @@
   [table
    fields
    {:keys [truncation-size limit order-by] :or {limit max-sample-rows} :as _opts}]
-  (let [database           (table/database table)
+  (let [database           (t2/select-one :model/Database (:db_id table))
         driver             (driver.u/database->driver database)
         text-fields        (filter text-field? fields)
         field->expressions (when (and truncation-size (driver/database-supports? driver :expressions database))
@@ -169,8 +193,8 @@
                    order-by
                    (assoc :order-by order-by)
 
-                   (:database_require_filter table)
-                   (query-with-default-partitioned-field-filter (:id table)))
+                   true
+                   add-required-filters-if-needed)
      :middleware {:format-rows?           false
                   :skip-results-metadata? true}}))
 
diff --git a/src/metabase/driver/common/parameters/dates.clj b/src/metabase/driver/common/parameters/dates.clj
index 54c5cd6a75b..bae7440dbec 100644
--- a/src/metabase/driver/common/parameters/dates.clj
+++ b/src/metabase/driver/common/parameters/dates.clj
@@ -7,7 +7,6 @@
    [metabase.legacy-mbql.schema :as mbql.s]
    [metabase.legacy-mbql.util :as mbql.u]
    [metabase.lib.schema.parameter :as lib.schema.parameter]
-   [metabase.models.params :as params]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.util.date-2 :as u.date]
    [metabase.util.i18n :refer [tru]]
@@ -473,7 +472,7 @@
    returns a corresponding MBQL filter clause for a given field reference."
   [date-string :- :string
    field       :- [:or ms/PositiveInt mbql.s/Field]]
-  (or (execute-decoders all-date-string-decoders :filter (params/wrap-field-id-if-needed field) date-string)
+  (or (execute-decoders all-date-string-decoders :filter (mbql.u/wrap-field-id-if-needed field) date-string)
       (throw (ex-info (tru "Don''t know how to parse date string {0}" (pr-str date-string))
                       {:type        qp.error-type/invalid-parameter
                        :date-string date-string}))))
diff --git a/src/metabase/driver/common/parameters/operators.clj b/src/metabase/driver/common/parameters/operators.clj
index af36c58df1f..7ba42202953 100644
--- a/src/metabase/driver/common/parameters/operators.clj
+++ b/src/metabase/driver/common/parameters/operators.clj
@@ -9,8 +9,8 @@
      :value [3 5]}"
   (:require
    [metabase.legacy-mbql.schema :as mbql.s]
+   [metabase.legacy-mbql.util :as mbql.u]
    [metabase.lib.schema.parameter :as lib.schema.parameter]
-   [metabase.models.params :as params]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.util.i18n :refer [tru]]
    [metabase.util.malli :as mu]))
@@ -60,7 +60,7 @@
   `:type qp.error-type/invalid-parameter` if arity is incorrect."
   [{param-type :type [a b :as param-value] :value [_ field :as _target] :target options :options :as _param}]
   (verify-type-and-arity field param-type param-value)
-  (let [field' (params/wrap-field-id-if-needed field)]
+  (let [field' (mbql.u/wrap-field-id-if-needed field)]
     (case (operator-arity param-type)
       :binary
       (cond-> [(keyword (name param-type)) field' a b]
diff --git a/src/metabase/legacy_mbql/util.cljc b/src/metabase/legacy_mbql/util.cljc
index 148e479f4cc..9c88ccdcf5d 100644
--- a/src/metabase/legacy_mbql/util.cljc
+++ b/src/metabase/legacy_mbql/util.cljc
@@ -762,3 +762,33 @@
           (sequential? form) (recur (onto-stack (map-indexed vector form)) matches)
           :else              (recur stack                                  matches)))
       matches)))
+
+(defn wrap-field-id-if-needed
+  "Wrap a raw Field ID in a `:field` clause if needed."
+  [field-id-or-form]
+  (cond
+    (mbql-clause? field-id-or-form)
+    field-id-or-form
+
+    (integer? field-id-or-form)
+    [:field field-id-or-form nil]
+
+    :else
+    field-id-or-form))
+
+(mu/defn unwrap-field-clause :- [:maybe mbql.s/field]
+  "Unwrap something that contains a `:field` clause, such as a template tag.
+  Also handles unwrapped integers for legacy compatibility.
+
+    (unwrap-field-clause [:field 100 nil]) ; -> [:field 100 nil]"
+  [field-form]
+  (if (integer? field-form)
+    [:field field-form nil]
+    (lib.util.match/match-one field-form :field)))
+
+(mu/defn unwrap-field-or-expression-clause :- mbql.s/Field
+  "Unwrap a `:field` clause or expression clause, such as a template tag. Also handles unwrapped integers for
+  legacy compatibility."
+  [field-or-ref-form]
+  (or (unwrap-field-clause field-or-ref-form)
+      (lib.util.match/match-one field-or-ref-form :expression)))
diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj
index 9b9eb8fc464..c42e2727bb5 100644
--- a/src/metabase/models/field_values.clj
+++ b/src/metabase/models/field_values.clj
@@ -27,10 +27,10 @@
    [java-time.api :as t]
    [malli.core :as mc]
    [medley.core :as m]
+   [metabase.db.metadata-queries :as metadata-queries]
    [metabase.db.query :as mdb.query]
    [metabase.models.interface :as mi]
    [metabase.models.serialization :as serdes]
-   [metabase.plugins.classloader :as classloader]
    [metabase.public-settings.premium-features :refer [defenterprise]]
    [metabase.util :as u]
    [metabase.util.date-2 :as u.date]
@@ -343,9 +343,8 @@
   FieldValues. You most likely should not be using this directly in code outside of this namespace, unless it's for a
   very specific reason, such as certain cases where we fetch ad-hoc FieldValues for GTAP-filtered Fields.)"
   [field]
-  (classloader/require 'metabase.db.metadata-queries)
   (try
-    (let [distinct-values         ((resolve 'metabase.db.metadata-queries/field-distinct-values) field)
+    (let [distinct-values         (metadata-queries/field-distinct-values field)
           limited-distinct-values (take-by-length *total-max-length* distinct-values)]
       {:values          limited-distinct-values
        ;; has_more_values=true means the list of values we return is a subset of all possible values.
@@ -359,7 +358,7 @@
                           ;; So, if the returned `distinct-values` has length equal to that exact limit,
                           ;; we assume the returned values is just a subset of what we have in DB.
                           (= (count distinct-values)
-                             @(resolve 'metabase.db.metadata-queries/absolute-max-distinct-values-limit)))})
+                             metadata-queries/absolute-max-distinct-values-limit))})
     (catch Throwable e
       (log/error e (trs "Error fetching field values"))
       nil)))
diff --git a/src/metabase/models/params.clj b/src/metabase/models/params.clj
index f851304d887..f93700a7a1b 100644
--- a/src/metabase/models/params.clj
+++ b/src/metabase/models/params.clj
@@ -47,36 +47,6 @@
     (throw (ex-info (tru ":parameter_mappings must be a sequence of maps with :parameter_id and :type keys")
                     {:parameter_mappings parameter_mappings}))))
 
-(mu/defn unwrap-field-clause :- [:maybe mbql.s/field]
-  "Unwrap something that contains a `:field` clause, such as a template tag.
-  Also handles unwrapped integers for legacy compatibility.
-
-    (unwrap-field-clause [:field 100 nil]) ; -> [:field 100 nil]"
-  [field-form]
-  (if (integer? field-form)
-    [:field field-form nil]
-    (lib.util.match/match-one field-form :field)))
-
-(mu/defn unwrap-field-or-expression-clause :- mbql.s/Field
-  "Unwrap a `:field` clause or expression clause, such as a template tag. Also handles unwrapped integers for
-  legacy compatibility."
-  [field-or-ref-form]
-  (or (unwrap-field-clause field-or-ref-form)
-      (lib.util.match/match-one field-or-ref-form :expression)))
-
-(defn wrap-field-id-if-needed
-  "Wrap a raw Field ID in a `:field` clause if needed."
-  [field-id-or-form]
-  (cond
-    (mbql.u/mbql-clause? field-id-or-form)
-    field-id-or-form
-
-    (integer? field-id-or-form)
-    [:field field-id-or-form nil]
-
-    :else
-    field-id-or-form))
-
 (def ^:dynamic *ignore-current-user-perms-and-return-all-field-values*
   "Whether to ignore permissions for the current User and return *all* FieldValues for the Fields being parameterized by
   Cards and Dashboards. This determines how `:param_values` gets hydrated for Card and Dashboard. Normally, this is
@@ -123,7 +93,7 @@
         ;; for unknown reasons. See #8917
         (if field-form
           (try
-           (unwrap-field-or-expression-clause field-form)
+           (mbql.u/unwrap-field-or-expression-clause field-form)
            (catch Exception e
              (log/error e "Failed unwrap field form" field-form)))
           (log/error "Could not find matching field clause for target:" target))))))
@@ -298,7 +268,7 @@
   [card]
   (set (for [[_ {dimension :dimension}] (get-in card [:dataset_query :native :template-tags])
              :when                      dimension
-             :let                       [field (unwrap-field-clause dimension)]
+             :let                       [field (mbql.u/unwrap-field-clause dimension)]
              :when                      field]
          field)))
 
diff --git a/src/metabase/models/params/chain_filter.clj b/src/metabase/models/params/chain_filter.clj
index abf2e54b9ac..8777bde0a21 100644
--- a/src/metabase/models/params/chain_filter.clj
+++ b/src/metabase/models/params/chain_filter.clj
@@ -67,6 +67,7 @@
    [clojure.string :as str]
    [honey.sql :as sql]
    [metabase.db :as mdb]
+   [metabase.db.metadata-queries :as metadata-queries]
    [metabase.db.query :as mdb.query]
    [metabase.driver.common.parameters.dates :as params.dates]
    [metabase.legacy-mbql.util :as mbql.u]
@@ -418,15 +419,15 @@
                              ;; TODO -- would this be more efficient if we just did an INNER JOIN against the original
                              ;; Table instead of a LEFT JOIN with this additional filter clause? Would that still
                              ;; work?
-                             :filter    [:not-null original-field-clause]
+                             :filter   [:not-null original-field-clause]
                              ;; for Field->Field remapping we want to return pairs of [original-value remapped-value],
                              ;; but sort by [remapped-value]
                              :order-by [[:asc [:field field-id nil]]]}))
                    (add-joins source-table-id joins)
-                   (add-filters source-table-id joined-table-ids constraints)))
+                   (add-filters source-table-id joined-table-ids constraints)
+                   metadata-queries/add-required-filters-if-needed))
    :middleware {:disable-remaps? true}})
 
-
 ;;; ------------------------ Chain filter (powers GET /api/dashboard/:id/params/:key/values) -------------------------
 
 (mu/defn ^:private unremapped-chain-filter :- ms/FieldValuesResult
diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj
index f31bd78dede..7b47e42c96b 100644
--- a/src/metabase/models/table.clj
+++ b/src/metabase/models/table.clj
@@ -6,8 +6,6 @@
    [metabase.driver :as driver]
    [metabase.models.audit-log :as audit-log]
    [metabase.models.data-permissions :as data-perms]
-   [metabase.models.database :refer [Database]]
-   [metabase.models.field :refer [Field]]
    [metabase.models.humanization :as humanization]
    [metabase.models.interface :as mi]
    [metabase.models.permissions-group :as perms-group]
@@ -61,7 +59,7 @@
 (t2/define-before-insert :model/Table
   [table]
   (let [defaults {:display_name (humanization/name->human-readable-name (:name table))
-                  :field_order  (driver/default-field-order (t2/select-one-fn :engine Database :id (:db_id table)))}]
+                  :field_order  (driver/default-field-order (t2/select-one-fn :engine :model/Database :id (:db_id table)))}]
     (merge defaults table)))
 
 (defn- set-new-table-permissions!
@@ -128,9 +126,9 @@
   [table]
   (doall
    (map-indexed (fn [new-position field]
-                  (t2/update! Field (u/the-id field) {:position new-position}))
+                  (t2/update! :model/Field (u/the-id field) {:position new-position}))
                 ;; Can't use `select-field` as that returns a set while we need an ordered list
-                (t2/select [Field :id]
+                (t2/select [:model/Field :id]
                            :table_id  (u/the-id table)
                            {:order-by (case (:field_order table)
                                         :custom       [[:custom_position :asc]]
@@ -147,7 +145,7 @@
 (defn- valid-field-order?
   "Field ordering is valid if all the fields from a given table are present and only from that table."
   [table field-ordering]
-  (= (t2/select-pks-set Field
+  (= (t2/select-pks-set :model/Field
        :table_id (u/the-id table)
        :active   true)
      (set field-ordering)))
@@ -159,8 +157,8 @@
   (t2/update! Table (u/the-id table) {:field_order :custom})
   (doall
     (map-indexed (fn [position field-id]
-                   (t2/update! Field field-id {:position        position
-                                               :custom_position position}))
+                   (t2/update! :model/Field field-id {:position        position
+                                                      :custom_position position}))
                  field-order)))
 
 
@@ -221,7 +219,7 @@
   [tables]
   (with-objects :fields
     (fn [table-ids]
-      (t2/select Field
+      (t2/select :model/Field
         :active          true
         :table_id        [:in table-ids]
         :visibility_type [:not= "retired"]
@@ -239,14 +237,14 @@
 (defn database
   "Return the `Database` associated with this `Table`."
   [table]
-  (t2/select-one Database :id (:db_id table)))
+  (t2/select-one :model/Database :id (:db_id table)))
 
 ;;; ------------------------------------------------- Serialization -------------------------------------------------
 (defmethod serdes/dependencies "Table" [table]
   [[{:model "Database" :id (:db_id table)}]])
 
 (defmethod serdes/generate-path "Table" [_ table]
-  (let [db-name (t2/select-one-fn :name 'Database :id (:db_id table))]
+  (let [db-name (t2/select-one-fn :name :model/Database :id (:db_id table))]
     (filterv some? [{:model "Database" :id db-name}
                     (when (:schema table)
                       {:model "Schema" :id (:schema table)})
@@ -261,18 +259,18 @@
         schema-name (when (= 3 (count path))
                       (-> path second :id))
         table-name  (-> path last :id)
-        db-id       (t2/select-one-pk Database :name db-name)]
+        db-id       (t2/select-one-pk :model/Database :name db-name)]
     (t2/select-one Table :name table-name :db_id db-id :schema schema-name)))
 
 (defmethod serdes/extract-one "Table"
   [_model-name _opts {:keys [db_id] :as table}]
   (-> (serdes/extract-one-basics "Table" table)
-      (assoc :db_id (t2/select-one-fn :name 'Database :id db_id))))
+      (assoc :db_id (t2/select-one-fn :name :model/Database :id db_id))))
 
 (defmethod serdes/load-xform "Table"
   [{:keys [db_id] :as table}]
   (-> (serdes/load-xform-basics table)
-      (assoc :db_id (t2/select-one-fn :id 'Database :name db_id))))
+      (assoc :db_id (t2/select-one-fn :id :model/Database :name db_id))))
 
 (defmethod serdes/storage-path "Table" [table _ctx]
   (concat (serdes/storage-table-path-prefix (serdes/path table))
diff --git a/src/metabase/query_processor/middleware/check_features.clj b/src/metabase/query_processor/middleware/check_features.clj
index bc211c20f80..bafb59ab4f7 100644
--- a/src/metabase/query_processor/middleware/check_features.clj
+++ b/src/metabase/query_processor/middleware/check_features.clj
@@ -12,10 +12,12 @@
 (defn assert-driver-supports
   "Assert that the driver/database supports keyword `feature`."
   [feature]
-  (when-not (driver/database-supports? driver/*driver* feature (lib.metadata/database (qp.store/metadata-provider)))
-    (throw (ex-info (tru "{0} is not supported by this driver." (name feature))
-                    {:type    qp.error-type/unsupported-feature
-                     :feature feature}))))
+  (let [database (lib.metadata/database (qp.store/metadata-provider))]
+    (when-not (driver/database-supports? driver/*driver* feature database)
+      (throw (ex-info (tru "{0} is not supported by {1} driver." (name feature) (name driver/*driver*))
+                      {:type    qp.error-type/unsupported-feature
+                       :feature feature
+                       :driver  driver/*driver*})))))
 
 ;; TODO - definitely a little incomplete. It would be cool if we cool look at the metadata in the schema namespace and
 ;; auto-generate this logic
diff --git a/src/metabase/query_processor/middleware/parameters/mbql.clj b/src/metabase/query_processor/middleware/parameters/mbql.clj
index 9b8c77901ff..2b032d7aa7c 100644
--- a/src/metabase/query_processor/middleware/parameters/mbql.clj
+++ b/src/metabase/query_processor/middleware/parameters/mbql.clj
@@ -9,7 +9,6 @@
    [metabase.lib.core :as lib]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
    [metabase.lib.util.match :as lib.util.match]
-   [metabase.models.params :as params]
    [metabase.query-processor.store :as qp.store]
    [metabase.util.malli :as mu]))
 
@@ -74,7 +73,7 @@
     ;; single value, date range. Generate appropriate MBQL clause based on date string
     (params.dates/date-type? param-type)
     (params.dates/date-string->filter
-     (parse-param-value-for-type query param-type param-value (params/unwrap-field-or-expression-clause field))
+     (parse-param-value-for-type query param-type param-value (mbql.u/unwrap-field-or-expression-clause field))
      field)
 
     ;; TODO - We can't tell the difference between a dashboard parameter (convert to an MBQL filter) and a native
@@ -86,8 +85,8 @@
     ;; single-value, non-date param. Generate MBQL [= [field <field> nil] <value>] clause
     :else
     [:=
-     (params/wrap-field-id-if-needed field)
-     (parse-param-value-for-type query param-type param-value (params/unwrap-field-or-expression-clause field))]))
+     (mbql.u/wrap-field-id-if-needed field)
+     (parse-param-value-for-type query param-type param-value (mbql.u/unwrap-field-or-expression-clause field))]))
 
 (defn expand
   "Expand parameters for MBQL queries in `query` (replacing Dashboard or Card-supplied params with the appropriate
diff --git a/src/metabase/sync/analyze/fingerprint/insights.clj b/src/metabase/sync/analyze/fingerprint/insights.clj
index 77dca8ba96f..8936c12225f 100644
--- a/src/metabase/sync/analyze/fingerprint/insights.clj
+++ b/src/metabase/sync/analyze/fingerprint/insights.clj
@@ -7,7 +7,6 @@
    [kixi.stats.protocols :as p]
    [medley.core :as m]
    [metabase.legacy-mbql.util :as mbql.u]
-   [metabase.models.field :refer [Field]]
    [metabase.models.interface :as mi]
    [metabase.sync.analyze.fingerprint.fingerprinters :as fingerprinters]
    [metabase.sync.util :as sync-util]
@@ -224,7 +223,7 @@
                      :col            (:name number-col)
                      :unit           unit))))))
       (format "Error generating timeseries insight keyed by: %s"
-              (sync-util/name-for-logging (mi/instance Field datetime))))))
+              (sync-util/name-for-logging (mi/instance :model/Field datetime))))))
 
 (defn insights
   "Based on the shape of returned data construct a transducer to statistically analyize data."
diff --git a/src/metabase/sync/util.clj b/src/metabase/sync/util.clj
index 539f885bcf8..a5c8153bf61 100644
--- a/src/metabase/sync/util.clj
+++ b/src/metabase/sync/util.clj
@@ -9,7 +9,6 @@
    [metabase.driver :as driver]
    [metabase.driver.util :as driver.u]
    [metabase.events :as events]
-   [metabase.models.field :refer [Field]]
    [metabase.models.interface :as mi]
    [metabase.models.task-history :refer [TaskHistory]]
    [metabase.query-processor.interface :as qp.i]
@@ -379,7 +378,7 @@
   [& {:keys [id name]}]
   (format "Field %s ''%s''" (or (str id) "") name))
 
-(defmethod name-for-logging Field [field]
+(defmethod name-for-logging :model/Field [field]
   (field-name-for-logging field))
 
 ;;; this is used for result metadata stuff.
diff --git a/test/metabase/db/metadata_queries_test.clj b/test/metabase/db/metadata_queries_test.clj
index bd34a7f2ae0..5bf40ed3900 100644
--- a/test/metabase/db/metadata_queries_test.clj
+++ b/test/metabase/db/metadata_queries_test.clj
@@ -89,7 +89,7 @@
       (is (=? [:> [:field (:id field2) {:base-type :type/Integer}] (mt/malli=? int?)]
               (get (#'metadata-queries/field-mbql-query table {}) :filter))))))
 
-(deftest text-field?-test
+(deftest ^:parallel text-field?-test
   (testing "recognizes fields suitable for fingerprinting"
     (doseq [field [{:base_type :type/Text}
                    {:base_type :type/Text :semantic_type :type/State}
@@ -99,3 +99,54 @@
                    {:base_type :type/Text :semantic_type :type/SerializedJSON} ; "legacy" json fields in pg
                    {:base_type :type/Text :semantic_type :type/XML}]]
       (is (not (#'metadata-queries/text-field? field))))))
+
+(deftest add-required-filter-if-needed-test
+  (mt/with-temp
+    [:model/Database db               {:engine :h2}
+     :model/Table    product          {:name "PRODUCT" :db_id (:id db)}
+     :model/Field    _product-id      {:name "ID" :table_id (:id product) :base_type :type/Integer}
+     :model/Field    _product-name    {:name "NAME" :table_id (:id product) :base_type :type/Integer}
+     :model/Table    buyer            {:name "BUYER" :database_require_filter true :db_id (:id db)}
+     :model/Field    buyer-id         {:name "ID" :table_id (:id buyer) :base_type :type/Integer :database_partitioned true}
+     :model/Table    order            {:name "ORDER" :database_require_filter true :db_id (:id db)}
+     :model/Field    order-id         {:name "ID" :table_id (:id order) :base_type :type/Integer}
+     :model/Field    _order-buyer-id  {:name "BUYER_ID" :table_id (:id order) :base_type :type/Integer}
+     :model/Field    order-product-id {:name "PRODUCT_ID" :table_id (:id order) :base_type :type/Integer :database_partitioned true}]
+    (mt/with-db db
+      (mt/$ids
+       (testing "no op for tables that do not require filter"
+         (let [query (:query (mt/mbql-query product))]
+           (is (= query
+                  (metadata-queries/add-required-filter-if-needed query)))))
+
+       (testing "if the source table requires a filter, add the partitioned filter"
+         (let [query (:query (mt/mbql-query order))]
+           (is (= (assoc query
+                         :filter [:> [:field (:id order-product-id) {:base-type :type/Integer}] -9223372036854775808])
+                  (metadata-queries/add-required-filters-if-needed query)))))
+
+       (testing "if a joined table require a filter, add the partitioned filter"
+         (let [query (:query (mt/mbql-query product {:joins [{:source-table (:id order)
+                                                              :condition    [:= $order.product_id $product.id]
+                                                              :alias        "Product"}]}))]
+           (is (= (assoc query
+                         :filter [:> [:field (:id order-product-id) {:base-type :type/Integer}] -9223372036854775808])
+                  (metadata-queries/add-required-filters-if-needed query)))))
+
+       (testing "if both source tables and joined table require a filter, add both"
+         (let [query (:query (mt/mbql-query order {:joins [{:source-table (:id buyer)
+                                                            :condition    [:= $order.buyer_id $buyer.id]
+                                                            :alias        "BUYER"}]}))]
+           (is (= (assoc query
+                         :filter [:and
+                                  [:> [:field (:id buyer-id) {:base-type :type/Integer}] -9223372036854775808]
+                                  [:> [:field (:id order-product-id) {:base-type :type/Integer}] -9223372036854775808]])
+                  (metadata-queries/add-required-filters-if-needed query)))))
+
+       (testing "Should add an and clause for existing filter"
+         (let [query (:query (mt/mbql-query order {:filter [:> $order.id 1]}))]
+           (is (= (assoc query
+                         :filter [:and
+                                  [:> [:field (:id order-id) nil] 1]
+                                  [:> [:field (:id order-product-id) {:base-type :type/Integer}] -9223372036854775808]])
+                  (metadata-queries/add-required-filters-if-needed query)))))))))
diff --git a/test/metabase/models/params_test.clj b/test/metabase/models/params_test.clj
index 401acf55356..baa1078db5b 100644
--- a/test/metabase/models/params_test.clj
+++ b/test/metabase/models/params_test.clj
@@ -3,6 +3,7 @@
   (:require
    [clojure.test :refer :all]
    [metabase.api.public-test :as public-test]
+   [metabase.legacy-mbql.util :as mbql.u]
    [metabase.models :refer [Card Field]]
    [metabase.models.params :as params]
    [metabase.test :as mt]
@@ -15,7 +16,7 @@
                         [:field "name" {:base-type :type/Text}] [:field "name" {:base-type :type/Text}]}]
     (testing x
       (is (= expected
-             (params/wrap-field-id-if-needed x))))))
+             (mbql.u/wrap-field-id-if-needed x))))))
 
 
 ;;; ---------------------------------------------- name_field hydration ----------------------------------------------
diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj
index 08a501f6450..662366a19e0 100644
--- a/test/metabase/query_processor_test/aggregation_test.clj
+++ b/test/metabase/query_processor_test/aggregation_test.clj
@@ -73,7 +73,7 @@
     (testing "Make sure standard deviations fail for drivers that don't support it"
       (is (thrown-with-msg?
            clojure.lang.ExceptionInfo
-           #"standard-deviation-aggregations is not supported by this driver"
+           #"standard-deviation-aggregations is not supported by sqlite driver"
            (mt/run-mbql-query venues
              {:aggregation [[:stddev $latitude]]}))))))
 
-- 
GitLab