Files
MyGoNavi/internal/sync/diff_paging_escape_test.go
Syngnat 2bdbdc9382 🐛 fix(sync): 修复分页比对与 DDL 字面量缺反斜杠转义导致的错删行与语法错误 DDL
- quoteSyncSQLString 改为方言感知:新增 syncDialectEscapesBackslash,对 mysql/mariadb/
  clickhouse/tdengine/starrocks/diros/oceanbase 先翻倍反斜杠再翻倍单引号;postgres 系、
  sqlserver、sqlite、duckdb 保持原样(翻倍反而会写入两个反斜杠并损坏比对)
- 含反斜杠的主键原先在 IN 列表里被目标库解释成别的字符串,导致源端存在的行先被判为插入、
  反查时又匹配不到而被判为删除并真的从目标删掉,最终 Success=true 但目标永久缺行
- dbType 逐层传入 formatSyncSQLLiteral 的 3 个调用点:buildPKInSelectQuery、
  buildKeysetPagedTableQuery、buildSourceQueryPKInSelectSQL
- escapeMySQLStringLiteral 同类修复:列注释与 DEFAULT 值内联进 MySQL/ClickHouse DDL 时,
  以反斜杠结尾的注释会让 COMMENT '...\' 吞掉闭合引号并生成语法错误的 DDL
- 补 5 项回归测试,方言分组做双向断言(漏加与误加都会失败)
2026-07-26 20:19:29 +08:00

106 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sync
import (
"strings"
"testing"
"GoNavi-Wails/internal/connection"
)
// TestQuoteSyncSQLStringEscapesBackslashPerDialect 覆盖分页比对字面量的方言感知转义。
//
// 回归背景quoteSyncSQLString 原先只翻倍单引号。含反斜杠的主键Windows 路径、正则、
// 转义 JSON在 MySQL/ClickHouse 等以反斜杠转义的目标上会被解释成别的字符串,导致
// IN 列表匹配不到:源端存在的行先被判为需要插入,随后反查时又匹配不到而被判为需要删除,
// 于是真的从目标删掉,最终 Success=true 但目标永久缺行。
func TestQuoteSyncSQLStringEscapesBackslashPerDialect(t *testing.T) {
t.Parallel()
// 分页白名单中以反斜杠转义的方言:必须翻倍。
for _, dbType := range []string{"mysql", "mariadb", "clickhouse", "tdengine", "starrocks", "diros"} {
if got := quoteSyncSQLString(dbType, `C:\logs`); got != `'C:\\logs'` {
t.Errorf("quoteSyncSQLString(%q, %q) = %s期望 %s", dbType, `C:\logs`, got, `'C:\\logs'`)
}
}
// 白名单中反斜杠为普通字面字符的方言:翻倍反而会损坏数据,必须保持原样。
for _, dbType := range []string{"postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "sqlite", "duckdb"} {
if got := quoteSyncSQLString(dbType, `C:\logs`); got != `'C:\logs'` {
t.Errorf("quoteSyncSQLString(%q, %q) = %s期望 %s", dbType, `C:\logs`, got, `'C:\logs'`)
}
}
// 单引号翻倍的既有行为必须保持不变。
if got := quoteSyncSQLString("mysql", `O'Brien`); got != `'O''Brien'` {
t.Errorf("mysql 单引号处理 = %s期望 %s", got, `'O''Brien'`)
}
if got := quoteSyncSQLString("postgres", `O'Brien`); got != `'O''Brien'` {
t.Errorf("postgres 单引号处理 = %s期望 %s", got, `'O''Brien'`)
}
// 反斜杠与单引号并存时顺序不能颠倒(先反斜杠、后单引号)。
if got := quoteSyncSQLString("mysql", `a\'b`); got != `'a\\''b'` {
t.Errorf("mysql 混合转义 = %s期望 %s", got, `'a\\''b'`)
}
}
// TestBuildPKInSelectQueryEscapesBackslashInPrimaryKey 端到端断言主键字面量不会越出引号。
func TestBuildPKInSelectQueryEscapesBackslashInPrimaryKey(t *testing.T) {
t.Parallel()
cols := []connection.ColumnDefinition{{Name: "id"}, {Name: "name"}}
mysqlQuery := buildPKInSelectQuery("mysql", "app.users", cols, "id", []interface{}{`C:\logs`})
if !strings.Contains(mysqlQuery, `'C:\\logs'`) {
t.Errorf("MySQL 主键字面量未转义反斜杠:%s", mysqlQuery)
}
pgQuery := buildPKInSelectQuery("postgres", "app.users", cols, "id", []interface{}{`C:\logs`})
if !strings.Contains(pgQuery, `'C:\logs'`) {
t.Errorf("PostgreSQL 主键字面量不应翻倍反斜杠:%s", pgQuery)
}
}
// TestBuildKeysetPagedTableQueryEscapesBackslash keyset 分页的 WHERE pk > 字面量同样受影响。
func TestBuildKeysetPagedTableQueryEscapesBackslash(t *testing.T) {
t.Parallel()
cols := []connection.ColumnDefinition{{Name: "id"}}
query := buildKeysetPagedTableQuery("mysql", "app.users", cols, "id", `a\b`, true, 100)
if !strings.Contains(query, `'a\\b'`) {
t.Errorf("keyset 分页字面量未转义反斜杠:%s", query)
}
}
// TestEscapeMySQLStringLiteralEscapesBackslash 列注释与 DEFAULT 值的 DDL 字面量。
func TestEscapeMySQLStringLiteralEscapesBackslash(t *testing.T) {
t.Parallel()
if got := escapeMySQLStringLiteral(`C:\tmp`); got != `C:\\tmp` {
t.Errorf("escapeMySQLStringLiteral(%q) = %q期望 %q", `C:\tmp`, got, `C:\\tmp`)
}
// 以反斜杠结尾的注释原先会吞掉闭合引号,生成语法错误的 DDL。
if got := escapeMySQLStringLiteral(`ends with\`); got != `ends with\\` {
t.Errorf("escapeMySQLStringLiteral(%q) = %q期望 %q", `ends with\`, got, `ends with\\`)
}
if got := escapeMySQLStringLiteral(`O'Brien`); got != `O''Brien` {
t.Errorf("单引号处理被破坏:%q", got)
}
}
// TestSyncDialectEscapesBackslashCoversPaginationWhitelist 固定方言分组,
// 漏加(数据被改写)与误加(反斜杠被写成两个)两个方向都必须失败。
func TestSyncDialectEscapesBackslashCoversPaginationWhitelist(t *testing.T) {
t.Parallel()
for _, dbType := range []string{"mysql", "mariadb", "clickhouse", "tdengine", "starrocks", "diros", "oceanbase"} {
if !syncDialectEscapesBackslash(dbType) {
t.Errorf("syncDialectEscapesBackslash(%q) = false期望 true", dbType)
}
}
for _, dbType := range []string{"postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "sqlite", "duckdb", ""} {
if syncDialectEscapesBackslash(dbType) {
t.Errorf("syncDialectEscapesBackslash(%q) = true期望 false", dbType)
}
}
}