Operations 的 Schema 能力是一条“实时检测 → 生成 SQL → 可选执行”的流水线。它适合开发和受控运维,但当前没有不可变迁移计划、预览哈希、执行租约或全有全无事务,不能按传统迁移平台理解。
1. 完整数据流
每次 preview 和 apply 都重新读取当前编译期 metadata、重新检测数据库、重新生成脚本。两次调用之间的代码、数据库或配置变化会改变结果。
2. 三个只读/执行用例
| 用例 | Level | 副作用 |
|---|---|---|
GetSchemaDrift.Query | 无 | 只返回漂移报告 |
PreviewSchemaMigration.Query | 调用方可传三级 | 返回 SQL、数量和级别 |
ApplySafeSchemaMigration.Command | 固定 SafeOnly | dry-run 或执行 |
ApplyAllSchemaMigration.Command | 固定 FullForce | dry-run 或执行 |
PreviewSchemaMigration 允许预览 FullForce,但返回值没有 plan ID、hash、有效期或审批绑定。
3. 声明元数据来源
Handler 调用 ModuleAssemblyRegistry.GetRegistrations(),展开每个注册项的 MetadataByType.Values。这意味着检测依据是组合根实际注册的编译期 ORM 元数据,不是运行时扫描实体特性。
若某模块没有进入 registry,其表不会出现在声明侧;漂移报告必须结合组合 Profile 阅读。
4. 安全级别
| Level | 允许的操作 |
|---|---|
SafeOnly | ADD COLUMN、放宽字符串长度、ADD INDEX |
WithConfirm | 以上 + 收窄长度、改变列类型 |
FullForce | 以上 + DROP COLUMN、DROP INDEX |
“安全”表示生成器分类,不代表无锁、零停机或对大表无影响。ADD INDEX、带默认值加列仍可能造成锁、日志增长或部署超时。
5. Dry-run 的真实语义
// ① 每次请求都读取当前 metadata 和当前数据库漂移。var report = await detector.DetectAsync(declaredEntities, cancellationToken);var script = generator.GenerateScript(report, MigrationSafetyLevel.SafeOnly);
// ② DryRun 只阻止执行;没有保存脚本或返回不可变计划标识。if (request.DryRun) return new MigrationResult( [script.ToSqlText()], script.Statements.Select(x => x.TableName).Distinct().ToList(), [$"[Dry-run] {script.Count} 条语句未执行"], clock.UtcNow);dry-run 把整段 SQL 放进 ExecutedSqls 形状的第一项,语义上是复用了执行结果 DTO。调用方不能把该字段名理解为已经执行。
6. 审批并非所有环境一致
Safe apply 只在 approvalGate.IsApprovalRequired 时校验。API Host 对 Production 与 Staging 返回 true。
FullForce 无条件调用 ValidateAsync,但 Host 的适配器在非强制环境直接返回 true。因此当前效果仍是 Production/Staging 强制,而不是源码注释宣称的所有环境强制。
当强制环境解析不到 IOpsExtensionStore 时,仅凭非空工单号放行;没有校验状态、创建者或变更内容。
7. FullForce 的 Reason 不是必填
ApplyAllSchemaMigration.Command.Reason 是 nullable。Handler 没有空值验证,审计中会写 (未提供)。
如果产品要求破坏性变更必须说明原因,应把非空和长度约束放入请求/领域验证,并纳入审批工单内容绑定。
8. 执行器为什么叫 ApplySafeAsync
ISchemaMigrationExecutor 只有 ApplySafeAsync(MigrationScript)。FullForce Handler 也调用这个方法。执行器不会再次检查 Level,只执行传入脚本;审批与危险级别完全由上游负责。
这不是说 FullForce 被降级为 SafeOnly,而是接口名容易造成错误安全感。商业接口应改成语义中立的 ApplyAsync,并显式验证允许的 Level 与审批证据。
9. 逐句 best-effort 语义
SqlSugar 执行器逐条循环:
- ADD INDEX 先按 SQL 正则提取索引名,再检查是否已存在;
- 每条通过
ExecuteCommand单独执行; - 异常写 warning 并加入
SkippedStatements; - 继续执行后续语句;
- 最终总是返回成功
Result<MigrationResult>。
因此“HTTP/Result 成功”只代表执行流程结束,不代表所有语句成功。
10. 正确判定结果
var result = await mediator.Send( new ApplySafeSchemaMigration.Command(ticket, DryRun: false), cancellationToken);
if (result.IsFailure) return ReleaseDecision.Block(result.Error.Code);
var migration = result.Value!;
// ① skipped 可能是幂等跳过,也可能是执行异常,当前只有文本可区分。if (migration.SkippedStatements.Any(x => x.Contains("失败:", StringComparison.Ordinal))) return ReleaseDecision.Block("存在失败迁移语句");
// ② 发布证据要保存 executed SQL 与受影响表,而不只保存 HTTP 200。return await evidence.RecordAsync(migration, cancellationToken);当前 skipped 是本地化文本而非结构化原因,自动化只能脆弱解析字符串。
11. 事务和并发边界
执行器没有包裹整个脚本的事务,也没有部署级分布式锁。两个实例可以同时检测并执行同一漂移。
只有 ADD INDEX 有专门幂等检查;其他语句依赖数据库结果,失败后被记为 skipped。部分成功是设计现状,回滚必须依靠人工或后续脚本。
12. 审计边界
Safe/FullForce 只在执行器返回 success 后写 ActivityRecord。由于执行器把单句错误转成 success,审计可能显示 IsSuccess=true,同时结果中含失败 skipped。
空脚本和 dry-run 不写活动审计;审批拒绝和执行前异常也不写失败审计。审计只记录计数、表名、原因和工单关联,不保存完整 SQL 或 before/after diff,尽管源码注释声称更强。
13. HTTP 可达性边界
四个 Schema 请求只有 [GenerateEndpoint],手写 OperationsEndpointGroup 没有对应映射。当前生成器不递归静态用例下的嵌套 Query/Command,因此特性本身不足以证明路由可达。
GA 前需要运行真实 Host 的路由清单/HTTP 合同测试,而不是只用源码 rg 检查特性存在。
14. 上线前最小 runbook
- 固定应用构建和数据库快照;
- 获取 drift 与目标级别 preview;
- 评审每条 SQL 的锁、日志、数据截断和回滚影响;
- 保存 SQL hash 并由不同人员批准;
- 进入单实例/部署锁维护窗口;
- 重新检测,若 hash 变化则重新批准;
- 执行并检查每条结构化状态;
- 再次检测必须无未解释漂移;
- 保存 before/after、审批、操作者与数据库标识;
- 演练部分成功后的补偿路径。
其中 4—8 的自动绑定能力当前尚未实现。
15. 关键 GA 缺口
- 不可变 migration plan、数据库指纹、SQL hash 和有效期;
- 预览、审批、执行三者强绑定;
- 数据库级或部署级互斥锁;
- 结构化逐句结果与失败整体策略;
- 可选全脚本事务/按 DDL 能力分组;
- 强制 Reason、SoD 和审批 fail-closed;
- 失败、拒绝、dry-run 与空计划审计;
- 真实 HTTP、SQL Server 并发与故障合同测试。
16. 测试命令
# Schema 应用层和生成/执行器测试。dotnet test tests/BitzOrcas.Application.Tests \ --filter 'FullyQualifiedName~SchemaMigration|FullyQualifiedName~SchemaDrift'
# 搜索仍不存在的商业安全原语;当前预期无实质实现。rg -n "PlanHash|DatabaseFingerprint|DistributedLock|MigrationLease" \ src/Platform/Operations src/Framework -g '*.cs' \ --glob '!**/bin/**' --glob '!**/obj/**'