Misaka_Company 4179d3e232 feat(logging): archive historical logs daily into Archive/
At startup, previously produced logs are relocated into an Archive/
subfolder next to the active log: the project sync.log gets a
-YYYY-MM-DD suffix when archived, and NSSM's nssm_*.log captures are
moved as-is. The log root then only shows the current day's sync.log.
Idempotent handler setup is preserved.

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
2026-07-16 09:24:41 +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%