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:
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.
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.
What happens
Boot
examples/app-showcaseon a brand-new empty SQLite file(
os dev --seed-admin -d file:/tmp/fresh.db). Before the server is even ready: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:
Both are exactly what
protocol.tsensureOverlayIndex(~L2165-2190) isdesigned to create, and the ADR-0048 comment there explains why the
COALESCE(package_id,'')form is the canonical one: a plain unique index treatsNULLs 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 fallbackshape for drivers without the runtime migration", not the shape the SQL driver
is expected to end up with.
So two separate misreads:
overlay_active— the detector cannot parse theCOALESCE(package_id, '')expression column. It drops it, reads the live index as
(type, name, organization_id), and reports a mismatch against a 4-columndeclaration. The 4th column is there; it is just an expression.
overlay_draft— created by the same runtime path, has no counterpart inthe 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:
idx_sys_metadata_overlay_draftis the unique index enforcing draft-overlayuniqueness. 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-destructiveas routine boot hygiene,which is precisely the habit that makes the next real drift warning dangerous.
Suggested direction
COALESCE(x,'')≡xfor identity purposes) before diffing, so the canonical runtime indexcompares equal to its declaration.
overlay_draftis notorphan-by-omission. Either add it to the
sys-metadatadeclaration or giveensureOverlayIndex-created indexes an exemption the detector honours.--allow-destructiveremedy for an index the framework itselfcreated on this same boot.
Repro
Observed on
main@ 0e96e46 withSqlDriver(better-sqlite3), single-tenant.Found while browser-sweeping showcase + Studio for #4879.