-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
unprepare.clj
111 lines (92 loc) · 4.15 KB
/
unprepare.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
(ns metabase.driver.sql.util.unprepare
"Utility functions for converting a prepared statement with `?` param placeholders into a plain SQL query by splicing
params in place.
TODO -- since this is no longer strictly a 'util' namespace (most `:sql-jdbc` drivers need to implement one or
methods from here) let's rename this `metabase.driver.sql.unprepare` when we get a chance."
(:require
[clojure.string :as str]
[java-time.api :as t]
[metabase.driver :as driver]
[metabase.driver.sql.util :as sql.u]
[metabase.util :as u]
[metabase.util.i18n :refer [trs]]
[metabase.util.log :as log])
(:import
(java.time Instant LocalDate LocalDateTime LocalTime OffsetDateTime OffsetTime ZonedDateTime)))
(set! *warn-on-reflection* true)
(defmulti unprepare-value
"Convert a single argument to appropriate raw SQL for splicing directly into a SQL query. Dispatches on both driver
and the class of `value`."
{:added "0.32.0" :arglists '(^String [driver value])}
(fn [driver value]
[(driver/the-initialized-driver driver) (class value)])
:hierarchy #'driver/hierarchy)
(defmethod unprepare-value :default
[_ value]
;; it's better return a slightly broken SQL query with a probably incorrect string representation of the value than
;; to have the entire QP run fail because of an unknown type.
(log/warn (trs "Don''t know how to unprepare values of class {0}" (.getName (class value))))
(str value))
(defmethod unprepare-value [:sql nil]
[_ _]
"NULL")
(defmethod unprepare-value [:sql String]
[_ s]
;; escape single-quotes like Cam's String -> Cam''s String
(str \' (sql.u/escape-sql s :ansi) \'))
(defmethod unprepare-value [:sql Boolean]
[_ value]
(if value "TRUE" "FALSE"))
(defmethod unprepare-value [:sql Number]
[_ value]
(str value))
(defmethod unprepare-value [:sql LocalDate]
[_ t]
(format "date '%s'" (t/format "yyyy-MM-dd" t)))
(defmethod unprepare-value [:sql LocalTime]
[_ t]
(format "time '%s'" (t/format "HH:mm:ss.SSS" t)))
(defmethod unprepare-value [:sql OffsetTime]
[_ t]
(format "time with time zone '%s'" (t/format "HH:mm:ss.SSSZZZZZ" t)))
(defmethod unprepare-value [:sql LocalDateTime]
[_ t]
(format "timestamp '%s'" (t/format "yyyy-MM-dd HH:mm:ss.SSS" t)))
(defmethod unprepare-value [:sql OffsetDateTime]
[_ t]
(format "timestamp with time zone '%s'" (t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)))
(defmethod unprepare-value [:sql ZonedDateTime]
[_ t]
(format "timestamp with time zone '%s'" (t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)))
;; TODO - pretty sure we can remove this
(defmethod unprepare-value [:sql Instant]
[driver t]
(unprepare-value driver (t/offset-date-time t (t/zone-offset 0))))
;; TODO - I think a name like `deparameterize` would be more appropriate here
(defmulti ^String unprepare
"Convert a normal SQL `[statement & prepared-statement-args]` vector into a flat, non-prepared statement.
Implementations should return a plain SQL string.
Drivers likely do not need to implement this method themselves -- instead, you should only need to provide
implementations of `unprepare-value` for the cases where it is needed."
{:added "0.32.0", :arglists '([driver [sql & args]]), :style/indent 1}
driver/dispatch-on-initialized-driver
:hierarchy #'driver/hierarchy)
(defmethod unprepare :sql [driver [sql & args]]
(transduce
identity
(completing
(fn [sql arg]
;; Only match single question marks; do not match ones like `??` which JDBC converts to `?` to use as Postgres
;; JSON operators amongst other things.
;;
;; TODO - this is not smart enough to handle question marks in non argument contexts, for example if someone
;; were to have a question mark inside an identifier such as a table name. I think we'd have to parse the SQL in
;; order to handle those situations.
(let [v (str (unprepare-value driver arg))]
(log/tracef "Splice %s as %s" (pr-str arg) (pr-str v))
(str/replace-first sql #"(?<!\?)\?(?!\?)" (str/re-quote-replacement v))))
(fn [spliced-sql]
(log/tracef "Spliced %s\n-> %s" (u/colorize 'green (pr-str sql)) (u/colorize 'blue (pr-str spliced-sql)))
spliced-sql))
sql
args))