Wire datetime_columns through query params and reads; add db_size and vacuum guard
This commit is contained in:
+131
-1
@@ -4,9 +4,17 @@ import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlmem._coerce import coerce_params, to_sqlite, to_sqlite_datetime
|
||||
from sqlmem._coerce import (
|
||||
coerce_params,
|
||||
from_sqlite_datetime,
|
||||
reverse_coerce_rows,
|
||||
to_sqlite,
|
||||
to_sqlite_datetime,
|
||||
)
|
||||
from sqlmem.cache import CacheManager
|
||||
|
||||
_UTC = datetime.timezone.utc
|
||||
|
||||
|
||||
class _FakeCursor:
|
||||
def __init__(self, rows):
|
||||
@@ -165,6 +173,128 @@ def test_non_datetime_columns_unaffected_by_datetime_columns(tmp_path):
|
||||
c.close()
|
||||
|
||||
|
||||
# --- param coercion for datetime_columns (A) --------------------------------
|
||||
|
||||
|
||||
def test_coerce_params_dt_table_iso_string_to_epoch():
|
||||
p = coerce_params(("2026-06-01T10:00:00",), dt_table=True)
|
||||
assert p == (to_sqlite_datetime("2026-06-01T10:00:00"),)
|
||||
|
||||
|
||||
def test_coerce_params_dt_table_datetime_to_epoch():
|
||||
dt = datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
assert coerce_params((dt,), dt_table=True) == (to_sqlite_datetime(dt),)
|
||||
|
||||
|
||||
def test_coerce_params_dt_table_false_keeps_iso_string():
|
||||
# No datetime table in the query → behaviour unchanged (string stays a string).
|
||||
assert coerce_params(("2026-06-01T10:00:00",), dt_table=False) == (
|
||||
"2026-06-01T10:00:00",
|
||||
)
|
||||
|
||||
|
||||
def test_coerce_params_dt_table_leaves_non_datetime_values():
|
||||
assert coerce_params(("hello", 5, None), dt_table=True) == ("hello", 5, None)
|
||||
|
||||
|
||||
def test_where_on_datetime_column_matches_with_iso_param(tmp_path):
|
||||
"""The critical fix: a WHERE on an INTEGER-µs column with an ISO string param
|
||||
must match instead of comparing INTEGER against TEXT (always 0 rows)."""
|
||||
c = CacheManager(
|
||||
db_path=tmp_path / "cache.db",
|
||||
backup_interval=9999,
|
||||
datetime_columns={"t": ["changed"]},
|
||||
return_datetime=False,
|
||||
)
|
||||
rows = [
|
||||
("1", datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)),
|
||||
("2", datetime.datetime(2026, 6, 3, 10, 0, 0, tzinfo=_UTC)),
|
||||
]
|
||||
c.load_table("t", ["id", "changed"], FakeSource(rows))
|
||||
_, out = c.execute_in_memory(
|
||||
"SELECT id FROM t WHERE changed > ?", ("2026-06-02T00:00:00",), ["t"]
|
||||
)
|
||||
assert [r[0] for r in out] == ["2"]
|
||||
c.close()
|
||||
|
||||
|
||||
def test_where_on_datetime_column_without_table_scope_is_unchanged(tmp_path):
|
||||
"""Without table scope the param isn't coerced — proves the fix is scoped."""
|
||||
c = CacheManager(
|
||||
db_path=tmp_path / "cache.db",
|
||||
backup_interval=9999,
|
||||
datetime_columns={"t": ["changed"]},
|
||||
return_datetime=False,
|
||||
)
|
||||
c.load_table(
|
||||
"t",
|
||||
["id", "changed"],
|
||||
FakeSource([("1", datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC))]),
|
||||
)
|
||||
# No `tables` arg → INTEGER vs TEXT comparison → no match (legacy behaviour).
|
||||
_, out = c.execute_in_memory("SELECT id FROM t WHERE changed > ?", ("2026-01-01T00:00:00",))
|
||||
assert out == []
|
||||
c.close()
|
||||
|
||||
|
||||
# --- reverse coercion: read back as datetime (B) ----------------------------
|
||||
|
||||
|
||||
def test_from_sqlite_datetime_roundtrip():
|
||||
dt = datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
assert from_sqlite_datetime(to_sqlite_datetime(dt)) == dt
|
||||
|
||||
|
||||
def test_from_sqlite_datetime_passes_non_int():
|
||||
assert from_sqlite_datetime("x") == "x"
|
||||
assert from_sqlite_datetime(None) is None
|
||||
assert from_sqlite_datetime(True) is True # bool is not treated as µs
|
||||
|
||||
|
||||
def test_reverse_coerce_rows_only_named_columns():
|
||||
us = to_sqlite_datetime(datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC))
|
||||
out = reverse_coerce_rows([("1", us)], ["id", "changed"], {"changed"})
|
||||
assert out[0][0] == "1"
|
||||
assert out[0][1] == datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
|
||||
|
||||
def test_read_returns_datetime_by_default(tmp_path):
|
||||
c = CacheManager(
|
||||
db_path=tmp_path / "cache.db",
|
||||
backup_interval=9999,
|
||||
datetime_columns={"t": ["changed"]},
|
||||
)
|
||||
dt = datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
c.load_table("t", ["id", "changed"], FakeSource([("1", dt)]))
|
||||
_, out = c.execute_in_memory("SELECT id, changed FROM t", None, ["t"])
|
||||
assert out == [("1", dt)] # returned as a datetime, not the raw int
|
||||
c.close()
|
||||
|
||||
|
||||
def test_return_datetime_false_keeps_raw_int(tmp_path):
|
||||
c = CacheManager(
|
||||
db_path=tmp_path / "cache.db",
|
||||
backup_interval=9999,
|
||||
datetime_columns={"t": ["changed"]},
|
||||
return_datetime=False,
|
||||
)
|
||||
dt = datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
c.load_table("t", ["id", "changed"], FakeSource([("1", dt)]))
|
||||
_, out = c.execute_in_memory("SELECT changed FROM t", None, ["t"])
|
||||
assert out == [(to_sqlite_datetime(dt),)] # raw INTEGER µs
|
||||
c.close()
|
||||
|
||||
|
||||
# --- public export (F) ------------------------------------------------------
|
||||
|
||||
|
||||
def test_datetime_to_epoch_us_is_public():
|
||||
from sqlmem import datetime_to_epoch_us
|
||||
|
||||
dt = datetime.datetime(2026, 6, 1, 10, 0, 0, tzinfo=_UTC)
|
||||
assert datetime_to_epoch_us(dt) == to_sqlite_datetime(dt)
|
||||
|
||||
|
||||
# --- integration: values reach the cache through coercion -------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user