JavaScript-facing interface to MBQL Lib v2. Generally, these functions wrap [[lib.core]] with conversion of inputs from JS data structures, and occasionally from legacy MLv1 formats as well. Outputs are usually CLJS data structures intended to be treated as opaque in the FE. Returned lists are converted from CLJS sequences to JS arrays (of opaque CLJS values). On the TypeScript side, lint rules restrict importing this file to only the Terms and typesA reference of what is meant in these docs by "column", "query", etc. Most of the CLJS maps have a
Code healthThis API surface grew mostly organically during the development of MLv2 and porting the query builder to use it. The result is that the API is not as systematic or clean as it could be. There are functions which are very specific to a particular use case in one part of the FE, and functions which support legacy compatibility but should be removed as those features are ported. Health info is surfaced on each function, using these categories:
Over time, the Deprecated functions will be removed, and the Legacy ones will become obsolete and get removed as legacy uses are ported to MLv2. Display InfoThe library functions typically return opaque CLJS data. We want to hide the library's internals, but we want it to be easy for the FE to consume queries, columns, aggregations, etc. and render them in the UI. This is accomplished using | (ns metabase.lib.js (:refer-clojure :exclude [filter]) (:require [clojure.string :as str] [clojure.walk :as walk] [goog.object :as gobject] [medley.core :as m] [metabase.legacy-mbql.js :as mbql.js] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.lib.cache :as lib.cache] [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib.core] [metabase.lib.drill-thru.common :as lib.drill-thru.common] [metabase.lib.equality :as lib.equality] [metabase.lib.expression :as lib.expression] [metabase.lib.field :as lib.field] [metabase.lib.join :as lib.join] [metabase.lib.js.metadata :as js.metadata] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.normalize :as lib.normalize] [metabase.lib.order-by :as lib.order-by] [metabase.lib.query :as lib.query] [metabase.lib.schema.util :as lib.schema.util] [metabase.lib.stage :as lib.stage] [metabase.lib.types.isa :as lib.types.isa] [metabase.lib.util :as lib.util] [metabase.util :as u] [metabase.util.log :as log] [metabase.util.memoize :as memoize] [metabase.util.time :as u.time])) |
This ensures that all of metabase.lib.* is loaded, so all the | (comment lib.core/keep-me) |
(defn- remove-undefined-properties [obj] (cond-> obj (object? obj) (gobject/filter (fn [e _ _] (not (undefined? e)))))) | |
(defn- convert-js-template-tags [tags] (-> tags (gobject/map (fn [e _ _] (remove-undefined-properties e))) js->clj (update-vals #(-> % (update-keys keyword) (update :type keyword))))) | |
Extract the template tags from a native query's text.
If the optional map of existing tags previously parsed is given, this will reuse the existing tags where they match up with the new one (in particular, it will preserve the UUIDs). Given the text of a native query, extract a possibly-empty set of template tag strings from it. These look like mustache templates. For variables, we only allow alphanumeric characters, eg. Invalid patterns are simply ignored, so something like Returns | (defn ^:export extract-template-tags ([query-text] (extract-template-tags query-text {})) ([query-text existing-tags] (->> (convert-js-template-tags existing-tags) (lib.core/extract-template-tags query-text) clj->js))) |
Return a nice description of a query.
| (defn ^:export suggestedName [query] (lib.core/suggested-name query)) |
Convert the provided metadata container to an MLv2 metadata provider.
If the | (defn ^:export metadataProvider [database-id metadata] (if (lib.metadata.protocols/metadata-provider? metadata) metadata (js.metadata/metadata-provider database-id metadata))) |
Creates an MLv2 query from the provided input: either a table or card metadata, or a legacy MLv1 query in JSON form.
There are two arities for this function: With two arguments With three arguments
It would be simpler to attach the MLv2 query to a (non-enumerable) property on the If the metadata gets updated, the | (defn ^:export query ([metadata-provider table-or-card-metadata] (lib.core/query metadata-provider table-or-card-metadata)) ([database-id metadata-provider query-map] ;; Since the query-map is possibly `Object.freeze`'d, we can't mutate it to attach the query. ;; Therefore, we attach a two-level cache to the metadata-provider: ;; The outer key is the database-id; the inner one i a weak ref to the legacy query-map (a JS object). ;; This should achieve efficient caching of legacy queries without retaining garbage. ;; (Except possibly for a few empty WeakMaps, if queries are cached and then GC'd.) ;; If the metadata changes, the metadata-provider is replaced, so all these caches are destroyed. (lib.cache/side-channel-cache-weak-refs (str database-id) metadata-provider query-map #(->> % lib.convert/js-legacy-query->pMBQL (lib.core/query metadata-provider)) {:force? true}))) |
Converts namespaced keywords to strings like [[clj->js]] supports overriding how keyword map keys get transformed, but it doesn't let you override how values are handled. So this function runs first to recursively transform keywords in value position into strings. As examples of such a value, TODO: Lots of utilities and helpers in this file. It would be easier to consume the API if the helpers were moved to
a utility namespace. Better would be to "upstream" them into | (defn- fix-namespaced-values [x] (cond (qualified-keyword? x) (str (namespace x) "/" (name x)) (map? x) (update-vals x fix-namespaced-values) (sequential? x) (map fix-namespaced-values x) :else x)) |
Coerce an MLv2 query (pMBQL in CLJS data structures) into a legacy MLv1 query in vanilla JSON form.
| (defn ^:export legacy-query [query-map] (-> (lib.query/->legacy-MBQL query-map) fix-namespaced-values (clj->js :keyword-fn u/qualified-name))) |
Adds a new, blank stage to the provided
| (defn ^:export append-stage [a-query] (lib.core/append-stage a-query)) |
Drops the final stage in the query, even if it's not empty. If there is only one stage, this is a no-op.
| (defn ^:export drop-stage [a-query] (lib.core/drop-stage a-query)) |
Drops all stages which are empty from No-op if there are no empty stages. Note that the first stage is never empty, since it contains eg.
| (defn ^:export drop-empty-stages [a-query] (lib.core/drop-empty-stages a-query)) |
When a query has aggregations in stage Given
| (defn ^:export as-returned [a-query stage-number card-id] (let [{a-query :query, :keys [stage-number]} (lib.core/wrap-native-query-with-mbql a-query stage-number card-id)] (if (and (empty? (lib.core/aggregations a-query stage-number)) (empty? (lib.core/breakouts a-query stage-number))) ;; No extra stage needed with no aggregations. #js {:query a-query :stageIndex stage-number} ;; An extra stage is needed, so see if one already exists. (if-let [next-stage (->> (lib.util/canonical-stage-index a-query stage-number) (lib.util/next-stage-number a-query))] ;; Already an extra stage, so use it. #js {:query a-query :stageIndex next-stage} ;; No new stage, so append one. #js {:query (lib.core/append-stage a-query) :stageIndex -1})))) |
Returns a JS Array of column metadata values for all columns which can be used to add an To add an Cached on
| (defn ^:export orderable-columns [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "orderable-columns" (str "stage-" stage-number)) a-query (fn [_] (to-array (lib.order-by/orderable-columns a-query stage-number))))) |
Display InfoThe FE routinely needs to know some information about opaque CLJS values in order to render the UI. To get that
information, it calls [[display-info]], providing the query and stage for context along with the value it wants to
know about: Life of a `display-info` call
These implementations return their info in CLJS form, as a map! That's because Conversion to JSON happens only at the last moment, in [[display-info]] in this namespace. Caching in detail
The outer surface is [[display-info]] in this file. It has a [[lib.cache/side-channel-cache]], so if
[[display-info*]] is the inner implementation. It calls [[lib.core/display-info]] to get the CLJS form, then [[display-info->js]] to convert it to JS. JS conversion in the tricky cases (maps and seqs) are handled by separate, LRU-cached functions
[[display-info-map->js]] and [[display-info-seq->js]]. Keywords are converted with [[u/qualified-name]], so they
retain their namespaces, eg. [[display-info-map->js]] converts CLJS maps to JS objects. Keys are converted from | |
[[display-info-seq->js]] converts CLJS | |
Subtlety: identity vs. value cachingIt's possible for | |
The [[lib.cache/side-channel-cache]] caches attached to individual column instances are implicitly per-query (since
| |
In contrast, the CLJS -> JS conversion step doesn't know about queries, so it can use | |
(declare ^:private display-info->js) | |
Converts idiomatic Clojure keys ( Namespaces are preserved. A | (defn- cljs-key->js-key [cljs-key] (let [key-str (u/qualified-name cljs-key) key-str (if (str/ends-with? key-str "?") (str "is-" (str/replace key-str #"\?$" "")) key-str)] (u/->camelCaseEn key-str))) |
Converts idiomatic JavaScript keys ( A | (defn- js-key->cljs-key [js-key] (let [key-str (if (str/starts-with? js-key "is") (str (subs js-key 2) "?") js-key)] (-> key-str u/->kebab-case-en keyword))) |
Converts a JavaScript object with | (defn- js-obj->cljs-map [an-object] (-> an-object js->clj (update-keys js-key->cljs-key))) |
Converts a Clojure map with | (defn- cljs-map->js-obj [a-map] (-> a-map (update-keys cljs-key->js-key) clj->js)) |
(defn- display-info-map->js* [x] (reduce (fn [obj [cljs-key cljs-val]] (let [js-key (cljs-key->js-key cljs-key) js-val (display-info->js cljs-val)] ;; Recursing through the cache (gobject/set obj js-key js-val) obj)) #js {} x)) | |
(def ^:private display-info-map->js (memoize/lru display-info-map->js* :lru/threshold 256)) | |
(defn- display-info-seq->js* [x] (to-array (map display-info->js x))) | |
(def ^:private display-info-seq->js (memoize/lru display-info-seq->js* :lru/threshold 256)) | |
Converts CLJS [[lib.core/display-info]] results into JS objects for the FE to consume. Recursively converts CLJS maps and sequences into JS objects and arrays. | (defn- display-info->js [x] (cond ;; `(seqable? nil) ; => true`, so we need to check for it before (nil? x) nil ;; Note that map? is only true for CLJS maps, not JS objects. (map? x) (display-info-map->js x) (string? x) x ;; Likewise, JS arrays are not seqable? while CLJS vectors, seqs and sets are. ;; (So are maps and strings, but those are already handled above.) (seqable? x) (display-info-seq->js x) (keyword? x) (u/qualified-name x) :else x)) |
Inner implementation of [[display-info]], which caches this function's results. See there for documentation. | (defn- display-info* [a-query stage-number x] (-> a-query (lib.stage/ensure-previous-stages-have-metadata stage-number) (lib.core/display-info stage-number x) display-info->js)) |
Given an opaque CLJS value (in the context of The info returned depends on what kind of value The JS objects returned by this function have all keys spelled as
Caches the result on | (defn ^:export display-info [a-query stage-number x] ;; Attaches a cached display-info blob to `x`, in case it gets called again for the same object. ;; TODO: Keying by stage is probably unnecessary - if we eg. fetched a column from different stages, it would be a ;; different object. Test that idea and remove the stage from the cache key. (lib.cache/side-channel-cache (keyword "display-info-outer" (str "stage-" stage-number)) x #(display-info* a-query stage-number %))) |
Create an
| (defn ^:export order-by-clause ([orderable] (order-by-clause orderable :asc)) ([orderable direction] (lib.core/order-by-clause (lib.core/normalize (js->clj orderable :keywordize-keys true)) (keyword direction)))) |
Add an
| (defn ^:export order-by [a-query stage-number orderable direction] (lib.core/order-by a-query stage-number orderable (keyword direction))) |
Get the Returns an empty array if there are no order-bys in the given stage.
| (defn ^:export order-bys [a-query stage-number] (to-array (lib.core/order-bys a-query stage-number))) |
Flip the direction of
| (defn ^:export change-direction [a-query current-order-by] (lib.core/change-direction a-query current-order-by)) |
Returns a JS array of opaque columns representing the columns that can be used as breakouts in the given stage of
Pass one of these values to [[breakout]] to add it to the query.
Caches the result on the query by stage. | (defn ^:export breakoutable-columns [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "breakoutable-columns" (str "stage-" stage-number)) a-query (fn [_] (to-array (lib.core/breakoutable-columns a-query stage-number))))) |
Get the list of breakout clauses in Returns an empty array if there are no breakouts in the query.
| (defn ^:export breakouts [a-query stage-number] (to-array (lib.core/breakouts a-query stage-number))) |
Add a
| (defn ^:export breakout [a-query stage-number breakoutable] (lib.core/breakout a-query stage-number (lib.core/ref breakoutable))) |
Given a That column will include any temporal bucketing or binning settings on the breakout.
| (defn ^:export breakout-column [a-query stage-number breakout-clause] (lib.core/breakout-column a-query stage-number breakout-clause)) |
Binning and BucketingMetabase supports a few styles of "rounding" in breakouts, to group rows of raw data into meaningful units. The two fundamental kinds of rounding are binning and temporal bucketing. BinningBinning groups a column's values in one of two ways, by either controlling number of bins, or the width of each bin. Fixed number of binsAny numeric column can be grouped into a fixed number of bins (say, 10), by dividing the range of the column's
values into | |
Aside: Fixed-width bins are different from quantiles! Quantiles would be slicing the other way: put an equal number of rows into each bin, and let the endpoints between the bins vary. Fixed width binsWhen we understand the units in which the column is defined, we can give each bin a fixed width, and return as many bins as necessary to hold all the rows. This is currently supported only for latitude and longitude columns. | |
Retrieves the binning settings for
| (defn ^:export binning [a-column-or-clause] (lib.core/binning a-column-or-clause)) |
Given If
| (defn ^:export with-binning [a-column-or-clause binning-option] (lib.core/with-binning a-column-or-clause binning-option)) |
Returns a JS array of available binning strategies for The list contains opaque values, which can be passed to [[display-info]] for rendering, or [[with-binning]] to
attach them to
| (defn ^:export available-binning-strategies ([a-query x] (-> (lib.core/available-binning-strategies a-query x) to-array)) ([a-query stage-number x] (-> (lib.core/available-binning-strategies a-query stage-number x) to-array))) |
Temporal Bucketing | |
The other way to "round" a column's value is by units of time. This is a very common use case: looking at monthly total sales, etc. | |
One subtlety is that some units are cyclic and others are truncated. For example, | |
For the purposes of the library, both styles are treated the same way: the unit is specified by name and passed on to visualizations and to the query processor, which are responsible for interpreting the meaning of the unit. | |
Get the current temporal bucketing setting of
| (defn ^:export temporal-bucket [a-clause-or-column] (lib.core/temporal-bucket a-clause-or-column)) |
Add the specified If
| (defn ^:export with-temporal-bucket [a-clause-or-column bucketing-option] (lib.core/with-temporal-bucket a-clause-or-column bucketing-option)) |
Get a list of available temporal bucketing options for Returns a JS array of opaque values, which can be passed to [[display-info]] for rendering and [[with-temporal-bucket]] to set the bucketing on a clause or column.
| (defn ^:export available-temporal-buckets ([a-query x] (-> (lib.core/available-temporal-buckets a-query x) to-array)) ([a-query stage-number x] (-> (lib.core/available-temporal-buckets a-query stage-number x) to-array))) |
The temporal bucketing units for date type expressions. | (defn ^:export available-temporal-units [] (to-array (map clj->js (lib.core/available-temporal-units)))) |
Manipulating ClausesThese three functions work on any kind of clause - aggregations, filters, breakouts, custom expressions, order-by. They are also intended to be smart, and leave the query in a good state. For example, removing a custom expression will also remove anything that depended on it, recursively. Moving or replacing a clause will update any references to it in other places (eg. an aggregation based on a custom expression that was just renamed). | |
Removes the Use this to remove any clause (aggregations, breakouts, order by, filters, custom expressions, joins) from a query. The deletion cascades, recursively removing any other clauses that depended on the removed clause, such as a filter based on a custom expression. Does nothing if the clause can't be found.
| (defn ^:export remove-clause [a-query stage-number clause] (lib.core/remove-clause a-query stage-number (lib.core/normalize (js->clj clause :keywordize-keys true)))) |
Replaces the Does nothing if the
| (defn ^:export replace-clause [a-query stage-number target-clause new-clause] (lib.core/replace-clause a-query stage-number (lib.core/normalize (js->clj target-clause :keywordize-keys true)) (lib.core/normalize (js->clj new-clause :keywordize-keys true)))) |
Exchanges the positions of two clauses of the same kind. Can be used for filters, aggregations, breakouts, and expressions. Returns the updated query. If it can't find both clauses in a single list, emits a warning and returns the query unchanged.
| (defn ^:export swap-clauses [a-query stage-number source-clause target-clause] (lib.core/swap-clauses a-query stage-number (lib.core/normalize (js->clj source-clause :keywordize-keys true)) (lib.core/normalize (js->clj target-clause :keywordize-keys true)))) |
(defn- unwrap [a-query] (let [a-query (mbql.js/unwrap a-query)] (cond-> a-query (map? a-query) (:dataset_query a-query)))) | |
(defn- normalize-to-clj [a-query] (let [normalize-fn (fn [q] (if (= (lib.util/normalized-query-type q) :mbql/query) (lib.normalize/normalize q) (mbql.normalize/normalize q)))] (-> a-query (js->clj :keywordize-keys true) unwrap normalize-fn))) | |
Normalize the MBQL or pMBQL query Returns the JS form of the normalized query. | (defn ^:export normalize [a-query] (-> a-query normalize-to-clj (clj->js :keyword-fn u/qualified-name))) |
Comparing queriesThere are a few places in the FE where we need to compare two queries, typically to check whether the current question has been changed and needs to be saved. | |
This currently only works for legacy queries in JSON form. At some point MLv2 queries will become the source of truth, and the format used on the wire. At that point, we'll want a similar comparison for MLv2 queries. | |
TODO: These equality checks only seem to clean and check the last stages - does that really suffice? | |
(defn- prep-query-for-equals-legacy [a-query field-ids] (-> a-query ;; If `:native` exists, but it doesn't have `:template-tags`, add it. (m/update-existing :native #(merge {:template-tags {}} %)) (m/update-existing :query (fn [inner-query] (let [fields (or (:fields inner-query) (for [id field-ids] [:field id nil]))] (-> inner-query ;; We ignore the order of the fields in the lists, but need to make sure any ;; dupes match up. Therefore de-dupe with `frequencies` rather than `set`. (assoc :fields (frequencies fields)) ;; Remove the randomized idents, which are of course not going to match. (dissoc :aggregation-idents :breakout-idents :expression-idents))))))) | |
(defn- prep-query-for-equals-pMBQL [a-query field-ids] (let [fields (or (some->> (lib.core/fields a-query) (map #(assoc % 1 {}))) (mapv (fn [id] [:field {} id]) field-ids))] (lib.util/update-query-stage a-query -1 #(-> % (assoc :fields (frequencies fields)) lib.schema.util/remove-randomized-idents)))) | |
(defn- prep-query-for-equals [a-query field-ids] (when-let [normalized-query (some-> a-query normalize-to-clj)] (if (contains? normalized-query :lib/type) (prep-query-for-equals-pMBQL normalized-query field-ids) (prep-query-for-equals-legacy normalized-query field-ids)))) | |
(defn- compare-field-refs [[key1 id1 opts1] [key2 id2 opts2]] ;; A mismatch of `:base-type` or `:effective-type` when both x and y have values for it is a failure. ;; If either ref does not have the `:base-type` or `:effective-type` set, that key is ignored. (letfn [(clean-opts [o1 o2] (not-empty (cond-> o1 (not (:base-type o2)) (dissoc :base-type) (not (:effective-type o2)) (dissoc :effective-type))))] (if (map? id1) (= [key1 (clean-opts id1 id2) opts1] [key2 (clean-opts id2 id1) opts2]) (= [key1 id1 (clean-opts opts1 opts2)] [key2 id2 (clean-opts opts2 opts1)])))) | |
(defn- query=* [x y] (cond (and (vector? x) (vector? y) (= (first x) (first y) :field)) (compare-field-refs x y) ;; Otherwise this is a duplicate of clojure.core/= except :lib/uuid and :ident values don't have to match. (and (map? x) (map? y)) (let [x (dissoc x :lib/uuid :ident) y (dissoc y :lib/uuid :ident)] (and (= (set (keys x)) (set (keys y))) (every? (fn [[k v]] (query=* v (get y k))) x))) (and (sequential? x) (sequential? y)) (and (= (count x) (count y)) (every? true? (map query=* x y))) ;; Either mismatched map/sequence/nil/etc., or primitives like strings. ;; Either way, = does the right thing. :else (= x y))) | |
Returns whether the provided queries should be considered equal. If Currently this works only for legacy queries in JS form!
It duplicates the logic formerly found in
| (defn ^:export query= ([query1 query2] (query= query1 query2 nil)) ([query1 query2 field-ids] (let [ids (mapv js->clj field-ids) n1 (prep-query-for-equals query1 ids) n2 (prep-query-for-equals query2 ids)] (query=* n1 n2)))) |
Column GroupsIn many places in the FE we show a list of columns which might be used to filter, aggregate, etc. These are shown in expandable groups by source: source table/previous stage first, then explicitly joined tables, then implicitly joinable by different FKs. | |
Given the list of columns returned by a function like [[orderable-columns]], groups those columns by source, in the appropriate shape for rendering in the Query Builder. Source is any of:
For example, given a sequence of columns like this:
the groups would be:
Groups have the type Use [[columns-group-columns]] to extract the columns from a group.
| (defn ^:export group-columns [column-metadatas] (to-array (lib.core/group-columns column-metadatas))) |
Return the columns in this
| (defn ^:export columns-group-columns [column-group] (to-array (lib.core/columns-group-columns column-group))) |
Temporal unit descriptionsThese return localized strings describing a temporal unit, interval, or relative date range. There's complex logic here, and it can be shared with BE for static viz, CSV downloads, etc. | |
Get a translated description of a temporal bucketing unit.
| (defn ^:export describe-temporal-unit [n unit] (let [unit (if (string? unit) (keyword unit) unit)] (lib.core/describe-temporal-unit n unit))) |
Get a translated description of a temporal bucketing interval.
| (defn ^:export describe-temporal-interval [n unit] (let [n (if (string? n) (keyword n) n) unit (if (string? unit) (keyword unit) unit)] (lib.core/describe-temporal-interval n unit))) |
Get a translated description of a relative datetime interval.
| (defn ^:export describe-relative-datetime [n unit] (let [n (if (string? n) (keyword n) n) unit (if (string? unit) (keyword unit) unit)] (lib.core/describe-relative-datetime n unit))) |
Aggregations | |
Adds an aggregation to Construct
| (defn ^:export aggregate [a-query stage-number an-aggregate-clause] (lib.core/aggregate a-query stage-number (js->clj an-aggregate-clause :keywordize-keys true))) |
Return a JS array of aggregations on a given stage of
| (defn ^:export aggregations [a-query stage-number] (to-array (lib.core/aggregations a-query stage-number))) |
Given an Returns
| (defn ^:export aggregation-column [a-query stage-number aggregation-clause] (lib.core/aggregation-column a-query stage-number aggregation-clause)) |
Returns a standalone aggregation clause for an For aggregations requiring an argument, Get a list of valid aggregation operators with [[available-aggregation-operators]].
| (defn ^:export aggregation-clause [aggregation-operator column] (if (undefined? column) (lib.core/aggregation-clause aggregation-operator) (lib.core/aggregation-clause aggregation-operator column))) |
Get the available aggregation operators for the stage with These are opaque values that can be passed to [[display-info]], or to [[aggregation-clause]] to construct an aggregation.
| (defn ^:export available-aggregation-operators [a-query stage-number] (to-array (lib.core/available-aggregation-operators a-query stage-number))) |
Return a JS array of columns which The columns are valid for the stage of the query that was used in
[[available-aggregation-operators]] to get | (defn ^:export aggregation-operator-columns [aggregation-operator] (to-array (lib.core/aggregation-operator-columns aggregation-operator))) |
Used when editing an aggregation. We need to show the list of possible aggregation operators with the selected one highlighted, and if it has a column, also the list of applicable columns with the selected one highlighted. Given a list of If that operator needs a column, also searches the columns and marks the column from Returns the same list of
| (defn ^:export selected-aggregation-operators [agg-operators agg-clause] (to-array (lib.core/selected-aggregation-operators (seq agg-operators) agg-clause))) |
FilteringFilters work in a similar way to aggregations and order-by, but are more complex since they can have several parameters, which can be columns, several types of literal value, etc. The basic flow is: [[filterable-columns]] returns the list of columns which can be used for filtering, which include the applicable filter operators. Call [[filter-clause]] with the operator, column and any more arguments, and pass that clause to [[filter]]. | |
Returns a JS array of columns available for filtering The columns have extra information attached, giving the filter operators that can be used with that column. Cached on the query.
| (defn ^:export filterable-columns [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "filterable-columns" (str "stage-" stage-number)) a-query (fn [_] (to-array (lib.core/filterable-columns a-query stage-number))))) |
Returns the filter operators which can be used in a filter for
| (defn ^:export filterable-column-operators [filterable-column] (to-array (lib.core/filterable-column-operators filterable-column))) |
Given a
| (defn ^:export filter-clause [filter-operator column & args] (apply lib.core/filter-clause filter-operator column args)) |
Returns the filter operator used in
| (defn ^:export filter-operator [a-query stage-number a-filter-clause] (lib.core/filter-operator a-query stage-number a-filter-clause)) |
Adds | (defn ^:export filter [a-query stage-number a-filter-clause] (lib.core/filter a-query stage-number (js->clj a-filter-clause :keywordize-keys true))) |
Returns a JS array of all the filters on stage Logically, the If there are no filters on this query, returns an empty list.
| (defn ^:export filters [a-query stage-number] (to-array (lib.core/filters a-query stage-number))) |
TODO: find-filter-for-legacy-filter is dead code and should be removed. | |
TODO: find-filterable-column-for-legacy-ref is dead code and should be removed. | |
ExpressionsCustom expressions are parsed from a string by a TS library, which returns legacy MBQL clauses. That may get ported to Clojure someday, but perhaps not - it's quite standalone and there's no use case for that logic in the BE. | |
MLv2 expression clauses are constructed with [[expression-clause]] from an operator and list of args, typically
coming from that parser. An expression clause can be attached to a query with | |
When rendering expressions, the FE calls [[expression-parts]], which returns a kind of AST for the expression. This form is deliberately different from the MBQL representation. | |
Returns a standalone expression clause for the given | (defn ^:export expression-clause [an-operator args options] (-> (lib.core/expression-clause (keyword an-operator) args (js->clj options :keywordize-keys true)) lib.core/normalize)) |
Returns an AST for Each clause is transformed to a JS object like:
Note that the
| (defn ^:export expression-parts [a-query stage-number an-expression-clause] (let [parts (lib.core/expression-parts a-query stage-number an-expression-clause)] (walk/postwalk (fn [node] (if (and (map? node) (= :mbql/expression-parts (:lib/type node))) (let [{:keys [operator options args]} node] #js {:operator (name operator) :options (clj->js (select-keys options [:case-sensitive :include-current])) :args (to-array (map #(if (keyword? %) (u/qualified-name %) %) args))}) node)) parts))) |
Creates a string filter clause based on FE-friendly filter parts. It should be possible to destructure each created
expression with [[string-filter-parts]]. To avoid mistakes the function requires | (defn ^:export string-filter-clause [operator column values options] (lib.core/string-filter-clause (keyword operator) column (js->clj values) (js-obj->cljs-map options))) |
Destructures a string filter clause created by [[string-filter-clause]]. Returns | (defn ^:export string-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/string-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [operator column values options]} filter-parts] #js {:operator (name operator) :column column :values (to-array (map clj->js values)) :options (cljs-map->js-obj options)}))) |
Creates a numeric filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[number-filter-parts]]. | (defn ^:export number-filter-clause [operator column values] (lib.core/number-filter-clause (keyword operator) column (js->clj values))) |
Destructures a numeric filter clause created by [[number-filter-clause]]. Returns | (defn ^:export number-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/number-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [operator column values]} filter-parts] #js {:operator (name operator) :column column :values (to-array (map clj->js values))}))) |
Creates a coordinate filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[coordinate-filter-parts]]. | (defn ^:export coordinate-filter-clause [operator column longitude-column values] (lib.core/coordinate-filter-clause (keyword operator) column longitude-column (js->clj values))) |
Destructures a coordinate filter clause created by [[coordinate-filter-clause]]. Returns | (defn ^:export coordinate-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/coordinate-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [operator column longitude-column values]} filter-parts] #js {:operator (name operator) :column column :longitudeColumn longitude-column :values (to-array (map clj->js values))}))) |
Creates a boolean filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[boolean-filter-parts]]. | (defn ^:export boolean-filter-clause [operator column values] (lib.core/boolean-filter-clause (keyword operator) column (js->clj values))) |
Destructures a boolean filter clause created by [[boolean-filter-clause]]. Returns | (defn ^:export boolean-filter-parts [a-query stage-boolean a-filter-clause] (when-let [filter-parts (lib.core/boolean-filter-parts a-query stage-boolean a-filter-clause)] (let [{:keys [operator column values]} filter-parts] #js {:operator (name operator) :column column :values (to-array (map clj->js values))}))) |
Creates a specific date filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[specific-date-filter-parts]]. | (defn ^:export specific-date-filter-clause [operator column values with-time?] (lib.core/specific-date-filter-clause (keyword operator) column (js->clj values) with-time?)) |
Destructures a specific date filter clause created by [[specific-date-filter-clause]]. Returns | (defn ^:export specific-date-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/specific-date-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [operator column values with-time?]} filter-parts] #js {:operator (name operator) :column column :values (to-array (map clj->js values)) :hasTime with-time?}))) |
Creates a relative date filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[relative-date-filter-parts]]. | (defn ^:export relative-date-filter-clause [column value unit offset-value offset-unit options] (lib.core/relative-date-filter-clause column (if (string? value) (keyword value) value) (keyword unit) offset-value (some-> offset-unit keyword) (js-obj->cljs-map options))) |
Destructures a relative date filter clause created by [[relative-date-filter-clause]]. Returns | (defn ^:export relative-date-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/relative-date-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [column value unit offset-value offset-unit options]} filter-parts] #js {:column column :value (if (keyword? value) (name value) value) :unit (name unit) :offsetValue offset-value :offsetUnit (some-> offset-unit name) :options (cljs-map->js-obj options)}))) |
Creates an exclude date filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[exclude-date-filter-parts]]. | (defn ^:export exclude-date-filter-clause [operator column unit values] (lib.core/exclude-date-filter-clause (keyword operator) column (some-> unit keyword) (js->clj values))) |
Destructures an exclude date filter clause created by [[exclude-date-filter-clause]]. Returns | (defn ^:export exclude-date-filter-parts [a-query stage-number a-filter-clause] (when-let [filter-parts (lib.core/exclude-date-filter-parts a-query stage-number a-filter-clause)] (let [{:keys [operator column unit values]} filter-parts] #js {:operator (name operator) :column column :unit (some-> unit name) :values (to-array (map clj->js values))}))) |
Creates a time filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[time-filter-parts]]. | (defn ^:export time-filter-clause [operator column values] (lib.core/time-filter-clause (keyword operator) column (js->clj values))) |
Destructures a time filter clause created by [[time-filter-clause]]. Returns | (defn ^:export time-filter-parts [a-query stage-boolean a-filter-clause] (when-let [filter-parts (lib.core/time-filter-parts a-query stage-boolean a-filter-clause)] (let [{:keys [operator column values]} filter-parts] #js {:operator (name operator) :column column :values (to-array (map clj->js values))}))) |
Creates a default filter clause based on FE-friendly filter parts. It should be possible to destructure each created expression with [[default-filter-parts]]. This clause works as a fallback for more specialized column types. | (defn ^:export default-filter-clause [operator column] (lib.core/default-filter-clause (keyword operator) column)) |
Destructures a default filter clause created by [[default-filter-clause]]. Returns | (defn ^:export default-filter-parts [a-query stage-boolean a-filter-clause] (when-let [filter-parts (lib.core/default-filter-parts a-query stage-boolean a-filter-clause)] (let [{:keys [operator column]} filter-parts] #js {:operator (name operator) :column column}))) |
Returns true if arg is an MLv2 column, ie. has
TODO remove once all filter-parts are migrated to MBQL lib | (defn ^:export is-column-metadata [arg] (and (map? arg) (= :metadata/column (:lib/type arg)))) |
Field selectionQueries can specify a subset of fields to return from their source table or previous stage. There are several functions provided to inspect and manage that list of fields. | |
Get the list of fields currently set on Returns
| (defn ^:export fields [a-query stage-number] (to-array (lib.core/fields a-query stage-number))) |
Set the fields list for This replaces any existing fields list. If
| (defn ^:export with-fields [a-query stage-number new-fields] (lib.core/with-fields a-query stage-number new-fields)) |
Return a JS array of columns that are valid to set in the fields list of Cached on the query.
| (defn ^:export fieldable-columns [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "fieldable-columns" (str "stage-" stage-number)) a-query (fn [_] (to-array (lib.core/fieldable-columns a-query stage-number))))) |
Adds a given Exactly what this means depends on where the column comes from:
| (defn ^:export add-field [a-query stage-number column] (lib.core/add-field a-query stage-number column)) |
TODO: There's a mismatch here around aggregations and breakouts. They are treated like normal fields in | |
Removes the field (a | (defn ^:export remove-field [a-query stage-number column] (lib.core/remove-field a-query stage-number column)) |
Visible and Returned ColumnsThese two sets of columns are fundamental. | |
Returned ColumnsThis is the set of columns that will go into the table viz, or become the previous stage columns for a later stage. Stages with aggregations are handled differently from other stages. | |
With at least one aggregation, the returned columns are exactly the aggregations and breakouts from this stage, and no more. | |
Otherwise, the returned columns come from several sources. The basic source is (a subset of) the columns from the source table/card/model or the previous stage. If the fields list is set, it names a subset of those columns which are included in this stage. With no fields list, all columns from the source are returned. | |
Next, each explicit join also has a fields list and defaults to including all the columns from the joined table. | |
Inner implementation for [[returned-columns]], which wraps this with caching. Finally, custom expressions are always returned. | (defn- returned-columns* [a-query stage-number] (let [stage (lib.util/query-stage a-query stage-number) unique-name-fn (lib.util/unique-name-generator (lib.metadata/->metadata-provider a-query))] (->> (lib.metadata.calculation/returned-columns a-query stage-number stage) (map #(-> % (assoc :selected? true) ;; Unique names are required by the FE for compatibility. ;; This applies only for JS; Clojure usage should prefer `:lib/desired-column-alias` to `:name`, and ;; that's already unique by construction. (update :name unique-name-fn))) to-array))) |
Return a JS array of columns which are returned from this stage of
| (defn ^:export returned-columns [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "returned-columns" (str "stage-" stage-number)) a-query (fn [_] (returned-columns* a-query stage-number)))) |
Visible Columns | |
Visible columns are all the columns which are in scope at the given stage of the query. | |
The most immediate source of columns for a first stage is the source table (or saved question, or model); for later stages it's the previous stage's returned columns. Then any custom expressions on this stage are visible, as are all the columns from any explicitly joined tables. | |
Finally, any foreign key in that set of columns can be implicitly joined, which brings in all the column from another table. Note that implicitly joinable FKs are not recursively implicitly joinable! | |
Aggregations and breakouts are not part of the visible columns, since the visible columns are what's available on the query to be aggregated or used as a breakout. | |
Inner implementation for [[visible-columns]], which wraps this with caching. What about the other sets of columns?This interface contains several other sets of columns, like [[filterable-columns]] and [[expressionable-columns]]; these are subsets of [[visible-columns]] possibly with extra information added, such as the set of filter operators which can be used with that column. Note: At the time of writing, | (defn- visible-columns* [a-query stage-number] (let [stage (lib.util/query-stage a-query stage-number) vis-columns (lib.metadata.calculation/visible-columns a-query stage-number stage) ret-columns (lib.metadata.calculation/returned-columns a-query stage-number stage)] (->> (for [col vis-columns :let [match (lib.equality/find-matching-column a-query stage-number col ret-columns)]] (assoc col :selected? (some? match))) to-array))) |
Returns a JS array of all columns "visible" at the given stage of Does not pass any options to [[lib.core/visible-columns]], so it uses the defaults (which are to include everything). One important difference from the Clojure-facing [[lib.core/visible-columns]]: this marks all the columns which are
returned from the query as
| (defn ^:export visible-columns ;; TODO: This may become unnecessary as legacy usages are ported. [a-query stage-number] ;; Attaches the cached columns directly to this query, in case it gets called again. (lib.cache/side-channel-cache (keyword "visible-columns" (str "stage-" stage-number)) a-query (fn [_] (visible-columns* a-query stage-number)))) |
Given a column, as returned by [[visible-columns]], [[returned-columns]] etc., return a string suitable for uniquely identifying the column on its query. This key will generally not be changed by unrelated edits to the query. (Currently this is powered by Column keys | (defn ^:export column-key [a-column] (or (:lib/desired-column-alias a-column) (:name a-column))) |
Legacy refs | (defn- normalize-legacy-ref [a-ref] (if (#{:aggregation :metric :segment} (first a-ref)) (subvec a-ref 0 2) (update a-ref 2 update-vals #(if (qualified-keyword? %) (u/qualified-name %) %)))) |
Given a column, metric or segment metadata from eg. [[fieldable-columns]] or [[available-segments]], return it as a legacy JSON field ref. For compatibility reasons, segment and metric references are always returned without options.
| (defn ^:export legacy-ref [a-query stage-number column] (lib.convert/with-aggregation-list (:aggregation (lib.util/query-stage a-query stage-number)) (-> column lib.core/ref lib.convert/->legacy-MBQL normalize-legacy-ref clj->js))) |
(defn- legacy-ref->pMBQL [a-legacy-ref] (-> a-legacy-ref (js->clj :keywordize-keys true) (update 0 keyword) (->> (mbql.normalize/normalize-fragment nil)) lib.convert/->pMBQL)) | |
(defn- ->column-or-ref [column] (if-let [^js legacy-column (when (object? column) column)] ;; Convert legacy columns like we do for metadata. (let [parsed (js.metadata/parse-column legacy-column)] (if (= (:lib/source parsed) :source/aggregations) ;; Special case: Aggregations need to be converted to a pMBQL :aggregation ref and :lib/source-uuid set. (let [agg-ref (legacy-ref->pMBQL (.-field_ref legacy-column))] (assoc parsed :lib/source-uuid (last agg-ref))) parsed)) ;; It's already a :metadata/column map column)) | |
Given a list of columns (either JS Returns a parallel list to the refs, with the corresponding index, or -1 if no matching column is found.
| (defn ^:export find-column-indexes-from-legacy-refs [a-query stage-number legacy-columns legacy-refs] ;; Set up this query stage's `:aggregation` list as the context for [[lib.convert/->pMBQL]] to convert legacy ;; `[:aggregation 0]` refs into pMBQL `[:aggregation uuid]` refs. (lib.convert/with-aggregation-list (:aggregation (lib.util/query-stage a-query stage-number)) (let [haystack (mapv ->column-or-ref legacy-columns) needles (map legacy-ref->pMBQL legacy-refs) column-refs (into {} (keep-indexed (fn [i col] [(-> col lib.core/ref lib.convert/->legacy-MBQL normalize-legacy-ref) i])) legacy-columns) exact-matches (map #(-> % (js->clj :keywordize-keys true) (update 0 keyword) column-refs) legacy-refs)] (if (every? #(and % (>= % 0)) exact-matches) (to-array exact-matches) #_{:clj-kondo/ignore [:discouraged-var]} (to-array (lib.equality/find-column-indexes-for-refs a-query stage-number needles haystack)))))) |
Returns the ID of the source table (as a number) or the ID of the source card (as a string prefixed
with "card__") of
| (defn ^:export source-table-or-card-id ;; TODO: Figure out what the callers of this function really need it for, and consider an alternative design. ;; [[with-different-table]] should be included in that refactor. [a-query] (or (lib.util/source-table-id a-query) (some->> (lib.util/source-card-id a-query) (str "card__")))) |
JoinsJoins are a relatively complex component of a query. They specify a table (or model, in theory), 1 or more conditions
(which resemble filters), an optional subset of fields to include from the joined table, and one of a handful of join
strategies ( These user-visible joins are referred to as explicit joins, to differentiate them from implicit joins, which simply name a foreign column and the foreign key on this query which points to its table. The query processor will collect these and unify them with the explicit joins to keep the size of the query down. | |
Get the strategy (
| (defn ^:export join-strategy [a-join] (lib.core/join-strategy a-join)) |
Returns
| (defn ^:export with-join-strategy [a-join strategy] (lib.core/with-join-strategy a-join strategy)) |
Returns a JS array of available join strategies for the current Database (based on the Database's supported [[metabase.driver/features]]), as opaque values suitable for passing to [[with-join-strategy]].
| (defn ^:export available-join-strategies [a-query stage-number] (to-array (lib.core/available-join-strategies a-query stage-number))) |
Returns a JS array of columns which are valid as the left-hand-side in a join condition. By "left-hand-side" is meant the source column, the one already present in the query. These columns come from the source table/card/model, a previous stage, or a previous join. When editing an existing join, When creating a new join, If you are changing the LHS of a condition for an existing join, pass in that existing join as When building a new join, either pass in If the left-hand-side column has already been chosen and we're UPDATING it, pass in If the right-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI),
pass it as Results will be returned in a 'somewhat smart' order, with PKs and FKs returned before other columns. Unlike most other things that return columns, implicitly joinable columns are not returned here.
| (defn ^:export join-condition-lhs-columns [a-query stage-number join-or-joinable lhs-column-or-nil rhs-column-or-nil] (to-array (lib.core/join-condition-lhs-columns a-query stage-number join-or-joinable lhs-column-or-nil rhs-column-or-nil))) |
Returns a JS array of columns which are valid as the right-hand side of a join condition. By "right-hand side" is meant the target column, the column on the table being joined into the query.
If the left-hand-side column has already been chosen (they can be chosen in any order in the Query Builder UI),
pass it as If we're editing an existing join condition with the RHS column already chosen, pass it as Results will be returned in a 'somewhat smart' order with PKs and FKs returned before other columns.
| (defn ^:export join-condition-rhs-columns [a-query stage-number join-or-joinable lhs-column-or-nil rhs-column-or-nil] (to-array (lib.core/join-condition-rhs-columns a-query stage-number join-or-joinable lhs-column-or-nil rhs-column-or-nil))) |
Returns a JS array of valid filter clause operators that can be used to build a join condition. In the Query Builder UI, this can be chosen at any point before or after choosing the LHS and RHS columns. Invalid operators are not currently filtered out based on values of the LHS or RHS, but in the future we can add this. See #31174.
| (defn ^:export join-condition-operators [a-query stage-number lhs-column-or-nil rhs-column-or-nil] (to-array (lib.core/join-condition-operators a-query stage-number lhs-column-or-nil rhs-column-or-nil))) |
TODO: Move the join and expressions functions to be contiguous instead of interleaved. | |
Adds a
| (defn ^:export expression [a-query stage-number expression-name an-expression-clause] (lib.core/expression a-query stage-number expression-name an-expression-clause)) |
Return a new expression clause like
| (defn ^:export with-expression-name [an-expression-clause new-name] ;; For normal expressions on a query stage, this sets the `:lib/expression-name` option. ;; For custom aggregation expressions this sets the `:display-name` option instead. (lib.core/with-expression-name an-expression-clause new-name)) |
Returns a JS array of expressions on the given stage of | (defn ^:export expressions [a-query stage-number] (to-array (lib.core/expressions a-query stage-number))) |
Returns a JS array of those columns that can be used in an expression in the given stage of Expressions can only see other expressions on the same stage which appear earlier in the list, so you must pass
When creating a new expression, Cached on the query and stage.
| (defn ^:export expressionable-columns [a-query stage-number expression-position] (lib.cache/side-channel-cache ;; Caching is based on both the stage and expression position, since they can return different sets. ;; TODO: Since these caches are mainly here to avoid expensively recomputing things in rapid succession, it would ;; probably suffice to cache only the last position, and evict if it's different. But the lib.cache system doesn't ;; support that currently. (keyword "expressionable-columns" (str "stage-" stage-number "-" expression-position)) a-query (fn [_] (to-array (lib.core/expressionable-columns a-query stage-number expression-position))))) |
Column extractions are a set of transformations possible on a given For example, we might extract the day of the week from a temporal column, or the domain name from an email or URL. Returns a (possibly empty) JS array of possible column extractions for the given column.
| (defn ^:export column-extractions [a-query column] (to-array (lib.core/column-extractions a-query column))) |
Given Generally this means adding a new expression. Returns an updated query.
| (defn ^:export extract [a-query stage-number extraction] (lib.core/extract a-query stage-number extraction)) |
Given
| (defn ^:export extraction-expression [_a-query _stage-number extraction] (lib.core/extraction-expression extraction)) |
Returns a JS array of possible default join conditions when joining against When editing a join, the Returns
| (defn ^:export suggested-join-conditions ([a-query stage-number joinable] (to-array (lib.core/suggested-join-conditions a-query stage-number joinable))) ([a-query stage-number joinable position] (to-array (lib.core/suggested-join-conditions a-query stage-number joinable position)))) |
Get the fields list associated with This is either a JS array of columns, or one of the keywords
| (defn ^:export join-fields [a-join] (let [joined-fields (lib.core/join-fields a-join)] (if (keyword? joined-fields) (u/qualified-name joined-fields) (to-array joined-fields)))) |
Set the This can either be a list of fields, or a string or keyword
| (defn ^:export with-join-fields [a-join new-fields] (lib.core/with-join-fields a-join (cond-> new-fields (string? new-fields) keyword))) |
Create a join clause (an
| (defn ^:export join-clause [joinable conditions strategy] (lib.core/join-clause joinable conditions strategy)) |
Add
| (defn ^:export join [a-query stage-number a-join] (lib.core/join a-query stage-number a-join)) |
Get the conditions associated with
| (defn ^:export join-conditions [a-join] (to-array (lib.core/join-conditions a-join))) |
Set the conditions for
| (defn ^:export with-join-conditions [a-join conditions] (lib.core/with-join-conditions a-join (js->clj conditions :keywordize-keys true))) |
Return a JS array of all joins on the given stage of Returns
| (defn ^:export joins [a-query stage-number] (to-array (lib.core/joins a-query stage-number))) |
Rename the join specified by
If the specified join cannot be found, then If renaming the join to
| (defn ^:export rename-join [a-query stage-number join-spec new-name] (lib.core/rename-join a-query stage-number join-spec new-name)) |
Remove the join specified by
If the specified join cannot be found, then Other clauses which reference the removed join (eg. filters, breakouts or aggregations which reference joined columns) are also removed, and so on recursively.
| (defn ^:export remove-join [a-query stage-number join-spec] (lib.core/remove-join a-query stage-number join-spec)) |
Return metadata about the origin of
| (defn ^:export joined-thing [a-query a-join] (lib.join/joined-thing a-query a-join)) |
Temporary solution providing access to internal IDs for the FE to pass on to MLv1 functions.
| (defn ^:export picker-info [a-query metadata] (case (:lib/type metadata) :metadata/table #js {:databaseId (:database a-query) :tableId (:id metadata)} :metadata/card #js {:databaseId (:database a-query) :tableId (str "card__" (:id metadata)) :cardId (:id metadata) :isModel (= (keyword (:type metadata)) :model)} (do (log/warn "Cannot provide picker-info for" (:lib/type metadata)) nil))) |
Convert an expression or filter
| (defn ^:export external-op [clause] (let [{:keys [operator options args]} (lib.core/external-op clause)] #js {:operator operator :options (clj->js options) :args (to-array args)})) |
Create a new native query. Native in this sense means a pMBQL query where the first stage is
| (defn ^:export native-query [database-id metadata inner-query] (lib.core/native-query (metadataProvider database-id metadata) inner-query)) |
Update the raw native query. The first stage of Replaces templates tags.
| (defn ^:export with-native-query [a-query inner-query] (lib.core/with-native-query a-query inner-query)) |
Updates the native first stage of
| (defn ^:export with-template-tags [a-query tags] (lib.core/with-template-tags a-query (convert-js-template-tags tags))) |
Returns the native query string for the native first stage of
| (defn ^:export raw-native-query [a-query] (lib.core/raw-native-query a-query)) |
Returns the template tags for the native first stage of
| (defn ^:export template-tags [a-query] (clj->js (lib.core/template-tags a-query))) |
Returns a JS array of the extra keys that are required for this database's native queries. For example
| (defn ^:export required-native-extras [database-id metadata] (to-array (map u/qualified-name (lib.core/required-native-extras (metadataProvider database-id metadata))))) |
Returns whether the database targeted by
| (defn ^:export has-write-permission [a-query] (lib.core/has-write-permission a-query)) |
Changes the database for
Returns the updated query.
| (defn ^:export with-different-database ([a-query database-id metadata] (with-different-database a-query database-id metadata nil)) ([a-query database-id metadata native-extras] (lib.core/with-different-database a-query (metadataProvider database-id metadata) (js->clj native-extras :keywordize-keys true)))) |
Updates the values of the extras required for the DB to run
Will ignore extras not in [[required-native-extras]].
| (defn ^:export with-native-extras [a-query native-extras] (lib.core/with-native-extras a-query (js->clj native-extras :keywordize-keys true))) |
Returns the native extras (eg. MongoDB collection name) associated with
| (defn ^:export native-extras [a-query] (clj->js (lib.core/native-extras a-query))) |
Returns the database engine of the database targeted by
| (defn ^:export engine [a-query] (name (lib.core/engine a-query))) |
Get metadata for the legacy Segment with
Legacy SegmentsSegments are a deprecated kind of reusable query fragments, roughly equivalent to a set of filter clauses. These functions still work, but they're Legacy and Single Use, and will be removed when legacy Segments are removed from the product in 2024. | (defn ^:export segment-metadata [metadata-providerable segment-id] (lib.metadata/segment metadata-providerable segment-id)) |
Returns a JS array of opaque legacy Segments metadata objects, that could be used as filters for
| (defn ^:export available-segments [a-query stage-number] (to-array (lib.core/available-segments a-query stage-number))) |
Returns a JS array of opaque metadata values for those Metrics that could be used as aggregations on
| (defn ^:export available-metrics [a-query stage-number] (to-array (lib.core/available-metrics a-query stage-number))) |
TODO: Move all the join logic into one block - it's scattered all through the lower half of this namespace. | |
Returns a JS array of columns that are available when joining
If The returned columns can be passed to [[with-join-fields]] to configure which list of columns are joined. Note that this is not cached like most of the other
| (defn ^:export joinable-columns [a-query stage-number join-or-joinable] ;; TODO: It's not practical to cache this currently. We need to be able to key off the query and the joinable, which ;; is not supported by the lib.cache system. (to-array (lib.core/joinable-columns a-query stage-number join-or-joinable))) |
TODO: table-or-card-metadata is too specific and leaks details of how sources are stored. We need a higher-level API for the sources of queries, especially with Metrics v2. | |
Given an integer Returns
| (defn ^:export table-or-card-metadata [query-or-metadata-provider table-id] (lib.metadata/table-or-card query-or-metadata-provider table-id)) |
TODO: "LHS" is a confusing name here. This is really the display name for the joined thing, usually a table. It's an internal detail that this is often based on the LHS of the first join condition, ie. the FK's name. | |
Get the display name for the joined table, card, model, etc. For an existing join, For a new join under construction, If the join has a condition set, its LHS column should be passed as
| (defn ^:export join-lhs-display-name [a-query stage-number join-or-joinable condition-lhs-column-or-nil] (lib.core/join-lhs-display-name a-query stage-number join-or-joinable condition-lhs-column-or-nil)) |
Get the Database ID ( Typically this is straightforward: queries generally specify the database ID they are querying. However, in some cases where the source is a saved question, a magic value is used, [[metabase.legacy-mbql.schema/saved-questions-virtual-database-id]]:
We attempt to resolve the correct Database ID by getting the metadata for any source card and checking its
database ID. If that is not available, this function will return
| (defn ^:export database-id [a-query] (lib.core/database-id a-query)) |
Updates the provided
Returns a new
| (defn ^:export join-condition-update-temporal-bucketing [a-query stage-number join-condition bucketing-option] (lib.core/join-condition-update-temporal-bucketing a-query stage-number join-condition bucketing-option)) |
(defn- fix-column-with-ref [a-ref column] (cond-> column ;; Sometimes the FE has result metadata from the QP, without the required :lib/source-uuid on it. ;; We have the UUID for the aggregation in its ref, so use that here. (some-> a-ref first (= :aggregation)) (assoc :lib/source-uuid (last a-ref)))) | |
Given a JS This properly handles fields, expressions and aggregations.
| (defn ^:export legacy-column->metadata [a-query stage-number ^js js-column] (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) (let [column-ref (when-let [a-ref (.-field_ref js-column)] (legacy-ref->pMBQL a-ref))] (fix-column-with-ref column-ref (js.metadata/parse-column js-column))))) |
Given a The spelling of the column key differs between multiple JS objects of this same general shape
( | (defn- js-cells-by [col-fn] (fn [^js cell] (let [column (js.metadata/parse-column (col-fn cell)) column-ref (when-let [a-ref (:field-ref column)] (legacy-ref->pMBQL a-ref))] {:column (fix-column-with-ref column-ref column) :column-ref column-ref :value (.-value cell)}))) |
(def ^:private row-cell (js-cells-by #(.-col ^js %))) (def ^:private dimension-cell (js-cells-by #(.-column ^js %))) | |
Return an array (possibly empty) of drill-thrus given:
Note that
Drill Thrudrill-thru is the somewhat opaque name given to the system which shows context menus when clicking on different parts of visualizations. For example, if looking at the table view for a simple query of the Orders table, clicking a column header will show a certain set of actions you can take (eg. filtering on that column, sorting by it, summarizing it in a few different ways, etc.). Clicking a cell in the table will offer a different set of actions. All of these actions are implemented in this library through two calls:
A few of the more complex drills have nontrivial UIs, for example "Break out by" and "Filter by this column", which have specific helper functions defined here to inform the UI. Code healthOverall the drill-thru logic might have been better as FE code written on top of this library, rather than as part of the library. All of this code is Single use and should not be called by any code other than the drill-thru context menus. In the long term, it should be factored out of | (defn ^:export available-drill-thrus [a-query stage-number card-id column value row dimensions] (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) (let [column-ref (when-let [a-ref (and column (.-field_ref ^js column))] (legacy-ref->pMBQL a-ref))] (->> (merge {:column (when column (fix-column-with-ref column-ref (js.metadata/parse-column column))) :column-ref column-ref :value (cond (undefined? value) nil ; Missing a value, ie. a column click (nil? value) :null ; Provided value is null, ie. database NULL :else value) :card-id card-id} (when row {:row (mapv row-cell row)}) (when (not-empty dimensions) {:dimensions (mapv dimension-cell dimensions)})) (lib.core/available-drill-thrus a-query stage-number) to-array)))) |
Applies the given Any number of additional The exact effect on the query depends on the specific drill-thru and the
| (defn ^:export drill-thru [a-query stage-number card-id a-drill-thru & args] (apply lib.core/drill-thru a-query stage-number card-id a-drill-thru args)) |
Returns a JS object with the details needed to render the complex UI for Since The return value has the form:
| (defn ^:export filter-drill-details [{a-query :query :keys [column stage-number value] :as _filter-drill}] #js {"column" column "query" a-query "stageIndex" stage-number "value" (lib.drill-thru.common/drill-value->js value)}) |
Returns a JS object with the details needed to render the complex UI for | (defn ^:export combine-column-drill-details [{a-query :query :keys [column stage-number]}] #js {"query" a-query "stageIndex" stage-number "column" column}) |
Returns a JS object with the details needed to render the complex UI for The return value has the form:
| (defn ^:export aggregation-drill-details [{:keys [aggregation] :as _aggregation-drill}] #js {"aggregation" aggregation}) |
Returns a JS array of the possible column extractions offered by The extractions are opaque values of the same type as are returned by [[column-extractions]].
| (defn ^:export column-extract-drill-extractions [column-extract-drill] (to-array (lib.core/extractions-for-drill column-extract-drill))) |
Returns a JS array of pivot types that are available in The list contains a subset of the strings
| (defn ^:export pivot-types [a-drill-thru] (->> (lib.core/pivot-types a-drill-thru) (map name) to-array)) |
Returns a JS array of pivotable columns for
| (defn ^:export pivot-columns-for-type [a-drill-thru pivot-type] (to-array (lib.core/pivot-columns-for-type a-drill-thru (keyword pivot-type)))) |
Changes an existing Can be passed an integer table id or a legacy
| (defn ^:export with-different-table [a-query table-id] (lib.core/with-different-table a-query table-id)) |
Given a
| (defn ^:export format-relative-date-range [n unit offset-n offset-unit options] (u.time/format-relative-date-range n (keyword unit) offset-n (some-> offset-unit keyword) (js->clj options :keywordize-keys true))) |
Given Matching is based on finding the basically plausible matches first. There is often zero or one plausible matches, and this can return quickly. If there are multiple plausible matches, they are disambiguated by the most important extra included in the
| (defn ^:export find-matching-column [a-query stage-number a-ref columns] (lib.core/find-matching-column a-query stage-number a-ref columns)) |
Return This returns
| (defn ^:export has-clauses [a-query stage-number] (lib.core/has-clauses? a-query stage-number)) |
Returns the number of stages in
| (defn ^:export stage-count [a-query] (lib.core/stage-count a-query)) |
Provides a reasonable display name for Can be expanded as needed but only currently defined for a narrow set of date filters. Falls back to the full filter display-name.
| (defn ^:export filter-args-display-name [a-query stage-number a-filter-clause] (lib.core/filter-args-display-name a-query stage-number a-filter-clause)) |
Convert
| (defn ^:export expression-clause-for-legacy-expression [a-query stage-number legacy-expression] (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) (let [expr (js->clj legacy-expression :keywordize-keys true) expr (first (mbql.normalize/normalize-fragment [:query :aggregation] [expr]))] (lib.core/normalize (lib.convert/->pMBQL expr))))) |
Convert When processing aggregation clauses with custom expressions, any
| (defn ^:export legacy-expression-for-expression-clause [a-query stage-number an-expression-clause] (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) (let [legacy-expr (-> an-expression-clause lib.convert/->legacy-MBQL)] (clj->js (cond-> legacy-expr (and (vector? legacy-expr) (#{:aggregation-options :value} (first legacy-expr))) (get 1)) :keyword-fn u/qualified-name)))) |
Checks
Cyclic references are checked only when Returns an i18n error message describing the problem, or
| (defn ^:export diagnose-expression [a-query stage-number expression-mode legacy-expression expression-position] (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) (let [expr (as-> legacy-expression expr (js->clj expr :keywordize-keys true) (first (mbql.normalize/normalize-fragment [:query :aggregation] [expr])) (lib.convert/->pMBQL expr) (lib.core/normalize expr))] (-> (lib.expression/diagnose-expression a-query stage-number (keyword expression-mode) expr expression-position) clj->js)))) |
TODO: [[field-values-search-info]] seems over-specific - I feel like we can do a better job of extracting search info from arbitrary entities, akin to [[display-info]]. | |
Info about whether the column in question has FieldValues associated with it for purposes of powering a search widget in the QB filter modals.
| (defn ^:export field-values-search-info [metadata-providerable column] (-> (lib.field/field-values-search-info metadata-providerable column) (update :has-field-values name) ;; TODO: This should probably reuse `display-info->js` for caching and uniformity. (update-keys cljs-key->js-key) clj->js)) |
Add or update a filter against a
Specialized FilteringThese specialized filter updates support the drag-and-drop "brush" filtering in the UI. Eg. dragging a box on a map visualization, or dragging between two points in a time series. This is a very FE-specific use case, but the logic is sufficiently complex and well-delimited that I think there's room for them in the library. TODO: All of these are consuming legacy columns and converting them; that should be happening on the calling side, or refactored away. | (defn ^:export update-lat-lon-filter [a-query stage-number latitude-column longitude-column card-id bounds] ;; (.log js/console "update-lat-lon-filter") (let [bounds (js->clj bounds :keywordize-keys true) latitude-column (legacy-column->metadata a-query stage-number latitude-column) longitude-column (legacy-column->metadata a-query stage-number longitude-column)] (lib.core/with-wrapped-native-query a-query stage-number card-id lib.core/update-lat-lon-filter latitude-column longitude-column bounds))) |
Add or update a filter against
| (defn ^:export update-numeric-filter [a-query stage-number numeric-column card-id start end] (let [numeric-column (legacy-column->metadata a-query stage-number numeric-column)] (lib.core/with-wrapped-native-query a-query stage-number card-id lib.core/update-numeric-filter numeric-column start end))) |
Add or update a filter against Modifies the temporal unit for any breakouts to on
| (defn ^:export update-temporal-filter [a-query stage-number temporal-column card-id start end] (let [temporal-column (legacy-column->metadata a-query stage-number temporal-column)] (lib.core/with-wrapped-native-query a-query stage-number card-id lib.core/update-temporal-filter temporal-column start end))) |
Given two columns, returns true if
| (defn ^:export valid-filter-for? [src-column dst-column] (lib.types.isa/valid-filter-for? src-column dst-column)) |
Return a JS array of entities which Required entities are all tables and cards which are used as sources or joined in, etc. Each entity is returned as a JS map
| (defn ^:export dependent-metadata [a-query card-id card-type] (to-array (map clj->js (lib.core/dependent-metadata a-query card-id (keyword card-type))))) |
Return a JS array of entities which are needed upfront to create a new query based on a table/card. Each entity is returned as a JS map
| (defn ^:export table-or-card-dependent-metadata [metadata-providerable table-id] (to-array (map clj->js (lib.core/table-or-card-dependent-metadata metadata-providerable table-id)))) |
Returns true if the query is runnable.
MBQL queries are always runnable. Native queries can run when:
| (defn ^:export can-run ([a-query] (can-run a-query "question")) ([a-query card-type] (lib.cache/side-channel-cache (keyword "can-run" card-type) a-query (fn [_] (lib.core/can-run a-query (keyword card-type)))))) |
Truncates a query for use in the Notebook editor's "preview" system. Takes
The
If the resulting query fails [[can-preview]], returns nil.
| (defn ^:export preview-query [a-query stage-number clause-type clause-index] (let [truncated-query (lib.core/preview-query a-query stage-number (keyword clause-type) clause-index)] (when (lib.core/can-preview truncated-query) truncated-query))) |
Returns true if the query can be saved.
A query can be saved when:
| (defn ^:export can-save ([a-query] (can-save a-query "question")) ([a-query card-type] (lib.cache/side-channel-cache (keyword "can-save" card-type) a-query (fn [_] (lib.core/can-save a-query (keyword card-type)))))) |
Adds an empty stage to This is so that parameters can address both the stage before and after the aggregation. Adding filters to the result at stage -1 will filter after the summary, filters added at stage -2 filter before the summary.
| (defn ^:export ensure-filter-stage [a-query] (lib.core/ensure-filter-stage a-query)) |