Store named datetime columns as INTEGER microseconds (datetime_columns)

This commit is contained in:
Jan Doubravský
2026-06-09 18:18:38 +02:00
parent a21b5a2a04
commit 8e46ee3547
11 changed files with 255 additions and 22 deletions
+53 -1
View File
@@ -1,6 +1,6 @@
import sqlite3
import threading
from datetime import datetime
from datetime import datetime, timezone
from types import SimpleNamespace
import pytest
@@ -140,6 +140,18 @@ def test_bind_watermark_passes_through_non_datetime():
assert _bind_watermark("12345") == "12345"
# --- INTEGER µs watermark binding (datetime_columns, 1.12.0) ----------------
def test_bind_watermark_epoch_us_reconstructs_datetime():
dt = datetime(2026, 6, 5, 14, 54, 24, 823000, tzinfo=timezone.utc)
us = int(dt.timestamp() * 1_000_000)
# Whether the watermark is an int or its digit string (it round-trips through
# the TEXT last_synced_at column), it binds back to the same UTC datetime.
assert _bind_watermark(us, epoch_us=True) == dt
assert _bind_watermark(str(us), epoch_us=True) == dt
class _SpyCursor:
def __init__(self, rows):
self._rows = list(rows)
@@ -174,6 +186,46 @@ def test_refresh_binds_watermark_as_datetime(env):
assert params == (datetime(2026, 6, 5, 14, 54, 24, 823000),)
class _RowSource:
"""Returns fixed rows for any query (for loading datetime-typed source data)."""
def __init__(self, rows):
self._rows = rows
def execute(self, sql, params=()):
return _SpyCursor(self._rows)
def test_datetime_column_watermark_stored_as_int_and_bound_back(tmp_path):
"""A change column declared in datetime_columns is stored as INTEGER µs; the
watermark is bound back to a real datetime for the source query."""
cache = CacheManager(
db_path=tmp_path / "c.db",
backup_interval=9999,
datetime_columns={"products": ["changed"]},
)
dt1 = datetime(2026, 6, 1, 10, 0, 0, tzinfo=timezone.utc)
dt2 = datetime(2026, 6, 1, 10, 5, 0, tzinfo=timezone.utc)
cache.load_table("products", ["id", "changed"], _RowSource([("1", dt1), ("2", dt2)]))
cache.create_unique_index("products", ["id"])
cache.set_last_synced_at("products", cache.max_value("products", "changed"))
# Watermark persisted as the max INTEGER µs (digit string out of the TEXT col).
wm = cache.get_last_synced_at("products")
assert wm == str(int(dt2.timestamp() * 1_000_000))
refresher = DeltaRefresher(
cache, {"products": ResolvedDelta("changed", ["id"])}
)
spy = _SpySource(rows=[]) # no new rows — just capture the bound watermark
refresher.refresh(spy)
assert spy.bound, "source query was never issued"
_, params = spy.bound[-1]
assert params == (dt2,) # bound back as datetime, not an int/string
cache.close()
# ---------------------------------------------------------------------------
# Refresh failures are recorded (4.3) so a stuck delta is visible in stats
# ---------------------------------------------------------------------------