Skip to content

Commit a2a3777

Browse files
docs(search): name the empty-search-set refusal on the pages that describe the data read route's search (#24361)
Fixes #24355 Clause-②: no ## What This PR adds the empty-search-set refusal to every hand-written page that describes the data read route's `search` and did not mention it yet. Since `ec749fbb44` (PR #24320), a non-blank `search` over an object whose search set comes out empty is refused with `400 INVALID_FIELD` instead of answering every row. Until now these pages described field resolution only. | page | where | change | |---|---|---| | `content/docs/protocol/kernel/error-handling.mdx` | `INVALID_FIELD`, the read-axis parenthesis in the catalog pointer | `search` added beside `searchFields` | | `content/docs/api/data-api.mdx` | `GET /data/:object` parameter table, `search` row | one sentence: when the set is empty, a non-blank term is `400 INVALID_FIELD`, never every row | | same | the request/result table under "Neither is a search narrowing, a grouping, or an aggregation" | one row: `?search=alpha` on an object with no searchable field gives `400 INVALID_FIELD` | | same | the `searchFields` bullet under that table | now names both `search` and `searchFields`. The bullet called `searchFields` "the only parameter whose failure changed which rows came back", and the new row would make that untrue. One sentence says what a bare `search` used to do | | `content/docs/data-modeling/queries.mdx` | Full-Text Search, opening paragraph | one sentence, engine calls included | | `content/docs/protocol/objectql/query-syntax.mdx` | field-resolution paragraph | the sentence about internal callers keeping the tolerant intersection now also says what they do not keep: the engine itself refuses on `find` / `findOne` / `count` / `aggregate` (`param: 'search'`) | | `content/docs/data-modeling/schema-design.mdx` | Global search | outside the card's four (see Enumeration). One sentence: list search is refused, global search skips the object, mirror the title | | `content/docs/ui/views.mdx` | Toolbar search runtime table | outside the card's four. The runtime cell of the "key omitted" row gains "`400 INVALID_FIELD` when that set is empty" | No new section. `content/docs/references/**`, `content/docs/releases/**` and `content/docs/api/error-catalog.mdx` are untouched. ## The producer, read from the code (base `e4bcced0b4`) - `emptySearchSetError` and its single call site, `ObjectQL.expandSearchOnAst` (`packages/objectql/src/engine.ts`). It throws when the term is non-blank (`normalizeSearch(raw).query.trim()`) and `expandSearchToFilter` returned `null`. The thrown error carries `status`/`httpStatus` 400, `code: 'INVALID_FIELD'`, `object` and `param: 'search'`. It has no `field`. - The set comes from `resolveSearchFieldResolution` (`packages/spec/src/data/search-fields.ts`). A declared entry counts when it names a field that exists and is not virtual (`formula`). If no entry counts, the auto-default applies: text / email / phone / url / autonumber / textarea / markdown / select, minus hidden and system columns. `internal: true` fields are removed from the map before resolution. - Verbs: `find` and `findOne` call `expandSearchOnAst` directly; `count` and `aggregate` reach it through `expandSearchOnOptions`. All four live in the engine, so a caller of `engine.find()` meets the refusal too. That is why query-syntax's sentence about internal callers keeping the tolerant intersection is kept and narrowed, not deleted: the tolerant half, `resolveSearchFields` dropping unknown requested names, has not changed. - One door still answers `200`. Global search (`searchAll`, `packages/metadata-protocol/src/protocol.ts`) skips an object whose set is empty before it queries. That is stated on schema-design, the only page here that describes global search. - The REST pages say `400 INVALID_FIELD` and leave out `param`. The flat `/data` arm (the `INVALID_FIELD` arm in `packages/types/src/data-error-classification.ts`) sends `error`, `code`, `field` and `object`, not `param`. Only query-syntax, which describes the engine's thrown error, names `param: 'search'`. Sister sentence: the `### INVALID_FIELD` sentence that PR #24353 adds to `error-catalog.mdx`, read at its head `d87f5a77ff`. Each page reuses its terms: resolved search set is empty, no `searchableFields` entry names a stored field, the default search scans none, `internal: true` fields never count, mirror the composed title onto a stored text field. The error-handling parenthesis says the catalog entry lists `search`. That is only true once #24353 lands, so the two should land together or #24353 first. ## Enumeration `git grep -lE 'searchableFields|searchFields' e4bcced -- content/docs`, minus `references/` and `releases/`, gives 8 pages. I read each once. | page | describes the data read route's `search`? | disposition | |---|---|---| | `api/data-api.mdx` | yes | edited | | `data-modeling/queries.mdx` | yes | edited | | `protocol/kernel/error-handling.mdx` | yes (the `INVALID_FIELD` read axes) | edited | | `protocol/objectql/query-syntax.mdx` | yes | edited | | `data-modeling/schema-design.mdx` | yes: `?search=apollo`, `$search`, the auto-default fallback | edited (outside the card's four) | | `ui/views.mdx` | yes: the toolbar search's runtime answers are the data door's | edited (outside the card's four) | | `api/error-catalog.mdx` | yes | left: PR #24353 covers it | | `kernel/contracts/data-engine.mdx` | no: the `EngineQueryOptions` type listing and the predicate rule for `findOne`, neither of which describes the route | read and left | ## Line budget The card's four pages: +16 / -6, net +10 (budget +16). The two outside them: schema-design +4 / -1, views +1 / -1. All six: +21 / -8. ## Verification (HEAD `d0e954f51`, after merging `origin/main` at `d1f082d94`) Docs-only: no package is touched, so no build closure and no package tests are owed. The gate list is the 41 commands from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (merge base `d1f082d94`). Each ran with its exit code captured before any pipe: - 40 exited 0. They include `pnpm check:doc-authoring`, `check:doc-anchors`, `check:error-status-conformance`, `check:nul-bytes` and `check:corpus-claim-drift`. Four of them ran after building `@objectstack/lint...` (lint, formula, sdui-parser, spec) under the verify lock (held 117 s): `pnpm --filter @objectstack/spec run check:docs`, `pnpm check:docs-transcript-drift`, and `pnpm --filter @objectstack/lint run check:doc-formula-expressions` / `check:doc-security-posture`. - 1 NOT MEASURED: `pnpm --filter @objectstack/spec run check:skill-examples` exited 3, PREREQUISITE NOT MET, because `packages/client-react/dist` holds no `.d.ts`. Building it means a 37-package closure. Declared narrowing: none of the eight hunks is inside a fenced code block (checked per hunk), so no marked example changed. CI runs the gate. - `--ran` reconciliation: "41 derived famil(ies) accounted for — 40 run, 1 NOT-MEASURED". - Left to CI, not run locally: the path-scheduled `Build Docs` job and the type-check lanes. - Control-byte scan of the six files: no hit. ## Acceptance notes - `ui/views.mdx`, under "What the object allows": the allowed set for a declaring object is described as "that list, verbatim — whatever the field types are". In the code, `resolveSearchFieldResolution` drops entries that name no field or a `formula` field. The last table row in the same section does refuse the formula case. Docs drift, not filed, no carrier. - `kernel/contracts/data-engine.mdx`: the `EngineQueryOptions` comment for `search` does not mention the refusal. The engine-caller statement is on query-syntax. Read and left (see above). --- _Generated by [Claude Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c4526a7 commit a2a3777

6 files changed

Lines changed: 21 additions & 8 deletions

File tree

‎content/docs/api/data-api.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Query records with filtering, sorting, selection, and pagination.
2323
| `top` | query | Max records to return. No default — omitting it returns all matching records. |
2424
| `skip` | query | Offset |
2525
| `expand` | query | Comma-separated list of relations to eager-load. Must name a reference field (`lookup` / `master_detail` / `user` / `tree`) — otherwise `400 INVALID_FIELD`. |
26-
| `search` | query | Full-text search term, scanned case-insensitively across the object's searchable fields (its declared `searchableFields`, or a text-like auto-default when none are declared). |
26+
| `search` | query | Full-text search term, scanned case-insensitively across the object's searchable fields (its declared `searchableFields`, or a text-like auto-default when none are declared). When that set is empty — no `searchableFields` entry names a stored field, and the auto-default finds none — a non-blank term is `400 INVALID_FIELD`, never every row. |
2727
| `searchFields` | query | Comma-separated subset of the searchable fields for `search` to scan — narrows the scan, never widens it. A name outside the searchable set is `400 INVALID_FIELD`. |
2828

2929
> **Note:** OData-style `$`-prefixed parameters (`$filter`, `$select`, `$orderby`, `$top`, `$skip`, `$expand`, `$count`, `$search`) are also accepted directly on this same endpoint as aliases — they're normalized internally to the parameter names above. There is no separate standalone OData endpoint.
@@ -156,14 +156,17 @@ corrupts something the earlier axes do not:
156156
| `?search=alpha&searchFields=no_such_field` | `400 INVALID_FIELD` |
157157
| `?search=alpha&searchFields=amount` | `400 INVALID_FIELD` — real field, but not searchable |
158158
| `?search=alpha&searchFields=project_id.name` | `400 INVALID_FIELD` — search scans this object's own columns; mirror the related title instead (see below) |
159+
| `?search=alpha` on an object with no searchable field (two lookups and a `formula` title, say) | `400 INVALID_FIELD` — nothing to scan; declare a stored field searchable or mirror the title (see below) |
159160
| `groupBy: ["status"]` | one bucket per status value |
160161
| `groupBy: ["no_such_field"]` | `400 INVALID_FIELD` |
161162
| `aggregations: [{function:"sum", field:"amount", alias:"total"}]` | the real total |
162163
| `aggregations: [{function:"sum", field:"no_such_field", alias:"total"}]` | `400 INVALID_FIELD` |
163164
| `aggregations: [{function:"count", alias:"n"}]` | `count(*)` — the one legitimate field-less form |
164165

165-
- **`searchFields`** — the only parameter whose failure changed which **rows**
166-
came back. An unknown name used to be dropped, and an override left empty
166+
- **`search`** and **`searchFields`** — the only parameters whose failure
167+
changed which **rows** came back. A `search` over an object with nothing to
168+
scan added no predicate, so every row came back as if every row matched. An
169+
unknown `searchFields` name used to be dropped, and an override left empty
167170
fell back to scanning *every* searchable column: a parameter that exists
168171
only to **narrow** a search failed by **widening** it. Three causes get
169172
three messages, because the fixes differ: a name that is no field (a typo in

‎content/docs/data-modeling/queries.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,10 @@ dotted `fields` path (`'customer.name'`) for a single related column:
477477

478478
`search` takes the query text; the server resolves which fields to search from object
479479
metadata (ADR-0061). Pass `searchFields` to narrow that set — it is intersected with what
480-
the object allows, so it can only narrow, never widen.
480+
the object allows, so it can only narrow, never widen. When the object allows nothing —
481+
no `searchableFields` entry names a stored field, and the default search scans none
482+
(`internal: true` fields never count) — a non-blank `search` is refused with
483+
`400 INVALID_FIELD` instead of answering every row, engine calls included.
481484

482485
```typescript
483486
{

‎content/docs/data-modeling/schema-design.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ To control exactly which fields are matched, set `searchableFields` on the objec
102102
Queries then pass a top-level `$search` parameter to match across `searchableFields`
103103
(e.g. `{ $search: 'acme' }`); a request may narrow the set with
104104
`$searchFields`. When `searchableFields` is unset, search falls back to the
105-
name/title field plus short-text fields.
105+
name/title field plus short-text fields. When neither leaves a stored field to scan — a
106+
junction object of lookups with a `formula` title, say — a list search is refused
107+
`400 INVALID_FIELD` rather than answered with every row, and global search skips the
108+
object; mirror the title onto a stored text field, as below.
106109

107110
#### Searching by a related record's title — mirror the value
108111

‎content/docs/protocol/kernel/error-handling.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ knows which object was addressed.
624624
625625
The [error catalog's `INVALID_FIELD` entry](/docs/api/error-catalog#invalid_field) carries
626626
the authoritative cause text — it enumerates every read axis this one code answers on
627-
(`select`, `expand`, `searchFields`, `groupBy`, `aggregations[].field`) and the
627+
(`select`, `expand`, `search`, `searchFields`, `groupBy`, `aggregations[].field`) and the
628628
off-request `backfillSummaryNulls` case.
629629
630630
### Resource Errors

‎content/docs/protocol/objectql/query-syntax.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,11 @@ plus short-text/enum fields), so naming a field outside that set can never widen
11241124
search — and over the REST/protocol ingress it is `400 INVALID_FIELD` outright (#4254),
11251125
because the engine-side intersection alone used to drop the unknown name and fall back to
11261126
scanning the full searchable set. Internal callers reaching `engine.find()` directly keep
1127-
the tolerant intersection. Multiple whitespace-separated terms are AND-ed and
1127+
the tolerant intersection, but not a search with nothing to scan: when no
1128+
`searchableFields` entry names a stored field and the auto-default finds none
1129+
(`internal: true` fields never count), the engine itself refuses a non-blank `search`
1130+
with `400 INVALID_FIELD` (`param: 'search'`) on `find`, `findOne`, `count` and
1131+
`aggregate` alike, rather than answering every row. Multiple whitespace-separated terms are AND-ed and
11281132
fields are OR-ed. Case sensitivity comes from the operator the expansion emits, not
11291133
from the expansion: it emits `$icontains`, which ignores **ASCII case** by the
11301134
rule in [Case Sensitivity](#case-sensitivity) above. So a user typing `acme` does

‎content/docs/ui/views.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ all.
154154
| What you write on the view | `os validate` | Toolbar search at runtime |
155155
| :--- | :--- | :--- |
156156
| a subset of the allowed set | clean | scans exactly those columns |
157-
| key omitted, or `searchableFields: []` | clean | scans the object's full allowed set |
157+
| key omitted, or `searchableFields: []` | clean | scans the object's full allowed set — `400 INVALID_FIELD` when that set is empty |
158158
| a renamed / mistyped column, or a dotted path | `searchable-field-unknown` | `400 INVALID_FIELD` |
159159
| a real column outside the allowed set | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
160160
| a virtual `formula` column — no stored column to scan (#6674) | `searchable-field-unsearchable` | `400 INVALID_FIELD` |

0 commit comments

Comments
 (0)