You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while fixing #4171 (PR #4194). Out of scope there; filing rather than widening it.
The finding
FieldNodeSchema in packages/spec/src/data/query.zod.ts declares two forms for a select-list entry:
exporttypeFieldNode=|string// "name"|{field: string;fields?: FieldNode[];alias?: string};// nested select on a relationship
The object form is declared-but-inert. Every producer and consumer on the QueryAST['fields'] path treats the list as string[]:
packages/objectql/src/engine.ts — ast.fields as string[] at four sites (planFormulaProjection for find and findOne, plus both known-field filters); one of them already reaches for String(f).split('.')[0], which is what defensiveness against a shape you don't actually handle looks like.
packages/plugins/driver-sql/src/sql-driver.ts — (query.fields as string[]).map(f => this.mapSortField(f)).
packages/plugins/driver-mongodb/src/mongodb-driver.ts — indexed the projection with the entry itself, so an object form would have produced the projection key "[object Object]". (Fixed in fix(spec): five exported symbols resolved to any — type the recursive schemas and gate it in CI (#4171) #4194 to read .field, because the newly-precise type made it a compile error — but that is defensive handling of a shape nothing sends, not evidence the shape is live.)
Nothing anywhere reads .fields or .alias off a FieldNode. The capability the object form describes — selecting fields on a related object — is actually served by a different key: QueryAST['expand'], which the engine resolves via batch $in queries with a default max depth of 3.
So this is the ADR-0049 enforce-or-remove shape, and the ADR-0078 no-silently-inert shape: an authorable structure the runtime cannot honour. An author who writes fields: [{ field: 'owner', fields: ['name'] }] today gets it accepted by validation and then silently dropped or mangled, depending on the driver.
Decision needed
Remove (likely correct): drop the object form, leaving FieldNode = string, and let expand be the single spelling for nested selection. That deletes a second de-facto contract for one capability, which is Prime Directive #12's whole argument. Needs the ADR-0087 conversion/tombstone treatment if the shape was ever publicly authorable — FieldNodeSchema is exported from ./data, so it was.
Enforce: lower the object form into expand at the schema boundary (a conversion-layer entry, not a consumer-side ??), so the two spellings converge before any executor sees them. More work, and it buys an alias for something expand already expresses.
packages/spec/liveness + .claude/skills/spec-property-retirement cover the removal route; the liveness ledger is the right place to confirm the dead verdict before acting.
Note on scope
#4194 gave FieldNode a real type (it was z.ZodType<any>, so QueryAST['fields'] was any[]). That change is what made the inertness visible — it did not create it. The check:exported-any gate added there does not cover this class; a declared-but-unproduced shape is a liveness question, not a type question.
Found while fixing #4171 (PR #4194). Out of scope there; filing rather than widening it.
The finding
FieldNodeSchemainpackages/spec/src/data/query.zod.tsdeclares two forms for a select-list entry:The object form is declared-but-inert. Every producer and consumer on the
QueryAST['fields']path treats the list asstring[]:packages/objectql/src/engine.ts—ast.fields as string[]at four sites (planFormulaProjectionforfindandfindOne, plus both known-field filters); one of them already reaches forString(f).split('.')[0], which is what defensiveness against a shape you don't actually handle looks like.packages/plugins/driver-sql/src/sql-driver.ts—(query.fields as string[]).map(f => this.mapSortField(f)).packages/plugins/driver-mongodb/src/mongodb-driver.ts— indexed the projection with the entry itself, so an object form would have produced the projection key"[object Object]". (Fixed in fix(spec): five exported symbols resolved toany— type the recursive schemas and gate it in CI (#4171) #4194 to read.field, because the newly-precise type made it a compile error — but that is defensive handling of a shape nothing sends, not evidence the shape is live.)Nothing anywhere reads
.fieldsor.aliasoff aFieldNode. The capability the object form describes — selecting fields on a related object — is actually served by a different key:QueryAST['expand'], which the engine resolves via batch$inqueries with a default max depth of 3.So this is the ADR-0049 enforce-or-remove shape, and the ADR-0078 no-silently-inert shape: an authorable structure the runtime cannot honour. An author who writes
fields: [{ field: 'owner', fields: ['name'] }]today gets it accepted by validation and then silently dropped or mangled, depending on the driver.Decision needed
Remove (likely correct): drop the object form, leaving
FieldNode = string, and letexpandbe the single spelling for nested selection. That deletes a second de-facto contract for one capability, which is Prime Directive #12's whole argument. Needs the ADR-0087 conversion/tombstone treatment if the shape was ever publicly authorable —FieldNodeSchemais exported from./data, so it was.Enforce: lower the object form into
expandat the schema boundary (a conversion-layer entry, not a consumer-side??), so the two spellings converge before any executor sees them. More work, and it buys an alias for somethingexpandalready expresses.packages/spec/liveness+.claude/skills/spec-property-retirementcover the removal route; the liveness ledger is the right place to confirm thedeadverdict before acting.Note on scope
#4194 gave
FieldNodea real type (it wasz.ZodType<any>, soQueryAST['fields']wasany[]). That change is what made the inertness visible — it did not create it. Thecheck:exported-anygate added there does not cover this class; a declared-but-unproduced shape is a liveness question, not a type question.Refs #4171, ADR-0049, ADR-0078, ADR-0087.