Skip to content

[P3] data: FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove #4196

Description

@os-zhuang

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.tsast.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.

Refs #4171, ADR-0049, ADR-0078, ADR-0087.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions