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:
34
src/sync/serialize.py
Normal file
34
src/sync/serialize.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Access value serialization helpers.
|
||||
|
||||
Converts pyodbc/Access cell values into JSON-serializable Python types so a
|
||||
captured row dict can be ``json.dumps``-ed before being enqueued on SQL Server.
|
||||
|
||||
Order matters: ``bool`` MUST be checked before ``int`` because ``bool`` is a
|
||||
subclass of ``int`` in Python (``isinstance(True, int) is True``). If the order
|
||||
were reversed, ``True`` would be returned as ``1`` and lose its type.
|
||||
"""
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
|
||||
def to_jsonable(v):
|
||||
"""Coerce a single Access/pyodbc value to a JSON-serializable form.
|
||||
|
||||
- ``None`` -> ``None``
|
||||
- ``bool`` -> ``bool`` (must precede the ``int`` branch)
|
||||
- ``datetime``/``date`` -> ISO-8601 string (``"2026-07-14T08:41:18"``)
|
||||
- ``Decimal`` -> string (preserves exact scale, e.g. ``"12.50"``)
|
||||
- ``int``/``float``/``str`` -> unchanged
|
||||
- anything else (e.g. ``bytes`` for OLE) -> ``str(v)`` fallback
|
||||
"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, bool): # MUST be before int
|
||||
return v
|
||||
if isinstance(v, (datetime.datetime, datetime.date)):
|
||||
return v.isoformat()
|
||||
if isinstance(v, decimal.Decimal):
|
||||
return str(v)
|
||||
if isinstance(v, (int, float, str)):
|
||||
return v
|
||||
return str(v)
|
||||
Reference in New Issue
Block a user