问题描述
SQL driver 的自动 schema 同步(initObjects / syncSchema)是 only-additive:表已存在时只对"元数据声明了、但物理表里还没有的列"执行 createColumn,已存在的列一律跳过,且不比较类型。
当一个列名在物理表里已存在、但其物理类型与新版本元数据声明的类型不一致时(典型场景:先删字段 f2、残留了旧列 → 后续版本又加回同名 f2 但类型不同),同步逻辑会静默跳过该列:
- 不会
ALTER 列类型 - 不打任何 warn / log
- 安装/升级照常显示成功
类型不符要到运行时才暴露:读出的值类型不对、过滤/排序结果异常、或写入触发 PgSQL 类型错误,而且没有任何线索指回"这是残留列类型未对齐",排查成本高。
代码定位
packages/plugins/driver-sql/src/sql-driver.ts(约 L1477–L1492,initObjects 表已存在分支):
constcolumnInfo=awaitthis.knex(tableName).columnInfo();constexistingColumns=Object.keys(columnInfo);// 只取列名awaitthis.knex.schema.alterTable(tableName,(table)=>{for(const[name,field]ofObject.entries(obj.fields)){if(!existingColumns.includes(name)){// 只判断列名是否存在this.createColumn(table,name,field);}// 没有 else:列已存在则什么都不做——不比类型、不报警}});columnInfo[name].type 携带了真实物理类型,但当前只用 Object.keys() 取了列名,类型信息被完全忽略。
相关:packages/metadata/src/migration/executor.ts 的 modify_field 分支仅 console.warn('Not fully implemented'),且未接入自动装包路径。
复现步骤
- PgSQL datasource,部署 v1:对象
o1 含字段 f2(如 type: 'text')。 - v2:从
o1 删除 f2(物理列 f2 残留,数据保留——见 only-additive 行为)。 - v3:
o1 重新加入字段 f2,但类型改为 number。 - 安装 v3 → 提示成功;但物理列
f2 仍是旧 text 类型,与元数据声明的 number 不一致,无任何提示。
期望行为
至少要让类型漂移可见,不能静默。
解决方案建议
按成本从低到高,建议先做 1,再视情况推进 2/3:
1.(必做)安装期检测 + 告警 —— drift detection
在 initObjects 表已存在分支,对每个"列名已存在"的字段,比较 columnInfo[name].type 与该字段映射后的期望物理类型(复用现有 JSON_COLUMN_TYPES / NUMERIC_SCALAR_TYPES / DDL 类型 switch 作为单一真相源),不一致则发出结构化告警:
logger.warn,包含 { datasource, table, column, expectedType, actualType };- 默认不阻断安装(保持向后兼容),保证至少留下排查线索。
注意跨方言归一化(PgSQL/MySQL/SQLite 的 columnInfo().type 表述不同,SQLite 类型亲和性更宽松),避免误报。
2.(推荐)os doctor / 安装报告里汇总 schema drift
把检测结果汇总进 os doctor(或安装结束的 summary),列出所有"列已存在但类型不符 / 元数据已无此字段的孤儿列",并给出建议的修复 DDL(ALTER ... TYPE / DROP COLUMN / RENAME),让运维在部署前/后能一眼看到。
3.(可选,需谨慎设计)受控的类型迁移
为 modify_field 提供真正实现,但仅在显式授权下执行(如 --allow-destructive 或受 ADR-0015 DDL gate 约束的 schemaMode: 'managed')。类型变更可能丢数据/失败(如 text→number 含非数字值),必须:
- 默认 dry-run 输出将执行的 DDL;
- 提供归档残留列的选项(
RENAME COLUMN f2 -> f2__archived_<ts> 再建新列),而非直接 ALTER TYPE; - 全程结构化日志 + 安装报告。
最小可行:先落地方案 1 的 warn,即可消除"静默"这个最大的坑。
影响
- 标签建议:
bug, protocol:data - 优先级:建议
priority:p1(涉及生产数据正确性,且失败完全无提示)
问题描述
SQL driver 的自动 schema 同步(
initObjects/syncSchema)是 only-additive:表已存在时只对"元数据声明了、但物理表里还没有的列"执行createColumn,已存在的列一律跳过,且不比较类型。当一个列名在物理表里已存在、但其物理类型与新版本元数据声明的类型不一致时(典型场景:先删字段
f2、残留了旧列 → 后续版本又加回同名f2但类型不同),同步逻辑会静默跳过该列:ALTER列类型类型不符要到运行时才暴露:读出的值类型不对、过滤/排序结果异常、或写入触发 PgSQL 类型错误,而且没有任何线索指回"这是残留列类型未对齐",排查成本高。
代码定位
packages/plugins/driver-sql/src/sql-driver.ts(约 L1477–L1492,initObjects表已存在分支):columnInfo[name].type携带了真实物理类型,但当前只用Object.keys()取了列名,类型信息被完全忽略。相关:
packages/metadata/src/migration/executor.ts的modify_field分支仅console.warn('Not fully implemented'),且未接入自动装包路径。复现步骤
o1含字段f2(如type: 'text')。o1删除f2(物理列f2残留,数据保留——见 only-additive 行为)。o1重新加入字段f2,但类型改为number。f2仍是旧text类型,与元数据声明的number不一致,无任何提示。期望行为
至少要让类型漂移可见,不能静默。
解决方案建议
按成本从低到高,建议先做 1,再视情况推进 2/3:
1.(必做)安装期检测 + 告警 —— drift detection
在
initObjects表已存在分支,对每个"列名已存在"的字段,比较columnInfo[name].type与该字段映射后的期望物理类型(复用现有JSON_COLUMN_TYPES/NUMERIC_SCALAR_TYPES/ DDL 类型 switch 作为单一真相源),不一致则发出结构化告警:logger.warn,包含{ datasource, table, column, expectedType, actualType };注意跨方言归一化(PgSQL/MySQL/SQLite 的
columnInfo().type表述不同,SQLite 类型亲和性更宽松),避免误报。2.(推荐)
os doctor/ 安装报告里汇总 schema drift把检测结果汇总进
os doctor(或安装结束的 summary),列出所有"列已存在但类型不符 / 元数据已无此字段的孤儿列",并给出建议的修复 DDL(ALTER ... TYPE/DROP COLUMN/RENAME),让运维在部署前/后能一眼看到。3.(可选,需谨慎设计)受控的类型迁移
为
modify_field提供真正实现,但仅在显式授权下执行(如--allow-destructive或受 ADR-0015 DDL gate 约束的schemaMode: 'managed')。类型变更可能丢数据/失败(如 text→number 含非数字值),必须:RENAME COLUMN f2 -> f2__archived_<ts>再建新列),而非直接ALTER TYPE;最小可行:先落地方案 1 的 warn,即可消除"静默"这个最大的坑。
影响
bug,protocol:datapriority:p1(涉及生产数据正确性,且失败完全无提示)