(ns metabase.query-analysis.native-query-analyzer.replacement (:require [clojure.string :as str] [macaw.core :as macaw] [metabase.driver.common.parameters :as params] [metabase.driver.common.parameters.parse :as params.parse] [metabase.driver.common.parameters.values :as params.values] [metabase.query-analysis.native-query-analyzer.impl :as nqa.impl] [metabase.query-processor.setup :as qp.setup] [metabase.query-processor.store :as qp.store])) | |
(defn- first-unique [raw-query f] (first (filter #(not (str/includes? raw-query %)) (repeatedly f)))) | |
(defn- gensymed-string [] (format "'%s'" (gensym "metabase_sentinel_"))) | |
(defn- gen-variable-sentinel [raw-query] (first-unique raw-query gensymed-string)) | |
(defn- gen-field-filter-sentinel [raw-query] (first-unique raw-query #(apply format "(%s = %s)" (repeat 2 (gensymed-string))))) | |
(defn- gen-table-sentinel [raw-query] (first-unique raw-query #(str (gensym "metabase_sentinel_table_")))) | |
(defn- gen-snippet-sentinel [raw-query {:keys [content]}] (let [delimited-snippet (fn [sentinel snippet-contents] (format "/* snippet_start_%s */ %s /* snippet_end_%s */" sentinel snippet-contents sentinel))] (first-unique raw-query #(delimited-snippet (gensym "mb_") content)))) | |
(defn- gen-option-sentinels [raw-query] (let [unique-sentinels? (fn [[open close]] (not (or (str/includes? raw-query open) (str/includes? raw-query close)))) gen-sentinel-candidates (fn [] (let [postfix (gensym "mb_") template "/* opt_%s_%s */"] [(format template "open" postfix) (format template "close" postfix)]))] (first (filter unique-sentinels? (repeatedly gen-sentinel-candidates))))) | |
(defn- braceify [s] (format "{{%s}}" s)) | |
(defn- add-tag [all-subs new-sub token] (assoc all-subs new-sub (braceify (:k token)))) | |
(defn- parse-tree->clean-query [raw-query tokens param->value] (loop [[token & rest] tokens query-so-far substitutions {}] (cond (nil? token) {:query query-so-far :substitutions substitutions} (string? token) (recur rest (str query-so-far token) substitutions) :else (let [v (param->value (:k token)) card-ref? (params/ReferencedCardQuery? v) snippet? (params/ReferencedQuerySnippet? v)] (cond card-ref? (let [sub (gen-table-sentinel raw-query)] (recur rest (str query-so-far sub) (add-tag substitutions sub token))) snippet? (let [sub (gen-snippet-sentinel raw-query v)] (recur rest (str query-so-far sub) (add-tag substitutions sub token))) (params/Optional? token) (let [[open-sentinel close-sentinel] (gen-option-sentinels raw-query) {inner-query :query inner-subs :substitutions} (parse-tree->clean-query raw-query (:args token) param->value)] (recur rest (str query-so-far open-sentinel inner-query close-sentinel) (merge inner-subs substitutions {open-sentinel "[[" close-sentinel "]]"}))) ;; Plain variable ;; Note that the order of the clauses matters: `card-ref?` or `snippet?` could be true when is a `Param?`, ;; so we need to handle those cases specially first and leave this as the token fall-through (params/Param? token) (let [sub (gen-variable-sentinel raw-query)] (recur rest (str query-so-far sub) (add-tag substitutions sub token))) (params/FieldFilter? token) (let [sub (gen-field-filter-sentinel raw-query)] (recur rest (str query-so-far sub) (add-tag substitutions sub token))) :else ;; "this should never happen" but if it does, we certainly want to know about it. (throw (ex-info "Unsupported token in native query" {:token token}))))))) | |
Return (replace-all "foo bar baz" {"foo" "quux" "ba" "xx"}) ;; => "quux xxr xxz" | (defn- replace-all [the-string replacements] (reduce (fn [s [from to]] (str/replace s from to)) the-string replacements)) |
(defn- param-values [query] (if (qp.store/initialized?) (params.values/query->params-map (:native query)) (qp.setup/with-qp-setup [q query] (params.values/query->params-map (:native q))))) | |
Given a dataset_query and a map of renames (with keys | (defn replace-names ;; This arity exists as a convenience for all the tests that are fairly driver agnostic. ([query renames] ;; Postgres is both popular and adheres closely to the standard SQL specifications. (replace-names :postgres query renames)) ;; Currently we take just the driver, but in future it may more sense to take the entire database entity, to match ;; the actual configuration, reserved words for the given version, etc. ([driver query renames] (let [raw-query (get-in query [:native :query]) parsed-query (params.parse/parse raw-query) param->value (param-values query) {clean-query :query tt-subs :substitutions} (parse-tree->clean-query raw-query parsed-query param->value) macaw-opts (nqa.impl/macaw-options driver) renamed-query (macaw/replace-names clean-query renames (assoc macaw-opts :allow-unused? true))] (replace-all renamed-query tt-subs)))) |