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/filter-text-operator-declared-type-door.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@objectstack/spec": minor
---

feat(spec)!: a text operator over a field whose DECLARED type can never store a string is refused at the engine's field-aware door — the contract rows (#15661)

<!-- adr-0087: not-required (no-migration-prescription) nothing is renamed, retired or respelled: no metadata key, export or operator changes shape, so `objectstack migrate meta` has nothing to rewrite; an author's remedy is to drop or retarget the mistaken filter condition, which is a change to the query they meant, not to a spelling -->

**BREAKING** accept-set narrowing, declared here and enforced at the engine door: a text operator (`$contains` / `$notContains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) over a field whose DECLARED type is numeric, boolean, temporal (`date` / `datetime` / `time`) or structured JSON is refused before any driver runs — `INVALID_FILTER` / 400, naming the field and its declared type — instead of answering `[]` or a dialect accident. Shipped as `minor` under the repo's launch-window convention for breaking changes. Maintainer ruling 2026-09-05 on #15661 (director decision batch #43, verbatim 「同意」): option C-deny.

The refused set is the union of six EXISTING classes in `field-value.zod.ts`, by reference — `NUMERIC_VALUE_TYPES` ∪ `BOOLEAN_VALUE_TYPES` ∪ `CALENDAR_DATE_TYPES` ∪ `INSTANT_TYPES` ∪ `CLOCK_TIME_TYPES` ∪ `STRUCTURED_JSON_TYPES` — so no new vocabulary is minted and a member added to one of those sets later is refused without a change here. String-valued classes pass: `STRING_VALUE_TYPES`, `autonumber`, the option-code classes (single and multi — `tags` included), the record-id classes, and the file classes. `formula` is judged as the field type its declared `returnType` names (`text` passes; `number` / `boolean` / `date` are refused) and is deferred — not judged — when `returnType` is absent. A dotted path into a structured-JSON field stays unjudged, as `filter-dotted-head` already declares.

New on `@objectstack/spec/data` (`filter-text-operator-declared-type.ts`): `TEXT_FILTER_OPERATORS` (pinned equal to `StringOperatorSchema`'s keys), `TEXT_OPERATOR_DOOR_REFUSED_TYPES` / `TEXT_OPERATOR_DOOR_PASSING_TYPES`, `FORMULA_RETURN_TYPE_AS_FIELD_TYPE`, the pure verdict `textOperatorDoorVerdict`, the class table `TEXT_OPERATOR_DOOR_TYPE_CLASSES` (every `FieldType` member exactly once — pinned as a census), the fixture object `TEXT_OPERATOR_DOOR_FIXTURE`, and the derived case table `TEXT_OPERATOR_DOOR_CASES` the engine suite consumes.

The door itself lands in `@objectstack/objectql` under its own engine-lane card (beside the `INVALID_FIELD` unknown-field door, judged against the object's real field map, before any driver dispatch); this changeset is the contract half. Beneath the door nothing moves: a direct driver call — and every evaluator no door fronts — keeps answering `FILTER_TEXT_CASES`' stored-value row (#14079), and the SQL faces' compile-time type-gate set `NON_TEXT_STORED_VALUE_TYPES` stays numeric + boolean, deliberately narrower than the door's set.

What an author sees after the door lands: a condition such as `{ amount: { $contains: '5' } }` over a `number` field, which used to answer an empty list with no signal, is refused with a message naming `amount`, `number` and `$contains`. The condition was a mistake in every measured occurrence (a substring over a number can never match); drop it, or aim it at the text field that was meant.
20 changes: 20 additions & 0 deletions packages/spec/api-surface/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@
"FILTER_TEXT_ROWS (const)",
"FILTER_TOKEN_WRAPPED_RE (const)",
"FORMER_CREDENTIAL_ALIASES (const)",
"FORMULA_RETURN_TYPE_AS_FIELD_TYPE (const)",
"FeedFilterMode (type)",
"FeedItemType (type)",
"Field (type)",
Expand Down Expand Up @@ -609,6 +610,14 @@
"TEMPORAL_TIME_CASES (const)",
"TEMPORAL_TIME_ROWS (const)",
"TENANT_SCOPE_FIELD_DEF (const)",
"TEXT_FILTER_OPERATORS (const)",
"TEXT_OPERATOR_DOOR_CASES (const)",
"TEXT_OPERATOR_DOOR_FIXTURE (const)",
"TEXT_OPERATOR_DOOR_FIXTURE_FIELDS (const)",
"TEXT_OPERATOR_DOOR_FIXTURE_OBJECT (const)",
"TEXT_OPERATOR_DOOR_PASSING_TYPES (const)",
"TEXT_OPERATOR_DOOR_REFUSED_TYPES (const)",
"TEXT_OPERATOR_DOOR_TYPE_CLASSES (const)",
"TITLE_ELIGIBLE (const)",
"TITLE_ELIGIBLE_TYPES (const)",
"TITLE_INELIGIBLE_TYPES (const)",
Expand All @@ -620,6 +629,15 @@
"TemporalWriterForm (type)",
"TenancyConfig (type)",
"TenancyConfigSchema (const)",
"TextFilterOperator (type)",
"TextOperatorDoorCase (type)",
"TextOperatorDoorDeferredCase (interface)",
"TextOperatorDoorFieldMeta (interface)",
"TextOperatorDoorFixtureField (interface)",
"TextOperatorDoorPassesCase (interface)",
"TextOperatorDoorRefusalCase (interface)",
"TextOperatorDoorTypeClass (interface)",
"TextOperatorDoorVerdict (type)",
"TimeUpdateInterval (type)",
"TitleEligibleFieldDef (interface)",
"TransformType (type)",
Expand Down Expand Up @@ -724,6 +742,7 @@
"isPlainRecord (function)",
"isRuntimeDefaultToken (function)",
"isTenancyDisabled (function)",
"isTextFilterOperator (function)",
"isTitleEligible (function)",
"isUniqueDeclared (function)",
"isVirtualSearchField (function)",
Expand Down Expand Up @@ -779,6 +798,7 @@
"stripLegacyApiMethods (function)",
"suggestDefaultValueToken (function)",
"suggestFieldTypeForSqlType (function)",
"textOperatorDoorVerdict (function)",
"unprovisionedInjectedColumns (function)",
"urlCredentialQueryParams (function)",
"urlUserinfoPassword (function)",
Expand Down
20 changes: 20 additions & 0 deletions packages/spec/export-origins/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@
"FILTER_TEXT_ROWS": "src/data/filter-text-conformance.ts#FILTER_TEXT_ROWS (const)",
"FILTER_TOKEN_WRAPPED_RE": "src/data/context-tokens.zod.ts#FILTER_TOKEN_WRAPPED_RE (const)",
"FORMER_CREDENTIAL_ALIASES": "src/data/driver/common.zod.ts#FORMER_CREDENTIAL_ALIASES (const)",
"FORMULA_RETURN_TYPE_AS_FIELD_TYPE": "src/data/filter-text-operator-declared-type.ts#FORMULA_RETURN_TYPE_AS_FIELD_TYPE (const)",
"FeedFilterMode": "src/data/feed.zod.ts#FeedFilterMode (type)",
"FeedItemType": "src/data/feed.zod.ts#FeedItemType (type)",
"Field": "src/data/field.zod.ts#Field (type)",
Expand Down Expand Up @@ -609,6 +610,14 @@
"TEMPORAL_TIME_CASES": "src/data/temporal-conformance.ts#TEMPORAL_TIME_CASES (const)",
"TEMPORAL_TIME_ROWS": "src/data/temporal-conformance.ts#TEMPORAL_TIME_ROWS (const)",
"TENANT_SCOPE_FIELD_DEF": "src/data/injected-system-column-provenance.ts#TENANT_SCOPE_FIELD_DEF (const)",
"TEXT_FILTER_OPERATORS": "src/data/filter-text-operator-declared-type.ts#TEXT_FILTER_OPERATORS (const)",
"TEXT_OPERATOR_DOOR_CASES": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_CASES (const)",
"TEXT_OPERATOR_DOOR_FIXTURE": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_FIXTURE (const)",
"TEXT_OPERATOR_DOOR_FIXTURE_FIELDS": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_FIXTURE_FIELDS (const)",
"TEXT_OPERATOR_DOOR_FIXTURE_OBJECT": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_FIXTURE_OBJECT (const)",
"TEXT_OPERATOR_DOOR_PASSING_TYPES": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_PASSING_TYPES (const)",
"TEXT_OPERATOR_DOOR_REFUSED_TYPES": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_REFUSED_TYPES (const)",
"TEXT_OPERATOR_DOOR_TYPE_CLASSES": "src/data/filter-text-operator-declared-type.ts#TEXT_OPERATOR_DOOR_TYPE_CLASSES (const)",
"TITLE_ELIGIBLE": "src/data/display-name.ts#TITLE_ELIGIBLE (const)",
"TITLE_ELIGIBLE_TYPES": "src/data/display-name.ts#TITLE_ELIGIBLE_TYPES (const)",
"TITLE_INELIGIBLE_TYPES": "src/data/display-name.ts#TITLE_INELIGIBLE_TYPES (const)",
Expand All @@ -620,6 +629,15 @@
"TemporalWriterForm": "src/data/temporal-conformance.ts#TemporalWriterForm (type)",
"TenancyConfig": "src/data/object.zod.ts#TenancyConfig (type)",
"TenancyConfigSchema": "src/data/object.zod.ts#TenancyConfigSchema (const)",
"TextFilterOperator": "src/data/filter-text-operator-declared-type.ts#TextFilterOperator (type)",
"TextOperatorDoorCase": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorCase (type)",
"TextOperatorDoorDeferredCase": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorDeferredCase (interface)",
"TextOperatorDoorFieldMeta": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorFieldMeta (interface)",
"TextOperatorDoorFixtureField": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorFixtureField (interface)",
"TextOperatorDoorPassesCase": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorPassesCase (interface)",
"TextOperatorDoorRefusalCase": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorRefusalCase (interface)",
"TextOperatorDoorTypeClass": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorTypeClass (interface)",
"TextOperatorDoorVerdict": "src/data/filter-text-operator-declared-type.ts#TextOperatorDoorVerdict (type)",
"TimeUpdateInterval": "src/data/analytics.zod.ts#TimeUpdateInterval (type)",
"TitleEligibleFieldDef": "src/data/display-name.ts#TitleEligibleFieldDef (interface)",
"TransformType": "src/data/mapping.zod.ts#TransformType (type)",
Expand Down Expand Up @@ -724,6 +742,7 @@
"isPlainRecord": "src/data/authoring-key-lint.ts#isPlainRecord (function)",
"isRuntimeDefaultToken": "src/data/default-value-tokens.ts#isRuntimeDefaultToken (function)",
"isTenancyDisabled": "src/data/object.zod.ts#isTenancyDisabled (function)",
"isTextFilterOperator": "src/data/filter-text-operator-declared-type.ts#isTextFilterOperator (function)",
"isTitleEligible": "src/data/display-name.ts#isTitleEligible (function)",
"isUniqueDeclared": "src/data/field.zod.ts#isUniqueDeclared (function)",
"isVirtualSearchField": "src/data/search-fields.ts#isVirtualSearchField (function)",
Expand Down Expand Up @@ -779,6 +798,7 @@
"stripLegacyApiMethods": "src/data/object.zod.ts#stripLegacyApiMethods (function)",
"suggestDefaultValueToken": "src/data/default-value-shape.ts#suggestDefaultValueToken (function)",
"suggestFieldTypeForSqlType": "src/data/type-compat.ts#suggestFieldTypeForSqlType (function)",
"textOperatorDoorVerdict": "src/data/filter-text-operator-declared-type.ts#textOperatorDoorVerdict (function)",
"unprovisionedInjectedColumns": "src/data/injected-system-column-provenance.ts#unprovisionedInjectedColumns (function)",
"urlCredentialQueryParams": "src/data/driver/common.zod.ts#urlCredentialQueryParams (function)",
"urlUserinfoPassword": "src/data/driver/common.zod.ts#urlUserinfoPassword (function)",
Expand Down
Loading
Loading