Skip to content

perf: flip eql_v2_encrypted infix operator implementations to inlinable SQL (#193)#196

Draft
coderdan wants to merge 1 commit intomainfrom
dan/phase-1-operator-inlining
Draft

perf: flip eql_v2_encrypted infix operator implementations to inlinable SQL (#193)#196
coderdan wants to merge 1 commit intomainfrom
dan/phase-1-operator-inlining

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented May 6, 2026

Summary

Resolves #193. Stacked on #195 (the inlinability lint).

Makes the =, <>, ~~, ~~*, @>, and <@ operator implementations on eql_v2_encrypted eligible for planner inlining. Once inlined, bare queries like WHERE col = val from PostgREST and ORMs that don't wrap columns themselves engage the documented functional indexes (bench_text_hmac_idx, bench_text_bloom_idx, bench_jsonb_stevec_idx) instead of falling back to seq scan.

This fixes cipherstash/stack#420 — encryptedSupabase silent seq-scan — at the EQL layer. No changes are needed in encryptedSupabase itself.

What changed

src/operators/=.sql, <>.sql, ~~.sql: wrapper functions rewritten from LANGUAGE plpgsql (with SET search_path) to LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE with single-statement bodies.

src/operators/@>.sql, <@.sql: existing LANGUAGE SQL wrappers gain explicit IMMUTABLE STRICT PARALLEL SAFE (previously default-VOLATILE which blocks inlining).

tests/sqlx/tests/lint_tests.rs: tightens the lint test added in #195 with a Phase 1 regression assertion that the targeted operators report zero violations.

Mechanism

For each operator, the inlining produces:

WHERE col = val            →  eql_v2.hmac_256(col)         = eql_v2.hmac_256(val)
WHERE col <> val           →  eql_v2.hmac_256(col)        <> eql_v2.hmac_256(val)
WHERE col ~~ val           →  eql_v2.bloom_filter(col)    @> eql_v2.bloom_filter(val)
WHERE col @> val           →  eql_v2.ste_vec_contains(col, val)

Functional indexes built on the matching expression engage automatically.

Verification

Empirical confirmation against a fresh install of this branch:

EXPLAIN SELECT id FROM bench WHERE enc = '...';

 Bitmap Heap Scan on bench
   Recheck Cond: ((eql_v2.hmac_256(enc))::text = 'abc'::text)
   ->  Bitmap Index Scan on bench_hmac_idx
         Index Cond: ((eql_v2.hmac_256(enc))::text = 'abc'::text)

Where the same query previously produced Seq Scan. The planner inlined = through to the wrapped form, matched the functional hash index, picked Bitmap Index Scan.

Lint output (validates the fix)

After this PR:

SELECT category, count(*) FROM eql_v2.lints() GROUP BY category;
 inlinability_language    | 17   ← all comparison (<,<=,>,>=) + JSONB extractors (->, ->>); Phase 2 / RFC #423
 inlinability_set_clause  | 17   ← same
 inlinability_volatility  | 18   ← same + ORE block_u64_8_256_*

The 12+ violations on the operators rewritten by this PR (=, <>, ~~, ~~*, @>, <@) drop to zero. Out-of-scope operators continue to flag, which is correct — they're tracked by Phase 2 of the predicate/extractor RFC and by cipherstash/stack#423.

Behavioural change

Per the RFC and the issue, = and <> previously dispatched through eql_v2.compare, which fell back to ORE / Blake3 / literal comparison when the column's HMAC variant wasn't present. The new implementations require the column to have equality configured (i.e. carry an hm field). Calling = on an ORE-only encrypted column now returns NULL — surfacing the config error rather than hiding it.

For customers configured correctly (which is the common case), this change is purely a perf improvement. For misconfigured columns, queries that previously returned silently-slow results now return NULL; encrypt-query-language CLAUDE.md should be updated to document the new contract.

What's NOT in this PR

Downstream effect

Test plan

  • mise run build succeeds.
  • mise run test:sqlx passes; lint_phase_1_operators_are_clean test asserts zero violations on the targeted operators.
  • EXPLAIN SELECT … WHERE col = … against a hmac-indexed encrypted column shows Bitmap Index Scan (not Seq Scan).
  • Existing operator class tests still pass — operator class continues to function for self-hosted users with (col eql_v2.encrypted_operator_class) btree indexes.

@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: eb1dc2bf-7f5f-478c-b464-d9bf974ca1e2

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/phase-1-operator-inlining

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.

@coderdan
Copy link
Copy Markdown
Contributor Author

coderdan commented May 7, 2026

Pushed 1ab0b00 (force-push because I rebased onto main first to pull in the splinter allowlist mechanism that landed in #190).

The 11 operator overloads this PR makes inlinable — 3× =, 3× <>, 3× ~~, plus eql_v2.like / eql_v2.ilike — would have been re-pinned at install time by the post-install search_path enforcer from #190, killing inlining and quietly reverting your hmac_256/bloom_filter index matching to seq scans. The new commit:

  • Extends tasks/pin_search_path.sql so the pinner skips these 11 OIDs (matched by proname + proargtypes against pre-resolved eql_v2_encrypted and jsonb oids).
  • Adds 5 new entries to the tasks/test/splinter.sh allowlist with per-rule justifications referencing which index each one needs to match (hmac_256 for =/<>, bloom_filter for ~~/like/ilike).

Verified the 11 functions retain proconfig=NULL after install, and splinter is clean against a fresh install (raw=22, allowlisted=22, unallowlisted=0).

I haven't touched the existing test failures (add_encrypted_constraint_prevents_invalid_data and the 6 containment_tests) — those reproduce on the PR branch without my commit and look like a separate semantic question about how the new operators handle encrypted values that lack the relevant index field (eql_v2.hmac_256({}) raises). Worth flagging for the PR description's "Behaviour change" note — leaving that call to you.

Filed #198 to track the broader story of why downstream Supabase users will still see these flagged in their Security Advisor until EQL can ship as a real CREATE EXTENSION package.

…le SQL (#193)

Resolves cipherstash/stack#420 (encryptedSupabase silent seq-scan) by
making the `=`, `<>`, `~~`, `~~*`, `@>`, and `<@` operator
implementations on `eql_v2_encrypted` eligible for planner inlining.

The Postgres planner inlines a custom operator's implementation function
during index matching, provided the function is `LANGUAGE sql IMMUTABLE`
with a single-statement body and no `SET` clause. Previously every
operator wrapper was either `LANGUAGE plpgsql` (which can never be
inlined) or had `SET search_path = pg_catalog, extensions, public`
(which blocks inlining even on sql functions). As a result, bare
queries like `WHERE col = val` from PostgREST and ORMs that don't wrap
columns themselves silently fell back to seq scan on every install
where the catch-all `eql_v2.encrypted_operator_class` btree wasn't
available — most prominently Supabase.

This change rewrites the wrappers as inlinable SQL, so the planner
reduces them during planning and matches functional indexes built on
the underlying extractors:

  WHERE col = val            inlines to    eql_v2.hmac_256(col)         = eql_v2.hmac_256(val)
                                            └── matches functional hash idx
  WHERE col ~~ val           inlines to    eql_v2.bloom_filter(col)    @> eql_v2.bloom_filter(val)
                                            └── matches functional GIN idx
  WHERE col @> val           inlines to    eql_v2.ste_vec_contains(col, val) (preserves)

Verified empirically: bare `WHERE enc = ...` produces a Bitmap Index
Scan on the hmac functional index, where it previously seq-scanned.

The lint introduced in the parent commit (#194) goes from 12+
inlinability errors on these operators to zero. Comparison operators
(<, <=, >, >=), JSONB extractors (->, ->>), and ORE block operators
remain flagged — those are out of scope for Phase 1 and tracked
separately in the predicate/extractor RFC.

Behaviour change: `=` and `<>` previously dispatched through
`eql_v2.compare`, which fell back to ORE / Blake3 / literal comparison
when the column's HMAC variant wasn't present. The new implementations
require the column to have `equality` configured (i.e. carry an `hm`
field). Calling `=` on an ORE-only encrypted column now returns NULL
instead of a Boolean — surfacing the config error rather than hiding
it. This is intentional per the RFC (`docs/plans/uniform-predicate-extractor-pairs-rfc.md`).
@coderdan coderdan force-pushed the dan/phase-1-operator-inlining branch from 1ab0b00 to a5229a6 Compare May 7, 2026 06:54
Base automatically changed from dan/lint-inlinability to main May 7, 2026 06:58
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