Skip to content

driver-sql: initObjects / registerObjectMetadata still omit indexes from a parameter type they read it through — the shape #4311 fixed for tenancy, one key over #16570

Description

@os-musk

Found while writing the overflow-tail coverage on #16289 (not addressed there — different defect class, and widening a public signature is its own verification surface).

What

packages/drivers/driver-sql/src/sql-driver.ts. Both entry points that take a caller's object list declare the same structural parameter type:

registerObjectMetadata(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>): void      // :9629
async initObjects(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>): Promise<void>  // :9641

indexes is not in that type. It IS read out of the very same objects, one call deep:

this.managedObjectIndexes.set(tableName, (obj as any).indexes);   // :9510

and managedObjectIndexes is what syncDeclaredIndexes renders every declared UNIQUE from. So the driver's whole index-sync path is driven by a key its own signature says does not exist.

The trap, and why nothing has tripped over it

TypeScript's excess-property check fires on a FRESH object literal and not on one bound to a variable first, so the same object is accepted or rejected depending only on where it is spelled:

// REJECTED by tsc — TS2353: Object literal may only specify known properties,
// and 'indexes' does not exist in type
// '{ name: string; fields?: Record<string, any> | undefined; tenancy?: any; }'
await driver.initObjects([{ ...bare, indexes: [] }]);

// ACCEPTED, and the index is synced
const withoutIndex = { ...bare, indexes: [] };
await driver.initObjects([withoutIndex]);

Measured on packages/drivers/driver-sql at dfaa8325a: pnpm --filter @objectstack/driver-sql exec tsc --noEmit reported exactly that error for the inline form, and zero for the variable form. Every existing caller in the package's own tests happens to bind a variable first, which is why the package typechecks clean today — the workaround is load-bearing and nobody wrote it down.

An author (or an AI) reading the signature concludes indexes is not accepted by initObjects and drops it. That is a silently unsynced UNIQUE constraint, decided at authoring time by a type that contradicts the runtime.

The contract text is already in the file, three lines above the signature

This exact defect was fixed once, for a different key, and the reason is recorded in the source:

tenancy is part of what this method READS — each object flows into computeAndRecordTenantField, which consumes obj.tenancy to pick the tenant column and to set or clear the sticky explicit-opt-out. It went undeclared here until #4311 (registerExternalObject and computeAndRecordTenantField both had it), so a caller spelling the key correctly was rejected by the type while the driver read it regardless.

That paragraph describes indexes today, word for word, sitting immediately above the signature that still omits it.

A sibling method on the same class already declares the key it reads — detectManagedDrift(objects?: Array<{ name: string; fields?: Record<string, any>; indexes?: any[] }>) — so the two halves of the same class disagree about the shape of the same input.

Suggested shape, not asserted

Add indexes?: any[] to both parameter types (matching detectManagedDrift's spelling) and drop the as any at :9510, or state the narrow type deliberately and make the index path read from somewhere the signature admits. Whichever way, the (obj as any) cast is the evidence that the declared type and the read disagree.

⛔ Filed unassigned, as an observation for triage. Grade deliberately not asserted: the visible symptom is a compile error on a correct call, and the worst case is a dropped UNIQUE that the author never learns was dropped.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions