Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/29546-declared-search-column-no-clause-comment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/spec': patch
---

docs(spec): the `resolveSearchFieldResolution` docblock says a declared search column whose type can never hold a string is admitted to the set but contributes no clause

Clause-②: no
Metadata-protocol: none

The declared-branch text on `resolveSearchFieldResolution` (`@objectstack/spec`, shipped in its `.d.ts`) still said a `json` column declared in `searchableFields` is "a scan that CAN match". The `$search` expansion in `@objectstack/objectql` gives such a column no clause: its declared type can never hold a string, which is the text-operator door's `door-refusal` verdict (`textOperatorDoorVerdict`). The docblock now says so:

- a declared `json` column (or any numeric, boolean, date or time, or structured-JSON column) is still admitted to the set, but contributes no clause;
- a set made only of such columns answers no rows, never an error;
- a declared single-valued `lookup` column still matches by substring over the stored foreign key;
- only an entry that names no column at all is dropped, as before.

Comment-only: no behaviour, schema or accepted value moves.
19 changes: 12 additions & 7 deletions packages/spec/src/data/search-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,18 @@ describe('a virtual field declared in searchableFields — not admitted, since n

it('CONTROL — the declared branch still bypasses the auto-default exclusions', () => {
// The dividing line is STORAGE, not search quality. `lookup` and `json` are
// auto-EXCLUDED types, and declaring them is still the author's choice: the
// engine runs a `$contains` over the stored foreign key / the stored JSON
// text. Narrow and rarely useful, but a scan that CAN match — so it must not
// be swept up by the virtual filter. This control is what keeps #6674 from
// silently becoming "the declared branch is type-filtered after all", which
// would reject metadata the runtime accepts (ADR-0072 D1, and the #4830
// changeset's explicit carve-out).
// auto-EXCLUDED types, and declaring them is still the author's choice, so
// both are ADMITTED here. What each then contributes is the expansion's call
// (`expandSearchToFilter`, `@objectstack/objectql`): a single-valued
// `lookup` becomes an `$icontains` over the stored foreign key, a narrow scan
// that can match;
// [#29394] `json` can never hold a string (the text-operator door's
// `door-refusal` verdict), so it contributes NO clause, and a set made only
// of such columns answers no rows. Neither is swept up by the virtual
// filter, which drops only an entry with no column at all. This control is
// what keeps #6674 from silently becoming "the declared branch is
// type-filtered after all", which would reject metadata the runtime accepts
// (ADR-0072 D1, and the #4830 changeset's explicit carve-out).
expect(
resolveSearchFieldResolution({
fields,
Expand Down
33 changes: 22 additions & 11 deletions packages/spec/src/data/search-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ export const SEARCH_AUTO_EXCLUDED_TYPES: ReadonlySet<string> = new Set([
]);
/**
* [#6674] Field types with NO STORED COLUMN — the value is computed on read, so
* a `$contains` scan has nothing to look at. Exactly `formula` today.
* a substring scan has nothing to look at. Exactly `formula` today.
*
* This is a STORAGE fact, not a taste judgment, and that is what separates it
* from the three vocabularies above. `SEARCH_AUTO_EXCLUDED_TYPES` says "the
* auto-default does not GUESS this type" — an author may still declare a `json`
* or `lookup` column in `searchableFields` and the engine executes it (a
* `$contains` over the stored JSON text, over the stored foreign key), which is
* a choice that can match. A `formula` entry cannot: the drivers materialize no
* column for it (`driver-sql/src/schema-drift.ts` `fieldHasColumn`,
* or `lookup` column in `searchableFields` and it is admitted, because a stored
* column exists (what each then contributes to a search is set out on
* {@link resolveSearchFieldResolution}). A `formula` entry is not: the drivers
* materialize no column for it (`driver-sql/src/schema-drift.ts` `fieldHasColumn`,
* `driver-turso/src/remote-transport.ts` "Virtual — no column"), and the engine
* excludes it from the projection it sends down (`objectql/src/engine.ts`
* `buildFormulaPlan`) because the driver would fail on the column name.
Expand Down Expand Up @@ -279,15 +279,26 @@ function autoDefaultFields(fields: Record<string, SearchFieldMeta>, displayField
* 2. [#6674] SCANNABILITY — an entry naming a VIRTUAL field
* ({@link isVirtualSearchField}) is dropped. The entry names a real field,
* passes existence, and can still never match, because the field has no
* stored column for `$contains` to scan.
* stored column for a substring scan to read.
*
* Filter 2 is deliberately NOT a type allow-list on the declared branch. The
* declaration remains the author's explicit choice and still bypasses the
* auto-default's exclusions: a `json` or `lookup` column declared here is
* executed by the engine as a `$contains` over the stored JSON text / stored
* foreign key — narrow, rarely useful, but a scan that CAN match. Only "there
* is no column at all" is refused, which is why the vocabulary is a storage
* fact rather than a search-quality one.
* auto-default's exclusions, so a `lookup` or `json` column declared here is
* ADMITTED to the set. What an admitted column then contributes to a search is
* the expansion's call (`expandSearchToFilter`, `@objectstack/objectql`), not
* this function's:
*
* - a single-valued `lookup` column becomes an `$icontains` over the stored
* foreign key — narrow, rarely useful, but a scan that can match;
* - [#29394] a column whose DECLARED type can never hold a string — the
* text-operator door's `door-refusal` verdict (`textOperatorDoorVerdict`,
* `./filter-text-operator-declared-type`: numeric, boolean, calendar-date,
* instant, clock-time and structured-JSON types such as `json`) —
* contributes NO clause. A set made only of such columns answers no rows:
* never an error, never every row.
*
* Only "there is no column at all" is refused, which is why the vocabulary is
* a storage fact rather than a search-quality one.
*
* Why the drop is not the whole fix. Dropping alone would only make the
* declaration silently NARROWER — the failure mode #4254 exists to close on
Expand Down
Loading