Code owners
Assign users and groups as approvers for specific file changes. Learn more.
join.cljc 8.97 KiB
(ns metabase.lib.join
(:require
[medley.core :as m]
[metabase.lib.dispatch :as lib.dispatch]
[metabase.lib.metadata :as lib.metadata]
[metabase.lib.metadata.calculation :as lib.metadata.calculation]
[metabase.lib.options :as lib.options]
[metabase.lib.schema :as lib.schema]
[metabase.lib.schema.common :as lib.schema.common]
[metabase.lib.schema.expression :as lib.schema.expression]
[metabase.lib.schema.join :as lib.schema.join]
[metabase.lib.util :as lib.util]
[metabase.shared.util.i18n :as i18n]
[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
"Resolve a join with a specific `join-alias`."
[query :- ::lib.schema/query
stage-number :- :int
join-alias :- ::lib.schema.common/non-blank-string]
(or (m/find-first #(= (:alias %) join-alias)
(:joins (lib.util/query-stage query stage-number)))
(throw (ex-info (i18n/tru "No join named {0}" (pr-str join-alias))
{:join-alias join-alias
:query query
:stage-number stage-number}))))
(defmethod lib.metadata.calculation/display-name-method :mbql/join
[query _stage-number {[first-stage] :stages, :as _join}]
(if-let [source-table (:source-table first-stage)]
(if (integer? source-table)
(:display_name (lib.metadata/table query source-table))
;; handle card__<id> source tables.
(let [[_ card-id-str] (re-matches #"^card__(\d+)$" source-table)]
(i18n/tru "Saved Question #{0}" card-id-str)))
(i18n/tru "Native Query")))
(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
display name."
[query :- ::lib.schema/query
stage-number :- :int
column-metadata :- lib.metadata/ColumnMetadata
join-alias :- ::lib.schema.common/non-blank-string]
(let [column-metadata (assoc column-metadata :source_alias join-alias)
col (-> (assoc column-metadata
:display_name (lib.metadata.calculation/display-name query stage-number column-metadata)
:lib/source :source/fields)
(with-join-alias join-alias))]
(assert (= (current-join-alias col) join-alias))
col))
(defmethod lib.metadata.calculation/metadata-method :mbql/join
[query stage-number {:keys [fields stages], join-alias :alias, :or {fields :none}, :as _join}]
(when-not (= fields :none)
(let [field-metadatas (if (= fields :all)
(lib.metadata.calculation/metadata (assoc query :stages stages) -1 (last stages))
(for [field-ref fields]
;; resolve the field ref in the context of the join. Not sure if this is right.
(lib.metadata.calculation/metadata query stage-number field-ref)))]
(mapv (fn [field-metadata]
(column-from-join-fields query stage-number field-metadata join-alias))
field-metadatas))))
(defmulti ^:private ->join-clause
{:arglists '([query stage-number x])}
(fn [_query _stage-number x]
(lib.dispatch/dispatch-value x)))
;; TODO -- should the default implementation call [[metabase.lib.query/query]]? That way if we implement a method to
;; create an MBQL query from a `Table`, then we'd also get [[join]] support for free?
(defmethod ->join-clause :mbql/join
[_query _stage-number a-join-clause]
a-join-clause)
(defmethod ->join-clause :mbql/query
[_query _stage-number another-query]
(-> {:lib/type :mbql/join
:stages (:stages (lib.util/pipeline another-query))}
lib.options/ensure-uuid))
(defmethod ->join-clause :mbql.stage/mbql
[_query _stage-number mbql-stage]
(-> {:lib/type :mbql/join
:stages [mbql-stage]}
lib.options/ensure-uuid))
(defmethod ->join-clause :metadata/table
[query stage-number table-metadata]
(->join-clause query
stage-number
{:lib/type :mbql.stage/mbql
:lib/options {:lib/uuid (str (random-uuid))}
:source-table (:id table-metadata)}))
(defmethod ->join-clause :dispatch-type/fn
[query stage-number f]
(->join-clause query stage-number (f query stage-number)))
;; TODO this is basically the same as lib.common/->op-args,
;; but requiring lib.common leads to crircular dependencies:
;; join -> common -> field -> join.
(defmulti ^:private ->join-condition
{:arglists '([query stage-number x])}
(fn [_query _stage-number x]
(lib.dispatch/dispatch-value x)))
(defmethod ->join-condition :default
[_query _stage-number x]
x)
(defmethod ->join-condition :lib/external-op
[query stage-number {:keys [operator options args] :or {options {}}}]
(->join-condition query stage-number
(lib.options/ensure-uuid (into [operator options] args))))
(defmethod ->join-condition :dispatch-type/fn
[query stage-number f]
(->join-condition query stage-number (f query stage-number)))
(mu/defn join-condition :- [:or
fn?
::lib.schema.expression/boolean]
"Create a MBQL condition expression to include as the `:condition` in a join map.
- One arity: return a function that will be resolved later once we have `query` and `stage-number.`
- Three arity: return the join condition expression immediately."
([x]
(fn [query stage-number]
(join-condition query stage-number x)))
([query stage-number x]
(->join-condition query stage-number x)))
(defn join-clause
"Create an MBQL join map from something that can conceptually be joined against. A `Table`? An MBQL or native query? A
Saved Question? You should be able to join anything, and this should return a sensible MBQL join map."
([x]
(fn [query stage-number]
(join-clause query stage-number x)))
([x condition]
(fn [query stage-number]
(join-clause query stage-number x condition)))
([query stage-number x]
(->join-clause query stage-number x))
([query stage-number x condition]
(cond-> (join-clause query stage-number x)
condition (assoc :condition (join-condition query stage-number condition)))))
(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
references."
[x fields :- ::lib.schema.join/fields]
(if (fn? x)
(fn [query stage-number]
(with-join-fields (x query stage-number) fields))
(assoc x :fields fields)))
(mu/defn join :- ::lib.schema/query
"Create a join map as if by [[join-clause]] and add it to a `query`.
`condition` is currently required, but in the future I think we should make this smarter and try to infer a sensible
default condition for things, e.g. when joining a Table B from Table A, if there is an FK relationship between A and
B, join via that relationship. Not yet implemented!"
([query a-join-clause]
(join query -1 a-join-clause (:condition a-join-clause)))
([query x condition]
(join query -1 x condition))
([query stage-number x condition]
(let [stage-number (or stage-number -1)
new-join (cond-> (->join-clause query stage-number x)
condition (assoc :condition (join-condition query stage-number condition)))]
(lib.util/update-query-stage query stage-number update :joins (fn [joins]
(conj (vec joins) new-join))))))
(mu/defn joins :- ::lib.schema.join/joins
"Get all joins in a specific `stage` of a `query`. If `stage` is unspecified, returns joins in the final stage of the
query."
([query]
(joins query -1))
([query :- ::lib.schema/query
stage-number :- ::lib.schema.common/int-greater-than-or-equal-to-zero]
(not-empty (get (lib.util/query-stage query stage-number) :joins))))