Skip to content

Commit 89eb997

Browse files
claude[bot]claude
andauthored
docs(spec): the $search expansion compiles to $icontains, not $contains (#15053)
The `DriverCapabilities.fullTextSearch` tombstone prescription and five hand-written passages still described the ADR-0061 search expansion as an `$or` of `$contains`. `expandSearchToFilter` has emitted `$icontains` since #7641; `$contains` is contractually case-sensitive (#4706 Q2 = A). One callout went further than a stale operator name: it told the reader the case-insensitivity question "remains a separate open question, and one that can now actually be answered" — inviting a reader acting in good faith to re-open an adjudicated ruling. It now states the settled answer. Site C lands in the `.describe()` source, never in the generated `.mdx`; the four reference rows are regenerated by `check:generated --fix`. Every other `$contains` mention on the three pages was classified individually — the operator's own rows, examples and case-sensitivity rulings are correct and are untouched. `packages/objectql/src/search-filter.ts` is not in this diff: its `__search` companion clause is deliberately `$contains` and must not be aligned. `content/docs/releases/` is not touched; it was already correct. Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i Co-authored-by: Claude <noreply@anthropic.com>
1 parent 101ad2c commit 89eb997

8 files changed

Lines changed: 48 additions & 23 deletions

File tree

.changeset/olive-pugs-shave.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Correct the `$search` expansion description: it compiles to `$icontains`, not `$contains`
6+
7+
The `DriverCapabilities.fullTextSearch` tombstone prescription in `driver.zod.ts` said
8+
`$search` is compiled into "an `$or` of `$contains` predicates". It has compiled to
9+
`$icontains` since #7641 — textual search is case-insensitive by ruling, and `$contains`
10+
is contractually case-**sensitive** (#4706 Q2 = A). The prescription now says so, and the
11+
four generated driver reference rows it feeds regenerate with it.
12+
13+
The same false sentence is corrected on the three hand-written pages that carried it
14+
(`protocol/objectql/query-syntax.mdx`, `data-modeling/queries.mdx`,
15+
`data-modeling/schema-design.mdx`), including a callout that told readers the
16+
case-insensitivity question "remains a separate open question" when #7641 closed it.
17+
18+
Documentation only — no accept/reject, emitted-shape or runtime behaviour change.

content/docs/data-modeling/queries.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ The structured form is equivalent and carries the experimental knobs below — `
493493

494494
<Callout type="warn">
495495
Only `query` and `fields` are implemented. The engine expands `search` into a driver-agnostic
496-
`$and`-of-`$or`-of-`$contains` filter (ADR-0061) — `fuzzy`, `operator`, `boost`, `minScore`,
496+
`$and`-of-`$or`-of-`$icontains` filter (ADR-0061) — `fuzzy`, `operator`, `boost`, `minScore`,
497497
`language`, and `highlight` are accepted by `QuerySchema` but read nowhere in
498498
`expandSearchToFilter()` / `normalizeSearch()`, so they have no effect; since #4286 their
499499
`.describe()` markers say so. Multiple search terms are always AND-ed regardless of
@@ -530,7 +530,7 @@ The answer is a **mirror field**: copy the related record's title into a stored
530530
field on this object and declare *that* field searchable. A task list searched by
531531
project name gets a `project_name` text column on `task`, maintained on write and
532532
listed in `task.searchableFields`. It has to be a **stored** field — a `formula`
533-
field is virtual, so no driver has a column for `$contains` to scan. Cross-object
533+
field is virtual, so no driver has a column for `$icontains` to scan. Cross-object
534534
search paths are rejected by design, not pending. Full recipe (the hooks that keep
535535
the mirror fresh, and the lint wording) in [Schema Design → Searching by a related
536536
record's title](/docs/data-modeling/schema-design#searching-by-a-related-records-title--mirror-the-value).

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,15 @@ To let users search a task list by project name:
129129
}
130130
```
131131

132-
`?search=apollo` now expands to `name $contains 'apollo' OR project_name
133-
$contains 'apollo'` — one single-table scan, on every driver, with no traversal.
132+
`?search=apollo` now expands to `name $icontains 'apollo' OR project_name
133+
$icontains 'apollo'` — one single-table scan, on every driver, with no traversal.
134134
If the object declares no `searchableFields` at all, a `text` mirror is picked up
135135
by the auto-default anyway; declare the set explicitly when you want to pin it.
136136

137137
<Callout type="warn">
138138
**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
139+
`formula` field is *virtual*: no driver materializes a column for it, so an
140+
`$icontains` predicate against one has nothing to scan (the SQL driver would emit
141141
a `WHERE` over a column that does not exist). A CEL formula also only reads this
142142
record's own fields (`record.<field>`), so it cannot fetch the related title in
143143
the first place.

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -969,9 +969,9 @@ disclose every other tenant's values for that column.
969969
### Full-Text Search
970970

971971
The `search` parameter does **not** reach a full-text index. The engine expands it into
972-
an `$or` of `$contains` predicates across the object's server-resolved searchable fields
972+
an `$or` of `$icontains` predicates across the object's server-resolved searchable fields
973973
(ADR-0061) and deletes `search` from the AST before the driver sees it — every driver
974-
already runs `$or`/`$contains`, so no driver support is needed (which is also why the
974+
already runs `$or`/`$icontains`, so no driver support is needed (which is also why the
975975
old `supports.fullTextSearch` capability bit had no reader and was retired in 17.0.0,
976976
#4634).
977977

@@ -1006,13 +1006,15 @@ because the engine-side intersection alone used to drop the unknown name and fal
10061006
scanning the full searchable set. Internal callers reaching `engine.find()` directly keep
10071007
the tolerant intersection. Multiple whitespace-separated terms are AND-ed and
10081008
fields are OR-ed. Case sensitivity comes from the operator the expansion emits, not
1009-
from the expansion: it emits a plain `$contains`, which is **case-sensitive** by the
1010-
rule in [Case Sensitivity](#case-sensitivity) above. Note what that means for search —
1011-
a user typing `acme` does not find `ACME Corp`. Only `select` / `status` option
1012-
*labels* are matched case-insensitively by the expansion itself.
1009+
from the expansion: it emits `$icontains`, which ignores **ASCII case** by the
1010+
rule in [Case Sensitivity](#case-sensitivity) above. So a user typing `acme` does
1011+
find `ACME Corp` — search is case-insensitive by ruling
1012+
([#7641](https://github.com/objectstack-ai/objectstack/issues/7641)). `select` /
1013+
`status` option *labels* are matched case-insensitively too, by a separate
1014+
label→value mapping in the expansion rather than by the operator.
10131015

10141016
<Callout type="info">
1015-
**Measured today: every driver matches that rule.** The `$contains` alignment
1017+
**Measured today: both operators mean one thing on every driver.** The `$contains` alignment
10161018
landed in three steps —
10171019
[#6518](https://github.com/objectstack-ai/objectstack/issues/6518) made `SqlDriver`
10181020
case-exact per dialect (`GLOB` on the SQLite dialects, `LIKE` unchanged on
@@ -1024,8 +1026,12 @@ a user typing `acme` does not find `ACME Corp`. Only `select` / `status` option
10241026
the divergence this callout warned about is closed, and
10251027
`FILTER_TEXT_CASES` holds all five drivers to it. Whether the expansion should emit
10261028
`$icontains` instead of `$contains` — i.e. whether search is case-insensitive by
1027-
definition — remains a separate open question, and one that can now actually be
1028-
answered, since both operators mean one thing everywhere.
1029+
definition — is **settled, not open**:
1030+
[#7641](https://github.com/objectstack-ai/objectstack/issues/7641) ruled that it should,
1031+
and `expandSearchToFilter` has compiled `$search` to `$icontains` ever since.
1032+
`$contains` stays case-**sensitive** by contract
1033+
([#4706](https://github.com/objectstack-ai/objectstack/issues/4706) Q2 = A) — the two
1034+
are different operators, not two spellings of one.
10291035
</Callout>
10301036
`fuzzy`, `boost`, `operator`, `minScore`, `language`, and `highlight` carry
10311037
`[EXPERIMENTAL — not enforced]` markers (#4286): the schema accepts them, the
@@ -1050,8 +1056,8 @@ The declarative answer is a **mirror field**: copy the related record's title
10501056
into a stored field on this object and declare *that* field searchable — a task
10511057
list searched by project name carries a `project_name` text column on `task`,
10521058
maintained on write and listed in `task.searchableFields`, so the expansion stays
1053-
a single-table `$or` of `$contains`. The mirror must be **stored**: a `formula`
1054-
field is virtual, no driver materializes a column for it, and a `$contains`
1059+
a single-table `$or` of `$icontains`. The mirror must be **stored**: a `formula`
1060+
field is virtual, no driver materializes a column for it, and an `$icontains`
10551061
against one has nothing to scan. Cross-object search paths are rejected by
10561062
design, not pending — see [Schema Design → Searching by a related record's
10571063
title](/docs/data-modeling/schema-design#searching-by-a-related-records-title--mirror-the-value)

content/docs/references/data/driver-nosql.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ const result = AggregationPipelineSchema.parse(data);
180180
| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. |
181181
| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. |
182182
| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. |
183-
| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. |
183+
| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. |
184184
| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. |
185185
| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. |
186186
| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. |

content/docs/references/data/driver-sql.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const result = DataTypeMappingSchema.parse(data);
9494
| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. |
9595
| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. |
9696
| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. |
97-
| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. |
97+
| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$icontains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. The operator is `$icontains`, NOT `$contains`: textual search is case-insensitive by ruling (#7641). Delete the key. |
9898
| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. |
9999
| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. |
100100
| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0: nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. |

0 commit comments

Comments
 (0)