Skip to content

Commit f98fa65

Browse files
fix(driver-sql): stop a fresh DB booting "drifted", and never point --allow-destructive at a framework-created index (#4884) (#4954)
A brand-new empty SQLite file printed two `[schema-drift]` warnings before the server was ready, both about the ADR-0048 overlay indexes the same boot had just created. The second told the operator to `os migrate apply --allow-destructive` away `idx_sys_metadata_overlay_draft` — the unique index enforcing draft-overlay uniqueness. Three fixes, all on the detector side (no metadata declaration changed: `sys-metadata.object.ts` documents its four-column entry as the fallback shape for drivers WITHOUT the runtime migration, and that contract still holds): - Read the index key from its DEFINITION, not from the dialect's per-column catalogue view. `PRAGMA index_info` reports a NULL column for an expression key, so `(type, name, organization_id, COALESCE(package_id,''))` arrived as three columns. SQLite/Postgres now parse `sqlite_master.sql` / `pg_get_indexdef`, MySQL reads `STATISTICS.EXPRESSION` where available, and `COALESCE(col, <literal>)` is attributed to `col`. - Capture the partial predicate. An index `syncDeclaredIndexes` can neither create nor rebuild is never claimed as ours, never called orphaned, and never given a remedy this differ could not undo (`isSyncReproducibleIndex`). - Keep a ledger of the index DDL the driver itself executed through raw `execute()` — how `ensureOverlayIndex` issues its migration — so an index the framework created on this boot is provably its own, including the plain-index fallback it takes on dialects that reject partial indexes. Genuine drift is untouched: orphaned generated indexes, redefined declared indexes and the #3696 legacy-unique replacement are all still detected and categorised exactly as before. Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX Co-authored-by: Claude <noreply@anthropic.com>
1 parent eb3e650 commit f98fa65

5 files changed

Lines changed: 858 additions & 24 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
fix(driver-sql): a fresh database no longer boots "drifted", and the drift
6+
detector never points `--allow-destructive` at an index the framework created
7+
(#4884)
8+
9+
Booting `examples/app-showcase` on a brand-new empty SQLite file printed two
10+
`[schema-drift]` warnings before the server was even ready, both about the
11+
ADR-0048 overlay indexes the same boot had just created. Both were false, and
12+
one of them was dangerous:
13+
14+
> `[schema-drift] sys_metadata: index 'idx_sys_metadata_overlay_draft' UNIQUE
15+
> (type, name, organization_id) carries ObjectStack's generated naming but
16+
> matches no declared index (orphaned) — "os migrate apply --allow-destructive"
17+
> to drop it.`
18+
19+
`idx_sys_metadata_overlay_draft` is the unique index enforcing **draft-overlay
20+
uniqueness**. An operator following our own boot advice would have dropped a
21+
live data-integrity guarantee to fix a problem that did not exist — and, worse,
22+
learned to treat `--allow-destructive` as routine boot hygiene, which is exactly
23+
what makes the *next*, real drift warning dangerous.
24+
25+
Three fixes, in the driver's detector only (no metadata declaration changed —
26+
`sys-metadata.object.ts` documents its four-column `indexes[]` entry as *the
27+
fallback shape for drivers without the runtime migration*, and that contract
28+
still holds for the drivers that rely on it):
29+
30+
- **The index key is now read as written.** Introspection took the key from each
31+
dialect's per-column catalogue view (`PRAGMA index_info`, `pg_attribute`,
32+
`STATISTICS.COLUMN_NAME`), which describes an expression key as a NULL column
33+
and nothing else. The canonical
34+
`(type, name, organization_id, COALESCE(package_id,''))` overlay index
35+
therefore arrived as three columns and was reported as a mismatch against its
36+
own four-column declaration. SQLite and Postgres now parse the index
37+
definition (`sqlite_master.sql` / `pg_get_indexdef`), MySQL reads
38+
`STATISTICS.EXPRESSION` where the server has it, and `COALESCE(col, <literal>)`
39+
is recognised as keying on `col` — which is what ADR-0048 uses it for: a plain
40+
UNIQUE index treats NULLs as distinct, so package-less globals would not be
41+
unique among themselves.
42+
- **Partial predicates are captured.** A `WHERE`-restricted index is something
43+
`syncDeclaredIndexes` can neither create nor rebuild, so the detector no
44+
longer claims authorship of one, no longer calls it orphaned, and never
45+
proposes a remedy it could not undo.
46+
- **The driver keeps a ledger of the index DDL it executed.** An index this
47+
process created through raw `execute()` — how `metadata-protocol`'s
48+
`ensureOverlayIndex` issues its migration — is the framework's to manage. This
49+
also covers the plain-index fallback the same migration takes on dialects that
50+
reject partial indexes.
51+
52+
Genuine drift is unaffected: an orphaned generated index, a redefined declared
53+
index and the #3696 legacy-unique replacement are all still detected, still
54+
categorised exactly as before, and still remediable through `os migrate`.

‎packages/plugins/driver-sql/src/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type {
1414

1515
// Managed-schema drift / reconcile (#2186), incl. the index dimension (#3728)
1616
export {
17+
applyIndexKeyParts,
18+
classifyIndexKeyPart,
1719
diffManagedTable,
1820
driftKey,
1921
fieldHasColumn,
@@ -24,9 +26,12 @@ export {
2426
isIndexDriftOp,
2527
isInPlaceSchemaWork,
2628
isManagedIndexName,
29+
isRuntimeManagedIndex,
30+
isSyncReproducibleIndex,
2731
legacyUniqueIndexNames,
2832
legacyUniqueReplacements,
2933
normalizeDeclaredIndex,
34+
parseIndexDdl,
3035
uniqueIndexesFromFields,
3136
INDEX_DRIFT_OPS,
3237
} from './schema-drift.js';
@@ -35,6 +40,8 @@ export type {
3540
DriftOp,
3641
DriftCategory,
3742
SqlDialectName,
43+
IndexKeyPart,
44+
ParsedIndexDdl,
3845
PhysicalColumn,
3946
PhysicalIndex,
4047
ExpectedIndex,

0 commit comments

Comments
 (0)