Skip to content
Snippets Groups Projects
Unverified Commit 2b037d83 authored by pawit-metabase's avatar pawit-metabase Committed by GitHub
Browse files

sqlite: update sqlite-jdbc to 3.36.0.1 and improve date/time handling (#17567)

* update sqlite-jdbc to 3.36.0.1

The new version includes arm64 binaries and allows Cypress,
which uses SQLite, to run on Apple Silicon.

* Improve date/time handling in SQLite
parent cd758c96
No related branches found
No related tags found
No related merge requests found
......@@ -2,4 +2,4 @@
["src" "resources"]
:deps
{org.xerial/sqlite-jdbc {:mvn/version "3.25.2"}}}
{org.xerial/sqlite-jdbc {:mvn/version "3.36.0.1"}}}
......@@ -376,14 +376,30 @@
(.close stmt)
(throw e)))))
;; (.getObject rs i LocalDate) doesn't seem to work, nor does `(.getDate)`; and it seems to be the case that
;; timestamps come back as `Types/DATE` as well? Fetch them as a String and then parse them
;; SQLite has no intrinsic date/time type. The sqlite-jdbc driver provides the following de-facto mappings:
;; DATE or DATETIME => Types/DATE (only if type is int or string)
;; TIMESTAMP => Types/TIMESTAMP (only if type is int)
;; The data itself can be stored either as
;; 1) integer (unix epoch) - this is "point in time", so no confusion about timezone
;; 2) float (julian days) - this is "point in time", so no confusion about timezone
;; 3) string (ISO8601) - zoned or unzoned depending on content, sqlite-jdbc always treat it as local time
;; Note that it is possible to store other invalid data in the column as SQLite does not perform any validation.
(defn- sqlite-handle-timestamp
[^ResultSet rs ^Integer i]
(let [obj (.getObject rs i)]
(cond
;; For strings, use our own parser which is more flexible than sqlite-jdbc's and handles timezones correctly
(instance? String obj) (u.date/parse obj)
;; For other types, fallback to sqlite-jdbc's parser
;; Even in DATE column, it is possible to put DATETIME, so always treat as DATETIME
(some? obj) (t/local-date-time (.getTimestamp rs i)))))
(defmethod sql-jdbc.execute/read-column-thunk [:sqlite Types/DATE]
[_ ^ResultSet rs _ ^Integer i]
(fn []
(try
(when-let [t (.getDate rs i)]
(t/local-date t))
(catch Throwable _
(when-let [s (.getString rs i)]
(u.date/parse s))))))
(sqlite-handle-timestamp rs i)))
(defmethod sql-jdbc.execute/read-column-thunk [:sqlite Types/TIMESTAMP]
[_ ^ResultSet rs _ ^Integer i]
(fn []
(sqlite-handle-timestamp rs i)))
......@@ -142,3 +142,64 @@
:base-type :type/DateTime
:database-position 0}}}
(driver/describe-table driver db (Table (mt/id :timestamp_table)))))))))))))
(deftest select-query-datetime
(mt/test-driver :sqlite
(let [db-name "datetime_test"
details (mt/dbdef->connection-details :sqlite :db {:database-name db-name})]
(doseq [stmt ["DROP TABLE IF EXISTS datetime_table;"
"CREATE TABLE datetime_table (
test_case varchar,
col_timestamp timestamp,
col_date date,
col_datetime datetime);"
"INSERT INTO datetime_table
(test_case, col_timestamp, col_date, col_datetime) VALUES
('epoch', 1629865104000, 1629849600000, 1629865104000),
('iso8601-ms', '2021-08-25 04:18:24.111', null, '2021-08-25 04:18:24.111'),
('iso8601-no-ms', '2021-08-25 04:18:24', null, '2021-08-25 04:18:24'),
('iso8601-no-time', null, '2021-08-25', null),
('null', null, null, null);"]]
(jdbc/execute! (sql-jdbc.conn/connection-details->spec :sqlite details)
[stmt]))
(mt/with-temp Database [db {:engine :sqlite :details (assoc details :dbname db-name)}]
(sync/sync-database! db)
;; In SQLite, you can actually store any value in any date/timestamp column,
;; let's test only values we'd reasonably run into.
;; Caveat: TIMESTAMP stored as string doesn't get parsed and is returned as-is by the driver,
;; some upper layer will handle it.
(mt/with-db db
(testing "select datetime stored as unix epoch"
(is (= [["2021-08-25T04:18:24Z" ; TIMESTAMP
"2021-08-25T00:00:00Z" ; DATE
"2021-08-25T04:18:24Z"]] ; DATETIME
(qp.test/rows
(mt/run-mbql-query :datetime_table
{:fields [$col_timestamp $col_date $col_datetime]
:filter [:= $test_case "epoch"]})))))
(testing "select datetime stored as string with milliseconds"
(is (= [["2021-08-25 04:18:24.111" ; TIMESTAMP (raw string)
"2021-08-25T04:18:24.111Z"]] ; DATETIME
(qp.test/rows
(mt/run-mbql-query :datetime_table
{:fields [$col_timestamp $col_datetime]
:filter [:= $test_case "iso8601-ms"]})))))
(testing "select datetime stored as string without milliseconds"
(is (= [["2021-08-25 04:18:24" ; TIMESTAMP (raw string)
"2021-08-25T04:18:24Z"]] ; DATETIME
(qp.test/rows
(mt/run-mbql-query :datetime_table
{:fields [$col_timestamp $col_datetime]
:filter [:= $test_case "iso8601-no-ms"]})))))
(testing "select date stored as string without time"
(is (= [["2021-08-25T00:00:00Z"]] ; DATE
(qp.test/rows
(mt/run-mbql-query :datetime_table
{:fields [$col_date]
:filter [:= $test_case "iso8601-no-time"]})))))
(testing "select NULL"
(is (= [[nil nil nil]]
(qp.test/rows
(mt/run-mbql-query :datetime_table
{:fields [$col_timestamp $col_date $col_datetime]
:filter [:= $test_case "null"]}))))))))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment