Compare commits
15 Commits
python-run
...
lib-equali
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1eba19cb5 | ||
|
|
a9f683a4f4 | ||
|
|
c29b7fe94e | ||
|
|
fcb7f18aaf | ||
|
|
af10b73290 | ||
|
|
ff21c04a89 | ||
|
|
d5e2e7ce81 | ||
|
|
b04a33dfac | ||
|
|
bd4773f3b1 | ||
|
|
3f532553f5 | ||
|
|
876a8ca9cf | ||
|
|
4435f235b1 | ||
|
|
c1ea67473d | ||
|
|
de22e1a1a2 | ||
|
|
9cb983832c |
@@ -272,6 +272,7 @@
|
||||
clojure.java.jdbc/with-db-connection {:message "Use sql-jdbc.execute/do-with-connection-with-options (for drivers) or t2/with-connection (for the app DB) instead of jdbc/with-db-connection"}
|
||||
clojure.java.jdbc/with-db-metadata {:message "Use sql-jdbc.execute/do-with-connection-with-options + .getMetaData instead of jdbc/with-db-metadata"}
|
||||
clojure.pprint/pprint {:message "Use metabase.util.log instead of clojure.pprint/pprint"}
|
||||
clojure.pprint/print-table {:message "Use metabase.util.log if you want to print something"}
|
||||
clojure.string/lower-case {:message "Use metabase.util/lower-case-en instead of clojure.string/lower-case"}
|
||||
clojure.string/upper-case {:message "Use metabase.util/upper-case-en instead of clojure.string/upper-case"}
|
||||
compojure.core/context {:message "Use metabase.api.util.handlers/route-map-handler instead of compojure.core/context, it is much faster and supports OpenAPI spec generation"}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
#(-> % (dissoc :operators :field-values) col->index))
|
||||
default-temporal-breakout (when with-default-temporal-breakout?
|
||||
(->> breakouts
|
||||
(map #(lib/find-matching-column % visible-cols))
|
||||
(map #(lib/find-matching-column metric-query -1 % visible-cols {:find-matching-column/ignore-binning-and-bucketing? true}))
|
||||
(m/find-first lib.types.isa/temporal?)))
|
||||
field-id-prefix (metabot-v3.tools.u/card-field-id-prefix id)]
|
||||
(cond-> {:id id
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"Calls `ensure-audit-db-installed!` before and after `body` to ensure that the audit DB is installed and then
|
||||
restored if necessary. Also disables audit content loading if it is already loaded."
|
||||
`(let [audit-collection-exists?# (t2/exists? :model/Collection :type "instance-analytics")]
|
||||
(clojure.pprint/print-table (t2/select [:model/Database :id :name]))
|
||||
(mt/with-temp-env-var-value! [mb-load-analytics-content (not audit-collection-exists?#)]
|
||||
(mbc/ensure-audit-db-installed!)
|
||||
(try
|
||||
|
||||
@@ -23,10 +23,17 @@ export function aggregationOperatorColumns(
|
||||
}
|
||||
|
||||
export function selectedAggregationOperators(
|
||||
query: Query,
|
||||
stageIndex: number,
|
||||
operators: AggregationOperator[],
|
||||
clause: AggregationClause,
|
||||
): AggregationOperator[] {
|
||||
return ML.selected_aggregation_operators(operators, clause);
|
||||
return ML.selected_aggregation_operators(
|
||||
query,
|
||||
stageIndex,
|
||||
operators,
|
||||
clause,
|
||||
);
|
||||
}
|
||||
|
||||
export function aggregate(
|
||||
|
||||
@@ -123,7 +123,7 @@ function setup({
|
||||
|
||||
const baseOperators = Lib.availableAggregationOperators(query, stageIndex);
|
||||
const operators = clause
|
||||
? Lib.selectedAggregationOperators(baseOperators, clause)
|
||||
? Lib.selectedAggregationOperators(query, stageIndex, baseOperators, clause)
|
||||
: baseOperators;
|
||||
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
@@ -19,6 +19,8 @@ export const getAggregationItems = ({
|
||||
const { displayName } = Lib.displayInfo(query, stageIndex, aggregation);
|
||||
|
||||
const operators = Lib.selectedAggregationOperators(
|
||||
query,
|
||||
stageIndex,
|
||||
Lib.availableAggregationOperators(query, stageIndex),
|
||||
aggregation,
|
||||
);
|
||||
|
||||
@@ -98,7 +98,12 @@ function AggregationPopover({
|
||||
const operators = useMemo(() => {
|
||||
const baseOperators = Lib.availableAggregationOperators(query, stageIndex);
|
||||
return isUpdate
|
||||
? Lib.selectedAggregationOperators(baseOperators, clause)
|
||||
? Lib.selectedAggregationOperators(
|
||||
query,
|
||||
stageIndex,
|
||||
baseOperators,
|
||||
clause,
|
||||
)
|
||||
: baseOperators;
|
||||
}, [query, clause, stageIndex, isUpdate]);
|
||||
|
||||
|
||||
@@ -404,7 +404,9 @@
|
||||
|
||||
(mu/defn selected-aggregation-operators :- [:maybe [:sequential SelectedOperatorWithColumns]]
|
||||
"Mark the operator and the column (if any) in `agg-operators` selected by `agg-clause`."
|
||||
[agg-operators :- [:maybe [:sequential OperatorWithColumns]]
|
||||
[query
|
||||
stage-number
|
||||
agg-operators :- [:maybe [:sequential OperatorWithColumns]]
|
||||
agg-clause]
|
||||
(when (seq agg-operators)
|
||||
(let [[op _ agg-col] agg-clause
|
||||
@@ -418,6 +420,8 @@
|
||||
(fn [cols]
|
||||
(if (lib.util/ref-clause? agg-col)
|
||||
(let [cols (lib.equality/mark-selected-columns
|
||||
query
|
||||
stage-number
|
||||
cols
|
||||
[(lib.options/update-options agg-col dissoc :temporal-unit)])]
|
||||
(mapv (fn [c]
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
stage-number
|
||||
(get existing-breakouts position)
|
||||
columns
|
||||
{:generous? true}))
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true}))
|
||||
(range (count existing-breakouts)))]
|
||||
(mapv #(let [positions (column->breakout-positions %)]
|
||||
(cond-> %
|
||||
@@ -96,27 +96,16 @@
|
||||
"Returns existing breakouts (as MBQL expressions) for `column` in a stage if there are any. Returns `nil` if there
|
||||
are no existing breakouts."
|
||||
([query stage-number column]
|
||||
(existing-breakouts query stage-number column nil))
|
||||
(existing-breakouts query stage-number column {:find-matching-column/ignore-binning-and-bucketing? true}))
|
||||
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
column :- ::lib.schema.metadata/column
|
||||
{:keys [same-binning-strategy?
|
||||
same-temporal-bucket?], :as _options} :- [:maybe
|
||||
[:map
|
||||
[:same-binning-strategy? {:optional true, :default false} [:maybe :boolean]]
|
||||
[:same-temporal-bucket? {:optional true, :default false} [:maybe :boolean]]]]]
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
column :- ::lib.schema.metadata/column
|
||||
options :- [:maybe ::lib.equality/find-matching-column.options]]
|
||||
(not-empty
|
||||
(into []
|
||||
(filter (fn [a-breakout]
|
||||
(and (lib.equality/find-matching-column query stage-number a-breakout [column])
|
||||
(or (not same-temporal-bucket?)
|
||||
(= (lib.temporal-bucket/temporal-bucket a-breakout)
|
||||
(lib.temporal-bucket/temporal-bucket column)))
|
||||
(or (not same-binning-strategy?)
|
||||
(lib.binning/binning= (lib.binning/binning a-breakout)
|
||||
(lib.binning/binning column))))))
|
||||
(breakouts query stage-number)))))
|
||||
(filterv (fn [a-breakout]
|
||||
(lib.equality/find-matching-column query stage-number a-breakout [column] options))
|
||||
(breakouts query stage-number)))))
|
||||
|
||||
(defn breakout-column?
|
||||
"Returns if `column` is a breakout column of stage with `stage-number` of `query`."
|
||||
@@ -148,14 +137,20 @@
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
breakout-ref :- ::lib.schema.ref/ref]
|
||||
(when-let [column (lib.equality/find-matching-column breakout-ref
|
||||
(let [column (or (lib.equality/find-matching-column query
|
||||
stage-number
|
||||
breakout-ref
|
||||
(breakoutable-columns query stage-number)
|
||||
{:generous? true})]
|
||||
(let [binning (lib.binning/binning breakout-ref)
|
||||
bucket (lib.temporal-bucket/temporal-bucket breakout-ref)]
|
||||
(cond-> column
|
||||
binning (lib.binning/with-binning binning)
|
||||
bucket (lib.temporal-bucket/with-temporal-bucket bucket))))))
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true})
|
||||
(throw (ex-info "Failed to resolve breakout ref"
|
||||
{:query query
|
||||
:stage-number stage-number
|
||||
:ref breakout-ref})))
|
||||
binning (lib.binning/binning breakout-ref)
|
||||
bucket (lib.temporal-bucket/temporal-bucket breakout-ref)]
|
||||
(cond-> column
|
||||
binning (lib.binning/with-binning binning)
|
||||
bucket (lib.temporal-bucket/with-temporal-bucket bucket)))))
|
||||
|
||||
(mu/defn remove-all-breakouts :- ::lib.schema/query
|
||||
"Remove all breakouts from a query stage."
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
([query context]
|
||||
(available-drill-thrus query -1 context))
|
||||
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
{:keys [column column-ref] :as context} :- ::lib.schema.drill-thru/context]
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
{:keys [column] :as context} :- ::lib.schema.drill-thru/context]
|
||||
(try
|
||||
(into []
|
||||
(when (and (lib.metadata/editable? query)
|
||||
@@ -114,12 +114,12 @@
|
||||
;; underlying table id instead of "card__blah", and that can cause issues with drills in certain
|
||||
;; scenarios. Changing the returned column metadata would be a large and scary change, so we just
|
||||
;; grab the equivalent column from returned-columns here instead
|
||||
new-column (and column-ref
|
||||
(->> (lib.metadata.calculation/returned-columns query stage-number)
|
||||
(lib.equality/find-matching-column query stage-number column-ref)))
|
||||
old-types (select-keys column [:base-type :effective-type :semantic-type])
|
||||
context (cond-> context
|
||||
(and column column-ref new-column) (assoc :column (merge new-column old-types)))
|
||||
new-column (and column
|
||||
(->> (lib.metadata.calculation/returned-columns query stage-number)
|
||||
(lib.equality/find-matching-column query stage-number column)))
|
||||
old-types (select-keys column [:base-type :effective-type :semantic-type])
|
||||
context (cond-> context
|
||||
(and column new-column) (assoc :column (merge new-column old-types)))
|
||||
{:keys [query stage-number]} (lib.query/wrap-native-query-with-mbql
|
||||
query stage-number (:card-id context))
|
||||
context (context-with-dimensions-or-row-dimensions query context)
|
||||
|
||||
@@ -21,9 +21,12 @@
|
||||
Question transformation:
|
||||
- None"
|
||||
(:require
|
||||
[better-cond.core :as b]
|
||||
[metabase.lib.drill-thru.common :as lib.drill-thru.common]
|
||||
[metabase.lib.field.util :as lib.field.util]
|
||||
[metabase.lib.filter :as lib.filter]
|
||||
[metabase.lib.filter.operator :as lib.filter.operator]
|
||||
[metabase.lib.ref :as lib.ref]
|
||||
[metabase.lib.schema :as lib.schema]
|
||||
[metabase.lib.schema.drill-thru :as lib.schema.drill-thru]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
@@ -52,9 +55,7 @@
|
||||
column :- ::lib.schema.metadata/column
|
||||
column-ref :- ::lib.schema.ref/ref
|
||||
adding :- [:enum :filter :expression]]
|
||||
(let [next-stage (->> (lib.util/canonical-stage-index query stage-number)
|
||||
(lib.util/next-stage-number query))
|
||||
base (cond
|
||||
(let [base (b/cond
|
||||
;; An extra stage is needed if:
|
||||
;; - The target column is an aggregation
|
||||
;; - OR the target column is a breakout AND we are adding a custom expression based on it.
|
||||
@@ -64,17 +65,25 @@
|
||||
(and (:lib/breakout? column)
|
||||
(= adding :expression))))
|
||||
{:query query
|
||||
:stage-number stage-number}
|
||||
|
||||
:stage-number stage-number
|
||||
:column-ref column-ref}
|
||||
;; An extra stage is needed.
|
||||
:let [next-stage-ref (-> column
|
||||
lib.field.util/update-keys-for-col-from-previous-stage
|
||||
lib.ref/ref)]
|
||||
;; If there's a later stage, then use it.
|
||||
next-stage {:query query
|
||||
:stage-number next-stage}
|
||||
:let [next-stage-number (lib.util/next-stage-number query stage-number)]
|
||||
next-stage-number
|
||||
{:query query
|
||||
:stage-number next-stage-number
|
||||
:column-ref next-stage-ref}
|
||||
;; And if there isn't a later stage, add one.
|
||||
:else {:query (lib.stage/append-stage query)
|
||||
:stage-number -1})
|
||||
:else
|
||||
{:query (lib.stage/append-stage query)
|
||||
:stage-number -1
|
||||
:column-ref next-stage-ref})
|
||||
filter-column (lib.drill-thru.common/matching-filterable-column
|
||||
(:query base) (:stage-number base) column-ref column)]
|
||||
(:query base) (:stage-number base) (:column-ref base) column)]
|
||||
;; If we cannot find the matching column, don't allow to drill
|
||||
(when filter-column
|
||||
(assoc base :column filter-column))))
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
column-ref :- ::lib.schema.ref/ref
|
||||
column :- ::lib.schema.metadata/column]
|
||||
(let [columns (lib.filter/filterable-columns query stage-number)]
|
||||
(or (lib.equality/find-matching-column query stage-number column-ref columns)
|
||||
(or (lib.equality/find-matching-column query stage-number column-ref columns {:find-matching-column/ignore-binning-and-bucketing? true})
|
||||
(and (:lib/source-uuid column)
|
||||
(m/find-first #(= (:lib/source-uuid %) (:lib/source-uuid column))
|
||||
columns)))))
|
||||
|
||||
@@ -35,12 +35,14 @@
|
||||
(lib.equality/find-matching-column query
|
||||
stage-number
|
||||
column-ref
|
||||
(lib.order-by/orderable-columns query stage-number)))
|
||||
(lib.order-by/orderable-columns query stage-number)
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true}))
|
||||
|
||||
(mu/defn- existing-order-by-clause :- [:maybe ::lib.schema.order-by/order-by]
|
||||
[query stage-number column]
|
||||
(m/find-first (fn [[_direction _opts expr, :as _asc-or-desc-clause]]
|
||||
(lib.equality/find-matching-column query stage-number expr [column]))
|
||||
(lib.equality/find-matching-column query stage-number expr [column]
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true}))
|
||||
(lib.order-by/order-bys query stage-number)))
|
||||
|
||||
(mu/defn- existing-order-by-direction :- [:maybe ::lib.schema.order-by/direction]
|
||||
|
||||
@@ -103,8 +103,7 @@
|
||||
stage-number :- :int
|
||||
{{:keys [semantic-type] :as column} :column, :keys [value], :as _context} :- ::lib.schema.drill-thru/context]
|
||||
(when (and column value (not= value :null)
|
||||
(or (not (or (= semantic-type :type/Latitude)
|
||||
(= semantic-type :type/Longitude)))
|
||||
(or (not (#{:type/Latitude :type/Longitude} semantic-type))
|
||||
(not (has-lat-lon? query stage-number))))
|
||||
(when-let [existing-breakout (first (lib.breakout/existing-breakouts query
|
||||
(lib.underlying/top-level-stage-number query)
|
||||
@@ -158,10 +157,12 @@
|
||||
;; filters to top-level-stage-number breaks the binning.
|
||||
(let [top-level-stage-number (lib.underlying/top-level-stage-number query)
|
||||
resolved-column (lib.drill-thru.common/breakout->resolved-column query stage-number column)
|
||||
old-filters (filter (fn [[operator _opts filter-column]]
|
||||
(and (#{:>= :<} operator)
|
||||
(lib.equality/find-matching-column filter-column [column])))
|
||||
(lib.filter/filters query stage-number))]
|
||||
old-filters (filterv (fn [[operator _opts filter-expr]]
|
||||
(and (#{:>= :<} operator)
|
||||
(lib.equality/find-matching-column
|
||||
query stage-number filter-expr [column]
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true})))
|
||||
(lib.filter/filters query stage-number))]
|
||||
(-> (reduce #(lib.remove-replace/remove-clause %1 stage-number %2) query old-filters)
|
||||
(update-breakout top-level-stage-number column resolved-column new-binning)
|
||||
(lib.filter/filter stage-number (lib.filter/>= resolved-column min-value))
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
:when (and (lib.util/clause-of-type? breakout-ref :field)
|
||||
(lib.temporal-bucket/temporal-bucket breakout-ref))
|
||||
{:keys [column] :as dimension} dimensions
|
||||
:when (and (lib.equality/find-matching-column breakout-ref [column])
|
||||
:when (and (lib.equality/find-matching-column query stage-number breakout-ref [column])
|
||||
(= (lib.temporal-bucket/raw-temporal-bucket breakout-ref)
|
||||
(or (lib.temporal-bucket/raw-temporal-bucket column)
|
||||
;; If query is multi-stage and column comes from a call
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
(:require
|
||||
[medley.core :as m]
|
||||
[metabase.lib.binning :as lib.binning]
|
||||
[metabase.lib.card :as lib.card]
|
||||
[metabase.lib.convert :as lib.convert]
|
||||
[metabase.lib.dispatch :as lib.dispatch]
|
||||
[metabase.lib.field.util :as lib.field.util]
|
||||
[metabase.lib.hierarchy :as lib.hierarchy]
|
||||
[metabase.lib.join.util :as lib.join.util]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.metadata.calculation :as lib.metadata.calculation]
|
||||
[metabase.lib.options :as lib.options]
|
||||
[metabase.lib.ref :as lib.ref]
|
||||
[metabase.lib.schema :as lib.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.schema.ref :as lib.schema.ref]
|
||||
[metabase.lib.temporal-bucket :as lib.temporal-bucket]
|
||||
[metabase.lib.util :as lib.util]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.log :as log]
|
||||
[metabase.util.malli :as mu]))
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.registry :as mr]))
|
||||
|
||||
(defmulti =
|
||||
"Determine whether two already-normalized pMBQL maps, clauses, or other sorts of expressions are equal. The basic rule
|
||||
@@ -103,378 +101,170 @@
|
||||
(sequential? x) ((get-method = :dispatch-type/sequential) x y)
|
||||
:else (clojure.core/= x y)))
|
||||
|
||||
(defn- columns-equal-by-fn-when-non-nil-in-both [f col-1 col-2]
|
||||
;; for debugging purposes the implementation of [[=]] for two columns return the "reason" things are not equal so we
|
||||
;; can log this; [[=]] is basically just (not <reason>)
|
||||
|
||||
(defn- columns-not-equal-by-fn-when-non-nil-in-both
|
||||
[f col-1 col-2]
|
||||
(let [v1 (f col-1)
|
||||
v2 (f col-2)]
|
||||
(if (and v1 v2)
|
||||
(clojure.core/= v1 v2)
|
||||
true)))
|
||||
(when (and v1 v2 (not= v1 v2))
|
||||
f)))
|
||||
|
||||
(defn- columns-equal-by-fn [f col-1 col-2]
|
||||
(clojure.core/= (f col-1) (f col-2)))
|
||||
(defn- columns-not-equal-by-fn
|
||||
[f col-1 col-2]
|
||||
(when (not= (f col-1) (f col-2))
|
||||
f))
|
||||
|
||||
(defn- ignore-default-temporal-bucket [bucket]
|
||||
(when-not (clojure.core/= bucket :default)
|
||||
bucket))
|
||||
|
||||
(defmethod = :metadata/column
|
||||
(defn- columns-not-equal-reason
|
||||
"Returns a keyword representing the reason why two columns fail an [[=]] check (for debugging purposes)."
|
||||
[col-1 col-2]
|
||||
(and
|
||||
;; two column metadatas with different IDs are NEVER equal.
|
||||
(columns-equal-by-fn-when-non-nil-in-both :id col-1 col-2)
|
||||
;; from the same source.
|
||||
(columns-equal-by-fn-when-non-nil-in-both :lib/source col-1 col-2)
|
||||
;; same join alias
|
||||
(columns-equal-by-fn lib.join.util/current-join-alias col-1 col-2)
|
||||
;;
|
||||
;; columns that don't have the same binning or temporal bucketing are never the same.
|
||||
;;
|
||||
;; same binning
|
||||
(columns-equal-by-fn :metabase.lib.field/binning col-1 col-2)
|
||||
;; same bucketing
|
||||
(columns-equal-by-fn (comp ignore-default-temporal-bucket lib.temporal-bucket/raw-temporal-bucket) col-1 col-2)
|
||||
;; check `:inherited-temporal-unit` as well if both columns have it.
|
||||
(columns-equal-by-fn-when-non-nil-in-both (comp ignore-default-temporal-bucket :inherited-temporal-unit) col-1 col-2)
|
||||
;; finally make sure they have the same `:lib/source-column-alias` (if both columns have it) or `:name` (if for
|
||||
;; some reason they do not)
|
||||
(or
|
||||
;; two column metadatas with different IDs are NEVER equal.
|
||||
(columns-not-equal-by-fn-when-non-nil-in-both :id col-1 col-2)
|
||||
;; from the same source.
|
||||
(columns-not-equal-by-fn-when-non-nil-in-both :lib/source col-1 col-2)
|
||||
;; same join alias
|
||||
(columns-not-equal-by-fn :metabase.lib.join/join-alias col-1 col-2)
|
||||
;; same FK Field (for implicitly joined columns)
|
||||
(columns-not-equal-by-fn-when-non-nil-in-both :fk-field-id col-1 col-2)
|
||||
(columns-not-equal-by-fn :fk-join-alias col-1 col-2)
|
||||
;; TODO (Cam 9/4/25) -- not super clear that this ought to be a reason for columns to be considered different since
|
||||
;; `:fk-field-name` doesn't really seem to be super important... but this check seems to be needed, otherwise when
|
||||
;; a there are multiple remappings from Col A => Col B (e.g. in a self-join) we'll potentially accidentally match
|
||||
;; the wrong one. Maybe we can figure out a better way to make sure that doesn't happen.
|
||||
(columns-not-equal-by-fn-when-non-nil-in-both :fk-field-name col-1 col-2)
|
||||
;;
|
||||
;; columns that don't have the same binning or temporal bucketing are never the same.
|
||||
;;
|
||||
;; same binning
|
||||
(columns-not-equal-by-fn :metabase.lib.field/binning col-1 col-2)
|
||||
;; same bucketing
|
||||
(when (columns-not-equal-by-fn (comp ignore-default-temporal-bucket lib.temporal-bucket/raw-temporal-bucket) col-1 col-2)
|
||||
'temporal-bucket)
|
||||
;; check `:inherited-temporal-unit` as well if both columns have it.
|
||||
(when (columns-not-equal-by-fn-when-non-nil-in-both (comp ignore-default-temporal-bucket :inherited-temporal-unit) col-1 col-2)
|
||||
:inherited-temporal-unit)
|
||||
;; finally make sure they have the same `:lib/source-column-alias` (if both columns have it) or `:name` (if for
|
||||
;; some reason they do not)
|
||||
(let [k (m/find-first (fn [k]
|
||||
(and (k col-1)
|
||||
(k col-2)))
|
||||
[:lib/source-column-alias :name])]
|
||||
(assert k "No key common to both columns")
|
||||
(columns-equal-by-fn k col-1 col-2))))
|
||||
(columns-not-equal-by-fn k col-1 col-2))))
|
||||
|
||||
(mu/defn- resolve-field-id-in-source-card :- ::lib.schema.metadata/column
|
||||
"Integer Field ID: get metadata from the metadata provider. If this is the first stage of the query, merge in
|
||||
Saved Question metadata if available.
|
||||
(defmethod = :metadata/column
|
||||
[col-1 col-2]
|
||||
(let [not-equal-reason (columns-not-equal-reason col-1 col-2)]
|
||||
(if not-equal-reason
|
||||
(log/debugf "Columns are not equal. Reason: %s" (pr-str not-equal-reason))
|
||||
(log/debug "Columns are equal."))
|
||||
(not not-equal-reason)))
|
||||
|
||||
This doesn't really have a good home. It's used here and by [[metabase.lib.field]], but because it depends on eg.
|
||||
[[metabase.lib.card]] and [[metabase.lib.convert]] it can't go in [[metabase.lib.metadata.calculation]]."
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
field-id :- ::lib.schema.id/field]
|
||||
(merge
|
||||
(when (lib.util/first-stage? query stage-number)
|
||||
(when-let [card-id (lib.util/source-card-id query)]
|
||||
(when-let [card-metadata (lib.card/saved-question-metadata query card-id)]
|
||||
(m/find-first #(clojure.core/= (:id %) field-id)
|
||||
card-metadata))))
|
||||
(try
|
||||
(lib.metadata/field query field-id)
|
||||
(catch #?(:clj Throwable :cljs :default) _
|
||||
nil))))
|
||||
(mr/def ::find-matching-column.options
|
||||
[:map
|
||||
{:closed true}
|
||||
[:find-matching-column/ignore-binning-and-bucketing? {:optional true, :default false} [:maybe :any]]
|
||||
;; TODO (Cam 9/4/25) -- this is sort of a hack, the only reason we really need it is that field resolution trips up
|
||||
;; and guesses the source for a ref sometimes, especially inside join conditions as we're building the join (i.e.,
|
||||
;; before we add appropriate join aliases). We don't want that to be a reason that we mark selected columns
|
||||
;; incorrectly. If field resolution never messed up, or if we came up with a better way of resolving columns in
|
||||
;; join conditions, we wouldn't need this key.
|
||||
;;
|
||||
;; PLEASE don't use this key outside of this namespace. That's why it starts with `-`. If you do I will hunt you
|
||||
;; down. -- Cam
|
||||
[:find-matching-column/-HACK-ignore-source? {:optional true, :default false} [:maybe :any]]])
|
||||
|
||||
(mu/defn- column-join-alias :- [:maybe :string]
|
||||
[column :- ::lib.schema.metadata/column]
|
||||
;; TODO (Cam 6/19/25) -- seems busted to be using joins that happened at ANY LEVEL previously for equality purposes
|
||||
;; so lightly but removing this breaks stuff. We should just remove this and do smarter matching like we do
|
||||
;; in [[plausible-matches-for-name-with-join-alias]] below.
|
||||
((some-fn :metabase.lib.join/join-alias :lib/original-join-alias :source-alias) column))
|
||||
(defn- find-matching-column-by-uuid
|
||||
"Implementation for [[find-matching-column]]; attempt to do a quick and dirty match using
|
||||
`:lib/uuid`/`:lib/source-uuid`.
|
||||
|
||||
(mu/defn- matching-join? :- :boolean
|
||||
[[_ref-kind {:keys [join-alias source-field source-field-name
|
||||
source-field-join-alias]} _ref-id] :- ::lib.schema.ref/ref
|
||||
column :- ::lib.schema.metadata/column]
|
||||
(if source-field
|
||||
(and (clojure.core/= source-field (:fk-field-id column))
|
||||
;; `source-field-name` is not available on old refs
|
||||
(or (nil? source-field-name) (clojure.core/= source-field-name (:fk-field-name column)))
|
||||
(clojure.core/= source-field-join-alias (:fk-join-alias column)))
|
||||
(clojure.core/= (column-join-alias column) join-alias)))
|
||||
This should never return a match that wouldn't be returned by [[find-matching-column-by-=]], and is thus here mainly
|
||||
as a performance optimization: theoretically it should be possible to remove this entirely and still have things
|
||||
work normally.
|
||||
|
||||
(defn- plausible-matches-for-name-no-join-alias [ref-name columns]
|
||||
(some (fn [k]
|
||||
(not-empty
|
||||
(filter #(= (k %) ref-name)
|
||||
columns)))
|
||||
[;; We SHOULD be using the source column alias (aka the desired column alias from the previous stage) so try
|
||||
;; that first.
|
||||
:lib/source-column-alias
|
||||
;; if that fails, maybe we're using the old QP results metadata deduplicated names, so look for matches with
|
||||
;; that.
|
||||
:lib/deduplicated-name
|
||||
;; if that fails, fall back to looking for matches using the old broken ambiguous `:name` key.
|
||||
:name]))
|
||||
TODO (Cam 9/4/25) -- I considered removing this and taking the performance hit to simplify the code but it seems
|
||||
like doing so actually causes test failures, which can only mean this does return matches that otherwise wouldn't
|
||||
have been considered [[=]]. We should spend some time investigating why this is happening and fix whatever is going
|
||||
on."
|
||||
[ref-or-col columns]
|
||||
(when-let [ref-or-col-uuid (if (map? ref-or-col)
|
||||
(:lib/source-uuid ref-or-col)
|
||||
(lib.options/uuid ref-or-col))]
|
||||
;; a `:field` ref should never match an `:expression` definition, etc. This can happen if an expression definition
|
||||
;; is a `:field` ref like
|
||||
;;
|
||||
;; [:field {:lib/expression-name "abc"} 123]
|
||||
;;
|
||||
;; in this case `returned-columns` will return metadata for expression `abc` when what you REALLY wanted was the
|
||||
;; previous-stage metadata for Field 123. See for
|
||||
;; example [[metabase.lib.remove-replace-test/remove-clause-adjust-ref-names-test]].
|
||||
;;
|
||||
;; We only need to filter these columns out here and not for [[find-matching-column-by-=]] because the `=` code is
|
||||
;; smart enough not to get tripped up by these anyway.
|
||||
(let [columns (filter (case (lib.dispatch/dispatch-value ref-or-col)
|
||||
:field #(not (#{:source/expressions :source/aggregations} (:lib/source %)))
|
||||
:expression #(= (:lib/source %) :source/expressions)
|
||||
:aggregation #(= (:lib/source %) :source/aggregations)
|
||||
:metadata/column identity)
|
||||
columns)
|
||||
matches (filter (fn [a-col]
|
||||
(clojure.core/= (:lib/source-uuid a-col) ref-or-col-uuid))
|
||||
columns)]
|
||||
;; if there are multiple potential matches then fall back to matching with `=` -- this can happen if we
|
||||
;; concatenate returned columns and visible columns together --
|
||||
;; see [[metabase.lib.equality-test/find-matching-column-prefer-exact-matches-test]] for more info
|
||||
(when (clojure.core/= (count matches) 1)
|
||||
(let [col (first matches)]
|
||||
(when-let [original-id (if (map? ref-or-col)
|
||||
(:id ref-or-col)
|
||||
(when (pos-int? (last ref-or-col))
|
||||
(last ref-or-col)))]
|
||||
(when-let [resolved-id (:id col)]
|
||||
(assert (= resolved-id original-id)
|
||||
(lib.util/format "Resolved column has different ID (%d) than original column (%d), despite having the same UUID (%s)!"
|
||||
resolved-id original-id (pr-str ref-or-col-uuid)))))
|
||||
col)))))
|
||||
|
||||
(defn- plausible-matches-for-name-with-join-alias [join-alias ref-name columns]
|
||||
;; first, look for matches for a join that came from the current stage -- `:metabase.lib.join/join-alias`; if we
|
||||
;; don't see any columns a match for that key, assume the join was from a previous stage and look at
|
||||
;; `:lib/original-join-alias` instead.
|
||||
(letfn [(plausible-matches [columns]
|
||||
(or
|
||||
;; ideally, the ref would be using the column name exported by the join... assuming `columns` is the set
|
||||
;; of columns exported by the stage as a whole, then the `source-column-alias` for these columns is the
|
||||
;; name exported by their source (i.e., this join)
|
||||
(not-empty
|
||||
(filter #(= (:lib/source-column-alias %) ref-name)
|
||||
columns))
|
||||
;; If we fail to find a match using source column alias, then look for a match using a deduplicated name
|
||||
;; RELATIVE to the join, not the WHOLE STAGE!!!! We need to recalculate these. Consider this case. If we
|
||||
;; have a stage that returns these three columns:
|
||||
;;
|
||||
;; | :id | :name | :join-alias | :deduplicated-name |
|
||||
;; |-----+-------+-------------+--------------------|
|
||||
;; | 1 | ID | | ID |
|
||||
;; | 2 | ID | J | ID_2 |
|
||||
;; | 3 | ID | J | ID_3 |
|
||||
;;
|
||||
;; which column does
|
||||
;;
|
||||
;; [:field {:join-alias "J"}, "ID_2"]
|
||||
;;
|
||||
;; refer to? The correct answer (IMO) is column 3, because `ID_2` and `ID_3` are 'exported' as as `ID`
|
||||
;; and `ID_2` by the join itself!
|
||||
(let [deduplicated-name->col (zipmap (map :lib/deduplicated-name (lib.field.util/add-deduplicated-names columns))
|
||||
columns)]
|
||||
(not-empty
|
||||
(keep (fn [[deduplicated-name col]]
|
||||
(when (clojure.core/= deduplicated-name ref-name)
|
||||
col))
|
||||
deduplicated-name->col)))
|
||||
;; if we failed to find a deduplicated name match then try to match on original name
|
||||
(not-empty
|
||||
(filter #(= (:lib/original-name %) ref-name)
|
||||
columns))
|
||||
;; and if THAT still fails fall back to broken `:name`.
|
||||
(not-empty
|
||||
(filter #(= (:name %) ref-name)
|
||||
columns))))]
|
||||
(when-let [columns-from-join (some (fn [k]
|
||||
(not-empty (filter #(= (k %) join-alias) columns)))
|
||||
[:metabase.lib.join/join-alias
|
||||
:lib/original-join-alias
|
||||
;; use the `:source-alias` key which was traditionally set by QP result
|
||||
;; metadata sometimes if neither one of the other keys had match(es)
|
||||
:source-alias])]
|
||||
(plausible-matches columns-from-join))))
|
||||
(defn- find-matching-column-update-col-for-options [col options]
|
||||
(cond-> col
|
||||
(:find-matching-column/ignore-binning-and-bucketing? options) (-> (lib.binning/with-binning nil)
|
||||
(lib.temporal-bucket/with-temporal-bucket nil))
|
||||
(:find-matching-column/-HACK-ignore-source? options) (dissoc :lib/source)))
|
||||
|
||||
(mu/defn- plausible-matches-for-name :- [:maybe [:sequential ::lib.schema.metadata/column]]
|
||||
[[_ref-kind opts ref-name :as _a-ref] :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]]
|
||||
(or (when-let [join-alias (:join-alias opts)]
|
||||
(or (plausible-matches-for-name-with-join-alias join-alias ref-name columns)
|
||||
;; if there's no match for a join then fall back to trying to match by ignoring the join alias.
|
||||
(do (log/warnf "Failed to find match for column %s with join alias %s, looking for match without join alias..."
|
||||
(pr-str ref-name)
|
||||
(pr-str join-alias))
|
||||
nil)))
|
||||
(plausible-matches-for-name-no-join-alias ref-name columns)))
|
||||
|
||||
(mu/defn- plausible-matches-for-id :- [:sequential ::lib.schema.metadata/column]
|
||||
[[_ref-kind opts ref-id :as a-ref] :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]
|
||||
generous? :- [:maybe :boolean]]
|
||||
(or (not-empty (filter #(and (clojure.core/= (:id %) ref-id)
|
||||
;; TODO: If the target ref has no join-alias, AND the source is card, the join alias on
|
||||
;; the column can be ignored. QP can set it when it shouldn't. See #33972.
|
||||
(or (and (not (:join-alias opts))
|
||||
(= (:lib/source %) :source/card))
|
||||
(matching-join? a-ref %)))
|
||||
columns))
|
||||
(when generous?
|
||||
(not-empty (filter #(clojure.core/= (:id %) ref-id) columns)))
|
||||
[]))
|
||||
|
||||
(defn- ambiguous-match-error [a-ref columns]
|
||||
(ex-info "Ambiguous match! Implement more logic in disambiguate-matches."
|
||||
{:ref a-ref
|
||||
:columns columns}))
|
||||
|
||||
(mu/defn- expression-column? [column]
|
||||
(or (clojure.core/= (:lib/source column) :source/expressions)
|
||||
(:lib/expression-name column)))
|
||||
|
||||
(mu/defn- disambiguate-matches-dislike-field-refs-to-expressions :- [:maybe ::lib.schema.metadata/column]
|
||||
"If a custom column is a simple wrapper for a field, that column gets `:id`, `:table_id`, etc.
|
||||
A custom column should get a ref like `[:expression {} \"expr name\"]`, not `[:field {} 17]`.
|
||||
If we got a `:field` ref, prefer matches which are not `:lib/source :source/expressions`."
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]]
|
||||
(or (when (clojure.core/= (first a-ref) :field)
|
||||
(when-let [non-exprs (not-empty (remove expression-column? columns))]
|
||||
(when-not (next non-exprs)
|
||||
(first non-exprs))))
|
||||
;; In all other cases, this is an ambiguous match.
|
||||
#_(throw (ambiguous-match-error a-ref columns))
|
||||
#?(:cljs (js/console.warn (ambiguous-match-error a-ref columns))
|
||||
:clj (log/warn (ambiguous-match-error a-ref columns)))))
|
||||
|
||||
(defn- matching-col-with-fn [columns col-fn]
|
||||
(let [matching-columns (filter col-fn columns)]
|
||||
(when (clojure.core/= (count matching-columns) 1)
|
||||
(first matching-columns))))
|
||||
|
||||
(mu/defn- disambiguate-matches-find-match-with-same-binning :- [:maybe ::lib.schema.metadata/column]
|
||||
"If there are multiple matching columns and `a-ref` has a binning value, check if only one column has that same
|
||||
binning."
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential {:min 2} ::lib.schema.metadata/column]]
|
||||
(or (let [binning (lib.binning/binning a-ref)]
|
||||
(matching-col-with-fn columns #(lib.binning/binning= (lib.binning/binning %) binning)))
|
||||
(when-let [original-binning (:lib/original-binning (lib.options/options a-ref))]
|
||||
(matching-col-with-fn columns #(lib.binning/binning= (:lib/original-binning %) original-binning)))
|
||||
(disambiguate-matches-dislike-field-refs-to-expressions a-ref columns)))
|
||||
|
||||
(mu/defn- disambiguate-matches-find-match-with-same-temporal-bucket :- [:maybe ::lib.schema.metadata/column]
|
||||
"If there are multiple matching columns and `a-ref` has a temporal bucket, check if only one column has that same
|
||||
unit."
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential {:min 2} ::lib.schema.metadata/column]]
|
||||
(or (let [bucket (lib.temporal-bucket/raw-temporal-bucket a-ref)]
|
||||
(matching-col-with-fn columns #(clojure.core/= (lib.temporal-bucket/raw-temporal-bucket %) bucket)))
|
||||
(when-let [inherited-bucket (:inherited-temporal-unit (lib.options/options a-ref))]
|
||||
(matching-col-with-fn columns #(clojure.core/= (:inherited-temporal-unit %) inherited-bucket)))
|
||||
(disambiguate-matches-find-match-with-same-binning a-ref columns)))
|
||||
|
||||
(mu/defn- disambiguate-matches-prefer-explicit :- [:maybe ::lib.schema.metadata/column]
|
||||
"Prefers table-default or explicitly joined columns over implicitly joinable ones."
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]]
|
||||
(if-let [no-implicit (not-empty (remove :fk-field-id columns))]
|
||||
(if-not (next no-implicit)
|
||||
(first no-implicit)
|
||||
(disambiguate-matches-find-match-with-same-temporal-bucket a-ref no-implicit))
|
||||
nil))
|
||||
|
||||
(mu/defn- disambiguate-matches-ignoring-join-alias :- [:maybe ::lib.schema.metadata/column]
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]]
|
||||
;; a-ref without :join-alias - if exactly one column has no :source-alias, that's the match.
|
||||
;; ignore the source alias on columns with :source/card
|
||||
(if-let [no-alias (not-empty (remove #(and (column-join-alias %)
|
||||
(not= (:lib/source %) :source/card))
|
||||
columns))]
|
||||
;; At least 1 matching column with no :source-alias.
|
||||
(if-not (next no-alias)
|
||||
(first no-alias)
|
||||
;; More than 1, keep digging.
|
||||
(disambiguate-matches-prefer-explicit a-ref no-alias))
|
||||
;; No columns are missing :source-alias - pass them all to the next stage.
|
||||
;; TODO: I'm not certain this one is sound, but it's necessary to make `lib.join/select-home-column` work as
|
||||
;; written. If this case causes issues, that logic may need rewriting.
|
||||
nil))
|
||||
|
||||
(mu/defn- disambiguate-matches :- [:maybe ::lib.schema.metadata/column]
|
||||
[a-ref :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]]
|
||||
(let [{:keys [join-alias]} (lib.options/options a-ref)]
|
||||
(or
|
||||
;; try to find matches with the same join alias (which might be `nil` for both).
|
||||
;;
|
||||
;; TODO (Cam 6/26/25) -- we should first try this using just the `:metabase.lib.join/join-alias` (join alias from
|
||||
;; this stage) and only then fall back to using `:lib/original-alias` and what not
|
||||
(when-let [matches (not-empty (filter #(clojure.core/= (column-join-alias %) join-alias) columns))]
|
||||
(if-not (next matches)
|
||||
(first matches)
|
||||
;; if there wasn't exactly 1 match then log a warning only if we had a join alias in the first place. Then
|
||||
;; try again ignoring join alias.
|
||||
(do
|
||||
(when join-alias
|
||||
(#?(:cljs js/console.warn :clj log/warn)
|
||||
"Multiple plausible matches with the same :join-alias - more disambiguation needed"
|
||||
{:ref a-ref
|
||||
:matches matches}))
|
||||
nil)))
|
||||
(disambiguate-matches-ignoring-join-alias a-ref columns))))
|
||||
|
||||
(def ^:private FindMatchingColumnOptions
|
||||
[:map [:generous? {:optional true} :boolean]])
|
||||
(defn- find-matching-column-by-=
|
||||
[query stage-number ref-or-col columns options]
|
||||
(when-let [col (if (map? ref-or-col)
|
||||
ref-or-col
|
||||
(u/prog1 (lib.metadata.calculation/metadata query stage-number ref-or-col)
|
||||
(log/debugf "Resolved ref\n%s\nto\n%s" (u/cprint-to-str ref-or-col) (u/cprint-to-str <>))))]
|
||||
(let [col (find-matching-column-update-col-for-options col options)]
|
||||
(m/find-first #(u/prog1 (= (find-matching-column-update-col-for-options % options) col)
|
||||
(log/debugf "Matches?\n%s\n=> %s" (u/cprint-to-str %) (pr-str <>)))
|
||||
columns))))
|
||||
|
||||
(mu/defn find-matching-column :- [:maybe ::lib.schema.metadata/column]
|
||||
"Given `a-ref-or-column` and a list of `columns`, finds the column that best matches this ref or column.
|
||||
"Find the column in `columns` that matches a `ref-or-col`. Matches with `:lib/uuid`/`:lib/source-uuid` if there is
|
||||
exactly one match, otherwise resolves the ref if needed and returns the first match with [[=]].
|
||||
|
||||
Matching is based on finding the basically plausible matches first. There is often zero or one plausible matches, and
|
||||
this can return quickly.
|
||||
For less-strict matching you can pass `{:find-matching-column/ignore-binning-and-bucketing? true}`."
|
||||
([query stage-number ref-or-col columns]
|
||||
(find-matching-column query stage-number ref-or-col columns nil))
|
||||
|
||||
If there are multiple plausible matches, they are disambiguated by the most important extra included in the `ref`.
|
||||
(`:join-alias` first, then `:temporal-unit`, etc.)
|
||||
|
||||
- Integer IDs in the `ref` are matched by ID; this usually is unambiguous.
|
||||
- If there are multiple joins on one table (including possible implicit joins), check `:join-alias` next.
|
||||
- If `a-ref` has a `:join-alias`, only a column which matches it can be the match, and it should be unique.
|
||||
- If `a-ref` doesn't have a `:join-alias`, prefer the column with no `:join-alias`, and prefer already selected
|
||||
columns over implicitly joinable ones.
|
||||
- There may be broken cases where the ref has an ID but the column does not. Therefore the ID must be resolved to a
|
||||
name or `:lib/desired-column-alias` and matched that way.
|
||||
- `query` and `stage-number` are required for this case, since they're needed to resolve the correct name.
|
||||
- Columns with `:id` set are dropped to prevent them matching. (If they didn't match by `:id` above they shouldn't
|
||||
match by name due to a coincidence of column names in different tables.)
|
||||
- String IDs are checked against `:lib/desired-column-alias` first.
|
||||
- If that doesn't match any columns, `:name` is compared next.
|
||||
- The same disambiguation (by `:join-alias` etc.) is applied if there are multiple plausible matches.
|
||||
|
||||
Returns the matching column, or nil if no match is found."
|
||||
([a-ref columns]
|
||||
(find-matching-column a-ref columns {}))
|
||||
|
||||
([[ref-kind _opts ref-id :as a-ref] :- ::lib.schema.ref/ref
|
||||
columns :- [:sequential ::lib.schema.metadata/column]
|
||||
{:keys [generous?]} :- FindMatchingColumnOptions]
|
||||
(case ref-kind
|
||||
;; Aggregations are referenced by the UUID of the column being aggregated.
|
||||
:aggregation (m/find-first #(and (clojure.core/= (:lib/source %) :source/aggregations)
|
||||
(clojure.core/= (:lib/source-uuid %) ref-id))
|
||||
columns)
|
||||
;; Expressions are referenced by name; fields by ID or name.
|
||||
(:expression
|
||||
:field) (let [plausible (if (string? ref-id)
|
||||
(plausible-matches-for-name a-ref columns)
|
||||
(plausible-matches-for-id a-ref columns generous?))]
|
||||
(case (count plausible)
|
||||
0 nil
|
||||
1 (first plausible)
|
||||
(disambiguate-matches a-ref plausible)))
|
||||
(throw (ex-info "Unknown type of ref" {:ref a-ref}))))
|
||||
|
||||
([query stage-number a-ref-or-column columns]
|
||||
(find-matching-column query stage-number a-ref-or-column columns {}))
|
||||
|
||||
([query :- [:maybe ::lib.schema/query]
|
||||
stage-number :- :int
|
||||
a-ref-or-column :- [:or ::lib.schema.metadata/column ::lib.schema.ref/ref]
|
||||
columns :- [:sequential ::lib.schema.metadata/column]
|
||||
opts :- FindMatchingColumnOptions]
|
||||
;; if we're matching against column metadata then we can try to use [[=]] to match instead of trying to do
|
||||
;; ref-based matching.
|
||||
(or
|
||||
(when (clojure.core/= (:lib/type a-ref-or-column) :metadata/column)
|
||||
(let [col a-ref-or-column
|
||||
matches (filter #(= % col) columns)]
|
||||
;; only return the match found this way if it is unambiguous.
|
||||
;;
|
||||
;; TODO (Cam 8/19/25) -- we should update this to disambiguate matches for these instead of falling back to
|
||||
;; the ref-based-resolution code, since finding matches between column metadata is more accurate
|
||||
(when (clojure.core/= (count matches) 1)
|
||||
(first matches))))
|
||||
(let [[ref-kind ref-opts ref-id :as a-ref] (if (lib.util/clause? a-ref-or-column)
|
||||
a-ref-or-column
|
||||
(lib.ref/ref a-ref-or-column))]
|
||||
(or (find-matching-column a-ref columns opts)
|
||||
;; Aggregations are matched by `:source-uuid` but if we're comparing old columns to new refs or vice versa
|
||||
;; the random UUIDs won't match up. This falls back to the `:lib/source-name` option on aggregation refs, if
|
||||
;; present.
|
||||
(when (and (clojure.core/= ref-kind :aggregation)
|
||||
(:lib/source-name ref-opts))
|
||||
(m/find-first #(and (clojure.core/= (:lib/source %) :source/aggregations)
|
||||
(clojure.core/= (:name %) (:lib/source-name ref-opts)))
|
||||
columns))
|
||||
;; We failed to match by ID, so try again with the column's name. Any columns with `:id` set are dropped.
|
||||
;; Why? Suppose there are two CREATED_AT columns in play - if one has an :id and it failed to match above, then
|
||||
;; it certainly shouldn't match by name just because of the coincidence of column names!
|
||||
(when (and query (number? ref-id))
|
||||
(when-let [no-id-columns (not-empty (remove :id columns))]
|
||||
(when-let [resolved (if (lib.util/clause? a-ref-or-column)
|
||||
(resolve-field-id-in-source-card query stage-number ref-id)
|
||||
a-ref-or-column)]
|
||||
(find-matching-column (-> (assoc a-ref 2 (or (:lib/desired-column-alias resolved)
|
||||
(:name resolved)))
|
||||
;; make sure the :field ref has a `:base-type`, it's against the rules for a
|
||||
;; nominal :field ref not to have a base-type -- this can fail schema
|
||||
;; validation if it's missing in the Field ID ref we generate the nominal ref
|
||||
;; from.
|
||||
(lib.options/update-options (partial merge {:base-type :type/*})))
|
||||
no-id-columns
|
||||
opts)))))))))
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
ref-or-col :- [:or
|
||||
::lib.schema.metadata/column
|
||||
::lib.schema.ref/ref]
|
||||
columns :- [:sequential ::lib.schema.metadata/column]
|
||||
options :- [:maybe ::find-matching-column.options]]
|
||||
(or (find-matching-column-by-uuid ref-or-col columns)
|
||||
(find-matching-column-by-= query stage-number ref-or-col columns options))))
|
||||
|
||||
(defn- ref-id-or-name [[_ref-kind _opts id-or-name]]
|
||||
id-or-name)
|
||||
@@ -536,16 +326,16 @@
|
||||
(mu/defn find-column-for-legacy-ref :- [:maybe ::lib.schema.metadata/column]
|
||||
"Like [[find-matching-column]], but takes a legacy MBQL reference. The name here is for consistency with other
|
||||
FE names for similar functions."
|
||||
([query legacy-ref metadatas]
|
||||
(find-column-for-legacy-ref query -1 legacy-ref metadatas))
|
||||
([query legacy-ref cols]
|
||||
(find-column-for-legacy-ref query -1 legacy-ref cols))
|
||||
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
legacy-ref :- :some
|
||||
metadatas :- [:maybe [:sequential ::lib.schema.metadata/column]]]
|
||||
(find-matching-column query stage-number (lib.convert/legacy-ref->pMBQL query stage-number legacy-ref) metadatas)))
|
||||
cols :- [:maybe [:sequential ::lib.schema.metadata/column]]]
|
||||
(find-matching-column query stage-number (lib.convert/legacy-ref->pMBQL query stage-number legacy-ref) cols)))
|
||||
|
||||
(defn mark-selected-columns
|
||||
(mu/defn mark-selected-columns
|
||||
"Mark `columns` as `:selected?` if they appear in `selected-columns-or-refs`. Uses fuzzy matching with
|
||||
[[find-matching-column]].
|
||||
|
||||
@@ -555,26 +345,32 @@
|
||||
;;
|
||||
;; return (visibile-columns query), but if any of those appear in `:fields`, mark then `:selected?`
|
||||
(mark-selected-columns (visible-columns query) (:fields stage))"
|
||||
([cols selected-columns-or-refs]
|
||||
(mark-selected-columns nil -1 cols selected-columns-or-refs))
|
||||
|
||||
([query stage-number cols selected-columns-or-refs]
|
||||
(when (> (count selected-columns-or-refs) (count cols))
|
||||
(log/errorf "[mark-selected-columns] There are more selected columns (%d) than there are total columns (%d)"
|
||||
(count selected-columns-or-refs) (count cols)))
|
||||
(when (seq cols)
|
||||
(let [matching-selected-cols (into #{}
|
||||
(keep (fn [selected-col-or-ref]
|
||||
(or (find-matching-column query stage-number selected-col-or-ref cols)
|
||||
(do
|
||||
(log/warnf "[mark-selected-columns] failed to find match for %s" (pr-str selected-col-or-ref))
|
||||
nil))))
|
||||
selected-columns-or-refs)]
|
||||
(when-not (clojure.core/= (count selected-columns-or-refs) (count matching-selected-cols))
|
||||
(log/warnf "[mark-selected-columns] %d refs are selected, but we found %d matches"
|
||||
(count selected-columns-or-refs)
|
||||
(count matching-selected-cols)))
|
||||
(mapv #(assoc % :selected? (contains? matching-selected-cols %)) cols)))))
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
cols :- [:sequential ::lib.schema.metadata/column]
|
||||
selected-columns-or-refs :- [:or
|
||||
[:sequential ::lib.schema.metadata/column]
|
||||
[:sequential ::lib.schema.ref/ref]]]
|
||||
(when (> (count selected-columns-or-refs) (count cols))
|
||||
(log/errorf "[mark-selected-columns] There are more selected columns (%d) than there are total columns (%d)"
|
||||
(count selected-columns-or-refs) (count cols)))
|
||||
(when (seq cols)
|
||||
(let [matching-selected-cols (into #{}
|
||||
(keep (fn [selected-col-or-ref]
|
||||
;; ignore temporal bucketing for marking columns as selected
|
||||
;; purposes (#32920)
|
||||
(or (find-matching-column query stage-number selected-col-or-ref cols
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true
|
||||
:find-matching-column/-HACK-ignore-source? true})
|
||||
(do
|
||||
(log/warnf "[mark-selected-columns] failed to find match for %s" (pr-str selected-col-or-ref))
|
||||
nil))))
|
||||
selected-columns-or-refs)]
|
||||
(when-not (clojure.core/= (count selected-columns-or-refs) (count matching-selected-cols))
|
||||
(log/warnf "[mark-selected-columns] %d refs are selected, but we found %d matches"
|
||||
(count selected-columns-or-refs)
|
||||
(count matching-selected-cols)))
|
||||
(mapv #(assoc % :selected? (contains? matching-selected-cols %)) cols))))
|
||||
|
||||
(mu/defn matching-column-sets? :- :boolean
|
||||
"Returns true if the provided `refs` is the same set as the provided `columns`.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
(ns metabase.lib.expression
|
||||
(:refer-clojure :exclude [+ - * / case coalesce abs time concat replace float])
|
||||
(:require
|
||||
[better-cond.core :as b]
|
||||
[clojure.string :as str]
|
||||
[medley.core :as m]
|
||||
[metabase.lib.common :as lib.common]
|
||||
[metabase.lib.field.util :as lib.field.util]
|
||||
[metabase.lib.hierarchy :as lib.hierarchy]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.metadata.calculation :as lib.metadata.calculation]
|
||||
@@ -15,6 +17,8 @@
|
||||
[metabase.lib.schema.expression :as lib.schema.expression]
|
||||
[metabase.lib.schema.expression.conditional :as lib.schema.expression.conditional]
|
||||
[metabase.lib.schema.expression.temporal :as lib.schema.expression.temporal]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.lib.schema.mbql-clause :as lib.schema.mbql-clause]
|
||||
[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]
|
||||
@@ -51,6 +55,48 @@
|
||||
(m/find-first (comp #{expression-name} lib.util/expression-name)
|
||||
(:expressions stage)))))
|
||||
|
||||
(mu/defn- -resolve-expression :- [:map
|
||||
[:stage-number [:maybe :int]]
|
||||
[:expression [:ref ::lib.schema.mbql-clause/clause]]
|
||||
[:card-id {:optional true} ::lib.schema.id/card]]
|
||||
[query stage-number expression-name :- :string]
|
||||
(b/cond
|
||||
:let [resolved (maybe-resolve-expression query stage-number expression-name)]
|
||||
resolved
|
||||
{:stage-number stage-number, :expression resolved}
|
||||
|
||||
:do
|
||||
(log/warnf "Expression %s does not exist in stage %d"
|
||||
(pr-str expression-name) (lib.util/canonical-stage-index query stage-number))
|
||||
|
||||
:let [previous-stage-number (lib.util/previous-stage-number query stage-number)]
|
||||
previous-stage-number
|
||||
(u/prog1 (-resolve-expression query previous-stage-number expression-name)
|
||||
(when <>
|
||||
(log/warn (u/format-color :red "Found expression %s in previous stage. Next time, use a :field name ref!"
|
||||
(pr-str expression-name)))))
|
||||
|
||||
:let [source-card-id (when (lib.util/first-stage? query stage-number)
|
||||
(lib.util/source-card-id query))
|
||||
resolved-in-source-card (when source-card-id
|
||||
(when-let [source-card (lib.metadata/card query source-card-id)]
|
||||
(-resolve-expression ((#?(:clj requiring-resolve :cljs resolve) 'metabase.lib.query/query)
|
||||
(lib.metadata/->metadata-provider query)
|
||||
(:dataset-query source-card))
|
||||
stage-number
|
||||
expression-name)))]
|
||||
resolved-in-source-card
|
||||
(do
|
||||
(log/warn (u/format-color :red "Found expression %s in source card %d. Next time, use a :field name ref!"
|
||||
(pr-str expression-name) source-card-id))
|
||||
(assoc resolved-in-source-card :stage-number nil, :card-id source-card-id))
|
||||
|
||||
:else
|
||||
(throw (ex-info (i18n/tru "No expression named {0}" (pr-str expression-name))
|
||||
{:expression-name expression-name
|
||||
:query query
|
||||
:stage-number stage-number}))))
|
||||
|
||||
(mu/defn resolve-expression :- ::lib.schema.expression/expression
|
||||
"Find the expression with `expression-name` in a given stage of a `query`, or throw an Exception if it doesn't
|
||||
exist."
|
||||
@@ -60,26 +106,7 @@
|
||||
([query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
expression-name :- ::lib.schema.common/non-blank-string]
|
||||
(or (maybe-resolve-expression query stage-number expression-name)
|
||||
(log/warnf "Expression %s does not exist in stage %d" (pr-str expression-name) (lib.util/canonical-stage-index query stage-number))
|
||||
(when-let [previous-stage-number (lib.util/previous-stage-number query stage-number)]
|
||||
(u/prog1 (resolve-expression query previous-stage-number expression-name)
|
||||
(when <>
|
||||
(log/warnf "Found expression %s in previous stage" (pr-str expression-name)))))
|
||||
(when (lib.util/first-stage? query stage-number)
|
||||
(when-let [source-card-id (lib.util/source-card-id query)]
|
||||
(when-let [source-card (lib.metadata/card query source-card-id)]
|
||||
(u/prog1 (resolve-expression ((#?(:clj requiring-resolve :cljs resolve) 'metabase.lib.query/query)
|
||||
(lib.metadata/->metadata-provider query)
|
||||
(:dataset-query source-card))
|
||||
expression-name)
|
||||
(when <>
|
||||
(log/warnf "Found expression %s in source card %d. Next time, use a :field name ref!"
|
||||
(pr-str expression-name) source-card-id))))))
|
||||
(throw (ex-info (i18n/tru "No expression named {0}" (pr-str expression-name))
|
||||
{:expression-name expression-name
|
||||
:query query
|
||||
:stage-number stage-number})))))
|
||||
(:expression (-resolve-expression query stage-number expression-name))))
|
||||
|
||||
(defmethod lib.metadata.calculation/type-of-method :expression
|
||||
[query stage-number [_expression _opts expression-name, :as _expression-ref]]
|
||||
@@ -88,21 +115,37 @@
|
||||
|
||||
(mu/defmethod lib.metadata.calculation/metadata-method :expression :- ::lib.metadata.calculation/visible-column
|
||||
[query stage-number [_expression opts expression-name, :as expression-ref-clause]]
|
||||
(merge {:lib/type :metadata/column
|
||||
;; TODO (Cam 8/7/25) -- is the source UUID of an expression ref supposed to be the ID of the ref, or the ID
|
||||
;; of the expression definition??
|
||||
:lib/source-uuid (:lib/uuid opts)
|
||||
:name expression-name
|
||||
:lib/expression-name expression-name
|
||||
:lib/source-column-alias expression-name
|
||||
:display-name (lib.metadata.calculation/display-name query stage-number expression-ref-clause)
|
||||
:base-type (lib.metadata.calculation/type-of query stage-number expression-ref-clause)
|
||||
:lib/source :source/expressions}
|
||||
(when-let [unit (lib.temporal-bucket/raw-temporal-bucket expression-ref-clause)]
|
||||
{:metabase.lib.field/temporal-unit unit})
|
||||
(when lib.metadata.calculation/*propagate-binning-and-bucketing*
|
||||
(when-let [unit (lib.temporal-bucket/raw-temporal-bucket expression-ref-clause)]
|
||||
{:inherited-temporal-unit unit}))))
|
||||
(let [{resolved-stage-number :stage-number
|
||||
resolved-expression :expression
|
||||
resolved-card-id :card-id} (-resolve-expression query stage-number expression-name)
|
||||
expr-type (or (:base-type opts)
|
||||
(lib.metadata.calculation/type-of query stage-number resolved-expression))
|
||||
col (merge {:lib/type :metadata/column
|
||||
:lib/source-uuid (lib.options/uuid resolved-expression)
|
||||
:name expression-name
|
||||
:lib/expression-name expression-name
|
||||
:lib/source-column-alias expression-name
|
||||
:display-name (or (:display-name opts)
|
||||
(lib.metadata.calculation/display-name query stage-number resolved-expression))
|
||||
:base-type expr-type
|
||||
:effective-type expr-type
|
||||
:lib/source :source/expressions}
|
||||
(when-let [converted-timezone (lib.util.match/match-one resolved-expression
|
||||
:convert-timezone
|
||||
(let [[_convert-timezone _opts _expr source-tz] &match]
|
||||
source-tz))]
|
||||
{:converted-timezone converted-timezone})
|
||||
(when-let [unit (lib.temporal-bucket/raw-temporal-bucket expression-ref-clause)]
|
||||
{:metabase.lib.field/temporal-unit unit})
|
||||
(when lib.metadata.calculation/*propagate-binning-and-bucketing*
|
||||
(when-let [unit (lib.temporal-bucket/raw-temporal-bucket expression-ref-clause)]
|
||||
{:inherited-temporal-unit unit})))]
|
||||
(cond-> col
|
||||
(not= resolved-stage-number stage-number)
|
||||
lib.field.util/update-keys-for-col-from-previous-stage
|
||||
|
||||
resolved-card-id
|
||||
(assoc :lib/source :source/card, :lib/card-id resolved-card-id))))
|
||||
|
||||
(defmethod lib.temporal-bucket/available-temporal-buckets-method :expression
|
||||
[query stage-number [_expression opts _expr-name, :as expr-clause]]
|
||||
|
||||
@@ -132,9 +132,6 @@
|
||||
stage-number
|
||||
{join-alias :metabase.lib.join/join-alias
|
||||
original-join-alias :lib/original-join-alias
|
||||
;; TODO (Cam 6/19/25) -- `:source-alias` is deprecated, see description for column metadata
|
||||
;; schema. Still getting set/used in a few places tho. Work on removing it altogether.
|
||||
source-alias :source-alias
|
||||
fk-field-id :fk-field-id
|
||||
original-fk-field-id :lib/original-fk-field-id
|
||||
table-id :table-id
|
||||
@@ -142,8 +139,7 @@
|
||||
style
|
||||
display-name]
|
||||
(let [join-alias (or join-alias
|
||||
original-join-alias
|
||||
source-alias)
|
||||
original-join-alias)
|
||||
fk-field-id (or fk-field-id original-fk-field-id)
|
||||
join-display-name (when (and (= style :long)
|
||||
;; don't prepend a join display name if `:display-name` already contains one! Legacy
|
||||
@@ -419,19 +415,6 @@
|
||||
options (merge {:lib/uuid (str (random-uuid))
|
||||
:effective-type (column-metadata-effective-type metadata)}
|
||||
(select-renamed-keys metadata field-ref-propagated-keys)
|
||||
;; MEGA HACK! QP result metadata includes `:source-alias` (which is basically any
|
||||
;; join alias that was ever used for the column); if that is present then we need to
|
||||
;; generate field refs that use as a join alias because even tho that sounds
|
||||
;; completely broken that is traditionally what we've done. Taking this out
|
||||
;; breakouts [[metabase.lib.drill-thru.column-filter-test/column-filter-join-alias-test]].
|
||||
;;
|
||||
;; TODO (Cam 6/26/25) -- figure out if we can actually take this out or not.
|
||||
(when-let [source-alias (and (not inherited-column?)
|
||||
(not (:fk-field-id metadata))
|
||||
(not= :source/implicitly-joinable
|
||||
(:lib/source metadata))
|
||||
(:source-alias metadata))]
|
||||
{:join-alias source-alias})
|
||||
(when-not inherited-column?
|
||||
(select-renamed-keys metadata field-ref-propagated-keys-for-non-inherited-columns)))
|
||||
id-or-name (or (lib.field.util/inherited-column-name metadata)
|
||||
@@ -443,8 +426,7 @@
|
||||
(case source
|
||||
:source/aggregations (lib.aggregation/column-metadata->aggregation-ref metadata)
|
||||
:source/expressions (lib.expression/column-metadata->expression-ref metadata)
|
||||
#_else
|
||||
(column-metadata->field-ref metadata)))
|
||||
#_else (column-metadata->field-ref metadata)))
|
||||
|
||||
(defn- expression-columns
|
||||
"Return the [[::lib.schema.metadata/column]] for all the expressions in a stage of a query."
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.schema.ref :as lib.schema.ref]
|
||||
[metabase.lib.util :as lib.util]
|
||||
[metabase.lib.util.log :as lib.util.log]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.log :as log]
|
||||
[metabase.util.malli :as mu]
|
||||
@@ -67,7 +68,7 @@
|
||||
[metadata-providerable :- ::lib.schema.metadata/metadata-providerable
|
||||
table-id :- [:maybe ::lib.schema.id/table]
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving Field %s from metadata provider" (pr-str id-or-name))
|
||||
(log/debugf "Resolving %s from metadata provider" (lib.util.log/format-field metadata-providerable id-or-name))
|
||||
(when-some [col (cond
|
||||
(pos-int? id-or-name)
|
||||
(lib.metadata/field metadata-providerable id-or-name)
|
||||
@@ -109,21 +110,25 @@
|
||||
;; `id-or-name` is an ID
|
||||
(or (m/find-first #(= (:id %) id-or-name) cols)
|
||||
(do
|
||||
(log/debugf "Failed to find column in metadata with ID %s" (pr-str id-or-name))
|
||||
(log/debugf "Failed to find %s in metadata" (lib.util.log/format-field metadata-providerable id-or-name))
|
||||
(when-some [field (lib.metadata/field metadata-providerable id-or-name)]
|
||||
(log/debugf "Looking for match in metadata with name %s" (pr-str (:name field)))
|
||||
(log/debugf "Looking for match in metadata for %s" (lib.util.log/format-field metadata-providerable field))
|
||||
(when-some [col (resolve* (:name field))]
|
||||
;; don't return a match that is definitely for a different column (has an ID, but it's for a
|
||||
;; different column)
|
||||
(when (or (not (:id col))
|
||||
(= (:id col) id-or-name))
|
||||
col)))))))]
|
||||
(cond
|
||||
(= (:id col) id-or-name)
|
||||
col
|
||||
;; if resolved col is missing ID, merge in metadata from the metadata provider
|
||||
(not (:id col))
|
||||
(merge-metadata (lib.metadata/field metadata-providerable id-or-name)
|
||||
col))))))))]
|
||||
(u/prog1 (resolve* id-or-name)
|
||||
(if <>
|
||||
(log/debugf "Found match %s"
|
||||
(pr-str (select-keys <> [:id :lib/desired-column-alias :lib/deduplicated-name])))
|
||||
(log/debugf "Failed to find match for %s. Found:\n%s"
|
||||
(pr-str id-or-name)
|
||||
(lib.util.log/format-field metadata-providerable id-or-name)
|
||||
(u/pprint-to-str (map #(select-keys % [:id :lib/desired-column-alias :lib/deduplicated-name])
|
||||
cols)))))))
|
||||
|
||||
@@ -297,18 +302,24 @@
|
||||
join-alias :- ::lib.schema.join/alias
|
||||
source-field :- [:maybe ::lib.schema.id/field]
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving %s (join alias = %s) in joins in stage %s" (pr-str id-or-name) (pr-str join-alias) (pr-str stage-number))
|
||||
(log/debugf "Resolving %s (join alias = %s) in joins in stage %d"
|
||||
(lib.util.log/format-field query id-or-name)
|
||||
(pr-str join-alias)
|
||||
stage-number)
|
||||
;; find the matching join.
|
||||
(let [stage (lib.util/query-stage query stage-number)]
|
||||
(if-some [join (m/find-first #(= (:alias %) join-alias)
|
||||
(:joins stage))]
|
||||
;; found matching join at this stage
|
||||
(do
|
||||
(log/debugf "Resolving %s in join %s in stage %s"
|
||||
(pr-str id-or-name)
|
||||
(log/debugf "Resolving %s in join %s in stage %d"
|
||||
(lib.util.log/format-field query id-or-name)
|
||||
(pr-str join-alias)
|
||||
(pr-str stage-number))
|
||||
(let [join-cols (cond->> (lib.join/join-returned-columns-relative-to-parent-stage query stage-number join nil)
|
||||
stage-number)
|
||||
;; TODO (Cam 9/4/25) -- we should almost certainly be passing `{:include-remaps? true}` everywhere we call
|
||||
;; some variant of `returned-columns` in this namespace, or better yet forwarding it somehow from calls in
|
||||
;; `returned-columns`. But just doing it here for now will have to do.
|
||||
(let [join-cols (cond->> (lib.join/join-returned-columns-relative-to-parent-stage query stage-number join {:include-remaps? true})
|
||||
source-field (remove (fn [col]
|
||||
(when-some [col-source-field ((some-fn :fk-field-id :lib/original-fk-field-id) col)]
|
||||
(not= col-source-field source-field)))))]
|
||||
@@ -319,9 +330,7 @@
|
||||
;; a join with this alias does not exist at this stage of the query... try looking recursively in previous
|
||||
;; stage(s)
|
||||
(do
|
||||
(log/debugf "Join %s does not exist in stage %s, looking in previous stages"
|
||||
(pr-str join-alias)
|
||||
(pr-str stage-number))
|
||||
(log/debugf "Join %s does not exist in stage %d, looking in previous stages" (pr-str join-alias) stage-number)
|
||||
(if-some [source-cols (or (when-some [previous-stage-number (lib.util/previous-stage-number query stage-number)]
|
||||
(lib.metadata.calculation/returned-columns query previous-stage-number))
|
||||
(when-some [source-card-id (:source-card (lib.util/query-stage query stage-number))]
|
||||
@@ -423,14 +432,14 @@
|
||||
(let [[join join-stage-number] (find-reified-implicit-join-with-fk-field-id query stage-number source-field-id)]
|
||||
(when (and join-stage-number
|
||||
(not= join-stage-number stage-number))
|
||||
(log/errorf (str "Field ref %s in stage %s specifies :source-field-id %s, but we found the implicit join %s in"
|
||||
" earlier stage %s, which doesn't return this column. Query almost certainly won't work"
|
||||
(log/errorf (str "Field ref %s in stage %d specifies :source-field-id %s, but we found the implicit join %s in"
|
||||
" earlier stage %d, which doesn't return this column. Query almost certainly won't work"
|
||||
" correctly.")
|
||||
(pr-str id-or-name)
|
||||
(pr-str stage-number)
|
||||
(pr-str source-field-id)
|
||||
(lib.util.log/format-field query id-or-name)
|
||||
stage-number
|
||||
(lib.util.log/format-field query source-field-id)
|
||||
(pr-str (:alias join))
|
||||
(pr-str join-stage-number))
|
||||
join-stage-number)
|
||||
(when-some [col (resolve-in-implicit-join-current-stage query source-field-id id-or-name)]
|
||||
(-> col
|
||||
lib.field.util/update-keys-for-col-from-previous-stage
|
||||
@@ -445,8 +454,8 @@
|
||||
stage-number :- :int
|
||||
source-field-id :- ::lib.schema.id/field
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving implicitly joined %s (source Field ID = %s) in stage %s"
|
||||
(pr-str id-or-name) (pr-str source-field-id) (pr-str stage-number))
|
||||
(log/debugf "Resolving implicitly joined %s (source Field ID = %s) in stage %d"
|
||||
(lib.util.log/format-field query id-or-name) (pr-str source-field-id) stage-number)
|
||||
(or (resolve-in-implicit-join-previous-stage query stage-number source-field-id id-or-name)
|
||||
(resolve-unreturned-column-in-reified-implicit-join-in-previous-stage query stage-number source-field-id id-or-name)
|
||||
;; if there is no previous stage or we were unable to find the column in a previous stage then that means the
|
||||
@@ -457,7 +466,7 @@
|
||||
[query :- ::lib.schema/query
|
||||
previous-stage-number :- :int
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving %s in previous stage returned columns" (pr-str id-or-name))
|
||||
(log/debugf "Resolving %s in previous stage returned columns" (lib.util.log/format-field query id-or-name))
|
||||
(when-some [previous-stage-columns (lib.metadata.calculation/returned-columns query previous-stage-number)]
|
||||
(resolve-in-previous-stage-metadata-and-update-keys query previous-stage-columns id-or-name)))
|
||||
|
||||
@@ -465,7 +474,7 @@
|
||||
[query :- ::lib.schema/query
|
||||
source-card-id :- ::lib.schema.id/card
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving %s in source Card %s metadata" (pr-str id-or-name) (pr-str source-card-id))
|
||||
(log/debugf "Resolving %s in source Card %s metadata" (lib.util.log/format-field query id-or-name) (pr-str source-card-id))
|
||||
(when-some [card (lib.metadata/card query source-card-id)]
|
||||
(let [card-metadata-columns (lib.metadata.calculation/returned-columns query card)]
|
||||
(when-some [col (resolve-in-previous-stage-metadata-and-update-keys query card-metadata-columns id-or-name)]
|
||||
@@ -476,7 +485,7 @@
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving %s in current stage metadata" (pr-str id-or-name))
|
||||
(log/debugf "Resolving %s in current stage metadata" (lib.util.log/format-field query id-or-name))
|
||||
(let [stage (lib.util/query-stage query stage-number)]
|
||||
(if-some [current-stage-metadata-columns (not-empty (get-in stage [:lib/stage-metadata :columns]))]
|
||||
;; TODO (Cam 8/7/25) -- sort of a hacc to add deduplicated names here just so we can properly
|
||||
@@ -495,11 +504,11 @@
|
||||
(when-some [source-card-id (:source-card (lib.util/query-stage query stage-number))]
|
||||
(resolve-in-card-returned-columns query source-card-id id-or-name)))
|
||||
|
||||
(defn- fallback-metadata [id-or-name]
|
||||
(defn- fallback-metadata [metadata-providerable id-or-name]
|
||||
(log/warn (u/format-color :red
|
||||
(str "We tried every trick we could think of and still failed to resolve a field"
|
||||
" ref. If the query doesn't work, this is why. Returning fallback metadata for %s")
|
||||
(pr-str id-or-name)))
|
||||
(lib.util.log/format-field metadata-providerable id-or-name)))
|
||||
(merge
|
||||
{:lib/type :metadata/column
|
||||
;; guess that the column came from the previous stage
|
||||
@@ -552,7 +561,7 @@
|
||||
(mu/defn- resolve-ref-missing-join-alias :- [:maybe ::lib.metadata.calculation/visible-column]
|
||||
"Try finding a match in joins (field ref is missing `:join-alias`)."
|
||||
[query stage-number id-or-name]
|
||||
(log/debugf "Assuming %s is from a join, and missing :join-alias" (pr-str id-or-name))
|
||||
(log/debugf "Assuming %s is from a join, and missing :join-alias" (lib.util.log/format-field query id-or-name))
|
||||
(or (when (string? id-or-name)
|
||||
(let [parts (str/split id-or-name #"__" 2)]
|
||||
(when (= (count parts) 2)
|
||||
@@ -564,17 +573,55 @@
|
||||
(resolve-in-join query stage-number (:alias join) nil id-or-name))
|
||||
(:joins (lib.util/query-stage query stage-number)))
|
||||
(do
|
||||
(log/debugf "Failed to find a match in one of the query's joins in stage %s" (pr-str stage-number))
|
||||
(log/debugf "Failed to find a match in one of the query's joins in stage %d" stage-number)
|
||||
nil)))
|
||||
|
||||
;;; This is mostly needed because of really messed up test cases in [[metabase.lib.drill-thru.test-util.canned]], see
|
||||
;;; for example `:test.query/orders-count-by-created-at-and-product-category`
|
||||
(defn- fallback-metadata-for-demonic-field-id-from-different-source-table
|
||||
[query stage-number col]
|
||||
(when-let [source-table-id (lib.util/source-table-id query)]
|
||||
(log/warn (u/format-color :red (str "Field ref for %s in stage %d is from a different source table (%s) than"
|
||||
" the query :source-table (%s), but it is missing :join-alias and"
|
||||
" :source-field. This is not and has never been supported, but we will do"
|
||||
" our best to make the query work anyway. If it doesn't work, this is"
|
||||
" probably why. Refs like these might not work in the future.")
|
||||
(lib.util.log/format-field query col)
|
||||
stage-number
|
||||
(lib.util.log/format-table query (:table-id col))
|
||||
(lib.util.log/format-table query source-table-id)))
|
||||
(if-let [col (m/find-first (fn [implicit-col]
|
||||
(and (= (:id implicit-col) (:id col))
|
||||
(:fk-field-id implicit-col)))
|
||||
(lib.metadata.calculation/implicitly-joinable-columns
|
||||
query
|
||||
(lib.metadata.calculation/returned-columns query (lib.metadata/table query source-table-id))))]
|
||||
{:lib/source :source/implicitly-joinable
|
||||
:fk-field-id (:fk-field-id col)}
|
||||
(do
|
||||
(log/errorf "Failed to find a way to implicitly join %s" (lib.util.log/format-field query col))
|
||||
nil))))
|
||||
|
||||
(defn- fallback-metadata-for-field [query stage-number id-or-name]
|
||||
(let [source-table-id (:source-table (lib.util/query-stage query stage-number))]
|
||||
(when-some [col (field-metadata query source-table-id id-or-name)]
|
||||
(assoc col
|
||||
::fallback-metadata? true
|
||||
:lib/source (if (zero? (lib.util/canonical-stage-index query stage-number))
|
||||
:source/table-defaults
|
||||
:source/previous-stage)))))
|
||||
(when-some [col (field-metadata query (:source-table (lib.util/query-stage query stage-number)) id-or-name)]
|
||||
(merge-metadata
|
||||
col
|
||||
{::fallback-metadata? true}
|
||||
(b/cond
|
||||
(not (lib.util/first-stage? query stage-number))
|
||||
{:lib/source :source/previous-stage}
|
||||
|
||||
:let [source-card-id (lib.util/source-card-id query)]
|
||||
source-card-id
|
||||
{:lib/source :source/card, :lib/card-id source-card-id}
|
||||
|
||||
:when-let [source-table-id (lib.util/source-table-id query)]
|
||||
|
||||
(= (:table-id col) source-table-id)
|
||||
{:lib/source :source/table-defaults}
|
||||
|
||||
:else
|
||||
(fallback-metadata-for-demonic-field-id-from-different-source-table query stage-number col)))))
|
||||
|
||||
(defn- maybe-resolve-expression-in-current-stage [query stage-number id-or-name]
|
||||
(when (string? id-or-name)
|
||||
@@ -583,7 +630,7 @@
|
||||
(str "Resolved field %s to an expression. Please remember to use :expression references"
|
||||
" for expressions in the current stage -- using a :field ref is unsupported and may"
|
||||
" not be allowed in the future.")
|
||||
(pr-str id-or-name)))
|
||||
(lib.util.log/format-field query id-or-name)))
|
||||
(-> (lib.expression/expression-metadata query stage-number expr)
|
||||
(assoc :lib/source-column-alias id-or-name)))))
|
||||
|
||||
@@ -591,10 +638,12 @@
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
id-or-name :- ::id-or-name]
|
||||
(log/debugf "Resolving %s from previous stage, source table, or source card" (pr-str id-or-name))
|
||||
(log/debugf "Resolving %s from previous stage, source table, or source card" (lib.util.log/format-field query id-or-name))
|
||||
(let [col (or (resolve-from-previous-stage-or-source* query stage-number id-or-name)
|
||||
(do
|
||||
(log/debugf "Failed to resolve Field %s in stage %s. Trying other methods..." (pr-str id-or-name) (pr-str stage-number))
|
||||
(log/debugf "Failed to resolve Field %s in stage %d. Trying other methods..."
|
||||
(lib.util.log/format-field query id-or-name)
|
||||
stage-number)
|
||||
(resolve-ref-missing-join-alias query stage-number id-or-name))
|
||||
;; if we haven't found a match yet try getting metadata from the metadata provider if this is a
|
||||
;; Field ID ref. It's likely a ref that makes little or no sense (e.g. wrong table) but we can
|
||||
@@ -604,7 +653,7 @@
|
||||
;; expression.
|
||||
(maybe-resolve-expression-in-current-stage query stage-number id-or-name)
|
||||
;; if we STILL can't find a match, return made-up fallback metadata.
|
||||
(fallback-metadata id-or-name))]
|
||||
(fallback-metadata query id-or-name))]
|
||||
(merge-metadata
|
||||
col
|
||||
(when col
|
||||
@@ -619,7 +668,7 @@
|
||||
[_tag {:keys [source-field join-alias], :as opts} id-or-name, :as #?(:clj field-ref :cljs _field-ref)] :- :mbql.clause/field]
|
||||
;; this is just for easier debugging
|
||||
(let [stage-number (lib.util/canonical-stage-index query stage-number)]
|
||||
(log/debugf "Resolving %s in stage %s" (pr-str id-or-name) (pr-str stage-number))
|
||||
(log/debugf "Resolving %s in stage %d" (lib.util.log/format-field query id-or-name) stage-number)
|
||||
(-> (merge-metadata
|
||||
{:lib/type :metadata/column}
|
||||
(or (when join-alias
|
||||
@@ -629,7 +678,7 @@
|
||||
(resolve-from-previous-stage-or-source query stage-number id-or-name)
|
||||
(merge
|
||||
(or (fallback-metadata-for-field query stage-number id-or-name)
|
||||
(fallback-metadata id-or-name))
|
||||
(fallback-metadata query id-or-name))
|
||||
(when (and join-alias
|
||||
(contains? (into #{}
|
||||
(map :alias)
|
||||
|
||||
@@ -555,7 +555,7 @@
|
||||
legacy-ref :- some?]
|
||||
(let [a-ref (lib.convert/legacy-ref->pMBQL query stage-number legacy-ref)
|
||||
columns (filterable-columns query stage-number)]
|
||||
(lib.equality/find-matching-column a-ref columns))))
|
||||
(lib.equality/find-matching-column query stage-number a-ref columns))))
|
||||
|
||||
(def ^:private FilterParts
|
||||
[:map
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
[metabase.util.malli.registry :as mr]
|
||||
[metabase.util.time :as u.time]))
|
||||
|
||||
(defn- is-ref-for-column? [expr column]
|
||||
(defn- is-ref-for-column? [query stage-number expr column]
|
||||
(and (lib.util/clause-of-type? expr :field)
|
||||
(lib.equality/find-matching-column expr [column])))
|
||||
(lib.equality/find-matching-column query stage-number expr [column])))
|
||||
|
||||
(defn- contains-ref-for-column? [expr column]
|
||||
(defn- contains-ref-for-column? [query stage-number expr column]
|
||||
(letfn [(ref-for-column? [expr]
|
||||
(is-ref-for-column? expr column))]
|
||||
(is-ref-for-column? query stage-number expr column))]
|
||||
(lib.util.match/match-lite-recursive expr
|
||||
(x :guard ref-for-column?) true)))
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
column :- ::lib.schema.metadata/column
|
||||
matches? :- fn?]
|
||||
matches? :- [:=>
|
||||
[:cat ::lib.schema/query #_stage-number :int #_expr :any ::lib.schema.metadata/column]
|
||||
:any]]
|
||||
(reduce
|
||||
(fn [query [_tag _opts expr :as filter-clause]]
|
||||
(if (matches? expr column)
|
||||
(if (matches? query stage-number expr column)
|
||||
(lib.remove-replace/remove-clause query stage-number filter-clause)
|
||||
query))
|
||||
query
|
||||
@@ -142,7 +144,7 @@
|
||||
([{:keys [query]}]
|
||||
query)
|
||||
([{:keys [query has-seen-column?], :as m} breakout]
|
||||
(if (is-ref-for-column? breakout column)
|
||||
(if (is-ref-for-column? query stage-number breakout column)
|
||||
(let [query' (if has-seen-column?
|
||||
;; already seen a breakout for this column: remove other breakouts.
|
||||
(lib.remove-replace/remove-clause query stage-number breakout)
|
||||
|
||||
@@ -252,15 +252,12 @@
|
||||
join-alias :- ::lib.schema.join/alias]
|
||||
(-> col
|
||||
(assoc
|
||||
;; TODO (Cam 6/19/25) -- we need to get rid of `:source-alias` it's just causing confusion; don't need two
|
||||
;; keys for join aliases.
|
||||
:source-alias join-alias
|
||||
:lib/original-join-alias join-alias
|
||||
:lib/source :source/joins
|
||||
::join-alias join-alias
|
||||
:lib/original-name ((some-fn :lib/original-name :name) col)
|
||||
:lib/original-display-name (or (:lib/original-display-name col)
|
||||
(lib.metadata.calculation/display-name query stage-number (dissoc col ::join-alias :lib/original-join-alias :source-alias))))
|
||||
(lib.metadata.calculation/display-name query stage-number (dissoc col ::join-alias :lib/original-join-alias))))
|
||||
(set/rename-keys {:lib/expression-name :lib/original-expression-name})
|
||||
(as-> $col (assoc $col :display-name (lib.metadata.calculation/display-name query stage-number $col)))))
|
||||
|
||||
@@ -340,7 +337,7 @@
|
||||
identity))
|
||||
(lib.metadata.calculation/returned-columns query stage-number join options))))
|
||||
|
||||
(mu/defn join-fields-to-add-to-parent-stage :- [:maybe [:sequential ::lib.metadata.calculation/column-metadata-with-source]]
|
||||
(mu/defn join-fields-to-add-to-parent-stage :- [:maybe ::lib.metadata.calculation/visible-columns]
|
||||
"The resolved `:fields` from a join, which we automatically append to the parent stage's `:fields`."
|
||||
[query
|
||||
stage-number
|
||||
@@ -358,10 +355,14 @@
|
||||
;; for an example of where this happens. Having it here will cause `lib.equality` to fail to
|
||||
;; find a match.
|
||||
;;
|
||||
;; TODO (Cam 9/4/25) -- not sure if this is still true after the recent `lib.equality` rewrite.
|
||||
;;
|
||||
;; TODO (Cam 8/29/25) -- investigate how this is happening and consider whether Join `:fields`
|
||||
;; should disallow refs with `:source-field`/remove it automatically.
|
||||
:let [field-ref (lib.options/update-options field-ref dissoc :source-field)
|
||||
match (or (lib.equality/find-matching-column field-ref cols)
|
||||
match (or (lib.equality/find-matching-column query stage-number field-ref cols
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true})
|
||||
|
||||
(when-let [resolved (lib.metadata.calculation/metadata (-> (assoc query :stages (:stages join))
|
||||
lib.stage.util/append-stage)
|
||||
(with-join-alias field-ref nil))]
|
||||
@@ -515,10 +516,10 @@
|
||||
(u/assoc-dissoc joinable :fields fields)))
|
||||
|
||||
(defn- select-home-column
|
||||
[home-cols cond-fields]
|
||||
[query stage-number home-cols cond-fields]
|
||||
(when (seq cond-fields)
|
||||
(let [cond-home-cols (keep #(lib.equality/find-matching-column % home-cols) cond-fields)]
|
||||
;; first choice: the leftmost FK or PK in the condition referring to a home column
|
||||
(let [cond-home-cols (keep #(lib.equality/find-matching-column query stage-number % home-cols) cond-fields)]
|
||||
;; first choice: the leftmost FK or PK in the condition referring to a home column
|
||||
(or (m/find-first (some-fn lib.types.isa/foreign-key? lib.types.isa/primary-key?) cond-home-cols)
|
||||
;; otherwise the leftmost home column in the condition
|
||||
(first cond-home-cols)
|
||||
@@ -587,10 +588,15 @@
|
||||
(cond
|
||||
;; no sides obviously belong to joined
|
||||
(not (or lhs-alias rhs-alias))
|
||||
(if (lib.equality/find-matching-column query stage-number rhs home-cols)
|
||||
[op op-opts (with-join-alias lhs join-alias) rhs]
|
||||
[op op-opts lhs (with-join-alias rhs join-alias)])
|
||||
|
||||
(let [potential-home-cols (cond-> #{}
|
||||
(lib.equality/find-matching-column query stage-number lhs home-cols) (conj :lhs)
|
||||
(lib.equality/find-matching-column query stage-number rhs home-cols) (conj :rhs))]
|
||||
;; if there's still any ambiguity as to whether LHS or RHS is the home column, assume LHS. Only use RHS if
|
||||
;; it's a potential home column and LHS is not.
|
||||
(case potential-home-cols
|
||||
(#{:lhs :rhs} #{:lhs} #{}) [op op-opts lhs (with-join-alias rhs join-alias)]
|
||||
#{:rhs} [op op-opts (with-join-alias lhs join-alias) rhs]))
|
||||
;; both sides seem to belong to joined assuming this resulted from
|
||||
;; overly fuzzy matching, we remove the join alias from the LHS
|
||||
;; unless the RHS seems to belong to home too while the LHS doesn't
|
||||
@@ -620,7 +626,7 @@
|
||||
home-cols (lib.metadata.calculation/visible-columns query stage-number)]
|
||||
(default-alias query stage-number a-join stage home-cols)))
|
||||
|
||||
([query _stage-number a-join stage home-cols]
|
||||
([query stage-number a-join stage home-cols]
|
||||
(let [home-cols home-cols
|
||||
cond-fields (into []
|
||||
(mapcat (fn [condition]
|
||||
@@ -629,7 +635,7 @@
|
||||
(when (and (lib.util/field-clause? lhs) (lib.util/field-clause? rhs))
|
||||
[lhs rhs]))))
|
||||
(:conditions a-join))
|
||||
home-col (select-home-column home-cols cond-fields)]
|
||||
home-col (select-home-column query stage-number home-cols cond-fields)]
|
||||
(-> (calculate-join-alias query a-join home-col)
|
||||
(generate-unique-name (keep :alias (:joins stage)))))))
|
||||
|
||||
@@ -819,7 +825,9 @@
|
||||
columns)]
|
||||
(concat pk fk other)))
|
||||
|
||||
(defn- mark-selected-column [query stage-number existing-column-or-nil columns]
|
||||
(defn- mark-selected-column
|
||||
"Mark selected columns in join conditions."
|
||||
[query stage-number existing-column-or-nil columns]
|
||||
(if-not existing-column-or-nil
|
||||
columns
|
||||
(mapv (fn [column]
|
||||
@@ -879,8 +887,7 @@
|
||||
(contains? join-aliases-to-ignore col-join-alias))))
|
||||
(mark-selected-column query
|
||||
stage-number
|
||||
(when (or (lib.util/clause-of-type? lhs-expression-or-nil :field)
|
||||
(lib.util/clause-of-type? lhs-expression-or-nil :expression))
|
||||
(when (lib.util/clause-of-type? lhs-expression-or-nil #{:field :expression})
|
||||
lhs-expression-or-nil))
|
||||
sort-join-condition-columns))))
|
||||
|
||||
@@ -1035,18 +1042,16 @@
|
||||
(let [join-alias (lib.join.util/current-join-alias a-join)]
|
||||
(map (fn [col]
|
||||
(-> col
|
||||
(with-join-alias join-alias)
|
||||
;; TODO (Cam 6/25/25) -- remove `:source-alias` since it is busted
|
||||
(assoc :source-alias join-alias))))))
|
||||
(with-join-alias join-alias))))))
|
||||
|
||||
(defn- xform-mark-selected-joinable-columns
|
||||
"Mark the column metadatas in `cols` as `:selected` if they appear in `a-join`'s `:fields`."
|
||||
[a-join]
|
||||
[query stage-number a-join]
|
||||
(let [j-fields (join-fields a-join)]
|
||||
(case j-fields
|
||||
:all (map #(assoc % :selected? true))
|
||||
(:none nil) (map #(assoc % :selected? false))
|
||||
(mapcat #(lib.equality/mark-selected-columns [%] j-fields)))))
|
||||
(mapcat #(lib.equality/mark-selected-columns query stage-number [%] j-fields)))))
|
||||
|
||||
(def ^:private xform-fix-source-for-joinable-columns
|
||||
(map #(assoc % :lib/source :source/joins)))
|
||||
@@ -1070,7 +1075,7 @@
|
||||
(if a-join
|
||||
(comp xform-fix-source-for-joinable-columns
|
||||
(xform-add-join-alias a-join)
|
||||
(xform-mark-selected-joinable-columns a-join))
|
||||
(xform-mark-selected-joinable-columns query stage-number a-join))
|
||||
identity)
|
||||
cols)))
|
||||
|
||||
|
||||
@@ -901,8 +901,8 @@
|
||||
Returns the same list of `agg-operators` with those adjustments made.
|
||||
|
||||
> **Code health:** Healthy"
|
||||
[agg-operators agg-clause]
|
||||
(to-array (lib.core/selected-aggregation-operators (seq agg-operators) agg-clause)))
|
||||
[a-query stage-number agg-operators agg-clause]
|
||||
(to-array (lib.core/selected-aggregation-operators a-query stage-number (seq agg-operators) agg-clause)))
|
||||
|
||||
;; # Filtering
|
||||
;; Filters work in a similar way to aggregations and order-by, but are more complex since they can have several
|
||||
|
||||
@@ -508,8 +508,11 @@
|
||||
|
||||
;;; if you pass in an integer assume it's a stage number; use the method for the query stage itself.
|
||||
(defmethod returned-columns-method :dispatch-type/integer
|
||||
[query _stage-number stage-number options]
|
||||
(returned-columns-method query stage-number (lib.util/query-stage query stage-number) options))
|
||||
[query stage-number x-stage-number options]
|
||||
(assert (= (lib.util/canonical-stage-index query stage-number)
|
||||
(lib.util/canonical-stage-index query x-stage-number))
|
||||
(lib.util/format "Call to returned-columns with different stage numbers, %d and %d" stage-number x-stage-number))
|
||||
(returned-columns-method query x-stage-number (lib.util/query-stage query x-stage-number) options))
|
||||
|
||||
(mu/defn returned-columns :- [:maybe ::returned-columns]
|
||||
"Return a sequence of metadata maps for all the columns expected to be 'returned' at a query, stage of the query, or
|
||||
@@ -534,7 +537,7 @@
|
||||
(returned-columns query (lib.util/query-stage query -1)))
|
||||
|
||||
([query x]
|
||||
(returned-columns query -1 x))
|
||||
(returned-columns query (if (int? x) x -1) x))
|
||||
|
||||
([query stage-number x]
|
||||
(returned-columns query stage-number x nil))
|
||||
@@ -637,9 +640,10 @@
|
||||
"Given a seq of columns, return metadata for any remapped columns, if the `:include-remaps?` option is set."
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
source-cols :- [:maybe [:sequential ::lib.schema.metadata/column]]
|
||||
source-cols :- [:maybe [:sequential ::column-metadata-with-source]]
|
||||
{:keys [include-remaps?] :as _options} :- [:maybe ::returned-columns.options]]
|
||||
(when (and include-remaps?
|
||||
(not (get-in query [:middleware :disable-remaps?]))
|
||||
(lib.util/first-stage? query stage-number))
|
||||
(let [existing-ids (into #{} (keep :id) source-cols)]
|
||||
(for [column source-cols
|
||||
@@ -672,9 +676,8 @@
|
||||
Does not include columns from any Tables that are already explicitly joined.
|
||||
|
||||
Does not include columns that would be implicitly joinable via multiple hops."
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int
|
||||
cols :- [:sequential ::lib.schema.metadata/column]]
|
||||
[query :- ::lib.schema/query
|
||||
cols :- [:sequential ::lib.schema.metadata/column]]
|
||||
(let [remap-target-ids (into #{} (keep (comp :field-id :lib/external-remap)) cols)
|
||||
existing-table-ids (into #{} (comp (remove (comp remap-target-ids :id))
|
||||
(map :table-id))
|
||||
@@ -697,7 +700,7 @@
|
||||
(into []
|
||||
(mapcat (fn [{:keys [table-id], ::keys [fk-field-id fk-field-name fk-join-alias]}]
|
||||
(let [table (id->table table-id)]
|
||||
(for [field (returned-columns query stage-number table)]
|
||||
(for [field (returned-columns query table)]
|
||||
(m/assoc-some field
|
||||
:fk-field-id fk-field-id
|
||||
:fk-field-name fk-field-name
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[metabase.lib.aggregation :as lib.aggregation]
|
||||
[metabase.lib.card :as lib.card]
|
||||
[metabase.lib.convert :as lib.convert]
|
||||
[metabase.lib.expression :as lib.expression]
|
||||
[metabase.lib.field.util :as lib.field.util]
|
||||
[metabase.lib.join :as lib.join]
|
||||
[metabase.lib.join.util :as lib.join.util]
|
||||
@@ -37,7 +36,7 @@
|
||||
(mr/def ::col
|
||||
;; TODO (Cam 6/19/25) -- I think we should actually namespace all the keys added here (to make it clear where they
|
||||
;; came from) and then have the `annotate` middleware convert them to something else for QP results purposes. Then
|
||||
;; we can 'ban' stuff like `:source-alias` and `:source` within Lib itself. See #59772 for some experimental work
|
||||
;; we can 'ban' stuff like`:source` within Lib itself. See #59772 for some experimental work
|
||||
;; there.
|
||||
[:map
|
||||
[:source {:optional true} ::lib.schema.metadata/column.legacy-source]
|
||||
@@ -142,50 +141,8 @@
|
||||
:base-type base-type
|
||||
:effective-type base-type}))
|
||||
|
||||
;;; TODO (Cam 6/17/25) -- move this into [[metabase.lib.expression]] or something and have it be part of the mainline
|
||||
;;; methods for metadata calculation
|
||||
(mu/defn- add-converted-timezone :- [:sequential ::kebab-cased-map]
|
||||
"Add `:converted-timezone` to columns that are from `:convert-timezone` expressions (or expressions wrapping them) --
|
||||
this is used by [[metabase.query-processor.middleware.format-rows/format-rows-xform]]."
|
||||
[query :- ::lib.schema/query
|
||||
cols :- [:sequential ::kebab-cased-map]]
|
||||
(mapv (fn [col]
|
||||
(let [converted-timezone (when-let [expression-name ((some-fn :lib/expression-name :lib/original-expression-name) col)]
|
||||
;; don't look inside expressions that happened inside of joins,
|
||||
;; `lib.expression/resolve-expression` is unfortunately not smart enough to
|
||||
;; figure out how to find them and will just log a noisy error
|
||||
(when-not (:lib/original-join-alias col)
|
||||
(when-let [expr (try
|
||||
(lib.expression/resolve-expression query expression-name)
|
||||
(catch #?(:clj Throwable :cljs :default) e
|
||||
(log/error e "Column metadata has invalid :lib/expression-name (this was probably incorrectly propagated from a previous stage) (QUE-1342)")
|
||||
(log/debugf "In query:\n%s" (u/pprint-to-str query))
|
||||
nil))]
|
||||
(lib.util.match/match-one expr
|
||||
:convert-timezone
|
||||
(let [[_convert-timezone _opts _expr source-tz] &match]
|
||||
source-tz)))))]
|
||||
(cond-> col
|
||||
converted-timezone (assoc :converted-timezone converted-timezone))))
|
||||
cols))
|
||||
|
||||
(defn- any-join-alias [col]
|
||||
((some-fn lib.join.util/current-join-alias :source-alias :lib/original-join-alias) col))
|
||||
|
||||
(mu/defn- add-source-alias :- [:sequential ::kebab-cased-map]
|
||||
"`:source-alias` (`:source_alias`) is still needed
|
||||
for [[metabase.query-processor.middleware.remove-inactive-field-refs]]
|
||||
and [[metabase.lib.equality/column-join-alias]] to work correctly. Why? Not 100% sure -- we should theoretically be
|
||||
able to use `:metabase.lib.join/join-alias` for this purpose -- but that doesn't seem to work. Until I figure that
|
||||
out, include the `:source-alias` key.
|
||||
|
||||
Note that this is no longer used on the FE -- see QUE-1355"
|
||||
[cols :- [:sequential ::kebab-cased-map]]
|
||||
(for [col cols]
|
||||
(merge
|
||||
(when-let [join-alias (any-join-alias col)]
|
||||
{:source-alias join-alias})
|
||||
col)))
|
||||
((some-fn lib.join.util/current-join-alias :lib/original-join-alias) col))
|
||||
|
||||
(defn- implicit-join-aliases [query stage-number]
|
||||
(let [aliases (into #{}
|
||||
@@ -208,7 +165,7 @@
|
||||
(= (:lib/source col) :source/joins)
|
||||
(assoc :lib/source :source/implicitly-joinable)))
|
||||
remove-aliases (fn [col]
|
||||
(dissoc col :metabase.lib.join/join-alias :lib/original-join-alias :source-alias))
|
||||
(dissoc col :metabase.lib.join/join-alias :lib/original-join-alias))
|
||||
implicitly-joined? (fn [col]
|
||||
(when-let [join-alias (any-join-alias col)]
|
||||
(contains? implicit-aliases join-alias)))
|
||||
@@ -391,9 +348,7 @@
|
||||
((fn [cols]
|
||||
(cond-> cols
|
||||
(seq lib-cols) (merge-cols lib-cols))))
|
||||
(add-converted-timezone query)
|
||||
(remove-implicit-join-aliases query)
|
||||
add-source-alias
|
||||
add-legacy-source
|
||||
deduplicate-names
|
||||
(add-legacy-field-refs query)
|
||||
|
||||
@@ -162,10 +162,12 @@
|
||||
([database-id :- ::lib.schema.id/database
|
||||
metadata-providerable :- ::lib.schema.metadata/metadata-providerable
|
||||
stages]
|
||||
{:lib/type :mbql/query
|
||||
:lib/metadata (lib.metadata/->metadata-provider metadata-providerable)
|
||||
:database database-id
|
||||
:stages stages}))
|
||||
(lib.normalize/normalize
|
||||
::lib.schema/query
|
||||
{:lib/type :mbql/query
|
||||
:lib/metadata (lib.metadata/->metadata-provider metadata-providerable)
|
||||
:database database-id
|
||||
:stages stages})))
|
||||
|
||||
(defn- query-from-legacy-query
|
||||
[metadata-providerable legacy-query]
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
#(lib.util.match/replace
|
||||
%
|
||||
:field
|
||||
(let [old-matching-column (lib.equality/find-matching-column &match old-columns)]
|
||||
(let [old-matching-column (lib.equality/find-matching-column query-original stage-number &match old-columns)]
|
||||
(if-let [new-column (some-> old-matching-column :lib/source-uuid source-uuid->new-column)]
|
||||
(assoc &match 2 ((some-fn :lib/source-column-alias :name) new-column))
|
||||
(do
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
|
||||
(mr/def ::stage.common
|
||||
[:map
|
||||
[:parameters {:optional true} [:ref ::lib.schema.parameter/parameters]]])
|
||||
[:parameters {:optional true} [:ref ::lib.schema.parameter/parameters]]
|
||||
;; TODO (Cam 9/4/25) -- make this non-`:maybe` and have normalization code remove it if it's `nil`
|
||||
[:lib/stage-metadata {:optional true} [:maybe [:ref ::lib.schema.metadata/stage]]]])
|
||||
|
||||
(mr/def ::stage.native
|
||||
[:and
|
||||
@@ -59,7 +61,6 @@
|
||||
(not (and (= k :collection)
|
||||
(nil? v))))))}
|
||||
[:lib/type [:= {:decode/normalize common/normalize-keyword} :mbql.stage/native]]
|
||||
[:lib/stage-metadata {:optional true} [:maybe [:ref ::lib.schema.metadata/stage]]]
|
||||
;; the actual native query, depends on the underlying database. Could be a raw SQL string or something like that.
|
||||
;; Only restriction is that, if present, it is non-nil.
|
||||
;; It is valid to have a blank query like `{:type :native}` in legacy.
|
||||
@@ -203,9 +204,8 @@
|
||||
[:merge
|
||||
::stage.common
|
||||
[:map
|
||||
{:decode/normalize normalize-mbql-stage}
|
||||
{:decode/normalize #'normalize-mbql-stage}
|
||||
[:lib/type [:= {:decode/normalize common/normalize-keyword} :mbql.stage/mbql]]
|
||||
[:lib/stage-metadata {:optional true} [:maybe [:ref ::lib.schema.metadata/stage]]]
|
||||
[:joins {:optional true} [:ref ::join/joins]]
|
||||
[:expressions {:optional true} [:ref ::expression/expressions]]
|
||||
[:breakout {:optional true} [:ref ::breakouts]]
|
||||
@@ -260,7 +260,7 @@
|
||||
(mr/def ::stage
|
||||
[:and
|
||||
{:default {:lib/type :mbql.stage/mbql}
|
||||
:decode/normalize normalize-stage
|
||||
:decode/normalize #'normalize-stage
|
||||
:encode/serialize #(dissoc %
|
||||
;; this stuff is all added at runtime by QP middleware.
|
||||
:params
|
||||
@@ -369,7 +369,7 @@
|
||||
(mr/def ::stages
|
||||
[:and
|
||||
[:sequential {:min 1
|
||||
:decode/normalize normalize-stages
|
||||
:decode/normalize #'normalize-stages
|
||||
:default []}
|
||||
[:ref ::stage]]
|
||||
[:cat
|
||||
@@ -393,7 +393,7 @@
|
||||
[:and
|
||||
[:map
|
||||
{:decode/normalize common/normalize-map
|
||||
:encode/serialize serialize-query}
|
||||
:encode/serialize #'serialize-query}
|
||||
[:lib/type [:=
|
||||
{:decode/normalize common/normalize-keyword, :default :mbql/query}
|
||||
:mbql/query]]
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
;;; only support fixing really broken types like `:type/creationtime` to `:type/CreationTime` in prod... Malli checks
|
||||
;;; will throw in dev. See [[metabase.lib.schema.common-test/normalize-base-type-test]] for more info
|
||||
|
||||
(mu/defn- normalize-base-type* :- [:ref ::base-type]
|
||||
(mu/defn- normalize-base-type* :- [:maybe [:ref ::base-type]]
|
||||
[x]
|
||||
(normalize-keyword x))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns metabase.lib.schema.metadata
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[medley.core :as m]
|
||||
[metabase.lib.schema.binning :as lib.schema.binning]
|
||||
@@ -190,7 +191,14 @@
|
||||
(and (:id m) (not (pos-int? (:id m))))
|
||||
(dissoc :id)))
|
||||
;; remove deprecated `:ident` and `:model/inner_ident` keys (normalized to `:model/inner-ident`)
|
||||
(dissoc :ident :model/inner-ident))))
|
||||
(dissoc :ident :model/inner-ident)
|
||||
;; rename source-alias to the appropriate key
|
||||
(as-> $col (cond-> $col
|
||||
(:source-alias $col) (set/rename-keys {:source-alias (case (:lib/source $col)
|
||||
(nil :source/joins :source/implicitly-joinable)
|
||||
:metabase.lib.join/join-alias
|
||||
#_else
|
||||
:lib/original-join-alias)}))))))
|
||||
|
||||
(def ^:private column-validate-for-source-specs
|
||||
"Schemas to use to validate columns with a given `:lib/source`. Since a lot of these schemas are applicable to
|
||||
@@ -206,6 +214,12 @@
|
||||
:schema [:fn
|
||||
{:error/message ":lib/expression-name is required for columns with :source/expressions"}
|
||||
:lib/expression-name]}
|
||||
;; TODO (Cam 9/4/25) -- you would think a requirement like this would be obvious and easy to enable but it actually
|
||||
;; breaks a handful of tests... I still want to enable it at some point but we need to go fix up all those tests.
|
||||
#_{:include :source/table-defaults
|
||||
:schema [:fn
|
||||
{:error/message ":table-id is required for columns with :source/table-defaults"}
|
||||
:table-id]}
|
||||
;; Current stage join alias (`:metabase.lib.join/join-alias`) should only be set for columns whose `:lib/source` is
|
||||
;; `:source/joins`
|
||||
{:exclude :source/joins
|
||||
@@ -318,7 +332,7 @@
|
||||
[:and
|
||||
[:map
|
||||
{:error/message "Valid column metadata"
|
||||
:decode/normalize normalize-column}
|
||||
:decode/normalize #'normalize-column}
|
||||
[:lib/type [:= {:decode/normalize lib.schema.common/normalize-keyword, :default :metadata/column} :metadata/column]]
|
||||
;;
|
||||
;; TODO (Cam 6/19/25) -- change all these comments to proper `:description`s like we have
|
||||
@@ -356,18 +370,6 @@
|
||||
;; an foreign key, and points to this Field ID. This is mostly used to determine how to add implicit joins by
|
||||
;; the [[metabase.query-processor.middleware.add-implicit-joins]] middleware.
|
||||
[:fk-target-field-id {:optional true} [:maybe ::lib.schema.id/field]]
|
||||
;;
|
||||
;; Join alias of the table we're joining against, if any. Not really 100% clear why we would need this on top
|
||||
;; of [[metabase.lib.join/current-join-alias]], which stores the same info under a namespaced key. I think we can
|
||||
;; remove it.
|
||||
;;
|
||||
;; TODO (Cam 6/19/25) -- yes, we should remove this key, I've tried to do so but a few places are still
|
||||
;; setting (AND USING!) it. It actually appears that this gets propagated beyond the current stage where the join
|
||||
;; has happened and has thus taken on a purposes as a 'previous stage join alias' column. We should use
|
||||
;; `:lib/original-join-alias` instead to serve this purpose since `:source-alias` is not set or used correctly.
|
||||
;; Check out experimental https://github.com/metabase/metabase/pull/59772 where I updated this schema to 'ban'
|
||||
;; this key so we can root out anywhere trying to use it. (QUE-1403)
|
||||
[:source-alias {:optional true} [:maybe ::lib.schema.common/non-blank-string]]
|
||||
;; Join alias of the table we're joining against, if any. SHOULD ONLY BE SET IF THE JOIN HAPPENED AT THIS STAGE OF
|
||||
;; THE QUERY! (Also ok within a join's conditions for previous joins within the parent stage, because a join is
|
||||
;; allowed to join on the results of something else)
|
||||
@@ -515,7 +517,9 @@
|
||||
;;
|
||||
;; TODO (Cam 6/13/25) -- go add this to some of the other metadata schemas as well.
|
||||
::kebab-cased-map
|
||||
[:ref ::column.validate-for-source]])
|
||||
[:ref ::column.validate-for-source]
|
||||
(lib.schema.common/disallowed-keys
|
||||
{:source-alias ":source-alias is deprecated, set :metabase.lib.join/current-join-alias or :lib/original-join-alias instead"})])
|
||||
|
||||
(mr/def ::persisted-info.definition
|
||||
"Definition spec for a cached table."
|
||||
|
||||
@@ -263,6 +263,9 @@
|
||||
(nil? l) (assoc :type :number/<=, :value [u])))
|
||||
param))))
|
||||
|
||||
(mr/def ::id
|
||||
::lib.schema.common/non-blank-string)
|
||||
|
||||
(mr/def ::parameter
|
||||
"Schema for the *value* of a parameter (e.g. a Dashboard parameter or a native query template tag) as passed in as
|
||||
part of the `:parameters` list in a query."
|
||||
@@ -272,7 +275,7 @@
|
||||
[:type [:ref ::type]]
|
||||
;; TODO -- these definitely SHOULD NOT be optional but a ton of tests aren't passing them in like they should be.
|
||||
;; At some point we need to go fix those tests and then make these keys required
|
||||
[:id {:optional true} ::lib.schema.common/non-blank-string]
|
||||
[:id {:optional true} [:ref ::id]]
|
||||
[:target {:optional true} [:ref ::target]]
|
||||
;; not specified if the param has no value. TODO - make this stricter; type of `:value` should be validated based
|
||||
;; on the [[ParameterType]]
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
(lib.hierarchy/derive :expression ::ref)
|
||||
|
||||
(mr/def ::aggregation-options
|
||||
(mr/def ::aggregation.options
|
||||
[:merge
|
||||
::common/options
|
||||
[:map
|
||||
@@ -126,7 +126,7 @@
|
||||
(mbql-clause/define-mbql-clause :aggregation
|
||||
[:tuple
|
||||
[:= {:decode/normalize common/normalize-keyword} :aggregation]
|
||||
::aggregation-options
|
||||
::aggregation.options
|
||||
:string])
|
||||
|
||||
(defmethod expression/type-of-method :aggregation
|
||||
|
||||
@@ -44,25 +44,20 @@
|
||||
|
||||
(mu/defn- existing-stage-metadata :- [:maybe ::lib.metadata.calculation/returned-columns]
|
||||
"Return existing stage metadata attached to a stage if is already present: return it as-is, but only if this is a
|
||||
native stage or a source-Card or a metric stage. If it's any other sort of stage then ignore the metadata, it's
|
||||
probably wrong; we can recalculate the correct metadata anyway."
|
||||
native stage. If it's any other sort of stage then ignore the metadata, it's probably wrong; we can recalculate the
|
||||
correct metadata anyway."
|
||||
[query :- ::lib.schema/query
|
||||
stage-number :- :int]
|
||||
(let [{stage-type :lib/type, :keys [source-card] :as stage} (lib.util/query-stage query stage-number)]
|
||||
(when-let [metadata (:lib/stage-metadata stage)]
|
||||
(when (or (= stage-type :mbql.stage/native)
|
||||
source-card)
|
||||
(let [source-type (case stage-type
|
||||
:mbql.stage/native :source/native
|
||||
:mbql.stage/mbql :source/card)]
|
||||
(not-empty
|
||||
(into []
|
||||
(comp (map #(assoc % :lib/source source-type))
|
||||
;; do not truncate the desired column aliases coming back from a native query, because if a
|
||||
;; native query returns a 'crazy long' column name then we need to use that in the next stage.
|
||||
;; See [[metabase.lib.stage-test/propagate-crazy-long-native-identifiers-test]]
|
||||
(lib.field.util/add-source-and-desired-aliases-xform query (lib.util/non-truncating-unique-name-generator)))
|
||||
(:columns metadata))))))))
|
||||
(let [{stage-type :lib/type, :as stage} (lib.util/query-stage query stage-number)]
|
||||
(when (= stage-type :mbql.stage/native)
|
||||
(when-let [cols (not-empty (get-in stage [:lib/stage-metadata :columns]))]
|
||||
(into []
|
||||
(comp (map #(assoc % :lib/source :source/native))
|
||||
;; do not truncate the desired column aliases coming back from a native query, because if a
|
||||
;; native query returns a 'crazy long' column name then we need to use that in the next stage.
|
||||
;; See [[metabase.lib.stage-test/propagate-crazy-long-native-identifiers-test]]
|
||||
(lib.field.util/add-source-and-desired-aliases-xform query (lib.util/non-truncating-unique-name-generator)))
|
||||
cols)))))
|
||||
|
||||
(mu/defn- breakouts-columns :- [:maybe ::lib.metadata.calculation/visible-columns]
|
||||
[query :- ::lib.schema/query
|
||||
@@ -261,7 +256,7 @@
|
||||
(into (vec existing-columns)
|
||||
;; add implicitly joinable columns if desired
|
||||
(when include-implicitly-joinable?
|
||||
(lib.metadata.calculation/implicitly-joinable-columns query stage-number existing-columns)))))
|
||||
(lib.metadata.calculation/implicitly-joinable-columns query existing-columns)))))
|
||||
|
||||
(defn- add-cols-from-join
|
||||
"The columns from `:fields` may contain columns from `:joins` -- so if the joins specify their own `:fields` we need
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
[metabase.lib.field.util :as lib.field.util]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.metadata.calculation :as lib.metadata.calculation]
|
||||
[metabase.lib.ref :as lib.ref]
|
||||
[metabase.lib.schema :as lib.schema]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.util :as lib.util]
|
||||
[metabase.util.malli :as mu]))
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.registry :as mr]))
|
||||
|
||||
(mu/defn- pop-until-aggregation-or-breakout :- [:tuple [:maybe ::lib.schema/query] [:int {:max -1}]]
|
||||
"Strips off any trailing stages that do not contain aggregations or breakouts.
|
||||
@@ -56,7 +55,7 @@
|
||||
(second (pop-until-aggregation-or-breakout query)))
|
||||
-1))
|
||||
|
||||
(def ^:private TopLevelColumnOptions
|
||||
(mr/def ::top-level-column.options
|
||||
[:map
|
||||
{:closed true}
|
||||
[:rename-superflous-options? {:optional true} :boolean]])
|
||||
@@ -71,12 +70,16 @@
|
||||
([query :- ::lib.schema/query
|
||||
column :- ::lib.schema.metadata/column]
|
||||
(top-level-column query column :rename-superflous-options? true))
|
||||
|
||||
([query :- ::lib.schema/query
|
||||
column :- ::lib.schema.metadata/column
|
||||
& {:keys [rename-superflous-options?]} :- TopLevelColumnOptions]
|
||||
& {:keys [rename-superflous-options?]} :- [:maybe ::top-level-column.options]]
|
||||
(when (> (count (:stages query)) 1)
|
||||
(assert (not= (:lib/source column) :source/table-defaults)
|
||||
"Column metadata does not appear to be for the last stage... what stage is it from?"))
|
||||
(let [top-query (top-level-query query)]
|
||||
(if (= query top-query)
|
||||
column ;; Unchanged if this is already a top-level query. That includes keeping the "superfluous" options!
|
||||
column ; Unchanged if this is already a top-level query. That includes keeping the "superfluous" options!
|
||||
(loop [query query
|
||||
column column]
|
||||
(if (= query top-query)
|
||||
@@ -86,11 +89,11 @@
|
||||
;; circumstances, you will not need them. On the off chance you do need them, they'll still be available.
|
||||
(set/rename-keys {::lib.field/temporal-unit ::temporal-unit
|
||||
::lib.field/binning ::binning}))
|
||||
(let [prev-cols (for [col (lib.metadata.calculation/returned-columns query -2 (lib.util/previous-stage query -1))]
|
||||
(let [prev-cols (for [col (lib.metadata.calculation/returned-columns query -2)]
|
||||
(-> col
|
||||
lib.field.util/update-keys-for-col-from-previous-stage
|
||||
(assoc ::original col)))
|
||||
prev-col (some-> (lib.equality/find-matching-column query -2 (lib.ref/ref column) prev-cols)
|
||||
prev-col (some-> (lib.equality/find-matching-column query -2 column prev-cols)
|
||||
::original)]
|
||||
(when prev-col
|
||||
(recur (update query :stages pop) prev-col)))))))))
|
||||
|
||||
37
src/metabase/lib/util/log.cljc
Normal file
37
src/metabase/lib/util/log.cljc
Normal file
@@ -0,0 +1,37 @@
|
||||
(ns metabase.lib.util.log
|
||||
"EXPERIMENTAL helper functions for logging stuff. Very important when I spend all day
|
||||
debugging [[metabase.lib.field.resolution]] -- Cam"
|
||||
(:require
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.util :as lib.util]))
|
||||
|
||||
(defn- table-name
|
||||
[metadata-providerable x]
|
||||
(cond
|
||||
(= (:lib/type x) :metadata/table) (:name x)
|
||||
(pos-int? x) (recur metadata-providerable (lib.metadata/table metadata-providerable x))))
|
||||
|
||||
(defn format-table
|
||||
"Render a Table metadata or ID in a nice way e.g.
|
||||
|
||||
Table 100 \"ORDERS\""
|
||||
[metadata-providerable x]
|
||||
(cond
|
||||
(= (:lib/type x) :metadata/table) (lib.util/format "Table %d %s" (:id x) (pr-str (:name x)))
|
||||
(pos-int? x) (recur metadata-providerable (lib.metadata/table metadata-providerable x))
|
||||
:else (lib.util/format "Table %s" (pr-str x))))
|
||||
|
||||
(defn format-field
|
||||
"Render a Column metadata or Field ID in a nice way e.g.
|
||||
|
||||
Field 200 \"ORDERS\".\"CREATED_AT\""
|
||||
[metadata-providerable x]
|
||||
(cond
|
||||
(= (:lib/type x) :metadata/column) (str "Field "
|
||||
(when (:id x)
|
||||
(str (:id x) " "))
|
||||
(when (:table-id x)
|
||||
(str (pr-str (table-name metadata-providerable (:table-id x))) "."))
|
||||
(pr-str ((some-fn :lib/source-column-alias :name) x)))
|
||||
(pos-int? x) (recur metadata-providerable (lib.metadata/field metadata-providerable x))
|
||||
:else (lib.util/format "Field %s" (pr-str x))))
|
||||
@@ -8,11 +8,12 @@
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[medley.core :as m]
|
||||
[metabase.lib-be.metadata.jvm :as lib-be.metadata.jvm]
|
||||
[metabase.lib.core :as lib]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.schema :as lib.schema]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.lib.types.isa :as lib.types.isa]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.parameters.params :as params]
|
||||
[metabase.parameters.schema :as parameters.schema]
|
||||
[metabase.query-processor :as qp]
|
||||
[metabase.query-processor.util :as qp.util]
|
||||
@@ -59,20 +60,31 @@
|
||||
Maybe we should lower it for the sake of displaying a parameter dropdown."
|
||||
1000)
|
||||
|
||||
(defn- values-from-card-query
|
||||
[card value-field-ref {:keys [query-string] :as _opts}]
|
||||
(let [metadata-provider (lib-be.metadata.jvm/application-database-metadata-provider (:database_id card))
|
||||
query (lib/query metadata-provider (lib.metadata/card metadata-provider (:id card)))
|
||||
value-column (lib/find-column-for-legacy-ref query value-field-ref (lib/visible-columns query))
|
||||
textual? (lib.types.isa/string? value-column)
|
||||
nonempty ((if textual? lib/not-empty lib/not-null) value-column)
|
||||
query-filter (when query-string
|
||||
(if textual?
|
||||
(lib/contains (lib/lower value-column) (u/lower-case-en query-string))
|
||||
(lib/= value-column query-string)))]
|
||||
(mu/defn- values-from-card-query :- ::lib.schema/query
|
||||
[card :- [:map
|
||||
[:id ::lib.schema.id/card]
|
||||
[:database_id ::lib.schema.id/database]]
|
||||
value-field-ref
|
||||
{:keys [query-string] :as _opts}]
|
||||
;; query will use `:source-card` if Card is Model or Metric but will use the Card's query directly if it is a
|
||||
(let [query (params/card->query card)
|
||||
value-column (-> (or (lib/find-column-for-legacy-ref query value-field-ref (lib/returned-columns query))
|
||||
(throw (ex-info (format "Failed to find column for legacy ref %s" (pr-str value-field-ref))
|
||||
{:query query, :ref value-field-ref, :cols (lib/returned-columns query)})))
|
||||
;; since the query we're building will add a breakout and filters using this column to a
|
||||
;; subsequent stage, to "do this the right way" we should update this metadata so it is
|
||||
;; ready for use in subsequent stages.
|
||||
lib/update-keys-for-col-from-previous-stage)
|
||||
textual? (lib.types.isa/string? value-column)
|
||||
nonempty-filter ((if textual? lib/not-empty lib/not-null) value-column)
|
||||
query-filter (when query-string
|
||||
(if textual?
|
||||
(lib/contains (lib/lower value-column) (u/lower-case-en query-string))
|
||||
(lib/= value-column query-string)))]
|
||||
(-> query
|
||||
lib/append-stage
|
||||
(lib/limit *max-rows*)
|
||||
(lib/filter nonempty)
|
||||
(lib/filter nonempty-filter)
|
||||
(cond-> #_query query-filter (lib/filter query-filter))
|
||||
(lib/breakout value-column)
|
||||
;; TODO(Braden, 07/04/2025): This should probably become a lib helper? I suspect this isn't the only
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
|
||||
There are 3 mains ways to provide values to a parameter:
|
||||
- chain-filter: see [metabase.parameters.chain-filter]
|
||||
- field-values: see [metabase.parameters.field-values]
|
||||
- custom-values: see [metabase.parameters.custom-values]"
|
||||
- chain-filter: see [[metabase.parameters.chain-filter]]
|
||||
- field-values: see [[metabase.parameters.field-values]]
|
||||
- custom-values: see [[metabase.parameters.custom-values]]"
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[medley.core :as m]
|
||||
@@ -19,8 +19,12 @@
|
||||
[metabase.lib-be.metadata.jvm :as lib.metadata.jvm]
|
||||
[metabase.lib.core :as lib]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.schema :as lib.schema]
|
||||
[metabase.lib.schema.common :as lib.schema.common]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.schema.parameter :as lib.schema.parameter]
|
||||
[metabase.lib.util :as lib.util]
|
||||
[metabase.lib.util.match :as lib.util.match]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.parameters.schema :as parameters.schema]
|
||||
@@ -176,38 +180,78 @@
|
||||
;;; | DASHBOARD-SPECIFIC |
|
||||
;;; +----------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
(defn- filterable-columns-for-query
|
||||
(mr/def ::context
|
||||
[:map
|
||||
[:param-id->field-ids [:map-of
|
||||
::lib.schema.parameter/id
|
||||
[:set ::lib.schema.id/field]]]
|
||||
[:card-id->filterable-columns [:map-of
|
||||
::lib.schema.id/card
|
||||
[:map-of
|
||||
#_stage-number :int
|
||||
[:sequential ::lib.schema.metadata/column]]]]
|
||||
[:card-id->query {:optional true} [:map-of ::lib.schema.id/card ::lib.schema/query]]])
|
||||
|
||||
(mu/defn card->query :- ::lib.schema/query
|
||||
"Given a `card` (from the app DB) return a MBQL 5 query for parameter purposes.
|
||||
|
||||
This works slightly differently if the Card is a regular saved `:question` versus a `:model` or `:metric`."
|
||||
[{database-id :database_id, :as card} :- [:map
|
||||
[:id ::lib.schema.id/card]
|
||||
[:database_id ::lib.schema.id/database]
|
||||
[:type ::lib.schema.metadata/card.type]
|
||||
[:dataset_query :map]]]
|
||||
(let [mp (lib.metadata.jvm/application-database-metadata-provider database-id)]
|
||||
;; Regular questions are used directly. If a model or metric has been used directly in this card, wrap it into
|
||||
;; a query against that model or metric.
|
||||
(if (= :question (:type card))
|
||||
(let [{card-query :dataset_query, result-metadata :result_metadata} card]
|
||||
(cond-> (lib/query mp card-query)
|
||||
result-metadata (lib/update-query-stage -1 (fn [stage]
|
||||
(->> (assoc stage :lib/stage-metadata (lib.util/->stage-metadata result-metadata))
|
||||
(lib/normalize ::lib.schema/stage))))))
|
||||
(lib/query mp (lib.metadata/card mp (u/the-id card))))))
|
||||
|
||||
(mu/defn- ensure-query-for-card :- ::context
|
||||
[ctx :- ::context
|
||||
{card-id :id, :as card} :- [:map
|
||||
[:id ::lib.schema.id/card]]]
|
||||
(cond-> ctx
|
||||
(not (get-in ctx [:card-id->query card-id])) (assoc-in [:card-id->query card-id] (card->query card))))
|
||||
|
||||
(mu/defn- filterable-columns-for-card-query :- [:maybe [:sequential ::lib.schema.metadata/column]]
|
||||
"Get filterable columns for query."
|
||||
[database-id card stage-number]
|
||||
(let [metadata-provider (lib.metadata.jvm/application-database-metadata-provider database-id)
|
||||
;; Regular questions are used directly. If a model or metric has been used directly in this card, wrap it into
|
||||
;; a query against that model or metric.
|
||||
query (lib/query metadata-provider (if (= :question (:type card))
|
||||
(:dataset_query card)
|
||||
(lib.metadata/card metadata-provider (:id card))))
|
||||
[ctx :- ::context
|
||||
card :- [:map
|
||||
[:id ::lib.schema.id/card]]
|
||||
stage-number :- :int]
|
||||
(let [query (get-in ctx [:card-id->query (:id card)])
|
||||
_ (assert query "Missing card-id->query for Card")
|
||||
;; for backward compatibility, append a filter stage only with explicit stage numbers
|
||||
query (cond-> query (>= stage-number 0) lib/ensure-filter-stage)]
|
||||
(when (and (>= stage-number -1) (< stage-number (lib/stage-count query)))
|
||||
(lib/filterable-columns query stage-number))))
|
||||
|
||||
(defn- ensure-filterable-columns-for-card
|
||||
[ctx
|
||||
(mu/defn- ensure-filterable-columns-for-card :- ::context
|
||||
[ctx :- ::context
|
||||
{database-id :database_id
|
||||
dataset-query :dataset_query
|
||||
card-id :id
|
||||
:as card}
|
||||
stage-number]
|
||||
:as card} :- :map
|
||||
stage-number :- :int]
|
||||
(cond-> ctx
|
||||
(and (not (get-in ctx [:card-id->filterable-columns card-id stage-number]))
|
||||
(seq dataset-query)
|
||||
(pos-int? database-id))
|
||||
(assoc-in [:card-id->filterable-columns card-id stage-number]
|
||||
(or (filterable-columns-for-query database-id card stage-number) []))))
|
||||
(or (filterable-columns-for-card-query ctx card stage-number) []))))
|
||||
|
||||
(defn- field-id-from-dashcards-filterable-columns
|
||||
(mu/defn- field-id-from-dashcards-filterable-columns
|
||||
"Update the `ctx` with `field-id`. This function is supposed to be used on params where target is a name field, in
|
||||
reducing step of [[field-id-into-context-rf]], when it is certain that param target is no integer id field."
|
||||
[ctx param-dashcard-info stage-number]
|
||||
[ctx :- ::context
|
||||
param-dashcard-info
|
||||
stage-number :- :int]
|
||||
(let [param-id (get-in param-dashcard-info [:param-mapping :parameter_id])
|
||||
param-target (get-in param-dashcard-info [:param-mapping :target])
|
||||
card-id (or (get-in param-dashcard-info [:param-mapping :card_id])
|
||||
@@ -215,13 +259,31 @@
|
||||
filterable-columns (get-in ctx [:card-id->filterable-columns card-id stage-number])
|
||||
[_ dimension] (->> (mbql.normalize/normalize-tokens param-target :ignore-path)
|
||||
(mbql.u/check-clause :dimension))]
|
||||
|
||||
(if-some [field-id (lib.util.match/match-one dimension
|
||||
;; TODO it's basically a workaround for ignoring non-dimension parameter targets such as SQL variables
|
||||
;; TODO code is misleading; let's check for :dimension and drop the match call here
|
||||
[:field (field-name :guard string?) _]
|
||||
(->> filterable-columns
|
||||
(lib/find-matching-column (lib/->pMBQL dimension))
|
||||
:id))]
|
||||
(let [mbql5-dimension (lib/->pMBQL dimension)
|
||||
query (get-in ctx [:card-id->query card-id])]
|
||||
(assert query "Missing card-id->query for Card")
|
||||
;; apparently we allow parameters for stages that don't exist yet, but just one additional
|
||||
;; stage for so-called "post-aggregation" parameters. but we only allow parameters for
|
||||
;; stage N+1, and no imaginary stages beyond that.
|
||||
(-> (cond
|
||||
(> stage-number (count (:stages query)))
|
||||
(do
|
||||
(log/warnf "Ignoring parameter %s with stage number %d: Card query doesn't have that many stages"
|
||||
(pr-str dimension)
|
||||
stage-number)
|
||||
nil)
|
||||
|
||||
(= stage-number (count (:stages query)))
|
||||
(lib/find-matching-column (lib/append-stage query) stage-number mbql5-dimension filterable-columns)
|
||||
|
||||
:else
|
||||
(lib/find-matching-column query stage-number mbql5-dimension filterable-columns))
|
||||
:id)))]
|
||||
(-> ctx
|
||||
(update :param-id->field-ids #(merge {param-id #{}} %))
|
||||
(update-in [:param-id->field-ids param-id] conj field-id))
|
||||
@@ -278,6 +340,7 @@
|
||||
;; aggregates a native query model with a field that was mapped to a db field), we need to load metadata in
|
||||
;; [[ensure-filterable-columns-for-card]] to find the originating field. (#42829)
|
||||
(-> ctx
|
||||
(ensure-query-for-card card)
|
||||
(ensure-filterable-columns-for-card card stage-number)
|
||||
(field-id-from-dashcards-filterable-columns param-dashcard-info stage-number)))))))
|
||||
|
||||
@@ -289,9 +352,10 @@
|
||||
{:dashcard dashcard
|
||||
:param-mapping mapping
|
||||
:param-target-field (param-target->field-clause (:target mapping) (:card dashcard))}))]
|
||||
(transduce (mapcat dashcard->param-dashcard-info)
|
||||
field-id-into-context-rf
|
||||
dashcards)))
|
||||
(lib.metadata.jvm/with-metadata-provider-cache
|
||||
(transduce (mapcat dashcard->param-dashcard-info)
|
||||
field-id-into-context-rf
|
||||
dashcards))))
|
||||
|
||||
(declare card->template-tag-param-id->field-ids)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
[metabase.util.i18n :refer [tru]]
|
||||
[metabase.util.malli :as mu]))
|
||||
|
||||
;;; TODO (Cam 9/5/25) -- why do we this AND [[metabase.lib.query/query-from-legacy-inner-query]]? Can't we just use
|
||||
;;; that everywhere?
|
||||
(mu/defn legacy-inner-query->mlv2-query :- ::lib.schema/query
|
||||
"Convert a legacy `inner-query` to an MLv2 query. Requires bound QP store."
|
||||
[inner-query :- [:and
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
;; this stage (expression/aggregation -> field, field-id to string)
|
||||
(let [replacement (lib.util.match/replace replacement
|
||||
[(tag :guard #{:expression :field :aggregation}) _ _]
|
||||
(if-let [col (lib/find-matching-column &match columns)]
|
||||
(if-let [col (lib/find-matching-column query stage-number &match columns)]
|
||||
(lib/ref col)
|
||||
;; This is probably due to a field-id where it shouldn't be
|
||||
&match))]
|
||||
|
||||
@@ -54,12 +54,14 @@
|
||||
nil)))
|
||||
|
||||
(mu/defn- update-second-stage-refs :- ::lib.schema/stage
|
||||
[stage :- ::lib.schema/stage
|
||||
[query
|
||||
stage-number
|
||||
stage :- ::lib.schema/stage
|
||||
first-stage-cols :- [:sequential ::lib.schema.metadata/column]]
|
||||
(lib.util.match/replace stage
|
||||
#{:field :expression}
|
||||
(if-let [col (when-not (some #{:expressions} &parents)
|
||||
(lib.equality/find-matching-column &match first-stage-cols))]
|
||||
(lib.equality/find-matching-column query stage-number &match first-stage-cols))]
|
||||
(-> col
|
||||
lib/update-keys-for-col-from-previous-stage
|
||||
update-temporal-bucket
|
||||
@@ -157,7 +159,9 @@
|
||||
first-stage-cols (lib.walk/apply-f-for-stage-at-path lib/returned-columns query path)]
|
||||
(-> stage
|
||||
(dissoc :joins :source-table :source-card :lib/stage-metadata :filters)
|
||||
(update-second-stage-refs first-stage-cols)
|
||||
(as-> $stage (lib.walk/apply-f-for-stage-at-path (fn [query stage-number]
|
||||
(update-second-stage-refs query stage-number $stage first-stage-cols))
|
||||
query path))
|
||||
(dissoc :expressions)
|
||||
add-implicit-breakout-order-bys)))
|
||||
|
||||
|
||||
@@ -3049,11 +3049,13 @@
|
||||
:dataset_query (mt/mbql-query products)}
|
||||
:model/Card card2 {:database_id (mt/id)
|
||||
:table_id (mt/id :venues)
|
||||
:dataset_query (-> (lib/query mp (lib.metadata/card mp (:id model)))
|
||||
lib.convert/->legacy-MBQL)}
|
||||
:dataset_query (lib/query mp (lib.metadata/card mp (:id model)))}
|
||||
:model/DashboardCard dashcard {:card_id (:id card)
|
||||
:dashboard_id (:id dashboard)
|
||||
:parameter_mappings [{:parameter_id "_CATEGORY_"
|
||||
;; Note this Card ID is different from the one for the
|
||||
;; DashCard itself... this parameter is meant for the
|
||||
;; series
|
||||
:card_id (:id card2)
|
||||
:target [:dimension [:field "CATEGORY" {:base-type :type/Text}] {:stage-number 0}]}]}
|
||||
:model/DashboardCardSeries _ {:card_id (:id card2)
|
||||
|
||||
@@ -497,10 +497,10 @@
|
||||
{:short :max
|
||||
:requires-column? true
|
||||
:columns scope-cols}]
|
||||
(lib/selected-aggregation-operators aggregation-operators (second aggregations)))))
|
||||
(lib/selected-aggregation-operators query -1 aggregation-operators (second aggregations)))))
|
||||
(testing "selected-aggregation-operators w/ column"
|
||||
(let [selected-operators (lib/selected-aggregation-operators
|
||||
aggregation-operators (first aggregations))]
|
||||
query -1 aggregation-operators (first aggregations))]
|
||||
(is (=? [{:short :count
|
||||
:requires-column? false}
|
||||
{:short :sum
|
||||
@@ -615,7 +615,7 @@
|
||||
:effective-type :type/Integer
|
||||
:semantic-type :type/Category
|
||||
:lib/source :source/implicitly-joinable}]}]
|
||||
(->> (lib/selected-aggregation-operators aggregation-operators (first aggregations))
|
||||
(->> (lib/selected-aggregation-operators query -1 aggregation-operators (first aggregations))
|
||||
(filterv :selected?))))))))
|
||||
|
||||
(deftest ^:parallel preserve-field-settings-metadata-test
|
||||
@@ -850,10 +850,10 @@
|
||||
;; Hawk's =? will think these are predicates and try to run them!
|
||||
true (dissoc :display-info)
|
||||
(= (:short op) :distinct) (assoc :selected? true)))
|
||||
(lib/selected-aggregation-operators available (first (lib/aggregations query)))))
|
||||
(lib/selected-aggregation-operators query -1 available (first (lib/aggregations query)))))
|
||||
(is (thrown? #?(:clj Exception :cljs js/Error)
|
||||
(with-redefs [lib.util/ref-clause? (constantly true)]
|
||||
(lib/selected-aggregation-operators available (first (lib/aggregations query)))))))))
|
||||
(lib/selected-aggregation-operators query -1 available (first (lib/aggregations query)))))))))
|
||||
|
||||
(deftest ^:parallel aggregable-columns-test
|
||||
(let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :venues))
|
||||
|
||||
@@ -266,20 +266,20 @@
|
||||
{:id (meta/id :venues :latitude) :name "LATITUDE"}
|
||||
{:id (meta/id :venues :longitude) :name "LONGITUDE"}
|
||||
{:id (meta/id :venues :price) :name "PRICE"}
|
||||
{:lib/type :metadata/column
|
||||
:name "ID"
|
||||
:display-name "ID"
|
||||
:source-alias "Cat"
|
||||
:id (meta/id :categories :id)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/BigInteger}
|
||||
{:lib/type :metadata/column
|
||||
:name "NAME"
|
||||
:display-name "Name"
|
||||
:source-alias "Cat"
|
||||
:id (meta/id :categories :name)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/Text}]
|
||||
{:lib/type :metadata/column
|
||||
:name "ID"
|
||||
:display-name "ID"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:id (meta/id :categories :id)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/BigInteger}
|
||||
{:lib/type :metadata/column
|
||||
:name "NAME"
|
||||
:display-name "Name"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:id (meta/id :categories :name)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/Text}]
|
||||
(lib/breakoutable-columns query))))))))
|
||||
|
||||
(deftest ^:parallel breakoutable-columns-source-card-test
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
(ns metabase.lib.drill-thru.column-filter-test
|
||||
(:require
|
||||
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))
|
||||
[clojure.set :as set]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[medley.core :as m]
|
||||
[metabase.lib.core :as lib]
|
||||
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
|
||||
[metabase.lib.drill-thru.test-util.canned :as canned]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.test-metadata :as meta]
|
||||
[metabase.lib.test-util :as lib.tu]))
|
||||
|
||||
@@ -327,13 +329,17 @@
|
||||
(meta/field-metadata :products :id))])))
|
||||
columns (lib/returned-columns query)
|
||||
category (-> (m/find-first #(= (:name %) "CATEGORY") columns)
|
||||
(dissoc :join-alias :metabase.lib.join/join-alias :lib/source))
|
||||
(set/rename-keys {:metabase.lib.join/join-alias :source-alias})
|
||||
(dissoc :lib/source :join-alias :metabase.lib.join/join-alias)
|
||||
(->> (lib/normalize ::lib.schema.metadata/column)))
|
||||
context {:column category
|
||||
:column-ref (lib/ref category)
|
||||
:value nil}
|
||||
drills (lib/available-drill-thrus query -1 context)
|
||||
colfilter (m/find-first #(= (:type %) :drill-thru/column-filter) drills)]
|
||||
(is (= "Products" (:source-alias category)))
|
||||
(testing "Normalization should rename :source-alias to :metabase.lib.join/join-alias"
|
||||
(is (= "Products"
|
||||
(:metabase.lib.join/join-alias category))))
|
||||
(is (= "Products" (-> context :column-ref second :join-alias)))
|
||||
(is (some? (:column colfilter))))))
|
||||
|
||||
|
||||
@@ -144,7 +144,6 @@
|
||||
(is (not (m/find-first #(= (:type %) :drill-thru/fk-details)
|
||||
(lib/available-drill-thrus query -1 context)))))))
|
||||
|
||||
`preserve-filters-for-other-fks-forming-multi-column-pk-test
|
||||
(deftest ^:parallel preserve-filters-for-other-fks-forming-multi-column-pk-test
|
||||
(testing "with multiple FKs forming a multi-column PK on another table"
|
||||
(let [provider (merged-mock/merged-mock-metadata-provider
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
(->> (lib/returned-columns mbql-query)
|
||||
(map #(-> %
|
||||
(assoc :lib/source :source/native)
|
||||
(dissoc :lib/breakout? :lib/expression-name :metabase.lib.join/join-alias))))
|
||||
(dissoc :lib/breakout? :lib/expression-name :metabase.lib.join/join-alias :fk-field-id))))
|
||||
(catch #?(:clj clojure.lang.ExceptionInfo :cljs js/Error) e
|
||||
;; Not all the input queries passed to this are real. Some of them are skeletons used
|
||||
;; as test expectations. If we fail to generate the returned columns, just return nil.
|
||||
@@ -264,7 +264,7 @@
|
||||
true (map #(assoc % ::was-breakout-in-original-query? (:lib/breakout? %)))
|
||||
(= query-kind :native) (map #(-> %
|
||||
(assoc :lib/source :source/native)
|
||||
(dissoc :lib/breakout? :lib/expression-name :metabase.lib.join/join-alias))))
|
||||
(dissoc :lib/breakout? :lib/expression-name :metabase.lib.join/join-alias :fk-field-id))))
|
||||
by-name (m/index-by :name cols)
|
||||
col (get by-name column-name)
|
||||
refs (update-vals by-name lib/ref)
|
||||
@@ -475,15 +475,13 @@
|
||||
& variants]
|
||||
(assert (even? (count variants)) "variants must come in variant-desc and variant-case pairs")
|
||||
|
||||
(when-not (= "SKIP" base-desc)
|
||||
(testing base-desc
|
||||
(test-fn base-case)))
|
||||
(testing base-desc
|
||||
(test-fn base-case))
|
||||
|
||||
(doseq [[variant-desc variant-case-or-fn] (partition 2 variants)]
|
||||
(when-not (= "SKIP" variant-desc)
|
||||
(testing variant-desc
|
||||
(test-fn (merge base-case
|
||||
(cond (fn? variant-case-or-fn) (variant-case-or-fn base-case)
|
||||
(map? variant-case-or-fn) variant-case-or-fn
|
||||
:else (throw (ex-info "Invalid variant case. Must be a fn or map."
|
||||
{:variant-case variant-case-or-fn})))))))))
|
||||
(testing variant-desc
|
||||
(test-fn (merge base-case
|
||||
(cond (fn? variant-case-or-fn) (variant-case-or-fn base-case)
|
||||
(map? variant-case-or-fn) variant-case-or-fn
|
||||
:else (throw (ex-info "Invalid variant case. Must be a fn or map."
|
||||
{:variant-case variant-case-or-fn}))))))))
|
||||
|
||||
@@ -141,6 +141,13 @@
|
||||
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
|
||||
(lib/aggregate (lib/count))
|
||||
(lib/breakout (meta/field-metadata :orders :created-at))
|
||||
;; HEY! HOW ARE WE SUPPOSED TO BREAKOUT ON A COLUMN FROM ANOTHER TABLE! WE NEED THE VERSION
|
||||
;; OF THE COLUMN WITH IMPLICIT JOIN INFO!
|
||||
;;
|
||||
;; TODO (Cam 9/15/25) -- because the tests are so busted here I had to write extra field
|
||||
;; resolution code just to work around these tests.
|
||||
;; See [[fallback-metadata-for-demonic-field-id-from-different-source-table]] for example.
|
||||
;; Maybe we should fix the tests.
|
||||
(lib/breakout (meta/field-metadata :products :category)))
|
||||
:row {"CREATED_AT" "2022-12-01T00:00:00+02:00"
|
||||
"CATEGORY" "Doohickey"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
|
||||
[metabase.lib.drill-thru.test-util.canned :as canned]
|
||||
[metabase.lib.drill-thru.zoom-in-bins :as zoom-in]
|
||||
[metabase.lib.schema.metadata :as lib.schema.metadata]
|
||||
[metabase.lib.test-metadata :as meta]))
|
||||
|
||||
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
|
||||
@@ -376,11 +377,10 @@
|
||||
(lib/breakout people-orders-query people-orders-column)
|
||||
|
||||
people-orders-clicked-column
|
||||
(-> (meta/field-metadata :orders :total)
|
||||
(lib/with-binning {:strategy :num-bins, :min-value 0, :max-value 160, :num-bins 8, :bin-width 20})
|
||||
;; I have to construct this weird column because I cannot find a way to reproduce
|
||||
;; the columns that lib/existing-breakouts is being passed
|
||||
(assoc :source-alias "Orders"))
|
||||
(lib/normalize ::lib.schema.metadata/column
|
||||
(-> (meta/field-metadata :orders :total)
|
||||
(lib/with-binning {:strategy :num-bins, :min-value 0, :max-value 160, :num-bins 8, :bin-width 20})
|
||||
(assoc :source-alias "Orders")))
|
||||
|
||||
people-orders-ref
|
||||
(first (lib/breakouts people-orders-query-breakout))
|
||||
@@ -393,10 +393,9 @@
|
||||
:value 80})]
|
||||
;; make sure we're testing the right thing
|
||||
(assert (get-in people-orders-ref [1 :join-alias]))
|
||||
(assert (nil? (:metabase.lib.join/join-alias people-orders-clicked-column)))
|
||||
;; somehow the column (as printed out in console.log) has :source-alias but not :metabase.lib.join/join-alias
|
||||
(assert (:source-alias people-orders-clicked-column))
|
||||
|
||||
;; `:source-alias` should get renamed to `:metabase.lib.join/join-alias` automatically when normalizing column metadata
|
||||
(assert (nil? (:source-alias people-orders-clicked-column)))
|
||||
(assert (:metabase.lib.join/join-alias people-orders-clicked-column))
|
||||
(assert (nil? (get-in orders-people-ref [1 :join-alias])))
|
||||
(assert (nil? (:metabase.lib.join/join-alias orders-people-clicked-column)))
|
||||
(assert (nil? (:source-alias orders-people-clicked-column)))
|
||||
@@ -404,4 +403,4 @@
|
||||
(testing "zoom-in binning should not depend on join order"
|
||||
(is (= orders-people-zoom
|
||||
(some-> people-orders-zoom
|
||||
(update :column dissoc :source-alias)))))))
|
||||
(update :column dissoc :metabase.lib.join/join-alias)))))))
|
||||
|
||||
@@ -210,12 +210,14 @@
|
||||
(lib/aggregate (lib/count))
|
||||
(lib/breakout (-> (meta/field-metadata :orders :created-at)
|
||||
(lib/with-temporal-bucket :year))))
|
||||
created-at-col (m/find-first #(= (:name %) "CREATED_AT")
|
||||
(lib/returned-columns base-query))
|
||||
_ (is (some? created-at-col))
|
||||
created-at-col (fn [query]
|
||||
{:post [(some? %)]}
|
||||
(m/find-first #(= (:name %) "CREATED_AT")
|
||||
(lib/returned-columns query)))
|
||||
multi-stage-query (lib.drill-thru.tu/append-filter-stage base-query "count")]
|
||||
(doseq [[query-type query] {"single-stage" base-query
|
||||
"multi-stage" multi-stage-query}
|
||||
:let [created-at-col (created-at-col query)]
|
||||
[message context] {"pivot cell (no column, value = NULL) (#36173)"
|
||||
{:value :null
|
||||
:column nil
|
||||
|
||||
@@ -624,25 +624,50 @@
|
||||
;;; The tests below are adapted from frontend/src/metabase-lib/drills.unit.spec.ts
|
||||
;;;
|
||||
|
||||
(deftest ^:parallel available-drill-thrus-test-1
|
||||
(deftest ^:parallel available-drill-thrus-test-1-mbql
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :unaggregated
|
||||
:column-name "ID"
|
||||
:query-kinds [:mbql]
|
||||
:expected [{:type :drill-thru/zoom
|
||||
:object-id (get-in lib.drill-thru.tu/test-queries ["ORDERS" :unaggregated :row "ID"])
|
||||
:many-pks? false}]}))
|
||||
|
||||
(deftest ^:parallel available-drill-thrus-test-2
|
||||
(deftest ^:parallel available-drill-thrus-test-1-native
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :unaggregated
|
||||
:column-name "ID"
|
||||
:query-kinds [:native]
|
||||
:expected [{:type :drill-thru/zoom
|
||||
:object-id (get-in lib.drill-thru.tu/test-queries ["ORDERS" :unaggregated :row "ID"])
|
||||
:many-pks? false}
|
||||
{:type :drill-thru/quick-filter, :operators [{:name "="} {:name "≠"}]}]}))
|
||||
|
||||
(deftest ^:parallel available-drill-thrus-test-2-mbql
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :unaggregated
|
||||
:column-name "USER_ID"
|
||||
:query-kinds [:mbql]
|
||||
:expected [{:type :drill-thru/fk-filter}
|
||||
{:type :drill-thru/fk-details
|
||||
:object-id (get-in lib.drill-thru.tu/test-queries ["ORDERS" :unaggregated :row "USER_ID"])
|
||||
:many-pks? false}]}))
|
||||
|
||||
(deftest ^:parallel available-drill-thrus-test-2-native
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :unaggregated
|
||||
:column-name "USER_ID"
|
||||
:query-kinds [:native]
|
||||
:expected [{:type :drill-thru/fk-filter}
|
||||
{:type :drill-thru/fk-details
|
||||
:object-id (get-in lib.drill-thru.tu/test-queries ["ORDERS" :unaggregated :row "USER_ID"])
|
||||
:many-pks? false}
|
||||
{:type :drill-thru/quick-filter, :operators [{:name "="} {:name "≠"}]}]}))
|
||||
|
||||
(deftest ^:parallel available-drill-thrus-test-3
|
||||
(lib.drill-thru.tu/test-drill-variants-with-merged-args
|
||||
lib.drill-thru.tu/test-available-drill-thrus
|
||||
@@ -783,27 +808,28 @@
|
||||
{:type :drill-thru/zoom-in.timeseries, :display-name "See this month by week"}]
|
||||
:native-drills #{:drill-thru/quick-filter}})))
|
||||
|
||||
;; FIXME: quick-filter gets returned for non-metric column (#34443)
|
||||
(deftest ^:parallel available-drill-thrus-test-11
|
||||
#_(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :aggregated
|
||||
:column-name "PRODUCT_ID"
|
||||
:expected [{:type :drill-thru/fk-filter}
|
||||
{:type :drill-thru/fk-details, :object-id 3, :many-pks? false}
|
||||
{:row-count 2, :table-name "Orders", :type :drill-thru/underlying-records}]}))
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :aggregated
|
||||
:column-name "PRODUCT_ID"
|
||||
:expected [{:type :drill-thru/automatic-insights}
|
||||
{:type :drill-thru/fk-filter}
|
||||
{:type :drill-thru/fk-details, :object-id 3, :many-pks? false}
|
||||
{:type :drill-thru/underlying-records, :row-count pos-int?, :table-name "Orders"}
|
||||
{:type :drill-thru/zoom-in.timeseries}]}))
|
||||
|
||||
;; FIXME: quick-filter gets returned for non-metric column (#34443)
|
||||
(deftest ^:parallel available-drill-thrus-test-12
|
||||
#_(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :aggregated
|
||||
:column-name "CREATED_AT"
|
||||
:expected [{:type :drill-thru/quick-filter, :operators [{:name "<"}
|
||||
{:name ">"}
|
||||
{:name "="}
|
||||
{:name "≠"}]}
|
||||
{:row-count 3, :table-name "Orders", :type :drill-thru/underlying-records}]}))
|
||||
(lib.drill-thru.tu/test-available-drill-thrus
|
||||
{:click-type :cell
|
||||
:query-type :aggregated
|
||||
:column-name "CREATED_AT"
|
||||
:expected [{:type :drill-thru/automatic-insights}
|
||||
{:type :drill-thru/quick-filter, :operators [{:name "<"}
|
||||
{:name ">"}
|
||||
{:name "="}
|
||||
{:name "≠"}]}
|
||||
{:type :drill-thru/underlying-records, :row-count pos-int?, :table-name "Orders"}]}))
|
||||
|
||||
;; FIXME: for some reason the results for aggregated query are not correct (#34223, #34341)
|
||||
(deftest ^:parallel available-drill-thrus-test-13
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
(gen/fmap rand-nth
|
||||
(mg/generator [:repeat {:min 2, :max 2} qualified-keyword?]))]))
|
||||
|
||||
(def map-pair-generator
|
||||
(def ^:private map-pair-generator
|
||||
"Generate a key-value pair using the rules discussed above."
|
||||
(let [expr-generator (gen/recursive-gen
|
||||
(fn [_gen] (mg/generator [:ref ::expr]))
|
||||
@@ -238,7 +238,9 @@
|
||||
haystack (lib.metadata.calculation/returned-columns query)]
|
||||
(testing "Should find a matching column ignoring :temporal-unit if needed (#32920)"
|
||||
(is (=? column
|
||||
(lib.equality/find-matching-column needle haystack))))
|
||||
(lib.equality/find-matching-column
|
||||
query -1 needle haystack
|
||||
{:find-matching-column/ignore-binning-and-bucketing? true}))))
|
||||
(testing "Should find a matching ref ignoring :temporal-unit if needed (#32920)"
|
||||
(is (=? [:field
|
||||
{:lib/uuid string?
|
||||
@@ -263,8 +265,6 @@
|
||||
{:name "DISCOUNT", :selected? false}
|
||||
{:name "CREATED_AT", :selected? true}
|
||||
{:name "QUANTITY", :selected? false}]
|
||||
(mapv #(select-keys % [:name :selected?])
|
||||
(lib.equality/mark-selected-columns cols selected))
|
||||
(mapv #(select-keys % [:name :selected?])
|
||||
(lib.equality/mark-selected-columns query -1 cols selected)))))))
|
||||
|
||||
@@ -287,7 +287,6 @@
|
||||
[:field {} (meta/id :categories :name)]]
|
||||
refs))
|
||||
(is (= (nth cols 7)
|
||||
(lib.equality/find-matching-column a-ref cols)
|
||||
(lib.equality/find-matching-column query -1 a-ref cols))))))
|
||||
|
||||
(deftest ^:parallel find-matching-column-from-column-test
|
||||
@@ -324,7 +323,6 @@
|
||||
[:name :lib/original-join-alias :lib/original-name :lib/deduplicated-name :lib/source-column-alias :lib/desired-column-alias]
|
||||
cols)))
|
||||
(are [a-ref expected-index] (= (nth cols expected-index)
|
||||
(lib.equality/find-matching-column a-ref cols)
|
||||
(lib.equality/find-matching-column query -1 a-ref cols))
|
||||
;; Good ref.
|
||||
[:field {:lib/uuid (str (random-uuid)), :base-type :type/Text}
|
||||
@@ -366,26 +364,26 @@
|
||||
(map #(dissoc % :name)))
|
||||
(sorted (lib/returned-columns query))))
|
||||
(testing "matches the defaults by ID"
|
||||
(doseq [col-key (meta/fields :orders)]
|
||||
(doseq [col-key (meta/fields :orders)
|
||||
:let [a-ref (-> (meta/field-metadata :orders col-key)
|
||||
table-col
|
||||
lib/ref)]]
|
||||
(is (=? (table-col (meta/field-metadata :orders col-key))
|
||||
(-> (meta/field-metadata :orders col-key)
|
||||
table-col
|
||||
lib/ref
|
||||
(lib.equality/find-matching-column visible))))))
|
||||
(lib.equality/find-matching-column query -1 a-ref visible)))))
|
||||
(testing "matches the joined columns by ID"
|
||||
(doseq [col-key (meta/fields :orders)]
|
||||
(doseq [col-key (meta/fields :orders)
|
||||
:let [a-ref (-> (meta/field-metadata :orders col-key)
|
||||
join-col
|
||||
lib/ref)]]
|
||||
(is (=? (join-col (meta/field-metadata :orders col-key))
|
||||
(-> (meta/field-metadata :orders col-key)
|
||||
join-col
|
||||
lib/ref
|
||||
(lib.equality/find-matching-column visible))))))
|
||||
(lib.equality/find-matching-column query -1 a-ref visible)))))
|
||||
(testing "the matches of default and joins are distinct"
|
||||
(let [matches (for [col-key (meta/fields :orders)
|
||||
col-fn [table-col join-col]]
|
||||
(-> (meta/field-metadata :orders col-key)
|
||||
col-fn
|
||||
lib/ref
|
||||
(lib.equality/find-matching-column visible)))]
|
||||
col-fn [table-col join-col]
|
||||
:let [a-ref (-> (meta/field-metadata :orders col-key)
|
||||
col-fn
|
||||
lib/ref)]]
|
||||
(lib.equality/find-matching-column query -1 a-ref visible))]
|
||||
(is (every? some? matches))
|
||||
(is (= (count matches)
|
||||
(count (set matches)))))))))
|
||||
@@ -409,79 +407,23 @@
|
||||
{:lib/desired-column-alias "Orders__ID"}]
|
||||
(lib/returned-columns just-3)))
|
||||
(testing "matching the four fields against"
|
||||
(let [hr-own-id {:lib/type :metadata/column
|
||||
:description "Own ID"
|
||||
:base-type :type/BigInteger
|
||||
:semantic-type :type/PK
|
||||
:effective-type :type/BigInteger
|
||||
:table-id (meta/id :orders)
|
||||
:id (meta/id :orders :id)
|
||||
:name "ID"
|
||||
:lib/source :source/table-defaults
|
||||
:fk-target-field-id nil
|
||||
:parent-id nil
|
||||
:display-name "ID"
|
||||
:position 0}
|
||||
hr-own-tax {:lib/type :metadata/column
|
||||
:description "Own Tax"
|
||||
:base-type :type/Float
|
||||
:semantic-type nil
|
||||
:effective-type :type/Float
|
||||
:table-id (meta/id :orders)
|
||||
:id (meta/id :orders :tax)
|
||||
:name "TAX"
|
||||
:lib/source :source/table-defaults
|
||||
:fk-target-field-id nil
|
||||
:parent-id nil
|
||||
:display-name "Tax"
|
||||
:position 4}
|
||||
hr-join-id {:lib/type :metadata/column
|
||||
:description "Join ID"
|
||||
:base-type :type/BigInteger
|
||||
:semantic-type :type/PK
|
||||
:effective-type :type/BigInteger
|
||||
:table-id (meta/id :orders)
|
||||
:id (meta/id :orders :id)
|
||||
:name "ID_2"
|
||||
:metabase.lib.join/join-alias "Orders"
|
||||
:lib/source :source/joins
|
||||
:fk-target-field-id nil
|
||||
:parent-id nil
|
||||
:display-name "Orders → ID"
|
||||
:position 0}
|
||||
hr-join-tax {:lib/type :metadata/column
|
||||
:description "Join Tax"
|
||||
:base-type :type/Float
|
||||
:semantic-type nil
|
||||
:effective-type :type/Float
|
||||
:table-id (meta/id :orders)
|
||||
:id (meta/id :orders :tax)
|
||||
:name "TAX_2"
|
||||
:metabase.lib.join/join-alias "Orders"
|
||||
:lib/source :source/joins
|
||||
:fk-target-field-id nil
|
||||
:parent-id nil
|
||||
:display-name "Orders → Tax"
|
||||
:position 4}
|
||||
ret-4 [hr-own-id hr-own-tax hr-join-id hr-join-tax]
|
||||
ret-3 [hr-own-id hr-own-tax hr-join-id]
|
||||
refs (for [join-alias [nil "Orders"]
|
||||
[column coltype] [[:id :type/Integer] [:tax :type/Float]]]
|
||||
[:field (merge {:lib/uuid (str (random-uuid))
|
||||
:base-type coltype
|
||||
:effective-type coltype}
|
||||
(when join-alias
|
||||
{:join-alias join-alias}))
|
||||
(meta/id :orders column)])
|
||||
exp-4 [{:display-name "ID"}
|
||||
{:display-name "Tax"}
|
||||
{:display-name "Orders → ID"}
|
||||
{:display-name "Orders → Tax"}]
|
||||
exp-3 (update exp-4 3 (constantly nil))]
|
||||
(let [ret-4 (binding [lib.metadata.calculation/*display-name-style* :long]
|
||||
(lib/returned-columns all-4)) #_[hr-own-id hr-own-tax hr-join-id hr-join-tax]
|
||||
ret-3 (take 3 ret-4)
|
||||
refs (for [join-alias [nil "Orders"]
|
||||
[column coltype] [[:id :type/Integer] [:tax :type/Float]]]
|
||||
[:field (merge {:lib/uuid (str (random-uuid))
|
||||
:base-type coltype
|
||||
:effective-type coltype}
|
||||
(when join-alias
|
||||
{:join-alias join-alias}))
|
||||
(meta/id :orders column)])
|
||||
exp-4 [{:display-name "ID"}
|
||||
{:display-name "Tax"}
|
||||
{:display-name "Orders → ID"}
|
||||
{:display-name "Orders → Tax"}]
|
||||
exp-3 (update exp-4 3 (constantly nil))]
|
||||
(testing "all-4 matches everything"
|
||||
(is (=? exp-4
|
||||
(->> refs
|
||||
(map #(lib.equality/find-matching-column % ret-4)))))
|
||||
(is (=? exp-4
|
||||
(->> refs
|
||||
(map #(lib.equality/find-matching-column all-4 -1 % ret-4)))))
|
||||
@@ -489,9 +431,6 @@
|
||||
#_{:clj-kondo/ignore [:discouraged-var]}
|
||||
(lib.equality/find-column-indexes-for-refs all-4 -1 refs ret-4))))
|
||||
(testing "just-3 does not match the joined TAX"
|
||||
(is (=? exp-3
|
||||
(->> refs
|
||||
(map #(lib.equality/find-matching-column % ret-3)))))
|
||||
(is (=? exp-3
|
||||
(->> refs
|
||||
(map #(lib.equality/find-matching-column just-3 -1 % ret-3)))))
|
||||
@@ -503,18 +442,6 @@
|
||||
(let [query (-> (lib.tu/venues-query)
|
||||
(lib/aggregate (lib/count)))
|
||||
[ag] (lib/aggregations query)]
|
||||
(testing "without passing query"
|
||||
(testing "matches with UUID"
|
||||
(is (=? {:display-name "Count", :lib/source :source/aggregations}
|
||||
(lib.equality/find-matching-column
|
||||
[:aggregation {:lib/uuid (str (random-uuid))} (lib.options/uuid ag)]
|
||||
(lib/returned-columns query)))))
|
||||
(testing "fails with bad UUID but good source-name"
|
||||
(is (nil? (lib.equality/find-matching-column
|
||||
[:aggregation {:lib/uuid (str (random-uuid))
|
||||
:lib/source-name "count"}
|
||||
"this is a bad UUID"]
|
||||
(lib/returned-columns query))))))
|
||||
(testing "when passing query"
|
||||
(testing "matches with UUID"
|
||||
(is (=? {:display-name "Count", :lib/source :source/aggregations}
|
||||
@@ -534,6 +461,8 @@
|
||||
(deftest ^:parallel find-matching-column-expression-test
|
||||
(is (=? {:name "expr", :lib/source :source/expressions}
|
||||
(lib.equality/find-matching-column
|
||||
(lib.tu/query-with-expression)
|
||||
-1
|
||||
[:expression {:lib/uuid (str (random-uuid))} "expr"]
|
||||
(lib/visible-columns (lib.tu/query-with-expression))))))
|
||||
|
||||
@@ -599,26 +528,28 @@
|
||||
query (-> base
|
||||
(lib/breakout base-user-source)
|
||||
(lib/breakout base-category))
|
||||
returned (map #(assoc %1 :source-alias %2)
|
||||
(lib/returned-columns query)
|
||||
[nil "PEOPLE__via__USER_ID" "PRODUCTS__via__PRODUCT_ID" nil])]
|
||||
returned (lib/returned-columns query)]
|
||||
(is (= :source/implicitly-joinable (:lib/source base-user-source)))
|
||||
(is (= :source/implicitly-joinable (:lib/source base-category)))
|
||||
(is (= 4 (count returned)))
|
||||
(is (= (map :name returned)
|
||||
(for [col returned]
|
||||
(:name (lib.equality/find-matching-column query -1 (lib/ref col) returned)))))))
|
||||
(:name (lib.equality/find-matching-column query -1 (lib/ref col) returned))))))))
|
||||
|
||||
(deftest ^:parallel implicitly-joinable-columns-test-2
|
||||
(testing "implicitly joinable columns via joins should be matched correctly"
|
||||
(let [query (lib.tu/query-with-self-join)
|
||||
visible-cols (lib/visible-columns query)]
|
||||
(let [query (lib.tu/query-with-self-join)
|
||||
visible-cols (lib/visible-columns query)]
|
||||
(doseq [col visible-cols]
|
||||
(is (= col (lib/find-matching-column (lib/ref col) visible-cols))))))
|
||||
(is (= col (lib/find-matching-column query -1 (lib/ref col) visible-cols)))))))
|
||||
|
||||
(deftest ^:parallel implicitly-joinable-columns-test-3
|
||||
(testing "implicitly joinable columns from the previous query stage are matched correctly"
|
||||
(let [query (-> (lib.tu/query-with-self-join) lib/append-stage)
|
||||
visible-cols (lib/visible-columns query)
|
||||
implicit-cols (filter #(= :source/implicitly-joinable (:lib/source %)) visible-cols)]
|
||||
(doseq [col implicit-cols]
|
||||
(is (= col (lib.equality/find-matching-column (lib.ref/ref col) implicit-cols)))))))
|
||||
(is (= col (lib.equality/find-matching-column query -1 (lib.ref/ref col) implicit-cols)))))))
|
||||
|
||||
(deftest ^:parallel field-refs-to-custom-expressions-test
|
||||
(testing "custom columns that wrap a Field must not have `:id` (#44940)"
|
||||
@@ -630,12 +561,11 @@
|
||||
(testing "different columns"
|
||||
(is (int? (:id created-at)))
|
||||
(is (nil? (:id ca-expr))))
|
||||
|
||||
(testing "both refs should match correctly"
|
||||
(is (= created-at
|
||||
(lib.equality/find-matching-column (lib/ref created-at) columns)))
|
||||
(lib.equality/find-matching-column query -1 (lib/ref created-at) columns)))
|
||||
(is (= ca-expr
|
||||
(lib.equality/find-matching-column (lib/ref ca-expr) columns)))))))
|
||||
(lib.equality/find-matching-column query -1 (lib/ref ca-expr) columns)))))))
|
||||
|
||||
(deftest ^:parallel disambiguate-matches-using-temporal-unit-if-needed-test
|
||||
(let [created-at-month (lib/with-temporal-bucket (meta/field-metadata :orders :created-at) :month)
|
||||
@@ -644,6 +574,8 @@
|
||||
created-at-year]]
|
||||
(is (= col
|
||||
(lib.equality/find-matching-column
|
||||
(lib/query meta/metadata-provider (meta/table-metadata :orders))
|
||||
-1
|
||||
(lib/ref col)
|
||||
[created-at-month
|
||||
created-at-year]))))))
|
||||
@@ -656,9 +588,13 @@
|
||||
latitude-20]]
|
||||
(is (= col
|
||||
(lib.equality/find-matching-column
|
||||
(lib/query meta/metadata-provider (meta/table-metadata :people))
|
||||
-1
|
||||
(lib/ref col)
|
||||
[latitude-10
|
||||
latitude-20]))))))
|
||||
latitude-20])))))))
|
||||
|
||||
(deftest ^:parallel disambiguate-matches-using-binning-if-needed-test-2
|
||||
(testing "'num-bins' binning strategy"
|
||||
(let [total-10 (lib/with-binning (meta/field-metadata :orders :total) {:strategy :num-bins, :num-bins 10})
|
||||
total-20 (lib/with-binning (meta/field-metadata :orders :total) {:strategy :num-bins, :num-bins 20})]
|
||||
@@ -666,23 +602,26 @@
|
||||
total-20]]
|
||||
(is (= col
|
||||
(lib.equality/find-matching-column
|
||||
(lib/query meta/metadata-provider (meta/table-metadata :orders))
|
||||
-1
|
||||
(lib/ref col)
|
||||
[total-10
|
||||
total-20])))))))
|
||||
|
||||
(deftest ^:parallel find-matching-column-by-id-with-expression-aliasing-joined-column-test
|
||||
(testing "find-matching-column should be able to find columns based on ID even when a joined column is aliased as an expression (#44940)"
|
||||
(let [a-ref [:field {:lib/uuid (str (random-uuid))
|
||||
:base-type :type/Text
|
||||
:join-alias "Cat"}
|
||||
(meta/id :categories :name)]
|
||||
(let [a-ref (fn []
|
||||
[:field {:lib/uuid (str (random-uuid))
|
||||
:base-type :type/Text
|
||||
:join-alias "Cat"}
|
||||
(meta/id :categories :name)])
|
||||
query (-> (lib.tu/query-with-join)
|
||||
(lib/expression "Joined Name" a-ref))
|
||||
(lib/expression "Joined Name" (a-ref)))
|
||||
cols (lib/returned-columns query)]
|
||||
(is (=? {:name "NAME_2"
|
||||
:lib/desired-column-alias "Cat__NAME"
|
||||
:id (meta/id :categories :name)}
|
||||
(lib.equality/find-matching-column query -1 a-ref cols))))))
|
||||
(lib.equality/find-matching-column query -1 (a-ref) cols))))))
|
||||
|
||||
(deftest ^:parallel find-matching-ref-multiple-breakouts-test
|
||||
(testing "should be able to distinguish between multiple breakouts of the same column in the previous stage"
|
||||
@@ -750,6 +689,8 @@
|
||||
cols (lib/visible-columns query)]
|
||||
(is (=? {:name "ID_2", :lib/source-column-alias "Products__ID"}
|
||||
(lib.equality/find-matching-column
|
||||
query
|
||||
-1
|
||||
[:field
|
||||
{:base-type :type/BigInteger, :lib/uuid "00000000-0000-0000-0000-000000000002"}
|
||||
"Products__ID"]
|
||||
@@ -759,13 +700,14 @@
|
||||
(let [field-ref [:field {:lib/uuid "86e6d41c-d693-4f08-ae30-7ad411da8ec7"
|
||||
:effective-type :type/DateTime
|
||||
:base-type :type/DateTime
|
||||
:inherited-temporal-unit :month} 39]
|
||||
:inherited-temporal-unit :month}
|
||||
(meta/id :orders :created-at)]
|
||||
cols [{:base-type :type/DateTime
|
||||
:created-at "2025-06-24T20:58:39.593446-07:00"
|
||||
:description "The date and time an order was submitted."
|
||||
:display-name "Created At: Year"
|
||||
:effective-type :type/DateTime
|
||||
:id 39
|
||||
:id (meta/id :orders :created-at)
|
||||
:inherited-temporal-unit :year
|
||||
:last-analyzed "2025-06-24T20:58:41.088313-07:00"
|
||||
:lib/deduplicated-name "CREATED_AT"
|
||||
@@ -778,30 +720,35 @@
|
||||
:lib/type :metadata/column
|
||||
:name "CREATED_AT"
|
||||
:semantic-type :type/CreationTimestamp
|
||||
:table-id 5
|
||||
:table-id (meta/id :orders)
|
||||
:updated-at "2025-06-24T20:58:41.088313-07:00"}
|
||||
{:base-type :type/DateTime
|
||||
:created-at "2025-06-24T20:58:39.593446-07:00"
|
||||
:description "The date and time an order was submitted."
|
||||
:display-name "Created At: Month"
|
||||
:effective-type :type/DateTime
|
||||
:id 39
|
||||
:id (meta/id :orders :created-at)
|
||||
:inherited-temporal-unit :month
|
||||
:last-analyzed "2025-06-24T20:58:41.088313-07:00"
|
||||
:lib/deduplicated-name "CREATED_AT_2"
|
||||
:lib/desired-column-alias "CREATED_AT_2"
|
||||
:lib/deduplicated-name "CREATED_AT"
|
||||
:lib/desired-column-alias "CREATED_AT"
|
||||
:lib/original-display-name "Created At"
|
||||
:lib/original-name "CREATED_AT"
|
||||
:lib/source :source/previous-stage
|
||||
:lib/source-column-alias "CREATED_AT_2"
|
||||
:lib/source-column-alias "CREATED_AT"
|
||||
:lib/source-uuid "1f16a57a-2afd-4c92-8d6c-b41062235a49"
|
||||
:lib/type :metadata/column
|
||||
:name "CREATED_AT"
|
||||
:semantic-type :type/CreationTimestamp
|
||||
:table-id 5
|
||||
:table-id (meta/id :orders)
|
||||
:updated-at "2025-06-24T20:58:41.088313-07:00"}]]
|
||||
(is (=? {:display-name "Created At: Month"}
|
||||
(lib.equality/find-matching-column field-ref cols)))))
|
||||
(lib.equality/find-matching-column
|
||||
(-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
|
||||
lib/append-stage)
|
||||
-1
|
||||
field-ref
|
||||
cols)))))
|
||||
|
||||
(deftest ^:parallel mark-selected-columns-works-for-js-use-cases-test
|
||||
(testing "Does mark-selected-columns actually work for the uses cases in [[metabase.lib.js/visible-columns*]] now?"
|
||||
@@ -840,13 +787,6 @@
|
||||
{:display-name "Count"}]
|
||||
(lib/visible-columns query)))
|
||||
(testing `lib.equality/mark-selected-columns
|
||||
(testing "2-arity"
|
||||
(is (=? [{:display-name "Created At: Year"}
|
||||
{:display-name "Created At: Month"}
|
||||
{:display-name "Count"}]
|
||||
(lib.equality/mark-selected-columns
|
||||
(lib/visible-columns query)
|
||||
(lib/returned-columns query)))))
|
||||
(testing "4-arity"
|
||||
(is (=? [{:display-name "Created At: Year"}
|
||||
{:display-name "Created At: Month"}
|
||||
@@ -893,13 +833,6 @@
|
||||
{:display-name "Count"}]
|
||||
(lib/visible-columns query)))
|
||||
(testing `lib.equality/mark-selected-columns
|
||||
(testing "2-arity"
|
||||
(is (=? [{:display-name "Total: 10 bins"}
|
||||
{:display-name "Total: 50 bins"}
|
||||
{:display-name "Count"}]
|
||||
(lib.equality/mark-selected-columns
|
||||
(lib/visible-columns query)
|
||||
(lib/returned-columns query)))))
|
||||
(testing "4-arity"
|
||||
(is (=? [{:display-name "Total: 10 bins"}
|
||||
{:display-name "Total: 50 bins"}
|
||||
@@ -961,7 +894,7 @@
|
||||
(let [cols [{:base-type :type/DateTime
|
||||
:display-name "Created At"
|
||||
:effective-type :type/DateTime
|
||||
:id 66
|
||||
:id (meta/id :orders :created-at)
|
||||
:lib/card-id 5
|
||||
:lib/desired-column-alias "CREATED_AT"
|
||||
:lib/model-display-name "Created At"
|
||||
@@ -974,7 +907,7 @@
|
||||
{:base-type :type/DateTime
|
||||
:display-name "Products → Created At"
|
||||
:effective-type :type/DateTime
|
||||
:id 66
|
||||
:id (meta/id :orders :created-at)
|
||||
:lib/card-id 5
|
||||
:lib/desired-column-alias "CREATED_AT_2"
|
||||
:lib/model-display-name "Products → Created At"
|
||||
@@ -989,9 +922,13 @@
|
||||
:effective-type :type/DateTime
|
||||
:base-type :type/DateTime
|
||||
:metabase.lib.query/transformation-added-base-type true}
|
||||
66]]
|
||||
(is (=? {:id 66, :display-name "Created At"}
|
||||
(lib.equality/find-matching-column a-ref cols))))))
|
||||
(meta/id :orders :created-at)]
|
||||
mp (lib.tu/mock-metadata-provider
|
||||
meta/metadata-provider
|
||||
{:cards [{:id 5
|
||||
:dataset-query (lib/query meta/metadata-provider (meta/table-metadata :orders))}]})]
|
||||
(is (=? {:id (meta/id :orders :created-at), :display-name "Created At"}
|
||||
(lib.equality/find-matching-column (lib/query mp (lib.metadata/card mp 5)) -1 a-ref cols))))))
|
||||
|
||||
(deftest ^:parallel match-by-source-uuid-test
|
||||
(let [col {:base-type :type/BigInteger
|
||||
@@ -1009,7 +946,6 @@
|
||||
:metabase.lib.join/join-alias "Orders"
|
||||
:name "ID_2"
|
||||
:semantic-type :type/PK
|
||||
:source-alias "Orders"
|
||||
:table-id 55060}
|
||||
refs [[:field
|
||||
{:lib/uuid "1c2a0643-f25c-4099-a2d5-7c7e790b632f"
|
||||
@@ -1050,7 +986,11 @@
|
||||
(deftest ^:parallel find-matching-column-prefer-exact-matches-test
|
||||
(testing `lib.equality/find-matching-column
|
||||
(testing "For field name refs, prefer matches that have the same [lack of] join alias and same source-column-alias"
|
||||
(let [a-ref [:field
|
||||
(let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :venues))
|
||||
(lib/join (-> (lib/join-clause (meta/table-metadata :categories))
|
||||
(lib/with-join-alias "Cat")))
|
||||
lib/append-stage)
|
||||
a-ref [:field
|
||||
{:lib/uuid "00000000-0000-0000-0000-000000000000"
|
||||
:effective-type :type/Text
|
||||
:base-type :type/Text}
|
||||
@@ -1085,7 +1025,7 @@
|
||||
(doseq [cols [[col-1 col-2]
|
||||
[col-2 col-1]]]
|
||||
(is (= col-2
|
||||
(lib.equality/find-matching-column a-ref cols))))))))
|
||||
(lib.equality/find-matching-column query -1 a-ref cols))))))))
|
||||
|
||||
(deftest ^:parallel never-return-match-with-different-id-test
|
||||
(testing "find-matching-ref should never return a match with a different :id"
|
||||
@@ -1198,7 +1138,7 @@
|
||||
{:lib/source-column-alias "BODY", :display-name "Body", :selected? true}
|
||||
{:lib/source-column-alias "CREATED_AT", :display-name "Created At", :selected? true}
|
||||
;; the following two Card 1 → ID should have :selected? true
|
||||
{:lib/source-column-alias "ID", :display-name "Card 1 → ID", :selected? true} ; FIXME - these should be true
|
||||
{:lib/source-column-alias "ID", :display-name "Card 1 → ID", :selected? true}
|
||||
{:lib/source-column-alias "o__ID", :display-name "Card 1 → ID", :selected? true}
|
||||
;; these are implicitly joinable fields, :selected? false is right
|
||||
{:lib/source-column-alias "ID", :display-name "ID", :selected? false}
|
||||
|
||||
@@ -178,11 +178,12 @@
|
||||
(lib/query metadata-provider)
|
||||
lib/returned-columns
|
||||
(m/index-by :name))
|
||||
query (-> (lib/query metadata-provider (meta/table-metadata :checkins))
|
||||
query (-> (lib/query metadata-provider (meta/table-metadata :users))
|
||||
(lib/join (lib/with-join-alias
|
||||
(lib/join-clause (lib.metadata/card metadata-provider 1)
|
||||
[(lib/= (meta/field-metadata :users :id)
|
||||
(lib/ref (get cols "USER_ID")))])
|
||||
[:field {:lib/uuid "00000000-0000-0000-0000-000000000000", :base-type :type/Integer}
|
||||
"USER_ID"])])
|
||||
"checkins_by_user"))
|
||||
(lib/breakout (lib/with-temporal-bucket (meta/field-metadata :users :last-login) :month))
|
||||
(lib/aggregate (lib/avg (lib/with-join-alias (lib/ref (get cols "count")) "checkins_by_user"))))]
|
||||
@@ -420,9 +421,6 @@
|
||||
:lib/original-display-name "Category"
|
||||
:lib/original-name "CATEGORY"
|
||||
:lib/original-join-alias "Products"
|
||||
;; this key is DEPRECATED (see description in column metadata schema) but still used (FOR
|
||||
;; NOW) (QUE-1403)
|
||||
:source-alias "Products"
|
||||
:lib/source :source/card ; or is it supposed to be `:source/table-defaults`
|
||||
:lib/source-uuid (lib.options/uuid breakout-ref)
|
||||
:lib/type :metadata/column
|
||||
@@ -1485,7 +1483,6 @@
|
||||
(is (=? {:display-name "ID → Name"
|
||||
:id (meta/id :categories :name)
|
||||
:semantic-type :type/Name
|
||||
:source-alias "J"
|
||||
:lib/deduplicated-name "NAME_2"
|
||||
:lib/original-fk-field-id (meta/id :venues :id)
|
||||
:lib/original-join-alias "J"
|
||||
@@ -1499,7 +1496,6 @@
|
||||
(is (=? {:display-name "Category → Name"
|
||||
:id (meta/id :categories :name)
|
||||
:semantic-type :type/Name
|
||||
:source-alias "J"
|
||||
:lib/deduplicated-name "NAME"
|
||||
:lib/original-fk-field-id (meta/id :venues :category-id)
|
||||
:lib/original-join-alias "J"
|
||||
@@ -1508,3 +1504,34 @@
|
||||
:lib/source-column-alias "CATEGORIES__via__CATEGORY_ID__NAME"
|
||||
:metabase.lib.join/join-alias "J"}
|
||||
(lib.field.resolution/resolve-field-ref query 0 field-ref)))))))))
|
||||
|
||||
(deftest ^:parallel satanic-resolution-of-implicitly-joined-ref-without-source-field-test
|
||||
(testing "Can we resolve the 'ref from hell' from [[metabase.lib.drill-thru.zoom-in-bins-test/zoom-in-bins-available-test]]?"
|
||||
(let [query (lib/query
|
||||
meta/metadata-provider
|
||||
{:lib/type :mbql/query
|
||||
:database (meta/id)
|
||||
:stages [{:lib/type :mbql.stage/mbql
|
||||
:source-table (meta/id :orders)
|
||||
:aggregation [[:count {:lib/uuid "cfdf2003-e7ca-4d09-b1b6-349afc76e70b"}]]
|
||||
:breakout [[:field
|
||||
{:lib/uuid "24803786-506f-4029-b298-c85b79cf26e4", :effective-type :type/Text, :base-type :type/Text}
|
||||
(meta/id :products :category)]]}]})]
|
||||
(is (=? {:base-type :type/Text
|
||||
:display-name "Category"
|
||||
:effective-type :type/Text
|
||||
:fk-field-id (meta/id :orders :product-id)
|
||||
:id (meta/id :products :category)
|
||||
:name "CATEGORY"
|
||||
:semantic-type :type/Category
|
||||
:table-id (meta/id :products)
|
||||
:lib/source :source/implicitly-joinable
|
||||
:lib/source-column-alias "CATEGORY"
|
||||
:lib/source-uuid "24803786-506f-4029-b298-c85b79cf26e4"
|
||||
:lib/type :metadata/column
|
||||
::lib.field.resolution/fallback-metadata? true}
|
||||
(lib.field.resolution/resolve-field-ref
|
||||
query 0
|
||||
[:field
|
||||
{:lib/uuid "24803786-506f-4029-b298-c85b79cf26e4", :effective-type :type/Text, :base-type :type/Text}
|
||||
(meta/id :products :category)]))))))
|
||||
|
||||
@@ -72,11 +72,19 @@
|
||||
expression-ref (last (lib/fields query))]
|
||||
(is (=? [:expression {} "CATEGORY"]
|
||||
expression-ref))
|
||||
;; should return a column with properties like:
|
||||
;; {... :lib/source :source/expressions, :name "CATEGORY", :lib/desired-column-alias "CATEGORY_2"}
|
||||
(is (nil? (lib.equality/find-matching-column
|
||||
expression-ref
|
||||
(lib/visible-columns query)))))))
|
||||
(is (=? {:base-type :type/Text
|
||||
:display-name "CATEGORY"
|
||||
:effective-type :type/Text
|
||||
:name "CATEGORY"
|
||||
:lib/expression-name "CATEGORY"
|
||||
:lib/source :source/expressions
|
||||
:lib/source-column-alias "CATEGORY"
|
||||
:lib/type :metadata/column}
|
||||
(lib.equality/find-matching-column
|
||||
query
|
||||
-1
|
||||
expression-ref
|
||||
(lib/visible-columns query)))))))
|
||||
|
||||
(deftest ^:parallel returned-columns-bad-field-refs-test
|
||||
(let [query (lib/query
|
||||
|
||||
@@ -227,17 +227,22 @@
|
||||
(let [date-field (assoc (meta/field-metadata :people :birth-date)
|
||||
:base-type :type/Text
|
||||
:effective-type :type/Date
|
||||
:coercion-strategy :Coercion/ISO8601->Date)
|
||||
:coercion-strategy :Coercion/ISO8601->Date
|
||||
:table-id (meta/id :venues))
|
||||
datetime-field (assoc (meta/field-metadata :reviews :created-at)
|
||||
:table-id (meta/id :venues))
|
||||
time-field (assoc (meta/field-metadata :orders :created-at)
|
||||
:base-type :type/Time
|
||||
:effective-type :type/Time)
|
||||
:effective-type :type/Time
|
||||
:table-id (meta/id :venues))
|
||||
metadata-provider (lib.tu/mock-metadata-provider
|
||||
meta/metadata-provider
|
||||
{:fields [date-field
|
||||
datetime-field
|
||||
time-field]})
|
||||
query (lib/query metadata-provider (meta/table-metadata :venues))]
|
||||
{:fields {:date date-field
|
||||
:datetime (meta/field-metadata :reviews :created-at)
|
||||
:datetime datetime-field
|
||||
:time time-field}
|
||||
:metadata-provider metadata-provider
|
||||
:query query}))
|
||||
@@ -1397,15 +1402,18 @@
|
||||
(lib.metadata.calculation/returned-columns hidden)))
|
||||
(is (=? (map #(dissoc % :lib/source) exp-hidden)
|
||||
(filter :selected? (lib.equality/mark-selected-columns
|
||||
hidden
|
||||
-1
|
||||
(lib.metadata.calculation/visible-columns hidden)
|
||||
(lib.metadata.calculation/returned-columns hidden)))))
|
||||
|
||||
(testing "and showing it again"
|
||||
(let [shown (lib/add-field query -1 to-hide)]
|
||||
(is (=? exp-shown
|
||||
(lib.metadata.calculation/returned-columns shown)))
|
||||
(is (=? (map #(dissoc % :lib/source) exp-shown)
|
||||
(filter :selected? (lib.equality/mark-selected-columns
|
||||
hidden
|
||||
-1
|
||||
(lib.metadata.calculation/visible-columns shown)
|
||||
(lib.metadata.calculation/returned-columns shown)))))))))))))
|
||||
|
||||
@@ -1490,7 +1498,7 @@
|
||||
(dissoc :id :table-id)))
|
||||
join-cols [(-> (meta/field-metadata :products :category)
|
||||
(assoc :lib/source :source/card
|
||||
:source-alias "Products")
|
||||
:lib/original-join-alias "Products")
|
||||
(dissoc :id :table-id))]
|
||||
implicit-cols (for [col (meta/fields :people)]
|
||||
(-> (meta/field-metadata :people col)
|
||||
|
||||
@@ -483,12 +483,12 @@
|
||||
:effective-type :type/Boolean
|
||||
:semantic-type nil))
|
||||
|
||||
(defn- check-display-names [tests]
|
||||
(defn- check-display-names [source-table tests]
|
||||
(let [metadata-provider (lib/composed-metadata-provider
|
||||
(lib.tu/mock-metadata-provider
|
||||
{:fields [last-online-time is-active]})
|
||||
meta/metadata-provider)
|
||||
query (lib/query metadata-provider (meta/table-metadata :venues))]
|
||||
query (lib/query metadata-provider (meta/table-metadata source-table))]
|
||||
(doseq [{exp :name [op & args] :clause options :options} tests]
|
||||
(testing exp
|
||||
(is (= exp (lib/display-name
|
||||
@@ -501,6 +501,7 @@
|
||||
(let [created-at (meta/field-metadata :products :created-at)
|
||||
created-at-with #(lib/with-temporal-bucket created-at %1)]
|
||||
(check-display-names
|
||||
:products
|
||||
[{:clause [:= (created-at-with :year) "2023-10-02T00:00:00.000Z"]
|
||||
:name "Created At is Jan 1 – Dec 31, 2023"}
|
||||
{:clause [:during created-at "2023-10-02" :year]
|
||||
@@ -526,6 +527,7 @@
|
||||
(let [created-at (meta/field-metadata :products :created-at)
|
||||
created-at-with #(lib/with-temporal-bucket created-at %1)]
|
||||
(check-display-names
|
||||
:products
|
||||
[{:clause [:= (created-at-with :hour-of-day) 0]
|
||||
:name "Created At: Hour of day is on 12 AM"}
|
||||
{:clause [:= (lib/get-hour created-at) 0]
|
||||
@@ -644,6 +646,7 @@
|
||||
|
||||
(deftest ^:parallel time-frontend-filter-display-names-test
|
||||
(check-display-names
|
||||
:people
|
||||
[{:clause [:< last-online-time "00:00:00.000"], :name "Last Online Time is before 12:00 AM"}
|
||||
{:clause [:> last-online-time "12:00:00.000"], :name "Last Online Time is after 12:00 PM"}
|
||||
{:clause [:between last-online-time "12:00:00.000" "00:00:00.000"],
|
||||
@@ -654,6 +657,7 @@
|
||||
(deftest ^:parallel pk-frontend-filter-display-names-test
|
||||
(let [pk (meta/field-metadata :venues :id)]
|
||||
(check-display-names
|
||||
:venues
|
||||
[{:clause [:= pk 1], :name "ID is 1"}
|
||||
{:clause [:= pk 4], :name "ID is 4"}
|
||||
{:clause [:= pk 1 2], :name "ID is 2 selections"}
|
||||
@@ -673,12 +677,14 @@
|
||||
(let [longitude (meta/field-metadata :venues :longitude)
|
||||
latitude (meta/field-metadata :venues :latitude)]
|
||||
(check-display-names
|
||||
:venues
|
||||
[{:clause [:inside longitude latitude 1 2 3 4],
|
||||
:name "Longitude is between 3 and 1 and Latitude is between 2 and 4"}])))
|
||||
|
||||
(deftest ^:parallel fk-frontend-filter-display-names-test
|
||||
(let [fk (meta/field-metadata :orders :user-id)]
|
||||
(check-display-names
|
||||
:orders
|
||||
[{:clause [:= fk 1], :name "User ID is 1"}
|
||||
{:clause [:= fk 11], :name "User ID is 11"}
|
||||
{:clause [:= fk 1 2], :name "User ID is 2 selections"}
|
||||
@@ -697,6 +703,7 @@
|
||||
(deftest ^:parallel string-frontend-filter-display-names-test
|
||||
(let [nam (meta/field-metadata :venues :name)]
|
||||
(check-display-names
|
||||
:venues
|
||||
[{:clause [:= nam "ABC"], :name "Name is ABC"}
|
||||
{:clause [:= nam "A" "B"], :name "Name is 2 selections"}
|
||||
{:clause [:= nam "A" "B" "C"], :name "Name is 3 selections"}
|
||||
@@ -720,6 +727,7 @@
|
||||
|
||||
(deftest ^:parallel boolean-frontend-filter-display-names-test
|
||||
(check-display-names
|
||||
:orders
|
||||
[{:clause [:= is-active true], :name "Is Active is true"}
|
||||
{:clause [:= is-active false], :name "Is Active is false"}
|
||||
{:clause [:is-null is-active], :name "Is Active is empty"}
|
||||
@@ -730,6 +738,7 @@
|
||||
(deftest ^:parallel number-frontend-filter-display-names-test
|
||||
(let [tax (meta/field-metadata :orders :tax)]
|
||||
(check-display-names
|
||||
:orders
|
||||
[{:clause [:= tax 1], :name "Tax is equal to 1"}
|
||||
{:clause [:= tax 7], :name "Tax is equal to 7"}
|
||||
{:clause [:= tax 7 10], :name "Tax is equal to 2 selections"}
|
||||
@@ -756,6 +765,7 @@
|
||||
pos-clause (lib.expression/value pos-value)
|
||||
neg-clause (lib.expression/value neg-value)]
|
||||
(check-display-names
|
||||
:orders
|
||||
[{:clause [:= id pos-clause], :name (str "ID is " pos-value)}
|
||||
{:clause [:!= id pos-clause], :name (str "ID is not " pos-value)}
|
||||
{:clause [:> id pos-clause], :name (str "ID is greater than " pos-value)}
|
||||
@@ -769,6 +779,7 @@
|
||||
(deftest ^:parallel relative-datetime-frontend-filter-display-names-test
|
||||
(let [created-at (meta/field-metadata :products :created-at)]
|
||||
(check-display-names
|
||||
:products
|
||||
[{:clause [:time-interval created-at -1 :minute], :name "Created At is in the previous minute"}
|
||||
{:clause [:time-interval created-at -3 :minute], :name "Created At is in the previous 3 minutes"}
|
||||
{:clause [:time-interval created-at -1 :hour], :name "Created At is in the previous hour"}
|
||||
@@ -831,6 +842,7 @@
|
||||
(deftest ^:parallel specific-date-frontend-filter-display-names-test
|
||||
(let [created-at (meta/field-metadata :products :created-at)]
|
||||
(check-display-names
|
||||
:products
|
||||
[{:clause [:= created-at "2023-10-03"], :name "Created At is on Oct 3, 2023"}
|
||||
{:clause [:= created-at "2023-10-03T12:30:00"],
|
||||
:name "Created At is on Oct 3, 2023, 12:30 PM"}
|
||||
|
||||
@@ -1271,13 +1271,11 @@
|
||||
cols (lib/joinable-columns query -1 join)]
|
||||
(is (=? [{:name "ID"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:source-alias "Cat"
|
||||
:lib/source :source/joins
|
||||
:lib/source-column-alias "ID"
|
||||
:selected? id-selected?}
|
||||
{:name "NAME"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:source-alias "Cat"
|
||||
:lib/source :source/joins
|
||||
:lib/source-column-alias "NAME"
|
||||
:selected? name-selected?}]
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
:lib/source :source/implicitly-joinable}))
|
||||
(lib/visible-columns query))))))))
|
||||
|
||||
(def cols-fns [lib/visible-columns lib/filterable-columns lib/breakoutable-columns lib/orderable-columns])
|
||||
(def ^:private cols-fns [#'lib/visible-columns #'lib/filterable-columns #'lib/breakoutable-columns #'lib/orderable-columns])
|
||||
|
||||
(deftest ^:parallel inherited-temporal-unit-stage-propagation-test
|
||||
(let [unit :quarter
|
||||
@@ -328,20 +328,27 @@
|
||||
stage-2-query (lib/append-stage stage-1-query)]
|
||||
(testing "0th stage `orderable-columns` do not contain inherited-temporal-unit"
|
||||
(is ((complement contains?)
|
||||
(u/prog1 (lib/find-matching-column stage-0-breakout (lib/orderable-columns stage-0-query))
|
||||
(u/prog1 (lib/find-matching-column stage-0-query 0 stage-0-breakout (lib/orderable-columns stage-0-query))
|
||||
(is (= :metadata/column (:lib/type <>))))
|
||||
:inherited-temporal-unit)))
|
||||
(testing "1st stage col function contain inherited-temporal-unit"
|
||||
(doseq [cols-fn cols-fns
|
||||
:let [stage-1-cols (cols-fn stage-1-query)
|
||||
stage-1-col (lib/find-matching-column stage-0-breakout stage-1-cols)]]
|
||||
(is (= unit (:inherited-temporal-unit stage-1-col)))))
|
||||
:let [stage-1-cols (cols-fn stage-1-query)
|
||||
stage-1-ref (lib/ref
|
||||
(m/find-first #(= (:lib/source-column-alias %) "CREATED_AT")
|
||||
stage-1-cols))
|
||||
stage-1-col (lib/find-matching-column stage-1-query 1 stage-1-ref stage-1-cols)]]
|
||||
(is (=? {:inherited-temporal-unit unit}
|
||||
stage-1-col))))
|
||||
(testing "inherited-temporal-unit is propagated into 2nd stage (and further)"
|
||||
(doseq [cols-fn cols-fns
|
||||
:let [stage-2-cols (cols-fn stage-2-query)
|
||||
stage-1-ref (first (lib/fields stage-1-query))
|
||||
stage-2-col (lib/find-matching-column stage-1-ref stage-2-cols)]]
|
||||
(is (= unit (:inherited-temporal-unit stage-2-col)))))))
|
||||
:let [stage-2-cols (cols-fn stage-2-query)
|
||||
stage-2-ref (lib/ref
|
||||
(m/find-first #(= (:lib/source-column-alias %) "CREATED_AT")
|
||||
stage-2-cols))
|
||||
stage-2-col (lib/find-matching-column stage-2-query 2 stage-2-ref stage-2-cols)]]
|
||||
(is (=? {:inherited-temporal-unit unit}
|
||||
stage-2-col))))))
|
||||
|
||||
(deftest ^:parallel inherited-temporal-unit-card-propagation-test
|
||||
(let [unit :quarter
|
||||
@@ -350,13 +357,13 @@
|
||||
(lib/query (meta/table-metadata :orders))
|
||||
(lib/aggregate (lib/count))
|
||||
(lib/breakout (lib/with-temporal-bucket (meta/field-metadata :orders :created-at) unit)))
|
||||
breakout-ref (first (lib/breakouts card-query))
|
||||
mp (lib.tu/metadata-provider-with-card-from-query card-id card-query)
|
||||
query (lib/query mp (lib.metadata/card mp card-id))]
|
||||
query (lib/query mp (lib.metadata/card mp card-id))
|
||||
breakout-ref (lib/normalize [:field {:base-type :type/DateTime} "CREATED_AT"])]
|
||||
(testing "_cols functions_ return :inherited-temporal-unit for a card source"
|
||||
(doseq [cols-fn cols-fns]
|
||||
(is (contains? (lib/find-matching-column breakout-ref (cols-fn query))
|
||||
:inherited-temporal-unit))))))
|
||||
(is (=? {:inherited-temporal-unit keyword?}
|
||||
(lib/find-matching-column query -1 breakout-ref (cols-fn query))))))))
|
||||
|
||||
(deftest ^:parallel inherited-temporal-unit-propagation-from-expression-test
|
||||
(let [expression-name "created at + 1 month"
|
||||
@@ -372,12 +379,19 @@
|
||||
:quarter))
|
||||
(lib/append-stage $))]
|
||||
(testing "_cols functions_ return :inherited-temporla-unit for bucketed expressions"
|
||||
(doseq [cols-fn cols-fns]
|
||||
(is (= :quarter (-> (lib/expression-ref query 0 expression-name)
|
||||
(lib/find-matching-column (cols-fn query))
|
||||
:inherited-temporal-unit)))))
|
||||
(doseq [cols-fn cols-fns
|
||||
;; very ILLEGAL to use an expression ref from the first stage for matching purposes in the second stage
|
||||
;; but just see if we can figure this stuff out anyway.
|
||||
:let [a-ref (lib/expression-ref query 0 expression-name)]]
|
||||
(is (=? {:lib/source :source/previous-stage
|
||||
:lib/original-expression-name "created at + 1 month"
|
||||
:lib/expression-name (symbol "nil #_\"key is not present.\"")
|
||||
:inherited-temporal-unit :quarter}
|
||||
(lib/find-matching-column query 1 a-ref (cols-fn query))))))
|
||||
(testing "orderable columns do not contain inherited-temporal-unit for expression"
|
||||
(is (not (contains? (lib/find-matching-column (lib/expression-ref query 0 expression-name)
|
||||
(is (not (contains? (lib/find-matching-column query
|
||||
0
|
||||
(lib/expression-ref query 0 expression-name)
|
||||
(lib/orderable-columns query 0))
|
||||
:inherited-temporal-unit))))))
|
||||
|
||||
|
||||
@@ -811,8 +811,8 @@
|
||||
:aggregation [[:sum $orders.quantity]]}
|
||||
:alias "Orders"
|
||||
:condition [:= $id &Orders.orders.product-id]
|
||||
;; we can get title since product-id is remapped to title
|
||||
:fields [&Orders.title
|
||||
;; we can get products.title since orders.product-id is remapped to products.title
|
||||
:fields [&Orders.products.title
|
||||
&Orders.*sum/Integer]}]
|
||||
:fields [$title $category]
|
||||
:order-by [[:asc $id]]
|
||||
|
||||
@@ -344,20 +344,20 @@
|
||||
{:id (meta/id :venues :latitude) :name "LATITUDE"}
|
||||
{:id (meta/id :venues :longitude) :name "LONGITUDE"}
|
||||
{:id (meta/id :venues :price) :name "PRICE"}
|
||||
{:lib/type :metadata/column
|
||||
:name "ID"
|
||||
:display-name "ID"
|
||||
:source-alias "Cat"
|
||||
:id (meta/id :categories :id)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/BigInteger}
|
||||
{:lib/type :metadata/column
|
||||
:name "NAME"
|
||||
:display-name "Name"
|
||||
:source-alias "Cat"
|
||||
:id (meta/id :categories :name)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/Text}]
|
||||
{:lib/type :metadata/column
|
||||
:name "ID"
|
||||
:display-name "ID"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:id (meta/id :categories :id)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/BigInteger}
|
||||
{:lib/type :metadata/column
|
||||
:name "NAME"
|
||||
:display-name "Name"
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:id (meta/id :categories :name)
|
||||
:table-id (meta/id :categories)
|
||||
:base-type :type/Text}]
|
||||
(lib/orderable-columns query)))))))
|
||||
|
||||
(deftest ^:parallel orderable-columns-source-card-test
|
||||
@@ -645,7 +645,7 @@
|
||||
{:display-name "ID", :lib/source :source/joins}
|
||||
{:display-name "Name", :lib/source :source/joins}]
|
||||
(lib/orderable-columns query)))
|
||||
(let [query' (lib/order-by query (m/find-first #(and (= (:source-alias %) "Cat")
|
||||
(let [query' (lib/order-by query (m/find-first #(and (= (:metabase.lib.join/join-alias %) "Cat")
|
||||
(= (:display-name %) "Name"))
|
||||
(lib/orderable-columns query)))]
|
||||
(is (=? [{:display-name "ID", :lib/source :source/table-defaults}
|
||||
|
||||
@@ -169,20 +169,20 @@
|
||||
{:id (meta/id :venues :price), :name "PRICE", :lib/source :source/table-defaults}
|
||||
{:name "ID + 1", :lib/source :source/expressions}
|
||||
{:name "ID + 2", :lib/source :source/expressions}
|
||||
{:id (meta/id :categories :id)
|
||||
:name "ID_2"
|
||||
:lib/source :source/joins
|
||||
:source-alias "Cat"
|
||||
:display-name "ID"
|
||||
:lib/source-column-alias "ID"
|
||||
:lib/desired-column-alias "Cat__ID"}
|
||||
{:id (meta/id :categories :name)
|
||||
:name "NAME_2"
|
||||
:lib/source :source/joins
|
||||
:source-alias "Cat"
|
||||
:display-name "Name"
|
||||
:lib/source-column-alias "NAME"
|
||||
:lib/desired-column-alias "Cat__NAME"}]
|
||||
{:id (meta/id :categories :id)
|
||||
:name "ID_2"
|
||||
:lib/source :source/joins
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:display-name "ID"
|
||||
:lib/source-column-alias "ID"
|
||||
:lib/desired-column-alias "Cat__ID"}
|
||||
{:id (meta/id :categories :name)
|
||||
:name "NAME_2"
|
||||
:lib/source :source/joins
|
||||
:metabase.lib.join/join-alias "Cat"
|
||||
:display-name "Name"
|
||||
:lib/source-column-alias "NAME"
|
||||
:lib/desired-column-alias "Cat__NAME"}]
|
||||
metadata))
|
||||
(testing ":long display names"
|
||||
(is (= ["ID"
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
(let [filter-cols (fn [query]
|
||||
(let [cols (lib/visible-columns query)]
|
||||
(for [[_< _opts agg _value] (lib/filters query -1)]
|
||||
(lib/find-matching-column agg cols))))
|
||||
(lib/find-matching-column query -1 agg cols))))
|
||||
cols-before (filter-cols before)
|
||||
cols-after (filter-cols after)]
|
||||
(is (every? some? cols-before))
|
||||
@@ -190,6 +190,6 @@
|
||||
cols-after)))
|
||||
(testing "the refs to those breakouts are still aimed at the same columns as before, despite the name change"
|
||||
(let [[_- _opts _now days-ref] (first (lib/expressions after))]
|
||||
(is (=? days-after (lib/find-matching-column days-ref cols-after))))
|
||||
(is (=? days-after (lib/find-matching-column after -1 days-ref cols-after))))
|
||||
(let [[_= _opts months-ref _value] (first (lib/filters after))]
|
||||
(is (=? months-after (lib/find-matching-column months-ref cols-after))))))))
|
||||
(is (=? months-after (lib/find-matching-column after -1 months-ref cols-after))))))))
|
||||
|
||||
@@ -166,8 +166,7 @@
|
||||
(add-step {:kind :breakout})
|
||||
|
||||
(defn- breakout-exists? [query stage-number column]
|
||||
(boolean (lib.breakout/breakout-column? query stage-number column {:same-binning-strategy? true
|
||||
:same-temporal-bucket? true})))
|
||||
(boolean (lib.breakout/breakout-column? query stage-number column {:find-matching-column/ignore-binning-and-bucketing? false})))
|
||||
|
||||
(defmethod next-steps* :breakout [query _breakout]
|
||||
(let [stage-number (choose-stage query)
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
;; legacy result metadata will already include the Join name in the `:display-name`, so simulate that. Make
|
||||
;; sure we're not including it twice.
|
||||
(for [col (lib/returned-columns
|
||||
(lib/query meta/metadata-provider legacy-card-query))]
|
||||
(lib/query meta/metadata-provider legacy-card-query))
|
||||
:let [any-join-alias ((some-fn :metabase.lib.join/join-alias :lib/original-join-alias) col)]]
|
||||
(cond-> col
|
||||
(:source-alias col)
|
||||
any-join-alias
|
||||
(update :display-name (fn [display-name]
|
||||
(str (:source-alias col) " → " display-name))))))
|
||||
(str any-join-alias " → " display-name))))))
|
||||
|
||||
(defn query-with-legacy-source-card
|
||||
"An MLv2 query that has a `:source-card` that has a legacy query, and legacy metadata."
|
||||
|
||||
@@ -111,30 +111,35 @@
|
||||
(lib/breakout (-> (meta/field-metadata :orders :created-at)
|
||||
(lib/with-temporal-bucket :month)))
|
||||
(lib/breakout (-> (meta/field-metadata :orders :quantity)
|
||||
(lib/with-binning {:strategy :num-bins, :num-bins 10}))))
|
||||
(lib/with-binning {:strategy :num-bins, :num-bins 10})))
|
||||
lib/append-stage)
|
||||
temporal-col (m/find-first #(= (:name %) "CREATED_AT")
|
||||
(lib/returned-columns query))
|
||||
_ (is (some? temporal-col))
|
||||
binned-col (m/find-first #(= (:name %) "QUANTITY")
|
||||
(lib/returned-columns query))
|
||||
_ (is (some? binned-col))
|
||||
query (lib/append-stage query)]
|
||||
|
||||
(doseq [[col key-name] [[temporal-col "temporal-unit"]
|
||||
[binned-col "binning"]]
|
||||
rename? [true false]]
|
||||
(let [orig-key (keyword "metabase.lib.field" key-name)
|
||||
renamed-key (keyword "metabase.lib.underlying" key-name)
|
||||
top-level-col (lib.underlying/top-level-column query col :rename-superflous-options? rename?)]
|
||||
_ (is (some? temporal-col))
|
||||
binned-col (m/find-first #(= (:name %) "QUANTITY")
|
||||
(lib/returned-columns query))
|
||||
_ (is (some? binned-col))]
|
||||
(doseq [[col orig-key renamed-key expected] [[temporal-col
|
||||
:metabase.lib.field/temporal-unit
|
||||
:metabase.lib.underlying/temporal-unit
|
||||
:month]
|
||||
[binned-col
|
||||
:metabase.lib.field/binning
|
||||
:metabase.lib.underlying/binning
|
||||
{:strategy :num-bins, :num-bins 10}]]
|
||||
rename? [true false]]
|
||||
(let [top-level-col (lib.underlying/top-level-column query col :rename-superflous-options? rename?)]
|
||||
(testing (str "\nrename? " rename?
|
||||
"\norig-key " orig-key
|
||||
"\nrenamed-key " renamed-key
|
||||
"\ntop-level-col " top-level-col)
|
||||
(if rename?
|
||||
(do (is (= (orig-key col) (renamed-key top-level-col)))
|
||||
(is (= nil (orig-key top-level-col))))
|
||||
(do (is (= (orig-key col) (orig-key top-level-col)))
|
||||
(is (= nil (renamed-key top-level-col))))))))))))
|
||||
"\ntop-level-col " top-level-col
|
||||
"\n")
|
||||
(is (=? (if rename?
|
||||
{renamed-key expected
|
||||
orig-key (symbol "nil #_\"key is not present.\"")}
|
||||
{renamed-key (symbol "nil #_\"key is not present.\"")
|
||||
orig-key expected})
|
||||
top-level-col)))))))))
|
||||
|
||||
(deftest ^:parallel top-level-stage-number-test
|
||||
(testing `lib.underlying/top-level-stage-number
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
:effective-type :type/Integer
|
||||
:id (mt/id :orders :product_id)
|
||||
:lib/desired-column-alias "Orders__PRODUCT_ID"
|
||||
:display-name "Orders → Product ID"
|
||||
:source-alias "Orders"}
|
||||
:display-name "Orders → Product ID"}
|
||||
{:metabase.lib.join/join-alias "Orders"
|
||||
:lib/type :metadata/column
|
||||
:base-type :type/Integer
|
||||
@@ -85,8 +84,7 @@
|
||||
:lib/source-column-alias "sum"
|
||||
:effective-type :type/Integer
|
||||
:lib/desired-column-alias "Orders__sum"
|
||||
:display-name "Orders → Sum of Quantity"
|
||||
:source-alias "Orders"}]
|
||||
:display-name "Orders → Sum of Quantity"}]
|
||||
(binding [lib.metadata.calculation/*display-name-style* :long]
|
||||
(lib.metadata.calculation/returned-columns mlv2-query))))))
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
:values [["American"] ["Artisan"] ["Asian"]]}
|
||||
(custom-values/values-from-card
|
||||
card
|
||||
;; This is referring to CATEGORIES.NAME (from the breakout)
|
||||
[:field "NAME" {:base-type :type/Text}]))))
|
||||
(testing "get values from aggregation column"
|
||||
(is (= {:has_more_values true
|
||||
@@ -70,8 +71,9 @@
|
||||
(custom-values/values-from-card
|
||||
card
|
||||
[:field "NAME" {:base-type :type/Text}]
|
||||
{:query-string "bakery"}))))))))
|
||||
{:query-string "bakery"})))))))))
|
||||
|
||||
(deftest ^:parallel with-mbql-card-test-2b
|
||||
(testing "source card is a question" ; Questions are transparent, so this can drop the aggregations and filter the original.
|
||||
(binding [custom-values/*max-rows* 3]
|
||||
(testing "has aggregation column"
|
||||
|
||||
@@ -34,15 +34,17 @@
|
||||
:fk_target_field_id nil}}
|
||||
(-> (t2/select-one [:model/Field :name :table_id :semantic_type], :id (mt/id :venues :id))
|
||||
(t2/hydrate :name_field)
|
||||
mt/derecordize))))
|
||||
mt/derecordize)))))
|
||||
|
||||
(deftest ^:parallel hydrate-name-field-test-2
|
||||
(testing "make sure it works for multiple fields efficiently. Should only require one DB call to hydrate many Fields"
|
||||
(let [venues-fields (t2/select :model/Field :table_id (mt/id :venues))]
|
||||
(t2/with-call-count [call-count]
|
||||
(t2/hydrate venues-fields :name_field)
|
||||
(is (= 1
|
||||
(call-count))))))
|
||||
(call-count)))))))
|
||||
|
||||
(deftest ^:parallel hydrate-name-field-test-3
|
||||
(testing "It shouldn't hydrate for Fields that aren't PKs"
|
||||
(is (= {:name "PRICE"
|
||||
:table_id (mt/id :venues)
|
||||
@@ -50,8 +52,9 @@
|
||||
:name_field nil}
|
||||
(-> (t2/select-one [:model/Field :name :table_id :semantic_type], :id (mt/id :venues :price))
|
||||
(t2/hydrate :name_field)
|
||||
mt/derecordize))))
|
||||
mt/derecordize)))))
|
||||
|
||||
(deftest ^:parallel hydrate-name-field-test-4
|
||||
(testing "Or if it *is* a PK, but no name Field is available for that Table, it shouldn't hydrate"
|
||||
(is (= {:name "ID"
|
||||
:table_id (mt/id :checkins)
|
||||
@@ -119,7 +122,9 @@
|
||||
:dimensions []}]}
|
||||
(-> (t2/hydrate dashboard :param_fields)
|
||||
:param_fields
|
||||
mt/derecordize)))))
|
||||
mt/derecordize))))))
|
||||
|
||||
(deftest hydrate-param-fields-for-dashboard-test-2
|
||||
(testing "should ignore invalid parameter mappings"
|
||||
(mt/with-temporary-setting-values [enable-public-sharing true]
|
||||
(mt/with-temp [:model/Dashboard dashboard {:parameters [{:id "p1" :type :number/=}
|
||||
@@ -212,7 +217,9 @@
|
||||
(params/get-linked-field-ids
|
||||
[{:parameter_mappings
|
||||
[{:parameter_id "foo" :target [:dimension [:field 256 nil]]}
|
||||
{:parameter_id "bar" :target [:dimension [:field 267 nil]]}]}]))))
|
||||
{:parameter_id "bar" :target [:dimension [:field 267 nil]]}]}])))))
|
||||
|
||||
(deftest ^:parallel get-linked-field-ids-test-2
|
||||
(testing "get-linked-field-ids multiple fields to one param test"
|
||||
(is (= {"foo" #{256 10}
|
||||
"bar" #{267}}
|
||||
@@ -221,7 +228,9 @@
|
||||
[{:parameter_id "foo" :target [:dimension [:field 256 nil]]}
|
||||
{:parameter_id "bar" :target [:dimension [:field 267 nil]]}]}
|
||||
{:parameter_mappings
|
||||
[{:parameter_id "foo" :target [:dimension [:field 10 nil]]}]}]))))
|
||||
[{:parameter_id "foo" :target [:dimension [:field 10 nil]]}]}])))))
|
||||
|
||||
(deftest ^:parallel get-linked-field-ids-test-3
|
||||
(testing "get-linked-field-ids-test misc fields"
|
||||
(is (= {"1" #{1} "2" #{2} "3" #{3} "4" #{4} "5" #{5}}
|
||||
(params/get-linked-field-ids
|
||||
@@ -231,7 +240,9 @@
|
||||
{:parameter_id "wow" :target [:dimension [:field "wow" {:base-type :type/Integer}]]}
|
||||
{:parameter_id "3" :target [:dimension [:field 3 {:source-field 1}]]}
|
||||
{:parameter_id "4" :target [:dimension [:field 4 {:binning {:strategy :num-bins, :num-bins 1}}]]}
|
||||
{:parameter_id "5" :target [:dimension [:field 5]]}]}]))))
|
||||
{:parameter_id "5" :target [:dimension [:field 5]]}]}])))))
|
||||
|
||||
(deftest ^:parallel get-linked-field-ids-test-4
|
||||
(testing "get-linked-field-ids-test no fields"
|
||||
(is (= {}
|
||||
(params/get-linked-field-ids
|
||||
|
||||
@@ -582,7 +582,7 @@
|
||||
(meta/id :products :title)))
|
||||
query (lib/query
|
||||
mp
|
||||
(mt/mbql-query orders
|
||||
(lib.tu.macros/mbql-query orders
|
||||
{:aggregation [[:sum [:field (meta/id :orders :total)]]]
|
||||
:breakout [[:field
|
||||
(meta/id :orders :product-id)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
;; Can't use [[normal-drivers-with-feature]] during test initialization, because it means we end up having to load
|
||||
;; plugins and a bunch of other nonsense.
|
||||
(mb.hawk.init/assert-tests-are-not-initializing (pr-str (list* 'normal-drivers-with-feature feature more-features)))
|
||||
(let [features (set (cons feature more-features))]
|
||||
(let [features (into #{feature} more-features)]
|
||||
(set (for [driver (normal-drivers)
|
||||
:let [driver (tx/the-driver-with-test-extensions driver)]
|
||||
:when (driver/with-driver driver
|
||||
|
||||
@@ -1143,20 +1143,18 @@
|
||||
(testing "Make sure queries built with MLv2 that have source Cards with joins work correctly (#31769) (#33083)"
|
||||
(let [metadata-provider (lib.tu.mocks-31769/mock-metadata-provider
|
||||
(mt/metadata-provider)
|
||||
mt/id)]
|
||||
(qp.store/with-metadata-provider metadata-provider
|
||||
(let [legacy-query (lib.convert/->legacy-MBQL
|
||||
(lib.tu.mocks-31769/query metadata-provider))]
|
||||
(mt/with-native-query-testing-context legacy-query
|
||||
(let [results (qp/process-query legacy-query)]
|
||||
(is (= [["Products → Category" "Products__CATEGORY"]
|
||||
["Count" "count"]
|
||||
["Card 2 - Products → Category → Category" "Card 2 - Products → Category__CATEGORY"]]
|
||||
(map (juxt :display_name :lib/desired-column-alias)
|
||||
(mt/cols results))))
|
||||
(is (= [["Doohickey" 3976 "Doohickey"]
|
||||
["Gadget" 4939 "Gadget"]]
|
||||
(mt/rows results))))))))))
|
||||
mt/id)
|
||||
query (lib.tu.mocks-31769/query metadata-provider)]
|
||||
(mt/with-native-query-testing-context query
|
||||
(let [results (qp/process-query query)]
|
||||
(is (= [["Products → Category" "Products__CATEGORY"]
|
||||
["Count" "count"]
|
||||
["Card 2 - Products → Category → Category" "Card 2 - Products → Category__CATEGORY"]]
|
||||
(map (juxt :display_name :lib/desired-column-alias)
|
||||
(mt/cols results))))
|
||||
(is (= [["Doohickey" 3976 "Doohickey"]
|
||||
["Gadget" 4939 "Gadget"]]
|
||||
(mt/rows results))))))))
|
||||
|
||||
(deftest ^:parallel test-13000
|
||||
(testing "Should join MBQL Saved Questions (#13000, #13649, #13744)"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[metabase.lib.temporal-bucket :as lib.temporal-bucket]
|
||||
[metabase.lib.test-metadata :as meta]
|
||||
[metabase.lib.test-util :as lib.tu]
|
||||
[metabase.lib.test-util.notebook-helpers :as lib.tu.notebook]
|
||||
[metabase.lib.util :as lib.util]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.permissions.core :as perms]
|
||||
@@ -1678,9 +1679,7 @@
|
||||
[:field field-id nil]]}))))))))
|
||||
|
||||
(deftest ^:parallel space-names-test
|
||||
(mt/test-drivers (set/intersection
|
||||
(mt/normal-drivers-with-feature :identifiers-with-spaces)
|
||||
(mt/normal-drivers-with-feature :left-join))
|
||||
(mt/test-drivers (mt/normal-drivers-with-feature :identifiers-with-spaces :left-join)
|
||||
(mt/dataset
|
||||
crazy-names
|
||||
(let [mp (mt/metadata-provider)
|
||||
@@ -1693,8 +1692,7 @@
|
||||
(lib/with-join-alias (lib.metadata/field mp (mt/id "space table" "space column"))
|
||||
"Space Table Alias"))])))
|
||||
|
||||
(lib/breakout $q (m/find-first (every-pred (comp #{"Space Column"} :display-name) :source-alias)
|
||||
(lib/breakoutable-columns $q)))
|
||||
(lib/breakout $q (lib.tu.notebook/find-col-with-spec $q (lib/breakoutable-columns $q) {} {:display-name "Space Column"}))
|
||||
(lib/append-stage $q)
|
||||
(lib/breakout $q (first (lib/breakoutable-columns $q)))
|
||||
(lib/aggregate $q (lib/max (first (lib/visible-columns $q)))))]
|
||||
@@ -1702,9 +1700,7 @@
|
||||
[int int] (qp/process-query query))))))))
|
||||
|
||||
(deftest ^:parallel space-names-question-test
|
||||
(mt/test-drivers (set/intersection
|
||||
(mt/normal-drivers-with-feature :identifiers-with-spaces)
|
||||
(mt/normal-drivers-with-feature :left-join))
|
||||
(mt/test-drivers (mt/normal-drivers-with-feature :identifiers-with-spaces :left-join)
|
||||
(let [mp (mt/metadata-provider)
|
||||
card-query (-> (lib/query mp (lib.metadata/table mp (mt/id :orders)))
|
||||
(lib/order-by (lib.metadata/field mp (mt/id :orders :created_at)))
|
||||
|
||||
@@ -84,11 +84,13 @@
|
||||
(:definition_description (t2/hydrate segment-2 :definition_description))))))))
|
||||
|
||||
(deftest ^:parallel definition-description-missing-source-table-test
|
||||
;; kind of a weird test since the Segment itself still is required to have `table_id`
|
||||
(testing "Should work if `:definition` does not include `:source-table`"
|
||||
(mt/with-temp [:model/Segment segment {:name "Expensive BBQ Spots"
|
||||
:definition (mt/$ids venues
|
||||
{:filter
|
||||
[:= $price 4]})}]
|
||||
[:= $price 4]})
|
||||
:table_id (mt/id :venues)}]
|
||||
(is (= "Filtered by Price is equal to 4"
|
||||
(:definition_description (t2/hydrate segment :definition_description)))))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user