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

Support Legacy Metrics in MLv1 (#31885)

* Extra Metric tests; include `:description` in the Metric `display-info`

* Add `available-metrics` function & TS wrappers

* Support passing a Metric to `lib/aggregate`

* Include `:selected` information in Metric display info

* Fix indentation

* Revert changes to arithmetic type-of calculation

* Test fixes :wrench:

* Address PR feedback

* Address PR feedback

* Update docstring
parent 310ef9ab
No related branches found
No related tags found
No related merge requests found
Showing
with 184 additions and 47 deletions
...@@ -3,11 +3,11 @@ ...@@ -3,11 +3,11 @@
{:unresolved-symbol {:unresolved-symbol
{:exclude {:exclude
[instaparse.core/transform [instaparse.core/transform
(cljs.test/is [=?]) (cljs.test/is [=? malli=])
(clojure.core.logic/fresh) (clojure.core.logic/fresh)
(clojure.core.logic/matcha) (clojure.core.logic/matcha)
(clojure.core.logic/run) (clojure.core.logic/run)
(clojure.test/is [query= sql= =?]) (clojure.test/is [query= sql= =? malli=])
(clojure.tools.macro/macrolet) (clojure.tools.macro/macrolet)
(metabase.async.streaming-response/streaming-response) (metabase.async.streaming-response/streaming-response)
(metabase.driver.druid.query-processor-test/druid-query-returning-rows) (metabase.driver.druid.query-processor-test/druid-query-returning-rows)
......
import * as ML from "cljs/metabase.lib.js";
import type { MetricMetadata, Query } from "./types";
export function availableMetrics(query: Query): MetricMetadata[] {
return ML.available_metrics(query);
}
...@@ -14,6 +14,9 @@ export type TableMetadata = unknown & { _opaque: typeof TableMetadata }; ...@@ -14,6 +14,9 @@ export type TableMetadata = unknown & { _opaque: typeof TableMetadata };
declare const CardMetadata: unique symbol; declare const CardMetadata: unique symbol;
export type CardMetadata = unknown & { _opaque: typeof CardMetadata }; export type CardMetadata = unknown & { _opaque: typeof CardMetadata };
declare const MetricMetadata: unique symbol;
export type MetricMetadata = unknown & { _opaque: typeof MetricMetadata };
export type Limit = number | null; export type Limit = number | null;
declare const BreakoutClause: unique symbol; declare const BreakoutClause: unique symbol;
......
...@@ -12,6 +12,7 @@ export * from "./limit"; ...@@ -12,6 +12,7 @@ export * from "./limit";
export * from "./order_by"; export * from "./order_by";
export * from "./filter"; export * from "./filter";
export * from "./join"; export * from "./join";
export * from "./metrics";
export * from "./query"; export * from "./query";
export * from "./temporal_bucket"; export * from "./temporal_bucket";
export * from "./types"; export * from "./types";
...@@ -107,6 +107,8 @@ ...@@ -107,6 +107,8 @@
(setting/set-value-of-type! :json :custom-geojson new-value))) (setting/set-value-of-type! :json :custom-geojson new-value)))
:visibility :public) :visibility :public)
(def ^:private connection-timeout-ms 8000)
(defn- read-url-and-respond (defn- read-url-and-respond
"Reads the provided URL and responds with the contents as a stream." "Reads the provided URL and responds with the contents as a stream."
[url respond] [url respond]
...@@ -114,8 +116,8 @@ ...@@ -114,8 +116,8 @@
(io/reader resource) (io/reader resource)
(:body (http/get url {:as :reader (:body (http/get url {:as :reader
:redirect-strategy :none :redirect-strategy :none
:socket-timeout 8000 :socket-timeout connection-timeout-ms
:connection-timeout 8000}))) :connection-timeout connection-timeout-ms})))
is (ReaderInputStream. reader)] is (ReaderInputStream. reader)]
(respond (-> (response/response is) (respond (-> (response/response is)
(response/content-type "application/json"))))) (response/content-type "application/json")))))
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
(:require (:require
[medley.core :as m] [medley.core :as m]
[metabase.lib.common :as lib.common] [metabase.lib.common :as lib.common]
[metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.equality :as lib.equality] [metabase.lib.equality :as lib.equality]
[metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.hierarchy :as lib.hierarchy]
[metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata :as lib.metadata]
...@@ -223,12 +224,25 @@ ...@@ -223,12 +224,25 @@
[aggregation-clause] [aggregation-clause]
aggregation-clause) aggregation-clause)
(def ^:private Aggregatable
"Schema for something you can pass to [[aggregate]] to add to a query as an aggregation."
[:or
::lib.schema.aggregation/aggregation
::lib.schema.common/external-op
lib.metadata/MetricMetadata])
(mu/defn aggregate :- ::lib.schema/query (mu/defn aggregate :- ::lib.schema/query
"Adds an aggregation to query." "Adds an aggregation to query."
([query an-aggregate-clause] ([query aggregatable]
(aggregate query -1 an-aggregate-clause)) (aggregate query -1 aggregatable))
([query stage-number an-aggregate-clause]
(lib.util/add-summary-clause query stage-number :aggregation an-aggregate-clause))) ([query :- ::lib.schema/query
stage-number :- :int
aggregatable :- Aggregatable]
;; if this is a Metric metadata, convert it to `:metric` MBQL clause before adding.
(if (= (lib.dispatch/dispatch-value aggregatable) :metadata/metric)
(recur query stage-number (lib.ref/ref aggregatable))
(lib.util/add-summary-clause query stage-number :aggregation aggregatable))))
(mu/defn aggregations :- [:maybe [:sequential ::lib.schema.aggregation/aggregation]] (mu/defn aggregations :- [:maybe [:sequential ::lib.schema.aggregation/aggregation]]
"Get the aggregations in a given stage of a query." "Get the aggregations in a given stage of a query."
......
...@@ -188,6 +188,8 @@ ...@@ -188,6 +188,8 @@
display-info display-info
suggested-name suggested-name
type-of] type-of]
[lib.metric
available-metrics]
[lib.native [lib.native
#?@(:cljs [->TemplateTags #?@(:cljs [->TemplateTags
TemplateTags->]) TemplateTags->])
......
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
(defn derive (defn derive
"Like [[clojure.core/derive]], but affects [[hierarchy]] rather than the global hierarchy." "Like [[clojure.core/derive]], but affects [[hierarchy]] rather than the global hierarchy."
[tag parent] [tag parent]
(swap! hierarchy clojure.core/derive tag parent)) (swap! hierarchy clojure.core/derive tag parent)
;; for REPL convenience so we don't dump a lot of garbage
nil)
(defn isa? (defn isa?
"Like [[clojure.core/isa?]], but uses [[hierarchy]]." "Like [[clojure.core/isa?]], but uses [[hierarchy]]."
......
...@@ -555,3 +555,9 @@ ...@@ -555,3 +555,9 @@
"Returns the native query's template tags" "Returns the native query's template tags"
[a-query] [a-query]
(lib.core/TemplateTags-> (lib.core/template-tags a-query))) (lib.core/TemplateTags-> (lib.core/template-tags a-query)))
(defn ^:export available-metrics
"Get a list of Metrics that you may consider using as aggregations for a query. Returns JS array of opaque Metric
metadata objects."
[a-query]
(to-array (lib.core/available-metrics a-query)))
...@@ -375,6 +375,12 @@ ...@@ -375,6 +375,12 @@
:when (and a-field (= (:table-id a-field) table-id))] :when (and a-field (= (:table-id a-field) table-id))]
a-field)) a-field))
(defn- metrics [metadata table-id]
(for [[_id metric-delay] (some-> metadata :metrics deref)
:let [a-metric (some-> metric-delay deref)]
:when (and a-metric (= (:table-id a-metric) table-id))]
a-metric))
(defn metadata-provider (defn metadata-provider
"Use a `metabase-lib/metadata/Metadata` as a [[metabase.lib.metadata.protocols/MetadataProvider]]." "Use a `metabase-lib/metadata/Metadata` as a [[metabase.lib.metadata.protocols/MetadataProvider]]."
[database-id unparsed-metadata] [database-id unparsed-metadata]
...@@ -389,6 +395,7 @@ ...@@ -389,6 +395,7 @@
(card [_this card-id] (card metadata card-id)) (card [_this card-id] (card metadata card-id))
(tables [_this] (tables metadata database-id)) (tables [_this] (tables metadata database-id))
(fields [_this table-id] (fields metadata table-id)) (fields [_this table-id] (fields metadata table-id))
(metrics [_this table-id] (metrics metadata table-id))
;; for debugging: call [[clojure.datafy/datafy]] on one of these to parse all of our metadata and see the whole ;; for debugging: call [[clojure.datafy/datafy]] on one of these to parse all of our metadata and see the whole
;; thing at once. ;; thing at once.
......
...@@ -117,19 +117,27 @@ ...@@ -117,19 +117,27 @@
;; probably missing `:lib/type` and probably using `:snake_case` keys. ;; probably missing `:lib/type` and probably using `:snake_case` keys.
[:result-metadata {:optional true} [:maybe [:sequential :map]]]]) [:result-metadata {:optional true} [:maybe [:sequential :map]]]])
(def ^:private SegmentMetadata (def SegmentMetadata
"More or less the same as a [[metabase.models.segment]], but with kebab-case keys." "More or less the same as a [[metabase.models.segment]], but with kebab-case keys."
[:map [:map
[:lib/type [:= :metadata/segment]] [:lib/type [:= :metadata/segment]]
[:id ::lib.schema.id/segment] [:id ::lib.schema.id/segment]
[:name ::lib.schema.common/non-blank-string]]) [:name ::lib.schema.common/non-blank-string]])
(def ^:private MetricMetadata (def MetricMetadata
"More or less the same as a [[metabase.models.metric]], but with kebab-case keys." "Malli schema for a legacy v1 [[metabase.models.metric]], but with kebab-case keys. A Metric defines an MBQL snippet
with an aggregation and optionally a filter clause. You can add a `:metric` reference to the `:aggregations` in an
MBQL stage, and the QP treats it like a macro and expands it to the underlying clauses --
see [[metabase.query-processor.middleware.expand-macros]]."
[:map [:map
[:lib/type [:= :metadata/metric]] [:lib/type [:= :metadata/metric]]
[:id ::lib.schema.id/metric] [:id ::lib.schema.id/metric]
[:name ::lib.schema.common/non-blank-string]]) [:name ::lib.schema.common/non-blank-string]
[:table-id ::lib.schema.id/table]
;; the MBQL snippet defining this Metric; this may still be in legacy
;; format. [[metabase.lib.metric/metric-definition]] handles conversion to pMBQL if needed.
[:definition :map]
[:description {:optional true} [:maybe ::lib.schema.common/non-blank-string]]])
(def TableMetadata (def TableMetadata
"Schema for metadata about a specific [[metabase.models.table]]. More or less the same as a [[metabase.models.table]], "Schema for metadata about a specific [[metabase.models.table]]. More or less the same as a [[metabase.models.table]],
...@@ -155,7 +163,9 @@ ...@@ -155,7 +163,9 @@
(def MetadataProvider (def MetadataProvider
"Schema for something that satisfies the [[lib.metadata.protocols/MetadataProvider]] protocol." "Schema for something that satisfies the [[lib.metadata.protocols/MetadataProvider]] protocol."
[:fn lib.metadata.protocols/metadata-provider?]) [:fn
{:error/message "Valid MetadataProvider"}
#'lib.metadata.protocols/metadata-provider?])
(def MetadataProviderable (def MetadataProviderable
"Something that can be used to get a MetadataProvider. Either a MetadataProvider, or a map with a MetadataProvider in "Something that can be used to get a MetadataProvider. Either a MetadataProvider, or a map with a MetadataProvider in
......
...@@ -55,12 +55,13 @@ ...@@ -55,12 +55,13 @@
lib.metadata.protocols/MetadataProvider lib.metadata.protocols/MetadataProvider
(database [_this] (get-in-cache-or-fetch cache [:metadata/database] #(lib.metadata.protocols/database metadata-provider))) (database [_this] (get-in-cache-or-fetch cache [:metadata/database] #(lib.metadata.protocols/database metadata-provider)))
(table [_this table-id] (get-in-cache-or-fetch cache [:metadata/table table-id] #(lib.metadata.protocols/table metadata-provider table-id))) (table [_this table-id] (get-in-cache-or-fetch cache [:metadata/table table-id] #(lib.metadata.protocols/table metadata-provider table-id)))
(field [_this field-id] (get-in-cache-or-fetch cache [:metadata/column field-id] #(lib.metadata.protocols/field metadata-provider field-id))) (field [_this field-id] (get-in-cache-or-fetch cache [:metadata/column field-id] #(lib.metadata.protocols/field metadata-provider field-id)))
(card [_this card-id] (get-in-cache-or-fetch cache [:metadata/card card-id] #(lib.metadata.protocols/card metadata-provider card-id))) (card [_this card-id] (get-in-cache-or-fetch cache [:metadata/card card-id] #(lib.metadata.protocols/card metadata-provider card-id)))
(metric [_this metric-id] (get-in-cache-or-fetch cache [:metadata/metric metric-id] #(lib.metadata.protocols/metric metadata-provider metric-id))) (metric [_this metric-id] (get-in-cache-or-fetch cache [:metadata/metric metric-id] #(lib.metadata.protocols/metric metadata-provider metric-id)))
(segment [_this segment-id] (get-in-cache-or-fetch cache [:metadata/segment segment-id] #(lib.metadata.protocols/segment metadata-provider segment-id))) (segment [_this segment-id] (get-in-cache-or-fetch cache [:metadata/segment segment-id] #(lib.metadata.protocols/segment metadata-provider segment-id)))
(tables [_this] (get-in-cache-or-fetch cache [::database-tables] #(lib.metadata.protocols/tables metadata-provider))) (tables [_this] (get-in-cache-or-fetch cache [::database-tables] #(lib.metadata.protocols/tables metadata-provider)))
(fields [_this table-id] (get-in-cache-or-fetch cache [::table-fields table-id] #(lib.metadata.protocols/fields metadata-provider table-id))) (fields [_this table-id] (get-in-cache-or-fetch cache [::table-fields table-id] #(lib.metadata.protocols/fields metadata-provider table-id)))
(metrics [_this table-id] (get-in-cache-or-fetch cache [::table-metrics table-id] #(lib.metadata.protocols/metrics metadata-provider table-id)))
lib.metadata.protocols/CachedMetadataProvider lib.metadata.protocols/CachedMetadataProvider
(cached-database [_this] (get-in-cache cache [:metadata/database])) (cached-database [_this] (get-in-cache cache [:metadata/database]))
......
...@@ -174,7 +174,12 @@ ...@@ -174,7 +174,12 @@
;; otherwise if `:base-type` is specified, we can return that. ;; otherwise if `:base-type` is specified, we can return that.
(:base-type options) (:base-type options)
;; if none of the special cases are true, fall back to [[type-of-method]]. ;; if none of the special cases are true, fall back to [[type-of-method]].
(type-of-method query stage-number x))))) (let [calculated-type (type-of-method query stage-number x)]
;; if calculated type is not a true type but a placeholder like `:metabase.lib.schema.expression/type.unknown`
;; or a union of types then fall back to `:type/*`, an actual type.
(if (isa? calculated-type :type/*)
calculated-type
:type/*))))))
(defmethod type-of-method :default (defmethod type-of-method :default
[_query _stage-number expr] [_query _stage-number expr]
...@@ -358,7 +363,7 @@ ...@@ -358,7 +363,7 @@
(defmethod display-info-method :metadata/table (defmethod display-info-method :metadata/table
[query stage-number table] [query stage-number table]
(merge (default-display-info query stage-number table) (merge (default-display-info query stage-number table)
{:is-source-table (= (lib.util/source-table query) (:id table))})) {:is-source-table (= (lib.util/source-table-id query) (:id table))}))
(def ColumnsWithUniqueAliases (def ColumnsWithUniqueAliases
"Schema for column metadata that should be returned by [[visible-columns]]. This is mostly used "Schema for column metadata that should be returned by [[visible-columns]]. This is mostly used
......
...@@ -50,6 +50,11 @@ ...@@ -50,6 +50,11 @@
(mapv #(instance->metadata % :metadata/column) (mapv #(instance->metadata % :metadata/column)
(t2/select :model/Field :table_id table-id))) (t2/select :model/Field :table_id table-id)))
(defn- metrics [table-id]
(log/debugf "Fetching all Metrics for Table %d" table-id)
(mapv #(instance->metadata % :metadata/metric)
(t2/select :model/Metric :table_id table-id)))
(p/deftype+ UncachedApplicationDatabaseMetadataProvider [database-id] (p/deftype+ UncachedApplicationDatabaseMetadataProvider [database-id]
lib.metadata.protocols/MetadataProvider lib.metadata.protocols/MetadataProvider
(database [_this] (database [_this]
...@@ -72,6 +77,9 @@ ...@@ -72,6 +77,9 @@
(fields [_this table-id] (fields [_this table-id]
(fields table-id)) (fields table-id))
(metrics [_this table-id]
(metrics table-id))
lib.metadata.protocols/BulkMetadataProvider lib.metadata.protocols/BulkMetadataProvider
(bulk-metadata [_this metadata-type ids] (bulk-metadata [_this metadata-type ids]
(bulk-instances metadata-type ids)) (bulk-instances metadata-type ids))
......
...@@ -61,7 +61,11 @@ ...@@ -61,7 +61,11 @@
(fields [metadata-provider table-id] (fields [metadata-provider table-id]
"Return a sequence of Fields associated with a Table with the given `table-id`. Fields should satisfy "Return a sequence of Fields associated with a Table with the given `table-id`. Fields should satisfy
the [[metabase.lib.metadata/ColumnMetadata]] schema. If no such Table exists, this should error.")) the [[metabase.lib.metadata/ColumnMetadata]] schema. If no such Table exists, this should error.")
(metrics [metadata-provider table-id]
"Return a sequence of legacy v1 Metrics associated with a Table with the given `table-id`. Metrics should satisfy
the [[metabase.lib.metadata/MetricMetadata]] schema. If no such Table exists, this should error."))
(defn metadata-provider? (defn metadata-provider?
"Whether `x` is a valid [[MetadataProvider]]." "Whether `x` is a valid [[MetadataProvider]]."
......
...@@ -5,7 +5,11 @@ ...@@ -5,7 +5,11 @@
[metabase.lib.convert :as lib.convert] [metabase.lib.convert :as lib.convert]
[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.metadata.protocols :as lib.metadata.protocols]
[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.id :as lib.schema.id]
[metabase.lib.util :as lib.util] [metabase.lib.util :as lib.util]
[metabase.mbql.normalize :as mbql.normalize] [metabase.mbql.normalize :as mbql.normalize]
[metabase.shared.util.i18n :as i18n] [metabase.shared.util.i18n :as i18n]
...@@ -16,7 +20,7 @@ ...@@ -16,7 +20,7 @@
(lib.metadata/metric query metric-id))) (lib.metadata/metric query metric-id)))
(mu/defn ^:private metric-definition :- [:maybe ::lib.schema/stage.mbql] (mu/defn ^:private metric-definition :- [:maybe ::lib.schema/stage.mbql]
[{:keys [definition], :as _metric-metadata}] [{:keys [definition], :as _metric-metadata} :- lib.metadata/MetricMetadata]
(when definition (when definition
(if (:mbql/type definition) (if (:mbql/type definition)
definition definition
...@@ -28,6 +32,18 @@ ...@@ -28,6 +32,18 @@
lib.convert/->pMBQL lib.convert/->pMBQL
(lib.util/query-stage -1))))) (lib.util/query-stage -1)))))
(defmethod lib.ref/ref-method :metadata/metric
[{:keys [id], :as metric-metadata}]
(let [effective-type (or (:effective-type metric-metadata)
(:base-type metric-metadata)
(when-let [aggregation (first (:aggregation (metric-definition metric-metadata)))]
(let [ag-effective-type (lib.schema.expression/type-of aggregation)]
(when (isa? ag-effective-type :type/*)
ag-effective-type))))
options (cond-> {:lib/uuid (str (random-uuid))}
effective-type (assoc :effective-type effective-type))]
[:metric options id]))
(defmethod lib.metadata.calculation/type-of-method :metadata/metric (defmethod lib.metadata.calculation/type-of-method :metadata/metric
[query stage-number metric-metadata] [query stage-number metric-metadata]
(or (or
...@@ -41,19 +57,58 @@ ...@@ -41,19 +57,58 @@
(lib.metadata.calculation/type-of query stage-number metric-metadata)) (lib.metadata.calculation/type-of query stage-number metric-metadata))
:type/*)) :type/*))
(defn- fallback-display-name []
(i18n/tru "[Unknown Metric]"))
(defmethod lib.metadata.calculation/display-name-method :metadata/metric (defmethod lib.metadata.calculation/display-name-method :metadata/metric
[_query _stage-number metric-metadata _style] [_query _stage-number metric-metadata _style]
(or ((some-fn :display-name :name) metric-metadata) (or ((some-fn :display-name :name) metric-metadata)
(i18n/tru "[Unknown Metric]"))) (fallback-display-name)))
(defmethod lib.metadata.calculation/display-name-method :metric (defmethod lib.metadata.calculation/display-name-method :metric
[query stage-number [_tag _opts metric-id-or-name] style] [query stage-number [_tag _opts metric-id-or-name] style]
(or (when-let [metric-metadata (resolve-metric query metric-id-or-name)] (or (when-let [metric-metadata (resolve-metric query metric-id-or-name)]
(lib.metadata.calculation/display-name query stage-number metric-metadata style)) (lib.metadata.calculation/display-name query stage-number metric-metadata style))
(i18n/tru "[Unknown Metric]"))) (fallback-display-name)))
(mu/defn ^:private aggregating-by-metric? :- :boolean
"Whether a given stage of a query currently includes a `:metric` ref clause in its aggregations."
[query :- ::lib.schema/query
stage-number :- :int
metric-id :- ::lib.schema.id/metric]
(let [{aggregations :aggregation} (lib.util/query-stage query stage-number)]
(boolean
(some (fn [[tag :as clause]]
(when (= tag :metric)
(let [[_tag _opts id] clause]
(= id metric-id))))
aggregations))))
(defmethod lib.metadata.calculation/display-info-method :metadata/metric
[query stage-number {:keys [id description], :as metric-metadata}]
(merge
((get-method lib.metadata.calculation/display-info-method :default) query stage-number metric-metadata)
{:description description}
(when (aggregating-by-metric? query stage-number id)
{:selected true})))
(defmethod lib.metadata.calculation/display-info-method :metric
[query stage-number [_tag _opts metric-id-or-name]]
(if-let [metric-metadata (resolve-metric query metric-id-or-name)]
(lib.metadata.calculation/display-info query stage-number metric-metadata)
{:effective-type :type/*
:display-name (fallback-display-name)
:long-display-name (fallback-display-name)}))
(defmethod lib.metadata.calculation/column-name-method :metric (defmethod lib.metadata.calculation/column-name-method :metric
[query stage-number [_tag _opts metric-id-or-name]] [query stage-number [_tag _opts metric-id-or-name]]
(or (when-let [metric-metadata (resolve-metric query metric-id-or-name)] (or (when-let [metric-metadata (resolve-metric query metric-id-or-name)]
(lib.metadata.calculation/column-name query stage-number metric-metadata)) (lib.metadata.calculation/column-name query stage-number metric-metadata))
"metric")) "metric"))
(mu/defn available-metrics :- [:maybe [:sequential {:min 1} lib.metadata/MetricMetadata]]
"Get a list of Metrics that you may consider using as aggregations for a query. Only Metrics that have the same
`table-id` as the `source-table` for this query will be suggested."
[query :- ::lib.schema/query]
(when-let [source-table-id (lib.util/source-table-id query)]
(not-empty (lib.metadata.protocols/metrics (lib.metadata/->metadata-provider query) source-table-id))))
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
[metabase.lib.schema.expression :as expression] [metabase.lib.schema.expression :as expression]
[metabase.lib.schema.mbql-clause :as mbql-clause] [metabase.lib.schema.mbql-clause :as mbql-clause]
[metabase.types :as types] [metabase.types :as types]
[metabase.util.malli.registry :as mr])) [metabase.util.malli.registry :as mr]
))
;;; the logic for calculating the return type of a `:case` or similar statement is not optimal nor perfect. But it ;;; the logic for calculating the return type of a `:case` or similar statement is not optimal nor perfect. But it
;;; should be ok for now and errors on the side of being permissive. See this Slack thread for more info: ;;; should be ok for now and errors on the side of being permissive. See this Slack thread for more info:
...@@ -18,6 +19,11 @@ ...@@ -18,6 +19,11 @@
(nil? x) (nil? x)
y y
;; if the type of either x or y is unknown, then the overall type of this has to be unknown as well.
(or (= x ::expression/type.unknown)
(= y ::expression/type.unknown))
::expression/type.unknown
;; if both types are keywords return their most-specific ancestor. ;; if both types are keywords return their most-specific ancestor.
(and (keyword? x) (and (keyword? x)
(keyword? y)) (keyword? y))
...@@ -55,25 +61,15 @@ ...@@ -55,25 +61,15 @@
[:pred-expr-pairs [:sequential {:min 1} [:ref ::case-subclause]]] [:pred-expr-pairs [:sequential {:min 1} [:ref ::case-subclause]]]
[:default [:? [:schema [:ref ::expression/expression]]]]) [:default [:? [:schema [:ref ::expression/expression]]]])
(defn- allow-unkown-type
"A hack to allow case and coalesce expressions using fields only.
The real fix would pass in metadata so that the types of fields
can be determined."
[expr-type]
(if (= expr-type ::expression/type.unknown)
:type/*
expr-type))
(defmethod expression/type-of-method :case (defmethod expression/type-of-method :case
[[_tag _opts pred-expr-pairs default]] [[_tag _opts pred-expr-pairs default]]
(-> (reduce (reduce
(fn [best-guess [_pred expr]] (fn [best-guess [_pred expr]]
(let [expr-type (expression/type-of expr)] (let [expr-type (expression/type-of expr)]
(best-return-type best-guess expr-type))) (best-return-type best-guess expr-type)))
(when (some? default) (when (some? default)
(expression/type-of default)) (expression/type-of default))
pred-expr-pairs) pred-expr-pairs))
allow-unkown-type))
;;; TODO -- add constraint that these types have to be compatible ;;; TODO -- add constraint that these types have to be compatible
(mbql-clause/define-tuple-mbql-clause :coalesce (mbql-clause/define-tuple-mbql-clause :coalesce
...@@ -82,5 +78,4 @@ ...@@ -82,5 +78,4 @@
(defmethod expression/type-of-method :coalesce (defmethod expression/type-of-method :coalesce
[[_tag _opts expr null-value]] [[_tag _opts expr null-value]]
(-> (best-return-type (expression/type-of expr) (expression/type-of null-value)) (best-return-type (expression/type-of expr) (expression/type-of null-value)))
allow-unkown-type))
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
([tag :- simple-keyword? ([tag :- simple-keyword?
_arrow :- [:= :-] _arrow :- [:= :-]
return-type :- [:fn {:error/message "valid base type"} #(isa? % :type/*)] return-type :- ::expression/base-type
schema] schema]
(define-mbql-clause tag schema) (define-mbql-clause tag schema)
(defmethod expression/type-of-method tag (defmethod expression/type-of-method tag
......
(ns metabase.lib.schema.ref (ns metabase.lib.schema.ref
"Malli schema for a Field, aggregation, or expression reference (etc.)" "Malli schema for a Field, aggregation, or expression reference (etc.)"
(:require (:require
[clojure.string :as str]
[metabase.lib.dispatch :as lib.dispatch] [metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.hierarchy :as lib.hierarchy]
[metabase.lib.schema.common :as common] [metabase.lib.schema.common :as common]
[metabase.lib.schema.expression :as expression] [metabase.lib.schema.expression :as expression]
[metabase.lib.schema.id :as id] [metabase.lib.schema.id :as id]
...@@ -55,6 +57,8 @@ ...@@ -55,6 +57,8 @@
[:dispatch-type/integer ::field.id] [:dispatch-type/integer ::field.id]
[:dispatch-type/string ::field.literal]]]) [:dispatch-type/string ::field.literal]]])
(lib.hierarchy/derive :field ::ref)
(defmethod expression/type-of-method :field (defmethod expression/type-of-method :field
[[_tag opts _id-or-name]] [[_tag opts _id-or-name]]
(or ((some-fn :effective-type :base-type) opts) (or ((some-fn :effective-type :base-type) opts)
...@@ -68,6 +72,8 @@ ...@@ -68,6 +72,8 @@
(or ((some-fn :effective-type :base-type) opts) (or ((some-fn :effective-type :base-type) opts)
::expression/type.unknown)) ::expression/type.unknown))
(lib.hierarchy/derive :expression ::ref)
(mr/def ::aggregation-options (mr/def ::aggregation-options
[:merge [:merge
::common/options ::common/options
...@@ -86,10 +92,19 @@ ...@@ -86,10 +92,19 @@
(or ((some-fn :effective-type :base-type) opts) (or ((some-fn :effective-type :base-type) opts)
::expression/type.unknown)) ::expression/type.unknown))
(lib.hierarchy/derive :aggregation ::ref)
(mbql-clause/define-tuple-mbql-clause :metric :- ::expression/type.unknown
#_metric-id [:schema [:ref ::id/metric]])
(lib.hierarchy/derive :metric ::ref)
(mr/def ::ref (mr/def ::ref
[:and [:and
::mbql-clause/clause ::mbql-clause/clause
[:fn [:fn
{:error/message ":field, :expression, :or :aggregation reference"} {:error/fn (fn [_ _]
(str "Valid reference, must be one of these clauses: "
(str/join ", " (sort (descendants @lib.hierarchy/hierarchy ::ref)))))}
(fn [[tag :as _clause]] (fn [[tag :as _clause]]
(#{:field :expression :aggregation} tag))]]) (lib.hierarchy/isa? tag ::ref))]])
...@@ -456,7 +456,7 @@ ...@@ -456,7 +456,7 @@
(when-let [[_match card-id-str] (re-find #"^card__(\d+)$" table-id)] (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" table-id)]
(parse-long card-id-str)))) (parse-long card-id-str))))
(mu/defn source-table :- [:maybe ::lib.schema.id/table] (mu/defn source-table-id :- [:maybe ::lib.schema.id/table]
"If this query has a `:source-table` ID, return it." "If this query has a `:source-table` ID, return it."
[query] [query]
(-> query :stages first :source-table)) (-> query :stages first :source-table))
......
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