信创数据库兼容笔记

1. GORM AutoMigrate 把 Vastbase 打崩了

现象

应用启动时 Vastbase 进程崩溃重启,所有连接收到 connection reset by peer,应用 panic。

根因

GORM 的 AutoMigrate 每次启动都执行上百条无意义的 ALTER TABLE ... SET DEFAULT 语句。
PostgreSQL 扛得住这种重复 DDL,Vastbase 扛不住,进程直接挂。

不是内存不够,不是连接数超限,不是并发问题(AutoMigrate 是串行的)。
是 Vastbase 在 ~10 秒内连续执行 100+ 条 DDL 后内部崩溃。

复现环境

  • 镜像:registry.cn-hangzhou.aliyuncs.com/qiluo-images/vastbase_g100:20250514160619
  • 版本:Vastbase G100 V2.2 (Build 15) Release,编译于 2025-04-03,Commit 25780

为什么会有上百条无意义 DDL

GORM 在 AutoMigrate 时做以下事情:

  1. information_schema.columns 查出每列当前的 column_default
  2. parseDefaultValueValue 函数清洗这个值
  3. 和 Go struct tag 中的 default:xxx 做字符串比较
  4. 不一致就执行 ALTER TABLE ... ALTER COLUMN ... SET DEFAULT

问题在第 2 步。parseDefaultValueValue 做了两件事:

1
2
3
4
5
6
7
// gorm.io/driver/postgres@v1.6.0/migrator.go
func parseDefaultValueValue(defaultValue string) string {
    // 1. 去掉 ::type 后缀:'zh'::text → 'zh'
    value := regexp.MustCompile(`^(.*?)(?:::.*)?$`).ReplaceAllString(defaultValue, "$1")
    // 2. 去掉首尾引号:'zh' → zh
    return strings.Trim(value, "'")
}

Vastbase 与 PostgreSQL 在原始返回值上只有一处差异

类型 DDL 定义 PostgreSQL 返回 Vastbase 返回
text DEFAULT '' ''::text ''::text
int8 DEFAULT 0 0 0
bool DEFAULT true true true
text DEFAULT 'zh' 'zh'::text 'zh'::text
jsonb DEFAULT '{}' '{}'::jsonb '{}'::jsonb
text[] DEFAULT '{}' '{}'::text[] '{}'::text[]
timestamptz DEFAULT CURRENT_TIMESTAMP CURRENT_TIMESTAMP now()

只有 timestamptz 不一样:PostgreSQL 存 CURRENT_TIMESTAMP,Vastbase 存 now()

GORM 的比较逻辑(MigrateColumn)

GORM 按数据类型分支比较,每种类型的比较策略不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// gorm.io/gorm@v1.31.2/migrator/migrator.go MigrateColumn 方法

case schema.Time:
    // TrimSuffix("()") 后忽略大小写比较
    // now() → TrimSuffix("()") → "now"
    // CURRENT_TIMESTAMP → TrimSuffix("()") → "CURRENT_TIMESTAMP"
    // "now" ≠ "CURRENT_TIMESTAMP" → alterColumn = true
    // ← Vastbase 在这里每次都触发 DDL

case schema.String:
    // 双重比较:先原值,再去引号
    // dv="zh", tag="'zh'" → Trim("'zh'","'") = "zh" → 不触发
    // ← 这个分支已经修好了

case schema.Bool:
    // ParseBool 后比较 → 不受影响

default:
    // 直接比较字符串
    // dv="{}", tag="'{}'" → "{}" ≠ "'{}'" → alterColumn = true
    // ← jsonb 在这里每次都触发 DDL(PG 也一样)

三类数据库经过 GORM 处理后的最终对比

类型 DB 返回 → parseDefaultValueValue 结果 struct tag DefaultValue PostgreSQL Vastbase SQLite
text 'zh' zh 'zh' ✅(String 双重比较兜底)
text '' `` (空串) '' ✅(String 双重比较兜底)
int8 0 0 0
bool true true true
jsonb '{}' {} '{}' ❌(default 分支直接比较)
text[] '{}' {} '{}'
timestamptz PG: CURRENT_TIMESTAMP / VB: now() CURRENT_TIMESTAMP

结论:

  • Vastbase 独有问题:timestamptz(now() vs CURRENT_TIMESTAMP
  • GORM 通病:jsonb/数组(PostgreSQL 上也触发,只是不崩)

60 张表 × 2 个 timestamptz 列 = 120 条 DDL → Vastbase 崩溃。

解决方案

自定义 GORM Dialector,只抹平 Vastbase 与 PostgreSQL 的唯一差异:

1
2
3
4
5
6
// pkg/vastbasecompat/dialector.go
// VastbaseMigrator.ColumnTypes 中
if col.DefaultValueValue.String == "now()" &&
    (col.DataTypeValue.String == "timestamptz" || col.DataTypeValue.String == "timestamp") {
    col.DefaultValueValue.String = "CURRENT_TIMESTAMP"
}

修复后 Vastbase 上的 AutoMigrate 行为与 PostgreSQL 完全一致。


2. GORM jsonb/数组 DEFAULT 每次都 ALTER(已知 Bug)

这个问题在 PostgreSQL 上也存在,不是 Vastbase 特有的。

Issue 链路

Issue/PR 状态 描述
go-gorm/gorm#7553 Open jsonb 列每次 AutoMigrate 都执行 SET DEFAULT(2025-08-11)
go-gorm/gorm#7590 Closed serializer tag 触发同样问题
go-gorm/gorm#7591 Merged 修了 schema.String 分支的引号比较(gorm v1.31.2 已包含)
go-gorm/gorm#7770 Closed(未合并) 尝试修 default 分支(jsonb 等非 String 类型),被关闭
go-gorm/gorm#7806 Merged 修了 DEFAULT NULL 无限循环,未覆盖 jsonb
go-gorm/postgres#225 Open v1.5.4 改变了 jsonb 默认值生成(2023-11-21)

为什么一直没修

  1. jinzhu 先合并了 #7591,修了 schema.String 分支 → text 列的问题解决了
  2. jsonb 走的是 MigrateColumn 中的 default 分支(不是 schema.String),#7591 的修复对它无效
  3. 有人提了 #7770,给 default 分支加了同样的 strings.Trim 去引号逻辑,但被关闭了

根因在代码里一目了然:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// MigrateColumn 中的两个分支对比
case schema.String:
    // 已修复:加了去引号的第二次比较
    if dv != field.DefaultValue && dv != strings.Trim(field.DefaultValue, "'\"") {
        alterColumn = true
    }

default:
    // 未修复:只有直接比较,jsonb 走这里
    if dv != field.DefaultValue {  // "{}" ≠ "'{}'" → 每次都触发
        alterColumn = true
    }

jsonb 字段的 GORMDataType 是自定义类型名(如 jsonb),不是 String,所以走 default 分支。

处理建议

  • PostgreSQL 上无害,忽略即可
  • Vastbase 上如果 DDL 总量(timestamptz + jsonb)足以让服务端崩溃,先修 timestamptz(上一节方案),jsonb 部分等 GORM 上游修
  • 关注 go-gorm/gorm#7553,上游修复后升级 GORM 即可
  • 如果等不了,可以在自定义 Migrator 中补上 jsonb 的归一化

3. SQL 语法差异

Vastbase 不支持部分 PostgreSQL 高版本语法,需要在驱动层做 SQL 重写。

3.1 make_interval 不支持

1
2
3
4
5
-- PostgreSQL(River 生成)
WHERE scheduled_at <= now() + make_interval(secs => $3)

-- Vastbase 改写
WHERE scheduled_at <= now() + $3 * interval '1 second'

3.2 数组切片 arr[start:] 不支持

Vastbase 不支持 PostgreSQL 的数组切片语法 arr[start:]。River 用这个语法裁剪 attempted_by 数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- PostgreSQL(River 生成)
array_append(
    CASE WHEN array_length(attempted_by, 1) >= $2
    THEN attempted_by[array_length(attempted_by, 1) + 2 - $2:]
    ELSE attempted_by END,
    $3::text)

-- Vastbase 改写(跳过裁剪,直接追加)
array_append(
    CASE WHEN $2 >= 0 THEN attempted_by ELSE attempted_by END,
    $3::text)

裁剪的目的是防止数组无限增长,但 attempted_bymax_attempts(默认 25)自然约束,不裁剪也不会超过 25 个元素。语义上无影响。

3.3 ON CONFLICT … WHERE 不支持

1
2
3
4
5
-- PostgreSQL
ON CONFLICT (kind) WHERE unique_states IS NOT NULL DO UPDATE SET ...

-- Vastbase 改写(去掉 WHERE)
ON CONFLICT (kind) DO UPDATE SET ...

注意去掉 WHERE 可能影响条件化唯一索引的语义,需要评估。

3.4 WITH … INSERT … ON CONFLICT DO UPDATE 不支持

Vastbase 不支持 CTE 与 ON CONFLICT DO UPDATE 组合使用。改写方式是将 CTE 内联为 FROM 子查询。

3.5 xid 类型不支持与 integer 直接比较

1
2
3
4
5
-- PostgreSQL
(xmax != 0) AS unique_skipped_as_duplicate

-- Vastbase 改写
(xmax::text != '0') AS unique_skipped_as_duplicate

3.6 DSN 特殊字符

Vastbase DSN 中密码的特殊字符需要 URL 编码。


4. 连接行为差异

4.1 认证方式

Vastbase 默认用 sha256 认证(pg_hba.conf),pgx 驱动天然支持,无需适配。

4.2 密集 DDL 时的稳定性

Vastbase 在短时间内密集执行 DDL 时容易崩溃。
应对:减少不必要的 DDL,合理设置连接池参数。

4.3 vastbase 兼容模式

vastbase 支持多种数据库的协议连接,部署的时候固定,如果选择了 mysql 模式,则 postgresql 连接会出现很多故障。

SHOW dbcompatibility; 查看兼容模式

返回值 兼容模式 说明
A Oracle 兼容 Oracle 语法和协议
B MySQL 兼容 MySQL 语法和部分协议
C TD (Teradata) 兼容 Teradata 语法
PG / 空 PostgreSQL 原生 openGauss/PG 模式
本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy