(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) |
(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 nil)] (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)})) |