Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/driver-sql-object-def-param-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@objectstack/driver-sql": minor
---

`SqlDriver`'s object-definition parameters now DECLARE every key they read. `initObjects` accepts `lifecycle`, and the whole rotation chain — `rotateShards`, `ensureRotation`, `ensureShardTable` — accepts `tenancy` and `indexes`, spelled as a **fresh object literal** rather than only as a value bound to a variable first.

The driver read those keys off caller objects all along, through `(obj as any).<key>`, while the parameter's own inline type listed none of them. That is refused or accepted depending only on where the object is spelled: TypeScript's excess-property check fires on a fresh literal and not on one hoisted to a variable, so the same call compiles in one shape and is `TS2353` in the other. The loud outcome is the harmless one. The bad one is an author — or an AI reading the signature — concluding the key is not accepted and DROPPING it, at which point a declared UNIQUE is never synced and an ADR-0057 rotation policy is never armed, with nothing anywhere saying so.

This is the third instance of one class, not a third coincidence: `tenancy` (#4311) and `indexes` (#16570) were the first two, each fixed one key at a time. The class is now held by a gate — `scripts/check-object-def-param-keys.mjs` — that reads parameter lists as an AST and covers the shape no in-file check could see: a subclass in another published package overriding one of these methods with a narrower literal.

- **What widened.** `rotateShards(objectDef)` gains `tenancy?: any` and `indexes?: any[]`; `ensureRotation(…, obj, …)` gains the same two; `ensureShardTable(…, obj)` gains `indexes?: any[]`; `initObjects(objects)` gains `lifecycle?: any`. All three rotation links carry the keys, not just the leaf that reads them — declaring them only on the leaf would leave the two links above still narrowing the same value in flight, so a fresh literal handed to the public entry point would still have been refused.
- **What did NOT widen, deliberately.** The accept set still has a boundary: a misspelling (`indexs`, `tenancyy`, `lifecycl`) on a fresh literal is still `TS2353`, pinned by `@ts-expect-error` in `src/sql-driver-16711-object-def-param-keys.test.ts`. A "fix" that relaxed these parameters to `any`, or gave them an index signature, would have turned every other assertion green while deleting the entire layer of protection.
- **Four `as any` casts deleted**, including the residual one in `detectManagedDrift`, whose parameter had declared `indexes` all along. Behaviour is unchanged in every case — the keys were already being read.
9 changes: 9 additions & 0 deletions .changeset/driver-sqlite-wasm-inherits-object-def-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@objectstack/driver-sqlite-wasm": minor
---

`SqliteWasmDriver.initObjects` accepts `tenancy`, `indexes` and `lifecycle` in a **fresh object literal**, inherited from the widened `SqlDriver` — and that inheritance is now asserted rather than assumed.

This package overrides neither `initObjects` nor `registerObjectMetadata`, so its published `.d.ts` re-declares none of them and the door it exposes is `SqlDriver`'s, imported from `@objectstack/driver-sql`. Measured on the built declarations: zero re-declarations of `initObjects`, `registerObjectMetadata`, `rotateShards`, `ensureShardTable` or `registerManagedObjectMetadata`. That is the opposite direction of the defect the sibling packages carried — `TursoDriver` overrode `initObjects` with a narrower literal and shadowed a base-class fix for five weeks — and it is recorded here because a consumer reading only this package's changelog would otherwise never learn its accept set moved.

`src/sqlite-wasm-16711-inherited-object-def-keys.test.ts` pins the inheritance inside this package's own tsc program: the inherited parameter is not `any`, each key is present on the element type, a fresh literal carrying them compiles and is read at run time, and a misspelling is still `TS2353`. It goes red both ways — if the base narrows again, and if a future override here re-declares the door more narrowly.
13 changes: 13 additions & 0 deletions .changeset/driver-turso-init-objects-declares-base-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@objectstack/driver-turso": minor
---

`TursoDriver.initObjects` now declares every key `SqlDriver.initObjects` declares — `tenancy`, `indexes` and `lifecycle` — so a caller of this package can spell them in a **fresh object literal** instead of hoisting the object to a variable to get past the type.

`TursoDriver` OVERRIDES `initObjects`, and an override does not inherit the base's parameter type. Its own literal read `Array<{ name: string; fields?: Record<string, any> }>`, which is what every consumer of `@objectstack/driver-turso` saw — so when #4311 declared `tenancy` on the base in August, that fix did not exist from outside this package, and stayed invisible for five weeks with nothing red anywhere. #16570's `indexes` fix would have escaped by the identical route.

The type face was the only thing refusing the keys. The remote arm forwards the whole object through as `schema`, and `registerRemoteFieldMetadata` reads `tenancy` straight back off it, so the runtime carried both keys the entire time. `tenancy.enabled: false` is the key that decides whether a UNIQUE partitions globally or per organization — an author who hit the refusal and dropped it silently got the other answer.

- `registerRemoteFieldMetadata(obj)` declares `tenancy?: any` and reads it directly; its `(obj as any).tenancy` cast is gone.
- The boundary is intact: a misspelling on a fresh literal is still `TS2353`, pinned in `src/turso-driver-16711-init-objects-param.test.ts`.
- `scripts/check-object-def-param-keys.mjs` now fails the build if this override — or any other subclass override in the workspace — declares fewer keys than the method it shadows, or erases the base's shape with an opaque type or an index signature.
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4386,6 +4386,29 @@ jobs:
- name: Check every driver runs the shared conformance cases
run: pnpm check:driver-conformance

# Object-definition parameter keys (#16711). `SqlDriver` takes object
# definitions as inline object-literal parameter types and reads keys off
# them that the literal does not list, through `(obj as any).<key>`. Three
# single-key cards fixed one key each (#4311 `tenancy`, #16570 `indexes`,
# #16711 `lifecycle`) before anyone called it a class. The escape is
# silent: TypeScript's excess-property check fires on a FRESH object
# literal and not on one bound to a variable first, so an author who hits
# the refusal drops the key — an unsynced UNIQUE, an unarmed ADR-0057
# rotation policy — and nothing says so.
#
# ⛔ NOT scoped to sql-driver.ts, which is the whole ruling: `TursoDriver`
# OVERRIDES `initObjects` in a separately published package, so #4311's
# fix was invisible from outside `@objectstack/driver-sql` for five weeks
# and #16570's would have escaped identically. The gate compares every
# subclass override's declared keys against the base's, across packages,
# and refuses the two edits that would make that vacuous (an index
# signature, an opaque replacement type). AST-based, because the signature
# this class hides behind wraps across lines and a single-line grep for it
# returns a silence that reads exactly like a negative.
# Reads source files only; no build, ~2s.
- name: Check object-definition parameters declare the keys they are read for
run: pnpm check:object-def-param-keys

# Stall-guard self-test (#4250). scripts/run-with-stall-guard.mjs is what
# turns a frozen Test Core into a labeled red; six jobs across five
# workflows now route their test steps through it. But it only executes its
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"check:type-check-debt": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs --re-measure",
"check:driver-conformance": "node scripts/check-driver-conformance.mjs --self-test && node scripts/check-driver-conformance.mjs",
"check:driver-memory-census": "node scripts/check-driver-memory-census.mjs --self-test && node scripts/check-driver-memory-census.mjs",
"check:object-def-param-keys": "node scripts/check-object-def-param-keys.mjs --self-test && node scripts/check-object-def-param-keys.mjs",
"check:engine-double-contract": "node scripts/check-engine-double-contract.mjs --self-test && node scripts/check-engine-double-contract.mjs",
"check:where-matcher": "node scripts/check-where-matcher-conformance.mjs --self-test && node scripts/check-where-matcher-conformance.mjs",
"check:objectql-double-limit": "node scripts/check-objectql-double-limit.mjs --self-test && node scripts/check-objectql-double-limit.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,19 @@ for (const liveCell of [PG_CELL, MYSQL_CELL]) {
// it physically too. Both directions measured, boundary included.
const KT = `${T}_keyed`;
// Hoisted (not an inline literal) the way #11374's `boundedObject()`
// is: `indexes` rides through `initObjects` beyond its narrow
// parameter type, exactly as the platform objects declare it.
// is, exactly as the platform objects declare it.
//
// ⚠️ The second half of what this comment used to say has EXPIRED and
// is kept here as a dated record rather than deleted: it read
// "`indexes` rides through `initObjects` beyond its narrow parameter
// type", and that was true — the signature declared no `indexes` and
// the driver read the key through an `as any` anyway. #16570 declared
// it and #16711 closed the class, so the hoist is no longer LOAD-BEARING
// here; an inline literal would compile today. It stays because
// mirroring #11374's authoring shape is why it was written that way in
// the first place, and because this suite is about column widths, not
// about parameter types. The pin that must stay inline is
// `sql-driver-16711-object-def-param-keys.test.ts`.
const keyedObject = {
name: KT,
fields: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,17 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479
): Promise<{ logs: string[]; err: unknown }> => {
driver = new SqlDriver(cell.config());
const logs = spy();
await driver.initObjects([{ ...meta, indexes: [] }] as any);
// #16711: the `as any` that used to be on both of these calls was a
// workaround for `initObjects` not declaring `indexes`. The signature
// declares it now, so the cast is gone and these two calls are checked
// like any other.
await driver.initObjects([{ ...meta, indexes: [] }]);
const knex = (driver as any).knex;
await knex(meta.name).insert([
{ id: 'a', ...row },
{ id: 'b', ...row },
]);
const err: unknown = await driver.initObjects([meta] as any).then(
const err: unknown = await driver.initObjects([meta]).then(
() => null,
(e) => e,
);
Expand Down Expand Up @@ -228,7 +232,7 @@ declareDialectCell(MYSQL_CELL, 'hash-shadow plain unique over duplicates (#15479
it('still creates and enforces the plain shadow unique over clean data', async () => {
driver = new SqlDriver(cell.config());
spy();
await driver.initObjects([plainUniqueOn('os15479_clean')] as any);
await driver.initObjects([plainUniqueOn('os15479_clean')]);

const { cols, idx } = await catalog('os15479_clean');
const shadow = cols.find((c: any) => isHashShadowColumn(c.COLUMN_NAME));
Expand Down
Loading
Loading