Skip to content

Commit 242aa88

Browse files
committed
handle datetime conversion better
1 parent 26e8629 commit 242aa88

4 files changed

Lines changed: 59 additions & 23 deletions

File tree

.sql-formatter.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"language": "sqlite",
3+
"keywordCase": "lower"
4+
}

litequery/core.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Iterator
77
from contextlib import contextmanager
88
from dataclasses import dataclass
9-
from datetime import datetime
9+
from datetime import UTC, datetime
1010
from enum import Enum
1111
from pathlib import Path
1212
from typing import Any
@@ -38,19 +38,9 @@ def __init__(self, columns: list[str], values: list[Any]):
3838
dups = [c for c in columns if columns.count(c) > 1]
3939
raise ValueError(f"Duplicate columns: {set(dups)}. Use AS to alias.")
4040

41-
self._values = tuple(
42-
self._parse_datetime(v) if isinstance(v, str) else v for v in values
43-
)
41+
self._values = values
4442
self._index = {c: i for i, c in enumerate(columns)}
4543

46-
def _parse_datetime(self, value: str):
47-
if 19 <= len(value) <= 32 and value[0].isdigit():
48-
try:
49-
return datetime.fromisoformat(value)
50-
except ValueError:
51-
pass
52-
return value
53-
5444
def _available_columns(self) -> str:
5545
return ", ".join([f"'{c}'" for c in self._index.keys()])
5646

@@ -154,6 +144,16 @@ def row_factory(cursor, row):
154144
return Row(columns, row)
155145

156146

147+
def adapt_datetime(value: datetime):
148+
if value.tzinfo:
149+
value = value.astimezone(UTC)
150+
return value.replace(tzinfo=None).isoformat(sep=" ")
151+
152+
153+
def convert_datetime(value: bytes):
154+
return datetime.fromisoformat(value.decode())
155+
156+
157157
class Litequery:
158158
PRAGMAS = [
159159
("journal_mode", "wal"),
@@ -169,11 +169,15 @@ def __init__(self, config: Config, queries):
169169
self._thread_local = threading.local()
170170
self._create_methods(queries)
171171

172+
sqlite3.register_adapter(datetime, adapt_datetime)
173+
sqlite3.register_converter("datetime", convert_datetime)
174+
172175
def _create_connection(self) -> sqlite3.Connection:
173176
conn = sqlite3.connect(
174177
self.config.database_path,
175178
timeout=30,
176179
autocommit=True,
180+
detect_types=sqlite3.PARSE_COLNAMES | sqlite3.PARSE_DECLTYPES,
177181
)
178182
conn.row_factory = row_factory
179183
pragmas = (f"PRAGMA {p} = {v}" for p, v in self.PRAGMAS)

tests/queries/users.sql

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
-- name: get_all_users
22
select
3-
*
3+
*
44
from
5-
users;
5+
users;
66

77
-- name: get_user_by_id^
88
select
9-
*
9+
*
1010
from
11-
users
11+
users
1212
where
13-
id = :id;
13+
id = :id;
1414

1515
-- name: get_last_user_id$
1616
select
17-
id
17+
id
1818
from
19-
users
19+
users
2020
order by
21-
id desc;
21+
id desc;
2222

2323
-- name: insert_user<!
24-
insert into users (name, email)
25-
values (:name, :email);
24+
insert into
25+
users (name, email)
26+
values
27+
(:name, :email);
2628

2729
-- name: delete_all_users!
2830
delete from users;
29-

tests/test_conversions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import UTC, datetime, timedelta, timezone
2+
3+
4+
def test_naive_datetime_roundtrip(lq):
5+
dt = datetime(2025, 10, 24, 9, 30, 0, 0)
6+
row = lq.raw_one("select :dt as 'dt [datetime]'", dt=dt)
7+
8+
assert isinstance(row.dt, datetime)
9+
assert row.dt.tzinfo is None
10+
assert row.dt == dt
11+
12+
13+
def test_aware_datetime_converts_to_utc_naive(lq):
14+
tz = timezone(timedelta(hours=5))
15+
dt = datetime(2024, 1, 2, 10, 0, 0, tzinfo=tz)
16+
row = lq.raw_one("select :dt as 'dt [datetime]'", dt=dt)
17+
18+
dt_normalized = dt.astimezone(UTC).replace(tzinfo=None)
19+
assert row.dt.tzinfo is None
20+
assert row.dt == dt_normalized
21+
22+
23+
def test_datetime_column_is_naive(lq):
24+
user = lq.get_user_by_id(id=1)
25+
26+
assert isinstance(user.created_at, datetime)
27+
assert user.created_at.tzinfo is None

0 commit comments

Comments
 (0)