diff --git a/frontend/src/metabase-lib/join.ts b/frontend/src/metabase-lib/join.ts new file mode 100644 index 0000000000000000000000000000000000000000..abe1ea7a9a0db9106250fed35623569482712971 --- /dev/null +++ b/frontend/src/metabase-lib/join.ts @@ -0,0 +1,57 @@ +import * as ML from "cljs/metabase.lib.js"; + +import type { + ColumnMetadata, + FilterOperator, + Join, + JoinStrategy, + Query, + TableMetadata, + CardMetadata, +} from "./types"; + +export function joinStrategy(join: Join): JoinStrategy { + return ML.join_strategy(join); +} + +export function withJoinStrategy(join: Join, strategy: JoinStrategy): Join { + return ML.with_join_strategy(join, strategy); +} + +export function availableJoinStrategies( + query: Query, + stageIndex: number, +): JoinStrategy[] { + return ML.available_join_strategies(query, stageIndex); +} + +export function joinConditionLHSColumns( + query: Query, + stageIndex: number, + rhsColumn?: ColumnMetadata, +): ColumnMetadata[] { + return ML.join_condition_lhs_columns(query, stageIndex, rhsColumn); +} + +export function joinConditionRHSColumns( + query: Query, + stageIndex: number, + joinedThing: TableMetadata | CardMetadata, + lhsColumn?: ColumnMetadata, +): ColumnMetadata[] { + return ML.join_condition_rhs_columns( + query, + stageIndex, + joinedThing, + lhsColumn, + ); +} + +export function joinConditionOperators( + query: Query, + stageIndex: number, + lhsColumn?: ColumnMetadata, + rhsColumn?: ColumnMetadata, +): FilterOperator[] { + return ML.join_condition_operators(query, stageIndex, lhsColumn, rhsColumn); +} diff --git a/frontend/src/metabase-lib/types.ts b/frontend/src/metabase-lib/types.ts index e79359731c2d323dceac6c4cac19bca0fd2c90d8..b442376a4248e2876367b8ae0492573c445e6e97 100644 --- a/frontend/src/metabase-lib/types.ts +++ b/frontend/src/metabase-lib/types.ts @@ -8,6 +8,12 @@ export type Query = unknown & { _opaque: typeof Query }; declare const MetadataProvider: unique symbol; export type MetadataProvider = unknown & { _opaque: typeof MetadataProvider }; +declare const TableMetadata: unique symbol; +export type TableMetadata = unknown & { _opaque: typeof TableMetadata }; + +declare const CardMetadata: unique symbol; +export type CardMetadata = unknown & { _opaque: typeof CardMetadata }; + export type Limit = number | null; declare const OrderByClause: unique symbol; @@ -55,3 +61,15 @@ export type OrderByClauseDisplayInfo = Pick< > & { direction: OrderByDirection; }; + +declare const FilterOperator: unique symbol; +export type FilterOperator = unknown & { _opaque: typeof FilterOperator }; + +declare const Join: unique symbol; +export type Join = unknown & { _opaque: typeof Join }; + +export type JoinStrategy = + | "left-join" + | "right-join" + | "inner-join" + | "full-join"; diff --git a/frontend/src/metabase-lib/v2.ts b/frontend/src/metabase-lib/v2.ts index f20a6b4659a02e37a443af8d9ab53fe973e17823..5c84f8fb4a93fde0551aa99b7fbf4a5169b2b843 100644 --- a/frontend/src/metabase-lib/v2.ts +++ b/frontend/src/metabase-lib/v2.ts @@ -7,5 +7,6 @@ export * from "./breakout"; export * from "./fields"; export * from "./limit"; export * from "./order_by"; +export * from "./join"; export * from "./query"; export * from "./types"; diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index 246b26ad9364f3be790d919773e7f1fc9e535e2e..89faa7f9aabf6f5eea680891891216c718f46818 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -167,7 +167,13 @@ join-fields joins with-join-alias - with-join-fields] + with-join-fields + join-strategy + with-join-strategy + available-join-strategies + join-condition-lhs-columns + join-condition-rhs-columns + join-condition-operators] [lib.limit current-limit limit] diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc index 3afea708e1acf6634d759e62c63a1cdffce4188a..0eed4e27dcf3dfdfe70ed44197ae8cf4426fe232 100644 --- a/src/metabase/lib/join.cljc +++ b/src/metabase/lib/join.cljc @@ -9,7 +9,9 @@ [metabase.lib.options :as lib.options] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.common :as lib.schema.common] + [metabase.lib.schema.filter :as lib.schema.filter] [metabase.lib.schema.join :as lib.schema.join] + [metabase.lib.types.isa :as lib.types.isa] [metabase.lib.util :as lib.util] [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) @@ -314,3 +316,145 @@ "Get all join conditions for the given join" [j :- ::lib.schema.join/join] (:fields j)) + +(mu/defn join-strategy :- ::lib.schema.join/strategy + "Get the raw keyword strategy (type) of a given join, e.g. `:left-join` or `:right-join`. This is either the value + of the optional `:strategy` key or the default, `:left-join`, if `:strategy` is not specified." + [a-join :- ::lib.schema.join/join] + (get a-join :strategy :left-join)) + +(mu/defn with-join-strategy :- [:or ::lib.schema.join/join fn?] + "Return a copy of `a-join` with its `:strategy` set to `strategy`." + [a-join :- [:or + ::lib.schema.join/join + fn?] + strategy :- ::lib.schema.join/strategy] + (if (fn? a-join) + (fn [query stage-metadata] + (with-join-strategy (a-join query stage-metadata) strategy)) + (assoc a-join :strategy strategy))) + +(mu/defn available-join-strategies :- [:sequential ::lib.schema.join/strategy] + "Get available join strategies for the current Database (based on the Database's + supported [[metabase.driver/driver-features]]) as raw keywords like `:left-join`." + ([query] + (available-join-strategies query -1)) + + ;; stage number is not currently used, but it is taken as a parameter for consistency with the rest of MLv2 + ([query :- ::lib.schema/query + _stage-number :- :int] + (let [database (lib.metadata/database query) + features (:features database)] + (filterv (partial contains? features) + [:left-join + :right-join + :inner-join + :full-join])))) + +;;; Building join conditions: +;;; +;;; The QB GUI needs to build a join condition before the join itself is attached to the query. There are three parts +;;; to a join condition. Suppose we're building a query like +;;; +;;; SELECT * FROM order JOIN user ON order.user_id = user.id +;;; +;;; The condition is +;;; +;;; order.user_id = user.id +;;; ^^^^^^^^^^^^^ ^ ^^^^^^^ +;;; 1 2 3 +;;; +;;; and the three parts are: +;;; +;;; 1. LHS/source column: the column in the left-hand side of the condition, e.g. the `order.user_id` in the example +;;; above. Either comes from the source Table, or a previous stage of the query, or a previously-joined +;;; Table/Model/Saved Question. `order.user_id` presumably is an FK to `user.id`, and while this is typical, is not +;;; required. +;;; +;;; 2. The operator: `=` in the example above. Corresponds to an `:=` MBQL clause. `=` is selected by default. +;;; +;;; 3. RHS/destination/target column: the column in the right-hand side of the condition e.g. `user.id` in the example +;;; above. `user.id` is a column in the Table/Model/Saved Question we are joining against. +;;; +;;; The Query Builder allows selecting any of these three parts in any order. The functions below return possible +;;; 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] + "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]] + (let [{:keys [pk fk other]} (group-by (fn [column] + (cond + (lib.types.isa/primary-key? column) :pk + (lib.types.isa/foreign-key? column) :fk + :else :other)) + columns)] + (concat pk fk other))) + +(mu/defn join-condition-lhs-columns :- [:sequential lib.metadata/ColumnMetadata] + "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. + + If the right-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI), + pass in the chosen RHS column. In the future, this may be used to restrict results to compatible columns. (See #31174) + + Results will be returned in a 'somewhat smart' order with PKs and FKs returned before other columns. + + Unlike most other things that return columns, implicitly-joinable columns ARE NOT returned here." + ([query rhs-column-or-nil] + (join-condition-lhs-columns query -1 rhs-column-or-nil)) + + ([query :- ::lib.schema/query + stage-number :- :int + ;; not yet used, hopefully we will use in the future when present for filtering incompatible columns out. + _rhs-column-or-nil :- [:maybe lib.metadata/ColumnMetadata]] + (sort-join-condition-columns + (lib.metadata.calculation/visible-columns query + stage-number + (lib.util/query-stage query stage-number) + {:include-implicitly-joinable? false})))) + +(mu/defn join-condition-rhs-columns :- [:sequential lib.metadata/ColumnMetadata] + "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, `joined-thing`, which can be something like a + Table ([[metabase.lib.metadata/TableMetadata]]), Saved Question/Model ([[metabase.lib.metadata/CardMetadata]]), + another query, etc. -- anything you can pass to [[join-clause]]. + + If the lhs-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI), + pass in the chosen LHS column. In the future, this may be used to restrict results to compatible columns. (See #31174) + + Results will be returned in a 'somewhat smart' order with PKs and FKs returned before other columns." + ([query joined-thing lhs-column-or-nil] + (join-condition-rhs-columns query -1 joined-thing lhs-column-or-nil)) + + ([query :- ::lib.schema/query + stage-number :- :int + joined-thing + ;; not yet used, hopefully we will use in the future when present for filtering incompatible columns out. + _lhs-column-or-nil :- [:maybe lib.metadata/ColumnMetadata]] + ;; I was on the fence about whether these should get `:lib/source :source/joins` or not -- it seems like based on + ;; the QB UI they shouldn't. See screenshots in #31174 + (sort-join-condition-columns + (lib.metadata.calculation/visible-columns query stage-number joined-thing {:include-implicitly-joinable? false})))) + +(mu/defn join-condition-operators :- [:sequential ::lib.schema.filter/operator] + "Return a sequence of valid filter clause operators that can be used to build a join condition. In the Query Builder + UI, this can be chosen at any point before or after choosing the LHS and RHS. Invalid options are not currently + filtered out based on values of the LHS or RHS, but in the future we can add this -- see #31174." + ([query lhs-column-or-nil rhs-column-or-nil] + (join-condition-operators query -1 lhs-column-or-nil rhs-column-or-nil)) + + ([_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]] + ;; currently hardcoded to these six operators regardless of LHS and RHS. + [{:lib/type :mbql.filter/operator, :short :=, :display-name (i18n/tru "Equal to")} + {:lib/type :mbql.filter/operator, :short :>, :display-name (i18n/tru "Greater than")} + {:lib/type :mbql.filter/operator, :short :<, :display-name (i18n/tru "Less than")} + {:lib/type :mbql.filter/operator, :short :>=, :display-name (i18n/tru "Greater than or equal to")} + {:lib/type :mbql.filter/operator, :short :<=, :display-name (i18n/tru "Less than or equal to")} + {:lib/type :mbql.filter/operator, :short :!=, :display-name (i18n/tru "Not equal to")}])) diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs index 4cb9b7087cc7ae18dce3a3c75d1c0bb866bf030f..f82262feea07736e9781b23bf496e659bb2bfc07 100644 --- a/src/metabase/lib/js.cljs +++ b/src/metabase/lib/js.cljs @@ -407,3 +407,52 @@ "Return a sequence of column metadatas for columns that you can specify in the `:fields` of a query." [a-query stage-number] (to-array (lib.core/fieldable-columns a-query stage-number))) + +(defn ^:export join-strategy + "Get the strategy (type) of a given join as a plain string like `left-join`." + [a-join] + (u/qualified-name (lib.core/join-strategy a-join))) + +(defn ^:export with-join-strategy + "Return a copy of `a-join` with its `:strategy` set to `strategy`." + [a-join strategy] + (lib.core/with-join-strategy a-join (keyword strategy))) + +(defn ^:export available-join-strategies + "Get available join strategies for the current Database (based on the Database's + supported [[metabase.driver/driver-features]]) as strings like `left-join`." + [a-query stage-number] + (to-array (map u/qualified-name (lib.core/available-join-strategies a-query stage-number)))) + +(defn ^:export join-condition-lhs-columns + "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. + + If the right-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI), + pass in the chosen RHS column. In the future, this may be used to restrict results to compatible columns. (See #31174) + + Results will be returned in a 'somewhat smart' order with PKs and FKs returned before other columns. + + Unlike most other things that return columns, implicitly-joinable columns ARE NOT returned here." + [a-query stage-number rhs-column-or-nil] + (to-array (lib.core/join-condition-lhs-columns a-query stage-number rhs-column-or-nil))) + +(defn ^:export join-condition-rhs-columns + "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, `joined-thing`, which can be something like a + Table ([[metabase.lib.metadata/TableMetadata]]), Saved Question/Model ([[metabase.lib.metadata/CardMetadata]]), + another query, etc. -- anything you can pass to [[join-clause]]. + + If the lhs-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI), + pass in the chosen LHS column. In the future, this may be used to restrict results to compatible columns. (See #31174) + + Results will be returned in a 'somewhat smart' order with PKs and FKs returned before other columns." + [a-query stage-number joined-thing lhs-column-or-nil] + (to-array (lib.core/join-condition-rhs-columns a-query stage-number joined-thing lhs-column-or-nil))) + +(defn ^:export join-condition-operators + "Return a sequence of valid filter clause operators that can be used to build a join condition. In the Query Builder + UI, this can be chosen at any point before or after choosing the LHS and RHS. Invalid options are not currently + filtered out based on values of the LHS or RHS, but in the future we can add this -- see #31174." + [a-query stage-number lhs-column-or-nil rhs-column-or-nil] + (to-array (lib.core/join-condition-operators a-query stage-number lhs-column-or-nil rhs-column-or-nil))) diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc index ece7432fcd4c9b1850f687ffecb8986f787975da..f6d134bbd44ea0aea5475999f4e3c975c3bd7549 100644 --- a/src/metabase/lib/metadata.cljc +++ b/src/metabase/lib/metadata.cljc @@ -100,11 +100,12 @@ ;; for [[metabase.lib.field/fieldable-columns]] it means its already present in `:fields` [:selected? {:optional true} :boolean]]) -(def ^:private CardMetadata - "More or less the same as a [[metabase.models.card]], but with kebab-case keys. Note that the `:dataset-query` is not - necessarily converted to pMBQL yet. Probably safe to assume it is normalized however. Likewise, `:result-metadata` - is probably not quite massaged into a sequence of `ColumnMetadata`s just yet. - See [[metabase.lib.card/card-metadata-columns]] that converts these as needed." +(def CardMetadata + "Schema for metadata about a specific Saved Question (which may or may not be a Model). More or less the same as + a [[metabase.models.card]], but with kebab-case keys. Note that the `:dataset-query` is not necessarily converted to + pMBQL yet. Probably safe to assume it is normalized however. Likewise, `:result-metadata` is probably not quite + massaged into a sequence of `ColumnMetadata`s just yet. See [[metabase.lib.card/card-metadata-columns]] that + converts these as needed." [:map [:lib/type [:= :metadata/card]] [:id ::lib.schema.id/card] @@ -130,8 +131,9 @@ [:id ::lib.schema.id/metric] [:name ::lib.schema.common/non-blank-string]]) -(def ^:private TableMetadata - "More or less the same as a [[metabase.models.table]], but with kebab-case keys." +(def TableMetadata + "Schema for metadata about a specific [[metabase.models.table]]. More or less the same as a [[metabase.models.table]], + but with kebab-case keys." [:map [:lib/type [:= :metadata/table]] [:id ::lib.schema.id/table] @@ -146,7 +148,10 @@ [:lib/type [:= :metadata/database]] [:id ::lib.schema.id/database] ;; Like `:fields` for [[TableMetadata]], this is now optional -- we can fetch the Tables separately if needed. - [:tables {:optional true} [:sequential TableMetadata]]]) + [:tables {:optional true} [:sequential TableMetadata]] + ;; TODO -- this should validate against the driver features list in [[metabase.driver/driver-features]] if we're in + ;; Clj mode + [:features {:optional true} [:set :keyword]]]) (def MetadataProvider "Schema for something that satisfies the [[lib.metadata.protocols/MetadataProvider]] protocol." diff --git a/src/metabase/lib/schema/join.cljc b/src/metabase/lib/schema/join.cljc index 9bb7c3c506047d8edd27d4cb3853fcfb27a04076..ab1122b511ff74c4b9ad8faf342f59bd432de8dd 100644 --- a/src/metabase/lib/schema/join.cljc +++ b/src/metabase/lib/schema/join.cljc @@ -40,6 +40,18 @@ (mr/def ::conditions [:sequential {:min 1} [:ref ::expression/boolean]]) +;;; valid values for the optional `:strategy` key in a join. Note that these are only valid if the current Database +;;; supports that specific join type; these match 1:1 with the Database `:features`, e.g. a Database that supports +;;; left joins will support the `:left-join` feature. +;;; +;;; When `:strategy` is not specified, `:left-join` is the default strategy. +(mr/def ::strategy + [:enum + :left-join + :right-join + :inner-join + :full-join]) + (mr/def ::join [:map [:lib/type [:= :mbql/join]] @@ -47,7 +59,8 @@ [:stages [:ref :metabase.lib.schema/stages]] [:conditions ::conditions] [:fields {:optional true} ::fields] - [:alias {:optional true} ::alias]]) + [:alias {:optional true} ::alias] + [:strategy {:optional true} ::strategy]]) (mr/def ::joins [:and diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc index 78a47ab7a351589d94043678d9a43a91c50266cf..76556cf5dee3488e3d4e47ae098877d496538c05 100644 --- a/test/metabase/lib/join_test.cljc +++ b/test/metabase/lib/join_test.cljc @@ -13,7 +13,7 @@ #?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me)) (deftest ^:parallel resolve-join-test - (let [query (lib/query meta/metadata-provider (meta/table-metadata :venues)) + (let [query lib.tu/venues-query join-clause (-> ((lib/join-clause (meta/table-metadata :categories) [(lib/= @@ -275,3 +275,106 @@ :lib/source-column-alias "ID" :lib/desired-column-alias "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY_bfaf4e7b"}] (lib.metadata.calculation/metadata query))))) + +(deftest ^:parallel join-strategy-test + (let [query (lib.tu/query-with-join) + [join] (lib/joins query)] + (testing "join without :strategy" + (is (= :left-join + (lib/join-strategy join)))) + (testing "join with explicit :strategy" + (let [join' (lib/with-join-strategy join :right-join)] + (is (=? {:strategy :right-join} + join')) + (is (= :right-join + (lib/join-strategy join'))))))) + +(deftest ^:parallel with-join-strategy-test + (testing "Make sure `with-join-alias` works with unresolved functions" + (is (=? {:stages [{:joins [{:strategy :right-join}]}]} + (-> lib.tu/venues-query + (lib/join (-> (lib/join-clause (fn [_query _stage-number] + (meta/table-metadata :categories)) + [(lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Cat"))]) + (lib/with-join-strategy :right-join)))))))) + +(deftest ^:parallel available-join-strategies-test + (is (= [:left-join :right-join :inner-join] + (lib/available-join-strategies (lib.tu/query-with-join))))) + +(defn- query-with-join-with-fields + "A query against `VENUES` joining `CATEGORIES` with `:fields` set to return only `NAME`." + [] + (-> lib.tu/venues-query + (lib/join (-> (lib/join-clause + (meta/table-metadata :categories) + [(lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Cat"))]) + (lib/with-join-alias "Cat") + (lib/with-join-fields [(lib/with-join-alias (lib/field "CATEGORIES" "NAME") "Cat")]))))) + +(deftest ^:parallel join-condition-lhs-columns-test + (let [query lib.tu/venues-query] + (doseq [rhs [nil (lib/with-join-alias (lib.metadata/field query (meta/id :venues :category-id)) "Cat")]] + (testing (str "rhs = " (pr-str rhs)) + ;; sort PKs then FKs then everything else + (is (=? [{:lib/desired-column-alias "ID"} + {:lib/desired-column-alias "CATEGORY_ID"} + {:lib/desired-column-alias "NAME"} + {:lib/desired-column-alias "LATITUDE"} + {:lib/desired-column-alias "LONGITUDE"} + {:lib/desired-column-alias "PRICE"}] + (lib/join-condition-lhs-columns query rhs))))))) + +(deftest ^:parallel join-condition-lhs-columns-with-previous-join-test + (testing "Include columns from previous join(s)" + (let [query (query-with-join-with-fields)] + (doseq [rhs [nil (lib/with-join-alias (lib.metadata/field query (meta/id :users :id)) "User")]] + (testing (str "rhs = " (pr-str rhs)) + (is (=? [{:lib/desired-column-alias "ID"} + {:lib/desired-column-alias "Cat__ID"} ;; FIXME #31233 + {:lib/desired-column-alias "CATEGORY_ID"} + {:lib/desired-column-alias "NAME"} + {:lib/desired-column-alias "LATITUDE"} + {:lib/desired-column-alias "LONGITUDE"} + {:lib/desired-column-alias "PRICE"} + {:lib/desired-column-alias "Cat__NAME"}] + (lib/join-condition-lhs-columns query rhs))) + (is (= (lib/join-condition-lhs-columns query rhs) + (lib/join-condition-lhs-columns query -1 rhs)))))))) + +(deftest ^:parallel join-condition-rhs-columns-test + (let [query lib.tu/venues-query] + (doseq [lhs [nil (lib.metadata/field query (meta/id :venues :id))] + joined-thing [(meta/table-metadata :venues) + meta/saved-question-CardMetadata]] + (testing (str "lhs = " (pr-str lhs) + "\njoined-thing = " (pr-str joined-thing)) + (is (=? [{:lib/desired-column-alias "ID"} + {:lib/desired-column-alias "CATEGORY_ID"} + {:lib/desired-column-alias "NAME"} + {:lib/desired-column-alias "LATITUDE"} + {:lib/desired-column-alias "LONGITUDE"} + {:lib/desired-column-alias "PRICE"}] + (map #(select-keys % [:lib/desired-column-alias]) + (lib/join-condition-rhs-columns query joined-thing lhs)))))))) + +(deftest ^:parallel join-condition-operators-test + ;; just make sure that this doesn't barf and returns the expected output given any combination of LHS or RHS fields + ;; for now until we actually implement filtering there + (let [query lib.tu/venues-query] + (doseq [lhs [nil (lib.metadata/field query (meta/id :categories :id))] + rhs [nil (lib.metadata/field query (meta/id :venues :category-id))]] + (testing (pr-str (list `lib/join-condition-operators `lib.tu/venues-query lhs rhs)) + (is (=? [{:short :=} + {:short :>} + {:short :<} + {:short :>=} + {:short :<=} + {:short :!=}] + (lib/join-condition-operators lib.tu/venues-query lhs rhs))) + (is (= (lib/join-condition-operators lib.tu/venues-query lhs rhs) + (lib/join-condition-operators lib.tu/venues-query -1 lhs rhs))))))) diff --git a/test/metabase/lib/metadata_test.cljc b/test/metabase/lib/metadata_test.cljc index 63c3f824847dfd0dec99cd0b38cd178406d6a589..e7e89a3a77fde97035bc5e5816fc05ae88df61c7 100644 --- a/test/metabase/lib/metadata_test.cljc +++ b/test/metabase/lib/metadata_test.cljc @@ -37,7 +37,7 @@ :name "CATEGORY_ID" :base-type :type/Integer :effective-type :type/Integer - :semantic-type nil} + :semantic-type :type/FK} x) (lib.metadata/stage-column query "CATEGORY_ID") (lib.metadata/stage-column query -1 "CATEGORY_ID")))) diff --git a/test/metabase/lib/test_metadata.cljc b/test/metabase/lib/test_metadata.cljc index 6a9b950d1fc78c4af7dee3cea48006d1a0b71fd1..9a355db68088eb1092eba050f245858e97850391 100644 --- a/test/metabase/lib/test_metadata.cljc +++ b/test/metabase/lib/test_metadata.cljc @@ -2232,7 +2232,7 @@ :name "CATEGORY_ID" :base-type :type/Integer :effective-type :type/Integer - :semantic-type nil + :semantic-type :type/FK :fingerprint {:global {:distinct-count 28, :nil% 0.0} :type {:type/Number {:min 2.0 @@ -2298,10 +2298,16 @@ :columns card-results-metadata}) (def saved-question - "An representative Saved Question, with [[results-metadata]]. For testing queries that use a Saved Question as their - source. If you added `:lib/type`, `:id`, and `:name`, you could also use this as - a [[metabase.lib.metadata/CardMetadata]]." + "An representative Saved Question, with [[results-metadata]], against `VENUES`. For testing queries that use a Saved + Question as their source. See also [[saved-question-CardMetadata]] below." {:dataset-query {:database (id) :type :query :query {:source-table (id :venues)}} :result-metadata card-results-metadata}) + +(def saved-question-CardMetadata + "Mock [[metabase.lib.metadata/CardMetadata]] with a query against `VENUES`." + (assoc saved-question + :lib/type :metadata/card + :id 1 + :name "Card 1")) diff --git a/test/metabase/lib/test_util.cljc b/test/metabase/lib/test_util.cljc index 6d284f5a570eac0dad5bee0ea4e33ec8dbb5cfab..a87bab7327de974436432819e300f3068e97a818 100644 --- a/test/metabase/lib/test_util.cljc +++ b/test/metabase/lib/test_util.cljc @@ -17,11 +17,7 @@ (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me)) (def venues-query - {:lib/type :mbql/query - :lib/metadata meta/metadata-provider - :database (meta/id) - :stages [{:lib/type :mbql.stage/mbql - :source-table (meta/id :venues)}]}) + (lib/query meta/metadata-provider (meta/table-metadata :venues))) (defn venues-query-with-last-stage [m] (let [query (update-in venues-query [:stages 0] merge m)]