Skip to content

fix(sqlserver): complete schema diff DDL support - #7168

Merged
t8y2 merged 4 commits into
t8y2:mainfrom
Cherrs:fix/sqlserver-schema-diff-ddl
Aug 26, 2026
Merged

fix(sqlserver): complete schema diff DDL support#7168
t8y2 merged 4 commits into
t8y2:mainfrom
Cherrs:fix/sqlserver-schema-diff-ddl

Conversation

@Cherrs

@Cherrs Cherrs commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

变更说明

  • 补全 SQL Server 架构比较生成逻辑,针对表、列、默认约束、索引、外键、函数、序列、触发器、对象删除和条件执行生成原生 T-SQL
  • 在执行 ALTER COLUMN 前正确处理默认约束,保留已有约束名称和表达式,并生成可逆的回滚 SQL;同时包含 invert_change_string 的回滚修复
  • 复用共享的 SQL Server 扩展属性注释 helper:非空注释存在时执行 UPDATE、不存在时执行 ADD,删除注释时按存在性执行 DROP
  • 安全处理跨数据库类型和默认值转换,包括 MySQL 无符号整数范围、DATETIME2/DATETIMEOFFSET 当前时间精度、Unicode N'...' 字面量和方括号标识符转义

本 PR 基于 6cf8d8e,补全在验证 #7051 修复过程中发现的其余 SQL Server 架构比较场景。

Refs #7051

修复前后

此前 SQL Server 目标库仍可能收到不支持的语法,例如 ADD COLUMNALTER COLUMN ... SET DEFAULTCOMMENT ONCREATE INDEX IF NOT EXISTS;部分列变更还会被已有默认约束阻止。

现在生成的 SQL 全部使用 SQL Server 原生语法和系统目录检查,包括:

  • 使用 ALTER TABLE ... ADD,以及包含完整类型和 NULL|NOT NULLALTER COLUMN
  • 通过 sys.default_constraints 查找默认约束,并按删除、保留、重建的正确顺序执行
  • 通过 sys.extended_properties 以及 sp_updateextendedpropertysp_addextendedpropertysp_dropextendedproperty 管理注释
  • 使用 OBJECT_IDsys.indexes 条件检查,避免生成 SQL Server 不支持的 CREATE INDEX IF NOT EXISTS
  • 使用 sp_renameNEXT VALUE FOR 以及 SQL Server 专用的函数、序列、触发器和外键语法

本次改动仅涉及 dbx-core,没有 UI 或前端变更,因此不需要截图。

验证结果

  • cargo +1.97.1 test -p dbx-core --locked --no-default-features --lib:5,267 passed,55 ignored
  • cargo +1.97.1 test -p dbx-core --locked --no-default-features --lib sqlserver:304 passed,4 ignored;已在 rebase 到最终 main 后重新执行
  • API 合约、双向 diff、跨方言集成和兼容性测试:共 41 passed
  • cargo +1.97.1 clippy -p dbx-core --locked --no-default-features --all-targets -- -D warnings
  • cargo +1.97.1 fmt -p dbx-core --check
  • git diff --check

真实 SQL Server 执行验证

使用 sqlcmd 连接 SQL Server LocalDB 17.0.4025.3(Express Edition 64-bit)完成验证。测试对象创建在 tempdb 的事务内,执行结束后全部回滚。

真实执行覆盖:

  • 默认约束变更、保留和回滚
  • 表、列、索引注释的新增、更新和删除
  • MySQL 无符号整数映射后的边界值
  • SYSDATETIME()SYSDATETIMEOFFSET() 默认值
  • Unicode 默认值、中文注释,以及包含 ]、空格和单引号的标识符
  • 幂等索引创建、序列、函数、触发器和条件对象删除

执行结果为 PASS,并确认事务回滚后测试 schema 不存在。

4 个被忽略的 SQL Server 驱动测试要求配置 DBX_LIVE_SQLSERVER_HOST/PORT/USER/PASSWORD;LocalDB 提供的是本地命名管道,而不是该测试要求的 TCP 连接。这些测试针对空间类型和驱动集成,本 PR 涉及的 DDL 已由真实 SQL Server 引擎直接执行验证。

Microsoft 文档依据

@github-actions github-actions Bot added area/core Shared DBX core runtime bug Something isn't working db/sqlserver Database: SQL Server labels Aug 25, 2026

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request changes: SQL Server column dependency handling is still incomplete, and PostgreSQL defaults with parameterized casts are currently corrupted.

  1. crates/dbx-core/src/schema_diff.rs:2236 discards unchanged indexes and foreign keys, while the SQL generation around line 4615 only drops indexes reported as removed or modified. A type or nullability change on a column with an unchanged dependent index therefore emits ALTER COLUMN without dropping the dependency. Primary keys, CHECK constraints, and inbound foreign keys are also not fully represented.

    SQL Server documents that ALTER COLUMN fails when indexes, statistics, or constraints depend on the column: https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql

    Please make column changes dependency-aware: drop every affected index/key/CHECK/FK, including inbound FKs, then recreate the exact original object types. Add live regressions for an unchanged nonclustered index, primary key, CHECK constraint, and inbound foreign key.

  2. crates/dbx-core/src/schema_diff.rs:3566 stops consuming a PostgreSQL cast type at (. For example, '0.00'::numeric(10,2) becomes '0.00'(10,2), producing invalid T-SQL.

    Please consume balanced type modifiers, array suffixes, and schema-qualified type names, with regressions for numeric(10,2) and character varying(20).

Snapshot and restore live SQL Server indexes, keys, checks, statistics, and foreign keys around ALTER COLUMN. Parse complete PostgreSQL cast types when translating defaults and add unit plus live regressions.
@Cherrs

Cherrs commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@t8y2 感谢 review。关于 SQL Server ALTER COLUMN 的依赖处理,我补充一下采用当前方案的原因和本地验证结果,也想请您确认这个方向是否合适。

为什么需要这样处理

现有 schema diff 主要描述“发生变化”的索引和外键。目标库中未发生变化、但依赖被修改列的对象不会出现在 diff 中;CHECK、入站外键和独立统计信息也无法仅靠当前 diff 数据完整获取。

因此,如果生成 SQL 时直接执行 ALTER TABLE ... ALTER COLUMN,目标库里已有的以下对象可能阻止修改:

  • 普通索引,包括键列和 INCLUDE 列
  • PRIMARY KEY / UNIQUE 约束
  • CHECK 约束
  • 出站、入站、自引用及复合 FOREIGN KEY
  • 用户创建的独立统计信息
  • DEFAULT 约束(继续由原有专用逻辑处理)

当前提交 ecfbbbb8f88617631b91e33ab1ad27e570d82e3d 增加了 sqlserver_dependencies.rs:修改列前从目标库系统目录读取并保存相关对象的精确定义,按依赖顺序删除,执行 ALTER COLUMN,随后按相反顺序恢复。整个过程使用事务/保存点;遇到当前不支持安全重建的索引类型或无法解析的分区依赖时,会在删除任何对象之前失败,避免留下半完成状态。

之所以不能只处理 diff 中的对象,是因为这些阻塞对象可能完全没有变化,只有运行时查询目标库目录才能发现。

本地 SQL Server 负向测试

环境:SQL Server LocalDB 17.0.4025.3 RTM(Express Edition)。测试对象使用 GUID 唯一名称,每个用例在独立事务中执行并回滚,最终残留对象数为 0

跳过“保存定义 → 删除依赖 → 修改列 → 恢复依赖”,直接执行裸 ALTER COLUMN 的结果:

场景 结果
无依赖,INT → BIGINT 成功
普通非聚集索引键列,INT → BIGINT 失败,错误 4922
普通非聚集索引 INCLUDE 列,INT → BIGINT 失败,错误 4922
PRIMARY KEY,INT → BIGINT 失败,错误 4922
UNIQUE 约束,INT → BIGINT 失败,错误 4922
CHECK 约束,INT → BIGINT 失败,错误 4922
出站 FOREIGN KEY,INT → BIGINT 失败,错误 4922
入站 FOREIGN KEY + 被引用键,INT → BIGINT 失败,错误 4922
用户统计信息,INT → BIGINT 失败,错误 4922
DEFAULT 约束,INT → BIGINT 失败,错误 4922

同时复刻了回归测试中的组合依赖:

  • id INT NOT NULL → BIGINT NOT NULL:失败,id 保持 INT NOT NULL
  • code INT NOT NULL → INT NULL:失败,code 保持 INT NOT NULL
  • 两者都返回:ALTER TABLE ALTER COLUMN ... failed because one or more objects access this column.

使用当前依赖感知流程后,LocalDB 回归可以成功修改列,并验证普通索引、PK、UQ、CHECK、出站 FK 和入站 FK 均已按原属性恢复。

实现取舍,希望听取您的意见

本地测试也确认并非每种操作都必须删除依赖,例如:

  • 带普通索引的 VARCHAR(10) → VARCHAR(20) 可以直接成功。
  • 只有普通非唯一索引时,INT NOT NULL → INT NULL 可以直接成功。

所以当前实现是偏保守的统一方案:只要发生类型或可空性变化,就保存并重建相关依赖,以优先保证所有阻塞场景正确;代价是代码和生成 SQL 的复杂度较高,少数 SQL Server 原本允许直接修改的场景也会发生依赖重建。

这部分改动确实比较大,想请您确认一下设计方向:您更倾向于保留当前这种统一兜底、以正确性为先的实现,还是希望进一步缩小范围,只针对确认会阻塞的修改类型和依赖组合进行处理?我可以根据您的意见继续调整。

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a small follow-up patch on top at 409903c.

The narrowed conditional-table prefix check in wrap_conditional_check dropped SQLite CREATE TEMP TABLE from IF NOT EXISTS wrapping, and PostgreSQL GLOBAL/LOCAL TEMPORARY/UNLOGGED forms lost the DO-block idempotent wrapper. The patch restores coverage for all CREATE TABLE variants via a shared create_table_variant_prefix helper (fail-open when the keyword cannot be located), with regression tests for both dialects.

One known follow-up stays open and does not block this PR: a diff-"modified" UNIQUE-constraint backing index is still recreated as a plain CREATE UNIQUE INDEX. The drop side correctly resolves ALTER TABLE ... DROP CONSTRAINT at runtime, but the create side cannot know constraint-ness at generation time. Impact is metadata fidelity only — uniqueness and FK referenceability are preserved (CREATE TABLE (Transact-SQL)). A proper fix needs constraint-ness threaded into the index metadata snapshot; tracked for a separate change.

Checks run: cargo test -p dbx-core --no-default-features --lib script_generator (63 passed, including the two new regressions).

@t8y2 t8y2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved the conflict with main at bb9f74d (old head 409903c, base c8c9870).

Cause: both this branch and the freshly landed schema-sync change edited crates/dbx-core/src/schema_diff.rs; the only textual conflict was the ddl_profile import list — resolved to the union (main's expanded import). Everything else auto-merged.

Behavior preservation: the main-side column_def auto-increment gating (MySQL-family only) and this branch's dedicated sqlserver_column_definition coexist — the SQL Server path emits IDENTITY(1,1) directly after the type with correct T-SQL clause order. The regression test from main was updated to assert that order ([seq] INT IDENTITY(1,1) NOT NULL) instead of absence, which matches both sides' intent.

Checks run: cargo test -p dbx-core --no-default-features --lib schema_diff:: (228 passed) and script_generator (63 passed) on the resolved head; git diff --check clean, no conflict markers.

@t8y2
t8y2 force-pushed the fix/sqlserver-schema-diff-ddl branch from bb9f74d to 1e2caf1 Compare August 26, 2026 05:08
@t8y2
t8y2 merged commit a4bf844 into t8y2:main Aug 26, 2026
13 checks passed
@t8y2

t8y2 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution! Merged in a4bf844, will be released in the next version.

@Cherrs

Cherrs commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution! Merged in a4bf844, will be released in the next version.

目前此功能在sqlserver上已基本可用,我会持续跟进sqlserver diff DDL

@Cherrs
Cherrs deleted the fix/sqlserver-schema-diff-ddl branch August 29, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Shared DBX core runtime bug Something isn't working db/sqlserver Database: SQL Server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants