Repository navigation
[P3] data: FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove #4196
Description
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:
export type FieldNode =
| string // "name"
| { field: string; fields?: FieldNode[]; alias?: string }; // nested select on a relationshipThe 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 (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 .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.
Refs #4171, ADR-0049, ADR-0078, ADR-0087.
Activity
Merge main into the wait-timeout retirement branch
refactor(spec,plugin-security)!: QueryAST stops declaring what no exe…
Merge main into the wait-timeout retirement branch (2nd)
refactor(data)!: a select-list entry is a field name — the nested-sel…
docs(spec): `fields` stops prescribing a dotted path no driver resolv…
test(spec): the last data/ file group's test titles state each cited …