Skip to content

feat(driver-sql)!: NULL 安全的组织级唯一约束 + 声明索引 'organization' 作用域(ADR-0120 D3/D4,#5030) - #5212

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5030-null-safe-org-unique
Aug 4, 2026
Merged

feat(driver-sql)!: NULL 安全的组织级唯一约束 + 声明索引 'organization' 作用域(ADR-0120 D3/D4,#5030)#5212
os-zhuang merged 1 commit into
mainfrom
claude/issue-5030-null-safe-org-unique

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5030

合并次序(请勿提前合并):等 v17.0 cut 之后合入,且必须先于 #4986 的 spec/lint PR —— driver 侧先落地,保证 spec 侧接受 'organization' 时驱动侧的兑现已经在位(ADR-0120 D7 的刻意次序)。

背景

#5030 实测:SQL UNIQUE 是 NULL-distinct 的,#3696(organization_id, field) 复合唯一索引在 organization 为 NULL 的行上什么都不约束 —— 而单租户栈上内核无条件注入该列且从不填值,即每一行都是 NULL:字段级 unique: true 在单租户部署上是静默零约束。ADR-0120(Status: Accepted)裁定方向,本 PR 是 17.x 波第一块(driver 侧 D3+D4)。

改动(packages/plugins/driver-sql + 守卫点 plugin-auth)

D3 — 组织键部 NULL 安全化

drift 两侧同一 normalize(#4884 纪律延伸)

D4 — 收紧走仪式 + 重复数据预检

  • 存量 (organization_id, X) → COALESCE 形是 recreate_index(tightenNullSafeOnly)纯收紧,由重复数据预检裁决:探测干净 → 判 safe(dev autoMigrate: 'safe' / 普通 os migrate apply 可应用,unique 索引迁移在启动时静默执行 DDL,os migrate plan 看不到 —— 运维无预检手段 #3728 机制);发现重复(旧空约束错误放进来的数据)→ op 阻塞,按组报告冲突行,旧索引原地保留。apply 期重探,--allow-destructive 也无法在替代索引不可建时先删旧索引;创建失败还有裸复合索引回滚兜底(durability 级 error 日志)。CLI 面零改动 —— 复用既有 plan/apply 通道。
  • MySQL < 8.0.13 / MariaDB 不支持函数键部:降级为裸复合 + error 级说明未兑现范围,drift 持续报告待服务器升级后收紧。

'__global__' 保留守卫(ADR-0120 D3 guardrail)

  • 铸造点本仓可达:plugin-auth beforeCreateOrganization 钩子拒绝 id/slug 等于 '__global__' 的组织创建,报错指向 ADR-0120 D3。

测试(ADR-0120 D6.6)

测试证据

pnpm --filter @objectstack/driver-sql vitest run   → 56 passed | 4 skipped (665 tests passed)
pnpm --filter @objectstack/driver-sql typecheck    → clean
pnpm --filter @objectstack/plugin-auth vitest run  → 32 passed (715 tests passed)
pnpm --filter @objectstack/plugin-auth typecheck   → clean
runtime real-driver 集成测试(5 文件)            → passed
node scripts/check-adr-anchors.mjs                 → OK (22 anchored)

边界(刻意不做)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Akrzh2mHi2siSNVPPtfTw7


Generated by Claude Code

…rganization' scope (ADR-0120 D3/D4, #5030)

Every organization-scoped unique index now materializes its organization key
part as COALESCE(organization_id, '__global__') instead of the bare column:
SQL UNIQUE is NULL-distinct, so the #3696 composite enforced nothing on
NULL-organization rows — on a single-tenant stack, every row (#5030, measured).

- uniqueIndexesFromFields / legacy replacements emit the NULL-safe key part;
  field-level unique: 'organization' accepted as the explicit synonym of true.
- normalizeDeclaredIndex / expectedIndexes / syncDeclaredIndexes learn declared
  unique: 'organization' (prepend the org key part; degrade with no tenant
  column; a listed tenant column is made NULL-safe in place). 'global' / bare
  true stays VERBATIM — the #3696 contract, now the 'global' arm (spec token
  lands separately via #4986; driver deliberately merges first).
- Drift both sides share one normalization: physical COALESCE(col, <literal>)
  attributes to col, compared literal-agnostically; the org key part is the
  sync's own vocabulary (isSyncReproducibleIndex + tenantField), scoped so the
  ADR-0048 overlay indexes keep their #4884 protection.
- D4 ceremony: the bare-composite tightening is a recreate_index gated by a
  duplicate pre-flight probe — clean → safe (dev autoMigrate applies), dirty →
  BLOCKED with a row report, old index kept; apply re-probes so even
  --allow-destructive cannot drop a constraint whose replacement fails.
- plugin-auth: '__global__' reserved as organization id/slug at the
  beforeCreateOrganization seam (ADR-0120 D3 guardrail).
- Tests per ADR-0120 D6.6: contract header rewritten, #5030 probe graduated as
  a permanent regression, NULL-bucket semantics, declared 'organization'
  pins, pre-flight both states; 'exactly as authored' retained for 'global'.
- ADR anchors for schema-drift.ts / sql-driver.ts (PD #13).

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

vercel Bot commented Aug 4, 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 Aug 4, 2026 9:19am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/plugin-auth.

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

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/contracts/cache-service.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql, @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/plugin-auth)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

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 August 4, 2026 11:51
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 4addd9d Aug 4, 2026
37 of 38 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5030-null-safe-org-unique branch August 4, 2026 12:02
os-zhuang pushed a commit that referenced this pull request Aug 4, 2026
Resolves the scripts/adr-anchors.json conflict by keeping all five
ADR-0120 anchor entries (spec/lint side + driver side from #5212), and
clears the os-regen deferred merge of packages/spec/api-surface.json via
spec build + gen:api-surface (isOrganizationUnique retained).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Akrzh2mHi2siSNVPPtfTw7
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

2 participants