Skip to content
Snippets Groups Projects
Unverified Commit 006d182f authored by Cam Saul's avatar Cam Saul Committed by GitHub
Browse files

Metadata/refs overhaul. RIP `field_ref` (#29711)

* Metadata/refs overhaul

* Fix Kondo error

* Fix #29702

* Fix #29701

* Revert silly changes

* Fix syntax error
parent 6b5791ea
No related branches found
No related tags found
No related merge requests found
Showing
with 439 additions and 215 deletions
...@@ -11,6 +11,15 @@ ...@@ -11,6 +11,15 @@
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]))
(mu/defn column-metadata->aggregation-ref :- :mbql.clause/aggregation
"Given `:metadata/field` column metadata for an aggregation, construct an `:aggregation` reference."
[metadata :- lib.metadata/ColumnMetadata]
(let [options {:lib/uuid (str (random-uuid))
:effective-type ((some-fn :effective_type :base_type) metadata)}
index (::aggregation-index metadata)]
(assert (integer? index) "Metadata for an aggregation reference should include ::aggregation-index")
[:aggregation options index]))
(mu/defn resolve-aggregation :- ::lib.schema.aggregation/aggregation (mu/defn resolve-aggregation :- ::lib.schema.aggregation/aggregation
"Resolve an aggregation with a specific `index`." "Resolve an aggregation with a specific `index`."
[query :- ::lib.schema/query [query :- ::lib.schema/query
...@@ -32,14 +41,16 @@ ...@@ -32,14 +41,16 @@
(for [aggregation aggregations] (for [aggregation aggregations]
(lib.metadata.calculation/display-name query stage-number aggregation))))) (lib.metadata.calculation/display-name query stage-number aggregation)))))
(defmethod lib.metadata.calculation/metadata :aggregation (defmethod lib.metadata.calculation/metadata-method :aggregation
[query stage-number [_ag opts index, :as aggregation-ref]] [query stage-number [_ag opts index, :as _aggregation-ref]]
(let [aggregation (resolve-aggregation query stage-number index)] (let [aggregation (resolve-aggregation query stage-number index)]
(merge (merge
(lib.metadata.calculation/metadata query stage-number aggregation) (lib.metadata.calculation/metadata query stage-number aggregation)
{:field_ref aggregation-ref} {:lib/source :source/aggregations}
(when (:base-type opts) (when (:base-type opts)
{:base_type (:base-type opts)})))) {:base_type (:base-type opts)})
(when (:effective-type opts)
{:effective_type opts}))))
;;; TODO -- merge this stuff into `defop` somehow. ;;; TODO -- merge this stuff into `defop` somehow.
...@@ -213,10 +224,6 @@ ...@@ -213,10 +224,6 @@
stage-number :- :int] stage-number :- :int]
(when-let [aggregation-exprs (not-empty (:aggregation (lib.util/query-stage query stage-number)))] (when-let [aggregation-exprs (not-empty (:aggregation (lib.util/query-stage query stage-number)))]
(map-indexed (fn [i aggregation] (map-indexed (fn [i aggregation]
(let [metadata (lib.metadata.calculation/metadata query stage-number aggregation) (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation)]
ag-ref [:aggregation (assoc metadata :lib/source :source/aggregations, ::aggregation-index i)))
{:lib/uuid (str (random-uuid))
:base-type (:base_type metadata)}
i]]
(assoc metadata :field_ref ag-ref, :source :aggregation)))
aggregation-exprs))) aggregation-exprs)))
...@@ -37,5 +37,5 @@ ...@@ -37,5 +37,5 @@
stage-number :- :int] stage-number :- :int]
(when-let [breakout-exprs (not-empty (:breakout (lib.util/query-stage query stage-number)))] (when-let [breakout-exprs (not-empty (:breakout (lib.util/query-stage query stage-number)))]
(mapv (fn [field-ref] (mapv (fn [field-ref]
(assoc (lib.metadata.calculation/metadata query stage-number field-ref) :source :breakout)) (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :lib/source :source/breakouts))
breakout-exprs))) breakout-exprs)))
(ns metabase.lib.common (ns metabase.lib.common
(:require (:require
[metabase.lib.dispatch :as lib.dispatch] [metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.field :as lib.field]
#_{:clj-kondo/ignore [:unused-namespace]}
[metabase.lib.options :as lib.options] [metabase.lib.options :as lib.options]
[metabase.lib.ref :as lib.ref]
[metabase.lib.schema.common :as schema.common] [metabase.lib.schema.common :as schema.common]
#_{:clj-kondo/ignore [:unused-namespace]}
[metabase.util.malli :as mu]) [metabase.util.malli :as mu])
#?(:cljs (:require-macros [metabase.lib.common]))) #?(:cljs (:require-macros [metabase.lib.common])))
(comment lib.options/keep-me
mu/keep-me)
(mu/defn external-op :- [:maybe ::schema.common/external-op] (mu/defn external-op :- [:maybe ::schema.common/external-op]
"Convert the internal operator `clause` to the external format." "Convert the internal operator `clause` to the external format."
[[operator options :as clause]] [[operator options :as clause]]
...@@ -29,8 +30,8 @@ ...@@ -29,8 +30,8 @@
x) x)
(defmethod ->op-arg :metadata/field (defmethod ->op-arg :metadata/field
[query stage-number field-metadata] [_query _stage-number field-metadata]
(lib.field/field query stage-number field-metadata)) (lib.ref/ref field-metadata))
(defmethod ->op-arg :lib/external-op (defmethod ->op-arg :lib/external-op
[query stage-number {:keys [operator options args] :or {options {}}}] [query stage-number {:keys [operator options args] :or {options {}}}]
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"Currently this is mostly a convenience namespace for REPL and test usage. We'll probably have a slightly different "Currently this is mostly a convenience namespace for REPL and test usage. We'll probably have a slightly different
version of this for namespace for QB and QP usage in the future -- TBD." version of this for namespace for QB and QP usage in the future -- TBD."
(:refer-clojure :exclude [filter remove replace and or not = < <= > ->> >= not-empty case count distinct max min (:refer-clojure :exclude [filter remove replace and or not = < <= > ->> >= not-empty case count distinct max min
+ - * / time abs concat replace]) + - * / time abs concat replace ref])
(:require (:require
[metabase.lib.aggregation :as lib.aggregation] [metabase.lib.aggregation :as lib.aggregation]
[metabase.lib.breakout :as lib.breakout] [metabase.lib.breakout :as lib.breakout]
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
[metabase.lib.normalize :as lib.normalize] [metabase.lib.normalize :as lib.normalize]
[metabase.lib.order-by :as lib.order-by] [metabase.lib.order-by :as lib.order-by]
[metabase.lib.query :as lib.query] [metabase.lib.query :as lib.query]
[metabase.lib.ref :as lib.ref]
[metabase.lib.segment :as lib.segment] [metabase.lib.segment :as lib.segment]
[metabase.lib.stage :as lib.stage] [metabase.lib.stage :as lib.stage]
[metabase.lib.table :as lib.table] [metabase.lib.table :as lib.table]
...@@ -38,6 +39,7 @@ ...@@ -38,6 +39,7 @@
lib.normalize/keep-me lib.normalize/keep-me
lib.order-by/keep-me lib.order-by/keep-me
lib.query/keep-me lib.query/keep-me
lib.ref/keep-me
lib.segment/keep-me lib.segment/keep-me
lib.stage/keep-me lib.stage/keep-me
lib.table/keep-me lib.table/keep-me
...@@ -67,6 +69,7 @@ ...@@ -67,6 +69,7 @@
table] table]
[lib.expression [lib.expression
expression expression
expressions
+ +
- -
* *
...@@ -161,6 +164,8 @@ ...@@ -161,6 +164,8 @@
remove-clause remove-clause
replace-clause replace-clause
saved-question-query] saved-question-query]
[lib.ref
ref]
[lib.stage [lib.stage
append-stage append-stage
drop-stage] drop-stage]
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
"Conveniences for usage in REPL and tests. Things in this namespace are not meant for normal usage in the FE client or "Conveniences for usage in REPL and tests. Things in this namespace are not meant for normal usage in the FE client or
in QB code." in QB code."
(:require (:require
[metabase.lib.field :as lib.field]
[metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata :as lib.metadata]
[metabase.lib.query :as lib.query] [metabase.lib.query :as lib.query]
[metabase.lib.ref :as lib.ref]
[metabase.lib.schema :as lib.schema] [metabase.lib.schema :as lib.schema]
[metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.common :as lib.schema.common]
[metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.id :as lib.schema.id]
...@@ -17,13 +17,11 @@ ...@@ -17,13 +17,11 @@
::lib.schema.common/non-blank-string]] ::lib.schema.common/non-blank-string]]
(if (integer? id-or-name) (if (integer? id-or-name)
(let [id id-or-name] (let [id id-or-name]
(fn [query stage-number] (fn [query _stage-number]
(->> (lib.metadata/field query id) (lib.ref/ref (lib.metadata/field query id))))
(lib.field/field query stage-number))))
(let [column-name id-or-name] (let [column-name id-or-name]
(fn [query stage-number] (fn [query stage-number]
(->> (lib.metadata/stage-column query stage-number column-name) (lib.ref/ref (lib.metadata/stage-column query stage-number column-name))))))
(lib.field/field query stage-number))))))
([table-name :- ::lib.schema.common/non-blank-string ([table-name :- ::lib.schema.common/non-blank-string
field-name :- ::lib.schema.common/non-blank-string] field-name :- ::lib.schema.common/non-blank-string]
...@@ -32,9 +30,8 @@ ...@@ -32,9 +30,8 @@
([schema :- [:maybe ::lib.schema.common/non-blank-string] ([schema :- [:maybe ::lib.schema.common/non-blank-string]
table-name :- ::lib.schema.common/non-blank-string table-name :- ::lib.schema.common/non-blank-string
field-name :- ::lib.schema.common/non-blank-string] field-name :- ::lib.schema.common/non-blank-string]
(fn [query stage-number] (fn [query _stage-number]
(->> (lib.metadata/field query schema table-name field-name) (lib.ref/ref (lib.metadata/field query schema table-name field-name)))))
(lib.field/field query stage-number)))))
(mu/defn query-for-table-name :- ::lib.schema/query (mu/defn query-for-table-name :- ::lib.schema/query
"Create a new query for a specific Table with a table name." "Create a new query for a specific Table with a table name."
......
...@@ -16,6 +16,13 @@ ...@@ -16,6 +16,13 @@
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]))
(mu/defn column-metadata->expression-ref :- :mbql.clause/expression
"Given `:metadata/field` column metadata for an expression, construct an `:expression` reference."
[metadata :- lib.metadata/ColumnMetadata]
(let [options {:lib/uuid (str (random-uuid))
:effective-type ((some-fn :effective_type :base_type) metadata)}]
[:expression options (:name metadata)]))
(mu/defn resolve-expression :- ::lib.schema.expression/expression (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 "Find the expression with `expression-name` in a given stage of a `query`, or throw an Exception if it doesn't
exist." exist."
...@@ -34,11 +41,11 @@ ...@@ -34,11 +41,11 @@
[{:keys [operator options args] :or {options {}}}] [{:keys [operator options args] :or {options {}}}]
(lib.schema.expression/type-of* (into [(keyword operator) options] args))) (lib.schema.expression/type-of* (into [(keyword operator) options] args)))
(defmethod lib.metadata.calculation/metadata :expression (defmethod lib.metadata.calculation/metadata-method :expression
[query stage-number [_expression opts expression-name, :as expression-ref]] [query stage-number [_expression opts expression-name, :as expression-ref]]
(let [expression (resolve-expression query stage-number expression-name)] (let [expression (resolve-expression query stage-number expression-name)]
{:lib/type :metadata/field {:lib/type :metadata/field
:field_ref expression-ref :lib/source :source/expressions
:name expression-name :name expression-name
:display_name (lib.metadata.calculation/display-name query stage-number expression-ref) :display_name (lib.metadata.calculation/display-name query stage-number expression-ref)
:base_type (or (:base-type opts) :base_type (or (:base-type opts)
...@@ -208,11 +215,15 @@ ...@@ -208,11 +215,15 @@
(mu/defn expressions :- [:sequential lib.metadata/ColumnMetadata] (mu/defn expressions :- [:sequential lib.metadata/ColumnMetadata]
"Get metadata about the expressions in a given stage of a `query`." "Get metadata about the expressions in a given stage of a `query`."
[query :- ::lib.schema/query ([query]
stage-number :- :int] (expressions query -1))
(for [[expression-name expression-definition] (:expressions (lib.util/query-stage query stage-number))]
(let [metadata (lib.metadata.calculation/metadata query stage-number expression-definition)] ([query :- ::lib.schema/query
(merge stage-number :- :int]
metadata (for [[expression-name expression-definition] (:expressions (lib.util/query-stage query stage-number))]
{:field_ref [:expression {:lib/uuid (str (random-uuid)), :base-type (:base_type metadata)} expression-name] (let [metadata (lib.metadata.calculation/metadata query stage-number expression-definition)]
:source :expressions})))) (merge
metadata
{:lib/source :source/expressions
:name expression-name
:display_name expression-name})))))
(ns metabase.lib.field (ns metabase.lib.field
(:require (:require
[metabase.lib.convert :as lib.convert] [metabase.lib.aggregation :as lib.aggregation]
[metabase.lib.dispatch :as lib.dispatch] [metabase.lib.expression :as lib.expression]
[metabase.lib.join :as lib.join] [metabase.lib.join :as lib.join]
[metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata :as lib.metadata]
[metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.metadata.calculation :as lib.metadata.calculation]
[metabase.lib.normalize :as lib.normalize] [metabase.lib.normalize :as lib.normalize]
[metabase.lib.options :as lib.options] [metabase.lib.ref :as lib.ref]
[metabase.lib.schema :as lib.schema] [metabase.lib.schema :as lib.schema]
[metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.common :as lib.schema.common]
[metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.id :as lib.schema.id]
...@@ -81,24 +81,29 @@ ...@@ -81,24 +81,29 @@
(update metadata :name (fn [field-name] (update metadata :name (fn [field-name]
(str parent-name \. field-name))))) (str parent-name \. field-name)))))
(defmethod lib.metadata.calculation/metadata :metadata/field (defmethod lib.metadata.calculation/metadata-method :metadata/field
[_query _stage-number field-metadata] [_query _stage-number field-metadata]
field-metadata) field-metadata)
;;; TODO -- base type should be affected by `temporal-unit`, right? ;;; TODO -- base type should be affected by `temporal-unit`, right?
(defmethod lib.metadata.calculation/metadata :field (defmethod lib.metadata.calculation/metadata-method :field
[query stage-number [_tag {:keys [base-type temporal-unit], :as opts} :as field-ref]] [query stage-number [_tag {:keys [source-field effective-type base-type temporal-unit join-alias], :as opts} :as field-ref]]
(let [field-metadata (resolve-field-metadata query stage-number field-ref) (let [field-metadata (resolve-field-metadata query stage-number field-ref)
metadata (merge metadata (merge
{:lib/type :metadata/field {:lib/type :metadata/field}
:field_ref field-ref}
field-metadata field-metadata
{:display_name (or (:display-name opts) {:display_name (or (:display-name opts)
(lib.metadata.calculation/display-name query stage-number field-ref))} (lib.metadata.calculation/display-name query stage-number field-ref))}
(when effective-type
{:effective_type effective-type})
(when base-type (when base-type
{:base_type base-type}) {:base_type base-type})
(when temporal-unit (when temporal-unit
{:unit temporal-unit}))] {::temporal-unit temporal-unit})
(when join-alias
{::join-alias join-alias})
(when source-field
{:fk_field_id source-field}))]
(cond->> metadata (cond->> metadata
(:parent_id metadata) (add-parent-column-metadata query)))) (:parent_id metadata) (add-parent-column-metadata query))))
...@@ -131,47 +136,67 @@ ...@@ -131,47 +136,67 @@
;; mostly for the benefit of JS, which does not enforce the Malli schemas. ;; mostly for the benefit of JS, which does not enforce the Malli schemas.
(i18n/tru "[Unknown Field]"))) (i18n/tru "[Unknown Field]")))
(defmulti ^:private ->field (defmethod lib.temporal-bucket/current-temporal-bucket-method :field
{:arglists '([query stage-number field])} [[_tag opts _id-or-name]]
(fn [_query _stage-number field] (:temporal-unit opts))
(lib.dispatch/dispatch-value field)))
(defmethod ->field :field (defmethod lib.temporal-bucket/current-temporal-bucket-method :metadata/field
[_query _stage-number field-clause] [metadata]
field-clause) (::temporal-unit metadata))
(defmethod ->field :metadata/field (defmethod lib.temporal-bucket/temporal-bucket-method :field
[_query _stage-number {base-type :base_type, field-id :id, field-name :name, field-ref :field_ref, :as _field-metadata}] [[_tag options id-or-name] unit]
(cond-> (or (when field-ref (if unit
(lib.convert/->pMBQL field-ref)) [:field (assoc options :temporal-unit unit) id-or-name]
[:field {} (or field-id [:field (dissoc options :temporal-unit) id-or-name]))
field-name)])
base-type (lib.options/update-options assoc :base-type base-type)
true lib.options/ensure-uuid))
(defmethod ->field :dispatch-type/integer
[query _stage-number field-id]
(lib.metadata/field query field-id))
;;; Pass in a function that takes `query` and `stage-number` to support ad-hoc usage in tests etc (defmethod lib.temporal-bucket/temporal-bucket-method :metadata/field
(defmethod ->field :dispatch-type/fn [metadata unit]
[query stage-number f] (assoc metadata ::temporal-unit unit))
(f query stage-number))
(defmethod lib.temporal-bucket/temporal-bucket* :field (defmethod lib.join/current-join-alias-method :field
[[_field options id-or-name] unit] [[_tag opts]]
[:field (assoc options :temporal-unit unit) id-or-name]) (get opts :join-alias))
(mu/defn field :- :mbql.clause/field (defmethod lib.join/current-join-alias-method :metadata/field
"Create a `:field` clause." [metadata]
([query x] (::join-alias metadata))
(->field query -1 x))
([query stage-number x]
(->field query stage-number x)))
(defmethod lib.join/with-join-alias-method :field (defmethod lib.join/with-join-alias-method :field
[field-ref join-alias] [[_tag opts id-or-name] join-alias]
(lib.options/update-options field-ref assoc :join-alias join-alias)) (if join-alias
[:field (assoc opts :join-alias join-alias) id-or-name]
[:field (dissoc opts :join-alias) id-or-name]))
(defmethod lib.join/with-join-alias-method :metadata/field
[metadata join-alias]
(assoc metadata ::join-alias join-alias))
(defmethod lib.ref/ref-method :field
[field-clause]
field-clause)
(defmethod lib.ref/ref-method :metadata/field
[metadata]
(case (:lib/source metadata)
:source/aggregation (lib.aggregation/column-metadata->aggregation-ref metadata)
:source/expressions (lib.expression/column-metadata->expression-ref metadata)
(let [options (merge
{:lib/uuid (str (random-uuid))
:base-type (:base_type metadata)
:effective-type ((some-fn :effective_type :base_type) metadata)}
(when-let [join-alias (::join-alias metadata)]
{:join-alias join-alias})
(when-let [temporal-unit (::temporal-unit metadata)]
{:temporal-unit temporal-unit})
(when-let [source-field-id (:fk_field_id metadata)]
{:source-field source-field-id})
;; TODO -- binning options.
)
always-use-name? (#{:source/card :source/native :source/previous-stage} (:lib/source metadata))]
[:field options (if always-use-name?
(:name metadata)
(or (:id metadata) (:name metadata)))])))
(defn fields (defn fields
"Specify the `:fields` for a query." "Specify the `:fields` for a query."
...@@ -183,6 +208,9 @@ ...@@ -183,6 +208,9 @@
(fields query -1 xs)) (fields query -1 xs))
([query stage-number xs] ([query stage-number xs]
(let [xs (mapv #(->field query stage-number %) (let [xs (mapv (fn [x]
(lib.ref/ref (if (fn? x)
(x query stage-number)
x)))
xs)] xs)]
(lib.util/update-query-stage query stage-number assoc :fields xs)))) (lib.util/update-query-stage query stage-number assoc :fields xs))))
...@@ -13,6 +13,42 @@ ...@@ -13,6 +13,42 @@
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]))
(defmulti with-join-alias-method
"Implementation for [[with-join-alias]]."
{:arglists '([x join-alias])}
(fn [x _join-alias]
(lib.dispatch/dispatch-value x)))
(defmethod with-join-alias-method :dispatch-type/fn
[f join-alias]
(fn [query stage-number]
(let [x (f query stage-number)]
(with-join-alias-method x join-alias))))
(defmethod with-join-alias-method :mbql/join
[join join-alias]
(assoc join :alias join-alias))
(mu/defn with-join-alias
"Add a specific `join-alias` to something `x`, either a `:field` or join map. Does not recursively update other
references (yet; we can add this in the future)."
[x join-alias :- ::lib.schema.common/non-blank-string]
(with-join-alias-method x join-alias))
(defmulti current-join-alias-method
"Impl for [[current-join-alias]]."
{:arglists '([x])}
lib.dispatch/dispatch-value)
(defmethod current-join-alias-method :default
[_x]
nil)
(mu/defn current-join-alias :- [:maybe ::lib.schema.common/non-blank-string]
"Get the current join alias associated with something, if it has one."
[x]
(current-join-alias-method x))
(mu/defn resolve-join :- ::lib.schema.join/join (mu/defn resolve-join :- ::lib.schema.join/join
"Resolve a join with a specific `join-alias`." "Resolve a join with a specific `join-alias`."
[query :- ::lib.schema/query [query :- ::lib.schema/query
...@@ -35,21 +71,22 @@ ...@@ -35,21 +71,22 @@
(i18n/tru "Saved Question #{0}" card-id-str))) (i18n/tru "Saved Question #{0}" card-id-str)))
(i18n/tru "Native Query"))) (i18n/tru "Native Query")))
(mu/defn ^:private column-from-join-fields :- lib.metadata/ColumnMetadata (mu/defn ^:private column-from-join-fields :- lib.metadata.calculation/ColumnMetadataWithSource
"For a column that comes from a join `:fields` list, add or update metadata as needed, e.g. include join name in the "For a column that comes from a join `:fields` list, add or update metadata as needed, e.g. include join name in the
display name." display name."
[query :- ::lib.schema/query [query :- ::lib.schema/query
stage-number :- :int stage-number :- :int
column-metadata :- lib.metadata/ColumnMetadata column-metadata :- lib.metadata/ColumnMetadata
join-alias :- ::lib.schema.common/non-blank-string] join-alias :- ::lib.schema.common/non-blank-string]
(let [[ref-type options arg] (:field_ref column-metadata) (let [column-metadata (assoc column-metadata :source_alias join-alias)
ref-with-join-alias [ref-type (assoc options :join-alias join-alias) arg] col (-> (assoc column-metadata
column-metadata (assoc column-metadata :source_alias join-alias)] :display_name (lib.metadata.calculation/display-name query stage-number column-metadata)
(assoc column-metadata :lib/source :source/fields)
:field_ref ref-with-join-alias (with-join-alias join-alias))]
:display_name (lib.metadata.calculation/display-name query stage-number column-metadata)))) (assert (= (current-join-alias col) join-alias))
col))
(defmethod lib.metadata.calculation/metadata :mbql/join
(defmethod lib.metadata.calculation/metadata-method :mbql/join
[query stage-number {:keys [fields stages], join-alias :alias, :or {fields :none}, :as _join}] [query stage-number {:keys [fields stages], join-alias :alias, :or {fields :none}, :as _join}]
(when-not (= fields :none) (when-not (= fields :none)
(let [field-metadatas (if (= fields :all) (let [field-metadatas (if (= fields :all)
...@@ -149,28 +186,6 @@ ...@@ -149,28 +186,6 @@
(cond-> (join-clause query stage-number x) (cond-> (join-clause query stage-number x)
condition (assoc :condition (join-condition query stage-number condition))))) condition (assoc :condition (join-condition query stage-number condition)))))
(defmulti with-join-alias-method
"Implementation for [[with-join-alias]]."
{:arglists '([x join-alias])}
(fn [x _join-alias]
(lib.dispatch/dispatch-value x)))
(mu/defn with-join-alias
"Add a specific `join-alias` to something `x`, either a `:field` or join map. Does not recursively update other
references (yet; we can add this in the future)."
[x join-alias :- ::lib.schema.common/non-blank-string]
(with-join-alias-method x join-alias))
(defmethod with-join-alias-method :dispatch-type/fn
[f join-alias]
(fn [query stage-number]
(let [x (f query stage-number)]
(with-join-alias-method x join-alias))))
(defmethod with-join-alias-method :mbql/join
[join join-alias]
(assoc join :alias join-alias))
(mu/defn with-join-fields (mu/defn with-join-fields
"Update a join (or a function that will return a join) to include `:fields`, either `:all`, `:none`, or a sequence of "Update a join (or a function that will return a join) to include `:fields`, either `:all`, `:none`, or a sequence of
references." references."
......
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
[metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.common :as lib.schema.common]
[metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.id :as lib.schema.id]
[metabase.lib.util :as lib.util] [metabase.lib.util :as lib.util]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]
[metabase.util.malli.registry :as mr]))
;;; Column vs Field? ;;; Column vs Field?
;;; ;;;
...@@ -28,6 +29,27 @@ ...@@ -28,6 +29,27 @@
;;; will this affect what MLv2 needs to know or does? Not clear at this point, but we'll probably want to abstract ;;; will this affect what MLv2 needs to know or does? Not clear at this point, but we'll probably want to abstract
;;; away dealing with Dimensions in the future so the FE QB GUI doesn't need to special case them. ;;; away dealing with Dimensions in the future so the FE QB GUI doesn't need to special case them.
(mr/def ::column-source
[:enum
;; these are for things from some sort of source other than the current stage;
;; they must be referenced with string names rather than Field IDs
:source/card
:source/native
:source/previous-stage
;; these are for things that were introduced by the current stage of the query; `:field` references should be
;; referenced with Field IDs if available.
;;
;; default columns returned by the `:source-table` for the current stage.
:source/table-defaults
;; specifically introduced by the corresponding top-level clauses.
:source/fields
:source/aggregations
:source/breakouts
;; introduced by a join, not necessarily ultimately returned.
:source/joins
;; Introduced by `:expressions`; not necessarily ultimately returned.
:source/expressions])
(def ColumnMetadata (def ColumnMetadata
"Malli schema for a valid map of column metadata, which can mean one of two things: "Malli schema for a valid map of column metadata, which can mean one of two things:
...@@ -42,10 +64,25 @@ ...@@ -42,10 +64,25 @@
that they are largely compatible. So they're the same for now. We can revisit this in the future if we actually want that they are largely compatible. So they're the same for now. We can revisit this in the future if we actually want
to differentiate between the two versions." to differentiate between the two versions."
[:map [:map
[:lib/type [:= :metadata/field]] ; TODO -- should this be changed to `:metadata/column`? [:lib/type [:= :metadata/field]] ; TODO -- should this be changed to `:metadata/column`?
[:id {:optional true} ::lib.schema.id/field] [:name ::lib.schema.common/non-blank-string]
[:name ::lib.schema.common/non-blank-string] ;; TODO -- ignore `base_type` and make `effective_type` required; see #29707
[:display_name {:optional true} [:maybe ::lib.schema.common/non-blank-string]]]) [:base_type ::lib.schema.common/base-type]
[:id {:optional true} ::lib.schema.id/field]
[:display_name {:optional true} [:maybe ::lib.schema.common/non-blank-string]]
[:effective_type {:optional true} [:maybe ::lib.schema.common/base-type]]
;; if this is a field from another table (implicit join), this is the field in the current table that should be
;; used to perform the implicit join. e.g. if current table is `VENUES` and this field is `CATEGORIES.ID`, then the
;; `fk_field_id` would be `VENUES.CATEGORY_ID`. In a `:field` reference this is saved in the options map as
;; `:source-field`.
[:fk_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.
[:source_alias {:optional true} [:maybe ::lib.schema.common/non-blank-string]]
;; what top-level clause in the query this metadata originated from, if it is calculated (i.e., if this metadata
;; was generated by [[metabase.lib.metadata.calculation/metadata]])
[:lib/source {:optional true} [:ref ::column-source]]])
(def ^:private CardMetadata (def ^:private CardMetadata
[:map [:map
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
(:require (:require
[clojure.string :as str] [clojure.string :as str]
[metabase.lib.dispatch :as lib.dispatch] [metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.metadata :as lib.metadata]
[metabase.lib.options :as lib.options] [metabase.lib.options :as lib.options]
[metabase.lib.schema :as lib.schema] [metabase.lib.schema :as lib.schema]
[metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.common :as lib.schema.common]
...@@ -108,22 +109,41 @@ ...@@ -108,22 +109,41 @@
top-level-key :- TopLevelKey] top-level-key :- TopLevelKey]
(describe-top-level-key-method query stage-number (keyword top-level-key)))) (describe-top-level-key-method query stage-number (keyword top-level-key))))
(defmulti metadata (defmulti metadata-method
"Calculate appropriate metadata for something. What this looks like depends on what we're calculating metadata for. If "Impl for [[metadata]]."
it's a reference or expression of some sort, this should return a single `:metadata/field` map (i.e., something
satisfying the [[metabase.lib.metadata/ColumnMetadata]] schema. If it's something like a stage of a query or a join
definition, it should return a sequence of metadata maps for all the columns 'returned' at that stage of the query."
{:arglists '([query stage-number x])} {:arglists '([query stage-number x])}
(fn [_query _stage-number x] (fn [_query _stage-number x]
(lib.dispatch/dispatch-value x))) (lib.dispatch/dispatch-value x)))
(defmethod metadata :default (defmethod metadata-method :default
[query stage-number x] [query stage-number x]
{:lib/type :metadata/field {:lib/type :metadata/field
:base_type (lib.schema.expresssion/type-of x) :base_type (lib.schema.expresssion/type-of x)
:name (column-name query stage-number x) :name (column-name query stage-number x)
:display_name (display-name query stage-number x)}) :display_name (display-name query stage-number x)})
(def ColumnMetadataWithSource
"Schema for the column metadata that should be returned by [[metadata]]."
[:merge
lib.metadata/ColumnMetadata
[:map
[:lib/source ::lib.metadata/column-source]]])
(mu/defn metadata :- [:or
lib.metadata/ColumnMetadata
[:sequential ColumnMetadataWithSource]]
"Calculate appropriate metadata for something. What this looks like depends on what we're calculating metadata for.
If it's a reference or expression of some sort, this should return a single `:metadata/field` map (i.e., something
satisfying the [[metabase.lib.metadata/ColumnMetadata]] schema. If it's something like a stage of a query or a join
definition, it should return a sequence of metadata maps for all the columns 'returned' at that stage of the query,
and include the `:lib/source` of where they came from."
([query]
(metadata query -1 query))
([query x]
(metadata query -1 x))
([query stage-number x]
(metadata-method query stage-number x)))
(mu/defn describe-query :- ::lib.schema.common/non-blank-string (mu/defn describe-query :- ::lib.schema.common/non-blank-string
"Convenience for calling [[display-name]] on a query to describe the results of its final stage." "Convenience for calling [[display-name]] on a query to describe the results of its final stage."
[query] [query]
......
...@@ -35,9 +35,11 @@ ...@@ -35,9 +35,11 @@
"Default normalization functions keys when doing map normalization." "Default normalization functions keys when doing map normalization."
{:base-type keyword {:base-type keyword
:type keyword :type keyword
;; we can calculate `:field_ref` now using [[metabase.lib.ref/ref]]; `:field_ref` is wrong half of the time anyway,
;; so ignore it.
:field_ref (constantly ::do-not-use-me)
:lib/type keyword :lib/type keyword
:lib/options normalize :lib/options normalize})
:field_ref normalize})
(defn normalize-map (defn normalize-map
"[[normalize]] a map using `key-fn` (default [[clojure.core/keyword]]) for keys and "[[normalize]] a map using `key-fn` (default [[clojure.core/keyword]]) for keys and
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]))
;;; TODO -- not 100% sure we actually need all of this stuff anymore.
(defn- mbql-clause? [x] (defn- mbql-clause? [x]
(and (vector? x) (and (vector? x)
(keyword? (first x)))) (keyword? (first x))))
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
[metabase.lib.aggregation :as lib.aggregation] [metabase.lib.aggregation :as lib.aggregation]
[metabase.lib.breakout :as lib.breakout] [metabase.lib.breakout :as lib.breakout]
[metabase.lib.dispatch :as lib.dispatch] [metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.field :as lib.field]
[metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata :as lib.metadata]
[metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.metadata.calculation :as lib.metadata.calculation]
[metabase.lib.options :as lib.options] [metabase.lib.options :as lib.options]
[metabase.lib.ref :as lib.ref]
[metabase.lib.schema :as lib.schema] [metabase.lib.schema :as lib.schema]
[metabase.lib.schema.expression :as lib.schema.expression] [metabase.lib.schema.expression :as lib.schema.expression]
[metabase.lib.schema.order-by :as lib.schema.order-by] [metabase.lib.schema.order-by :as lib.schema.order-by]
...@@ -45,11 +45,14 @@ ...@@ -45,11 +45,14 @@
[_query _stage-number clause] [_query _stage-number clause]
(lib.options/ensure-uuid clause)) (lib.options/ensure-uuid clause))
;;; by default, try to convert `x` to a Field clause and then order by `:asc` (defmethod ->order-by-clause :dispatch-type/fn
[query stage-number f]
(->order-by-clause query stage-number (f query stage-number)))
;;; by default, try to convert `x` to a ref and then order by `:asc`
(defmethod ->order-by-clause :default (defmethod ->order-by-clause :default
[query stage-number x] [_query _stage-number x]
(let [field-clause (lib.field/field query stage-number x)] (lib.options/ensure-uuid [:asc (lib.ref/ref x)]))
(lib.options/ensure-uuid [:asc field-clause])))
(defn order-by-clause (defn order-by-clause
"Create an order-by clause independently of a query, e.g. for `replace` or whatever." "Create an order-by clause independently of a query, e.g. for `replace` or whatever."
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
{:type keyword {:type keyword
:stages (partial mapv lib.normalize/normalize)})) :stages (partial mapv lib.normalize/normalize)}))
(defmethod lib.metadata.calculation/metadata :mbql/query (defmethod lib.metadata.calculation/metadata-method :mbql/query
[query stage-number x] [query stage-number x]
(lib.metadata.calculation/metadata query stage-number (lib.util/query-stage x stage-number))) (lib.metadata.calculation/metadata query stage-number (lib.util/query-stage x stage-number)))
......
(ns metabase.lib.ref
(:refer-clojure :exclude [ref])
(:require
[metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.schema.ref :as lib.schema.ref]
[metabase.util.malli :as mu]))
(defmulti ref-method
"Impl for [[ref]]. This should create a new ref every time it is called, i.e. it should have a fresh UUID every time
you call it."
{:arglists '([x])}
lib.dispatch/dispatch-value)
(mu/defn ref :- ::lib.schema.ref/ref
"Create a fresh ref that can be added to a query, e.g. a `:field`, `:aggregation`, or `:expression` reference. Will
create a new UUID every time this is called."
([x]
(ref-method x)))
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
(defmethod expression/type-of* :field (defmethod expression/type-of* :field
[[_tag opts _id-or-name]] [[_tag opts _id-or-name]]
(or (:base-type opts) (or ((some-fn :effective-type :base-type) opts)
::expression/type.unknown)) ::expression/type.unknown))
(mbql-clause/define-tuple-mbql-clause :expression (mbql-clause/define-tuple-mbql-clause :expression
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
(defmethod expression/type-of* :expression (defmethod expression/type-of* :expression
[[_tag opts _expression-name]] [[_tag opts _expression-name]]
(or (:base-type opts) (or ((some-fn :effective-type :base-type) opts)
::expression/type.unknown)) ::expression/type.unknown))
(mr/def ::aggregation-options (mr/def ::aggregation-options
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
(defmethod expression/type-of* :aggregation (defmethod expression/type-of* :aggregation
[[_tag opts _index]] [[_tag opts _index]]
(or (:base-type opts) (or ((some-fn :effective-type :base-type) opts)
::expression/type.unknown)) ::expression/type.unknown))
(mr/def ::ref (mr/def ::ref
......
...@@ -30,13 +30,83 @@ ...@@ -30,13 +30,83 @@
{:aggregation (partial mapv lib.normalize/normalize) {:aggregation (partial mapv lib.normalize/normalize)
:filter lib.normalize/normalize})) :filter lib.normalize/normalize}))
(mu/defn ^:private fields-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]] (mu/defn ^:private ensure-previous-stages-have-metadata :- ::lib.schema/query
"Recursively calculate the metadata for the previous stages and add it to them, we'll need it for metadata
calculations for `stage-number` and we don't want to have to calculate it more than once..."
[query :- ::lib.schema/query
stage-number :- :int]
(let [previous-stage-number (lib.util/previous-stage-number query stage-number)]
(cond-> query
previous-stage-number
(lib.util/update-query-stage previous-stage-number
assoc
::cached-metadata
{:lib/type :metadata/results
:columns (stage-metadata query previous-stage-number)}))))
(def ^:private StageMetadataColumns
[:sequential {:min 1} lib.metadata.calculation/ColumnMetadataWithSource])
(def ^:private DistinctStageMetadataColumns
[:and
StageMetadataColumns
[:fn
;; should be dev-facing only, so don't need to i18n
{:error/message "Column :names must be distinct!"
:error/fn (fn [{:keys [value]} _]
(str "Column :names must be distinct, got: " (pr-str (mapv :name value))))}
(fn [columns]
(apply distinct? (map :name columns)))]])
(mu/defn ^:private existing-stage-metadata :- [:maybe StageMetadataColumns]
"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 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-table] :as stage} (lib.util/query-stage query stage-number)]
(or (::cached-metadata stage)
(when-let [metadata (:lib/stage-metadata stage)]
(when (or (= stage-type :mbql.stage/native)
(and (string? source-table)
(str/starts-with? source-table "card__")))
(let [source-type (case stage-type
:mbql.stage/native :source/native
:mbql.stage/mbql :source/card)]
(for [col (:columns metadata)]
(assoc col :lib/source source-type))))))))
(mu/defn ^:private breakouts-columns :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query
stage-number :- :int]
(not-empty
(for [breakout (lib.breakout/breakouts query stage-number)]
(assoc breakout :lib/source :source/breakouts))))
(mu/defn ^:private aggregations-columns :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query
stage-number :- :int]
(not-empty
(for [ag (lib.aggregation/aggregations query stage-number)]
(assoc ag :lib/source :source/aggregations))))
(mu/defn ^:private fields-columns :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query [query :- ::lib.schema/query
stage-number :- :int] stage-number :- :int]
(when-let [{fields :fields} (lib.util/query-stage query stage-number)] (when-let [{fields :fields} (lib.util/query-stage query stage-number)]
(mapv (fn [ref-clause] (not-empty
(assoc (lib.metadata.calculation/metadata query stage-number ref-clause) :source :fields)) (for [field-ref fields]
fields))) (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :lib/source :source/fields)))))
(mu/defn ^:private breakout-ags-fields-columns :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query
stage-number :- :int]
(not-empty
(into []
cat
[(breakouts-columns query stage-number)
(aggregations-columns query stage-number)
(fields-columns query stage-number)])))
(defn- remove-hidden-default-fields (defn- remove-hidden-default-fields
"Remove Fields that shouldn't be visible from the default Fields for a source Table. "Remove Fields that shouldn't be visible from the default Fields for a source Table.
...@@ -54,15 +124,7 @@ ...@@ -54,15 +124,7 @@
[(or position 0) (u/lower-case-en (or field-name ""))]) [(or position 0) (u/lower-case-en (or field-name ""))])
field-metadatas)) field-metadatas))
(defn- ensure-field-refs [field-metadatas] (mu/defn ^:private source-table-default-fields :- [:maybe StageMetadataColumns]
(for [field-metadata field-metadatas]
(cond-> field-metadata
(not (:field_ref field-metadata))
(assoc :field_ref [:field
{:lib/uuid (str (random-uuid)), :base-type (:base_type field-metadata)}
((some-fn :id :name) field-metadata)]))))
(mu/defn ^:private source-table-default-fields :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
"Determine the Fields we'd normally return for a source Table. "Determine the Fields we'd normally return for a source Table.
See [[metabase.query-processor.middleware.add-implicit-clauses/add-implicit-fields]]." See [[metabase.query-processor.middleware.add-implicit-clauses/add-implicit-fields]]."
[query :- ::lib.schema/query [query :- ::lib.schema/query
...@@ -71,7 +133,8 @@ ...@@ -71,7 +133,8 @@
(->> field-metadatas (->> field-metadatas
remove-hidden-default-fields remove-hidden-default-fields
sort-default-fields sort-default-fields
ensure-field-refs))) (map (fn [col]
(assoc col :lib/source :source/table-defaults))))))
(mu/defn ^:private default-join-alias :- ::lib.schema.common/non-blank-string (mu/defn ^:private default-join-alias :- ::lib.schema.common/non-blank-string
"Generate an alias for a join that doesn't already have one." "Generate an alias for a join that doesn't already have one."
...@@ -99,13 +162,14 @@ ...@@ -99,13 +162,14 @@
(not (:alias join)) (assoc :alias (unique-name-generator (default-join-alias query stage-number join))))) (not (:alias join)) (assoc :alias (unique-name-generator (default-join-alias query stage-number join)))))
joins))) joins)))
(mu/defn ^:private default-columns-added-by-join :- [:maybe [:sequential lib.metadata/ColumnMetadata]] (mu/defn ^:private default-columns-added-by-join :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query [query :- ::lib.schema/query
stage-number :- :int stage-number :- :int
join :- ::lib.schema.join/join] join :- ::lib.schema.join/join]
(lib.metadata.calculation/metadata query stage-number join)) (for [col (lib.metadata.calculation/metadata query stage-number join)]
(assoc col :lib/source :source/joins)))
(mu/defn ^:private default-columns-added-by-joins :- [:maybe [:sequential lib.metadata/ColumnMetadata]] (mu/defn ^:private default-columns-added-by-joins :- [:maybe StageMetadataColumns]
[query :- ::lib.schema/query [query :- ::lib.schema/query
stage-number :- :int] stage-number :- :int]
(when-let [joins (not-empty (:joins (lib.util/query-stage query stage-number)))] (when-let [joins (not-empty (:joins (lib.util/query-stage query stage-number)))]
...@@ -114,7 +178,19 @@ ...@@ -114,7 +178,19 @@
(mapcat (partial default-columns-added-by-join query stage-number)) (mapcat (partial default-columns-added-by-join query stage-number))
(ensure-all-joins-have-aliases query stage-number joins))))) (ensure-all-joins-have-aliases query stage-number joins)))))
(mu/defn ^:private default-columns :- [:sequential lib.metadata/ColumnMetadata] (mu/defn ^:private saved-question-metadata :- [:maybe StageMetadataColumns]
"Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string."
[query :- ::lib.schema/query
source-table-id]
(when (string? source-table-id)
(when-let [[_match card-id-str] (re-find #"^card__(\d+)$" source-table-id)]
(when-let [card-id (parse-long card-id-str)]
(when-let [result-metadata (:result_metadata (lib.metadata/card query card-id))]
(when-let [cols (not-empty (:columns result-metadata))]
(for [col cols]
(assoc col :lib/source :source/card))))))))
(mu/defn ^:private default-columns :- StageMetadataColumns
"Calculate the columns to return if `:aggregations`/`:breakout`/`:fields` are unspecified. "Calculate the columns to return if `:aggregations`/`:breakout`/`:fields` are unspecified.
Formula for the so-called 'default' columns is Formula for the so-called 'default' columns is
...@@ -136,66 +212,46 @@ ...@@ -136,66 +212,46 @@
;; 1: columns from the previous stage, source table or query ;; 1: columns from the previous stage, source table or query
(if-let [previous-stage-number (lib.util/previous-stage-number query stage-number)] (if-let [previous-stage-number (lib.util/previous-stage-number query stage-number)]
;; 1a. columns returned by previous stage ;; 1a. columns returned by previous stage
(stage-metadata query previous-stage-number) (for [col (stage-metadata query previous-stage-number)]
(assoc col :lib/source :source/previous-stage))
;; 1b or 1c ;; 1b or 1c
(let [{:keys [source-table], :as this-stage} (lib.util/query-stage query stage-number)] (let [{:keys [source-table], :as this-stage} (lib.util/query-stage query stage-number)]
(or (or
;; 1b: default visible Fields for the source Table ;; 1b: default visible Fields for the source Table
(when (integer? source-table) (when (integer? source-table)
(source-table-default-fields query source-table)) (source-table-default-fields query source-table))
;; 1c. Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string, OR ;; 1c. Metadata associated with a saved Question
(when (string? source-table) (saved-question-metadata query source-table)
(when-let [[_match card-id-str] (re-find #"^card__(\d+)$" source-table)] ;; 1d: `:lib/stage-metadata` for the (presumably native) query
(when-let [card-id (parse-long card-id-str)] (for [col (:columns (:lib/stage-metadata this-stage))]
(when-let [result-metadata (:result_metadata (lib.metadata/card query card-id))] (assoc col :lib/source :source/native)))))
(not-empty (for [col (:columns result-metadata)]
(assoc col
:field_ref [:field
{:lib/uuid (str (random-uuid)), :base-type (:base_type col)}
(:name col)])))))))
;; 1d: `:lib/stage-metadata` for the native query
(:columns (:lib/stage-metadata this-stage)))))
;; 2: columns added by joins at this stage ;; 2: columns added by joins at this stage
(default-columns-added-by-joins query stage-number))) (default-columns-added-by-joins query stage-number)))
(mu/defn ^:private stage-metadata :- [:and (defn- ensure-distinct-names [metadatas]
[:sequential {:min 1} lib.metadata/ColumnMetadata] (when (seq metadatas)
[:fn (let [f (mbql.u/unique-name-generator)]
;; should be dev-facing only, so don't need to i18n (for [metadata metadatas]
{:error/message "Column :names must be distinct!"} (update metadata :name f)))))
(fn [columns]
(apply distinct? (map :name columns)))]] (mu/defn ^:private stage-metadata :- DistinctStageMetadataColumns
"Return results metadata about the expected columns in an MBQL query stage. If the query has "Return results metadata about the expected columns in an MBQL query stage. If the query has
aggregations/breakouts/fields, then return THOSE. Otherwise return the defaults based on the source Table or aggregations/breakouts/fields, then return THOSE. Otherwise return the defaults based on the source Table or
previous stage + joins." previous stage + joins."
[query :- ::lib.schema/query [query :- ::lib.schema/query
stage-number :- :int] stage-number :- :int]
(or (ensure-distinct-names
;; stage metadata is already present: return it as-is (or
(when-let [metadata (:lib/stage-metadata (lib.util/query-stage query stage-number))] (existing-stage-metadata query stage-number)
(:columns metadata)) (let [query (ensure-previous-stages-have-metadata query stage-number)]
;; otherwise recursively calculate the metadata for the previous stages and add it to them, we'll need it for ;; ... then calculate metadata for this stage
;; calculations for this stage and we don't have to calculate it more than once... (or
(let [query (let [previous-stage-number (lib.util/previous-stage-number query stage-number)] (breakout-ags-fields-columns query stage-number)
(cond-> query (default-columns query stage-number))))))
previous-stage-number
(lib.util/update-query-stage previous-stage-number
assoc
:lib/stage-metadata
{:lib/type :metadata/results
:columns (stage-metadata query previous-stage-number)})))]
;; ... then calculate metadata for this stage
(or
(not-empty (into []
cat
[(lib.breakout/breakouts query stage-number)
(lib.aggregation/aggregations query stage-number)
(fields-columns query stage-number)]))
(default-columns query stage-number)))))
(doseq [stage-type [:mbql.stage/mbql (doseq [stage-type [:mbql.stage/mbql
:mbql.stage/native]] :mbql.stage/native]]
(defmethod lib.metadata.calculation/metadata stage-type (defmethod lib.metadata.calculation/metadata-method stage-type
[query stage-number _stage] [query stage-number _stage]
(stage-metadata query stage-number))) (stage-metadata query stage-number)))
...@@ -236,19 +292,15 @@ ...@@ -236,19 +292,15 @@
(m/distinct-by :fk_target_field_id) (m/distinct-by :fk_target_field_id)
(map (fn [{source-field-id :id, target-field-id :fk_target_field_id}] (map (fn [{source-field-id :id, target-field-id :fk_target_field_id}]
(-> (lib.metadata/field query target-field-id) (-> (lib.metadata/field query target-field-id)
(assoc :source-field-id source-field-id)))) (assoc ::source-field-id source-field-id))))
(remove #(contains? existing-table-ids (:table_id %))) (remove #(contains? existing-table-ids (:table_id %)))
(m/distinct-by :table_id) (m/distinct-by :table_id)
(mapcat (fn [{table-id :table_id, :keys [source-field-id]}] (mapcat (fn [{table-id :table_id, ::keys [source-field-id]}]
(for [field (source-table-default-fields query table-id)] (for [field (source-table-default-fields query table-id)]
(assoc field :field_ref [:field (assoc field :fk_field_id source-field-id)))))
{:lib/uuid (str (random-uuid))
:base-type (:base_type field)
:source-field source-field-id}
(:id field)])))))
column-metadatas))) column-metadatas)))
(mu/defn visible-columns :- [:sequential lib.metadata/ColumnMetadata] (mu/defn visible-columns :- StageMetadataColumns
"Columns that are visible inside a given stage of a query. Ignores `:fields`, `:breakout`, and `:aggregation`. "Columns that are visible inside a given stage of a query. Ignores `:fields`, `:breakout`, and `:aggregation`.
Includes columns that are implicitly joinable from other Tables." Includes columns that are implicitly joinable from other Tables."
[query stage-number] [query stage-number]
......
...@@ -36,18 +36,18 @@ ...@@ -36,18 +36,18 @@
(i18n/tru "next {0} {1}" (pr-str n) (unit->i18n n unit)) (i18n/tru "next {0} {1}" (pr-str n) (unit->i18n n unit))
(i18n/tru "last {0} {1}" (pr-str (abs n)) (unit->i18n (abs n) unit))))) (i18n/tru "last {0} {1}" (pr-str (abs n)) (unit->i18n (abs n) unit)))))
(defmulti temporal-bucket* (defmulti temporal-bucket-method
"Implementation for [[temporal-bucket]]. Implement this to tell [[temporal-bucket]] how to add a bucket to a "Implementation for [[temporal-bucket]]. Implement this to tell [[temporal-bucket]] how to add a bucket to a
particular MBQL clause." particular MBQL clause."
{:arglists '([x unit])} {:arglists '([x unit])}
(fn [x _unit] (fn [x _unit]
(lib.dispatch/dispatch-value x))) (lib.dispatch/dispatch-value x)))
(defmethod temporal-bucket* :dispatch-type/fn (defmethod temporal-bucket-method :dispatch-type/fn
[f unit] [f unit]
(fn [query stage-number] (fn [query stage-number]
(let [x (f query stage-number)] (let [x (f query stage-number)]
(temporal-bucket* x unit)))) (temporal-bucket-method x unit))))
(mu/defn temporal-bucket (mu/defn temporal-bucket
"Add a temporal bucketing unit, e.g. `:day` or `:day-of-year`, to an MBQL clause or something that can be converted to "Add a temporal bucketing unit, e.g. `:day` or `:day-of-year`, to an MBQL clause or something that can be converted to
...@@ -59,4 +59,33 @@ ...@@ -59,4 +59,33 @@
[:field 1 {:temporal-unit :day}]" [:field 1 {:temporal-unit :day}]"
[x unit :- ::lib.schema.temporal-bucketing/unit] [x unit :- ::lib.schema.temporal-bucketing/unit]
(temporal-bucket* x unit)) (temporal-bucket-method x unit))
(defmulti current-temporal-bucket-method
"Implementation of [[current-temporal-bucket]]. Return the current temporal bucketing unit associated with `x`."
{:arglists '([x])}
lib.dispatch/dispatch-value)
(defmethod current-temporal-bucket-method :default
[_x]
nil)
(mu/defn current-temporal-bucket :- [:maybe ::lib.schema.temporal-bucketing/unit]
"Get the current temporal bucketing unit associated with something, if any."
[x]
(current-temporal-bucket-method x))
(defmulti available-temporal-buckets-method
"Implementation for [[available-temporal-buckets]]. Return a set of units from
`:metabase.lib.schema.temporal-bucketing/unit` that are allowed to be used with `x`."
{:arglists '([x])}
lib.dispatch/dispatch-value)
(defmethod available-temporal-buckets-method :default
[_x]
nil)
(mu/defn available-temporal-buckets :- [:maybe [:set {:min 1} [:ref ::lib.schema.temporal-bucketing/unit]]]
"Get a set of available temporal bucketing units for `x`. Returns nil if no units are available."
[x]
(not-empty (temporal-bucket-method x)))
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
[clojure.string :as str] [clojure.string :as str]
[metabase.lib.options :as lib.options] [metabase.lib.options :as lib.options]
[metabase.lib.schema :as lib.schema] [metabase.lib.schema :as lib.schema]
[metabase.lib.schema.common :as lib.schema.common]
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
[metabase.util.malli :as mu])) [metabase.util.malli :as mu]))
...@@ -148,7 +147,7 @@ ...@@ -148,7 +147,7 @@
:native (native-query->pipeline query) :native (native-query->pipeline query)
:query (mbql-query->pipeline query))) :query (mbql-query->pipeline query)))
(mu/defn ^:private non-negative-stage-index :- ::lib.schema.common/int-greater-than-or-equal-to-zero (mu/defn ^:private non-negative-stage-index :- [:int {:min 0}]
"If `stage-number` index is a negative number e.g. `-1` convert it to a positive index so we can use `nth` on "If `stage-number` index is a negative number e.g. `-1` convert it to a positive index so we can use `nth` on
`stages`. `-1` = the last stage, `-2` = the penultimate stage, etc." `stages`. `-1` = the last stage, `-2` = the penultimate stage, etc."
[stages :- [:sequential [:ref ::lib.schema/stage]] [stages :- [:sequential [:ref ::lib.schema/stage]]
...@@ -162,12 +161,11 @@ ...@@ -162,12 +161,11 @@
{:num-stages (count stages)}))) {:num-stages (count stages)})))
stage-number')) stage-number'))
(defn previous-stage-number (mu/defn previous-stage-number :- [:maybe [:int {:min 0}]]
"The index of the previous stage, if there is one. `nil` if there is no previous stage." "The index of the previous stage, if there is one. `nil` if there is no previous stage."
[{:keys [stages], :as _query} stage-number] [{:keys [stages], :as _query} :- :map
(let [stage-number (if (neg? stage-number) stage-number :- :int]
(+ (count stages) stage-number) (let [stage-number (non-negative-stage-index stages stage-number)]
stage-number)]
(when (pos? stage-number) (when (pos? stage-number)
(dec stage-number)))) (dec stage-number))))
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
[clojure.test :refer [are deftest is testing]] [clojure.test :refer [are deftest is testing]]
[metabase.lib.convert :as lib.convert] [metabase.lib.convert :as lib.convert]
[metabase.lib.core :as lib] [metabase.lib.core :as lib]
[metabase.lib.field :as lib.field]
[metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata :as lib.metadata]
[metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.metadata.calculation :as lib.metadata.calculation]
[metabase.lib.query :as lib.query] [metabase.lib.query :as lib.query]
...@@ -193,5 +192,5 @@ ...@@ -193,5 +192,5 @@
(is (=? result-query (is (=? result-query
(-> q (-> q
(lib/aggregate {:operator :sum (lib/aggregate {:operator :sum
:args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]}) :args [(lib/ref (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]})
(dissoc :lib/metadata))))))) (dissoc :lib/metadata)))))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment