feat: access reader and value serialization

Add to_jsonable() (Access/pyodbc value -> JSON-safe) with strict TDD
(5 tests: datetime iso, bool preserved before int, decimal->str,
none/numbers, dict round-trip). bool branch precedes int because bool
is a subclass of int in Python.

Add AccessReader: reads TableChangeLog (read_log), reads a full source
row by RecordID via to_jsonable (read_row, warns on >4000-char values
that JSON_VALUE would truncate), and deletes processed log IDs in
chunked, retried batches (delete_log_ids, no-op on empty list).
Connects via the ACE ODBC driver from config.access.driver, shared
autocommit mode to coexist with the live .laccdb client.

Integration test against the real 氩弧焊.accdb (read-only; delete_log_ids
only exercised with []) passes.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-07-14 12:22:48 +08:00
parent 65939f5e85
commit 1f7191421a
4 changed files with 211 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
"""Integration test for AccessReader against the real 氩弧焊.accdb.
READ-ONLY: this test never mutates production data. ``delete_log_ids`` is only
exercised with an empty list (a guaranteed no-op) to confirm it does not raise.
No real log rows are deleted.
"""
import os
import pytest
from sync.access_reader import AccessReader
from sync.config import load_config
@pytest.mark.integration
def test_read_log_and_row_and_delete():
if not os.environ.get("RUN_INTEGRATION"):
pytest.skip("needs RUN_INTEGRATION=1")
cfg = load_config("config.yaml")
# Real live database (has a .laccdb lock from the client). Shared-mode
# read via ACE is validated to coexist with the lock.
db = os.environ.get(
"TEST_ACCDB",
r"\\192.168.110.114\生产进度表\2026年数据\氩弧焊.accdb",
)
r = AccessReader(db, cfg.access.driver)
try:
rows = r.read_log(500)
assert isinstance(rows, list)
if rows:
lr = rows[0]
assert lr.table_name and lr.record_id
assert lr.operate_type in ("Insert", "Update", "Delete")
d = r.read_row(lr.table_name, lr.record_id)
assert d is None or "ID" in d
r.delete_log_ids([], 100, 3) # empty list -> no-op, must not raise
finally:
r.close()

21
tests/test_serialize.py Normal file
View File

@@ -0,0 +1,21 @@
import datetime, decimal
from sync.serialize import to_jsonable
import json
def test_datetime_iso():
assert to_jsonable(datetime.datetime(2026,7,14,8,41,18)) == "2026-07-14T08:41:18"
def test_bool_preserved():
assert to_jsonable(True) is True and to_jsonable(False) is False
def test_decimal_to_str():
assert to_jsonable(decimal.Decimal("12.50")) == "12.50"
def test_none_and_numbers():
assert to_jsonable(None) is None
assert to_jsonable(5) == 5
assert to_jsonable("x") == "x"
def test_dict_serializes():
d = {"d": datetime.date(2026,1,1), "b": True, "n": None}
assert json.loads(json.dumps({k: to_jsonable(v) for k,v in d.items()})) == {"d":"2026-01-01","b":True,"n":None}