Skip to content

spec/docs still prescribe a dotted fields path that no driver implements and #7532 now refuses #7601

Description

@claude

What this is

Six in-repo surfaces offer a dotted fields path (fields: ['owner.name']) as the supported way to read one related column. No driver implements it. They were describing behaviour that did not exist before #7532, and since #7532 they are additionally contradicted by a 400 INVALID_FIELD at the ingress gate.

This is a restore-invariant fix — declared should equal enforced, not an open product question. The direction is already settled by #7532's ruling: dotted projections are refused, and dotted-path resolution is a feature that card explicitly did not authorise. What is left is aligning the prose (and the artifacts generated from it) to the enforced behaviour.

Filed separately rather than inside #7588 because it is cross-package spec/docs surface that regenerates artifacts, and #7534 is in flight in the same file family — that is precisely the collision the serialization constraint exists to prevent. Unassigned and without a domain:* label so routing stays the triage seat's call.

The surfaces

The normative one first — the rest largely follow from it:

# surface location
1 spec fields description — normative; generates the JSON Schema and the contract docs, so fixing this one propagates packages/spec/src/data/query.zod.ts:484
2 type pins (queryInput canonical shape; fieldNodeDotted) packages/spec/src/recursive-schema-input-assertions.ts:74,99
3 query.joins retirement prescription (#4286 / ADR-0049) packages/spec/src/migrations/entries/semantic/17.query-joins-retired.ts:10
4 nested-select object-form retirement prescription (#4196) packages/spec/src/migrations/entries/semantic/17.query-field-node-object-form-retired.ts:8
5 shipped JSON Schema artifacts (generated from 1) packages/spec/json-schema/api/FindDataRequest.json:154, .../ExportRequest.json:162, packages/spec/json-schema/objectstack.json:9791
6 docs prose content/docs/protocol/objectql/query-syntax.mdx:140,972; content/docs/references/data/query.mdx:125,134; content/docs/references/api/contract.mdx:407

Surfaces 3 and 4 are the sharpest: they are the automated upgrade prescriptions emitted to authors migrating off query.joins and off the retired { field, fields, alias } form. Both tell the author to write fields: ['owner.name'] — i.e. the migration tooling routes users directly into a spelling that now answers 400.

The measured fact that makes this actionable

Every surface above describes behaviour no driver implements. Measured on a real SqlDriver (better-sqlite3, :memory:), calling driver.find directly — no engine, no protocol:

no projection                  -> account amount created_at id name status updated_at
fields ['name']                -> name                        <- a plain name narrows
fields ['name','account.name'] -> account amount created_at id name status updated_at
fields ['account.name']        -> account amount created_at id name status updated_at

The dotted rows are byte-identical to no projection at all. Knex renders "account"."name" against a table that was never joined, sqlite answers no such column, and the #3821 recovery ladder retries select('*'). So the documented "read one related column" spelling never returned one related column — it returned every column. (That widening is itself filed as #7589.)

The coherent split — what to fix and what to leave

The zod schema still parses a dotted string, and that is correct and should stay: FieldNodeSchema is z.string(), a shape check. The semantic refusal lives at the ingress gate (assertProjectionFieldsExist, packages/metadata-protocol), which is where the field map is available to judge against.

So the fix is the .describe() prose and the artifacts generated from it — not the schema type. Specifically:

  • Do not narrow FieldNodeSchema to reject dots. That would move a semantic verdict into a shape check, refuse registry-less/internal callers the ingress deliberately tolerates, and duplicate a gate that already exists.
  • Do rewrite the fields description so it stops offering the dotted path, and regenerate the JSON Schema + contract docs from it.
  • The type pins (surface 2) pin that FieldNode admits a dotted string, which remains true at the type level. Decide whether to keep them as-is with a comment, or rename them so they stop reading as an endorsement of the feature.

What the corrected prose should say instead

expand is the sanctioned mechanism for related data on this door, and after #7532 it is the only one. It resolves reference fields into the related record via batched $in reads, driver-agnostic, and its nested QueryAST can both filter (where) and select (fields) the related record's columns.

Carry this sharp edge into the corrected prose — it is measured and it will bite:

The projection must retain the foreign-key column.

fields: ['title'],                expand: 'project_id'   ->  nothing to resolve; no related record
fields: ['title', 'project_id'],  expand: 'project_id'   ->  works

The relation is carried by the foreign key, so a narrowed projection that projects the FK away leaves expansion nothing to resolve. This is pinned as a GUARD test in #7588 ([#7532 GUARD] expand still delivers the related record the refusal prescribes), but it is not documented anywhere, and the prose being corrected here is exactly where a caller would look for it.

The other honest replacement, where the value is needed on the queried object itself, is to denormalise it onto that object (a stored field, written when the source changes) — the same remedy the sort axis prescribes (#6924), in the same words.

Not in scope

Implementing dotted-path resolution. #7532 explicitly did not authorise it, and this card is about making the declaration match the enforcement, not the other way round.

References


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions