Skip to content

Commit df67985

Browse files
fix(driver-sql): refusals, drift reports and log lines state each decision in words instead of a tracker number (stage 1) (#20795)
Part of #20513 Clause-②: no **Stage 1 of 5 of this lane (`driver-sql`), under the maintainer's A / A ruling on the card.** The card stays open for stages 2-5, so this PR carries no closing keyword. Text only: no error `code`, field name, HTTP status, export or control flow moves. Every changed line in the four source files is a string-literal line. ## What this does The SQL driver's refusals, drift reports and log lines sent the reader to a tracker number for the reason behind them. Each rewritten string now says that reason in words (form D, as the migration-entry rewrite applied it). Where the sentence already stated what was decided, only the citation goes. Where it did not, the decision is added in words: | Where | Cited | The sentence now says | |---|---|---| | `sql-driver.ts` `reference_to` DDL refusal | 3896, 11567 | Raw `registerObject` skips Zod; the FOREIGN KEY DDL the key used to gate is retired, because it could never fire for a spec-conformant lookup. | | `sql-driver.ts` NULL-safe UNIQUE messages (drift plan, restore, hash-shadow, direct, bare-composite fallback) | 5030 | SQL UNIQUE is NULL-distinct, so the old index never constrained rows without an organization. | | `sql-driver.ts` foreign column in the hash-shadow slot | 11627, 13015 | The column is not the driver's to replace: it may hold data. | | `sql-driver.ts` SQLite canonicalisation info lines | 3912, 3994, 12380 | Datetime and time are now one text form, so filters and sorts compare values rather than mixed encodings; json cells are now JSON-encoded strings, the one encoding a json column holds on every dialect. | | `sql-driver.ts` MySQL widening info lines | 3942, 3994 | `TIMESTAMP` ends at 2038-01-19, keeps no milliseconds and converts through the session timezone; a zero-precision `TIME` rounds the canonical milliseconds to the second. | | `cross-field-conformance-cases.ts` case notes | 5146, 7597, 1051 (cloud), 7398, 5041 | Each note states the rule it guards: `$not` is NULL-safe because each leaf is totalised; the `=` spelling of a field reference lowers to `$eq`; only a declared column reaches a SQL identifier position; scalar operators on a JSON column compare the whole serialized text; `$field` comparands compile, and the positions that cannot compile are refused. | | everything else (filter refusals, withheld-detail wording, TEXT-key and row-size explanations, other hash-shadow lines, drift entries, pagination warning, testkit error) | 4363, 4560, 5158, 5240, 5347, 5348, 5369, 5907 (uncompilable), 6050, 6212, 7398, 7929, 8197, 8220, 8790, 8931, 9350, 11067, 11374, 11535, 11541, 11565, 11627, 12121, 12999, 13015, 13056, 3696 | The sentence already said what was decided; only the citation goes. | The withheld-detail wording now reads "withheld from the message; the full diagnostic is in the server log". That is exactly the Turso remote transport's existing copy, which `remote-transport-compile-refusal-seam.test.ts` derives from this driver's sentence by stripping the citation, so the pin still holds. ## Three strings kept for the driver-turso stage Three aggregate refusals keep their citation: `sql-driver.ts` about 1674 (undeclared function, 5907), 1778 (per-aggregation `filter`, 10413) and 1812 (`count_distinct` without `field`, 6409). Their twins in `driver-turso`'s `remote-transport.ts` (about 948, 1071 and 1014) carry the same citation. Tests compare the two faces byte for byte: - `remote-transport-aggregate-function-refusal.test.ts` about 348, 408, 510 and 576; - `remote-transport-undeclared-aggregate-keys.test.ts` about 201. So one face cannot drop the number alone. Measured with a one-off ablation on the committed head: deleting only the 5907 citation here, rebuilding `driver-sql` (the marker present in `dist/`, 2 files) and running those two driver-turso files gave **8 failed, 25 passed**. After the restore (blob equal to HEAD, `git diff HEAD` empty, the marker absent from `dist/`, working tree clean) they gave 33/33. No test file was left behind. The order forbids moving another package's ledger rows here, so these three rows (`#5907: 1`, `#10413: 1`, `#6409: 1`) are handed to the `driver-turso` stage, where both twins change together. ## Pins re-pinned: 18 assertion lines in 6 test files Each asserted a number; each now asserts the words that carry the decision: - `#5030` (12 lines): `sql-driver-unique-violation-predicate.test.ts` 199, 208, 218, 232, 289; `sql-driver-14902-plain-unique-duplicate-preflight.test.ts` 222, 301, 308; `sql-driver-15479-shadow-plain-unique-duplicates.test.ts` 172, 199, 222; `sql-driver-unique-tenancy.test.ts` 694. They now assert `NULL-distinct`. The negative pins (289, 222 in 14902, 172 and 199 in 15479) keep their meaning: the plain-unique messages do not borrow the NULL-safe arm's story. - `#11374` (2) and `#12999` (3): `sql-driver-12999-stale-text-remedy.test.ts` 193, 243 and 196, 210, 224. They now assert the declare-`maxLength` sentence, and the absence of the stale-column sentence ("re-declaring `maxLength` changes nothing here"). - `#11535` (1): `schema-drift.base-type-mismatch.test.ts` 114. It now asserts "receives one opaque id instead of a list". ## Ledger burn-down `scripts/doc-authoring-prose-id.baseline.json` was regenerated with `node scripts/check-doc-authoring.mjs --census-ledger`. It was generated into a scratch file first, so the growth refusal ran against the checked-in baseline, and then copied into place. Only `driver-sql` rows moved: | File | Occurrences before | after | (file, id) pairs before | after | |---|---:|---:|---:|---:| | `driver-sql/src/sql-driver.ts` | 62 | 3 | 32 | 3 | | `driver-sql/src/cross-field-conformance-cases.ts` | 6 | 0 (row leaves) | 5 | 0 | | `driver-sql/src/schema-drift.ts` | 4 | 0 (row leaves) | 4 | 0 | | `driver-sql/src/live-dialect-matrix.testkit.ts` | 1 | 0 (row leaves) | 1 | 0 | | **driver-sql total** | **73** | **3** | **42** | **3** | Whole ledger: files 227 to 224, pairs 611 to 572, occurrences 931 to 861. The gate's printed "pinned site(s)" (id-bearing string literals) go 794 to 732. A census of every other package's sites before and after is identical. **The two counts on the card, reconciled.** The census counted 52 messages, and the ledger holds 73 occurrences. The census's 52 are `sql-driver.ts` 48 plus `schema-drift.ts` 4. It folds a `+` chain into one message, so its 48 messages are the gate's 54 string literals. Those 52 messages carry 66 occurrences, the ledger's 62 + 4. The census listing prints ids deduplicated per message, which gives 65. The one gap is the hash-shadow warning, whose two ternary branches each cite 13015. The remaining 7 occurrences are the 7 `src/`-shipped test strings (the six case notes and the testkit error), which sit outside the census's 52-message population. ## Verification (head `733b0e6815`) - **Build.** `turbo run build` over the `driver-turso` and `driver-sqlite-wasm` closures (which include `driver-sql`): 9/9. Then every package under `packages/`: 71/71. Both runs exited 0 under the verify lock. - **Built output.** The new sentences are in both `dist/index.js` and `dist/index.mjs`. The three held citations are the only runtime citations left in them; the other hits are docblocks, which the bundle keeps. - **Tests.** - `@objectstack/driver-sql`: 200 files passed and 11 skipped (the live PG / MySQL cells, no server here); 3,248 tests passed and 188 skipped. - The six re-pinned files, run on their own: 76 passed, 4 skipped. `sql-driver-15479-shadow-plain-unique-duplicates.test.ts` is MySQL-only. - `@objectstack/driver-turso` (the byte-parity consumer, reading the rebuilt `dist/`): 80 files, 2,195 tests passed, 33 skipped. - `@objectstack/driver-sqlite-wasm` (extends `SqlDriver`): 36 files, 675 tests passed. - **Typecheck.** `@objectstack/driver-sql typecheck` exits 0; `--listFiles` shows it reads all 211 of the package's test files. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands` (no paths; 12 paths against merge base `085ca6bc1`) names 71 commands, and all 71 exit 0. `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` first answered PREREQUISITE NOT MET (exit 3) on the partial build. After the full build they exit 0, and `check:dts-closure` was re-run there too (71 packages swept). `--ran` reconciliation: "71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN". `check:doc-authoring`: "sibling-package prose ids hold the baseline — 732 pinned site(s) across 224 file(s) ... no growth, no burn-down unrecorded". - **Lint, narrowed.** `eslint --no-inline-config --format json` over the 10 touched TypeScript files reports 10 files, 0 errors and 0 warnings. The population is the touched files, and the count is read from eslint's JSON. The narrowing is safe because `eslint.config.mjs` enables no type-aware linting: the resolved `parserOptions` are `ecmaVersion` and `sourceType` only, with no `project` or `projectService`. So this diff cannot move an untouched file's verdict. The repo-wide `pnpm lint` is CI's. - **NOT MEASURED here (CI's):** the live PG / MySQL cells of the `driver-sql` suite, including the three re-pins in the MySQL-only 15479 file. The required `Temporal Conformance (live PG + MySQL)` job runs the whole `driver-sql` suite against both servers. Also CI's: the Test Core shards, the Dogfood jobs and the workspace type-check lanes. ## Acceptance notes - **Carrier for the three held strings: the `driver-turso` stage (stage 4 of this lane).** It should rewrite each pair together, the `driver-sql` string and its `remote-transport.ts` twin, and that PR's ledger diff then moves one `driver-sql` row as well. That is the same serial ledger, one stage later. - **Two twins now differ by the citation only.** The declared-but-uncompiled refusal (5907, about 1749 here and 987 there) and the date-bucket refusal (6212, about 1867 here and 1108 there). Their contract is first-sentence parity, which holds; the remote copies drop the number in the `driver-turso` stage. - **Cited cards read.** Of the 41 distinct cited ids, 35 read as closed and completed. Five answer 404: 11067, 11374, 12380, 12999 and 13015. They were read through their landing commits and today's docblocks. The cloud-repository card behind the `$field` identifier note is not readable from this session; that note is rewritten from its own sentence. Each rewritten sentence describes today's code: `$field` comparands are compiled (the enforce-or-remove question was answered "enforce"), the `=` lowering fix landed, and the FK DDL is retired. - **Comments untouched.** Code comments and docblocks citing the same numbers are not runtime strings and are not changed here. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 96e7244 commit df67985

12 files changed

Lines changed: 120 additions & 132 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
driver-sql refusals, drift reports and log lines no longer cite tracker numbers; each states the reason in words
6+
7+
Clause-②: no
8+
9+
Many messages the SQL driver shows to authors and operators ended with an issue-tracker number where
10+
the reason belonged. The number goes, and where the sentence did not already say what was decided, it
11+
now does:
12+
13+
- Filter refusals (`INVALID_FILTER`): the withheld-detail wording ("withheld from the message; the full
14+
diagnostic is in the server log"), the JSON-column, zero-operator, `$null` / `$exists`, undefined
15+
comparand and unknown-combinator refusals, and the filter-array refusal.
16+
- Schema and index messages: the `reference_to` DDL refusal (the FOREIGN KEY DDL that key used to gate
17+
is retired, because it could never fire for a spec-conformant lookup), the MySQL TEXT-key and
18+
row-size explanations, the hash-shadow UNIQUE messages, and the `os migrate plan` drift entries.
19+
- The NULL-safe UNIQUE messages now say why rows without an organization were never constrained: SQL
20+
UNIQUE is NULL-distinct.
21+
- Boot log lines for the SQLite datetime, time and json canonicalisation and the MySQL `TIMESTAMP` /
22+
`TIME` widening now say what the conversion is for.
23+
24+
Text only: no error code, field name, status or behaviour changes. Three aggregate refusals keep their
25+
citation for now, because a test in `@objectstack/driver-turso` compares them byte for byte with the
26+
Turso remote transport's copies; they change together with those copies.

‎packages/drivers/driver-sql/src/cross-field-conformance-cases.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export const CROSS_FIELD_CASES: readonly CrossFieldCase[] = [
189189
name: '$not of a cross-field $gt returns exactly the rows $gt does not',
190190
filter: { $not: { amount: { $gt: { $field: 'budget' } } } },
191191
expected: ['2', '3', '4', '5', '6'],
192-
note: '#5146 made `$not` NULL-safe by totalising its leaves; a cross-field leaf is total by construction, so the negation is the exact complement — including the NULL rows the JS evaluator returns.',
192+
note: '`$not` is NULL-safe because each of its leaves is totalised (TRUE or FALSE, never NULL), as the JS evaluator reads it; a cross-field leaf is total by construction, so the negation is the exact complement — including the NULL rows the JS evaluator returns.',
193193
},
194194
{
195195
name: '$not of a cross-field $eq returns exactly the rows $eq does not',
@@ -235,7 +235,7 @@ export const CROSS_FIELD_CASES: readonly CrossFieldCase[] = [
235235
},
236236
},
237237
expected: ['2', '4', '5'],
238-
note: 'The complement of {1} ∪ {3,6}. A guard hoisted to the top of the `$not` instead of sitting on each leaf re-admits rows here — the failure #5146 wrote its rewrite to avoid.',
238+
note: 'The complement of {1} ∪ {3,6}. A guard hoisted to the top of the `$not` instead of sitting on each leaf re-admits rows here — the failure that totalising each leaf, rather than guarding the whole `$not`, exists to avoid.',
239239
},
240240
{
241241
name: 'a cross-field comparison nested two combinators deep',
@@ -314,7 +314,7 @@ export const CROSS_FIELD_AUTHORED_CASES: readonly CrossFieldAuthoredCase[] = [
314314
authored: ['amount', '>', { $field: 'budget' }],
315315
loweredTo: { amount: { $gt: { $field: 'budget' } } },
316316
expected: ['1'],
317-
note: 'Untouched by #7597 and asserted anyway: if this moves, the harness moved rather than the lowering.',
317+
note: 'Untouched by the fix that lowers the `=` spelling of a field reference to `$eq`, and asserted anyway: if this moves, the harness moved rather than the lowering.',
318318
},
319319

320320
// ── The sugar's own structures, carrying a reference leaf ────────────────
@@ -393,7 +393,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [
393393
name: 'an undeclared column is refused at compile time',
394394
filter: { amount: { $gt: { $field: 'no_such_column' } } },
395395
diagnosticIncludes: ['not a declared field'],
396-
note: 'The `$field` value lands in a SQL IDENTIFIER position. cloud#1051: letting it through unchecked is dismantling the guard rail — and a compile-time refusal is what makes AI-authored metadata wrong at authoring time rather than in the database.',
396+
note: 'The `$field` value lands in a SQL IDENTIFIER position, and letting it through unchecked would dismantle the guard rail that only a declared column reaches SQL — and a compile-time refusal is what makes AI-authored metadata wrong at authoring time rather than in the database.',
397397
},
398398
{
399399
name: 'an undeclared TARGET field is refused too',
@@ -446,7 +446,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [
446446
name: 'a multi-valued (JSON) column is refused as the referent',
447447
filter: { amount: { $gt: { $field: 'tags' } } },
448448
diagnosticIncludes: ['no scalar stored'],
449-
note: 'A JSON column holds a serialized array; SQL comparison operators have no element-wise reading of it, and #7398 already refuses the scalar operators on such a column for a value comparand.',
449+
note: 'A JSON column holds a serialized array; SQL comparison operators have no element-wise reading of it, and the scalar operators are already refused on such a column for a value comparand, because they compare the whole serialized text.',
450450
},
451451
{
452452
name: 'a formula (virtual) column is refused as the referent',
@@ -479,7 +479,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [
479479
name: 'a $field member of an $in list is refused',
480480
filter: { amount: { $in: [{ $field: 'budget' }, 1] } },
481481
diagnosticIncludes: ['index 0'],
482-
note: 'Before #5041 this did not even crash: it compiled, ran, and returned ZERO ROWS. The index is named because it is the only thing distinguishing the bad member from its legitimate neighbours.',
482+
note: 'Before this driver compiled `$field` comparands and refused the positions it cannot compile, this did not even crash: it compiled, ran, and returned ZERO ROWS. The index is named because it is the only thing distinguishing the bad member from its legitimate neighbours.',
483483
},
484484
{
485485
name: 'a $field member of a $nin list is refused',

‎packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export function currentLiveSchema(): string {
216216
const testPath = expect.getState().testPath;
217217
if (!testPath) {
218218
throw new Error(
219-
'live-dialect isolation (#9350): vitest reported no testPath, so this live connection ' +
219+
'live-dialect isolation: vitest reported no testPath, so this live connection ' +
220220
'cannot be given a per-file schema and would fall back to sharing one database with ' +
221221
'every other live file — the contention this removed. Build live connections from a ' +
222222
'test file, through DIALECT_CELLS[].config().',

‎packages/drivers/driver-sql/src/schema-drift.base-type-mismatch.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe('diffManagedTable — multi-value field over a stale textual column (#1
111111
expect(entry.message).toContain('proj_task.tags');
112112
expect(entry.message).toContain('json');
113113
expect(entry.message).toContain('character varying');
114-
expect(entry.message).toContain('#11535');
114+
expect(entry.message).toContain('receives one opaque id instead of a list');
115115
// The remedy is the real statement, not a gesture at one.
116116
expect(entry.message).toContain(manualJsonConversionSql('postgres', 'proj_task', 'tags'));
117117
// The orphaned single-value index is part of the same picture: a json

‎packages/drivers/driver-sql/src/schema-drift.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ export function diffManagedTable(args: {
995995
message:
996996
`${table}.${fieldName}: the column carries DEFAULT '${field.defaultValue}', but ` +
997997
`'${field.defaultValue}' is a runtime token the engine resolves per write — the database ` +
998-
`has been stamping the literal token into every insert that omitted the field (#4560). ` +
998+
`has been stamping the literal token into every insert that omitted the field. ` +
999999
`Dropping the default is non-destructive: run "os migrate apply". Rows already holding ` +
10001000
`'${field.defaultValue}' are NOT rewritten — the dangling-reference audit reports them.`,
10011001
});
@@ -1142,7 +1142,7 @@ export function diffManagedTable(args: {
11421142
`column is \`${col.type}\` — the database was created while the field was single-value and the ` +
11431143
`additive sync never migrates a column's type. Arrays are being written as the STRINGIFIED ` +
11441144
`literal (e.g. '["a","b"]') and read back as a string, so anything consuming the value ` +
1145-
`receives one opaque id instead of a list (#11535). REMEDY: run ` +
1145+
`receives one opaque id instead of a list. REMEDY: run ` +
11461146
`"${MULTI_VALUE_COLUMN_REMEDY_COMMAND}" — it is a DRY RUN by default that executes nothing ` +
11471147
`and prints the statements; take a backup, then re-run it with --apply. ObjectStack never ` +
11481148
`migrates this column on its own: the boot path only reports it and "os migrate apply" ` +
@@ -1347,8 +1347,8 @@ export function diffManagedTable(args: {
13471347
`\`varchar(${col.maxLength})\` and the additive sync never changes a column's type. The ` +
13481348
`column still caps at ${col.maxLength} characters, so the server refuses longer values the ` +
13491349
`declaration ALLOWS (Postgres 22001, MySQL ER_DATA_TOO_LONG) — a data URI in a ` +
1350-
`\`signature\`/\`qrcode\` field, or an ordinary rich-text body, is routinely past it ` +
1351-
`(#12121). ObjectStack does NOT migrate this column: "os migrate apply" reports this entry ` +
1350+
`\`signature\`/\`qrcode\` field, or an ordinary rich-text body, is routinely past it. ` +
1351+
`ObjectStack does NOT migrate this column: "os migrate apply" reports this entry ` +
13521352
`as skipped. Two operator routes — declare a \`maxLength\` this dialect can express, which ` +
13531353
`turns this into the widen op "os migrate apply" performs; or convert the column to TEXT by ` +
13541354
`hand, with a backup taken first, restating the FULL column definition on MySQL (MODIFY ` +
@@ -2391,7 +2391,7 @@ export function diffManagedIndexes(args: {
23912391
message:
23922392
`${table}.${l.legacyColumns.join('+')}: a legacy platform-wide UNIQUE index (${present.join(', ')}) still enforces ` +
23932393
`uniqueness across ALL tenants, but metadata scopes it per '${l.replacement.columns[0]}' — a second ` +
2394-
`tenant reusing the value is rejected on insert (#3696). Replacing it with ${indexSignature(l.replacement.columns, true, l.replacement.nullSafeColumns)} ` +
2394+
`tenant reusing the value is rejected on insert. Replacing it with ${indexSignature(l.replacement.columns, true, l.replacement.nullSafeColumns)} ` +
23952395
`is a pure relaxation: run "os migrate apply".`,
23962396
});
23972397
}

‎packages/drivers/driver-sql/src/sql-driver-12999-stale-text-remedy.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ describe('unkeyable TEXT column: the upgrade path names the real remedy (#12999)
190190

191191
expect(message).toMatch(/Column\(s\) "token" are stored as TEXT because the field declares no `maxLength`/);
192192
expect(message).toMatch(/Declare `maxLength` on the field\(s\) so the column is emitted as varchar\(n\)/);
193-
expect(message).toContain('#11374');
193+
expect(message).toContain('so the column is emitted as varchar(n) and can be keyed.');
194194
// ⛔ The new branch must not reach this deployment: nothing here is stale.
195195
expect(message).not.toMatch(/ALREADY declares/);
196-
expect(message).not.toContain('#12999');
196+
expect(message).not.toContain('re-declaring `maxLength` changes nothing here');
197197
});
198198

199199
it('COUNTER-PIN: a bound past the 768-character key ceiling is NOT a stale column', async () => {
@@ -207,7 +207,7 @@ describe('unkeyable TEXT column: the upgrade path names the real remedy (#12999)
207207

208208
expect(message).toMatch(/wider than 768 characters/);
209209
expect(message).not.toMatch(/ALREADY declares/);
210-
expect(message).not.toContain('#12999');
210+
expect(message).not.toContain('re-declaring `maxLength` changes nothing here');
211211
});
212212

213213
it('COUNTER-PIN: a table this driver holds no declaration for keeps the CREATE message', async () => {
@@ -221,7 +221,7 @@ describe('unkeyable TEXT column: the upgrade path names the real remedy (#12999)
221221
const message = (await explain(driver, 'os12999_unregistered', ['token'])) ?? '';
222222

223223
expect(message).toMatch(/Declare `maxLength` on the field\(s\)/);
224-
expect(message).not.toContain('#12999');
224+
expect(message).not.toContain('re-declaring `maxLength` changes nothing here');
225225
});
226226

227227
it('names BOTH dispositions when one key column is stale and another is unbounded', async () => {
@@ -240,7 +240,7 @@ describe('unkeyable TEXT column: the upgrade path names the real remedy (#12999)
240240
// …and the genuinely unbounded half is still told to declare a bound, so a
241241
// composite key does not send the operator down one route for both columns.
242242
expect(message).toMatch(/Column\(s\) "note" in the same key declare no usable bound and DO need `maxLength`/);
243-
expect(message).toContain('#11374');
243+
expect(message).toContain('DO need `maxLength` declared on the field.');
244244
});
245245

246246
it('still declines to explain a failure that is not the TEXT-key refusal', async () => {

‎packages/drivers/driver-sql/src/sql-driver-14902-plain-unique-duplicate-preflight.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ describe('#14902 plain unique index over duplicate rows', () => {
219219
expect(entry!.message).toContain('os migrate plan');
220220
// ⛔ And it must NOT borrow path A's story: no prior index admitted these
221221
// rows, so #5030 is not what happened here.
222-
expect(entry!.message).not.toContain('#5030');
222+
expect(entry!.message).not.toContain('NULL-distinct');
223223
expect(entry!.message).not.toContain('NULL-safe');
224224
});
225225

@@ -298,14 +298,14 @@ describe('#14902 plain unique index over duplicate rows', () => {
298298
const durability = logs.filter((l) => l.level === 'error');
299299
expect(durability).toHaveLength(1);
300300
expect(durability[0].msg).toContain('NULL-safe unique index');
301-
expect(durability[0].msg).toContain('#5030');
301+
expect(durability[0].msg).toContain('NULL-distinct');
302302
expect(durability[0].msg).toContain('ADR-0120 D4');
303303
expect(durability[0].msg).toContain("'organization_id, quote_number'");
304304

305305
const entry = (await driver.detectManagedDrift()).find((d) => d.op.type === 'create_index')!;
306306
expect(entry.category).toBe('destructive');
307307
expect(entry.severity).toBe('error');
308-
expect(entry.message).toContain('#5030');
308+
expect(entry.message).toContain('NULL-distinct');
309309
expect(entry.message).toContain('__global__');
310310
// Both key groups, with counts — the NULL-organization bucket and a real
311311
// organization, which is what makes the COALESCE key self-describing.

‎packages/drivers/driver-sql/src/sql-driver-15479-shadow-plain-unique-duplicates.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479
169169

170170
// ⛔ And it must NOT carry the NULL-safe arm's framing, which is false
171171
// here: nothing admitted these rows and there is no NULL-safe key.
172-
expect(diagnosis).not.toContain('#5030');
172+
expect(diagnosis).not.toContain('NULL-distinct');
173173
expect(diagnosis).not.toContain('NULL-safe');
174174
expect(diagnosis).not.toContain('COALESCE');
175175

@@ -196,7 +196,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479
196196
const diagnosis = logs.find((l) => l.includes("cannot create hash-shadow unique index 'uniq_os15479_global_v'"));
197197
expect(diagnosis, 'the degradation must reach the durability channel').toBeTruthy();
198198
expect(diagnosis).toMatch(/Conflicting group\(s\):/);
199-
expect(diagnosis).not.toContain('#5030');
199+
expect(diagnosis).not.toContain('NULL-distinct');
200200
expect(diagnosis).not.toContain('COALESCE');
201201

202202
const { idx } = await catalog('os15479_global');
@@ -219,7 +219,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479
219219
expect(err, 'the NULL-safe arm already survived the boot').toBeNull();
220220
const diagnosis = logs.find((l) => l.includes("cannot create hash-shadow unique index 'uniq_os15479_org_v'"));
221221
expect(diagnosis, 'the NULL-safe degradation must still be logged').toBeTruthy();
222-
expect(diagnosis).toContain('#5030');
222+
expect(diagnosis).toContain('NULL-distinct');
223223
expect(diagnosis).toContain('NULL-safe');
224224
expect(diagnosis).toContain("COALESCE(organization_id, '__global__')");
225225
}, 60_000);

‎packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ describe('SqlDriver unique × tenancy (#3696)', () => {
691691
expect(entry!.message).toMatch(/BLOCKED/);
692692
expect(entry!.message).toMatch(/__global__/);
693693
expect(entry!.message).toMatch(/"DUP"/);
694-
expect(entry!.message).toMatch(/#5030/);
694+
expect(entry!.message).toMatch(/NULL-distinct/);
695695

696696
// Even `--allow-destructive` cannot force it: apply re-probes and
697697
// refuses — at no point is a constraint dropped without its replacement

‎packages/drivers/driver-sql/src/sql-driver-unique-violation-predicate.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => {
196196
expect(errors[0]).toMatch(/cannot create NULL-safe unique index/);
197197
expect(errors[0]).toMatch(/uniq_product_organization_id_code/);
198198
expect(errors[0]).toMatch(/NOT enforced/);
199-
expect(errors[0]).toMatch(/#5030/);
199+
expect(errors[0]).toMatch(/NULL-distinct/);
200200
expect(errors[0]).toMatch(/ADR-0120 D4/);
201201
});
202202

@@ -205,7 +205,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => {
205205

206206
await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined();
207207
expect(errors).toHaveLength(1);
208-
expect(errors[0]).toMatch(/#5030/);
208+
expect(errors[0]).toMatch(/NULL-distinct/);
209209
});
210210

211211
it('reads the violation through a driver `cause` wrapper', async () => {
@@ -215,7 +215,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => {
215215

216216
await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined();
217217
expect(errors).toHaveLength(1);
218-
expect(errors[0]).toMatch(/#5030/);
218+
expect(errors[0]).toMatch(/NULL-distinct/);
219219
});
220220

221221
// ── Nothing the old regex caught may be narrowed ──────────────────────────
@@ -229,7 +229,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => {
229229

230230
await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined();
231231
expect(errors).toHaveLength(1);
232-
expect(errors[0]).toMatch(/#5030/);
232+
expect(errors[0]).toMatch(/NULL-distinct/);
233233
});
234234

235235
// ── The site's own business logic, untouched by the migration ─────────────
@@ -286,7 +286,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => {
286286
expect(errors[0]).toMatch(/× 2 rows/);
287287
// ⛔ And it does not borrow the NULL-safe arm's story: no earlier index
288288
// admitted these rows, so #5030 is not what happened here.
289-
expect(errors[0]).not.toMatch(/#5030/);
289+
expect(errors[0]).not.toMatch(/NULL-distinct/);
290290
expect(errors[0]).not.toMatch(/NULL-safe/);
291291
});
292292

0 commit comments

Comments
 (0)