- 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
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.txt):pyodbc、PyYAML、pydantic、pytest。
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.sql:dbo.usp_SyncApply集合化 apply 存储过程。
连接串/凭据以
config.yaml为准,README 不硬编码。
配置
编辑 config.yaml(从 config.example.yaml 复制并填入真实凭据;该文件 gitignored)。关键段:
sql_server:conn_str(ODBC 连接串)与sync_queue_table(默认dbo.SyncQueue)。access:driver(ACE 驱动名)与roots(年份→根目录映射,如2026: "\\\\srv\\生产进度表\\2026年数据")。runtime:poll_interval_seconds(轮询间隔)、capture_batch_size/apply_batch_size/cleanup_batch_size(各段批大小)、max_retries/retry_backoff_seconds(重试)、cleanup_lock_retries(Access 锁重试次数)、cleaned_retention_hours(cleaned行保留多久后 purge)。files:每个 Access 文件一条映射:file/root(对应access.roots的 key)/schema(SQL 目标 schema)。year_suffix:拼到表名后(2026年数据用_YEAR2026,2025年数据/合同表用"")。exclude_tables/include_tables:排除/包含规则,exclude 优先于 include。TableChangeLog必须排除。
命令行
统一入口 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_retries标dead待人工。 - 幂等:
SyncQueue唯一索引去重,重复 capture、中断续跑都不会重写或漏写。 - 与全量同步共用同一份
config.yaml和sync.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仅用于配置 pytest(pythonpath = ["src", "."]、testpaths、integration标记)。
项目结构
main.py 统一命令行入口(fullsync / incremental / compare)
config.yaml 真实配置(gitignored);config.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 写 SQL(SyncQueue/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'/pyodbc:venv 解释器与轮子 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/dead行:error会在下轮自动重试(未超max_retries);dead是超限放弃,需人工看ErrorMsg排查后处理。UserWarning: Field name "schema" ... shadows ... BaseModel:FileMapping.schema字段名与 Pydantic 基类属性重名,仅告警、不影响功能。- 控制台中文乱码:
main.py已强制 stdout/stderr 为 UTF-8;若仍乱码,设环境变量PYTHONIOENCODING=utf-8,或用compare --report输出 UTF-8 文件。