(ns metabase.lib.convert (:require #?@(:clj ([metabase.util.log :as log])) [clojure.data :as data] [clojure.set :as set] [clojure.string :as str] [malli.error :as me] [medley.core :as m] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.lib.dispatch :as lib.dispatch] [metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.normalize :as lib.normalize] [metabase.lib.options :as lib.options] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.expression :as lib.schema.expression] [metabase.lib.schema.ref :as lib.schema.ref] [metabase.lib.util :as lib.util] [metabase.util :as u] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr]) #?@(:cljs [(:require-macros [metabase.lib.convert :refer [with-aggregation-list]])])) | |
(def ^:private ^:dynamic *pMBQL-uuid->legacy-index*
{}) | |
(def ^:private ^:dynamic *legacy-index->pMBQL-uuid*
{}) | |
(defn- clean-location [almost-stage error-type error-location]
(let [operate-on-parent? #{:malli.core/missing-key :malli.core/end-of-input}
location (if (operate-on-parent? error-type)
(drop-last 2 error-location)
(drop-last 1 error-location))
[location-key] (if (operate-on-parent? error-type)
(take-last 2 error-location)
(take-last 1 error-location))]
(if (seq location)
(update-in almost-stage
location
(fn [error-loc]
(let [result (assoc error-loc location-key nil)]
(cond
(vector? error-loc) (into [] (remove nil?) result)
(map? error-loc) (u/remove-nils result)
:else result))))
(dissoc almost-stage location-key)))) | |
(def ^:private stage-keys
#{:aggregation :breakout :expressions :fields :filters :order-by :joins}) | |
(defn- clean-stage-schema-errors [almost-stage]
(binding [lib.schema.expression/*suppress-expression-type-check?* true]
(loop [almost-stage almost-stage
removals []]
(if-let [[error-type error-location] (->> (mr/explain ::lib.schema/stage.mbql almost-stage)
:errors
(filter (comp stage-keys first :in))
(map (juxt :type :in))
first)]
(let [new-stage (clean-location almost-stage error-type error-location)
error-desc (pr-str (or error-type
;; if `error-type` is missing, which seems to happen sometimes,
;; fall back to humanizing the entire error.
(me/humanize (mr/explain ::lib.schema/stage.mbql almost-stage))))]
;; TODO: Bring this back, for all the idents. We can't enforce this strictly when they're not being added
;; by the BE for pre-existing queries.
#_(when (= (last error-location) :ident)
(throw (ex-info "Ident error" {:loc error-location
:error-desc error-desc
:diff (first (data/diff almost-stage new-stage))})))
#?(:cljs (js/console.warn "Clean: Removing bad clause due to error!" error-location error-desc
(u/pprint-to-str (first (data/diff almost-stage new-stage))))
:clj (log/warnf "Clean: Removing bad clause in %s due to error %s:\n%s"
(u/colorize :yellow (pr-str error-location))
(u/colorize :yellow error-desc)
(u/colorize :red (u/pprint-to-str (first (data/diff almost-stage new-stage))))))
(if (= new-stage almost-stage)
almost-stage
(recur new-stage (conj removals [error-type error-location]))))
almost-stage)))) | |
(defn- clean-stage-ref-errors [almost-stage]
(reduce (fn [almost-stage [loc _]]
(clean-location almost-stage ::lib.schema/invalid-ref loc))
almost-stage
(lib.schema/ref-errors-for-stage almost-stage))) | |
(defn- clean-stage [almost-stage]
(-> almost-stage
clean-stage-schema-errors
clean-stage-ref-errors)) | |
If true (this is the default), the query is cleaned. When converting queries at later stages of the preprocessing pipeline, this cleaning might not be desirable. | (def ^:dynamic *clean-query* true) |
Runs the provided function with cleaning of queries disabled. This is preferred over directly cleaning the query. | (defn without-cleaning
[f]
(binding [*clean-query* false]
(f))) |
(defn- clean [almost-query]
(if-not *clean-query*
almost-query
(loop [almost-query almost-query
stage-index 0]
(let [current-stage (nth (:stages almost-query) stage-index)
new-stage (clean-stage current-stage)]
(if (= current-stage new-stage)
(if (= stage-index (dec (count (:stages almost-query))))
almost-query
(recur almost-query (inc stage-index)))
(recur (update almost-query :stages assoc stage-index new-stage) stage-index)))))) | |
Coerce something to pMBQL (the version of MBQL manipulated by Metabase Lib v2) if it's not already pMBQL. | (defmulti ->pMBQL
{:arglists '([x])}
lib.dispatch/dispatch-value
:hierarchy lib.hierarchy/hierarchy) |
(defn- default-MBQL-clause->pMBQL [mbql-clause]
(let [last-elem (peek mbql-clause)
last-elem-option? (map? last-elem)
[clause-type & args] (cond-> mbql-clause
last-elem-option? pop)
options (if last-elem-option?
last-elem
{})]
(lib.options/ensure-uuid (into [clause-type options] (map ->pMBQL) args)))) | |
(defmethod ->pMBQL :default
[x]
(if (and (vector? x)
(keyword? (first x)))
(default-MBQL-clause->pMBQL x)
x)) | |
(defmethod ->pMBQL :mbql/query [query] query) | |
In legacy MBQL, join Since the new pMBQL schema makes | (def legacy-default-join-alias "__join") |
Join Only deduplicate the default | (defn- deduplicate-join-aliases
[joins]
(let [unique-name-fn (lib.util/unique-name-generator)]
(mapv (fn [join]
(cond-> join
(= (:alias join) legacy-default-join-alias) (update :alias unique-name-fn)))
joins))) |
If a query | (defn- stage-source-card-id->pMBQL
[stage]
(if (string? (:source-table stage))
(-> stage
(assoc :source-card (lib.util/legacy-string-table-id->card-id (:source-table stage)))
(dissoc :source-table))
stage)) |
Impl for [[with-aggregation-list]]. | (defn do-with-aggregation-list
[aggregations thunk]
(let [legacy->pMBQL (into {}
(map-indexed (fn [idx [_tag {ag-uuid :lib/uuid}]]
[idx ag-uuid]))
aggregations)
pMBQL->legacy (into {}
(map-indexed (fn [idx [_tag {ag-uuid :lib/uuid}]]
[ag-uuid idx]))
aggregations)]
(binding [*legacy-index->pMBQL-uuid* legacy->pMBQL
*pMBQL-uuid->legacy-index* pMBQL->legacy]
(thunk)))) |
Macro for capturing the context of a query stage's | #?(:clj
(defmacro with-aggregation-list
[aggregations & body]
`(do-with-aggregation-list ~aggregations (fn [] ~@body)))) |
(defn- from-indexed-idents [stage list-key idents-key]
(let [idents (get stage idents-key)]
(->> (get stage list-key)
->pMBQL
(map-indexed (fn [i x]
(if-let [ident (or (get idents i)
;; Conversion from JSON keywordizes all keys, including these numbers!
(get idents (keyword (str i))))]
(lib.options/update-options x assoc :ident ident)
x)))
vec
not-empty))) | |
(defmethod ->pMBQL :mbql.stage/mbql
[stage]
(let [aggregations (from-indexed-idents stage :aggregation :aggregation-idents)
expr-idents (:expression-idents stage)
expressions (->> stage
:expressions
(mapv (fn [[k v]]
(let [expr (-> v
->pMBQL
(lib.util/top-level-expression-clause k))]
(if-let [ident (get expr-idents k)]
(lib.options/update-options expr assoc :ident ident)
expr))))
not-empty)]
(metabase.lib.convert/with-aggregation-list aggregations
(let [stage (-> stage
stage-source-card-id->pMBQL
(m/assoc-some :expressions expressions
:aggregation aggregations
:breakout (from-indexed-idents stage :breakout :breakout-idents)))
stage (reduce
(fn [stage k]
(if-not (get stage k)
stage
(update stage k ->pMBQL)))
(dissoc stage :aggregation-idents :breakout-idents :expression-idents)
(disj stage-keys :aggregation :breakout :expressions))]
(cond-> stage
(:joins stage) (update :joins deduplicate-join-aliases)))))) | |
(defmethod ->pMBQL :mbql.stage/native [stage] (m/update-existing stage :template-tags update-vals (fn [tag] (m/update-existing tag :dimension ->pMBQL)))) | |
(defmethod ->pMBQL :mbql/join
[join]
(let [join (-> join
(update :conditions ->pMBQL)
(update :stages ->pMBQL))]
(cond-> join
(:fields join) (update :fields (fn [fields]
(if (sequential? fields)
(mapv ->pMBQL fields)
(keyword fields))))
(not (:alias join)) (assoc :alias legacy-default-join-alias)))) | |
(defmethod ->pMBQL :dispatch-type/sequential [xs] (mapv ->pMBQL xs)) | |
(defmethod ->pMBQL :dispatch-type/map
[m]
(if (:type m)
(-> (lib.util/pipeline m)
(update :stages (fn [stages]
(mapv ->pMBQL stages)))
lib.normalize/normalize
(assoc :lib.convert/converted? true)
clean)
(update-vals m ->pMBQL))) | |
(defmethod ->pMBQL :field
[[_tag x y]]
(let [[id-or-name options] (if (map? x)
[y x]
[x y])]
(lib.options/ensure-uuid [:field options id-or-name]))) | |
(defmethod ->pMBQL :value
[[_tag value opts]]
;; `:value` uses `:snake_case` keys in legacy MBQL for some insane reason (actually this was to match the shape of
;; the keys in Field metadata), at least for the three type keys enumerated below.
;; See [[metabase.legacy-mbql.schema/ValueTypeInfo]].
(let [opts (set/rename-keys opts {:base_type :base-type
:semantic_type :semantic-type
:database_type :database-type})
;; in pMBQL, `:effective-type` is a required key for `:value`. `:value` SHOULD have always had `:base-type`,
;; but on the off chance it did not, get the type from value so the schema doesn't fail entirely.
opts (assoc opts :effective-type (or (:effective-type opts)
(:base-type opts)
(lib.schema.expression/type-of value)))]
(lib.options/ensure-uuid [:value opts value]))) | |
(doseq [tag [:case :if]]
(defmethod ->pMBQL tag
[[_tag pred-expr-pairs options]]
(let [default (:default options)]
(cond-> [tag (dissoc options :default) (mapv ->pMBQL pred-expr-pairs)]
:always lib.options/ensure-uuid
(some? default) (conj (->pMBQL default)))))) | |
(defmethod ->pMBQL :expression [[tag value opts]] (lib.options/ensure-uuid [tag opts value])) | |
(defn- get-or-throw!
[m k]
(let [result (get m k ::not-found)]
(if-not (= result ::not-found)
result
(throw (ex-info (str "Unable to find key " (pr-str k) " in map.")
{:m m
:k k}))))) | |
(defmethod ->pMBQL :aggregation
[[tag aggregation-index opts, :as clause]]
(lib.options/ensure-uuid
[tag opts (or (get *legacy-index->pMBQL-uuid* aggregation-index)
(throw (ex-info (str "Error converting :aggregation reference: no aggregation at index "
aggregation-index)
{:clause clause})))])) | |
(defmethod ->pMBQL :aggregation-options
[[_tag aggregation options]]
(let [[tag opts & args] (->pMBQL aggregation)]
(into [tag (merge opts options)] args))) | |
(defmethod ->pMBQL :time-interval
[[_tag field n unit options]]
(lib.options/ensure-uuid [:time-interval (or options {}) (->pMBQL field) n unit])) | |
(defmethod ->pMBQL :relative-time-interval
[[_tag & [_column _value _bucket _offset-value _offset-bucket :as args]]]
(lib.options/ensure-uuid (into [:relative-time-interval {}] (map ->pMBQL) args))) | |
| (defmethod ->pMBQL :offset
[[tag opts expr n, :as clause]]
{:pre [(= (count clause) 4)]}
[tag opts (->pMBQL expr) n]) |
These four expressions have a different form depending on the number of arguments. | (doseq [tag [:contains :starts-with :ends-with :does-not-contain]] (lib.hierarchy/derive tag ::string-comparison)) |
(defmethod ->pMBQL ::string-comparison
[[tag opts & args :as clause]]
(if (> (count args) 2)
;; Multi-arg, pMBQL style: [tag {opts...} x y z ...]
(lib.options/ensure-uuid (into [tag opts] (map ->pMBQL args)))
;; Two-arg, legacy style: [tag x y] or [tag x y opts].
(let [[tag x y opts] clause]
(lib.options/ensure-uuid [tag (or opts {}) (->pMBQL x) (->pMBQL y)])))) | |
Convert a legacy 'inner query' to a full legacy 'outer query' so you can pass it to stuff like [[metabase.legacy-mbql.normalize/normalize]], and then probably to [[->pMBQL]]. | (defn legacy-query-from-inner-query
[database-id inner-query]
(merge {:database database-id, :type :query}
(if (:native inner-query)
{:native (set/rename-keys inner-query {:native :query})}
{:query inner-query}))) |
Coerce something to legacy MBQL (the version of MBQL understood by the query processor and Metabase Lib v1) if it's not already legacy MBQL. | (defmulti ->legacy-MBQL
{:arglists '([x])}
lib.dispatch/dispatch-value
:hierarchy lib.hierarchy/hierarchy) |
Does keyword | (defn- metabase-lib-keyword?
[k]
(and (qualified-keyword? k)
(when-let [symb-namespace (namespace k)]
(or (= symb-namespace "lib")
(= symb-namespace "lib.columns")
(str/starts-with? symb-namespace "metabase.lib."))))) |
Remove any keys starting with the No args = return transducer to remove keys from a map. One arg = update a map | (defn- disqualify
([]
(remove (fn [[k _v]]
(metabase-lib-keyword? k))))
([m]
(into {} (disqualify) m))) |
Map of option keys in pMBQL to their legacy names. Keys are renamed before [[disqualify]] drops all namespaced keys. | (def ^:private options-preserved-in-legacy
{:metabase.lib.field/original-temporal-unit :original-temporal-unit}) |
Convert an options map in an MBQL clause to the equivalent shape for legacy MBQL. Remove | (defn- options->legacy-MBQL
[m]
(->> (cond-> m
;; Following construct ensures that transformation mbql -> pmbql -> mbql, does not add base-type where those
;; were not present originally. Base types are added in [[metabase.lib.query/add-types-to-fields]].
(contains? m :metabase.lib.query/transformation-added-base-type)
(dissoc :metabase.lib.query/transformation-added-base-type :base-type)
;; Removing the namespaces from a few
true (update-keys #(get options-preserved-in-legacy % %)))
(into {} (comp (disqualify)
(remove (fn [[k _v]]
(#{:effective-type :ident} k)))))
not-empty)) |
(defmulti ^:private aggregation->legacy-MBQL
{:arglists '([aggregation-clause])}
lib.dispatch/dispatch-value
:hierarchy lib.hierarchy/hierarchy) | |
(defmethod aggregation->legacy-MBQL :default
[[tag options & args]]
(let [inner (into [tag] (map ->legacy-MBQL) args)
;; the default value of the :case or :if expression is in the options
;; in legacy MBQL
inner (if (and (#{:case :if} tag) (next args))
(conj (pop inner) {:default (peek inner)})
inner)]
(if-let [aggregation-opts (not-empty (options->legacy-MBQL options))]
[:aggregation-options inner aggregation-opts]
inner))) | |
(defmethod aggregation->legacy-MBQL :offset [clause] (->legacy-MBQL clause)) | |
(defn- clause-with-options->legacy-MBQL [[k options & args]]
(if (map? options)
(into [k] (concat (map ->legacy-MBQL args)
(when-let [options (not-empty (options->legacy-MBQL options))]
[options])))
(into [k] (map ->legacy-MBQL (cons options args))))) | |
(defmethod ->legacy-MBQL :default
[x]
(cond
(and (vector? x)
(keyword? (first x))) (clause-with-options->legacy-MBQL x)
(map? x) (-> x
disqualify
(update-vals ->legacy-MBQL))
:else x)) | |
(doseq [tag [::aggregation ::expression]] (lib.hierarchy/derive tag ::aggregation-or-expression)) | |
(doseq [tag [:count :avg :count-where :distinct
:max :median :min :percentile
:share :stddev :sum :sum-where]]
(lib.hierarchy/derive tag ::aggregation)) | |
(doseq [tag [:+ :- :* :/
:case :if :coalesce
:abs :log :exp :sqrt :ceil :floor :round :power :interval
:relative-datetime :time :absolute-datetime :now :convert-timezone
:get-week :get-year :get-month :get-day :get-hour
:get-minute :get-second :get-quarter
:datetime-add :datetime-subtract
:concat :substring :replace :regex-match-first
:length :trim :ltrim :rtrim :upper :lower]]
(lib.hierarchy/derive tag ::expression)) | |
(defmethod ->legacy-MBQL ::aggregation-or-expression [input] (aggregation->legacy-MBQL input)) | |
(defn- stage-metadata->legacy-metadata [stage-metadata]
(into []
(comp (map #(update-keys % u/->snake_case_en))
(map ->legacy-MBQL))
(:columns stage-metadata))) | |
(mu/defn- chain-stages [{:keys [stages]} :- [:map [:stages [:sequential :map]]]]
;; :source-metadata aka :lib/stage-metadata is handled differently in the two formats.
;; In legacy, an inner query might have both :source-query, and :source-metadata giving the metadata for that nested
;; :source-query.
;; In pMBQL, the :lib/stage-metadata is attached to the same stage it applies to.
;; So when chaining pMBQL stages back into legacy form, if stage n has :lib/stage-metadata, stage n+1 needs
;; :source-metadata attached.
(let [inner-query (first (reduce (fn [[inner stage-metadata] stage]
[(cond-> (->legacy-MBQL stage)
inner (assoc :source-query inner)
stage-metadata (assoc :source-metadata (stage-metadata->legacy-metadata stage-metadata)))
;; Get the :lib/stage-metadata off the original pMBQL stage, not the converted one.
(:lib/stage-metadata stage)])
nil
stages))]
(cond-> inner-query
;; If this is a native query, inner query will be used like: `{:type :native :native #_inner-query {:query ...}}`
(:native inner-query) (set/rename-keys {:native :query})))) | |
(defmethod ->legacy-MBQL :dispatch-type/map [m]
(into {}
(comp (disqualify)
(map (fn [[k v]]
[k (->legacy-MBQL v)])))
m)) | |
(defmethod ->legacy-MBQL :aggregation [[_ opts agg-uuid :as ag]]
(if (map? opts)
(try
(let [opts (options->legacy-MBQL opts)
base-agg [:aggregation (get-or-throw! *pMBQL-uuid->legacy-index* agg-uuid)]]
(if (seq opts)
(conj base-agg opts)
base-agg))
(catch #?(:clj Throwable :cljs :default) e
(throw (ex-info (lib.util/format "Error converting aggregation reference to pMBQL: %s" (ex-message e))
{:ref ag}
e))))
;; Our conversion is a bit too aggressive and we're hitting legacy refs like [:aggregation 0] inside
;; source_metadata that are only used for legacy and thus can be ignored
ag)) | |
(defmethod ->legacy-MBQL :dispatch-type/sequential [xs] (mapv ->legacy-MBQL xs)) | |
(defmethod ->legacy-MBQL :field [[_ opts id]]
;; Fields are not like the normal clauses - they need that options field even if it's null.
;; TODO: Sometimes the given field is in the legacy order - that seems wrong.
(let [[opts id] (if (or (nil? opts) (map? opts))
[opts id]
[id opts])]
[:field
(->legacy-MBQL id)
(options->legacy-MBQL opts)])) | |
(defmethod ->legacy-MBQL :value
[[_tag opts value]]
(let [opts (-> opts
;; as mentioned above, `:value` in legacy MBQL expects `snake_case` keys for type info keys.
(set/rename-keys {:base-type :base_type
:semantic-type :semantic_type
:database-type :database_type})
options->legacy-MBQL)]
;; in legacy MBQL, `:value` has to be three args; `opts` has to be present, but it should can be `nil` if it is
;; empty.
[:value value opts])) | |
| (defmethod ->legacy-MBQL :offset
[[tag opts expr n, :as clause]]
{:pre [(= (count clause) 4)]}
[tag opts (->legacy-MBQL expr) n]) |
(defmethod ->legacy-MBQL ::string-comparison
[[tag opts & args]]
(if (> (count args) 2)
(into [tag (disqualify opts)] (map ->legacy-MBQL args)) ; Multi-arg, pMBQL style: [tag {opts...} x y z ...]
;; Two-arg, legacy style: [tag x y] or [tag x y opts].
(let [opts (disqualify opts)]
(cond-> (into [tag] (map ->legacy-MBQL args))
(seq opts) (conj opts))))) | |
(defn- update-list->legacy-boolean-expression
[m pMBQL-key legacy-key]
(cond-> m
(= (count (get m pMBQL-key)) 1) (m/update-existing pMBQL-key (comp ->legacy-MBQL first))
(> (count (get m pMBQL-key)) 1) (m/update-existing pMBQL-key #(into [:and] (map ->legacy-MBQL) %))
:always (set/rename-keys {pMBQL-key legacy-key}))) | |
(defmethod ->legacy-MBQL :mbql/join [join]
(let [base (cond-> (disqualify join)
(and *clean-query* (str/starts-with? (:alias join) legacy-default-join-alias)) (dissoc :alias))]
(merge (-> base
(dissoc :stages :conditions)
(update-vals ->legacy-MBQL))
(-> base
(select-keys [:conditions])
(update-list->legacy-boolean-expression :conditions :condition))
(chain-stages base)))) | |
If a pMBQL query stage has | (defn- source-card->legacy-source-table
[stage]
(if-let [source-card-id (:source-card stage)]
(-> stage
(dissoc :source-card)
(assoc :source-table (str "card__" source-card-id)))
stage)) |
(defn- stage-expressions->legacy-MBQL [expressions]
(into {}
(for [expression expressions
:let [legacy-clause (->legacy-MBQL expression)]]
[(lib.util/expression-name expression)
;; We wrap literals in :value ->pMBQL so unwrap this
;; direction. Also, `:aggregation-options` is not allowed
;; inside `:expressions` in legacy, we'll just have to toss
;; the extra info.
(if (#{:value :aggregation-options} (first legacy-clause))
(second legacy-clause)
legacy-clause)]))) | |
(defn- idents-by-index [clause-list]
(when (seq clause-list)
(into {} (map-indexed (fn [i clause]
[i (lib.options/ident clause)]))
clause-list))) | |
(defmethod ->legacy-MBQL :mbql.stage/mbql
[stage]
(metabase.lib.convert/with-aggregation-list (:aggregation stage)
(reduce #(m/update-existing %1 %2 ->legacy-MBQL)
(-> stage
disqualify
source-card->legacy-source-table
(m/assoc-some :aggregation-idents (idents-by-index (:aggregation stage)))
(m/update-existing :aggregation #(mapv aggregation->legacy-MBQL %))
(m/assoc-some :breakout-idents (idents-by-index (:breakout stage)))
(m/update-existing :breakout #(mapv ->legacy-MBQL %))
(m/assoc-some :expression-idents (->> (:expressions stage)
(into {} (map (juxt lib.util/expression-name
lib.options/ident)))
not-empty))
(m/update-existing :expressions stage-expressions->legacy-MBQL)
(update-list->legacy-boolean-expression :filters :filter))
(disj stage-keys :aggregation :breakout :filters :expressions)))) | |
(defmethod ->legacy-MBQL :mbql.stage/native [stage]
(-> stage
disqualify
(update-vals ->legacy-MBQL))) | |
(defmethod ->legacy-MBQL :mbql/query [query]
(try
(let [base (disqualify query)
parameters (:parameters base)
inner-query (chain-stages base)
query-type (if (-> query :stages last :lib/type (= :mbql.stage/native))
:native
:query)]
(merge (dissoc base :stages :parameters :lib.convert/converted?)
(cond-> {:type query-type}
(seq inner-query) (assoc query-type inner-query)
(seq parameters) (assoc :parameters parameters))))
(catch #?(:clj Throwable :cljs :default) e
(throw (ex-info (lib.util/format "Error converting MLv2 query to legacy query: %s" (ex-message e))
{:query query}
e))))) | |
TODO: Look into whether this function can be refactored away - it's called from several places but I (Braden) think
legacy refs shouldn't make it out of | (mu/defn legacy-ref->pMBQL :- ::lib.schema.ref/ref
"Convert a legacy MBQL `:field`/`:aggregation`/`:expression` reference to pMBQL. Normalizes the reference if needed,
and handles JS -> Clj conversion as needed."
([query legacy-ref]
(legacy-ref->pMBQL query -1 legacy-ref))
([query :- ::lib.schema/query
stage-number :- :int
legacy-ref :- some?]
(let [legacy-ref (->> #?(:clj legacy-ref :cljs (js->clj legacy-ref :keywordize-keys true))
(mbql.normalize/normalize-fragment nil))
{aggregations :aggregation} (lib.util/query-stage query stage-number)]
(with-aggregation-list aggregations
(try
(->pMBQL legacy-ref)
(catch #?(:clj Throwable :cljs :default) e
(throw (ex-info (lib.util/format "Error converting legacy ref to pMBQL: %s" (ex-message e))
{:query query
:stage-number stage-number
:legacy-ref legacy-ref
:legacy-index->pMBQL-uuid *legacy-index->pMBQL-uuid*}
e)))))))) |
(defn- from-json [query-fragment]
#?(:cljs (if (object? query-fragment)
(js->clj query-fragment :keywordize-keys true)
query-fragment)
:clj query-fragment)) | |
Given a JSON-formatted legacy MBQL query, transform it to pMBQL. If you have only the inner query map ( | (defn js-legacy-query->pMBQL
[query-map]
(let [clj-map (from-json query-map)]
(if (= (:lib/type clj-map) "mbql/query")
(lib.normalize/normalize clj-map)
(-> clj-map (u/assoc-default :type :query) mbql.normalize/normalize ->pMBQL)))) |
Given a JSON-formatted inner query, transform it to pMBQL. If you have a complete legacy query ( | (defn js-legacy-inner-query->pMBQL
[inner-query]
(js-legacy-query->pMBQL {:type :query
:query (from-json inner-query)})) |