diff --git a/frontend/test/metabase/scenarios/joins/joins.cy.spec.js b/frontend/test/metabase/scenarios/joins/joins.cy.spec.js index 78efb86e6714eba070398d4af716e054d096ba94..4c14144622533c8f23d1f99a9194448af480c799 100644 --- a/frontend/test/metabase/scenarios/joins/joins.cy.spec.js +++ b/frontend/test/metabase/scenarios/joins/joins.cy.spec.js @@ -14,7 +14,7 @@ describe("scenarios > question > joined questions", () => { cy.signInAsAdmin(); }); - it.skip("joining on a question with remapped values should work (metabase#15578)", () => { + it("joining on a question with remapped values should work (metabase#15578)", () => { cy.intercept("POST", "/api/dataset").as("dataset"); // Remap display value cy.request("POST", `/api/field/${ORDERS.PRODUCT_ID}/dimension`, { diff --git a/src/metabase/models/params/chain_filter.clj b/src/metabase/models/params/chain_filter.clj index 344c2e0a4d704057747cd3d9dbf593634629ef6c..8be8735a42ce89694b302fe654ba1fa812091bc4 100644 --- a/src/metabase/models/params/chain_filter.clj +++ b/src/metabase/models/params/chain_filter.clj @@ -391,7 +391,8 @@ ;; 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))) + :middleware {:disable-remaps? true}}) ;;; ------------------------ Chain filter (powers GET /api/dashboard/:id/params/:key/values) ------------------------- diff --git a/src/metabase/query_processor/middleware/add_dimension_projections.clj b/src/metabase/query_processor/middleware/add_dimension_projections.clj index 84ef1ad9e31d47612fef5d19f1e945636ad8b59d..b98722037218e0f34541cd3d79fd367ee8fe9b25 100644 --- a/src/metabase/query_processor/middleware/add_dimension_projections.clj +++ b/src/metabase/query_processor/middleware/add_dimension_projections.clj @@ -25,6 +25,8 @@ `name` is `:remapped_from` `:category_id`." (:require [clojure.data :as data] [clojure.tools.logging :as log] + [clojure.walk :as walk] + [medley.core :as m] [metabase.mbql.schema :as mbql.s] [metabase.mbql.util :as mbql.u] [metabase.models.dimension :refer [Dimension]] @@ -166,31 +168,18 @@ {:remaps (s/maybe (su/distinct [ExternalRemappingDimension])) :query mbql.s/Query}) -(declare add-fk-remaps) - -(s/defn ^:private source-query-remaps :- QueryAndRemaps - [{{:keys [source-query]} :query, :as query}] - (if (and source-query (not (:native source-query))) - ;; Only do lifting if source is MBQL query - (let [{source-query-remaps :remaps, source-query :query} (add-fk-remaps (assoc query :query source-query))] - {:remaps source-query-remaps - :query (assoc-in query [:query :source-query] (:query source-query))}) - {:remaps nil, :query query})) - -(s/defn ^:private add-fk-remaps :- QueryAndRemaps - "Add any Fields needed for `:external` remappings to the `:fields` clause of the query, and update `:order-by` and - `breakout` clauses as needed. Returns a map with `:query` (the updated query) and `:remaps` (a sequence - of [[ExternalRemappingDimension]] information maps)." - [{{:keys [fields order-by breakout]} :query, :as query} :- mbql.s/Query] - (let [{source-query-remaps :remaps, query :query} (source-query-remaps query)] +(defn- add-fk-remaps-one-level + [{:keys [fields order-by breakout], {source-query-remaps ::remaps} :source-query, :as query}] + (let [query (m/dissoc-in query [:source-query ::remaps])] ;; fetch remapping column pairs if any exist... (if-let [infos (not-empty (remap-column-infos (concat fields breakout)))] ;; if they do, update `:fields`, `:order-by` and `:breakout` clauses accordingly and add to the query (let [ ;; make a map of field-id-clause -> fk-clause from the tuples original->remapped (into {} (map (juxt :original-field-clause :new-field-clause)) infos) existing-fields (add-fk-remaps-rewrite-existing-fields infos fields) - ;; don't add any new entries for fields that already exist. Use [[mbql.u/remove-namespaced-options]] here so we don't - ;; add new entries even if the existing Field has some extra info e.g. extra unknown namespaced keys. + ;; don't add any new entries for fields that already exist. Use [[mbql.u/remove-namespaced-options]] here so + ;; we don't add new entries even if the existing Field has some extra info e.g. extra unknown namespaced + ;; keys. existing-normalized-fields-set (into #{} (map mbql.u/remove-namespaced-options) existing-fields) new-fields (into existing-fields @@ -198,15 +187,32 @@ (remove (comp existing-normalized-fields-set mbql.u/remove-namespaced-options))) infos) new-breakout (add-fk-remaps-rewrite-breakout original->remapped breakout) - new-order-by (add-fk-remaps-rewrite-order-by original->remapped order-by)] + new-order-by (add-fk-remaps-rewrite-order-by original->remapped order-by) + remaps (into [] (comp cat (distinct)) [source-query-remaps (map :dimension infos)])] ;; return the Dimensions we are using and the query - {:remaps (into [] (comp cat (distinct)) [source-query-remaps (map :dimension infos)]) - :query (cond-> query - (seq fields) (assoc-in [:query :fields] new-fields) - (seq order-by) (assoc-in [:query :order-by] new-order-by) - (seq breakout) (assoc-in [:query :breakout] new-breakout))}) + (cond-> query + (seq fields) (assoc :fields new-fields) + (seq order-by) (assoc :order-by new-order-by) + (seq breakout) (assoc :breakout new-breakout) + (seq remaps) (assoc ::remaps remaps))) ;; otherwise return query as-is - {:remaps source-query-remaps, :query query}))) + (cond-> query + (seq source-query-remaps) (assoc ::remaps source-query-remaps))))) + +(s/defn ^:private add-fk-remaps :- QueryAndRemaps + "Add any Fields needed for `:external` remappings to the `:fields` clause of the query, and update `:order-by` and + `breakout` clauses as needed. Returns a map with `:query` (the updated query) and `:remaps` (a sequence + of [[ExternalRemappingDimension]] information maps)." + [query] + (let [query (walk/postwalk + (fn [form] + (if (and (map? form) + ((some-fn :source-table :source-query) form) + (not (:condition form))) + (add-fk-remaps-one-level form) + form)) + query)] + {:query (m/dissoc-in query [:query ::remaps]), :remaps (get-in query [:query ::remaps])})) (defn add-remapped-columns "Pre-processing middleware. For columns that have remappings to other columns (FK remaps), rewrite the query to diff --git a/test/metabase/query_processor/middleware/add_dimension_projections_test.clj b/test/metabase/query_processor/middleware/add_dimension_projections_test.clj index ea7baf2c005ac987c3d08ced6f14dd61b757bc7d..d3a8ea18b7faf158a72970c0e0f004915825582f 100644 --- a/test/metabase/query_processor/middleware/add_dimension_projections_test.clj +++ b/test/metabase/query_processor/middleware/add_dimension_projections_test.clj @@ -444,3 +444,36 @@ {:display_name "Sender ID [external remap]", :name "NAME", :remapped_from "SENDER_ID"} {:display_name "Receiver ID [external remap]", :name "NAME_2", :remapped_from "RECEIVER_ID"}]} (#'add-dim-projections/add-remapped-to-and-from-metadata metadata remap-info nil))))))))))) + +(deftest add-remappings-inside-joins-test + (testing "Remappings should work inside joins (#15578)" + (mt/dataset sample-dataset + (mt/with-column-remappings [orders.product_id products.title] + (is (partial (mt/mbql-query products + {:joins [{:source-query {:source-table $$orders} + :alias "Q1" + :fields [&Q1.orders.id + &Q1.orders.product_id + &PRODUCTS__via__PRODUCT_ID.orders.product_id->title] + :condition [:= $id &Q1.orders.product_id] + :strategy :left-join} + {:source-table $$products + :alias "PRODUCTS__via__PRODUCT_ID" + :condition [:= $orders.product_id &PRODUCTS__via__PRODUCT_ID.products.id] + :strategy :left-join + :fk-field-id %orders.product_id}] + :fields [&Q1.orders.id + &Q1.orders.product_id + &PRODUCTS__via__PRODUCT_ID.orders.product_id->products.title] + :limit 2}) + (:query + (#'add-dim-projections/add-fk-remaps + (mt/mbql-query products + {:joins [{:strategy :left-join + :source-query {:source-table $$orders} + :alias "Q1" + :condition [:= $id &Q1.orders.product_id] + :fields [&Q1.orders.id + &Q1.orders.product_id]}] + :fields [&Q1.orders.id &Q1.orders.product_id] + :limit 2}))))))))) diff --git a/test/metabase/query_processor_test/remapping_test.clj b/test/metabase/query_processor_test/remapping_test.clj index 96a3be26bf82d806cbfd324f11ccfe5ed1479dd0..4db050ba34b3c0993fe14e77d9ef47daad6cfc3a 100644 --- a/test/metabase/query_processor_test/remapping_test.clj +++ b/test/metabase/query_processor_test/remapping_test.clj @@ -1,6 +1,8 @@ (ns metabase.query-processor-test.remapping-test "Tests for the remapping results" (:require [clojure.test :refer :all] + [metabase.driver :as driver] + [metabase.models.card :refer [Card]] [metabase.models.dimension :refer [Dimension]] [metabase.models.field :refer [Field]] [metabase.query-processor :as qp] @@ -257,3 +259,64 @@ [2 8 3 "Bip bip bip bip" "Annie Albatross" "Peter Pelican"] [3 3 2 "Coo" "Peter Pelican" "Lucky Pigeon"]] (mt/rows results)))))))))) + +(deftest remapped-columns-in-joined-source-queries-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-queries :left-join) + (testing "Remapped columns in joined source queries should work (#15578)" + (mt/dataset sample-dataset + (mt/with-bigquery-fks #{:bigquery :bigquery-cloud-sdk} + (mt/with-column-remappings [orders.product_id products.title] + (let [query (mt/mbql-query products + {:joins [{:source-query {:source-table $$orders + :breakout [$orders.product_id] + :aggregation [[:sum $orders.quantity]]} + :alias "Orders" + :condition [:= $id &Orders.orders.product_id] + :fields [&Orders.title + &Orders.*sum/Integer]}] + :fields [$title $category] + :order-by [[:asc $id]] + :limit 3})] + (mt/with-native-query-testing-context query + (let [results (qp/process-query query)] + (when (= driver/*driver* :h2) + (testing "Metadata" + (is (= [["TITLE" "Title"] + ["CATEGORY" "Category"] + ["TITLE_2" "Orders → Title"] + ["sum" "Orders → Sum"]] + (map (juxt :name :display_name) (mt/cols results)))))) + (is (= [["Rustic Paper Wallet" "Gizmo" "Rustic Paper Wallet" 347] + ["Small Marble Shoes" "Doohickey" "Small Marble Shoes" 352] + ["Synergistic Granite Chair" "Doohickey" "Synergistic Granite Chair" 286]] + (mt/formatted-rows [str str str int] + results)))))))))))) + +(deftest inception-style-nested-query-with-joins-test + (testing "source query > source query > query with join (with remappings) should work (#14724)" + ;; this error only seems to be triggered when actually using Cards as sources (and include the source metadata) + (mt/dataset sample-dataset + (mt/with-column-remappings [orders.product_id products.title] + ;; this is only triggered when using the results metadata from the Card itself -- see #19895 + (let [q1 (mt/mbql-query orders + {:fields [$id $product_id] + :joins [{:source-table $$products + :alias "Products" + :condition [:= $product_id &Products.products.id] + :fields [$id + &Products.products.title]}] + :order-by [[:asc $id]] + :limit 3})] + (mt/with-temp Card [{card-1-id :id} {:dataset_query q1 + :result_metadata (get-in (qp/process-query q1) + [:data :results_metadata :columns])}] + (let [q2 (mt/mbql-query nil {:source-table (format "card__%d" card-1-id)})] + (mt/with-temp Card [{card-2-id :id} {:dataset_query q2 + :result_metadata (get-in (qp/process-query q2) + [:data :results_metadata :columns])}] + (let [q3 (mt/mbql-query nil {:source-table (format "card__%d" card-2-id)})] + (mt/with-native-query-testing-context q3 + (is (= [[1 14 "Awesome Concrete Shoes" "Awesome Concrete Shoes"] + [2 123 "Mediocre Wooden Bench" "Mediocre Wooden Bench"] + [3 105 "Fantastic Wool Shirt" "Fantastic Wool Shirt"]] + (mt/rows (qp/process-query q3))))))))))))))