These functions take the info for a param fetched by the functions above and add additional info about how that param should be represented as SQL. (Specifically, they return information in this format: {;; appropriate SQL that should be used to replace the param snippet, e.g. {{x}}
:replacement-snippet "= ?"
;; ; any prepared statement args (values for | (ns metabase.driver.sql.parameters.substitution (:require [clojure.string :as str] [metabase.driver :as driver] [metabase.driver.common.parameters :as params] [metabase.driver.common.parameters.dates :as params.dates] [metabase.driver.common.parameters.operators :as params.ops] [metabase.driver.sql.query-processor :as sql.qp] [metabase.legacy-mbql.schema :as mbql.s] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.schema.parameter :as lib.schema.parameter] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.wrap-value-literals :as qp.wrap-value-literals] [metabase.query-processor.util.add-alias-info :as add] [metabase.util :as u] [metabase.util.date-2 :as u.date] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu]) (:import (clojure.lang IPersistentVector Keyword) (java.time.temporal Temporal) (java.util UUID) (metabase.driver.common.parameters Date DateRange DateTimeRange FieldFilter ReferencedCardQuery ReferencedQuerySnippet))) |
------------------------------------ ->prepared-substitution & default impls ------------------------------------- | |
Returns a | (defmulti ->prepared-substitution {:added "0.34.0" :arglists '([driver x])} (fn [driver x] [(driver/dispatch-on-initialized-driver driver) (class x)]) :hierarchy #'driver/hierarchy) |
Represents the SQL string replace value (usually ?) and the typed parameter value | (def PreparedStatementSubstitution [:map [:sql-string :string] [:param-values [:maybe [:sequential :any]]]]) |
(mu/defn make-stmt-subs :- PreparedStatementSubstitution "Create a `PreparedStatementSubstitution` map for `sql-string` and the `param-seq`" [sql-string param-seq] {:sql-string sql-string :param-values param-seq}) | |
Convert X to a replacement snippet info map by passing it to HoneySQL's | (defn- honeysql->prepared-stmt-subs [driver x] (let [honeysql (sql.qp/->honeysql driver x) [snippet & args] (sql.qp/format-honeysql driver honeysql)] (make-stmt-subs snippet args))) |
(mu/defmethod ->prepared-substitution [:sql nil] :- PreparedStatementSubstitution [driver _] (honeysql->prepared-stmt-subs driver nil)) | |
(mu/defmethod ->prepared-substitution [:sql Object] :- PreparedStatementSubstitution [driver obj] (honeysql->prepared-stmt-subs driver (str obj))) | |
(mu/defmethod ->prepared-substitution [:sql Number] :- PreparedStatementSubstitution [driver num] (honeysql->prepared-stmt-subs driver num)) | |
(mu/defmethod ->prepared-substitution [:sql Boolean] :- PreparedStatementSubstitution [driver b] (honeysql->prepared-stmt-subs driver b)) | |
(mu/defmethod ->prepared-substitution [:sql Keyword] :- PreparedStatementSubstitution [driver kwd] (honeysql->prepared-stmt-subs driver kwd)) | |
(mu/defmethod ->prepared-substitution [:sql Date] :- PreparedStatementSubstitution [_driver date] (make-stmt-subs "?" [date])) | |
(mu/defmethod ->prepared-substitution [:sql Temporal] :- PreparedStatementSubstitution [driver t] (honeysql->prepared-stmt-subs driver t)) | |
Returns a suitable temporal unit conversion keyword for | (defmulti align-temporal-unit-with-param-type {:added "0.48.0" :deprecated "0.49.0" :arglists '([driver field param-type])} driver/dispatch-on-initialized-driver :hierarchy #'driver/hierarchy) |
Returns a suitable temporal unit conversion keyword for | (defmulti align-temporal-unit-with-param-type-and-value {:added "0.49.0" :arglists '([driver field param-type value])} driver/dispatch-on-initialized-driver :hierarchy #'driver/hierarchy) |
#_{:clj-kondo/ignore [:deprecated-var]} (defmethod align-temporal-unit-with-param-type :default [_driver _field param-type] (when (params.dates/date-type? param-type) :day)) | |
(defmethod align-temporal-unit-with-param-type-and-value :default [_driver _field param-type value] (when (params.dates/date-type? param-type) (if-let [exclusion-type (params.dates/exclusion-date-type param-type value)] exclusion-type :default))) | |
------------------------------------------- ->replacement-snippet-info ------------------------------------------- | |
(def ^:private ParamSnippetInfo [:map [:replacement-snippet {:optional true} :string] ; allowed to be blank if this is an optional param [:prepared-statement-args {:optional true} [:maybe [:sequential :any]]]]) | |
Return information about how (->replacement-snippet-info :h2 "ABC") -> {:replacement-snippet "?", :prepared-statement-args "ABC"} | (defmulti ->replacement-snippet-info {:added "0.33.4" :arglists '([driver value])} (fn [driver v & _args] [(driver/the-initialized-driver driver) (class v)]) :hierarchy #'driver/hierarchy) |
(defn- create-replacement-snippet [driver nil-or-obj] (let [{:keys [sql-string param-values]} (->prepared-substitution driver nil-or-obj)] {:replacement-snippet sql-string :prepared-statement-args param-values})) | |
(defmethod ->replacement-snippet-info [:sql nil] [driver this] (create-replacement-snippet driver this)) | |
(defmethod ->replacement-snippet-info [:sql Object] [driver this] (create-replacement-snippet driver (str this))) | |
(defmethod ->replacement-snippet-info [:sql Number] [driver this] (create-replacement-snippet driver this)) | |
(defmethod ->replacement-snippet-info [:sql Boolean] [driver this] (create-replacement-snippet driver this)) | |
(defmethod ->replacement-snippet-info [:sql Keyword] [driver this] (if (= this params/no-value) {:replacement-snippet ""} (create-replacement-snippet driver this))) | |
(defmethod ->replacement-snippet-info [:sql UUID] [_driver this] {:replacement-snippet (format "CAST('%s' AS uuid)" (str this))}) | |
(defmethod ->replacement-snippet-info [:sql IPersistentVector] [driver values] (let [values (map (partial ->replacement-snippet-info driver) values)] {:replacement-snippet (str/join ", " (map :replacement-snippet values)) :prepared-statement-args (apply concat (map :prepared-statement-args values))})) | |
(mu/defn- maybe-parse-temporal-literal :- (lib.schema.common/instance-of-class java.time.temporal.Temporal) [x] (condp instance? x String (u.date/parse x) Temporal x (throw (ex-info (tru "Don''t know how to parse {0} {1} as a temporal literal" (class x) (pr-str x)) {:type qp.error-type/invalid-parameter :parameter x})))) | |
(defmethod ->replacement-snippet-info [:sql Date] [driver {:keys [s]}] (create-replacement-snippet driver (maybe-parse-temporal-literal s))) | |
(defn- prepared-ts-subs [driver operator date-str] (let [{:keys [sql-string param-values]} (->prepared-substitution driver (maybe-parse-temporal-literal date-str))] {:replacement-snippet (str operator " " sql-string) :prepared-statement-args param-values})) | |
(defmethod ->replacement-snippet-info [:sql DateRange] [driver {:keys [start end]}] (cond (= start end) (prepared-ts-subs driver \= start) (nil? start) (prepared-ts-subs driver \< end) (nil? end) (prepared-ts-subs driver \> start) :else ;; TIMEZONE FIXME - this is WRONG WRONG WRONG because date ranges should be inclusive for start and *exclusive* ;; for end (let [[start end] (map (fn [s] (->prepared-substitution driver (maybe-parse-temporal-literal s))) [start end])] {:replacement-snippet (format "BETWEEN %s AND %s" (:sql-string start) (:sql-string end)) :prepared-statement-args (concat (:param-values start) (:param-values end))}))) | |
(defmethod ->replacement-snippet-info [:sql DateTimeRange] [driver {:keys [start end]} & [field-identifier]] (let [[start end] (map (fn [s] (when s (->prepared-substitution driver (maybe-parse-temporal-literal s)))) [start end]) start-expr-native (when start (format "%s >= %s" field-identifier (:sql-string start))) end-expr-native (when end (format "%s < %s" field-identifier (:sql-string end)))] {:replacement-snippet (str/join " AND " (remove nil? [start-expr-native end-expr-native])) :prepared-statement-args (into [] (comp (keep :param-values) cat) [start end])})) | |
------------------------------------- Field Filter replacement snippet info -------------------------------------- | |
(mu/defn- combine-replacement-snippet-maps :- ParamSnippetInfo "Combine multiple `replacement-snippet-maps` into a single map using a SQL `AND` clause." [replacement-snippet-maps :- [:maybe [:sequential ParamSnippetInfo]]] {:replacement-snippet (str \( (str/join " AND " (map :replacement-snippet replacement-snippet-maps)) \)) :prepared-statement-args (mapcat :prepared-statement-args replacement-snippet-maps)}) | |
for relative dates convert the param to a | (mu/defn- date-range-field-filter->replacement-snippet-info :- ParamSnippetInfo [driver value] (->> (params.dates/date-string->range value) params/map->DateRange (->replacement-snippet-info driver))) |
(mu/defn- field-filter->equals-clause-sql :- ParamSnippetInfo [driver value] (-> (->replacement-snippet-info driver value) (update :replacement-snippet (partial str "= ")))) | |
(mu/defn- field-filter-multiple-values->in-clause-sql :- ParamSnippetInfo [driver values] (-> (->replacement-snippet-info driver (vec values)) (update :replacement-snippet (partial format "IN (%s)")))) | |
(mu/defn- honeysql->replacement-snippet-info :- ParamSnippetInfo "Convert `hsql-form` to a replacement snippet info map by passing it to HoneySQL's `format` function." [driver hsql-form] (let [[snippet & args] (sql.qp/format-honeysql driver hsql-form)] {:replacement-snippet snippet :prepared-statement-args args})) | |
(mu/defn- field->clause :- mbql.s/field [driver :- :keyword field :- ::lib.schema.metadata/column param-type :- ::lib.schema.parameter/type value] ;; The [[metabase.query-processor.middleware.parameters/substitute-parameters]] QP middleware actually happens before ;; the [[metabase.query-processor.middleware.resolve-fields/resolve-fields]] middleware that would normally fetch all ;; the Fields we need in a single pass, so this is actually necessary here. I don't think switching the order of the ;; middleware would work either because we don't know what Field this parameter actually refers to until we resolve ;; the parameter. There's probably _some_ way to structure things that would make this "duplicate" call unneeded, but ;; I haven't figured out what that is yet [:field (u/the-id field) {:base-type (:base-type field) :temporal-unit (align-temporal-unit-with-param-type-and-value driver field param-type value) ::add/source-table (:table-id field) ;; in case anyone needs to know we're compiling a Field filter. ::compiling-field-filter? true}]) | |
(mu/defn- field->identifier :- ::lib.schema.common/non-blank-string "Return an approprate snippet to represent this `field` in SQL given its param type. For non-date Fields, this is just a quoted identifier; for dates, the SQL includes appropriately bucketing based on the `param-type`." [driver field param-type value] (->> (field->clause driver field param-type value) (sql.qp/->honeysql driver) (honeysql->replacement-snippet-info driver) :replacement-snippet)) | |
Generate replacement snippet for field filter on datetime field. For details on how range is generated see the docstring of [[params.dates/date-str->datetime-range]]. | (defn- field-filter->replacement-snippet-for-datetime-field [driver {:keys [field] {:keys [value type]} :value :as _field-filter}] (letfn [(->datetime-replacement-snippet-info [range] (->replacement-snippet-info driver range (field->identifier driver field type value)))] (-> (params.dates/date-str->datetime-range value) params/map->DateTimeRange ->datetime-replacement-snippet-info))) |
(mu/defn- field-filter->replacement-snippet-info :- ParamSnippetInfo "Return `[replacement-snippet & prepared-statement-args]` appropriate for a field filter parameter." [driver {{param-type :type, value :value, :as params} :value, field :field, :as field-filter}] (assert (:id field) (format "Why doesn't Field have an ID?\n%s" (u/pprint-to-str field))) (letfn [(prepend-field [x] (update x :replacement-snippet (partial str (field->identifier driver field param-type value) " "))) (->honeysql [form] (sql.qp/->honeysql driver form))] (cond (params.ops/operator? param-type) (->> (assoc params :target [:template-tag (field->clause driver field param-type value)]) params.ops/to-clause mbql.u/desugar-filter-clause qp.wrap-value-literals/wrap-value-literals-in-mbql ->honeysql (honeysql->replacement-snippet-info driver)) (params.dates/exclusion-date-type param-type value) (let [field-clause (field->clause driver field param-type value)] (->> (params.dates/date-string->filter value field-clause) mbql.u/desugar-filter-clause qp.wrap-value-literals/wrap-value-literals-in-mbql ->honeysql (honeysql->replacement-snippet-info driver))) ;; Special handling for `FieldFilter`s on `:type/DateTime` fields. DateTime range is always generated. (and (params.dates/date-type? param-type) (isa? ((some-fn :effective-type :base-type) field) :type/DateTime)) (field-filter->replacement-snippet-for-datetime-field driver field-filter) ;; convert other date to DateRange record types (params.dates/not-single-date-type? param-type) (prepend-field (date-range-field-filter->replacement-snippet-info driver value)) ;; convert all other dates to `= <date>` (params.dates/date-type? param-type) (prepend-field (field-filter->equals-clause-sql driver (params/map->Date {:s value}))) ;; for sequences of multiple values we want to generate an `IN (...)` clause (sequential? value) (prepend-field (field-filter-multiple-values->in-clause-sql driver value)) ;; convert everything else to `= <value>` :else (prepend-field (field-filter->equals-clause-sql driver value))))) | |
(mu/defmethod ->replacement-snippet-info [:sql FieldFilter] [driver :- :keyword {:keys [value], :as field-filter} :- [:map [:field ::lib.schema.metadata/column] [:value :any]]] (cond ;; otherwise if the value isn't present just put in something that will always be true, such as `1` (e.g. `WHERE 1 ;; = 1`). This is only used for field filters outside of optional clauses (= value params/no-value) {:replacement-snippet "1 = 1"} ;; if we have a vector of multiple values recursively convert them to SQL and combine into an `AND` clause ;; (This is multiple values in the sense that the frontend provided multiple maps with value values for the same ;; FieldFilter, not in the sense that we have a single map with multiple values for `:value`.) (sequential? value) (combine-replacement-snippet-maps (for [v value] (->replacement-snippet-info driver (assoc field-filter :value v)))) ;; otherwise convert single value to SQL. :else (field-filter->replacement-snippet-info driver field-filter))) | |
------------------------------------ Referenced Card replacement snippet info ------------------------------------ | |
(defmethod ->replacement-snippet-info [:sql ReferencedCardQuery] [_ {:keys [query params]}] {:prepared-statement-args (not-empty params) :replacement-snippet (sql.qp/make-nestable-sql query)}) | |
---------------------------------- Native Query Snippet replacement snippet info --------------------------------- | |
(defmethod ->replacement-snippet-info [:sql ReferencedQuerySnippet] [_ {:keys [content]}] {:prepared-statement-args nil :replacement-snippet content}) | |