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
|**$icontains**|`string`| optional | Contains substring, ignoring case — but ONLY ASCII case (A-Z against a-z). Every other character compares literally, so "café" does NOT match "CAFÉ" and "москва" does not match "МОСКВА". The domain is ASCII because that is the one fold all five backends can deliver: SQLite (and therefore turso and sqlite-wasm) folds ASCII only, so a Unicode promise here would be a guarantee three of the five could not keep. The comparand is matched LITERALLY — "%", "_" and regex metacharacters are ordinary characters, not wildcards. Case-SENSITIVE containment is $contains. [#5701: declared by the protocol; the driver lowerings land with #5702.]|
150
+
|**$icontains**|`string`| optional | Contains substring, ignoring case — but ONLY ASCII case (A-Z against a-z). Every other character compares literally, so "café" does NOT match "CAFÉ" and "москва" does not match "МОСКВА". The domain is ASCII because that is the one fold all five backends can deliver: SQLite (and therefore turso and sqlite-wasm) folds ASCII only, so a Unicode promise here would be a guarantee three of the five could not keep. The comparand is matched LITERALLY — "%", "_" and regex metacharacters are ordinary characters, not wildcards. Case-SENSITIVE containment is $contains. [#5701 declared it; #5702 lowered it on the SQL family (driver-sql, driver-sqlite-wasm, driver-turso on both transports). driver-memory and driver-mongodb still REFUSE it with INVALID_FILTER / 400, so a filter using it is not portable across backends yet — #6520.]|
* `$contains`. An application whose users search non-ASCII text should not read
295
295
* `$icontains` as "accent- and case-blind search" — it is not one.
296
296
*
297
-
* ### Implementation status — declared here, NOT yet answered by any backend
298
-
*
299
-
* This PR is the contract half of the #4706 ruling and deliberately ships no
300
-
* runtime behaviour (#5701). No driver evaluates `$icontains` today; all five
301
-
* refuse it, loudly, as an operator they do not implement — which is the
302
-
* fail-closed direction and stays true until #5702 lands the lowerings. The
303
-
* same issue carries the `$contains`-family alignment the ruling above
304
-
* requires (SQLite/turso `LIKE` made case-exact, mongo's hardcoded `'i'`
305
-
* removed). Until then the sentence above is the DECLARATION and #5702 is the
306
-
* gap; `FILTER_TEXT_CASES` (`filter-text-conformance.ts`) is the standard that
307
-
* measures the gap, and the driver-conformance ledger carries one DEBT row per
308
-
* backend so the gap is counted rather than assumed.
297
+
* ### Implementation status — answered by the SQL family, refused by the rest
298
+
*
299
+
* #5701 shipped this declaration deliberately ahead of every runtime, and
300
+
* #5702 (closed 2026-08-08, retuned by #6518) landed the lowerings on the SQL
301
+
* family. Measured per backend, by running `{ name: { $icontains: 'acme' }}`
302
+
* against a fixture holding BOTH `acme corp` and `ACME CORP` — not by grepping
303
+
* for a case arm, which is blind to the face that inherits its compiler and so
304
+
* undercounts:
305
+
*
306
+
* | driver | `$icontains` | how it gets there |
307
+
* |---|---|---|
308
+
* | `driver-sql` | ANSWERS both rows | its own `case '$icontains'`, folding through the same emitter that carries the escaping |
309
+
* | `driver-sqlite-wasm` | ANSWERS both rows | INHERITED — `SqliteWasmDriver extends SqlDriver`; this package carries no text case arm of its own, on a different ENGINE |
310
+
* | `driver-turso` | ANSWERS both rows, on BOTH transports | local inherits `SqlDriver`; the remote transport compiles independently and has its own arm |
311
+
* | `driver-memory` | REFUSES — `INVALID_FILTER` / 400 | no arm; its `SUPPORTED_FIELD_OPERATORS` derives from {@link FILTER_OPERATORS}, which deliberately omits it |
312
+
* | `driver-mongodb` | REFUSES — `INVALID_FILTER` / 400 | no arm; falls to its translator's `default:` |
313
+
*
314
+
* The other JS evaluators sit on the refusing side too: objectql's `having`
315
+
* face records the omission in its own source, and `formula`'s `matchesFilter`
316
+
* has no `$icontains` arm.
317
+
*
318
+
* **So the sentence an author needs is no longer "no backend answers this".**
319
+
* It is: `$icontains` is EXECUTABLE on the SQL family and refused — loudly,
320
+
* fail-closed, never silently — everywhere else, so a filter that uses it is
321
+
* not portable across backends today. An app whose tests run on the in-memory
322
+
* double and whose production runs SQL gets two different answers from one
323
+
* filter: that divergence, and the remaining implementations, are #6520.
324
+
*
325
+
* **The vocabulary gate below is still closed, and #5702 is no longer what it
326
+
* waits for.** `$icontains` stays out of {@link FILTER_OPERATORS} on purpose —
327
+
* that array is a runtime allowlist, and listing an operator the in-memory
328
+
* `match()` cannot evaluate makes it answer `true` for a NON-match (measured;
329
+
* see that array's docblock). It joins when the JS faces get arms, in #6520.
330
+
*
331
+
* The `$contains`-family alignment the ruling above requires is likewise part
332
+
* done rather than pending: #6518 made that family case-EXACT across the SQL
333
+
* dialects, while `driver-memory`'s query path and `driver-mongodb` still fold
334
+
* the whole Unicode range — the two rows #6682 tracks.
335
+
*
336
+
* `FILTER_TEXT_CASES` (`filter-text-conformance.ts`) is the standard that
337
+
* measures all of the above, and the driver-conformance ledger still carries a
338
+
* DEBT row for each of the two backends left, so what is open stays counted
339
+
* rather than assumed.
309
340
*
310
341
* @see FILTER_TEXT_CASES — the conformance standard for every operator here.
311
342
* @see RETIRED_FILTER_OPERATORS — why `$regex` is not in this list.
312
343
* @see https://github.com/objectstack-ai/objectstack/issues/4706 (the ruling)
313
-
* @see https://github.com/objectstack-ai/objectstack/issues/5702 (the backends)
344
+
* @see https://github.com/objectstack-ai/objectstack/issues/5702 (the SQL family — landed)
345
+
* @see https://github.com/objectstack-ai/objectstack/issues/6520 (the JS faces — open)
0 commit comments