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
|`?search=alpha&searchFields=amount`|`400 INVALID_FIELD` — real field, but not searchable |
131
+
|`?search=alpha&searchFields=project_id.name`|`400 INVALID_FIELD` — search scans this object's own columns; mirror the related title instead (see below) |
131
132
|`groupBy: ["status"]`| one bucket per status value |
`?search=apollo` now expands to `name $contains 'apollo' OR project_name
133
+
$contains 'apollo'` — one single-table scan, on every driver, with no traversal.
134
+
If the object declares no `searchableFields` at all, a `text` mirror is picked up
135
+
by the auto-default anyway; declare the set explicitly when you want to pin it.
136
+
137
+
<Callouttype="warn">
138
+
**The mirror must be a stored field — a `formula` field does not work.** A
139
+
`formula` field is *virtual*: no driver materializes a column for it, so a
140
+
`$contains` predicate against one has nothing to scan (the SQL driver would emit
141
+
a `WHERE` over a column that does not exist). A CEL formula also only reads this
142
+
record's own fields (`record.<field>`), so it cannot fetch the related title in
143
+
the first place. Nothing catches the mistake for you — `searchableFields` admits
144
+
any field the object declares, so a formula entry passes both lint and the
145
+
ingress gate and then just never matches.
146
+
</Callout>
147
+
148
+
**Keeping the mirror fresh.** A mirror is denormalized data, only as current as
149
+
whatever maintains it. Two write paths have to be covered:
150
+
151
+
| When | What maintains the mirror |
152
+
|:-----|:--------------------------|
153
+
| A task is created, or re-pointed at another project |`beforeInsert` / `beforeUpdate` hook on `task` — read the parent's `name` for the incoming `project_id` and stamp `project_name`|
154
+
| A project is renamed |`afterUpdate` hook on `project` — re-stamp `project_name` on that project's tasks |
155
+
156
+
Rows written by a path that bypasses hooks (bulk import, direct SQL) need a
157
+
one-off backfill. See [Hooks](/docs/automation/hooks) for the hook shapes.
158
+
159
+
**The errors you get if you try the dotted path.** Both the lint and the runtime
160
+
send you to the same fix, so either message is greppable back to this section.
161
+
162
+
`os validate` reports `searchable-field-unknown`:
163
+
164
+
```text
165
+
searchableFields entry "project_id.name" is not a field on object "task". The
166
+
declaration is stale: searching it can never match, and the engine silently
167
+
drops it — leaving a narrower search than declared, or the auto-default set once
168
+
every entry is dropped.
169
+
170
+
hint: 'search' scans this object's own columns, so a related record's column
171
+
cannot be a search target — expand the relation and search the related object,
172
+
or copy the value onto a formula field here. Clients echo this declaration
173
+
verbatim as the '$searchFields' override, so a stale entry becomes a 400
174
+
INVALID_FIELD on list search (#4254), not just a quietly narrowed one.
175
+
```
176
+
177
+
(That hint's "text/formula" family of wording is loose — only the **stored**
178
+
half works; see the callout above.)
179
+
180
+
A request that sends the dotted path is `400 INVALID_FIELD`:
181
+
182
+
```text
183
+
Unknown field 'project_id.name' on object 'task'. '$searchFields' narrows which
184
+
columns 'search' scans, so a name the object does not declare cannot narrow
185
+
anything — and the engine used to drop it and scan the default columns instead,
186
+
answering a NARROWER search with a WIDER one. 'search' scans this object's own
187
+
columns; a related record's column cannot be a search target.
188
+
```
189
+
190
+
If the dotted path is in the object's own `searchableFields` (so clients echo it
191
+
back verbatim), the same 400 arrives under its stale-declaration wording
192
+
instead: `Field 'project_id.name' on object 'task' is declared in
|`searchableFields`|`string[]`|optional|Fieldsthetoolbarsearchscans — **narrows**theobject's set, never widens it (ADR-0061). Entries must be the object's**own**columns: alookup (`project_id`) oradottedpath (`project_id.name`) isrefused, andeverytoolbarsearchonthelistthenreturns`400 INVALID_FIELD` (#4254). Tosearchbyarelatedrecord's title, [mirror it into a stored field](/docs/data-modeling/schema-design#searching-by-a-related-records-title--mirror-the-value) on the object and list that|
0 commit comments