diff --git a/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md b/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md new file mode 100644 index 00000000000..e423f02de83 --- /dev/null +++ b/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md @@ -0,0 +1,26 @@ +--- +'@objectstack/driver-sql': patch +--- + +driver-sql refusals, drift reports and log lines no longer cite tracker numbers; each states the reason in words + +Clause-②: no + +Many messages the SQL driver shows to authors and operators ended with an issue-tracker number where +the reason belonged. The number goes, and where the sentence did not already say what was decided, it +now does: + +- Filter refusals (`INVALID_FILTER`): the withheld-detail wording ("withheld from the message; the full + diagnostic is in the server log"), the JSON-column, zero-operator, `$null` / `$exists`, undefined + comparand and unknown-combinator refusals, and the filter-array refusal. +- Schema and index messages: the `reference_to` DDL refusal (the FOREIGN KEY DDL that key used to gate + is retired, because it could never fire for a spec-conformant lookup), the MySQL TEXT-key and + row-size explanations, the hash-shadow UNIQUE messages, and the `os migrate plan` drift entries. +- The NULL-safe UNIQUE messages now say why rows without an organization were never constrained: SQL + UNIQUE is NULL-distinct. +- Boot log lines for the SQLite datetime, time and json canonicalisation and the MySQL `TIMESTAMP` / + `TIME` widening now say what the conversion is for. + +Text only: no error code, field name, status or behaviour changes. Three aggregate refusals keep their +citation for now, because a test in `@objectstack/driver-turso` compares them byte for byte with the +Turso remote transport's copies; they change together with those copies. diff --git a/packages/drivers/driver-sql/src/cross-field-conformance-cases.ts b/packages/drivers/driver-sql/src/cross-field-conformance-cases.ts index b931cc2081f..9b1b38fcfd4 100644 --- a/packages/drivers/driver-sql/src/cross-field-conformance-cases.ts +++ b/packages/drivers/driver-sql/src/cross-field-conformance-cases.ts @@ -189,7 +189,7 @@ export const CROSS_FIELD_CASES: readonly CrossFieldCase[] = [ name: '$not of a cross-field $gt returns exactly the rows $gt does not', filter: { $not: { amount: { $gt: { $field: 'budget' } } } }, expected: ['2', '3', '4', '5', '6'], - 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.', + 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.', }, { 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[] = [ }, }, expected: ['2', '4', '5'], - 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.', + 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.', }, { name: 'a cross-field comparison nested two combinators deep', @@ -314,7 +314,7 @@ export const CROSS_FIELD_AUTHORED_CASES: readonly CrossFieldAuthoredCase[] = [ authored: ['amount', '>', { $field: 'budget' }], loweredTo: { amount: { $gt: { $field: 'budget' } } }, expected: ['1'], - note: 'Untouched by #7597 and asserted anyway: if this moves, the harness moved rather than the lowering.', + 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.', }, // ── The sugar's own structures, carrying a reference leaf ──────────────── @@ -393,7 +393,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [ name: 'an undeclared column is refused at compile time', filter: { amount: { $gt: { $field: 'no_such_column' } } }, diagnosticIncludes: ['not a declared field'], - 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.', + 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.', }, { name: 'an undeclared TARGET field is refused too', @@ -446,7 +446,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [ name: 'a multi-valued (JSON) column is refused as the referent', filter: { amount: { $gt: { $field: 'tags' } } }, diagnosticIncludes: ['no scalar stored'], - 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.', + 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.', }, { name: 'a formula (virtual) column is refused as the referent', @@ -479,7 +479,7 @@ export const CROSS_FIELD_REFUSALS: readonly CrossFieldRefusalCase[] = [ name: 'a $field member of an $in list is refused', filter: { amount: { $in: [{ $field: 'budget' }, 1] } }, diagnosticIncludes: ['index 0'], - 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.', + 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.', }, { name: 'a $field member of a $nin list is refused', diff --git a/packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts b/packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts index 2b697985535..612ba447ad3 100644 --- a/packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts +++ b/packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts @@ -216,7 +216,7 @@ export function currentLiveSchema(): string { const testPath = expect.getState().testPath; if (!testPath) { throw new Error( - 'live-dialect isolation (#9350): vitest reported no testPath, so this live connection ' + + 'live-dialect isolation: vitest reported no testPath, so this live connection ' + 'cannot be given a per-file schema and would fall back to sharing one database with ' + 'every other live file — the contention this removed. Build live connections from a ' + 'test file, through DIALECT_CELLS[].config().', diff --git a/packages/drivers/driver-sql/src/schema-drift.base-type-mismatch.test.ts b/packages/drivers/driver-sql/src/schema-drift.base-type-mismatch.test.ts index d82ae1b08ee..86dd18df4f5 100644 --- a/packages/drivers/driver-sql/src/schema-drift.base-type-mismatch.test.ts +++ b/packages/drivers/driver-sql/src/schema-drift.base-type-mismatch.test.ts @@ -111,7 +111,7 @@ describe('diffManagedTable — multi-value field over a stale textual column (#1 expect(entry.message).toContain('proj_task.tags'); expect(entry.message).toContain('json'); expect(entry.message).toContain('character varying'); - expect(entry.message).toContain('#11535'); + expect(entry.message).toContain('receives one opaque id instead of a list'); // The remedy is the real statement, not a gesture at one. expect(entry.message).toContain(manualJsonConversionSql('postgres', 'proj_task', 'tags')); // The orphaned single-value index is part of the same picture: a json diff --git a/packages/drivers/driver-sql/src/schema-drift.ts b/packages/drivers/driver-sql/src/schema-drift.ts index 50ae8546842..86bed97c965 100644 --- a/packages/drivers/driver-sql/src/schema-drift.ts +++ b/packages/drivers/driver-sql/src/schema-drift.ts @@ -995,7 +995,7 @@ export function diffManagedTable(args: { message: `${table}.${fieldName}: the column carries DEFAULT '${field.defaultValue}', but ` + `'${field.defaultValue}' is a runtime token the engine resolves per write — the database ` + - `has been stamping the literal token into every insert that omitted the field (#4560). ` + + `has been stamping the literal token into every insert that omitted the field. ` + `Dropping the default is non-destructive: run "os migrate apply". Rows already holding ` + `'${field.defaultValue}' are NOT rewritten — the dangling-reference audit reports them.`, }); @@ -1142,7 +1142,7 @@ export function diffManagedTable(args: { `column is \`${col.type}\` — the database was created while the field was single-value and the ` + `additive sync never migrates a column's type. Arrays are being written as the STRINGIFIED ` + `literal (e.g. '["a","b"]') and read back as a string, so anything consuming the value ` + - `receives one opaque id instead of a list (#11535). REMEDY: run ` + + `receives one opaque id instead of a list. REMEDY: run ` + `"${MULTI_VALUE_COLUMN_REMEDY_COMMAND}" — it is a DRY RUN by default that executes nothing ` + `and prints the statements; take a backup, then re-run it with --apply. ObjectStack never ` + `migrates this column on its own: the boot path only reports it and "os migrate apply" ` + @@ -1347,8 +1347,8 @@ export function diffManagedTable(args: { `\`varchar(${col.maxLength})\` and the additive sync never changes a column's type. The ` + `column still caps at ${col.maxLength} characters, so the server refuses longer values the ` + `declaration ALLOWS (Postgres 22001, MySQL ER_DATA_TOO_LONG) — a data URI in a ` + - `\`signature\`/\`qrcode\` field, or an ordinary rich-text body, is routinely past it ` + - `(#12121). ObjectStack does NOT migrate this column: "os migrate apply" reports this entry ` + + `\`signature\`/\`qrcode\` field, or an ordinary rich-text body, is routinely past it. ` + + `ObjectStack does NOT migrate this column: "os migrate apply" reports this entry ` + `as skipped. Two operator routes — declare a \`maxLength\` this dialect can express, which ` + `turns this into the widen op "os migrate apply" performs; or convert the column to TEXT by ` + `hand, with a backup taken first, restating the FULL column definition on MySQL (MODIFY ` + @@ -2391,7 +2391,7 @@ export function diffManagedIndexes(args: { message: `${table}.${l.legacyColumns.join('+')}: a legacy platform-wide UNIQUE index (${present.join(', ')}) still enforces ` + `uniqueness across ALL tenants, but metadata scopes it per '${l.replacement.columns[0]}' — a second ` + - `tenant reusing the value is rejected on insert (#3696). Replacing it with ${indexSignature(l.replacement.columns, true, l.replacement.nullSafeColumns)} ` + + `tenant reusing the value is rejected on insert. Replacing it with ${indexSignature(l.replacement.columns, true, l.replacement.nullSafeColumns)} ` + `is a pure relaxation: run "os migrate apply".`, }); } diff --git a/packages/drivers/driver-sql/src/sql-driver-12999-stale-text-remedy.test.ts b/packages/drivers/driver-sql/src/sql-driver-12999-stale-text-remedy.test.ts index c549b3c3525..7280baf352c 100644 --- a/packages/drivers/driver-sql/src/sql-driver-12999-stale-text-remedy.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-12999-stale-text-remedy.test.ts @@ -190,10 +190,10 @@ describe('unkeyable TEXT column: the upgrade path names the real remedy (#12999) expect(message).toMatch(/Column\(s\) "token" are stored as TEXT because the field declares no `maxLength`/); expect(message).toMatch(/Declare `maxLength` on the field\(s\) so the column is emitted as varchar\(n\)/); - expect(message).toContain('#11374'); + expect(message).toContain('so the column is emitted as varchar(n) and can be keyed.'); // ⛔ The new branch must not reach this deployment: nothing here is stale. expect(message).not.toMatch(/ALREADY declares/); - expect(message).not.toContain('#12999'); + expect(message).not.toContain('re-declaring `maxLength` changes nothing here'); }); 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) expect(message).toMatch(/wider than 768 characters/); expect(message).not.toMatch(/ALREADY declares/); - expect(message).not.toContain('#12999'); + expect(message).not.toContain('re-declaring `maxLength` changes nothing here'); }); 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) const message = (await explain(driver, 'os12999_unregistered', ['token'])) ?? ''; expect(message).toMatch(/Declare `maxLength` on the field\(s\)/); - expect(message).not.toContain('#12999'); + expect(message).not.toContain('re-declaring `maxLength` changes nothing here'); }); 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) // …and the genuinely unbounded half is still told to declare a bound, so a // composite key does not send the operator down one route for both columns. expect(message).toMatch(/Column\(s\) "note" in the same key declare no usable bound and DO need `maxLength`/); - expect(message).toContain('#11374'); + expect(message).toContain('DO need `maxLength` declared on the field.'); }); it('still declines to explain a failure that is not the TEXT-key refusal', async () => { diff --git a/packages/drivers/driver-sql/src/sql-driver-14902-plain-unique-duplicate-preflight.test.ts b/packages/drivers/driver-sql/src/sql-driver-14902-plain-unique-duplicate-preflight.test.ts index f560851d900..b042819a8bb 100644 --- a/packages/drivers/driver-sql/src/sql-driver-14902-plain-unique-duplicate-preflight.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-14902-plain-unique-duplicate-preflight.test.ts @@ -219,7 +219,7 @@ describe('#14902 plain unique index over duplicate rows', () => { expect(entry!.message).toContain('os migrate plan'); // ⛔ And it must NOT borrow path A's story: no prior index admitted these // rows, so #5030 is not what happened here. - expect(entry!.message).not.toContain('#5030'); + expect(entry!.message).not.toContain('NULL-distinct'); expect(entry!.message).not.toContain('NULL-safe'); }); @@ -298,14 +298,14 @@ describe('#14902 plain unique index over duplicate rows', () => { const durability = logs.filter((l) => l.level === 'error'); expect(durability).toHaveLength(1); expect(durability[0].msg).toContain('NULL-safe unique index'); - expect(durability[0].msg).toContain('#5030'); + expect(durability[0].msg).toContain('NULL-distinct'); expect(durability[0].msg).toContain('ADR-0120 D4'); expect(durability[0].msg).toContain("'organization_id, quote_number'"); const entry = (await driver.detectManagedDrift()).find((d) => d.op.type === 'create_index')!; expect(entry.category).toBe('destructive'); expect(entry.severity).toBe('error'); - expect(entry.message).toContain('#5030'); + expect(entry.message).toContain('NULL-distinct'); expect(entry.message).toContain('__global__'); // Both key groups, with counts — the NULL-organization bucket and a real // organization, which is what makes the COALESCE key self-describing. diff --git a/packages/drivers/driver-sql/src/sql-driver-15479-shadow-plain-unique-duplicates.test.ts b/packages/drivers/driver-sql/src/sql-driver-15479-shadow-plain-unique-duplicates.test.ts index 0be53451181..bdf5363fb1f 100644 --- a/packages/drivers/driver-sql/src/sql-driver-15479-shadow-plain-unique-duplicates.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-15479-shadow-plain-unique-duplicates.test.ts @@ -169,7 +169,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479 // ⛔ And it must NOT carry the NULL-safe arm's framing, which is false // here: nothing admitted these rows and there is no NULL-safe key. - expect(diagnosis).not.toContain('#5030'); + expect(diagnosis).not.toContain('NULL-distinct'); expect(diagnosis).not.toContain('NULL-safe'); expect(diagnosis).not.toContain('COALESCE'); @@ -196,7 +196,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479 const diagnosis = logs.find((l) => l.includes("cannot create hash-shadow unique index 'uniq_os15479_global_v'")); expect(diagnosis, 'the degradation must reach the durability channel').toBeTruthy(); expect(diagnosis).toMatch(/Conflicting group\(s\):/); - expect(diagnosis).not.toContain('#5030'); + expect(diagnosis).not.toContain('NULL-distinct'); expect(diagnosis).not.toContain('COALESCE'); const { idx } = await catalog('os15479_global'); @@ -219,7 +219,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479 expect(err, 'the NULL-safe arm already survived the boot').toBeNull(); const diagnosis = logs.find((l) => l.includes("cannot create hash-shadow unique index 'uniq_os15479_org_v'")); expect(diagnosis, 'the NULL-safe degradation must still be logged').toBeTruthy(); - expect(diagnosis).toContain('#5030'); + expect(diagnosis).toContain('NULL-distinct'); expect(diagnosis).toContain('NULL-safe'); expect(diagnosis).toContain("COALESCE(organization_id, '__global__')"); }, 60_000); diff --git a/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts b/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts index 78e85311b86..b4c56a89d8d 100644 --- a/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts @@ -691,7 +691,7 @@ describe('SqlDriver unique × tenancy (#3696)', () => { expect(entry!.message).toMatch(/BLOCKED/); expect(entry!.message).toMatch(/__global__/); expect(entry!.message).toMatch(/"DUP"/); - expect(entry!.message).toMatch(/#5030/); + expect(entry!.message).toMatch(/NULL-distinct/); // Even `--allow-destructive` cannot force it: apply re-probes and // refuses — at no point is a constraint dropped without its replacement diff --git a/packages/drivers/driver-sql/src/sql-driver-unique-violation-predicate.test.ts b/packages/drivers/driver-sql/src/sql-driver-unique-violation-predicate.test.ts index 5a0549eef9a..17768d3632b 100644 --- a/packages/drivers/driver-sql/src/sql-driver-unique-violation-predicate.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-unique-violation-predicate.test.ts @@ -196,7 +196,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => { expect(errors[0]).toMatch(/cannot create NULL-safe unique index/); expect(errors[0]).toMatch(/uniq_product_organization_id_code/); expect(errors[0]).toMatch(/NOT enforced/); - expect(errors[0]).toMatch(/#5030/); + expect(errors[0]).toMatch(/NULL-distinct/); expect(errors[0]).toMatch(/ADR-0120 D4/); }); @@ -205,7 +205,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => { await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined(); expect(errors).toHaveLength(1); - expect(errors[0]).toMatch(/#5030/); + expect(errors[0]).toMatch(/NULL-distinct/); }); it('reads the violation through a driver `cause` wrapper', async () => { @@ -215,7 +215,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => { await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined(); expect(errors).toHaveLength(1); - expect(errors[0]).toMatch(/#5030/); + expect(errors[0]).toMatch(/NULL-distinct/); }); // ── Nothing the old regex caught may be narrowed ────────────────────────── @@ -229,7 +229,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => { await expect(sync([NULL_SAFE_INDEX])).resolves.toBeUndefined(); expect(errors).toHaveLength(1); - expect(errors[0]).toMatch(/#5030/); + expect(errors[0]).toMatch(/NULL-distinct/); }); // ── The site's own business logic, untouched by the migration ───────────── @@ -286,7 +286,7 @@ describe('syncDeclaredIndexes unique-violation discriminator (#6543)', () => { expect(errors[0]).toMatch(/× 2 rows/); // ⛔ And it does not borrow the NULL-safe arm's story: no earlier index // admitted these rows, so #5030 is not what happened here. - expect(errors[0]).not.toMatch(/#5030/); + expect(errors[0]).not.toMatch(/NULL-distinct/); expect(errors[0]).not.toMatch(/NULL-safe/); }); diff --git a/packages/drivers/driver-sql/src/sql-driver.ts b/packages/drivers/driver-sql/src/sql-driver.ts index 2462eccd9c0..47d5046ccd5 100644 --- a/packages/drivers/driver-sql/src/sql-driver.ts +++ b/packages/drivers/driver-sql/src/sql-driver.ts @@ -1746,7 +1746,7 @@ function uncompilableAggregateFunctionError(func: string): Error { `correctly and @objectstack/spec AggregationFunction declares it — this is a capability gap ` + `in the backend, not a mistake in the query, which is why it answers NOT_IMPLEMENTED/501 ` + `rather than a 400. Aggregate with a function this backend compiles; whether the declaration ` + - `itself should stand is ADR-0049's enforce-or-remove question (#5907).`, + `itself should stand is ADR-0049's enforce-or-remove question.`, ) as Error & { code?: string; status?: number }; err.code = StandardErrorCode.enum.NOT_IMPLEMENTED; err.status = 501; @@ -1864,7 +1864,7 @@ function refuseDateBucketedGroupBy(granularity: string, bucketedHere: string[], `a capability gap in the backend, not a mistake in the query, which is why it answers ` + `NOT_IMPLEMENTED/501 rather than a 400. A driver publishes the granularities it buckets ` + `natively as \`supports.queryDateGranularity\`; the engine reads that record and buckets ` + - `in memory for every granularity absent from it, which is always correct (#6212).`, + `in memory for every granularity absent from it, which is always correct.`, ) as Error & { code?: string; status?: number }; err.code = StandardErrorCode.enum.NOT_IMPLEMENTED; err.status = 501; @@ -1936,10 +1936,10 @@ function refuseRejectedReferenceAlias(column: string): never { `Did you mean \`reference_to\` → \`reference\`? \`reference\` is the only relationship spelling ` + `@objectstack/spec declares, and \`FieldSchema\` refuses this key with that same verdict ` + `(\`unrecognized_keys\`) — so a field still carrying it at DDL time went around the schema ` + - `(raw \`registerObject\` skips Zod, #3896). Rename the key. The column is built from ` + + `(raw \`registerObject\` skips Zod). Rename the key. The column is built from ` + `\`reference\`, and referential integrity is enforced by the ENGINE via \`deleteBehavior\` ` + - `(the 409 DELETE_RESTRICTED), not by a database FOREIGN KEY: #11567 retired the FK DDL this ` + - `key used to gate, which could never fire for a spec-conformant lookup in the first place.`, + `(the 409 DELETE_RESTRICTED), not by a database FOREIGN KEY: the FK DDL this ` + + `key used to gate is retired, because it could never fire for a spec-conformant lookup in the first place.`, ) as Error & { code?: string; status?: number }; err.code = StandardErrorCode.enum.VALIDATION_ERROR; err.status = 400; @@ -2406,7 +2406,7 @@ function filterArrayReachedDriverError(filters: unknown[]): Error { `A filter ARRAY reached the driver: ${JSON.stringify(filters)}. ` + `'where' is a FilterCondition object; the array form ('FilterArray') is input-only ` + `authoring sugar and is lowered by @objectstack/spec parseFilterAST() at the engine ` + - `and protocol doors before any driver sees it (#5158). This driver no longer carries a ` + + `and protocol doors before any driver sees it. This driver no longer carries a ` + `second compiler for it — call through ObjectQL, or lower the value yourself with ` + `parseFilterAST(). Note the INFIX join form ([condA, "or", condB]) has no lowering at ` + `all: write the prefix form ["or", condA, condB].`, @@ -2756,7 +2756,7 @@ function crossFieldComparisonError( `of a scalar comparison operator ($eq/$ne/$gt/$gte/$lt/$lte) between two same-table ` + `declared columns. Compare against a literal value here, or evaluate the rule in memory ` + `(matchesFilter). The columns and the operator this filter used are withheld from the ` + - `message (#7929); the full diagnostic is in the server log.`, + `message; the full diagnostic is in the server log.`, diagnostic, subtree, `${diagnostic} Cross-field comparison compiles only as the whole comparand of a scalar ` + @@ -2809,7 +2809,7 @@ function bareFieldReferenceError(field: string, ref: string, subtree?: unknown): `"OTHER_FIELD" } } } — which compiles to a column-to-column comparison. The bare form is ` + `refused because the in-memory evaluator does not read it as an equality (it matches no ` + `record), so compiling it here would make the two execution paths answer this filter ` + - `differently. The columns this filter named are withheld from the message (#7929); the ` + + `differently. The columns this filter named are withheld from the message; the ` + `full diagnostic is in the server log.`, diagnostic, subtree, @@ -2941,7 +2941,7 @@ function uncompilableFieldReferenceError( `Cross-field comparison on SQL push-down supports same-table columns the object ` + `declares, compared as the same type class, excluding the tenant-isolation column. ` + `The columns, the operator this filter used and the specific reason are withheld from ` + - `the message (#7929); the full diagnostic is in the server log.`, + `the message; the full diagnostic is in the server log.`, diagnostic, subtree, ); @@ -3200,7 +3200,7 @@ function unbindableComparandError( `A comparison in this filter requires a single comparable value, but received a value that ` + `cannot be bound as a SQL parameter. Use ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} (or a ` + `binary value); for a list use $in/$nin, and for a range use $between. The field, the ` + - `operator and the value this filter used are withheld from the message (#8197); the full ` + + `operator and the value this filter used are withheld from the message; the full ` + `diagnostic is in the server log.`, `Operator "${op}" on field "${field}" requires a single comparable value, but received ` + `${Array.isArray(value) ? 'an array' : `an object (${safeShapePreview(value)})`}, which cannot be ` + @@ -3228,7 +3228,7 @@ function unbindableComparandError( function betweenArityError(field: string, subtree?: unknown): Error { return withheldFilterError( `Operator "$between" in this filter requires a [min, max] value array. The field it was ` + - `aimed at is withheld from the message (#8197); the full diagnostic is in the server log.`, + `aimed at is withheld from the message; the full diagnostic is in the server log.`, `Operator "$between" on field "${field}" requires a [min, max] value array.`, subtree, ); @@ -3413,8 +3413,8 @@ function jsonColumnOperatorError(field: string, op: string, bare: boolean, subtr `"$contains" for any-of ({ "$or": [{ "FIELD": { "$contains": "a" } }, ` + `{ "FIELD": { "$contains": "b" } }] }). Refused rather than compiled because the answer ` + `was silently wrong in BOTH directions: $in/$eq matched nothing, while $nin/$ne returned ` + - `the very rows they were asked to exclude (#7398). The field and the operator this filter ` + - `used are withheld from the message (#8197); the full diagnostic is in the server log.`, + `the very rows they were asked to exclude. The field and the operator this filter ` + + `used are withheld from the message; the full diagnostic is in the server log.`, `${spelling}${on} WAS NOT APPLIED: "${field}" is a multi-value (or otherwise JSON-valued) ` + `field, stored by this driver as a JSON TEXT column (e.g. ["a","b"]), and "${op}" compares ` + `that whole serialized text against a single value — it can never equal one member. ` + @@ -3422,7 +3422,7 @@ function jsonColumnOperatorError(field: string, op: string, bare: boolean, subtr `"$contains" for any-of ({ "$or": [{ "${field}": { "$contains": "a" } }, ` + `{ "${field}": { "$contains": "b" } }] }). Refused rather than compiled because the answer ` + `was silently wrong in BOTH directions: $in/$eq matched nothing, while $nin/$ne returned ` + - `the very rows they were asked to exclude (#7398).`, + `the very rows they were asked to exclude.`, subtree, ); } @@ -4251,15 +4251,15 @@ function emptyFieldConstraintError(field: string, path: string, subtree?: unknow `direct comparand (e.g. { "FIELD": "value" }). It is refused rather than ignored because ` + `the backends disagreed on what it means — this driver dropped it inside $and/$or/$not ` + `(matching EVERY row) while refusing it at the top level, and driver-memory / ` + - `@objectstack/formula answered "matches nothing". #5240. The field this filter named and ` + - `its position are withheld from the message (#8197); the full diagnostic is in the server ` + + `@objectstack/formula answered "matches nothing". The field this filter named and ` + + `its position are withheld from the message; the full diagnostic is in the server ` + `log.`, `Field constraint at ${path} carries zero operators ({ "${field}": {} }). A field constraint ` + `must name at least one operator (e.g. { "${field}": { "$eq": "value" } }) or be a direct ` + `comparand (e.g. { "${field}": "value" }). It is refused rather than ignored because the ` + `backends disagreed on what it means — this driver dropped it inside $and/$or/$not (matching ` + `EVERY row) while refusing it at the top level, and driver-memory / @objectstack/formula ` + - `answered "matches nothing". #5240.`, + `answered "matches nothing".`, subtree, ); } @@ -4319,7 +4319,7 @@ function unknownLogicalOperatorError(key: string, path: string, subtree?: unknow `other key is a field name. It is refused rather than compiled as a COLUMN of that name, ` + `which is what this driver used to do — producing a predicate that matched no row and ` + `reported nothing, so a caller could not tell "no rows matched" from "the filter never ` + - `compiled" (#5348).`, + `compiled".`, subtree, ); } @@ -4394,11 +4394,11 @@ function nonBooleanExistsComparandError( `Operator "$exists" on field "${field}" requires a boolean comparand (true or false). ` + `Received ${describeFilterOperand(value)} (${safeShapePreview(value)}) at ${path}. ` + `@objectstack/spec FieldOperatorsSchema declares $exists as a boolean. It is refused rather ` + - `than coerced for the same reason $null is (#5347): a non-boolean lands on whichever side ` + + `than coerced for the same reason $null is: a non-boolean lands on whichever side ` + `the backend's two-branch conditional happens to default to, and those defaults point in ` + `OPPOSITE directions — this driver's \`=== false\` test compiles IS NOT NULL for anything ` + `but false, a \`=== true\` test compiles IS NULL for anything but true. Note "false" the ` + - `STRING is truthy, so it lands on the side opposite the false it was written to mean (#5369).`, + `STRING is truthy, so it lands on the side opposite the false it was written to mean.`, subtree, ); } @@ -4418,7 +4418,7 @@ function nonBooleanNullComparandError( `compiled IS NULL (anything but false), driver-memory's query path and driver-mongodb ` + `compiled IS NOT NULL (anything but true), and driver-memory's matcher dropped the ` + `constraint entirely. Note "false" the STRING is truthy, so it landed on the side opposite ` + - `the false it was written to mean (#5347).`, + `the false it was written to mean.`, subtree, ); } @@ -4645,7 +4645,7 @@ function undefinedComparandError(field: string, path: string, subtree?: unknown) `refused rather than compiled because the backends disagreed: driver-sql handed it to knex ` + `and got a bare "Undefined binding(s)" Error carrying no code, while Turso's remote transport ` + `compiled it to IS NULL — so \`{ owner_id: ctx.user?.id }\` with a missing id silently ` + - `matched every env-wide row instead of failing (#6050).`, + `matched every env-wide row instead of failing.`, subtree, ); } @@ -8739,7 +8739,7 @@ export class SqlDriver implements IDataDriver { this.updatedAtColumnState.set(object, 'absent'); this.logger.warn( `[sql-driver] '${object}' has no physical 'updated_at' column, so this driver will not ` + - 'stamp one on update (#11067). The object is registered as managed, whose schema this ' + + 'stamp one on update. The object is registered as managed, whose schema this ' + "driver's own DDL would give `created_at`/`updated_at` — a table migrated out-of-band " + 'without them will keep a stale "last modified" for every consumer that reads it ' + '(list-view sorts, delta sync, cache invalidation, audit). Add the column, or accept ' + @@ -9946,7 +9946,7 @@ export class SqlDriver implements IDataDriver { `[sql-driver] INVALID_FILTER — a WHERE column could not be resolved on '${object}'` + (column === null ? '' : ` ('${column}')`) + '. The dialect message below is kept server-side because it inlines the statement ' + - `bound literals (#7929, #8790): ${typeof detail === 'string' ? detail : String(error)}`, + `bound literals: ${typeof detail === 'string' ? detail : String(error)}`, ); const disclosed = column !== null && @@ -10014,7 +10014,7 @@ export class SqlDriver implements IDataDriver { `[sql-driver] DATABASE_ERROR — the backend refused a read on '${object}'` + (typeof code === 'string' && code.length > 0 ? ` (${code})` : '') + '. The dialect message below is kept server-side: it carries the compiled statement, ' + - 'and on the dialects that inline them the bound literals too (#7929, #8931): ' + + 'and on the dialects that inline them the bound literals too: ' + `${typeof detail === 'string' ? detail : String(error)}`, ); // [#13438] The table the statement was compiled against, resolved the way @@ -10782,7 +10782,7 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `[sql-driver] INVALID_FIELD — a groupBy/aggregation column could not be resolved on ` + `'${object}' ('${column}'). The dialect message below is kept server-side because it ` + - `inlines the statement bound literals (#7929, #11541): ` + + `inlines the statement bound literals: ` + `${typeof detail === 'string' ? detail : String(error)}`, ); const clause = @@ -12422,7 +12422,8 @@ export class SqlDriver implements IDataDriver { const converted = (res as any)?.changes ?? 0; if (converted) { this.logger.info?.( - `[sql-driver] canonicalised datetime storage (#3912) for ${table}.${field}`, + `[sql-driver] canonicalised datetime storage for ${table}.${field}: every value is now one ` + + `UTC ISO-8601 text form, so filters and sorts compare instants rather than mixed encodings`, { rowsConverted: converted }, ); } @@ -12609,7 +12610,8 @@ export class SqlDriver implements IDataDriver { } if (tally.converted) { this.logger.info?.( - `[sql-driver] canonicalised json storage (#12380) for ${table}.${field}`, + `[sql-driver] canonicalised json storage for ${table}.${field}: plain-text cells are now ` + + `JSON-encoded strings, the one encoding a json column holds on every dialect`, { rowsConverted: tally.converted }, ); } @@ -12734,7 +12736,8 @@ export class SqlDriver implements IDataDriver { const converted = (res as any)?.changes ?? 0; if (converted) { this.logger.info?.( - `[sql-driver] canonicalised time-of-day storage (#3994) for ${table}.${field}`, + `[sql-driver] canonicalised time-of-day storage for ${table}.${field}: every value is now one ` + + `HH:MM:SS[.sss] text form, so filters and sorts compare times rather than mixed encodings`, { rowsConverted: converted }, ); } @@ -12950,7 +12953,8 @@ export class SqlDriver implements IDataDriver { }); await this.runWideningAlters(table, statements); this.logger.info?.( - `[sql-driver] widened MySQL TIMESTAMP → DATETIME(3) (#3942) on ${table}`, + `[sql-driver] widened MySQL TIMESTAMP → DATETIME(3) on ${table}: TIMESTAMP ends at 2038-01-19, ` + + `keeps no milliseconds and converts through the session timezone`, { columns: legacy.map((c) => c.name) }, ); } catch (err) { @@ -13055,7 +13059,8 @@ export class SqlDriver implements IDataDriver { }); await this.runWideningAlters(table, statements); this.logger.info?.( - `[sql-driver] widened MySQL TIME → TIME(3) (#3994) on ${table}`, + `[sql-driver] widened MySQL TIME → TIME(3) on ${table}: a zero-precision TIME rounds the ` + + `canonical milliseconds to the nearest second, changing the stored wall clock`, { columns: legacy.map((c) => c.name) }, ); } catch (err) { @@ -13529,7 +13534,8 @@ export class SqlDriver implements IDataDriver { d.severity = 'error'; d.message = nullSafeKey ? `${op.table}: cannot ${tighten ? 'tighten' : 'create'} '${op.indexName}' as ${signature} — existing rows ` + - `already violate the NULL-safe unique constraint (duplicates the old index wrongly admitted, #5030): ` + + `already violate the NULL-safe unique constraint (duplicates the old index wrongly admitted: SQL ` + + `UNIQUE is NULL-distinct, so it never constrained rows without an organization): ` + `${report}. The op is BLOCKED: apply re-probes and refuses, and the existing index stays in place ` + `(ADR-0120 D4). Deduplicate the listed rows, then re-run "os migrate plan".` : // #14902: the plain unique has no #5030 history behind it — nothing @@ -14017,7 +14023,7 @@ export class SqlDriver implements IDataDriver { `[schema-drift] could not create the NULL-safe unique '${op.indexName}' on '${op.table}' after dropping ` + `the old index${restored ? ' — restored the previous bare composite' : ' — AND the restore failed, so the ' + 'constraint is currently NOT enforced'}. Rows without an organization are ${restored ? 'still ' : ''}not ` + - `constrained (#5030); re-run "os migrate plan" and apply the reported op (ADR-0120 D4).`, + `constrained, because SQL UNIQUE is NULL-distinct; re-run "os migrate plan" and apply the reported op (ADR-0120 D4).`, (cause as any)?.message ?? cause, ); } @@ -14775,7 +14781,7 @@ export class SqlDriver implements IDataDriver { this.logDurabilityFailure( `[sql-driver] cannot create hash-shadow unique index '${name}' on "${tableName}" — ` + `existing rows violate the NULL-safe key (duplicates the previous void constraint ` + - `admitted, #5030).${report} The constraint '${columns + `admitted: SQL UNIQUE is NULL-distinct, so it never constrained rows without an organization).${report} The constraint '${columns .map((c) => (nullSafe.has(c) ? `COALESCE(${c}, '${GLOBAL_TENANT}')` : c)) .join(', ')}' is NOT enforced until the data is deduplicated: run "os migrate plan" ` + `for the conflicting rows (ADR-0120 D4).`, @@ -14838,8 +14844,8 @@ export class SqlDriver implements IDataDriver { // did not land, so this does not read as never having been // attempted. this.logDurabilityFailure( - `[sql-driver] hash-shadow UNIQUE index '${name}' on "${tableName}" could not be created ` + - `(#11627); falling back to the refusal below.`, + `[sql-driver] hash-shadow UNIQUE index '${name}' on "${tableName}" could not be created; ` + + `falling back to the refusal below.`, shadowMsg, ); } @@ -14857,7 +14863,8 @@ export class SqlDriver implements IDataDriver { // normal), and let the D4 drift pre-flight report the exact rows. this.logDurabilityFailure( `[sql-driver] cannot create NULL-safe unique index '${name}' on "${tableName}" — existing rows ` + - `violate it (duplicates the previous NULL-distinct index admitted, #5030). The constraint ` + + `violate it (duplicates the previous NULL-distinct index admitted: it never constrained rows ` + + `without an organization). The constraint ` + `'${columns.join(', ')}' is NOT enforced until the data is deduplicated: run "os migrate plan" ` + `for the conflicting rows (ADR-0120 D4).`, msg, @@ -14948,7 +14955,7 @@ export class SqlDriver implements IDataDriver { this.logDurabilityFailure( `[sql-driver] this MySQL/MariaDB server rejects functional key parts — created '${name}' on ` + `"${tableName}" over the BARE columns instead. Rows without an organization are NOT constrained ` + - `by it (#5030): upgrade to MySQL >= 8.0.13 and re-run "os migrate plan" to tighten it (ADR-0120 D3).`, + `by it (SQL UNIQUE is NULL-distinct): upgrade to MySQL >= 8.0.13 and re-run "os migrate plan" to tighten it (ADR-0120 D3).`, msg, ); await this.knex.schema.alterTable(tableName, (table) => { @@ -16426,7 +16433,7 @@ export class SqlDriver implements IDataDriver { // so the naming half went here rather than to the caller. this.logger.warn( `[sql-driver] INVALID_FILTER — refusal detail withheld from the response because the ` + - `predicate was not positively marked as author-written (#7929, #8220, #8197). ` + + `predicate was not positively marked as author-written. ` + `Full diagnostic: ${diagnostic}`, ); } @@ -17671,8 +17678,8 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `Paged read of '${object}' is NOT deterministic: this driver did not create the table, ` + 'so it cannot name a unique column to order by, and the query asked for no sort of ' - + 'its own. Walking the pages may serve one row twice and never serve another ' - + '(objectstack#4363). Give the query an `orderBy` on a unique column, or declare the ' + + 'its own. Walking the pages may serve one row twice and never serve another. ' + + 'Give the query an `orderBy` on a unique column, or declare the ' + 'object so this driver manages its table.', ); } @@ -18121,14 +18128,14 @@ export class SqlDriver implements IDataDriver { .filter((c) => !staleBounds.has(c)) .map((c) => `"${c}"`) .join(', ')} in the same key declare no usable bound and DO need \`maxLength\` ` + - `declared on the field (#11374).` + `declared on the field.` : ''; return ( `[sql-driver] cannot create index '${indexName}' on "${tableName}" — MySQL refuses a TEXT/BLOB ` + `column in a key without a key length. Column(s) ${staleNamed} are stored as TEXT even though the ` + `field ALREADY declares a usable \`maxLength\` — the column was created before that bound was ` + `declared, and the additive sync never rewrites a column's type, so re-declaring \`maxLength\` ` + - `changes nothing here (#12999). REMEDY: convert the column(s) to varchar(n) by hand, with a backup ` + + `changes nothing here. REMEDY: convert the column(s) to varchar(n) by hand, with a backup ` + `taken first, restating the FULL column definition on MySQL (MODIFY drops a NOT NULL or DEFAULT you ` + `do not repeat) — e.g. ALTER TABLE \`${tableName}\` MODIFY \`${firstColumn}\` ` + `varchar(${firstBound}) plus that column's existing NOT NULL / DEFAULT clauses, restated verbatim; ` + @@ -18145,8 +18152,8 @@ export class SqlDriver implements IDataDriver { `[sql-driver] cannot create index '${indexName}' on "${tableName}" — MySQL refuses a TEXT/BLOB ` + `column in a key without a key length. ${named} because the field declares no \`maxLength\` (or one ` + `wider than ${SqlDriver.MAX_KEYABLE_VARCHAR_CHARS} characters, the most a utf8mb4 key part can hold). ` + - `Declare \`maxLength\` on the field(s) so the column is emitted as varchar(n) and can be keyed ` + - `(#11374). The table exists but this index does NOT, so any uniqueness it declared is currently ` + + `Declare \`maxLength\` on the field(s) so the column is emitted as varchar(n) and can be keyed. ` + + `The table exists but this index does NOT, so any uniqueness it declared is currently ` + `unenforced. A prefix index is deliberately not substituted: on a UNIQUE index it constrains the ` + `prefix rather than the value, and rejects two different values that share one.` ); @@ -18308,7 +18315,7 @@ export class SqlDriver implements IDataDriver { this.logDurabilityFailure( `[sql-driver] cannot carry UNIQUE index '${indexName}' on "${tableName}" on a hash shadow — the ` + `column "${shadow}" already exists and is NOT a generated column, so it is not the driver's to ` + - `replace (#11627/#13015). The declared constraint is NOT enforced; rename or drop that column.`, + `replace: it may hold data. The declared constraint is NOT enforced; rename or drop that column.`, ); return false; } @@ -18338,12 +18345,12 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `[sql-driver] UNIQUE index '${indexName}' on "${tableName}" is carried by the hash-shadow column ` + `"${shadow}" (SHA-256 of ${described}), because MySQL cannot key ${columns.length > 1 ? 'this column set' : 'a column'} ` + - `longer than ${SqlDriver.MAX_KEYABLE_VARCHAR_CHARS} characters directly (#11627). The declared ` + + `longer than ${SqlDriver.MAX_KEYABLE_VARCHAR_CHARS} characters directly. The declared ` + `constraint is enforced over the full value; only the physical key differs.` + (state === 'reusable' - ? ` The shadow column already existed and hashes this exact key — re-keyed in place (#13015).` + ? ` The shadow column already existed and hashes this exact key — re-keyed in place.` : state === 'stale' - ? ` A surviving shadow column hashed a DIFFERENT key and was re-generated (#13015).` + ? ` A surviving shadow column hashed a DIFFERENT key and was re-generated.` : ''), { tableName, indexName, columns, shadow, shadowColumnState: state }, ); @@ -18475,7 +18482,7 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `[sql-driver] NOT dropping "${tableName}"."${shadow}" while collecting the retired index ` + `'${indexName}' — a column of that name exists and is NOT a generated column, so it is not ` + - `the driver's to drop and may hold data (#11627/#13056). Remove it by hand if it is unwanted.`, + `the driver's to drop and may hold data. Remove it by hand if it is unwanted.`, { tableName, indexName, shadow }, ); return false; @@ -18501,7 +18508,7 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `[sql-driver] NOT dropping "${tableName}"."${shadow}" while collecting the retired index ` + `'${indexName}' — ${stillKeyedBy.length} index(es) still key that column ` + - `(${stillKeyedBy.join(', ')}), so dropping it would remove them too (#13056).`, + `(${stillKeyedBy.join(', ')}), so dropping it would remove them too.`, { tableName, indexName, shadow, stillKeyedBy }, ); return false; @@ -18511,7 +18518,7 @@ export class SqlDriver implements IDataDriver { this.logger.warn( `[sql-driver] dropped the hash-shadow column "${shadow}" from "${tableName}" — the UNIQUE index ` + `'${indexName}' it carried is retired, so the STORED generated column was recomputed on every ` + - `write for a constraint that no longer exists (#11627/#13056).`, + `write for a constraint that no longer exists.`, { tableName, indexName, shadow }, ); return true; @@ -18609,13 +18616,13 @@ export class SqlDriver implements IDataDriver { return ( `[sql-driver] duplicate value for the UNIQUE constraint '${indexName}' on "${tableName}" ` + `(${described}). The constraint is physically carried by a hash-shadow column, so the ` + - `server's own message quotes a binary digest instead of the value (#11627).` + `server's own message quotes a binary digest instead of the value.` ); } return ( `[sql-driver] HASH COLLISION on the shadow-carried UNIQUE index '${indexName}' on "${tableName}" ` + `(${described}): the write was rejected as a duplicate, but NO existing row carries these ` + - `values. Uniqueness on this index is enforced over a SHA-256 of them (#11627), so two different ` + + `values. Uniqueness on this index is enforced over a SHA-256 of them, so two different ` + `values produced the same digest. This is expected at a rate near 10^-59 for a billion rows — if ` + `you are reading this, please report it with the values above; the write itself is legitimate and ` + `is being refused.` @@ -19007,7 +19014,7 @@ export class SqlDriver implements IDataDriver { `(${SqlDriver.varcharPackLength(SqlDriver.DEFAULT_STRING_VARCHAR_CHARS, bytesPerChar)} bytes here), and ` + `so do \`lookup\`, \`user\`, \`auto_number\` and the option types — an object can reach this limit ` + `without declaring a bound anywhere. The server's own error names no column, which is the whole reason ` + - `this one does (#11565).` + `this one does.` ); } diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index b4b42fa4533..e5bc03465eb 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -112,55 +112,10 @@ "packages/drivers/driver-mongodb/src/test-mongod.ts": { "#5517": 3 }, - "packages/drivers/driver-sql/src/cross-field-conformance-cases.ts": { - "#1051": 1, - "#5041": 1, - "#5146": 2, - "#7398": 1, - "#7597": 1 - }, - "packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts": { - "#9350": 1 - }, - "packages/drivers/driver-sql/src/schema-drift.ts": { - "#11535": 1, - "#12121": 1, - "#3696": 1, - "#4560": 1 - }, "packages/drivers/driver-sql/src/sql-driver.ts": { "#10413": 1, - "#11067": 1, - "#11374": 2, - "#11541": 1, - "#11565": 1, - "#11567": 1, - "#11627": 7, - "#12380": 1, - "#12999": 1, - "#13015": 3, - "#13056": 3, - "#3896": 1, - "#3912": 1, - "#3942": 1, - "#3994": 2, - "#4363": 1, - "#5030": 5, - "#5158": 1, - "#5240": 2, - "#5347": 2, - "#5348": 1, - "#5369": 1, - "#5907": 2, - "#6050": 1, - "#6212": 1, - "#6409": 1, - "#7398": 2, - "#7929": 7, - "#8197": 5, - "#8220": 1, - "#8790": 1, - "#8931": 1 + "#5907": 1, + "#6409": 1 }, "packages/drivers/driver-turso/src/remote-canonical-backfill.ts": { "#1005": 1,