Conversation
GetTableColumns / GetTableColumnsWithTypes / GetTablePrimaryKeys 在 rows.Next() 循环后直接返回 nil error。rows.Next() 遇网络中断、包解码 错误或服务端提前关闭结果集时返回 false,错误只能从 rows.Err() 取到, 漏检会把"结果集被截断"当成"正常读完"。 两处后果都是静默的,且现有校验抓不到: - 列清单截断 → sync_data.go 用它构造 SELECT 与 CopyFrom 的 copyColumns → 少迁几列、零错误零告警,行数校验只比 COUNT(*) 照样通过 - 复合主键 (a,b,c) 截断成 (a,b) → len==2 通过"没有主键"空检查 → keyset 的 WHERE (a,b) > (?,?) 游标不再唯一 → 跨批次漏行+重复行, 而漏 N 行与重 N 行在 COUNT(*) 下可能刚好抵消,报告"数据一致" GetTablePrimaryKeys 的检查置于 len(primaryKeys)==0 判断之前, 否则部分截断会被空检查放过。 配套新增 connection_rows_err_test.go:用标准库 database/sql/driver 实现最小假 driver 模拟迭代中途出错,不引入 go-sqlmock 以保持 go.mod 精简。已验证测试有效性——移除修复后三个断言全部转红。 Closes #170
syncSingleTable 中「空表早返回」位于「TRUNCATE 目标表」之前,而
handleEmptyTable 内部只做 log、序列回填与行数校验,不执行 truncate。
触发前提是必然导致迁移终止,而非概率性:MySQL 侧空表 + PG 侧同名表
仍有上一轮数据 + truncate_before_sync=true
→ 空表分支早返回,truncate 永不执行
→ handleEmptyTable 得到 MySQL 0 行 vs PostgreSQL N 行
→ evaluateRowCountValidation 在 truncate=true 时返回
"数据校验不一致 (truncate_before_sync=true,终止迁移)"
错误信息指向"数据不一致",真实原因却是工具自己漏做了 truncate,
会把排查引向源库为什么"少数据"。重跑迁移(PG 表已存在且含上一轮数据、
MySQL 侧某表刚好被清空)很容易踩到。
修复:把 TRUNCATE 块移到空表早返回之前,并顺带把「取消检查」提到
TRUNCATE 之前,避免已取消仍执行 DDL。修复后顺序:
取消检查 → TRUNCATE → 空表早返回 → 分页插入。
注:issue #171 正文引用了一处代码中不存在的注释("即使表为空也执行"),
该引用有误,缺陷本身成立,详见 issue 讨论。
测试:truncateTable / handleEmptyTable 接收具体类型 *postgres.Connection
而非接口,无法在不引入接口抽象的前提下用替身断言执行顺序,故本次以
代码审查 + 现有测试不回归为准(build / vet / 全量测试通过)。
Closes #171
全仓库此前没有任何 recover(),而两类并发 worker 都在处理来自外部数据库
的任意数据:sync_data.go 的表级同步 worker(MySQL 行 → typedDest →
pgx.CopyFrom)与 manager.go 的 runBatchStage 批次 worker(8 个转换阶段的
通用派发器)。
数据层是最容易 panic 的地方:切片越界、类型断言失败、nil map 写入、
getRowSlice 的 (*s)[:numCols] 都在热路径上。未 recover 的 panic 会:
- crash 整个迁移进程,而非只让这一张表失败,此时目标表可能正处于
「已 TRUNCATE + 半截数据」状态且无法继续
- 跳过 errorChan 写入,聚合错误列表里完全看不到这次失败
- 不留下「哪张表、哪一批、什么堆栈」的上下文
两处 defer 内加 recover(),转成带对象名/阶段名与 debug.Stack() 的 error
送入各自 errorChan。recover 置于 defer 开头,确保后续资源释放语句自身
异常时错误已入队。manager.go 一处修改即覆盖全部 8 个转换阶段。
测试:runBatchStage 只依赖 m.conversionStats,可用 &Manager{} 零值直接
单测。在既有 TestRunBatchStage 中新增子测试(不新建文件、复用 drainErrors),
断言 panic 被转成 error、错误信息含阶段名/panic 值/堆栈、其余批次照常完成、
阶段统计仍记录。已验证有效性——移除 recover 后测试进程直接崩溃。
sync_data.go 的表级 worker 依赖具体类型 *mysql.Connection /
*postgres.Connection,需接口抽象后才能单测,本次以代码审查 +
现有测试不回归为准(build / vet / race / 全量测试通过)。
Closes #172
批次 context 用 context.WithoutCancel(ctx) 派生,该 ctx 既无 deadline 也不可取消。剥离取消信号本身是正确的(让已开启的批次能完整提交), 但没有补回任何超时。 驱动层证据:go-sql-driver@v1.7.1/connection.go:592-595 的 watchCancel 在 ctx.Done() == nil 时直接返回、不启动 watcher,而 WithoutCancel 的 Done() 正是 nil;中断阻塞 read 依赖的恰是该 watcher(connection.go:620-622 的 <-ctx.Done() → mc.cancel → cleanup 关闭 netConn)。 后果:网络半开或对端 hang 时 goroutine 永久阻塞在 socket read,永久占住 semaphore 槽位,wg.Wait() 永不返回,且根 ctx 已被剥离导致 Ctrl-C 无效 (只能 kill -9),此时目标表可能正处于「已 TRUNCATE + 半截数据」状态。 对比 mysql/metadata.go:176 已用 WithTimeout(120s),数据热路径漏了。 修复:抽出 buildTableSyncContext 纯函数,WithoutCancel 之后套 WithTimeout; paginateAndInsert 每轮增加超时检查并给出可操作的错误提示; config 新增 conversion.limits.table_sync_timeout_seconds(默认 3600)。 与 issue #173 正文的三处实施调整: 1. 配置项由 batch_timeout_seconds 改为 table_sync_timeout_seconds,并在 循环外创建一次而非逐批创建——流式读取的 rows 跨批次复用且绑定首轮 context,逐批 cancel 会关闭其底层连接导致后续批次读取失败。 2. 不提供 0=不限制的逃生口(那等于保留原缺陷),<=0 一律回落默认 3600, 与本结构体其余字段语义一致;超大表可显式配置更大值。 3. 不强制注入 DSN 的 readTimeout/writeTimeout:WithTimeout 已足以让驱动 watcher 生效,而强制注入会改变现有行为,可能让大表 COUNT(*) (服务端算完才返回首个 packet)意外失败,属独立取舍。 测试:buildTableSyncContext 的 Done() 非 nil、deadline 单位换算、 根取消不穿透、<=0 退化行为;ValidateConfig 的默认值回落(0/负数/显式值)。 两个正向断言与退化分支的 Done()==nil 断言互为对照,可捕获 WithTimeout 被误写回 WithoutCancel 的回归。build / vet / gofmt / 全量测试 / race 通过。 Closes #173
字段注释原断言「写入发生在同步 worker 启动前与全部结束后,不存在并发读写」,
该断言不成立:写入点位于 syncTableData 函数体内部,而它是 runBatchStage 的
stageFn,后者为每个批次起一个独立 goroutine。211 表 / max_ddl_per_batch=10
约 22 个 goroutine 并发写同一字段,同时 Log/logError 在其他 goroutine 中读。
两个后果:
1. Log/logError 的 `if m.progressLineGuard != nil { m.progressLineGuard.endLine() }`
是 check-then-use,两行之间被 Store(nil) 就会解引用 nil——endLine 首行即
p.mu.Lock()。提出本 issue 时全仓库 recover() 为零命中,该 panic 会直接
终止迁移进程(已由 #172 兜住,但根因仍在)。
2. go test -race 必报 DATA RACE,但 CI 的 -race 只覆盖单元测试,集成步骤用
裸 go build,因此结构性不可见。
修复:字段改为 atomic.Pointer[progressPrinter];两处读取改为先 Load 到局部
变量再判空调用,消除 check-then-use 窗口;修正字段注释。
本次不解决「多批各自的 printer 互相覆盖、争抢 stdout」的功能性问题——那需要
把 printer/progressChan 提升为 Manager 级单例并统一管理消费者 goroutine
生命周期,属独立结构性重构。
测试:TestProgressLineGuardConcurrentAccess(8 写 + 8 读 goroutine × 300 轮)
与 TestProgressLineGuardUsedByLog(断言 Log/logError 确实通过 endLine 复位
dirty,证明读写路径被真实覆盖而非空跑)。
已验证有效性:用普通指针复现修复前写法的临时 sanity 测试在 -race 下报
4 次 WARNING: DATA RACE 并 FAIL,改为 atomic.Pointer 后同一并发模式通过。
build / vet / gofmt / 全量测试 / race 均通过。
Closes #174
问题一:快照事务被多 goroutine 共用
consistent_snapshot=true 时 Connection 上只有一个 snapshotTx *sql.Tx,
querier() 对所有数据读取(GetTableData / QueryTableRows / 两个 keyset 分页 /
GetTableRowCount)返回同一个 Tx,而 semaphore 容量 = Concurrency(默认 10)。
一个 *sql.Tx 绑定一条 MySQL 连接,且标准库不串行化同一 Tx 上的并发查询——
sql.go:2245-2263 的 Tx.grabConn 只持 closemu.RLock(),该锁的注释写明用途是
「prevents the transaction from closing while there is an active query」,
读锁允许多 goroutine 同时持有。并发查询落到同一 packet buffer,
go-sql-driver@v1.7.1/buffer.go:136/158/169/177 在 b.length > 0 时返回
ErrBusyBuffer(注释三次重复 "Only one buffer (total) can be used at a time")。
isTransientConnError 又把 ErrBusyBuffer 判为可重试,而 retryOnTransientConn
的注释假设「重新发起查询即可由连接池换新连接执行」——在 Tx 上该前提不成立
(Tx 绑定固定连接),于是白白重试 2 次后整表失败,报出误导性的 busy buffer。
修复:ValidateConfig 增加互斥校验,放在 Concurrency 默认值回落之后以免误判。
问题二:隔离级别未显式指定
BeginTx(ctx, nil) 用服务端默认隔离级别,而 MySQL 的 WITH CONSISTENT SNAPSHOT
只在 REPEATABLE READ 下建立快照,在 READ COMMITTED 下被忽略并仅产生 warning。
源库为减少 gap lock 配成 RC 并不罕见,此时快照静默失效,用户却看到
manager.go:522 的「数据读取不受源库并发写入影响」。
修复:BeginTx 传 &sql.TxOptions{Isolation: LevelRepeatableRead, ReadOnly: true}。
更正审查过程中的一处判断:曾认为随后那条 START TRANSACTION WITH CONSISTENT
SNAPSHOT 与 BeginTx 冗余应删除——这是错的,本次保留。BeginTx 只设定隔离级别,
InnoDB 的一致性读快照在首次读取时才建立,各表首次读取时间点不同则跨表不一致;
该语句让快照在事务开始即刻建立。已在代码注释中写明,避免后来者再次「优化」掉。
回归风险核查:CI 配置、config.example.yml、集成测试脚本均未开启
consistent_snapshot(默认 false),新校验不影响现有流程。已更新
config.example.yml 该项注释说明互斥关系与隔离级别自动设置。
测试:互斥校验的 5 种组合(含「未配置并发时回落默认 1 不应误判」,
用于锁定校验位置在默认值回落之后)。已验证有效性——移除校验后
「快照开启 + 高并发必须拒绝」用例 FAIL。BeginConsistentSnapshot 依赖真实
MySQL 连接,以代码审查 + 现有测试不回归为准。
Closes #175
BatchInsertDataWithCompositeKeys 末尾曾在 lastValue 为 nil 时,于本 PG 事务上 执行 SELECT MAX(主键) FROM 目标表,并把结果当作下一轮 MySQL keyset 游标返回。 用目标库状态充当源库读取游标在设计上是错的。 完整追踪调用链后确认当前不可达,故定级为低(详见 issue #176): - 复合主键路径(sync_data.go:575)读取用 compositeLastValues,而 lastValue 仅在 len(primaryKeyIndexes)==1 时赋值,故此路径下 MAX 结果从不被使用; - 单主键路径经 BatchInsertDataWithTransactionAndGetLastValue(connection.go:950) 委托进来,:988-1009 用 EqualFold 定位后 resolvedPrimaryKeys 取自 copyColumns[i], 故 :1012-1019 的精确匹配必然成功,有数据时 lastValue 正常赋值、MAX 不触发; - 唯一触发场景是「本批 0 行」,而 currentBatchSize==0 会让 sync_data.go:594-597 立即 break,结果同样不被使用。 仍应删除的三个理由: 1. 复合主键场景每批一次无用的 PG 查询; 2. 注释称其为「后备方案」,误导读者以为存在跨库游标兜底能力; 3. 未来陷阱——一旦有人改动 :1048-1050 的赋值条件或让 :1003-1009 的 fallback 变得可达(如列名大小写策略调整),立刻变成真实的数据损坏: skip_existing_tables=true 且目标表已有数据时,PG 的 MAX 大于 MySQL 已读位置 会导致中间整段数据静默丢失,小于则重复读取撞主键,而 validateData 只比 COUNT(*),漏行与重行可能刚好抵消、报告仍显示「数据一致」。 在返回处留注释说明游标的唯一合法来源,以及不可用时应退回 OFFSET 分页并告警、 绝不应查询目标库。删除后 SELECT MAX 在本文件仅剩 :780 的序列回填(正确用法)。 行为无变化,现有测试未覆盖该分支。build / vet / gofmt / 全量测试通过。 Closes #176
bit(n) 已正确映射为 BIGINT(bit(64) 为 NUMERIC(20,0)),但 DEFAULT 子句中的
MySQL 位字面量 b'0101' 完全未处理,原样透传给 PostgreSQL。PG 中 b'...' 是
bit 类型字面量,赋给 BIGINT 列会因类型不匹配被拒绝。
实证(本地真实 PostgreSQL):
修复前 → "flags" BIGINT default b'0'
ERROR: 42804: column "flags" is of type bigint but default
expression is of type bit
修复后 → "flags" BIGINT default 0
CREATE TABLE 成功,且 INSERT ... DEFAULT VALUES RETURNING
得到 0|1|10 —— 默认值的数值语义也正确(b'1010' = 10),
不只是让语法通过
处理逻辑此前完全缺失:grep "b'0'|b'1'|bitLiteral" internal/ 零命中。
cleanTypeDefinition 只映射类型、清理字符集与零日期默认值,不涉及位字面量。
真实可达:MySQL 对带默认值的 bit 列,SHOW CREATE TABLE 输出的正是
DEFAULT b'0' 形态。CI 不可见的原因是 create_table.sql 中 grep 不到任何
bit ... DEFAULT b'...' 用例——case_64_bit_types 与 case_190_bit_full
都只有裸 bit 列无默认值,转换成功从而掩盖了该缺陷。
修复:新增包级正则 reBitLiteralDefault = (?i)(\bdefault\s+)b'([01]+)',
用 strconv.ParseUint(bin, 2, 64) 转十进制后回填。
- 限定在 default 之后,避免误伤其他位置;
- cleanTypeDefinition 的小写化用 toLowerOutsideQuotes,引号内内容保持原样,
故 b 会被小写而位串不受影响,正则用 (?i) 兼容大写 B'...' 输入;
- 位宽上限取 64:MySQL bit 最大即 bit(64),其最大值 18446744073709551615
恰为 uint64 上界,ParseUint 不会溢出;解析失败时保留原样,
让 PG 报错暴露而非静默吞掉。
测试:补 5 个转换用例(b'0'/b'1'/多位串/bit(64) 全 1 上界/大写 B'1010')
+ 断言产物不再残留 b';另加一个守卫测试验证字符串字面量内容不被误伤。
已验证有效性——移除转换逻辑后测试 FAIL 并打印出未转换的 b'0'。
build / vet / gofmt / 全量测试通过。
Closes #177
注:本项原计划先建 issue,但 issue 创建被环境策略拦截,故将完整问题描述
保留在此 commit message 中。
问题
----
reComment 只认 '' 双写转义,不认 MySQL 默认的反斜杠转义 \':
reComment = (?i)\s+comment\s+'((?:[^']|'')*)'\s*,?\s*|\s+comment\s+"([^"]*)"\s*,?\s*
而 MySQL 的 SHOW CREATE TABLE 对注释中的单引号输出的正是反斜杠转义形态
(除非 sql_mode 含 NO_BACKSLASH_ESCAPES)。英文注释带撇号(don't、user's)
在真实库中极其常见。
实证
----
输入(SHOW CREATE TABLE 的真实输出形态):
CREATE TABLE `t_comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`note` varchar(100) DEFAULT NULL COMMENT 'user\'s note',
`plain` varchar(50) DEFAULT NULL COMMENT 'normal comment',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='table\'s comment'
修复前 ConvertTableDDL 的产物:
CREATE TABLE "t_comment" ("id" SERIAL not null ,"note" VARCHAR(100)s note',"plain" VARCHAR(50), PRIMARY KEY ("id"))
转换器返回 err=nil、Warnings=[](完全静默)。真实 PostgreSQL 上执行:
ERROR: 42601: syntax error at or near "s"
匹配在 user\ 处提前闭合,导致三处破坏:残留 s note' 进入列定义、该列的
DEFAULT NULL 消失、ColumnComments 中 note 被截断为 "user\"(TableComment
同样被截断为 "table\")。列定义逐行处理,故后续 plain 列本身不受影响。
修复后(同样在真实 PG 上端到端验证):
CREATE TABLE "t_comment" ("id" SERIAL not null ,"note" VARCHAR(100),"plain" VARCHAR(50), PRIMARY KEY ("id"));
COMMENT ON TABLE "t_comment" IS 'table''s comment';
COMMENT ON COLUMN "t_comment"."note" IS 'user''s note';
COMMENT ON COLUMN "t_comment"."plain" IS 'normal comment';
四条语句全部执行成功,且读回内容为
table's comment | user's note | normal comment
即注释语义完全正确,不只是让语法通过。
可达性
------
真实可达。CI 不可见的原因是 scripts/mysql/create_table.sql 中 grep \' 零命中
——测试集的注释都不含撇号。
改动
----
1. reComment 两个分支都支持反斜杠转义:
单引号 '((?:[^']|'')*)' → '((?:[^'\\]|''|\\.)*)'
双引号 "([^"]*)" → "((?:[^"\\]|\\.)*)"
2. reTableComment(表级 COMMENT='...')同样处理。
3. 新增 unescapeMySQLStringLiteral,按 MySQL 语义把捕获内容还原为原始文本:
\' \" \\ → 对应字符;'' → ';\n \r \t \b → 控制符;\0 → 丢弃(PG 文本
不允许 NUL 字节);\% \_ → 保留反斜杠(MySQL 语义);其他 \x → 反斜杠
被忽略、取字符本身(MySQL 文档:For all other escape sequences,
backslash is ignored)。按字节遍历对 UTF-8 安全(多字节序列每字节 >= 0x80,
不会与 \ 0x5C 或 ' 0x27 冲突)。
4. 两个提取点(列注释、表注释)改为经该函数还原。
刻意不在提取端做 PG 转义:下游生成端已经做了——GenerateColumnCommentsSQL
与 processComment 均把 ' 转为 ''。提取端只负责还原,避免二次转义。
测试
----
- TestUnescapeMySQLStringLiteral:14 个用例覆盖全部转义规则(含中文混排、
末尾孤立反斜杠、多转义共存、NUL 丢弃、\% \_ 保留)
- TestConvertTableDDL_CommentBackslashEscape:端到端断言无残留、两列定义完整、
括号平衡、ColumnComments 与 TableComment 还原正确
- TestGenerateColumnCommentsSQL_EscapesSingleQuote:锁定生成端的 ' → '' 转义
已验证有效性:把 reComment/reTableComment 改回旧正则后测试 FAIL,并精确打印出
残留的 s note' 与被截断的 "user\" / "table\"。
build / vet / gofmt / 全量测试通过。
附带发现(未在本次修复,建议单独处理)
------------------------------------
processComment(manager.go:1196-1210)的替换顺序有误:先把 \n \r \t 转成
字面 \n \r \t,最后才执行 \\ → \\\\,于是刚生成的反斜杠被再次翻倍成
\\n \\r \\t;PG 在 standard_conforming_strings=on(默认)下会按字面存储。
反斜杠翻倍应当最先执行。此外 processComment(转成字面 \n)与
GenerateColumnCommentsSQL(直接删除换行符)是两条并行且策略不一致的注释
处理路径,建议统一。
注:本项原计划先建 issue,但 issue 创建被环境策略拦截,故将完整问题描述
保留在此 commit message 中。
问题
----
reSetVar = (?i)\bSET\s+(\w+)\s*=\s* → "$1 := "
它的本意是转换 plpgsql 的变量赋值(SET v = 1 → v := 1),但无法区分
UPDATE 语句的 SET 子句,会把 SET 整个删掉并把 = 改成 :=。
实证(输入为 SHOW CREATE FUNCTION 的真实形态):
UPDATE orders SET status = 1 WHERE id = p_id;
→ UPDATE orders status := 1 WHERE id = p_id;
UPDATE orders SET status = 1, amount = 0 WHERE id = p_id;
→ UPDATE orders status := 1, amount = 0 WHERE id = p_id; ← 首列被改、次列保留
SET v = p_id + 1; → v := p_id + 1; ← 这是唯一正确的目标场景
ConvertFunctionDDL 返回 err = nil。真实 PostgreSQL 上执行:
ERROR: 42601: syntax error at or near ":="
LINE 8: UPDATE orders status := 1 WHERE id = p_id;
plpgsql 在 CREATE FUNCTION 时就预编译函数体(check_function_bodies=on),
因此含 UPDATE 的函数一律创建失败。而 CI 的全部 10 个组合都是 functions:false,
这条路径在集成层面零覆盖;create_function.sql 中 grep 'UPDATE\s+\w+\s+SET'
零命中,单测也覆盖不到。
三条既有"修补规则"为何都救不回来
--------------------------------
- reUpdateThen / reUpdateThenEq(:81-82)针对的是 `UPDATE x THEN y :=` 形态,
而 reSetVar 实际产出的是 `UPDATE x y :=`(没有 THEN),故永不匹配;
- reUpdateSet 原先是 applyMiscFixes 内的局部变量,模式
`UPDATE\s+(\w+)\s+SET\s+` → `UPDATE $1 SET ` 是恒等替换,且每次调用都
重新编译正则。本次将其移到包级并加注释说明其真实作用仅为空白规范化。
这正是"正则修补正则产物"堆叠的典型后果:下游补丁假设的中间形态与上游
实际产出的形态不一致,规则之间不可组合。
修复
----
Go 的 RE2 不支持后顾断言,无法用 (?<!UPDATE ...) 排除,故采用「先掩蔽、后还原」:
maskedBody = reUpdateSetClause.ReplaceAllString(maskedBody, "${1}"+updateSetPlaceholder)
maskedBody = reSetVar.ReplaceAllString(maskedBody, "$1 := ")
maskedBody = strings.ReplaceAll(maskedBody, updateSetPlaceholder, "SET ")
- reUpdateSetClause = (?i)\b(UPDATE\s+[^;]*?)\bSET\s+
[^;]*? 非贪婪且不跨分号,只吃掉同一语句内最近的那个 SET;
\b 边界使表名中的 set 字样(如 dataset)不被误匹配。
- 占位符 __m2pg_upd_set__ 全小写、无空白、无引号,可安全穿过后续的小写化与
单词级替换(与 sync_sql_literals.go 中 literalMask 的设计同理)。
- ${1} 必须用花括号:占位符以下划线开头,否则会被并入组名解析。
- reSetVar 从 orderedReplacements 表中移出、改为单独处理,但仍在 mask.mask(body)
之后的文本上执行,因此字符串字面量里的 'SET a = 1' 不受影响(保留了原有的
字面量遮蔽保护)。
端到端验证(真实 PostgreSQL)
--------------------------
修复后产物 `UPDATE orders SET status = 1 WHERE id = p_id;`:
CREATE FUNCTION 成功;
INSERT INTO orders VALUES (1,0),(2,0); SELECT f_upd(1);
→ 返回 1,且 orders 变为 1|1 与 2|0,WHERE 子句正确生效。
即不只是语法通过,语义也正确。
测试
----
TestFunctionConverter_UpdateSetPreserved:10 个子测试覆盖单列/多列 UPDATE、
变量赋值仍转 :=、UPDATE 与变量赋值共存、表名含 set 字样、多表 UPDATE、
小写形态、UPDATE 与 SET 之间跨换行、字符串字面量内的 SET、两条 UPDATE 互不
干扰,并统一断言产物不残留掩蔽占位符。
已验证有效性:停用掩蔽行后 UPDATE 相关用例全部 FAIL 并打印出
`orders status :=` 与 `status := 1, amount = 0`,而"变量赋值仍应转换为 :="
用例仍通过——证明修复只影响 UPDATE 场景、未破坏原有目标行为。
既有 113 个函数的全量测试无回归(其中 func_001 含 LOOP 内的
SET v_counter = v_counter + 1,是对变量赋值路径的天然回归保护)。
build / vet / gofmt / 全量测试通过。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1、fix: MySQL 元数据查询补 rows.Err() 检查,防止结果集截断被当成完整读取
2、fix: 空表分支先于 TRUNCATE 返回,导致 truncate_before_sync 对空表失效
3、fix: worker goroutine 补 recover,单点 panic 降级为单表/单批失败
4、fix: 批次 context 补超时,避免网络半开时迁移永久挂死且 Ctrl-C 无效
5、fix: progressLineGuard 改用 atomic.Pointer,消除数据竞争与 nil 解引用窗口