G

Metabase CH

public
Guest Feb 17, 2025 Never 22
Clone
Clojure paste1.clj 576 lines (486 loc) | 21.66 KB
1
(ns metabase.driver.clickhouse-qp
2
"CLickHouse driver: QueryProcessor-related definition"
3
#_{:clj-kondo/ignore [:unsorted-required-namespaces]}
4
(:require [clojure.string :as str]
5
[honey.sql :as sql]
6
[java-time.api :as t]
7
[metabase.driver.clickhouse-nippy]
8
[metabase.driver.clickhouse-version :as clickhouse-version]
9
[metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
10
[metabase.driver.sql.query-processor :as sql.qp :refer [add-interval-honeysql-form]]
11
[metabase.driver.sql.util :as sql.u]
12
[metabase.driver.sql.util.unprepare :as unprepare]
13
[metabase.legacy-mbql.util :as mbql.u]
14
[metabase.query-processor.timezone :as qp.timezone]
15
[metabase.util :as u]
16
[metabase.util.date-2 :as u.date]
17
[metabase.util.honey-sql-2 :as h2x])
18
(:import [java.sql ResultSet ResultSetMetaData Types]
19
[java.time
20
LocalDate
21
LocalDateTime
22
LocalTime
23
OffsetDateTime
24
OffsetTime
25
ZonedDateTime]
26
java.util.Arrays))
27
28
;; (set! *warn-on-reflection* true) ;; isn't enabled because of Arrays/toString call
29
30
(defmethod sql.qp/quote-style :clickhouse [_] :mysql)
31
32
;; without try, there might be test failures when QP is not yet initialized
33
;; e.g., when a test is preparing the dataset
34
(defn- get-report-timezone-id-safely
35
[]
36
(try
37
(qp.timezone/report-timezone-id-if-supported)
38
(catch Throwable _e nil)))
39
40
;; datetime('europe/amsterdam') -> europe/amsterdam
41
(defn- extract-datetime-timezone
42
[db-type]
43
(when (and db-type (string? db-type))
44
(cond
45
;; e.g. DateTime64(3, 'Europe/Amsterdam')
46
(str/starts-with? db-type "datetime64")
47
(if (> (count db-type) 17) (subs db-type 15 (- (count db-type) 2)) nil)
48
;; e.g. DateTime('Europe/Amsterdam')
49
(str/starts-with? db-type "datetime")
50
(if (> (count db-type) 12) (subs db-type 10 (- (count db-type) 2)) nil)
51
;; _
52
:else nil)))
53
54
(defn- remove-low-cardinality-and-nullable
55
[db-type]
56
(when (and db-type (string? db-type))
57
(let [db-type-lowercase (u/lower-case-en db-type)
58
without-low-car (if (str/starts-with? db-type-lowercase "lowcardinality(")
59
(subs db-type-lowercase 15 (- (count db-type-lowercase) 1))
60
db-type-lowercase)
61
without-nullable (if (str/starts-with? without-low-car "nullable(")
62
(subs without-low-car 9 (- (count without-low-car) 1))
63
without-low-car)]
64
without-nullable)))
65
66
(defn- in-report-timezone
67
[expr]
68
(let [report-timezone (get-report-timezone-id-safely)
69
lower (u/lower-case-en (h2x/database-type expr))
70
db-type (remove-low-cardinality-and-nullable lower)]
71
(if (and report-timezone (string? db-type) (str/starts-with? db-type "datetime"))
72
(let [timezone (extract-datetime-timezone db-type)]
73
(if (not (= timezone (u/lower-case-en report-timezone)))
74
[:'toTimeZone expr (h2x/literal report-timezone)]
75
expr))
76
expr)))
77
78
(defmethod sql.qp/date [:clickhouse :default]
79
[_ _ expr]
80
expr)
81
82
;;; ------------------------------------------------------------------------------------
83
;;; Extract functions
84
;;; ------------------------------------------------------------------------------------
85
86
(defn- date-extract
87
[ch-fn expr db-type]
88
(-> [ch-fn (in-report-timezone expr)]
89
(h2x/with-database-type-info db-type)))
90
91
(defmethod sql.qp/date [:clickhouse :day-of-week]
92
[_ _ expr]
93
;; a tick in the function name prevents HSQL2 to make the function call UPPERCASE
94
;; https://cljdoc.org/d/com.github.seancorfield/honeysql/2.4.1011/doc/getting-started/other-databases#clickhouse
95
(sql.qp/adjust-day-of-week
96
:clickhouse (date-extract :'toDayOfWeek expr "uint8")))
97
98
(defmethod sql.qp/date [:clickhouse :month-of-year]
99
[_ _ expr]
100
(date-extract :'toMonth expr "uint8"))
101
102
(defmethod sql.qp/date [:clickhouse :minute-of-hour]
103
[_ _ expr]
104
(date-extract :'toMinute expr "uint8"))
105
106
(defmethod sql.qp/date [:clickhouse :hour-of-day]
107
[_ _ expr]
108
(date-extract :'toHour expr "uint8"))
109
110
(defmethod sql.qp/date [:clickhouse :day-of-month]
111
[_ _ expr]
112
(date-extract :'toDayOfMonth expr "uint8"))
113
114
(defmethod sql.qp/date [:clickhouse :day-of-year]
115
[_ _ expr]
116
(date-extract :'toDayOfYear expr "uint16"))
117
118
(defmethod sql.qp/date [:clickhouse :week-of-year-iso]
119
[_ _ expr]
120
(date-extract :'toISOWeek expr "uint8"))
121
122
(defmethod sql.qp/date [:clickhouse :quarter-of-year]
123
[_ _ expr]
124
(date-extract :'toQuarter expr "uint8"))
125
126
(defmethod sql.qp/date [:clickhouse :year-of-era]
127
[_ _ expr]
128
(date-extract :'toYear expr "uint16"))
129
130
;;; ------------------------------------------------------------------------------------
131
;;; Truncate functions
132
;;; ------------------------------------------------------------------------------------
133
134
(defn- date-trunc
135
[ch-fn expr]
136
[ch-fn (in-report-timezone expr)])
137
138
(defn- to-start-of-week
139
[expr]
140
(date-trunc :'toMonday expr))
141
142
(defmethod sql.qp/date [:clickhouse :minute]
143
[_ _ expr]
144
(date-trunc :'toStartOfMinute expr))
145
146
(defmethod sql.qp/date [:clickhouse :hour]
147
[_ _ expr]
148
(date-trunc :'toStartOfHour expr))
149
150
(defmethod sql.qp/date [:clickhouse :day]
151
[_ _ expr]
152
(date-trunc :'toStartOfDay expr))
153
154
(defmethod sql.qp/date [:clickhouse :week]
155
[driver _ expr]
156
(sql.qp/adjust-start-of-week driver to-start-of-week expr))
157
158
(defmethod sql.qp/date [:clickhouse :month]
159
[_ _ expr]
160
(date-trunc :'toStartOfMonth expr))
161
162
(defmethod sql.qp/date [:clickhouse :quarter]
163
[_ _ expr]
164
(date-trunc :'toStartOfQuarter expr))
165
166
(defmethod sql.qp/date [:clickhouse :year]
167
[_ _ expr]
168
(date-trunc :'toStartOfYear expr))
169
170
;;; ------------------------------------------------------------------------------------
171
;;; Unix timestamps functions
172
;;; ------------------------------------------------------------------------------------
173
174
(defmethod sql.qp/unix-timestamp->honeysql [:clickhouse :seconds]
175
[_ _ expr]
176
(h2x/->datetime expr))
177
178
(defmethod sql.qp/unix-timestamp->honeysql [:clickhouse :milliseconds]
179
[_ _ expr]
180
(let [report-timezone (get-report-timezone-id-safely)
181
inner-expr (h2x// expr 1000)]
182
(if report-timezone
183
[:'toDateTime64 inner-expr 3 report-timezone]
184
[:'toDateTime64 inner-expr 3])))
185
186
(defmethod sql.qp/unix-timestamp->honeysql [:clickhouse :microseconds]
187
[_ _ expr]
188
(let [report-timezone (get-report-timezone-id-safely)
189
inner-expr [:'toInt64 (h2x// expr 1000)]]
190
(if report-timezone
191
[:'fromUnixTimestamp64Milli inner-expr report-timezone]
192
[:'fromUnixTimestamp64Milli inner-expr])))
193
194
;;; ------------------------------------------------------------------------------------
195
;;; HoneySQL forms
196
;;; ------------------------------------------------------------------------------------
197
198
(defmethod sql.qp/->honeysql [:clickhouse :convert-timezone]
199
[driver [_ arg target-timezone source-timezone]]
200
(let [expr (sql.qp/->honeysql driver (cond-> arg (string? arg) u.date/parse))
201
with-tz-info? (h2x/is-of-type? expr #"(?:nullable\(|lowcardinality\()?(datetime64\(\d, {0,1}'.*|datetime\(.*)")
202
_ (sql.u/validate-convert-timezone-args with-tz-info? target-timezone source-timezone)]
203
(if (not with-tz-info?)
204
[:'plus
205
expr
206
[:'toIntervalSecond
207
[:'minus
208
[:'timeZoneOffset [:'toTimeZone expr target-timezone]]
209
[:'timeZoneOffset [:'toTimeZone expr source-timezone]]]]]
210
[:'toTimeZone expr target-timezone])))
211
212
(defmethod sql.qp/current-datetime-honeysql-form :clickhouse
213
[_]
214
(let [report-timezone (get-report-timezone-id-safely)
215
[expr db-type] (if report-timezone
216
[[:'now64 [:raw 9] (h2x/literal report-timezone)] (format "DateTime64(9, '%s')" report-timezone)]
217
[[:'now64 [:raw 9]] "DateTime64(9)"])]
218
(h2x/with-database-type-info expr db-type)))
219
220
(defn- date-time-parse-fn
221
[nano]
222
(if (zero? nano) :'parseDateTimeBestEffort :'parseDateTime64BestEffort))
223
224
(defmethod sql.qp/->honeysql [:clickhouse LocalDateTime]
225
[_ ^java.time.LocalDateTime t]
226
(let [formatted (t/format "yyyy-MM-dd HH:mm:ss.SSS" t)
227
report-tz (or (get-report-timezone-id-safely) "UTC")]
228
(if (zero? (.getNano t))
229
[:'parseDateTimeBestEffort formatted report-tz]
230
[:'parseDateTime64BestEffort formatted 3 report-tz])))
231
232
(defmethod sql.qp/->honeysql [:clickhouse ZonedDateTime]
233
[_ ^java.time.ZonedDateTime t]
234
(let [formatted (t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)
235
fn (date-time-parse-fn (.getNano t))]
236
[fn formatted]))
237
238
(defmethod sql.qp/->honeysql [:clickhouse OffsetDateTime]
239
[_ ^java.time.OffsetDateTime t]
240
;; copy-paste due to reflection warnings
241
(let [formatted (t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)
242
fn (date-time-parse-fn (.getNano t))]
243
[fn formatted]))
244
245
(defmethod sql.qp/->honeysql [:clickhouse LocalDate]
246
[_ ^java.time.LocalDate t]
247
[:'parseDateTimeBestEffort t])
248
249
(defn- local-date-time
250
[^java.time.LocalTime t]
251
(t/local-date-time (t/local-date 1970 1 1) t))
252
253
(defmethod sql.qp/->honeysql [:clickhouse LocalTime]
254
[driver ^java.time.LocalTime t]
255
(sql.qp/->honeysql driver (local-date-time t)))
256
257
(defmethod sql.qp/->honeysql [:clickhouse OffsetTime]
258
[driver ^java.time.OffsetTime t]
259
(sql.qp/->honeysql driver (t/offset-date-time
260
(local-date-time (.toLocalTime t))
261
(.getOffset t))))
262
263
(defn- args->float64
264
[args]
265
(map (fn [arg] [:'toFloat64 (sql.qp/->honeysql :clickhouse arg)]) args))
266
267
(defn- interval? [expr]
268
(mbql.u/is-clause? :interval expr))
269
270
(defmethod sql.qp/->honeysql [:clickhouse :+]
271
[driver [_ & args]]
272
(if (some interval? args)
273
(if-let [[field intervals] (u/pick-first (complement interval?) args)]
274
(reduce (fn [hsql-form [_ amount unit]]
275
(add-interval-honeysql-form driver hsql-form amount unit))
276
(sql.qp/->honeysql driver field)
277
intervals)
278
(throw (ex-info "Summing intervals is not supported" {:args args})))
279
(into [:+] (args->float64 args))))
280
281
(defmethod sql.qp/->honeysql [:clickhouse :log]
282
[driver [_ field]]
283
[:'log10 (sql.qp/->honeysql driver field)])
284
285
(defn- format-expr
286
[expr]
287
(first (sql/format-expr (sql.qp/->honeysql :clickhouse expr) {:nested true})))
288
289
(defmethod sql.qp/->honeysql [:clickhouse :percentile]
290
[_ [_ field p]]
291
[:raw (format "quantile(%s)(%s)" (format-expr p) (format-expr field))])
292
293
(defmethod sql.qp/->honeysql [:clickhouse :regex-match-first]
294
[driver [_ arg pattern]]
295
[:'extract (sql.qp/->honeysql driver arg) pattern])
296
297
(defmethod sql.qp/->honeysql [:clickhouse :stddev]
298
[driver [_ field]]
299
[:'stddevPop (sql.qp/->honeysql driver field)])
300
301
(defmethod sql.qp/->honeysql [:clickhouse :median]
302
[driver [_ field]]
303
[:'median (sql.qp/->honeysql driver field)])
304
305
;; Substring does not work for Enums, so we need to cast to String
306
(defmethod sql.qp/->honeysql [:clickhouse :substring]
307
[driver [_ arg start length]]
308
(let [str [:'toString (sql.qp/->honeysql driver arg)]]
309
(if length
310
[:'substring str
311
(sql.qp/->honeysql driver start)
312
(sql.qp/->honeysql driver length)]
313
[:'substring str
314
(sql.qp/->honeysql driver start)])))
315
316
(defmethod sql.qp/->honeysql [:clickhouse :var]
317
[driver [_ field]]
318
[:'varPop (sql.qp/->honeysql driver field)])
319
320
(defmethod sql.qp/->float :clickhouse
321
[_ value]
322
[:'toFloat64 value])
323
324
(defmethod sql.qp/->honeysql [:clickhouse :value]
325
[driver value]
326
(let [[_ value {base-type :base_type}] value]
327
(when (some? value)
328
(condp #(isa? %2 %1) base-type
329
:type/IPAddress [:'toIPv4 value]
330
(sql.qp/->honeysql driver value)))))
331
332
(defmethod sql.qp/->honeysql [:clickhouse :=]
333
[driver [op field value]]
334
(let [[qual valuevalue fieldinfo] value
335
hsql-field (sql.qp/->honeysql driver field)
336
hsql-value (sql.qp/->honeysql driver value)]
337
(if (and (isa? qual :value)
338
(isa? (:base_type fieldinfo) :type/Text)
339
(nil? valuevalue))
340
[:or
341
[:= hsql-field hsql-value]
342
[:= [:'empty hsql-field] 1]]
343
((get-method sql.qp/->honeysql [:sql :=]) driver [op field value]))))
344
345
(defmethod sql.qp/->honeysql [:clickhouse :!=]
346
[driver [op field value]]
347
(let [[qual valuevalue fieldinfo] value
348
hsql-field (sql.qp/->honeysql driver field)
349
hsql-value (sql.qp/->honeysql driver value)]
350
(if (and (isa? qual :value)
351
(isa? (:base_type fieldinfo) :type/Text)
352
(nil? valuevalue))
353
[:and
354
[:!= hsql-field hsql-value]
355
[:= [:'notEmpty hsql-field] 1]]
356
((get-method sql.qp/->honeysql [:sql :!=]) driver [op field value]))))
357
358
;; I do not know why the tests expect nil counts for empty results
359
;; but that's how it is :-)
360
;;
361
;; It would even be better if we could use countIf and sumIf directly
362
;;
363
;; metabase.query-processor-test.count-where-test
364
;; metabase.query-processor-test.share-test
365
(defmethod sql.qp/->honeysql [:clickhouse :count-where]
366
[driver [_ pred]]
367
[:case
368
[:> [:'count] 0]
369
[:sum [:case (sql.qp/->honeysql driver pred) 1 :else 0]]
370
:else nil])
371
372
(defmethod sql.qp/->honeysql [:clickhouse :sum-where]
373
[driver [_ field pred]]
374
[:sum [:case (sql.qp/->honeysql driver pred) (sql.qp/->honeysql driver field)
375
:else 0]])
376
377
(defmethod sql.qp/add-interval-honeysql-form :clickhouse
378
[_ dt amount unit]
379
(h2x/+ dt [:raw (format "INTERVAL %d %s" (int amount) (name unit))]))
380
381
(defn- clickhouse-string-fn
382
[fn-name field value options]
383
(let [hsql-field (sql.qp/->honeysql :clickhouse field)
384
hsql-value (sql.qp/->honeysql :clickhouse value)]
385
(if (get options :case-sensitive true)
386
[fn-name hsql-field hsql-value]
387
[fn-name [:'lowerUTF8 hsql-field] [:'lowerUTF8 hsql-value]])))
388
389
(defmethod sql.qp/->honeysql [:clickhouse :starts-with]
390
[_ [_ field value options]]
391
(let [starts-with (clickhouse-version/with-min 23 8
392
(constantly :'startsWithUTF8)
393
(constantly :'startsWith))]
394
(clickhouse-string-fn starts-with field value options)))
395
396
(defmethod sql.qp/->honeysql [:clickhouse :ends-with]
397
[_ [_ field value options]]
398
(let [ends-with (clickhouse-version/with-min 23 8
399
(constantly :'endsWithUTF8)
400
(constantly :'endsWith))]
401
(clickhouse-string-fn ends-with field value options)))
402
403
(defmethod sql.qp/->honeysql [:clickhouse :contains]
404
[_ [_ field value options]]
405
(let [hsql-field (sql.qp/->honeysql :clickhouse field)
406
hsql-value (sql.qp/->honeysql :clickhouse value)
407
position-fn (if (get options :case-sensitive true)
408
:'positionUTF8
409
:'positionCaseInsensitiveUTF8)]
410
[:> [position-fn hsql-field hsql-value] 0]))
411
412
(defmethod sql.qp/->honeysql [:clickhouse :datetime-diff]
413
[driver [_ x y unit]]
414
(let [x (sql.qp/->honeysql driver x)
415
y (sql.qp/->honeysql driver y)]
416
(case unit
417
;; Week: Metabase tests expect a bit different result from what `age` provides
418
(:week)
419
[:'intDiv [:'dateDiff (h2x/literal :day) (date-trunc :'toStartOfDay x) (date-trunc :'toStartOfDay y)] [:raw 7]]
420
;; -------------------------
421
(:year :month :quarter :day)
422
[:'age (h2x/literal unit) (date-trunc :'toStartOfDay x) (date-trunc :'toStartOfDay y)]
423
;; -------------------------
424
(:hour :minute :second)
425
[:'age (h2x/literal unit) (in-report-timezone x) (in-report-timezone y)])))
426
427
;; We do not have Time data types, so we cheat a little bit
428
(defmethod sql.qp/cast-temporal-string [:clickhouse :Coercion/ISO8601->Time]
429
[_driver _special_type expr]
430
[:'parseDateTimeBestEffort [:'concat "1970-01-01T" expr]])
431
432
(defmethod sql.qp/cast-temporal-byte [:clickhouse :Coercion/ISO8601->Time]
433
[_driver _special_type expr]
434
expr)
435
436
;;; ------------------------------------------------------------------------------------
437
;;; JDBC-related functions
438
;;; ------------------------------------------------------------------------------------
439
440
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/TINYINT]
441
[_ ^ResultSet rs ^ResultSetMetaData _ ^Integer i]
442
(fn []
443
(.getObject rs i)))
444
445
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/SMALLINT]
446
[_ ^ResultSet rs ^ResultSetMetaData _ ^Integer i]
447
(fn []
448
(.getObject rs i)))
449
450
;; This is for tests only - some of them expect nil values
451
;; getInt/getLong return 0 in case of a NULL value in the result set
452
;; the only way to check if it was actually NULL - call ResultSet.wasNull afterwards
453
(defn- with-null-check
454
[^ResultSet rs value]
455
(if (.wasNull rs) nil value))
456
457
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/BIGINT]
458
[_ ^ResultSet rs ^ResultSetMetaData _ ^Integer i]
459
(fn []
460
(with-null-check rs (.getBigDecimal rs i))))
461
462
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/INTEGER]
463
[_ ^ResultSet rs ^ResultSetMetaData _ ^Integer i]
464
(fn []
465
(with-null-check rs (.getLong rs i))))
466
467
(def ^:private utc-zone-id (java.time.ZoneId/of "UTC"))
468
(defn- zdt-in-report-timezone
469
[^ZonedDateTime zdt]
470
(let [maybe-report-timezone (get-report-timezone-id-safely)]
471
(if maybe-report-timezone
472
(.withZoneSameInstant zdt (java.time.ZoneId/of maybe-report-timezone))
473
(if (= (.getId (.getZone zdt)) "GMT0") ;; for test purposes only; GMT0 is a legacy tz
474
(.withZoneSameInstant zdt utc-zone-id)
475
zdt))))
476
477
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/DATE]
478
[_ ^ResultSet rs ^ResultSetMetaData _rsmeta ^Integer i]
479
(fn []
480
(when-let [sql-date (.getDate rs i)]
481
(.toLocalDate sql-date))))
482
483
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/TIMESTAMP]
484
[_ ^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
485
(fn []
486
(when-let [zdt (.getObject rs i ZonedDateTime)]
487
(let [db-type (remove-low-cardinality-and-nullable (.getColumnTypeName rsmeta i))]
488
(if (= db-type "datetime64(3, 'gmt0')")
489
;; a hack for some MB test assertions only; GMT0 is a legacy tz
490
(.toLocalDateTime (zdt-in-report-timezone zdt))
491
;; this is the normal behavior
492
(.toOffsetDateTime (.withZoneSameInstant
493
(zdt-in-report-timezone zdt)
494
utc-zone-id)))))))
495
496
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/TIME]
497
[_ ^ResultSet rs ^ResultSetMetaData _ ^Integer i]
498
(fn []
499
(.getObject rs i OffsetTime)))
500
501
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/NUMERIC]
502
[_ ^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
503
(fn []
504
; count is NUMERIC cause UInt64 is too large for the canonical SQL BIGINT,
505
; and defaults to BigDecimal, but we want it to be coerced to java Long
506
; cause it still fits and the tests are expecting that
507
(if (= (.getColumnLabel rsmeta i) "count")
508
(.getLong rs i)
509
(.getBigDecimal rs i))))
510
511
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/ARRAY]
512
[_ ^ResultSet rs ^ResultSetMetaData _rsmeta ^Integer i]
513
(fn []
514
(when-let [arr (.getArray rs i)]
515
(Arrays/deepToString (.getArray arr)))))
516
517
(defn- ipv4-column->string
518
[^ResultSet rs ^Integer i]
519
(when-let [inet-address (.getObject rs i java.net.Inet4Address)]
520
(.getHostAddress inet-address)))
521
522
(defn- ipv6-column->string
523
[^ResultSet rs ^Integer i]
524
(when-let [inet-address (.getObject rs i java.net.Inet6Address)]
525
(.getHostAddress inet-address)))
526
527
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/OTHER]
528
[_ ^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
529
(fn []
530
(let [normalized-db-type (remove-low-cardinality-and-nullable
531
(.getColumnTypeName rsmeta i))]
532
(cond
533
(= normalized-db-type "ipv4")
534
(ipv4-column->string rs i)
535
(= normalized-db-type "ipv6")
536
(ipv6-column->string rs i)
537
;; _
538
:else (.getObject rs i)))))
539
540
(defmethod sql-jdbc.execute/read-column-thunk [:clickhouse Types/VARCHAR]
541
[_ ^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
542
(fn []
543
(let [normalized-db-type (remove-low-cardinality-and-nullable
544
(.getColumnTypeName rsmeta i))]
545
(cond
546
;; Enum8/Enum16
547
(str/starts-with? normalized-db-type "enum")
548
(.getString rs i)
549
;; _
550
:else (.getObject rs i)))))
551
552
(defmethod unprepare/unprepare-value [:clickhouse LocalDate]
553
[_ t]
554
(format "'%s'" (t/format "yyyy-MM-dd" t)))
555
556
(defmethod unprepare/unprepare-value [:clickhouse LocalTime]
557
[_ t]
558
(format "'%s'" (t/format "HH:mm:ss.SSS" t)))
559
560
(defmethod unprepare/unprepare-value [:clickhouse OffsetTime]
561
[_ t]
562
(format "'%s'" (t/format "HH:mm:ss.SSSZZZZZ" t)))
563
564
(defmethod unprepare/unprepare-value [:clickhouse LocalDateTime]
565
[_ t]
566
(format "'%s'" (t/format "yyyy-MM-dd HH:mm:ss.SSS" t)))
567
568
(defmethod unprepare/unprepare-value [:clickhouse OffsetDateTime]
569
[_ ^OffsetDateTime t]
570
(format "%s('%s')"
571
(if (zero? (.getNano t)) "parseDateTimeBestEffort" "parseDateTime64BestEffort")
572
(t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)))
573
574
(defmethod unprepare/unprepare-value [:clickhouse ZonedDateTime]
575
[_ t]
576
(format "'%s'" (t/format "yyyy-MM-dd HH:mm:ss.SSSZZZZZ" t)))