perf(stack/drizzle): wrap eq/ne/inArray/notInArray in eql_v2.hmac_256(...)#425
perf(stack/drizzle): wrap eq/ne/inArray/notInArray in eql_v2.hmac_256(...)#425
Conversation
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR wraps Drizzle's equality operators ( ChangesEquality Operators with Functional Hash Index
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
🦋 Changeset detectedLatest commit: 2d21c39 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 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 |
44a68f4 to
4f130bb
Compare
bffcf21 to
651de1b
Compare
4f130bb to
0ab1d66
Compare
651de1b to
4b935f1
Compare
…(...)
Bare `col = value` (and `<>`, `IN`, `NOT IN`) only engages the
`eql_v2.encrypted_operator_class` btree index, which doesn't exist on
Supabase or any --exclude-operator-family install. Customers there silently
seq-scan every encrypted equality lookup.
Switch the emitted SQL to wrap both sides in `eql_v2.hmac_256(...)` so the
canonical hmac_256 functional hash index engages. Verified via the bench
introduced in the parent commit:
Before fix: After fix:
eq Seq Scan eq Index Scan on bench_text_hmac_idx
inArray Seq Scan inArray Bitmap Heap Scan
ne Seq Scan (low-selectivity) ne Seq Scan (low-selectivity, expected)
notInArray Seq Scan (" ") notInArray Seq Scan (" ", expected)
`ne` and `notInArray` continue to seq-scan post-fix, but for the right
reason: `<>` against a single value matches ~all rows, so the planner
correctly avoids the index. The SQL form is now correct and the planner
gets to make the right decision — same code path is correct on Supabase.
25e1b97 to
bbe8431
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/stack/src/drizzle/operators.ts`:
- Around line 1490-1495: The current code filters out undefined entries from
encryptedValues causing silent predicate changes; instead, check for missing
encryption results and throw an error (fail fast) when any encrypted value is
undefined so callers like inArray() and notInArray() don't get altered
predicates; update the logic around encryptedValues in operators.ts (the block
that builds conditions using eql_v2.hmac_256 and bindIfParam) to validate and
throw on undefined entries, and apply the same guard in encryptedNotInArray, or
alternatively enforce one-result-per-input inside encryptValues() so all callers
receive complete results.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: db7911e8-b9de-4522-9ef2-b691c4f4654d
📒 Files selected for processing (2)
.changeset/drizzle-hmac-256-equality.mdpackages/stack/src/drizzle/operators.ts
…login`) Per Lindsay's review: the documented login flow is now `npx stash auth login`, not the legacy `stash login`.
…ails Previously the operators silently filtered out array values whose encryption returned `undefined` — but that changes query semantics: - `inArray([a, b, c])` with `b` failing to encrypt would compile to `inArray([a, c])` and miss real matches. - `notInArray([a, b, c])` with `b` failing would compile to `notInArray([a, c])` and admit rows that should be excluded. Throw a `EncryptionOperatorError` instead so the failure surfaces at the boundary instead of producing a quietly wrong predicate. Spotted by CodeRabbit on #425.
Summary
Stacked on #424.
Fixes #421. Bare
col = value(and<>,IN,NOT IN) only engages theeql_v2.encrypted_operator_classbtree index, which doesn't exist on Supabase or any--exclude-operator-familyinstall. Customers there silently seq-scan every encrypted equality lookup at any scale.This change wraps both sides of the comparison in
eql_v2.hmac_256(...)so the canonical hmac_256 functional hash index engages on every install.What changed
packages/stack/src/drizzle/operators.ts:eq/ne(around line 728): emitted SQL goes fromcol = $1toeql_v2.hmac_256(col) = eql_v2.hmac_256($1)(and<>forne).inArray/notInArray(around lines 1486 / 1532): each comparison in the OR/AND chain wraps both sides ineql_v2.hmac_256(...). Postgres can BitmapOr several hash-index scans, so the OR-of-eq stays as fast as a single equality lookup per value.Plan-shape verification
EXPLAINon the same 10k-row fixture (from #424's bench), before vs after:eqinArraynenotInArrayneandnotInArraycontinue to seq-scan post-fix, but for the right reason:<>against a single value matches ~all rows, so the planner correctly avoids the index. The SQL form is now correct on Supabase and the planner makes the right call regardless.Wall-clock benchmark
pnpm -F @cipherstash/bench bench:localagainst the same 10k-row fixture, vitest--bench(tinybench under the hood). Higherhzis better; lowermeanis better.eq(string match)inArray(3 string matches)like(prefix)gt(int)between(int)orderBy desc + limit 10Operators outside the scope of this fix (
like,gt,between,orderBy) are unchanged within noise — those still seq-scan because their call-shaped forms aren't being inlined by the planner. Tracked separately in #422.What's NOT in this PR
packages/drizzle/src/pg/operators.ts:731has the identical bug. Tracked in perf: legacy @cipherstash/drizzle has the same bare-equality bug as @cipherstash/stack/drizzle #426.jsonbPathQueryFirst/jsonbGettyped as predicates but returneql_v2_encrypted) is an API-shape bug surfaced by the same bench. Tracked separately.Test plan
packages/bench:pnpm db:setup && pnpm test:local. Expect 9/9 green on__tests__/drizzle/operators.explain.test.ts.packages/bench/results/explain-shape.json—eqshould reportIndex Scan on bench_text_hmac_idx;inArrayshould reportBitmap Heap Scan.@cipherstash/stackunit tests still pass:pnpm -F @cipherstash/stack test.Summary by CodeRabbit