Skip to content

feat(driver-sql)!: index drift is planned, not silently executed at boot (#3728) - #3737

Merged
os-zhuang merged 1 commit into
mainfrom
claude/unique-index-migration-ddl-ybji9k
Jul 28, 2026
Merged

feat(driver-sql)!: index drift is planned, not silently executed at boot (#3728)#3737
os-zhuang merged 1 commit into
mainfrom
claude/unique-index-migration-ddl-ybji9k

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3728.

问题

#3696 / #3717 的 unique 收敛落在 syncTableIndexes就地执行:initObjects 期间跑一次 DROP + CREATE UNIQUE INDEX,所有环境都跑,只留一行日志。os migrate plan 什么都看不到——因为 detectManagedDrift 只有列维度,DriftOp 里根本没有索引这一类。想在 DDL 落库前审查的运维没有预检手段;而且这是在生产自动改 managed schema,恰恰是 #2186 明令禁止的("schema is never auto-altered in production")。

处置(按 owner 拍板的方案)

索引成为一等的 drift 维度,和列 drift 走同一条路。

行为变化

启动不再无条件重写索引:

环境 行为
dev(autoMigrate: 'safe',即 os dev / os serve) 重启时照旧自愈,本地工作流不变
生产 / autoMigrate: 'off' WARN + 指向 os migrate,不动 schema;plan 完整可见,apply 执行

代价是明确的:没跑 os migrate 的生产部署会继续带着遗留全局 unique 运行(多租户插入仍会撞号),直到有人执行 os migrate apply。这是 issue 里选定的取舍——可见、可预检的迁移,胜过看不见的迁移——并且由启动告警兜底,没人会在不知情的情况下带病运行。

其他

  • 索引名构造收敛到 schema-drift.ts 的单一定义,驱动创建的名字和差异器查找的名字不可能再分叉。
  • Postgres 索引内省改读 pg_index 而非 pg_indexes,这样 UNIQUE CONSTRAINT 背后的索引(正是 knex 旧 col.unique() 产生的东西)对检测器可见——原来的代码只能盲试 drop。
  • 顺手修掉:对象移除 indexes[]managedObjectIndexes 从不清空,导致漂移检测一直期待一个没人声明的索引。
  • SchemaDiffEntryKind 新增 index_mismatch / unmapped_index

测试

  • 新增 sql-driver-index-drift.test.ts(17 例):计划可见性、boot 不执行 DDL、autoMigrate: 'safe' 自愈、NODE_ENV=production 强制禁用、apply 幂等、声明式索引缺失/重定义/孤儿、纯差异器单元(遗留名但定义不符不误删、PK 索引永不算漂移、哈希截断长名识别)。
  • sql-driver-unique-tenancy.test.ts:三个迁移用例改为显式 autoMigrate: 'safe',锁定新的启动语义。
  • schema-migrate.integration.test.ts:端到端跑真实 bootSchemaStack,在 NODE_ENV=production 下断言 plan 同时看到 relax_not_nullreplace_unique_index,apply 后两者都消失且数据完好。
  • 全绿:driver-sql 325 例 / cli 642 例 / spec 6696 例 / objectql + runtime + service-datasource。

🤖 Generated with Claude Code

https://claude.ai/code/session_013LJzroBbHM7FTpHXrtv4pm


Generated by Claude Code

…oot (#3728)

The #3696 unique-scope migration converged in place: `syncTableIndexes` ran a
`DROP` + `CREATE UNIQUE INDEX` during `initObjects`, in every environment,
leaving one log line behind. `os migrate plan` showed nothing, because
`detectManagedDrift` was column-only — `DriftOp` had no index dimension at all.
An operator who wanted to review the DDL before it reached their database had
no way to, and a managed schema was being auto-altered in production, which the
#2186 contract explicitly forbids.

Index drift is now a first-class dimension, reconciled through the same path as
column drift.

- `syncTableIndexes` is ADDITIVE ONLY. It creates indexes; it never drops or
  rewrites one. `dropLegacyGlobalUniques` is gone.
- New `DriftOp` variants: `replace_unique_index` (safe — retire the legacy
  platform-wide unique in favour of the tenant composite), `create_index`
  (safe), `recreate_index` (needs-confirm; destructive when it tightens to
  UNIQUE, since the create can fail on existing duplicates after the drop) and
  `drop_index` (destructive).
- `detectManagedDrift` reports them, `os migrate plan` renders them (index ops
  display as `table [index_name]`), `os migrate apply` executes them. Index DDL
  is portable, so it applies directly on every dialect — no SQLite rebuild.
- `replace_unique_index` creates before it drops, and only drops once the
  replacement is confirmed present: a relaxation must never degrade into
  removing the constraint outright.
- Declared `indexes[]` drift is covered too — an index metadata declares but
  the database lacks, and one whose definition no longer matches the
  declaration (the additive sync skips those by name, so they could never
  self-heal on their own).
- Orphan detection is limited to ObjectStack's own generated naming (`uniq_…` /
  `idx_…`, plus the pre-#3696 `<table>_<column>_unique` knex spelling). A
  hand-rolled operational index is never reported as drift and
  `--allow-destructive` will not delete it.

Behaviour change: boot no longer rewrites the index unconditionally. Dev
(`autoMigrate: 'safe'`, what `os dev` / `os serve` use) still self-heals on
restart, so local workflows are unchanged. Production now warns with an
actionable `os migrate` hint and leaves the schema alone — the deployment stays
on the legacy global unique until someone runs `os migrate apply`. That is the
deliberate trade: a visible, pre-inspectable migration instead of an invisible
one.

Index name construction moves into `schema-drift.ts` as the single definition,
so the names the driver creates and the names the differ looks for cannot
diverge. Postgres index introspection reads `pg_index` rather than `pg_indexes`
so indexes backing a UNIQUE CONSTRAINT — exactly what knex's old `col.unique()`
produced — are visible to the detector.

Also fixed: `managedObjectIndexes` was never cleared when an object dropped its
`indexes[]`, so drift detection kept expecting an index nobody declared.

`SchemaDiffEntryKind` gains `index_mismatch` and `unmapped_index`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LJzroBbHM7FTpHXrtv4pm
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 1:05am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/driver-sql, @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 02:08
@os-zhuang
os-zhuang merged commit dac6a08 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/unique-index-migration-ddl-ybji9k branch July 28, 2026 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unique 索引迁移在启动时静默执行 DDL,os migrate plan 看不到 —— 运维无预检手段

2 participants