- Add sql/00_schema.sql: create dedicated ProductionDataBaseSync schema (idempotent) - Move SyncQueue and usp_SyncApply from dbo into ProductionDataBaseSync - Add sql/03_sync_log_archive.sql: permanent, append-only SyncLogArchive that records both OriginalOperateType and ProcessedOperateType plus the Access log OriginalTime, so pipeline divergences (e.g. Insert applied as Delete) stay reconstructible forever (SyncQueue is transient and only keeps processed type) - config.py: inject sync_queue_table / archive_table / apply_proc (default to the new schema); SqlWriter takes these names instead of hardcoding dbo - sql_writer.py: add ArchiveRow + insert_archive_row (dedup on source keys), parametrize queue/archive/proc names throughout - capture.py: archive every consumed log row before enqueue (preserves evidence before cleanup deletes the Access log) - service.py: pass the three names into SqlWriter - tests: read queue/proc names from config instead of hardcoding dbo.SyncQueue
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
"""Integration test for usp_SyncApply.
|
|
|
|
Validates: IDENTITY-preserving INSERT, last-write-wins UPDATE, BIT conversion
|
|
from JSON ``true``/``false``, and DELETE of the last op. Creates a throwaway
|
|
schema ``sync_test`` and table ``ApplyDemo_YEAR2026`` and cleans them up at the
|
|
end so no residue is left on CompanyDB.
|
|
|
|
Queue table and apply proc names are read from ``config.yaml`` so the test
|
|
follows whichever schema the deployment targets (currently ProductionDataBaseSync).
|
|
"""
|
|
import pytest
|
|
|
|
from sync.config import load_config
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_upsert_insert_update_delete_last_write_wins(sql_conn):
|
|
cfg = load_config("config.yaml")
|
|
qt = cfg.sql_server.sync_queue_table
|
|
proc = cfg.sql_server.apply_proc
|
|
cur = sql_conn.cursor()
|
|
sch, tbl = "sync_test", "ApplyDemo_YEAR2026"
|
|
|
|
# --- Arrange: fresh target table + empty queue -------------------------
|
|
cur.execute("IF SCHEMA_ID('sync_test') IS NULL EXEC('CREATE SCHEMA sync_test')")
|
|
cur.execute(
|
|
"IF OBJECT_ID('sync_test.ApplyDemo_YEAR2026') IS NOT NULL "
|
|
"DROP TABLE sync_test.ApplyDemo_YEAR2026"
|
|
)
|
|
cur.execute(
|
|
"CREATE TABLE sync_test.ApplyDemo_YEAR2026 ("
|
|
"ID INT IDENTITY(1,1) PRIMARY KEY, "
|
|
"名字 NVARCHAR(255) NULL, "
|
|
"数量 INT NULL, "
|
|
"时间 DATETIME2 NULL, "
|
|
"标记 BIT NULL)"
|
|
)
|
|
cur.execute("DELETE " + qt + " WHERE TargetSchema='sync_test'")
|
|
|
|
# Insert then a later Update for the same RecordID=1 -> last write wins.
|
|
cur.execute(
|
|
"INSERT " + qt + "(SourceFile,SourceTable,SourceLogID,TargetSchema,"
|
|
"TargetTable,RecordID,OperateType,RowData,Status) "
|
|
"VALUES('t.accdb','ApplyDemo',1,'sync_test','ApplyDemo_YEAR2026','1',"
|
|
"'Insert','{\"ID\":1,\"名字\":\"A\",\"数量\":3,\"时间\":\"2026-01-01T00:00:00\",\"标记\":true}','pending')"
|
|
)
|
|
cur.execute(
|
|
"INSERT " + qt + "(SourceFile,SourceTable,SourceLogID,TargetSchema,"
|
|
"TargetTable,RecordID,OperateType,RowData,Status) "
|
|
"VALUES('t.accdb','ApplyDemo',2,'sync_test','ApplyDemo_YEAR2026','1',"
|
|
"'Update','{\"ID\":1,\"名字\":\"A2\",\"数量\":5,\"时间\":\"2026-01-02T00:00:00\",\"标记\":false}','pending')"
|
|
)
|
|
sql_conn.commit()
|
|
|
|
# --- Act 1: apply upsert ----------------------------------------------
|
|
cur.execute("EXEC " + proc + " @MaxRetries=5")
|
|
sql_conn.commit()
|
|
|
|
# --- Assert 1: the later Update wins; BIT false -> 0 ------------------
|
|
cur.execute("SELECT 名字,数量,标记 FROM sync_test.ApplyDemo_YEAR2026 WHERE ID=1")
|
|
row = cur.fetchone()
|
|
assert row is not None, "expected ID=1 to exist after upsert"
|
|
assert row.名字 == "A2"
|
|
assert row.数量 == 5
|
|
assert row.标记 == 0 # BIT false
|
|
|
|
# --- Act 2: a later Delete wins ---------------------------------------
|
|
cur.execute("DELETE " + qt + " WHERE TargetSchema='sync_test'")
|
|
cur.execute(
|
|
"INSERT " + qt + "(SourceFile,SourceTable,SourceLogID,TargetSchema,"
|
|
"TargetTable,RecordID,OperateType,RowData,Status) "
|
|
"VALUES('t.accdb','ApplyDemo',3,'sync_test','ApplyDemo_YEAR2026','1',"
|
|
"'Delete',NULL,'pending')"
|
|
)
|
|
sql_conn.commit()
|
|
cur.execute("EXEC " + proc + " @MaxRetries=5")
|
|
sql_conn.commit()
|
|
|
|
# --- Assert 2: row removed --------------------------------------------
|
|
cur.execute("SELECT COUNT(*) FROM sync_test.ApplyDemo_YEAR2026 WHERE ID=1")
|
|
assert cur.fetchone()[0] == 0
|
|
|
|
# --- Cleanup ----------------------------------------------------------
|
|
cur.execute(
|
|
"IF OBJECT_ID('sync_test.ApplyDemo_YEAR2026') IS NOT NULL "
|
|
"DROP TABLE sync_test.ApplyDemo_YEAR2026"
|
|
)
|
|
cur.execute("DELETE " + qt + " WHERE TargetSchema='sync_test'")
|
|
sql_conn.commit()
|