Misaka_Company d71b7eab62 Migrate sync objects into ProductionDataBaseSync schema + add permanent audit archive
- 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
2026-07-16 12:33:01 +08:00

ProductionDataBaseSync_DataMacro

Access → SQL Server 单向增量同步(数据宏驱动)。把各 Access 库的业务数据周期性同步到 SQL Server 镜像表,作为 Access → SQL Server 迁移期的过渡数据层。

设计详见 docs/superpowers/specs/2026-07-14-access-datamacro-sync-design.md

背景

生产数据实际承载在网络共享下的多个 Access .accdb(按车间/年份分库)。旧机制靠客户端前端 VBA 写变更日志,但 VBA 只在特定表单事件触发,批量改表、直接改表等路径会绕过 → 漏数据。

改用 Access 数据宏(表级引擎触发器,任何写路径必触发):每张业务表挂 After Insert/Update/Delete,变更写入各库本地 TableChangeLog。本程序就是「读各库日志 → 增量同步到 SQL」的搬运器理论上 100% 捕获、对客户端零侵入。

架构

每个 Access 库通过数据宏把变更写入本地 TableChangeLog;同步服务周期性把这些变更搬运到 SQL Server 镜像表。单库一轮分三段:

阶段 动作 说明
Capture SELECT 读取 TableChangeLog 最旧的 N 条I/U 按 RecordID 回读整行 只读 Access
Apply 调用 dbo.usp_SyncApply 写入 SQL 镜像表 ID 精确落库,保序「最后操作胜」
Cleanup DELETE 已应用的 TableChangeLog ID 列表精确删除,遇锁自动退避重试

关键设计点:

  • 无水位线表——Access 日志「应用成功即删」,日志本身就是待处理队列;dbo.SyncQueue 的唯一索引 (SourceFile, SourceTable, SourceLogID) 兜底去重,重复捕获幂等。
  • 保序「最后操作胜」——同一 RecordID 多次操作(先 Insert 后 Delete 等)按日志顺序取最后一条,保证最终态与 Access 一致。
  • 每表一个事务——单表失败只回滚该表,失败行标 error 重试,超限标 dead 待人工。
  • SyncQueue 长期保留作审计/重试日志;applied 行清理后标 cleaned,超保留期再 purge控制表增长。

环境要求

  • Python 3.10+(实测 3.13)。代码用 X | None 等新语法。
  • ODBC 驱动(系统级,非 pip 安装,需预先装好):
    • Microsoft Access Driver (*.accdb, *.mdb)ACE Redist 2016
    • ODBC Driver 17 for SQL Server
  • SQL Server ≥ 2017(存储过程用 STRING_AGG ... WITHIN GROUP)。
  • 执行账号需对目标表有 ALTER 权限(SET IDENTITY_INSERT 要求)。

安装

python -m venv .venv
.venv/Scripts/python.exe -m pip install --upgrade pip
.venv/Scripts/python.exe -m pip install -r requirements.txt

依赖(requirements.txtpyodbcPyYAMLpydanticpytest

SQL 端部署

首次需在目标库建好暂存表与 apply 存储过程(两个脚本都幂等,可重复执行):

sqlcmd -S <SERVER>,1433 -U <USER> -P <PASSWORD> -d <DB> -C -N o -i sql/01_sync_queue.sql
sqlcmd -S <SERVER>,1433 -U <USER> -P <PASSWORD> -d <DB> -C -N o -i sql/02_sync_apply.sql
  • sql/01_sync_queue.sql:建 dbo.SyncQueue + 去重/清理索引 + CleanedAt 列。
  • sql/02_sync_apply.sqldbo.usp_SyncApply 集合化 apply 存储过程。

连接串/凭据以 config.yaml 为准README 不硬编码。

配置

编辑 config.yaml(从 config.example.yaml 复制并填入真实凭据;该文件 gitignored。关键段

  • sql_serverconn_strODBC 连接串)与 sync_queue_table(默认 dbo.SyncQueue)。
  • accessdriverACE 驱动名)与 roots(年份→根目录映射,如 2026: "\\\\srv\\生产进度表\\2026年数据")。
  • runtimepoll_interval_seconds(轮询间隔)、capture_batch_size/apply_batch_size/cleanup_batch_size(各段批大小)、max_retries/retry_backoff_seconds(重试)、cleanup_lock_retriesAccess 锁重试次数)、cleaned_retention_hourscleaned 行保留多久后 purge
  • files:每个 Access 文件一条映射:
    • file / root(对应 access.roots 的 key/ schemaSQL 目标 schema
    • year_suffix:拼到表名后(2026年数据_YEAR20262025年数据/合同表用 "")。
    • exclude_tables / include_tables:排除/包含规则,exclude 优先于 includeTableChangeLog 必须排除。

命令行

统一入口 main.py(仓库根目录),三个功能块都用它调用。配置固定读取同目录的 config.yaml,不在命令中指定:

.venv/Scripts/python.exe main.py fullsync    [--db FILE] [--table NAME] [--clear-change-log]
.venv/Scripts/python.exe main.py incremental [--loop] [--poll-interval N]
.venv/Scripts/python.exe main.py compare     [--granularity count|ids] [--db FILE] [--table NAME] [--report PATH]
子命令 说明 退出码
fullsync 一次性全量同步TRUNCATE + 批量 INSERT绕开增量队列。 0
incremental 增量同步一轮capture→apply→cleanup--loop 切持续轮询(服务模式)。 0
compare 数据一致性核对:默认行数总量,--granularity ids 精确到 ID 集合差异。 全一致 0 / 有不一致 1

三个块共用同一份 config.yaml,目标表集合完全一致(由 sync.targets 统一解析)。main.py 在根目录、自行把 src/ 加入 sys.path,无需 -m、无需设环境变量;控制台强制 UTF-8中文表名不乱码。

旧入口 -m sync.service / -m sync.fullsync 保留为兼容,行为不变。

全量同步

用于从零重建镜像表或修复 Access 与 SQL 之间的漂移。会清空目标表再全量写入,绕开增量队列:

.venv/Scripts/python.exe main.py fullsync                       # 全部库、全部表
.venv/Scripts/python.exe main.py fullsync --db OEM.accdb        # 仅单个库
.venv/Scripts/python.exe main.py fullsync --table 表壳焊接记录   # 仅单表(作用于所有库)
.venv/Scripts/python.exe main.py fullsync --clear-change-log    # 同步后同时清空 TableChangeLog谨慎
  • year_suffix 通过 FileMapping 拼到表名后(如 表壳焊接记录表壳焊接记录_YEAR2026)。
  • 写入时 SET IDENTITY_INSERT ON,保留 Access 原 ID保证后续增量的 RecordID 匹配不错位。
  • 无镜像表的目标表按设计跳过(target table missing),不报错。

增量同步

以 Access 数据宏日志为唯一变更源,每轮跑一遍 capture → apply → cleanup三段说明见上面「架构」。这是主用模式,生产上常驻运行。两种调用方式:

.venv/Scripts/python.exe main.py incremental                              # 跑一轮就退出(手动/按需补跑)
.venv/Scripts/python.exe main.py incremental --loop                       # 持续轮询(服务模式,不退出)
.venv/Scripts/python.exe main.py incremental --loop --poll-interval 30    # 覆盖 runtime.poll_interval_seconds
  • 生产环境以 nssm 服务 DataMacroSync 常驻(即 --loop 模式见下文「NSSM 服务」;手动单轮适合验证或临时补跑积压。
  • --loop 持续轮询直到进程被停(nssm stop 或 Ctrl+C默认单轮跑完即退出。
  • 单轮一次最多处理每库 capture_batch_size 条日志;积压多时连续跑几轮或用 --loop 直到清空。
  • 每个文件的 capture/cleanup 独立隔离单文件失败不影响其它apply 失败不阻塞 cleanup失败行 error 下轮重试、超 max_retriesdead 待人工。
  • 幂等:SyncQueue 唯一索引去重,重复 capture、中断续跑都不会重写或漏写。
  • 与全量同步共用同一份 config.yamlsync.targets,目标表集合完全一致;全量是「从零重建」的补充手段,不替代增量。

数据对比

核对 Access 源表与 SQL 镜像表是否一致。两种粒度:

  • 行数总量(默认):逐表比对 COUNT(*)
  • ID 集合--granularity ids):逐表比对两边 ID 集合报告「Access 有 / SQL 无」与「SQL 有 / Access 无」的 ID每表前 50 个 + 总数)。
.venv/Scripts/python.exe main.py compare                                       # 全部库、全部表,行数总量
.venv/Scripts/python.exe main.py compare --db 氩弧焊.accdb                      # 仅单个库
.venv/Scripts/python.exe main.py compare --granularity ids --table 表壳焊接记录  # 单表 ID 级
.venv/Scripts/python.exe main.py compare --report report.txt                    # 同时写入报告文件UTF-8
  • 无镜像表按设计跳过([SKIPPED no mirror]),不计为不一致——这类表多半是该排除却没排除(如 *_停 停用表、USysApplicationLog),可作为配置清理的线索。
  • 任一表不一致时退出码 1(便于脚本化);全部一致为 0
  • 实时增量同步存在秒级延迟窗口,刚写入 Access 的行可能尚未到 SQL属正常稍后再核或对照 SyncQueue 的 pending 行)。

NSSM 服务114

增量同步在 host 114 上以 nssm 服务 DataMacroSync 常驻运行。常用操作(经 ssh 114

ssh 114 "nssm status DataMacroSync"     # 查状态SERVICE_RUNNING / SERVICE_STOPPED
ssh 114 "nssm stop DataMacroSync"       # 停
ssh 114 "nssm start DataMacroSync"      # 起
ssh 114 "nssm restart DataMacroSync"    # 重启
ssh 114 "nssm list"                     # 列出所有 nssm 服务

测试

.venv/Scripts/python.exe -m pytest                       # 仅单元测试(默认)
RUN_INTEGRATION=1 .venv/Scripts/python.exe -m pytest     # 含集成测试(需能连真实 Access + SQL Server
  • 单元测试用 mock不依赖数据库集成测试@pytest.mark.integration)连 config.yaml 里的真实库,且自带清理。
  • pyproject.toml 仅用于配置 pytestpythonpath = ["src", "."]testpathsintegration 标记)。

项目结构

main.py                       统一命令行入口fullsync / incremental / compare
config.yaml                   真实配置gitignoredconfig.example.yaml 是模板
requirements.txt              依赖
pyproject.toml                pytest 配置
sql/
  01_sync_queue.sql           dbo.SyncQueue 建表 + 索引(幂等)
  02_sync_apply.sql           dbo.usp_SyncApply 存储过程
src/sync/
  config.py                   Pydantic 配置模型 + load_config
  targets.py                  共享目标表解析exclude/include全量/增量/对比共用)
  serialize.py                Access 值 → JSON 可序列化
  access_reader.py            读 Access日志/整行/计数/ID/删除日志)
  sql_writer.py               写 SQLSyncQueue/apply/计数/ID/全量灌表)
  capture.py                  增量编排:读日志→回读整行→入队
  cleanup.py                  清理编排:回删已应用日志
  service.py                  主循环 cycle() / run()
  fullsync.py                 一次性全量同步
  compare.py                  数据一致性对比count / ids
  logging_setup.py            日志配置(滚动文件 + 控制台)
tests/                        单元 + 集成测试
docs/superpowers/             设计文档与实现计划

常见问题

  • cleanup 报 -1102 无法更新;当前被锁定Access 是文件型数据库cleanup 反写 DELETE 与生产客户端数据宏写日志争用页级锁。服务已对锁冲突自动退避重试(access_reader.delete_log_ids 捕获 pyodbc.Error 并判断 -1102/「被锁定」)。偶发属正常,持续刷错再排查。
  • No module named 'pydantic_core' / pyodbcvenv 解释器与轮子 ABI 不匹配(常见于 Python 3.13 装到 cp310 轮子)。修复:.venv/Scripts/python.exe -m pip install --force-reinstall --no-cache-dir pyodbc pydantic
  • compare/fullsync 报 [SKIPPED no mirror] / target table missing:该表在 Access 里但 SQL 端没有镜像表(多为 *_停 停用表、USysApplicationLog 等系统表,或尚未建镜像的新表)。若是该停用的表,加进对应 exclude_tables;若该同步,先在 SQL 建表再 fullsync。
  • SyncQueue 出现 error/deaderror 会在下轮自动重试(未超 max_retriesdead 是超限放弃,需人工看 ErrorMsg 排查后处理。
  • UserWarning: Field name "schema" ... shadows ... BaseModelFileMapping.schema 字段名与 Pydantic 基类属性重名,仅告警、不影响功能。
  • 控制台中文乱码main.py 已强制 stdout/stderr 为 UTF-8若仍乱码设环境变量 PYTHONIOENCODING=utf-8,或用 compare --report 输出 UTF-8 文件。
Description
Access to SQL Server incremental sync, data-macro driven
Readme 209 KiB
Languages
Python 83.8%
TSQL 16.1%
Batchfile 0.1%