diff --git a/frontend/src/metabase-lib/join.ts b/frontend/src/metabase-lib/join.ts index 0b2f179502bd69e85bfef067b052b1db54653997..8f1feb8efe3cf325148edc11c5dc687d789552c5 100644 --- a/frontend/src/metabase-lib/join.ts +++ b/frontend/src/metabase-lib/join.ts @@ -196,12 +196,12 @@ export function joinConditionOperators( return ML.join_condition_operators(query, stageIndex, lhsColumn, rhsColumn); } -export function suggestedJoinCondition( +export function suggestedJoinConditions( query: Query, stageIndex: number, joinable: Joinable, -): JoinCondition | null { - return ML.suggested_join_condition(query, stageIndex, joinable); +): JoinCondition[] { + return ML.suggested_join_conditions(query, stageIndex, joinable); } export type JoinFields = ColumnMetadata[] | "all" | "none"; diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep/use-join.ts b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep/use-join.ts index e579f8e345545dc3ffc0edd68abd5748259e66e1..35e80c6d2399d481abb3d03a19702f2049460dde 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep/use-join.ts +++ b/frontend/src/metabase/query_builder/components/notebook/steps/JoinStep/use-join.ts @@ -94,17 +94,16 @@ export function useJoin(query: Lib.Query, stageIndex: number, join?: Lib.Join) { const setTable = useCallback( (nextTable: Lib.Joinable) => { - const suggestedCondition = Lib.suggestedJoinCondition( + const suggestedConditions = Lib.suggestedJoinConditions( query, stageIndex, nextTable, ); - if (suggestedCondition) { - const nextConditions = [suggestedCondition]; - _setConditions(nextConditions); + if (suggestedConditions.length > 0) { + _setConditions(suggestedConditions); - let nextJoin = Lib.joinClause(nextTable, nextConditions); + let nextJoin = Lib.joinClause(nextTable, suggestedConditions); nextJoin = Lib.withJoinFields(nextJoin, "all"); nextJoin = Lib.withJoinStrategy(nextJoin, strategy); diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index 0d552e5d21b268ec7dfdc7615627be7af6c3d19f..c7b14fd4af1b143093618269a1ec9cf223e080d7 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -209,7 +209,7 @@ joinable-columns joins raw-join-strategy - suggested-join-condition + suggested-join-conditions with-join-alias with-join-fields with-join-strategy diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc index a78193b86304d09a81211716cdbfb243d68b103e..ec3f4fa2857c2195e2b7ccdbf5627f82ed9bda9a 100644 --- a/src/metabase/lib/join.cljc +++ b/src/metabase/lib/join.cljc @@ -22,6 +22,7 @@ [metabase.lib.schema.expression :as lib.schema.expression] [metabase.lib.schema.filter :as lib.schema.filter] [metabase.lib.schema.join :as lib.schema.join] + [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.schema.temporal-bucketing :as lib.schema.temporal-bucketing] [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.types.isa :as lib.types.isa] @@ -200,7 +201,7 @@ display name." [query :- ::lib.schema/query stage-number :- :int - column-metadata :- lib.metadata/ColumnMetadata + column-metadata :- ::lib.schema.metadata/column join-alias :- ::lib.schema.common/non-blank-string] (let [column-metadata (assoc column-metadata :source-alias join-alias) col (-> (assoc column-metadata @@ -499,7 +500,7 @@ (declare join-conditions joined-thing - suggested-join-condition) + suggested-join-conditions) (mu/defn join :- ::lib.schema/query "Add a join clause to a `query`." @@ -510,10 +511,10 @@ stage-number :- :int a-join :- [:or lib.join.util/PartialJoin Joinable]] (let [a-join (join-clause a-join) - suggested-condition (when (empty? (join-conditions a-join)) - (suggested-join-condition query stage-number (joined-thing query a-join))) + suggested-conditions (when (empty? (join-conditions a-join)) + (suggested-join-conditions query stage-number (joined-thing query a-join))) a-join (cond-> a-join - suggested-condition (with-join-conditions [suggested-condition])) + (seq suggested-conditions) (with-join-conditions suggested-conditions)) a-join (add-default-alias query stage-number a-join)] (lib.util/update-query-stage query stage-number update :joins (fn [existing-joins] (conj (vec existing-joins) a-join)))))) @@ -619,11 +620,11 @@ ;;; options for each respective part. At the time of this writing, selecting one does not filter out incompatible ;;; options for the other parts, but hopefully we can implement this in the future -- see #31174 -(mu/defn ^:private sort-join-condition-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn ^:private sort-join-condition-columns :- [:sequential ::lib.schema.metadata/column] "Sort potential join condition columns as returned by [[join-condition-lhs-columns]] or [[join-condition-rhs-columns]]. PK columns are returned first, followed by FK columns, followed by other columns. Otherwise original order is maintained." - [columns :- [:sequential lib.metadata/ColumnMetadata]] + [columns :- [:sequential ::lib.schema.metadata/column]] (let [{:keys [pk fk other]} (group-by (fn [column] (cond (lib.types.isa/primary-key? column) :pk @@ -643,7 +644,7 @@ column)) (lib.equality/mark-selected-columns query stage-number columns [existing-column-or-nil])))) -(mu/defn join-condition-lhs-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn join-condition-lhs-columns :- [:sequential ::lib.schema.metadata/column] "Get a sequence of columns that can be used as the left-hand-side (source column) in a join condition. This column is the one that comes from the source Table/Card/previous stage of the query or a previous join. @@ -698,7 +699,7 @@ (mark-selected-column query stage-number lhs-column-or-nil) sort-join-condition-columns)))) -(mu/defn join-condition-rhs-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn join-condition-rhs-columns :- [:sequential ::lib.schema.metadata/column] "Get a sequence of columns that can be used as the right-hand-side (target column) in a join condition. This column is the one that belongs to the thing being joined, `join-or-joinable`, which can be something like a Table ([[metabase.lib.metadata/TableMetadata]]), Saved Question/Model ([[metabase.lib.metadata/CardMetadata]]), @@ -751,67 +752,80 @@ ([_query :- ::lib.schema/query _stage-number :- :int ;; not yet used, hopefully we will use in the future when present for filtering incompatible options out. - _lhs-column-or-nil :- [:maybe lib.metadata/ColumnMetadata] - _rhs-column-or-nil :- [:maybe lib.metadata/ColumnMetadata]] + _lhs-column-or-nil :- [:maybe ::lib.schema.metadata/column] + _rhs-column-or-nil :- [:maybe ::lib.schema.metadata/column]] ;; currently hardcoded to these six operators regardless of LHS and RHS. lib.filter.operator/join-operators)) -(mu/defn ^:private pk-column :- [:maybe lib.metadata/ColumnMetadata] - "Given something `x` (e.g. a Table metadata) find the PK column." +(mu/defn ^:private fk-columns-to :- [:maybe [:sequential + {:min 1} + [:and + ::lib.schema.metadata/column + [:map + [::target ::lib.schema.metadata/column]]]]] + "Find FK columns in `source` pointing at a column in `target`. Includes the target column under the `::target` key." [query :- ::lib.schema/query stage-number :- :int - x - options] - (m/find-first lib.types.isa/primary-key? - (lib.metadata.calculation/visible-columns query stage-number x options))) - -(mu/defn ^:private fk-column-for-pk-in :- [:maybe lib.metadata/ColumnMetadata] - [pk-col :- [:maybe lib.metadata/ColumnMetadata] - visible-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]]] - (when-let [pk-id (:id pk-col)] - (let [candidates (filter (fn [{:keys [fk-target-field-id], :as col}] - (and (lib.types.isa/foreign-key? col) - (= fk-target-field-id pk-id))) - visible-columns) - primary (first candidates)] - (if (some #(= (:id %) (:id primary)) (rest candidates)) - (when (not-any? #(= (:lib/desired-column-alias %) (:lib/desired-column-alias primary)) (rest candidates)) - ;; there are multiple candidates but :lib/desired-column-alias disambiguates them, - ;; so reference by name - (dissoc primary :id)) - primary)))) - -(mu/defn ^:private fk-column-for :- [:maybe lib.metadata/ColumnMetadata] - "Given a query stage find an FK column that points to the PK `pk-col`." - [query :- ::lib.schema/query - stage-number :- :int - pk-col :- [:maybe lib.metadata/ColumnMetadata]] - (when pk-col - (let [visible-columns (lib.metadata.calculation/visible-columns query stage-number (lib.util/query-stage query stage-number))] - (fk-column-for-pk-in pk-col visible-columns)))) - -(mu/defn suggested-join-condition :- [:maybe ::lib.schema.expression/boolean] ; i.e., a filter clause - "Return a suggested default join condition when constructing a join against `joinable`, e.g. a Table, Saved - Question, or another query. A suggested condition will be returned if the query stage has a foreign key to the + source + target] + (let [target-columns (delay + (lib.metadata.calculation/visible-columns + query stage-number target + {:include-implicitly-joinable? false + :include-implicitly-joinable-for-source-card? false}))] + (not-empty + (into [] + (keep (fn [{:keys [fk-target-field-id], :as col}] + (when (and (lib.types.isa/foreign-key? col) + fk-target-field-id) + (when-let [target-column (m/find-first (fn [target-column] + (= fk-target-field-id + (:id target-column))) + @target-columns)] + (assoc col ::target target-column))))) + (lib.metadata.calculation/visible-columns query stage-number source))))) + +(mu/defn suggested-join-conditions :- [:maybe [:sequential {:min 1} ::lib.schema.expression/boolean]] ; i.e., a filter clause + "Return suggested default join conditions when constructing a join against `joinable`, e.g. a Table, Saved + Question, or another query. Suggested conditions will be returned if the source Table has a foreign key to the primary key of the thing we're joining (see #31175 for more info); otherwise this will return `nil` if no default - condition is suggested." + conditions are suggested." ([query joinable] - (suggested-join-condition query -1 joinable)) + (suggested-join-conditions query -1 joinable)) ([query :- ::lib.schema/query stage-number :- :int joinable] - (let [joinable-col-options {:include-implicitly-joinable? false - :include-implicitly-joinable-for-source-card? false}] - (letfn [(filter-clause [x y] - (lib.filter/filter-clause (lib.filter.operator/operator-def :=) x y))] - (or (when-let [pk-col (pk-column query stage-number joinable joinable-col-options)] - (when-let [fk-col (fk-column-for query stage-number pk-col)] - (filter-clause fk-col pk-col))) - (when-let [pk-col (pk-column query stage-number (lib.util/query-stage query stage-number) nil)] - (when-let [fk-col (fk-column-for-pk-in pk-col (lib.metadata.calculation/visible-columns - query stage-number joinable joinable-col-options))] - (filter-clause pk-col fk-col)))))))) + (let [stage (lib.util/query-stage query stage-number)] + (letfn [ ;; only keep one FK to each target column e.g. for + ;; + ;; messages (sender_id REFERENCES user(id), recipient_id REFERENCES user(id)) + ;; + ;; we only want join on one or the other, not both, because that makes no sense. However with a composite + ;; FK -> composite PK suggest multiple conditions. See #34184 + (fks [source target] + (->> (fk-columns-to query stage-number source target) + (m/distinct-by #(-> % ::target :id)) + not-empty)) + (filter-clause [x y] + ;; DO NOT force broken refs for fields that come from Cards (broken refs in this case means use Field + ;; ID refs instead of nominal field literal refs), that will break things if a Card returns the same + ;; Field more than once (there would be no way to disambiguate). See #34227 for more info + (let [x (dissoc x ::lib.card/force-broken-id-refs) + y (dissoc y ::lib.card/force-broken-id-refs)] + (lib.filter/filter-clause (lib.filter.operator/operator-def :=) x y)))] + (or + ;; find cases where we have FK(s) pointing to joinable. Our column goes on the LHS. + (when-let [fks (fks stage joinable)] + (mapv (fn [fk] + (filter-clause fk (::target fk))) + fks)) + ;; find cases where the `joinable` has FK(s) pointing to us. Note our column is the target this time around -- + ;; keep in on the LHS. + (when-let [fks (fks joinable stage)] + (mapv (fn [fk] + (filter-clause (::target fk) fk)) + fks))))))) (defn- add-join-alias-to-joinable-columns [cols a-join] (let [join-alias (lib.join.util/current-join-alias a-join) @@ -833,7 +847,7 @@ cols) (lib.equality/mark-selected-columns cols j-fields)))) -(mu/defn joinable-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn joinable-columns :- [:sequential ::lib.schema.metadata/column] "Return information about the fields that you can pass to [[with-join-fields]] when constructing a join against something [[Joinable]] (i.e., a Table or Card) or manipulating an existing join. When passing in a join, currently selected columns (those in the join's `:fields`) will include `:selected true` information." @@ -931,7 +945,7 @@ ([query :- ::lib.schema/query stage-number :- :int join-or-joinable :- [:maybe JoinOrJoinable] - condition-lhs-column-or-nil :- [:maybe [:or lib.metadata/ColumnMetadata :mbql.clause/field]]] + condition-lhs-column-or-nil :- [:maybe [:or ::lib.schema.metadata/column :mbql.clause/field]]] (or (join-lhs-display-name-from-condition-lhs query stage-number join-or-joinable condition-lhs-column-or-nil) (join-lhs-display-name-for-first-join-in-first-stage query stage-number join-or-joinable) diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs index bdae5be61b7652f1afffdec227efe7e4f40b7653..d70db5d2597b9f995099372ad991f7994d976403 100644 --- a/src/metabase/lib/js.cljs +++ b/src/metabase/lib/js.cljs @@ -698,13 +698,13 @@ ([a-query stage-number expression-position] (to-array (lib.core/expressionable-columns a-query stage-number expression-position)))) -(defn ^:export suggested-join-condition - "Return a suggested default join condition when constructing a join against `joinable`, e.g. a Table, Saved - Question, or another query. A suggested condition will be returned if the source Table has a foreign key to the +(defn ^:export suggested-join-conditions + "Return suggested default join conditions when constructing a join against `joinable`, e.g. a Table, Saved + Question, or another query. Suggested conditions will be returned if the source Table has a foreign key to the primary key of the thing we're joining (see #31175 for more info); otherwise this will return `nil` if no default - condition is suggested." + conditions are suggested." [a-query stage-number joinable] - (lib.core/suggested-join-condition a-query stage-number joinable)) + (to-array (lib.core/suggested-join-conditions a-query stage-number joinable))) (defn ^:export join-fields "Get the `:fields` associated with a join." diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc index 2e4385eda212f4a7b162022e500fbce5c51ac099..53383223a399ffa79e5ce5ed2b303d1d74a11e86 100644 --- a/test/metabase/lib/field_test.cljc +++ b/test/metabase/lib/field_test.cljc @@ -746,8 +746,7 @@ (let [query (as-> (meta/table-metadata :orders) <> (lib/query meta/metadata-provider <>) (lib/join <> -1 (->> (meta/table-metadata :people) - (lib/suggested-join-condition <> -1) - vector + (lib/suggested-join-conditions <> -1) (lib/join-clause (meta/table-metadata :people))))) fields [[:field {} (meta/id :orders :id)] [:field {} (meta/id :orders :subtotal)] @@ -835,8 +834,7 @@ (let [query (as-> (meta/table-metadata :orders) <> (lib/query meta/metadata-provider <>) (lib/join <> -1 (->> (meta/table-metadata :people) - (lib/suggested-join-condition <> -1) - vector + (lib/suggested-join-conditions <> -1) (lib/join-clause (meta/table-metadata :people))))) all-columns (lib/returned-columns query) table-columns (lib/fieldable-columns query -1) @@ -992,8 +990,7 @@ (let [query (as-> (meta/table-metadata :orders) <> (lib/query meta/metadata-provider <>) (lib/join <> -1 (->> (meta/table-metadata :people) - (lib/suggested-join-condition <> -1) - vector + (lib/suggested-join-conditions <> -1) (lib/join-clause (meta/table-metadata :people))))) all-columns (lib/returned-columns query) table-columns (lib/fieldable-columns query -1) diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc index f804b190a31eea6b165ca2cf8064e5ca893aac4c..462e8578a9bce3a2f6a1e0fa3e3a41cfe87d254c 100644 --- a/test/metabase/lib/join_test.cljc +++ b/test/metabase/lib/join_test.cljc @@ -837,13 +837,13 @@ (meta/field-metadata :checkins :user-id))]))) :stages first :joins))))) -(deftest ^:parallel suggested-join-condition-test +(deftest ^:parallel suggested-join-conditions-test (testing "DO suggest a join condition for an FK -> PK relationship" - (are [query] (=? [:= - {} - [:field {} (meta/id :venues :category-id)] - [:field {} (meta/id :categories :id)]] - (lib/suggested-join-condition + (are [query] (=? [[:= + {} + [:field {} (meta/id :venues :category-id)] + [:field {} (meta/id :categories :id)]]] + (lib/suggested-join-conditions query (meta/table-metadata :categories))) ;; plain query @@ -853,26 +853,26 @@ (-> lib.tu/venues-query (lib/aggregate (lib/count)))))) -(deftest ^:parallel suggested-join-condition-pk->fk-test +(deftest ^:parallel suggested-join-conditions-pk->fk-test ;; this is to preserve the existing behavior from MLv1, it doesn't necessarily make sense, but we don't want to have ;; to update a million tests, right? Once v1-compatible joins lands then maybe we can go in and make this work, ;; since it seems like it SHOULD work. (testing "DO suggest join conditions for a PK -> FK relationship" - (is (=? [:= - {} - [:field {} (meta/id :categories :id)] - [:field {} (meta/id :venues :category-id)]] - (lib/suggested-join-condition + (is (=? [[:= + {} + [:field {} (meta/id :categories :id)] + [:field {} (meta/id :venues :category-id)]]] + (lib/suggested-join-conditions (lib/query meta/metadata-provider (meta/table-metadata :categories)) (meta/table-metadata :venues)))))) -(deftest ^:parallel suggested-join-condition-fk-from-join-test +(deftest ^:parallel suggested-join-conditions-fk-from-join-test (testing "DO suggest join conditions for a FK -> PK relationship if the FK comes from a join" - (is (=? [:= - {} - [:field {:join-alias "Venues"} (meta/id :venues :category-id)] - [:field {} (meta/id :categories :id)]] - (lib/suggested-join-condition + (is (=? [[:= + {} + [:field {:join-alias "Venues"} (meta/id :venues :category-id)] + [:field {} (meta/id :categories :id)]]] + (lib/suggested-join-conditions (-> (lib/query meta/metadata-provider (meta/table-metadata :checkins)) (lib/join (-> (lib/join-clause (meta/table-metadata :venues) @@ -882,7 +882,7 @@ (lib/with-join-alias "Venues")))) (meta/table-metadata :categories)))))) -(deftest ^:parallel suggested-join-condition-fk-from-implicitly-joinable-test +(deftest ^:parallel suggested-join-conditions-fk-from-implicitly-joinable-test (testing "DON'T suggest join conditions for a FK -> implicitly joinable PK relationship (#34526)" (let [orders (meta/table-metadata :orders) card-query (as-> (lib/query meta/metadata-provider orders) q @@ -898,7 +898,153 @@ :fields (lib/returned-columns card-query)}]}) card (lib.metadata/card metadata-provider 1) query (lib/query metadata-provider orders)] - (is (nil? (lib/suggested-join-condition query card)))))) + (is (nil? (lib/suggested-join-conditions query card)))))) + +(deftest ^:parallel suggested-join-condition-for-fks-pointing-to-non-pk-columns + (testing (str "We should be able to suggest a join condition if any column in the current table is an FK pointing " + "to the target, regardless of whether that column is marked as a PK or not") + (let [id-user 1 + id-order 2 + id-user-id 10 + id-order-user-id 20 + metadata-provider (lib.tu/mock-metadata-provider + {:database meta/database + :tables [{:id id-user + :name "user"} + {:id id-order + :name "order"}] + :fields [{:id id-user-id + :table-id id-user + :name "id" + :base-type :type/Integer} + {:id id-order-user-id + :table-id id-order + :name "user_id" + :base-type :type/Integer + :semantic-type :type/FK + :fk-target-field-id id-user-id}]}) + order (lib.metadata/table metadata-provider id-order) + user (lib.metadata/table metadata-provider id-user)] + (testing "ORDER joining USER (we have an FK to the joined thing)" + (let [query (lib/query metadata-provider order)] + (is (=? [[:= {} + [:field {} id-order-user-id] + [:field {} id-user-id]]] + (lib/suggested-join-conditions query user))))) + (testing "USER joining ORDER (joined thing has an FK to us)" + (let [query (lib/query metadata-provider user)] + (is (=? [[:= {} + [:field {} id-user-id] + [:field {} id-order-user-id]]] + (lib/suggested-join-conditions query order)))))))) + +(deftest ^:parallel suggested-join-conditions-multiple-fks-to-same-column-test + (testing "when there are multiple FKs to a table, but they point to the same column, only suggest one or the other" + (let [id-user 1 + id-message 2 + id-user-id 10 + id-message-sender-id 20 + id-message-recipient-id 21 + metadata-provider (lib.tu/mock-metadata-provider + {:database meta/database + :tables [{:id id-user + :name "user"} + {:id id-message + :name "message"}] + :fields [{:id id-user-id + :table-id id-user + :name "id" + :base-type :type/Integer + :semantic-type :type/PK} + {:id id-message-sender-id + :table-id id-message + :name "sender_id" + :base-type :type/Integer + :semantic-type :type/FK + :fk-target-field-id id-user-id} + {:id id-message-recipient-id + :table-id id-message + :name "recipient_id" + :base-type :type/Integer + :semantic-type :type/FK + :fk-target-field-id id-user-id}]}) + message (lib.metadata/table metadata-provider id-message) + user (lib.metadata/table metadata-provider id-user)] + (testing "MESSAGE joining USER (we have 2 FKs to the joined thing)" + (let [query (lib/query metadata-provider message)] + (is (=? [[:= {} + ;; doesn't particularly matter which one gets picked, and order is not necessarily determinate + [:field {} #(contains? #{id-message-sender-id id-message-recipient-id} %)] + [:field {} id-user-id]]] + (lib/suggested-join-conditions query user))))) + (testing "USER joining MESSAGE (joined thing has 2 FKs to us)" + (let [query (lib/query metadata-provider user)] + (is (=? [[:= {} + [:field {} id-user-id] + [:field {} #(contains? #{id-message-sender-id id-message-recipient-id} %)]]] + (lib/suggested-join-conditions query message)))))))) + +(deftest ^:parallel suggested-join-conditions-multiple-fks-to-different-columns-test + (testing "when there are multiple FKs to a table, and they point to different columns, suggest multiple join conditions (#34184)" + ;; let's pretend we live in crazy land and the PK for USER is ID + EMAIL (a composite PK) and ORDER has USER_ID + ;; and USER_EMAIL + (let [id-user 1 + id-order 2 + id-user-id 10 + id-user-email 11 + id-order-user-id 20 + id-order-user-email 21 + metadata-provider (lib.tu/mock-metadata-provider + {:database meta/database + :tables [{:id id-user + :name "user"} + {:id id-order + :name "order"}] + :fields [{:id id-user-id + :table-id id-user + :name "id" + :base-type :type/Integer} + {:id id-user-email + :table-id id-user + :name "email" + :base-type :type/Text} + {:id id-order-user-id + :table-id id-order + :name "user_id" + :base-type :type/Integer + :semantic-type :type/FK + :fk-target-field-id id-user-id} + {:id id-order-user-email + :table-id id-order + :name "user_email" + :base-type :type/Text + :semantic-type :type/FK + :fk-target-field-id id-user-email}]}) + order (lib.metadata/table metadata-provider id-order) + user (lib.metadata/table metadata-provider id-user) + ;; the order the conditions get returned in is indeterminate, so for convenience let's just sort them by + ;; Field IDs so we get consistent results in the test assertions. + sort-conditions #(sort-by (fn [[_= _opts [_field _opts lhs-id, :as _lhs] [_field _opts rhs-id, :as _rhs]]] + [lhs-id rhs-id]) + %)] + (testing "ORDER joining USER (we have a composite FK to the joined thing)" + (let [query (lib/query metadata-provider order)] + (is (=? [[:= {} + [:field {} id-order-user-id] + [:field {} id-user-id]] + [:= {} + [:field {} id-order-user-email] + [:field {} id-user-email]]] + (sort-conditions (lib/suggested-join-conditions query user)))))) + (testing "USER joining ORDER (joined thing has a composite FK to us)" + (let [query (lib/query metadata-provider user)] + (is (=? [[:= {} + [:field {} id-user-id] + [:field {} id-order-user-id]] + [:= {} + [:field {} id-user-email] + [:field {} id-order-user-email]]] + (sort-conditions (lib/suggested-join-conditions query order))))))))) (deftest ^:parallel join-conditions-test (let [joins (lib/joins lib.tu/query-with-join)] diff --git a/test/metabase/lib/test_util/mocks_31769.cljc b/test/metabase/lib/test_util/mocks_31769.cljc index 770e8259cba97a6f91caaee713d7a7f473736bc7..faf1c3d202c76b8269cc96b0d016ee7e14a59ff0 100644 --- a/test/metabase/lib/test_util/mocks_31769.cljc +++ b/test/metabase/lib/test_util/mocks_31769.cljc @@ -22,10 +22,10 @@ (as-> (lib/query metadata-provider orders) q (lib/join q (let [products (lib.metadata/table metadata-provider (id-fn :products))] (-> (lib/join-clause products) - (lib/with-join-conditions [(lib/suggested-join-condition q products)])))) + (lib/with-join-conditions (lib/suggested-join-conditions q products))))) (lib/join q (let [people (lib.metadata/table metadata-provider (id-fn :people))] (-> (lib/join-clause people) - (lib/with-join-conditions [(lib/suggested-join-condition q people)])))) + (lib/with-join-conditions (lib/suggested-join-conditions q people))))) (lib/breakout q (let [breakout (m/find-first #(and (= (:id %) (id-fn :products :category)) (not= (:lib/source %) :source/implicitly-joinable)) (lib/breakoutable-columns q))] diff --git a/test/metabase/query_processor_test/explicit_joins_test.clj b/test/metabase/query_processor_test/explicit_joins_test.clj index 7421ecbb528ef89cbfd3818e31f3608773e2de44..3e59b40630dd4a6aaf10ff8f7b0ef56dcc2054bf 100644 --- a/test/metabase/query_processor_test/explicit_joins_test.clj +++ b/test/metabase/query_processor_test/explicit_joins_test.clj @@ -378,8 +378,8 @@ (testing "suggested join condition references the FK by name" (let [query (lib/query metadata-provider (lib.metadata/table metadata-provider (mt/id :people))) card-meta (lib.metadata/card metadata-provider 3)] - (is (=? [:= {} [:field {} (mt/id :people :id)] [:field {} cuser-id]] - (lib/suggested-join-condition query card-meta)))))) + (is (=? [[:= {} [:field {} (mt/id :people :id)] [:field {} cuser-id]]] + (lib/suggested-join-conditions query card-meta)))))) (testing "the query runs and returns correct data" (is (= {:columns [cid cid2 cuser-id ccount cuser-id2 ccount2]