Skip to content

[schema-drift] Fresh DB boots "drifted": detector can't read COALESCE index columns, then tells the operator to --allow-destructive away a legitimate unique index #4884

Description

@xuyushun441-sys

What happens

Boot examples/app-showcase on a brand-new empty SQLite file
(os dev --seed-admin -d file:/tmp/fresh.db). Before the server is even ready:

[schema-drift] sys_metadata: index 'idx_sys_metadata_overlay_active' is UNIQUE (type, name, organization_id)
  but metadata declares UNIQUE (type, name, organization_id, package_id) — the additive sync skips it by name,
  so it must be rebuilt. Creating the UNIQUE index can fail on existing duplicates: "os migrate apply --allow-destructive".

[schema-drift] sys_metadata: index 'idx_sys_metadata_overlay_draft' UNIQUE (type, name, organization_id)
  carries ObjectStack's generated naming but matches no declared index (orphaned) —
  "os migrate apply --allow-destructive" to drop it.

A database created seconds ago, by this build, cannot meaningfully be drifted
from its own declaration. Both warnings are false, and the second one is
actively dangerous.

Both indexes are correct — the detector misreads them

What is actually in the fresh DB:

CREATE UNIQUE INDEX idx_sys_metadata_overlay_active
  ON sys_metadata (type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'active';
CREATE UNIQUE INDEX idx_sys_metadata_overlay_draft
  ON sys_metadata (type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'draft';

Both are exactly what protocol.ts ensureOverlayIndex (~L2165-2190) is
designed to create, and the ADR-0048 comment there explains why the
COALESCE(package_id,'') form is the canonical one: a plain unique index treats
NULLs as distinct, so package-less globals would not be unique among themselves.

sys-metadata.object.ts (~L205-218) even says so outright — the
['type','name','organization_id','package_id'] declaration is "the fallback
shape for drivers without the runtime migration"
, not the shape the SQL driver
is expected to end up with.

So two separate misreads:

  1. overlay_active — the detector cannot parse the COALESCE(package_id, '')
    expression column. It drops it, reads the live index as
    (type, name, organization_id), and reports a mismatch against a 4-column
    declaration. The 4th column is there; it is just an expression.
  2. overlay_draft — created by the same runtime path, has no counterpart in
    the object declaration at all, so the detector classifies it as orphaned.

Why #2 is the dangerous one

The remedy printed for the "orphaned" index is:

"os migrate apply --allow-destructive" to drop it.

idx_sys_metadata_overlay_draft is the unique index enforcing draft-overlay
uniqueness
. An operator following our own boot advice would drop a live data-
integrity guarantee — on a healthy database, to fix a problem that does not
exist. That is the opposite of what a destructive-migration prompt should ever be
pointed at.

It also trains operators to treat --allow-destructive as routine boot hygiene,
which is precisely the habit that makes the next real drift warning dangerous.

Suggested direction

  • Teach the drift comparator to normalize expression columns (COALESCE(x,'')
    x for identity purposes) before diffing, so the canonical runtime index
    compares equal to its declaration.
  • Let a runtime-managed index declare itself as such, so overlay_draft is not
    orphan-by-omission. Either add it to the sys-metadata declaration or give
    ensureOverlayIndex-created indexes an exemption the detector honours.
  • Never emit an --allow-destructive remedy for an index the framework itself
    created on this same boot.

Repro

cd examples/app-showcase
node ../../packages/cli/bin/run.js dev --ui --seed-admin -p 3000 -d file:/tmp/fresh.db
# both [schema-drift] lines appear before "Server is ready"
sqlite3 /tmp/fresh.db ".schema sys_metadata" | grep overlay

Observed on main @ 0e96e46 with SqlDriver(better-sqlite3), single-tenant.

Found while browser-sweeping showcase + Studio for #4879.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions