2323 *
2424 * ## What each leg pins
2525 *
26- * - boot 1 — a fresh `os dev` creates the NULL-safe index; with
27- * `OS_TELEMETRY_DB=0` it provisions no telemetry sibling (the opt-out).
28- * - boot 2 — after the card's step 3 restages the bare index, a plain `os dev`
29- * restart puts the NULL-safe one back, says `auto-reconciled`, and provisions
26+ * - boot 1 — a fresh `os dev` creates the plain `(organization_id, qa_code)`
27+ * composite (ADR-0131 D1: no COALESCE) and no legacy single-column UNIQUE;
28+ * with `OS_TELEMETRY_DB=0` it provisions no telemetry sibling (the opt-out).
29+ * - boot 2 — after the staging step plants the legacy platform-wide UNIQUE on
30+ * `qa_code` beside the composite, a plain `os dev` restart drops it (a pure
31+ * relaxation, the composite survives), says `auto-reconciled`, and provisions
3032 * `A.telemetry.db` (default-on in dev for a file-backed SQLite primary).
3133 * - boot 3 — a production boot (`os serve`, `NODE_ENV` unset) on the restaged
3234 * file reports the drift and leaves it: production never auto-migrates.
3335 *
36+ * ⚠️ Why not the drift the card staged. #21733 reproduced with the ADR-0120 D4
37+ * tightening — a bare composite against the NULL-safe expectation, a
38+ * `recreate_index` the duplicate probe recategorised `safe` (the table above).
39+ * ADR-0131 D1 retired it: the expectation is the plain composite now, so a bare
40+ * composite MATCHES and there is no drift at all. The defect is about the
41+ * standalone stack never applying the self-heal's `safe` subset, not about any
42+ * one op, so the staging carries the `safe` op that is still live:
43+ * `replace_unique_index`, the legacy global UNIQUE an older release left (the
44+ * same fixture `src/utils/schema-migrate.dev-self-heal-fence.integration.test.ts`
45+ * stages).
46+ *
3447 * The host-config path is pinned unchanged where it is decided
3548 * (`src/utils/storage-driver.test.ts`), and both hosts are held to one answer
3649 * per driver kind by `src/utils/dev-self-heal-host-parity.test.ts`; the
@@ -53,6 +66,7 @@ import { tmpdir } from 'node:os';
5366import { join , resolve } from 'node:path' ;
5467import { fileURLToPath } from 'node:url' ;
5568import Database from 'better-sqlite3' ;
69+ import { legacyUniqueIndexNames } from '@objectstack/driver-sql' ;
5670import {
5771 childEnv ,
5872 E2E_SECRET_KEY ,
@@ -72,7 +86,13 @@ const READY = /Press Ctrl\+C to stop/;
7286const BOOT_TIMEOUT_MS = 180_000 ;
7387const ALL_BOOTS_TIMEOUT_MS = 3 * ( BOOT_TIMEOUT_MS + 30_000 ) ;
7488
89+ /** The organization composite the declaration asks for (ADR-0131 D1: plain key parts). */
7590const INDEX = 'uniq_scaf_item_organization_id_qa_code' ;
91+ /**
92+ * The legacy platform-wide single-column UNIQUE, spelled by the driver's own
93+ * `legacyUniqueIndexNames` — its `uniq_` arm, the one the differ recognises.
94+ */
95+ const LEGACY = legacyUniqueIndexNames ( 'scaf_item' , 'qa_code' ) [ 1 ] ;
7696
7797/** The card's blank scaffold: one object, an org-scoped unique field, no plugins, no datasources. */
7898const SCAFFOLD = {
@@ -86,8 +106,10 @@ const SCAFFOLD = {
86106} ;
87107
88108interface BootRecord {
89- /** The index DDL `sqlite_master` holds after the boot stopped. */
109+ /** The composite's DDL `sqlite_master` holds after the boot stopped. */
90110 indexSql : string | undefined ;
111+ /** The legacy global UNIQUE's DDL after the boot stopped; `undefined` when absent. */
112+ legacySql : string | undefined ;
91113 telemetryExists : boolean ;
92114 output : string ;
93115}
@@ -97,10 +119,10 @@ let dir: string;
97119let db : string ;
98120const record : Partial < Record < 'fresh' | 'restart' | 'production' , BootRecord | Error > > = { } ;
99121
100- function readIndexSql ( ) : string | undefined {
122+ function readIndexSql ( name : string ) : string | undefined {
101123 const handle = new Database ( db , { readonly : true } ) ;
102124 try {
103- const row = handle . prepare ( "SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?" ) . get ( INDEX ) as
125+ const row = handle . prepare ( "SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?" ) . get ( name ) as
104126 | { sql : string }
105127 | undefined ;
106128 return row ?. sql ;
@@ -109,11 +131,16 @@ function readIndexSql(): string | undefined {
109131 }
110132}
111133
112- /** The card's step 3: replace the NULL-safe unique index with the bare one an older release left. */
113- function stageBareIndex ( ) : void {
134+ /**
135+ * The staging step: plant, beside the plain composite, the platform-wide UNIQUE
136+ * on `qa_code` a pre-#3696 release left — the state every additive boot after
137+ * such an upgrade produces, since the sync creates the composite by name and
138+ * never drops anything. Idempotent, so it restages after a heal.
139+ */
140+ function stageLegacyIndex ( ) : void {
114141 const handle = new Database ( db ) ;
115142 try {
116- handle . exec ( `DROP INDEX ${ INDEX } ; CREATE UNIQUE INDEX ${ INDEX } ON scaf_item (organization_id, qa_code);` ) ;
143+ handle . exec ( `CREATE UNIQUE INDEX IF NOT EXISTS ${ LEGACY } ON scaf_item (qa_code);` ) ;
117144 } finally {
118145 handle . close ( ) ;
119146 }
@@ -200,7 +227,12 @@ async function stopGroup(child: ChildProcess): Promise<void> {
200227async function recordBoot ( leg : 'fresh' | 'restart' | 'production' , boot : Promise < string > ) : Promise < void > {
201228 try {
202229 const output = await boot ;
203- record [ leg ] = { indexSql : readIndexSql ( ) , telemetryExists : existsSync ( join ( dir , 'A.telemetry.db' ) ) , output } ;
230+ record [ leg ] = {
231+ indexSql : readIndexSql ( INDEX ) ,
232+ legacySql : readIndexSql ( LEGACY ) ,
233+ telemetryExists : existsSync ( join ( dir , 'A.telemetry.db' ) ) ,
234+ output,
235+ } ;
204236 } catch ( error ) {
205237 record [ leg ] = error instanceof Error ? error : new Error ( String ( error ) ) ;
206238 }
@@ -213,7 +245,9 @@ function recorded(leg: 'fresh' | 'restart' | 'production'): BootRecord {
213245 return r ;
214246}
215247
216- const isNullSafe = ( sql : string | undefined ) : boolean => / C O A L E S C E \( ` ? o r g a n i z a t i o n _ i d ` ? / . test ( sql ?? '' ) ;
248+ /** The composite in its ADR-0131 D1 form: both key parts plain, no COALESCE. */
249+ const isPlainComposite = ( sql : string | undefined ) : boolean =>
250+ / \( \s * [ ` " ] ? o r g a n i z a t i o n _ i d [ ` " ] ? \s * , \s * [ ` " ] ? q a _ c o d e [ ` " ] ? \s * \) / . test ( sql ?? '' ) && ! / C O A L E S C E / i. test ( sql ?? '' ) ;
217251
218252beforeAll ( async ( ) => {
219253 requireBuiltCli ( RUN_JS_RESOLVES_FROM_DIST ) ;
@@ -224,12 +258,12 @@ beforeAll(async () => {
224258 // Boot 1: fresh, with the telemetry opt-out.
225259 await recordBoot ( 'fresh' , bootUntilReady ( [ 'dev' , '--no-watch' , '-d' , 'file:A.db' , '-p' , KERNEL_ASSIGNED_PORT ] , { OS_TELEMETRY_DB : '0' } ) ) ;
226260
227- // Boot 2: the card's restart, on the restaged bare index, telemetry default-on.
228- if ( existsSync ( db ) ) stageBareIndex ( ) ;
261+ // Boot 2: the card's restart, on the staged legacy index, telemetry default-on.
262+ if ( existsSync ( db ) ) stageLegacyIndex ( ) ;
229263 await recordBoot ( 'restart' , bootUntilReady ( [ 'dev' , '--no-watch' , '-d' , 'file:A.db' , '-p' , KERNEL_ASSIGNED_PORT ] , { } ) ) ;
230264
231- // Boot 3: production (`os serve`, NODE_ENV unset) on the restaged bare index.
232- if ( existsSync ( db ) ) stageBareIndex ( ) ;
265+ // Boot 3: production (`os serve`, NODE_ENV unset) on the restaged legacy index.
266+ if ( existsSync ( db ) ) stageLegacyIndex ( ) ;
233267 await recordBoot ( 'production' , bootUntilReady ( [ 'serve' , 'objectstack.config.ts' , '-p' , KERNEL_ASSIGNED_PORT ] , {
234268 OS_DATABASE_URL : 'file:A.db' ,
235269 OS_AUTH_SECRET : 'e2e-21733-production-leg-secret-not-for-real-use' ,
@@ -243,16 +277,19 @@ afterAll(async () => {
243277} ) ;
244278
245279describe ( '#21733 — a plain `os dev` (standalone stack) self-heals and provisions telemetry' , ( ) => {
246- it ( 'a fresh `os dev` creates the NULL-safe index , and OS_TELEMETRY_DB=0 provisions no sibling' , ( ) => {
280+ it ( 'a fresh `os dev` creates the plain organization composite , and OS_TELEMETRY_DB=0 provisions no sibling' , ( ) => {
247281 const fresh = recorded ( 'fresh' ) ;
248- expect ( isNullSafe ( fresh . indexSql ) , `fresh boot index: ${ fresh . indexSql } ` ) . toBe ( true ) ;
282+ expect ( isPlainComposite ( fresh . indexSql ) , `fresh boot index: ${ fresh . indexSql } ` ) . toBe ( true ) ;
283+ expect ( fresh . legacySql , 'the fresh boot created the legacy global unique itself' ) . toBeUndefined ( ) ;
249284 expect ( fresh . telemetryExists , 'OS_TELEMETRY_DB=0 still created A.telemetry.db' ) . toBe ( false ) ;
250285 } ) ;
251286
252- it ( 'a plain `os dev` restart puts the staged bare unique index back NULL-safe ' , ( ) => {
287+ it ( 'a plain `os dev` restart relaxes the staged legacy global unique to the organization composite ' , ( ) => {
253288 const restart = recorded ( 'restart' ) ;
254- expect ( isNullSafe ( restart . indexSql ) , `the restart left the bare index: ${ restart . indexSql } ` ) . toBe ( true ) ;
255- expect ( restart . output ) . toContain ( '[schema-drift] auto-reconciled recreate_index on scaf_item' ) ;
289+ expect ( restart . legacySql , `the restart left the legacy index: ${ restart . legacySql } ` ) . toBeUndefined ( ) ;
290+ // A relaxation, not a removal: the per-organization constraint survives.
291+ expect ( isPlainComposite ( restart . indexSql ) , `the restart lost the composite: ${ restart . indexSql } ` ) . toBe ( true ) ;
292+ expect ( restart . output ) . toContain ( '[schema-drift] auto-reconciled replace_unique_index on scaf_item.qa_code' ) ;
256293 } ) ;
257294
258295 it ( 'the same restart provisions the `telemetry` sibling next to the file-backed primary' , ( ) => {
@@ -262,8 +299,9 @@ describe('#21733 — a plain `os dev` (standalone stack) self-heals and provisio
262299
263300 it ( 'a production boot reports the staged drift and does not auto-migrate it' , ( ) => {
264301 const production = recorded ( 'production' ) ;
265- expect ( production . output ) . toContain ( `index ' ${ INDEX } ' tightens ` ) ;
302+ expect ( production . output ) . toContain ( `a legacy platform-wide UNIQUE index ( ${ LEGACY } ) ` ) ;
266303 expect ( production . output ) . not . toContain ( '[schema-drift] auto-reconciled' ) ;
267- expect ( isNullSafe ( production . indexSql ) , `production auto-migrated: ${ production . indexSql } ` ) . toBe ( false ) ;
304+ expect ( production . legacySql , 'production auto-migrated the legacy index away' ) . toBeDefined ( ) ;
305+ expect ( isPlainComposite ( production . indexSql ) ) . toBe ( true ) ;
268306 } ) ;
269307} ) ;
0 commit comments