August 21st, 2023 19:42 UTC · 1 year ago

allenap.meSQLite

ISO-8601 and DATETIME in SQLite

Or: how SQLite silently treats them differently

DATETIME comparisons (and presumably other DATETIME operations) silently degrade to operations on text, and this could spoil your weekend if you don’t spot it.

A demo

For the record, I’m running SQLite 3.39.5.

Let’s start with a very simple table with a DATETIME column:

create table foo (d datetime not null);

We insert the current timestamp. Behind the scenes I think that SQLite stores this as text. It will end up with a form like ‘2023-08-21 18:57:33’.

insert into foo (d) values (datetime('now'));

Next, we’ll insert an ISO-8601 (or SQLite-compatible subset thereof) date-time. I checked: this is a valid way to represent a DATETIME in SQLite. We construct it from component parts; it’ll end up with a form similar to the plain date-time above, but with a T separating the date and time, e.g. ‘2023-08-21T18:57:33’.

insert into foo (d) values (date('now') || 'T' || time('now'));

We have a table with a single column and two rows:

| d                   |
-----------------------
| 2023-08-21 19:03:17 |
| 2023-08-21T19:03:23 |

It’s now trivial to reproduce the problem:

sqlite> select d, d <= datetime('now') from foo;
2023-08-21 19:03:17 | 1
2023-08-21T19:03:23 | 0

i.e. SQLite thinks the ISO-8601 date-time is in the future.

Digging deeper

Let’s investigate further. We can use datetime function modifiers to search for a date-time that is ahead of 2023-08-21T19:03:23:

sqlite> select d, d <= datetime('now', '+10 hours') from foo where d like '%T%';
2023-08-21T19:03:23 | 1
sqlite> select d, d <= datetime('now', '+2 hours') from foo where d like '%T%';
2023-08-21T19:03:23 | 0
sqlite> select d, d <= datetime('now', '+6 hours') from foo where d like '%T%';
2023-08-21T19:03:23 | 1
sqlite> select d, d <= datetime('now', '+4 hours') from foo where d like '%T%';
2023-08-21T19:03:23 | 0
sqlite> select d, d <= datetime('now', '+5 hours') from foo where d like '%T%';
2023-08-21T19:03:23 | 1

Huh! Something changes between 4 and 5 hours from now! Well, yes: it’ll be tomorrow:

sqlite> select datetime('now', '+5 hours');
2023-08-22 00:12:20

Ummmmm 🤔 SQLite appears to be silently degrading to textual comparisons of DATETIME columns when they contain ISO-8601 strings – which are valid according to the documentation. It still performs proper date-time comparisons on values that don’t have that T in them.

Won’t get fooled again (yes I will)

A workaround is to always retell SQLite that it’s a DATETIME:

sqlite> select d, datetime(d) <= datetime('now') from foo;
2023-08-21 19:03:17 | 1
2023-08-21T19:03:23 | 1

⚠️ But SQLite will not complain if I (or you) forget – and so I (and you) will inevitably forget.

I would have so much preferred if SQLite had refused to do the comparison instead of something “helpful” but actually misleadingly useless/harmful instead.