Skip to content

feat(stack)!: rename EQL v3 encrypted contains() → matches() (#617)#640

Merged
coderdan merged 3 commits into
feat/eql-v3-text-search-schemafrom
feat/eql-v3-rename-contains-to-matches
Jul 13, 2026
Merged

feat(stack)!: rename EQL v3 encrypted contains() → matches() (#617)#640
coderdan merged 3 commits into
feat/eql-v3-text-search-schemafrom
feat/eql-v3-rename-contains-to-matches

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

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 true may be a false positive; a false never is). contains() promised substring/containment semantics it never had. Renamed to matches() 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).
  • Distinct gating (requires free-text search vs requires 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 existing cs/@> emit plumbing).
  • .contains() = native jsonb/array @> on plaintext columns; throws on an encrypted column, pointing to matches().
  • Compile-time guard (not just runtime): the typed surface keys matches() on encrypted-free-text-only columns (V3EncryptedFreeTextKeys) and contains() 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 physically jsonb, so it would allow .contains(); the encrypted/free-text distinction lives only in our v3 schema.)
  • like()/ilike() on encrypted columns now delegate to matches() 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 hides like/ilike (steering to matches() at compile time); delegation serves untyped/JS callers.

Tests / verification

  • Drizzle 357 unit + type tests; Supabase 450 unit + 25 type tests (incl. the tightened compile-time guard: matches on plaintext and contains on encrypted are @ts-expect-error); code:check clean. Wire-level cs/ciphertext assertions preserved (matches emits the same operator as the old encrypted contains). Integration matrices run on this PR.

Follow-ups

Closes #617

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

…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
@coderdan coderdan requested a review from a team as a code owner July 13, 2026 10:22
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf92ae3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
stash Patch
@cipherstash/bench Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch

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

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9cc62945-9814-4231-9c51-6eade12fd595

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eql-v3-rename-contains-to-matches

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

#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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 contains behavior into matches() (free-text bloom match) and contains() (exact encrypted JSONB containment).
  • Supabase v3: add .matches() for encrypted free-text and make .contains() plaintext-only; additionally make like/ilike on encrypted columns delegate (best-effort) to matches().
  • 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 containsmatches.
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 containsmatches.
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.

Comment thread packages/stack-supabase/src/types.ts Outdated
Comment thread packages/stack-supabase/src/query-builder-v3.ts
…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
@coderdan coderdan merged commit 0f12bc8 into feat/eql-v3-text-search-schema Jul 13, 2026
11 checks passed
@coderdan coderdan deleted the feat/eql-v3-rename-contains-to-matches branch July 13, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants