#4843·gf

contrib/drivers/gaussdb: Save/Replace/InsertIgnore 对所有非文本列失败(MERGE 的 USING 分支缺类型信息)

Author: lingcoderCreated Aug 11, 2026Updated Aug 11, 2026

问题

gaussdb 驱动的 Save() / Replace() / InsertIgnore() 都走 MERGEgaussdb_do_insert.godoMergeInsert),它把每一列写进 USING 子查询:

sql
MERGE INTO "t" AS T1
USING (SELECT 1 AS "id", 'n1' AS "nickname", '{1.5,2.5}' AS "val") AS T2
ON (T1."id" = T2."id")
WHEN MATCHED THEN UPDATE SET T1."val" = T2."val"

这个子查询没有任何类型上下文,PostgreSQL / openGauss 对其中的值一律按 text 处理,再赋回强类型列就会失败。

实测(master 41baf1e28,openGauss 7.0.0-RC1):

val numeric[]  = '{1.5,2.5}'                        -> pq: column "val" is of type numeric[] but expression is of type text
val jsonb      = '{"a":1}'                          -> pq: column "val" is of type jsonb but expression is of type text
val boolean    = 'true'                             -> pq: column "val" is of type boolean but expression is of type text
val uuid       = '550e8400-e29b-41d4-a716-44665544' -> pq: column "val" is of type uuid but expression is of type text
val int4       = 42                                 -> 通过(整数字面量恰好被推断为 int)

复现

go
// 建表:id bigserial PRIMARY KEY, nickname varchar(45), val numeric[]
db.Model(table).Data(g.Map{"id": 1, "nickname": "n1", "val": "{1.5,2.5}"}).Insert()  // OK

one, _ := db.Model(table).Where("id", 1).One()
data := one.Map()
data["nickname"] = "n1-updated"
_, err := db.Model(table).Data(data).Save()   // 失败

Insert() 不受影响,它走普通 INSERT,值是绑定参数;只有 MERGE 这条路径有问题。 因此任何「读出整行 → 改一个字段 → 整行写回」的用法,只要表里有一个非文本列就会挂。

与 #4840 的关系

PR #4840 修了其中「值为 NULL」的一支:给 NULL 占位符补上 ::<列类型> 的 cast。 但如上所示,非 NULL 的值同样失败,两者是同一个根因,只是 #4840 的范围没覆盖到。

可能的修法

对 USING 分支的每一列都套上真实列类型的 cast,而不只是 NULL 列。

类型表达式建议直接取自 format_type(a.atttypid, a.atttypmod),它给出的就是可 直接用于 cast 的完整形式,并且会在需要时自动加引号:

列定义 gf 现在拼的 TableField.Type format_type
int4 int4(32)(cast 非法) integer
varchar(45) varchar(45) character varying(45)
numeric[] _numeric numeric[]
"MyEnum" MyEnum(未加引号会被折成小写) "MyEnum"
timestamp timestamp timestamp without time zone

gaussdb_table_fields.go 目前把 COALESCE(character_maximum_length, numeric_precision, -1) 拼回类型名,int4 因此变成 int4(32) —— 这是 gf 自己 造出来的形式,PG 不接受用它做 cast。改用 format_type 可以连带免掉现在剥离 类型修饰符的处理。

代价是 gdb.TableField 没有存放该表达式的字段,需要驱动侧自行查询并缓存。

环境

  • master (41baf1e28)
  • openGauss 7.0.0-RC1.B023(CI 镜像)