Metabase CH
public
Feb 17, 2025
Never
22
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)))