feat(stack)!: rename EQL v3 encrypted contains() → matches() (#617)#640
Conversation
…tches() (#617) EQL v3 encrypted free-text search is fuzzy bloom-filter token matching — order- and multiplicity-insensitive, one-sided (a `true` may be a false positive) — not containment. `contains()` promised substring/containment semantics it never had. Rename it to `matches()` on the encrypted surface; keep `contains()` for genuine exact containment. Drizzle (`/v3`): - `matches()` = bloom free-text on text_match/text_search columns. - `contains()` = exact encrypted-JSON `@>` on `types.Json` (ste_vec) columns. - Split gating: matches requires free-text; contains requires JSON containment. Supabase: - `.matches()` = encrypted free-text (new `'matches'` FilterOp, additive to the shared base builder so v2 is untouched; rides the existing `cs`/`@>` plumbing). - `.contains()` = native jsonb/array `@>` on plaintext columns; throws on an encrypted column, pointing to matches(). - Typed surface: `matches` is keyed on encrypted-free-text-only columns (`V3EncryptedFreeTextKeys`) so `matches()` on a plaintext column is a COMPILE error, not a runtime throw; `contains` is keyed on plaintext columns. The runtime throws are the untyped-path backstop. - `like()`/`ilike()` on an encrypted column are no longer rejected — they delegate to matches() as a best-effort compatibility shim (approximate: fuzzy, case- insensitive, one-sided; surrounding `%` stripped, internal `%`/`_` rejected, a one-time warning). Plaintext columns keep real SQL LIKE. The typed surface still hides like/ilike (steering to matches at compile time); delegation serves the untyped/JS path. Docs + skills (stash-drizzle, stash-supabase) updated; changesets added (both adapters minor, stash patch for skills). Follow-ups: eql-repo rename (encrypt-query-language#396) and the delegating-facade refactor (#638). Closes #617 Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
🦋 Changeset detectedLatest commit: bf92ae3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
#617) CI on #640 caught that the integration matrices (separate from the unit __tests__/) still called the old encrypted contains(). Complete the rename there: - test-kit: the abstract matrix op `contains` → `matches` (free-text only; searchableJson stays deferred to the dedicated json-contains suite); oracle `containsPlain` → `matchesPlain`; run-family-suite + index updated. - Drizzle/Supabase integration adapters: `supportedOps` + `run()` case `contains` → `matches` (encrypted free-text). - Drizzle relational suite: free-text `ops.contains` → `ops.matches`; the json-contains suite keeps `ops.contains` (exact JSON @>). - Supabase wire suite: encrypted `.contains('email')` → `.matches('email')`; plaintext `.contains('tags'/'meta')` + or-conditions unchanged; the old "refuses like()" test replaced with delegation coverage (`like('%ada%')` delegates to matches; an internal `%`/`_` throws). Local: builds, unit + type tests, code:check all green; integration verified by CI. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
There was a problem hiding this comment.
Pull request overview
This PR renames the EQL v3 encrypted free-text operator from contains() to matches() across the Drizzle v3 and Supabase v3 adapters to better reflect the underlying fuzzy bloom-filter token matching semantics, while reserving contains() for exact (native) containment.
Changes:
- Drizzle v3: split the prior
containsbehavior intomatches()(free-text bloom match) andcontains()(exact encrypted JSONB containment). - Supabase v3: add
.matches()for encrypted free-text and make.contains()plaintext-only; additionally makelike/ilikeon encrypted columns delegate (best-effort) tomatches(). - Update skills/docs/tests and add changesets to reflect the rename and the compatibility shim behavior.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/stash-supabase/SKILL.md | Update Supabase skill docs to describe matches() vs plaintext contains(), and the encrypted like/ilike shim. |
| skills/stash-drizzle/SKILL.md | Update Drizzle skill docs to use matches() for free-text and reserve contains() for JSON containment. |
| packages/test-kit/src/run-family-suite.ts | Rename matrix op kind from contains to matches for free-text families. |
| packages/test-kit/src/oracle.ts | Rename plaintext oracle helper to matchesPlain. |
| packages/test-kit/src/ops.ts | Update query op kinds/capability mapping to use matches for free-text. |
| packages/test-kit/src/needle-for.ts | Rename documentation references from contains needle to matches needle. |
| packages/test-kit/src/integration/no-skips-reporter.ts | Update skip-report messaging to refer to matches. |
| packages/test-kit/src/index.ts | Re-export matchesPlain instead of containsPlain. |
| packages/stack-supabase/src/types.ts | Add typed matches() for encrypted free-text and narrow contains() to plaintext containment; extend FilterOp. |
| packages/stack-supabase/src/query-builder.ts | Add matches() recording and route matches through the existing cs/@> plumbing. |
| packages/stack-supabase/src/query-builder-v3.ts | Enforce v3 runtime behavior for matches()/contains() and implement encrypted like/ilike delegation to matches(). |
| packages/stack-supabase/src/helpers.ts | Treat matches as a containment-wire op (cs) for .or() parsing/formatting and query-type mapping. |
| packages/stack-supabase/integration/wire.integration.test.ts | Update live wire tests to use .matches() and validate encrypted like/ilike delegation/rejection rules. |
| packages/stack-supabase/integration/adapter.ts | Update integration adapter op from contains → matches. |
| packages/stack-supabase/tests/supabase-v3.test-d.ts | Update type tests for the new compile-time guards on matches()/contains(). |
| packages/stack-supabase/tests/supabase-v3-matrix.test.ts | Update matrix tests to assert matches() emits cs and that encrypted like() delegates to matches(). |
| packages/stack-supabase/tests/supabase-v3-builder.test.ts | Update builder wire-encoding tests for matches() and contains() behavior, plus like/ilike delegation. |
| packages/stack-drizzle/src/v3/operators.ts | Split operators into matches() (free-text) and contains() (JSON containment), with distinct gating/errors. |
| packages/stack-drizzle/integration/relational.integration.test.ts | Update integration tests to call ops.matches() and keep needle guard coverage. |
| packages/stack-drizzle/integration/json-contains.integration.test.ts | Clarify docstring and semantics for JSON containment via ops.contains(). |
| packages/stack-drizzle/integration/adapter.ts | Update integration adapter op from contains → matches. |
| packages/stack-drizzle/tests/v3/operators.test.ts | Update unit tests for matches() emission/guards and contains() JSON gating. |
| docs/reference/supabase-sdk.md | Update Supabase reference docs for matches()/plaintext contains() and encrypted like/ilike shim behavior. |
| .changeset/stash-skills-contains-to-matches.md | Add changeset for skills documentation updates shipped in the stash tarball. |
| .changeset/eql-v3-rename-contains-to-matches.md | Add changeset documenting the breaking rename and Supabase shim behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…640) - Narrow the untyped v3 `matches()` operand from NativeContainsValue to `string`: the free-text term is always a string, so `matches('email', {a:1})` is now a compile error on the untyped surface too, not just a runtime throw. - Close the last `contains`-name bypass: `not(col, 'contains', …)` on an encrypted column rewrote `contains`→`cs` in the base not() path, letting fuzzy bloom matching run under the `contains` name. The v3 builder now rejects it and steers to the honest `matches` spelling. (`.filter(col,'contains',…)` and structured `.or([{op:'contains'}])` already throw via queryTypeForRawOp; only not() rewrote it.) Updated the two wire tests to the `matches` spelling and added a rejection test. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Targets the 1.0 candidate
feat/eql-v3-text-search-schema.Why
EQL v3 encrypted free-text search is fuzzy bloom-filter token matching — the needle's downcased 3-gram set tested (bloom-approximated) as a subset of the haystack's. It is order- and multiplicity-insensitive and one-sided (a
truemay be a false positive; afalsenever is).contains()promised substring/containment semantics it never had. Renamed tomatches()on the encrypted surface;contains()is kept for genuine exact containment.Changes
Drizzle (
@cipherstash/stack-drizzle/v3) — split the one operator into two:matches()= bloom free-text (text_match/text_search).contains()= exact encrypted-JSON@>(types.Json/ste_vec).requires free-text searchvsrequires JSON containment).Supabase (
@cipherstash/stack-supabase):.matches()= encrypted free-text (new'matches'FilterOp, additive to the shared base builder so v2 is untouched; rides the existingcs/@>emit plumbing)..contains()= native jsonb/array@>on plaintext columns; throws on an encrypted column, pointing tomatches().matches()on encrypted-free-text-only columns (V3EncryptedFreeTextKeys) andcontains()on plaintext columns, so calling either on the wrong column kind is a compile error. The runtime throws are the untyped-path backstop. (supabase-js can't provide this — an encrypted column is physicallyjsonb, so it would allow.contains(); the encrypted/free-text distinction lives only in our v3 schema.)like()/ilike()on encrypted columns now delegate tomatches()as a best-effort compatibility shim instead of throwing — approximate: fuzzy, case-insensitive, one-sided; surrounding%stripped, internal%/_rejected, one-time warning. Plaintext columns keep real SQL LIKE. The typed surface still hideslike/ilike(steering tomatches()at compile time); delegation serves untyped/JS callers.Tests / verification
matcheson plaintext andcontainson encrypted are@ts-expect-error);code:checkclean. Wire-levelcs/ciphertext assertions preserved (matches emits the same operator as the old encrypted contains). Integration matrices run on this PR.Follow-ups
encrypt-query-language#396— rename the SQL-leveleql_v3.containsto match.Closes #617
https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w