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

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.

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