Skip to content

perf(stack/drizzle): wrap like/ilike in eql_v2.bloom_filter(...) @> ...#430

Closed
coderdan wants to merge 1 commit intodan/fix-bare-equality-supabasefrom
dan/fix-bloom-filter-like-ilike
Closed

perf(stack/drizzle): wrap like/ilike in eql_v2.bloom_filter(...) @> ...#430
coderdan wants to merge 1 commit intodan/fix-bare-equality-supabasefrom
dan/fix-bloom-filter-like-ilike

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented May 6, 2026

Summary

Stacked on #425.

Addresses the like / ilike slice of #422. eql_v2.like and eql_v2.ilike are SQL functions whose bodies are already a single inlinable SELECT eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b) — but they're marked VOLATILE, which blocks the planner from inlining them into an index match. The documented bench_text_bloom_idx GIN functional index never engages on Supabase, so every encrypted free-text search silently seq-scans.

Drizzle now emits the inlined containment form directly, bypassing the function indirection. Same shape as #425's hmac_256 wrap for eq/ne/inArray.

What changed

packages/stack/src/drizzle/operators.ts (around line 854): emitted SQL changes from

WHERE eql_v2.like(col, $1)

to

WHERE eql_v2.bloom_filter(col) @> eql_v2.bloom_filter($1::eql_v2_encrypted)

like and ilike resolve to the same post-encryption SQL — case sensitivity is determined by the column's freeTextSearch token filters (e.g. downcase), not by which operator the user picked. The wrapped form preserves that.

notIlike becomes NOT (bloom_filter(col) @> bloom_filter(value)) (same NOT wrap as before).

Plan-shape verification

Same 10k-row fixture as #425. Note: the search pattern needs to be selective — the bench was using %value-00000% which is shared by every seeded row, so the planner correctly picked seq scan even with the index available. Updated to %value-0000042% (matches one row).

Operator Before fix After fix
like Seq Scan Bitmap Heap Scan (on bench_text_bloom_idx)
ilike Seq Scan Bitmap Heap Scan (on bench_text_bloom_idx)

Proper fix (EQL-side, follow-up)

The durable cleanup is to mark eql_v2.like and eql_v2.ilike as IMMUTABLE in EQL itself — they're already single-statement LANGUAGE sql functions, so they'd inline cleanly and the planner would match the index without any Drizzle change. That helps every consumer of EQL (raw SQL, other ORMs, etc.), not just Drizzle. Will file an issue against cipherstash/encrypt-query-language describing the volatility fix.

This Drizzle-side wrap is the same kind of workaround #425 introduced for hmac_256 — it gets users the speedup immediately while the EQL fix lands.

What's NOT in this PR

Issue #422 also covers gt / gte / lt / lte / between / order_by / jsonb_path_*. None of those have a stack-side wrap available today:

  • gt / gte / lt / lte / between / order_by: no Supabase functional index path exists in EQL today. Blocked on the OPE work in flight in cipherstash/encrypt-query-language.
  • jsonb_path_query_first / jsonb_path_exists / jsonbGet: function bodies don't reduce to a containment shape on eql_v2.ste_vec(col). Needs an EQL-side investigation.
  • jsonbPathQueryFirst / jsonbGet typing: tracked separately in Drizzle jsonbPathQueryFirst / jsonbGet typed as predicates but return eql_v2_encrypted #423.

The bench's #422: remaining call-shaped operators suite continues to record their plan shapes (still seq scan) without asserting; tests won't block CI.

Test plan

  • On this branch in packages/bench: pnpm db:setup && pnpm test:local. Expect 10/10 green on __tests__/drizzle/operators.explain.test.ts.
  • Inspect packages/bench/results/explain-shape.jsonlike and ilike should report Bitmap Heap Scan.
  • Existing @cipherstash/stack unit tests still pass: pnpm -F @cipherstash/stack test.

`eql_v2.like` and `eql_v2.ilike` are SQL functions whose bodies are
already a single inlinable `SELECT eql_v2.bloom_filter(a) @>
eql_v2.bloom_filter(b)` — but they're marked `VOLATILE`, so the planner
won't inline them into an index match, and the documented
`bench_text_bloom_idx` GIN functional index never engages. Customers on
Supabase silently seq-scan every encrypted free-text search.

Emit the inlined containment form directly so the GIN index engages on
every install. Same shape as the hmac_256 wrap for eq/ne/inArray (#421).
Verified via the bench: like and ilike both report `Bitmap Heap Scan`
(over `bench_text_bloom_idx`) post-fix instead of `Seq Scan`.

Note that `eql_v2.like` and `eql_v2.ilike` resolve to the same
post-encryption SQL — case sensitivity is determined by the column's
`freeTextSearch` token filters (e.g. `downcase`), not by which operator
the user picked. The wrapped form preserves that behavior.

The proper EQL-side fix (mark these functions IMMUTABLE so the planner
can inline them and bypass the Drizzle workaround entirely) should be
filed against cipherstash/encrypt-query-language as a follow-up.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 6, 2026

🦋 Changeset detected

Latest commit: 3a17789

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

This PR includes changesets to release 3 packages
Name Type
@cipherstash/stack Patch
@cipherstash/bench Patch
@cipherstash/basic-example 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
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Important

Review skipped

Draft detected.

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: 03460286-2701-48dd-a367-4d8500c7a030

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 dan/fix-bloom-filter-like-ilike

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 and usage tips.

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.

1 participant