From 94a80325c413831848af7a47876543e4ce980d7e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 07:19:32 +0000 Subject: [PATCH] docs(objectql): document the FILTER-axis formula refusal and its denormalise remedy (#8372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `query-syntax.mdx` carried the denormalise-onto-a-stored-field remedy on the SORT axis (a Callout under "Sorting on Related Fields") and, in the search axis' own vocabulary, under "Searching by a related record's title". The FILTER axis grew the same refusal in #8296 and the page never gained it. Adds "### Filtering on a `formula` field" to §2, quoting the message the ingress door actually emits rather than paraphrasing it, and recording the two facts a caller needs beyond the remedy: the code is `INVALID_FIELD` (not `INVALID_FILTER` — the verdict is about the name's TYPE, not the value's shape), and `summary`/`autonumber` are deliberately not refused because both get real stored columns. Docs only: no code, no message text, no rule changed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz --- .../docs/protocol/objectql/query-syntax.mdx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index b1549a25a0..742e05acbf 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -543,6 +543,41 @@ const opportunities = await engine.find('opportunity', { }); ``` +### Filtering on a `formula` field + + +**Do not filter on a `formula` field.** A `formula` value is computed on read — no +driver materialises a column for it — so the predicate reaches the driver, matches +nothing, and the query answers an **empty list under a 200**. Both directions are +wrong and `false` is the dangerous one: `where: { is_open: false }` returns no +records where the same test against a stored boolean returns *every* record. The +formula still reads correctly in that same response (the engine hydrates it after +the driver returns), so the field is visibly populated and simultaneously +unfilterable. + +Since #8296 both doors refuse it with `400 INVALID_FIELD` — the REST/protocol +ingress and `engine.find()` alike: + +```text +Query parameter 'where' filters on 'is_open', a virtual 'formula' field on object +'showcase_task'. Its value is computed on read and never stored, so no driver +materializes a column to filter on: the predicate reaches the driver, matches +nothing, and the query answers an empty list under a 200 — in BOTH directions, so a +false test returns no records where the same test against a stored boolean returns +every record. Denormalise the value onto 'showcase_task' (a stored field, written +when the source changes) and filter that. +``` + +`INVALID_FIELD`, not `INVALID_FILTER`: the verdict is about the **name's type**, not +the value's shape. The remedy is the same one the sort axis prescribes — denormalise +the value onto the queried object as a **stored** field, one this object's own rows +carry, written when the source changes, and filter that. + +`summary` and `autonumber` are **not** refused: both get real, stored columns and +filter correctly. A dotted filter path has no verdict on this axis — it is judged by +the relation rule above. + + --- ## 3. Sorting