- 开发工具
- 代码生成
- 数据库
【免费下载链接】sqlc
Generate type-safe code from SQL
本文围绕 sqlc 仓库中的端到端测试用例
ddl_alter_type_rename_value展开:该用例的 MySQL 侧说明 明确指出 MySQL 不支持CREATE TYPE ... AS ENUM,枚举只能以内联ENUM类型直接定义在表列上。文章将解读这条文档背后的完整语义,并通过仓库源码与 PostgreSQL 对照组,讲清 sqlc 在两种数据库引擎下对枚举 DDL 的不同处理方式,帮助你在 schema 设计时正确选择枚举声明语法。
这个测试用例在验证什么
在 sqlc 的端到端测试目录中,ddl_alter_type_rename_value用于验证"修改枚举类型取值名"这一 DDL 场景(对应 PostgreSQL 的ALTER TYPE ... RENAME VALUE)。该目录下同时存在两套引擎的测试:
- mysql/:只有一份 README.md,没有任何 schema.sql、query.sql 或生成代码;
- postgresql/:包含完整的 stdlib、pgx/v4、pgx/v5 三套 Go 生成结果,以及对应的 schema.sql 与 sqlc.json。
这种目录结构本身就是 sqlc 对 MySQL 枚举能力边界的声明:MySQL 侧不提供对应的 DDL 测试,因为该语句在 MySQL 中根本不存在。
原文档核心结论:MySQL 没有独立的枚举类型 DDL
关联文档 mysql/README.md 的原文结论如下:
MySQL does not support
CREATE TYPE ... AS ENUM. Instead, enumerations are defined via theENUMtype directly in table columns.
翻译并展开为两层事实:
- 不存在
CREATE TYPE ... AS ENUM:与 PostgreSQL 可以先用CREATE TYPE status AS ENUM (...)创建独立类型、再在表中引用不同,MySQL 无法单独创建枚举类型对象; - 枚举必须内联定义在表列上:MySQL 的枚举形态是列级类型声明,例如
CREATE TABLE t (status ENUM('open','closed')),枚举取值集合是列定义的一部分,而不是一个可复用的独立数据库对象。
因此,ALTER TYPE status RENAME VALUE 'closed' TO 'shut'这类"重命名枚举取值"的语句在 MySQL 语法体系中不存在对应物,sqlc 也就没有为其编写 MySQL 端到端用例,而是用一篇简短文档说明原因,并把完整的测试覆盖留给 PostgreSQL 对照组。
sqlc 如何解析 MySQL 的列内联枚举
尽管 MySQL 没有独立枚举类型,sqlc 的 MySQL 引擎(内部代号 dolphin)仍能识别列上的ENUM类型,并把它纳入类型系统。可以从三层源码确认这一点:
1. 引擎方言层:enum 被识别为标量字符串类型
internal/engine/dolphin/dialect/types.jsonl 中声明了:
{"name": "enum", "category": "S"}category: "S"表示该类型被归入字符串(string)类目。这意味着在 MySQL 模式下,sqlc 首先把ENUM当作一个字符串类型的列来处理。
2. 代码生成层:MySQL 枚举列默认映射为 Go string
Go 代码生成器在 internal/codegen/golang/mysql_type.go 中处理enum类型:
case "enum": // TODO: Proper Enum support return "string"也就是说,当 schema 中直接出现ENUM('open','closed')这类列定义时,只要该枚举没有在 Catalog 中登记为具名枚举类型,生成的 Go 字段类型就是string(可空场景由上层逻辑处理为sql.NullString等)。从源码中的TODO: Proper Enum support注释可以看出,MySQL 内联枚举目前走的是"退化为字符串"的保守策略。
3. 目录构建层:内联枚举被合成为内部类型名
当 MySQL 的列定义携带枚举取值集合(Vals)时,sqlc 会把它们合成为一个内部枚举类型,命名规则为表名_列名,见 internal/sql/catalog/table.go 中的defineColumn:
if col.Vals != nil { typeName := ast.TypeName{ Name: fmt.Sprintf("%s_%s", table.Name, col.Colname), } s := &ast.CreateEnumStmt{TypeName: &typeName, Vals: col.Vals} if err := c.createEnum(s); err != nil { return nil, err } tc.Type = typeName tc.linkedType = true }这段代码揭示了 MySQL 内联枚举在 sqlc 内部的真实表示:列上写的ENUM('a','b')会被转换为一次隐式的CREATE ENUM,枚举名由表名与列名拼接而成,列类型再指向这个合成枚举。这也解释了mysql_type.go中default分支为何还会遍历schema.Enums来匹配列类型——当枚举被登记为具名类型后,生成逻辑会尝试输出枚举结构体而非纯字符串。
对照组:PostgreSQL 如何完整支持RENAME VALUE
PostgreSQL 侧才是ddl_alter_type_rename_value真正执行测试的地方,其完整链路可以一步步追踪:
1. 测试 schema 与生成结果
postgresql/stdlib/schema.sql 给出了最小可复现 schema:
CREATE TYPE status AS ENUM ('open', 'closed'); ALTER TYPE status RENAME VALUE 'closed' TO 'shut';对应的生成结果 postgresql/stdlib/go/models.go 中,枚举被生成为 Go 类型与常量:
type Status string const ( StatusOpen Status = "open" StatusShut Status = "shut" )注意两个关键点:
- 旧值
closed在生成常量中彻底消失,取而代之的是新值shut(StatusShut),证明 sqlc 的解析结果正确应用了重命名; - 该测试的 query.sql 仅有一条
SELECT 1占位查询,说明此用例只验证 schema 解析与类型生成,不涉及查询编译。
pgx/v4 与 pgx/v5 的生成结果与 stdlib 完全一致(枚举常量、Scan/Value方法结构相同),表明RENAME VALUE的处理不依赖具体驱动。
2. 解析器:把 PostgreSQL 的 ALTER ENUM 拆成两类语句
PostgreSQL 引擎解析器在 internal/engine/postgresql/parse.go 中处理AlterEnumStmt:当语句携带旧值(n.OldVal != "")时翻译为重命名取值语句,否则翻译为追加取值语句:
case *nodes.Node_AlterEnumStmt: n := inner.AlterEnumStmt rel, err := parseRelationFromNodes(n.TypeName) if err != nil { return nil, err } if n.OldVal != "" { return &ast.AlterTypeRenameValueStmt{ Type: rel.TypeName(), OldValue: makeString(n.OldVal), NewValue: makeString(n.NewVal), }, nil } else { return &ast.AlterTypeAddValueStmt{ ... }, nil }3. AST 节点:结构化表达重命名
internal/sql/ast/alter_type_rename_value_stmt.go 定义了对应的 AST 节点,携带三个字段:
type AlterTypeRenameValueStmt struct { Tag NodeTag[AlterTypeRenameValueStmt] `json:"tag"` Type *TypeName `json:"type,omitempty"` OldValue *string `json:"old_value,omitempty"` NewValue *string `json:"new_value,omitempty"` }4. Catalog 应用:校验并原地替换取值
最终落地在 internal/sql/catalog/types.go 的alterTypeRenameValue,其行为值得细读:
for i, val := range enum.Vals { if val == *stmt.OldValue { oldIndex = i } if val == *stmt.NewValue { newIndex = i } } if oldIndex < 0 { return fmt.Errorf("type %T does not have value %s", stmt.Type, *stmt.OldValue) } if newIndex >= 0 { return fmt.Errorf("type %T already has value %s", stmt.Type, *stmt.NewValue) } enum.Vals[oldIndex] = *stmt.NewValue实现要点:
- 保留原位置:只替换
Vals数组中的对应元素,不改变枚举值的顺序,这对依赖枚举顺序的代码生成(常量顺序、Valid/Values方法)很重要; - 双向校验:旧值不存在、或新值与已有值冲突时都会报错,保证 Catalog 中的枚举取值集合始终合法;
- schema 作用域:未显式指定 schema 时使用
DefaultSchema(ns = c.DefaultSchema),与 sqlc 的默认 schema 解析策略一致。
该节点在 internal/sql/catalog/catalog.go 中被分发到上述实现,形成"解析 → AST → Catalog 更新 → 代码生成"的完整链路。
实操建议:两种引擎下枚举 schema 应该怎么写
基于以上源码事实,可以给出直接可落地的 schema 编写建议:
| 场景 | 推荐写法 | sqlc 处理结果 |
|---|---|---|
| PostgreSQL | CREATE TYPE status AS ENUM ('open','closed');+ 表列引用 | 生成具名 Go 枚举类型与常量,ALTER TYPE ... RENAME VALUE可安全用于迁移 |
| MySQL | 列内联status ENUM('open','closed') | 枚举取值作为列的一部分被解析,列类型在代码生成中默认映射为string(源码标记为 TODO 的未完成特性);若希望获得具名类型行为,需依赖 Catalog 中的合成枚举路径,具体生成形态受 mysql_type.go 当前实现约束 |
关键结论:
- 不要试图在 MySQL schema 里写
CREATE TYPE ... AS ENUM或ALTER TYPE ... RENAME VALUE——MySQL 方言没有这些语句,dolphin 引擎的转换逻辑(internal/engine/dolphin/convert.go)只处理ALTER TABLE下的列操作,并不会识别独立的类型级 ALTER; - MySQL 的枚举变更需要写成
ALTER TABLE ... MODIFY COLUMN(重新声明列上的 ENUM 取值集合),而不是类型级语句; - 如果希望让 MySQL 枚举列获得与 PostgreSQL 一致的具名类型生成体验,目前需要在 sqlc 的 MySQL 类型映射(mysql_type.go)层面关注其演进,现阶段应默认接受
enum → string的映射结果。
小结
ddl_alter_type_rename_value这个测试用例用一份简短的 README 精准划出了 sqlc 对 MySQL 枚举 DDL 的能力边界:MySQL 不提供CREATE TYPE ... AS ENUM,枚举内联在表列中;而完整的ALTER TYPE ... RENAME VALUE支持链路(解析器翻译 → AST 节点 → Catalog 校验与替换 → 具名枚举生成)则完整存在于 PostgreSQL 引擎中,并有 stdlib、pgx/v4、pgx/v5 三套生成结果佐证。对使用者而言,理解这条边界能避免写出在 MySQL 下无法解析的 schema,也能在迁移到 PostgreSQL 时放心使用类型级枚举重命名。
- 开发工具
- 代码生成
- 数据库
【免费下载链接】sqlc
Generate type-safe code from SQL
相关推荐
sqlc 中枚举演进的处理:PostgreSQL `ALTER TYPE ... ADD VALUE` 与 MySQL 列内 `ENUM` 的差异解析
sqlc 中枚举演进的处理:PostgreSQL ALTER TYPE ... ADD VALUE 与 MySQL 列内 ENUM 的差异解析 在 sqlc 的
开发工具代码生成数据库ModernDive社区与支持:如何参与开源项目和获取帮助的完整指南
ModernDive社区与支持:如何参与开源项目和获取帮助的完整指南 ModernDive是一个专注于R和Tidyverse数据科学统计推断的开源教材项目,为学
终极NSwag枚举处理指南:如何正确配置字符串枚举与数值枚举
终极NSwag枚举处理指南:如何正确配置字符串枚举与数值枚举 NSwag是一个强大的Swagger/OpenAPI工具链,专为.NET开发者设计,能够轻松生成A
开发工具代码生成API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考