ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CubeSandbox PostgreSQL 迁移规范:基于 goose 的双方言 Schema 对齐实践

CubeSandbox PostgreSQL 迁移规范:基于 goose 的双方言 Schema 对齐实践 CubeSandbox PostgreSQL 迁移规范基于 goose 的双方言 Schema 对齐实践【免费下载链接】CubeSandboxInstant, Concurrent, Secure Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox导读本文档系统讲解 CubeSandbox 项目中 CubeDB 迁移模块 针对 PostgreSQL 的迁移约定包括与 MySQL 迁移保持一一对应的双方言 Schema 对齐机制、版本命名规范、-- goose指令与 advisory lock 用法、完整的 MySQL → PostgreSQL 类型映射表以及由 CI 测试强制约束的对齐防线。读完本文你将掌握如何为 CubeMaster/CubeOps 新增 PostgreSQL 迁移文件、如何保证两个数据库引擎最终落在完全一致的逻辑 Schema 上以及哪些方言差异是被测试接受的正常差异、哪些会被判定为 Schema 漂移。迁移机制总览本目录下的 PostgreSQL 迁移文件不是手动执行的而是在进程启动时由 pkgs/cubedb/migrate/migrate.go 通过github.com/pressly/goose/v3自动应用的。从源码 migrate.go 可以看到迁移引擎为每种方言维护了一份dialectSpecvar dialectSpecs map[string]dialectSpec{ mysql: { dialect: database.DialectMySQL, rootFS: mysqlMigrations, subdir: migrations/mysql, store: mysqlFingerprintStore{}, }, postgres: { dialect: database.DialectPostgres, rootFS: postgresMigrations, subdir: migrations/postgres, store: postgresFingerprintStore{}, }, }迁移文件通过//go:embed指令直接编译进二进制见 migrate.go因此部署时无需携带独立 SQL 文件目录。迁移引擎还做了三件事启动即自动迁移CubeMaster 与 CubeOps 进程启动时调用migrate.Run(ctx, sqlDB, dialect, locker)自动把 Schema 推进到 HEAD。可选开关CUBE_AUTO_MIGRATIONAutoMigrationEnabled() 默认返回true仅当环境变量显式为false/0/no/off大小写不敏感时才关闭。注释特别强调不用strconv.ParseBool因为后者会拒绝no/off而任何未设置、为空或无法识别的值都必须保持自动迁移的安全默认防止拼写错误导致静默跳过迁移。无 DDL 权限的运行时数据库账号应设置该变量为假改用高权限账号在带外out-of-band应用 Schema。乱序容忍与幂等goose.WithAllowOutOfOrder(true)允许合并后时间戳版本乱序落地配合迁移文件内的*_if_missing幂等写法见 migrate.go。此外迁移引擎还内置了一层指纹防御层fingerprint defence layer见 fingerprint.go在每次启动迁移前后对迁移文件内容做指纹记录与预检防止已应用过的版本被悄悄改写。与 MySQL 迁移的关系逻辑 Schema 完全一致PostgreSQL 迁移产生与其 MySQL 对应物完全相同的逻辑 Schema。每个迁移版本号与 MySQL 版本1:1 对应保证两个引擎最终落在同一个 HEAD 上SQL 语法只在 MySQL 与 PostgreSQL 分叉之处有所不同数据类型、DDL 构造、锁机制等。两个目录的文件列表一一对应各 28 个迁移文件migrations/mysqlMySQL 版本migrations/postgresPostgreSQL 版本例如0001_baseline_v0_2_2.sql的 PostgreSQL 版头部注释明确写着This file is the PG equivalent of mysql/0001_baseline_v0_2_2.sql见 postgres/0001_baseline_v0_2_2.sql。双防线文件对等 Schema 对齐跨方言对齐由测试自动强制两条防线一轻一重文件对等无需 DockerTestMigrationVersionParityAcrossDialects 逐一比对两个目录的版本集合与描述后缀migrations/mysql/下存在的每个版本及描述后缀必须同时存在于migrations/postgres/反之亦然。该测试专门拦截忘记移植迁移这类回归——测试注释明确指出正是这类 bug 曾导致t_cube_snapshot_runtime_active表在 PostgreSQL 中缺失。Schema 对齐需要 DockerTestSchemaAlignment_MySQL_vs_Postgres 在两个一次性的 dockertest 容器中分别把 MySQL 与 PostgreSQL 迁移到 HEAD然后抽取各自的规范化逻辑 Schema表、列、可空性、类型类别、含主键的有序索引签名进行等价性断言。规范化比对模型normalizedSchema 是跨引擎比较用的方言无关模型每张表由列集合nullable、typeCategory、defaultKind、defaultValue与索引集合列的有序列表 unique primary 签名构成。比对分三步见assertSchemasAlignedmigrate_alignment_test.go表集合是否一致能快速捕获runtime_active这类缺表问题每张表的列集合、可空性、类型类别、默认值是否一致索引签名集合是否一致。其中goose_db_version与t_cubemaster_migration_fingerprint两张记账表bookkeeping tables被显式排除在业务 Schema 之外见 migrate_alignment_test.go。命名规范与 MySQL 侧完全一致共两段式命名冻结的历史顺序块0001–0010这些 4 位数字文件永久冻结不得重命名、删除或改动版本号UTC 时间戳前缀迁移格式为YYYYMMDDhhmmss_description.sql例如20260812120000_template_replica_shim_version.sql。该策略由 TestMigrationFilenames 在 CI 中强制历史顺序块0001–0010必须原样存在且禁止新增任何 4 位顺序文件顺序号在 rebase 时会被复用导致已应用的迁移被静默跳过新迁移必须使用 14 位 UTC 时间戳前缀且描述为小写 snake_case任何两个迁移文件不得共享同一个整数版本号。从测试常量historicalMaxVersion 10migrate_naming_test.go可推断今后新增迁移一律走时间戳路径不要试图续号。迁移内容规则事务模式与 goose 指令每个迁移文件必须遵守文件顶部同时添加-- goose NO TRANSACTION和-- goose Up。PostgreSQL 的 DDL 本身是事务性的但为了与 MySQL 保持一致的 goose 模式且 advisory lock 与显式事务组合时会互相干扰故统一采用NO TRANSACTION模式。典型文件开头形如-- goose NO TRANSACTION -- goose Up使用IF NOT EXISTS/IF EXISTS保证幂等以便在乱序out-of-order落地或重复执行时安全重放。每个迁移用 advisory lock 包裹顶部获取SELECT cubemaster_acquire_migration_lock(cubemaster_migration_version_name, 60);底部释放SELECT pg_advisory_unlock(hashtext(cubemaster_migration_version_name));与 MySQL 兄弟文件使用完全相同的锁字符串含缩写后缀。提供对称的-- goose Down唯一例外是0001基线——不可逆。锁字符串为何要缩写MySQL 的GET_LOCK对锁名有64 字符硬限制超限报错 Error 4163。PostgreSQL 的 advisory lock 会对名称做哈希hashtext本来不受此限但为了双方言锁字符串一致MySQL 的限制适用于两侧。因此长描述必须缩写例如_shim_ver代替_template_replica_shim_version。该约束由 TestMigrationLockNamesRespectMySQLGETLockLimit 测试强制测试还给出了_rt_active、_tpl_alias_unique等缩写示例。完整示例带锁的时间戳迁移以下取自真实文件 postgres/20260812120000_template_replica_shim_version.sql与 MySQL 兄弟文件 mysql/20260812120000_template_replica_shim_version.sql 逐行对应-- goose NO TRANSACTION -- goose Up SELECT cubemaster_acquire_migration_lock(cubemaster_migration_20260812120000_shim_ver, 60); SELECT cubemaster_assert_table_exists(t_cube_template_replica); SELECT cubemaster_add_column_if_missing(t_cube_template_replica, shim_version, varchar(128) NOT NULL DEFAULT ); SELECT pg_advisory_unlock(hashtext(cubemaster_migration_20260812120000_shim_ver)); -- goose Down SELECT cubemaster_acquire_migration_lock(cubemaster_migration_20260812120000_shim_ver, 60); SELECT cubemaster_drop_column_if_exists(t_cube_template_replica, shim_version); SELECT pg_advisory_unlock(hashtext(cubemaster_migration_20260812120000_shim_ver));对比 MySQL 侧可清晰看到CALL cubemaster_acquire_migration_lock(...)/SELECT RELEASE_LOCK(...)与 PG 侧SELECT cubemaster_acquire_migration_lock(...)/SELECT pg_advisory_unlock(hashtext(...))的对称写法两边的锁字符串cubemaster_migration_20260812120000_shim_ver完全相同。基线迁移中的 PL/pgSQL 辅助函数0001基线迁移postgres/0001_baseline_v0_2_2.sql定义了后续所有迁移复用的 PL/pgSQL 辅助函数用-- goose StatementBegin/StatementEnd包裹以支持函数体内的分号函数作用cubemaster_acquire_migration_lock(lock_name, timeout_sec)循环调用pg_try_advisory_lock(hashtext(lock_name))超时默认 60 秒、每次pg_sleep(0.2)重试则RAISE EXCEPTION并携带锁名与锁 idcubemaster_drop_column_if_exists(tbl, col)仅当列存在时ALTER TABLE ... DROP COLUMN查information_schema.columnscubemaster_add_column_if_missing(tbl, col, coldef)仅当列不存在时ALTER TABLE ... ADD COLUMN配合format(%I)防注入cubemaster_add_index_if_missing(tbl, idx, idxdef)仅当索引不存在时执行完整的CREATE INDEX语句体查pg_indexescubemaster_drop_index_if_exists(tbl, idx)仅当索引存在时DROP INDEXcubemaster_assert_table_exists(tbl)/cubemaster_assert_column_exists(tbl, col)Schema 预检目标表/列不存在时立即RAISE EXCEPTION让迁移在启动阶段快速失败这些辅助函数是 PostgreSQL 幂等策略的核心由于NO TRANSACTION模式下失败不会整体回滚*_if_missing/*_if_exists语义保证了乱序重放或部分失败后的再次执行依然安全。0001基线的Down部分直接RAISE EXCEPTION cubemaster baseline migration (0001) is not reversible; restore from backuppostgres/0001_baseline_v0_2_2.sql即基线代表 v0.2.2 的地面真值不可回退。MySQL → PostgreSQL 类型映射表原文档的映射表在仓库中可直接对照两个基线文件验证如 mysql/0001_baseline_v0_2_2.sql 与 postgres/0001_baseline_v0_2_2.sqlMySQLPostgreSQLbigint unsignedbigint无 CHECKDDL 层不强制无符号范围——如需校验请在应用层保持int unsignedintegertinyint(1)booleantinyint非布尔smallintmediumtext/longtext/texttextjsonjsonbPG 首选 JSON 类型对齐测试将两侧归一化为json类别varchar(N)varchar(N)datetimetimestampAUTO_INCREMENTBIGSERIAL/SERIALENGINEInnoDB移除DEFAULT CHARSETutf8mb3/utf8mb4移除数据库级编码反引号引用双引号或不加引号GET_LOCK/RELEASE_LOCKpg_advisory_lock/pg_advisory_unlockON DUPLICATE KEY UPDATEON CONFLICT ... DO UPDATEINSERT IGNOREINSERT ... ON CONFLICT DO NOTHINGUPDATE ... JOINUPDATE ... FROM ... WHERE存储过程CREATE PROCEDUREPL/pgSQL 函数CREATE FUNCTION在实际基线中可验证的映射实例PG 侧t_cube_host_info.id bigserial NOT NULL对应 MySQLAUTO_INCREMENT、healthy boolean NOT NULL DEFAULT false对应 MySQLtinyint(1)、fail_msg text对应 MySQLmediumtext/longtext、private_ip_cnt smallint对应 MySQL 非布尔tinyint、created_at timestamp对应datetime且所有PRIMARY KEY (id)保持等价。被测试接受的方言差异不要当作漂移以下差异是故意的由对齐测试通过归一化normalization接受不要把它们当作 Schema 漂移去修复jsonvsjsonbmediumtext/longtextvstexttinyint/int unsigned/bigint unsignedvssmallint/integer/bigintMySQL 的ON UPDATE CURRENT_TIMESTAMP在 PostgreSQL 中无法表达为列默认值行为由应用代码或触发器负责PostgreSQL 偶发的NOT NULL文本列DEFAULT 而 MySQL 侧无默认值对我们的 DAO 路径而言空串插入语义等价PostgreSQL 的DEFAULT NULL/NULL::typenamevs MySQL 的 no default对可空列而言两者都表示 null 默认值对齐测试将二者归一化为none。默认值比较的精确规则默认值比较不仅比种类还比归一化后的字面量。测试代码defaultsCompatible见 migrate_alignment_test.go只放行一种none ↔ literal豁免仅限NOT NULL的 text/varchar 列且一侧无默认值、另一侧为DEFAULT 即文档所述的 PG 习惯如request_json这类列。其余任何none ↔ literal例如一侧DEFAULT 0而另一侧无默认或0vs1一律判失败。也就是说DEFAULT vs 无默认值 → 仅当列是 NOT NULL 文本/字符型时被接受DEFAULT 0vs 无默认值 → 失败DEFAULT 0vsDEFAULT 1→ 失败。新增 PostgreSQL 迁移的完整检查清单结合上述约定与 CI 测试新增一条迁移时请逐项确认成对提交新增 MySQL 迁移的同时必须在同一个 PR 里添加匹配的 PostgreSQL 文件TestMigrationVersionParityAcrossDialects会拦截遗漏。命名14 位 UTC 时间戳 小写 snake_case 描述形如20260901XXXXXX_description.sql严禁触碰冻结的0001–0010块。文件头-- goose NO TRANSACTION-- goose Up。幂等表/列/索引操作使用IF [NOT] EXISTS或复用cubemaster_add_column_if_missing等辅助函数。锁顶部SELECT cubemaster_acquire_migration_lock(cubemaster_migration_version_abbrev, 60);底部SELECT pg_advisory_unlock(hashtext(cubemaster_migration_version_abbrev));锁字符串与 MySQL 兄弟文件逐字符一致且不超过 64 字符必要时缩写描述后缀。Down 对称提供可逆的-- goose Down0001基线除外。类型映射按上表完成 MySQL → PostgreSQL 类型转换bigint无 CHECK、boolean、smallint、text、jsonb、timestamp、BIGSERIAL/SERIAL等一一对应。接受/拒绝差异仅保留上文被接受的方言差异DEFAULT 0之类的新增none ↔ literal不对称会被对齐测试判失败。测试防线一览测试位置依赖拦截目标TestMigrationFilenamesmigrate_naming_test.go无 Docker命名规范、冻结块被改、版本重复TestMigrationVersionParityAcrossDialectsmigrate_naming_test.go无 Docker某版本只在单方言存在、描述后缀不一致TestMigrationLockNamesRespectMySQLGETLockLimitmigrate_naming_test.go无 Docker锁字符串超 64 字符TestMigrationsDirHasReadmemigrate_naming_test.go无 Docker命名策略文档缺失TestSchemaAlignment_MySQL_vs_Postgresmigrate_alignment_test.godockertest 容器两引擎迁移到 HEAD 后逻辑 Schema 不一致表/列/可空性/类型类别/默认值/索引签名其中前四类无数据库依赖、可在每个 PR 的 CI 中低成本执行Schema 对齐测试则用一次性的 MySQL/PostgreSQL 容器做最终兜底验证。总结CubeSandbox 的 PostgreSQL 迁移体系以 goose 为执行引擎以与 MySQL 逻辑 Schema 完全一致为硬约束通过命名规范测试无 Docker与双引擎 Schema 对齐测试dockertest两层防线确保任何一条 MySQL 迁移都必须有等价的 PostgreSQL 版本、且最终落地 Schema 归一化后完全等价。对开发者而言记住三条铁律即可成对提交、冻结块勿动、锁串一致且 ≤64 字符对 Schema 差异的判定则以 README 中列出的被接受差异清单与defaultsCompatible测试逻辑为准。【免费下载链接】CubeSandboxInstant, Concurrent, Secure Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表