Skip to content

fix(eql): schema-qualify operators in comparison wrapper bodies - #1551

Merged
tobyhede merged 2 commits into
mainfrom
claude/project-thread-rckj8w
Nov 11, 2026
Merged

tobyhede merged 2 commits into
mainfrom
claude/project-thread-rckj8w

Conversation

@cipherstash-agent

@cipherstash-agent cipherstash-agent commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by CipherStash · project thread

Summary

Fixes CIP-5153. The _ord_ore comparison functions (eql_v3.lt, lte, gt, gte, eq, neq) gave silently wrong answers when public was not on the caller's search_path.

The wrappers are unpinned LANGUAGE sql so they inline into functional-index scans, which means PostgreSQL parses their bodies under the caller's search_path. The ORE wrappers compare eql_v3_internal.ore_block_256, a composite type whose operators are created in public. Without public on the path (a SECURITY DEFINER helper with SET search_path = '', as Supabase recommends, or a hardened role path), the bare < resolved to pg_catalog's built-in record comparison, ordering the ciphertext bytes with no error. Reproduced on PostgreSQL 16: a malformed pair raises "Ciphertexts are different lengths" on the default path and returns t under search_path = ''.

Changes

  • eql-domains: new Term::operator_schema() (public for ORE, pg_catalog for HMAC, bloom and OPE) and Term::term_for_operator(); extractor_for_operator now delegates to it.
  • eql-codegen: wrapper_entry takes the serving Term and emits OPERATOR(<schema>.<op>); the bloom empty-needle guard qualifies cardinality and its operators too. All generated wrapper SQL under src/v3/scalars/ is regenerated (body lines only).
  • src/v3/json/operators.sql: the four hand-written json_entry ordering wrappers name OPERATOR(pg_catalog.<op>).
  • skills/stash-postgres: a new trap entry on querying encrypted columns when public is off the search_path (a bare col < $1 still falls back to native jsonb there; use the function form or OPERATOR(public.<)).
  • Changeset: @cipherstash/eql patch, stash patch (skill).

Verification

  • cargo test -p eql-domains -p eql-codegen: green apart from two tests that fail when run as root (generate_type_failure_does_not_delete_before_writing, write_failure_preserves_existing_target_and_leaves_no_temp); they fail the same way on main in this environment.
  • New codegen test every_wrapper_body_schema_qualifies_its_operators checks every generated wrapper body across the catalog: no bare operator token, and the right schema per term (literal, so a wrong operator_schema mapping fails it; confirmed by flipping ORE to pg_catalog).
  • New SQLx suite v3_search_path_independence_tests: every ORE-carrying domain's wrappers, query-twin overloads and OPERATOR(public.<op>) forms under search_path = '', checked against compare_ore_block_256_terms on pairs where ORE order and byte order disagree; plus a SET search_path = '' SECURITY DEFINER helper on a malformed pair. Red on the published 3.0.6 installer, green on this branch (local PostgreSQL 16).
  • Fixture-free SQLx suites pass locally: lint_tests, v3_public_surface_tests, v3_operator_equivalents_tests, v3_jsonb_operator_surface_tests, v3_ore_fallback_tests, build_validation_tests, payload_schema_tests. Suites needing ZeroKMS-generated fixtures were not run locally (no credentials); CI runs them.
  • Index engagement checked by hand: EXPLAIN on c < query and on eql_v3.lt(c, query) under search_path = '' both use the eql_v3.ord_term_ore(c) btree index.
  • cargo fmt --check, cargo clippy clean.

Related

Review notes

  • Bare col < q with public off the path still resolves to native jsonb. That is the cross-family fall-through tracked in CIP-5155, out of scope here; the changeset and skill say so.
  • The release SQL bundles (packages/eql/packages/eql/sql, crates/eql-bindings/sql) are left alone; the release version hook rebuilds them when this changeset bumps EQL.

Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Encrypted-column comparisons now resolve consistently even when public is absent from PostgreSQL’s search_path, preventing incorrect results from unintended built-in comparisons.
    • Generated comparison functions remain eligible for inlining. Bare operators on encrypted columns still require public in the search path.
  • Documentation
    • Added guidance on querying encrypted columns without public in the search path. EQL must be reinstalled or upgraded to use the fix.

The eql_v3 comparison wrappers (eql_v3.lt, eq, ...) are unpinned
LANGUAGE sql functions so they can inline into functional-index scans,
which means PostgreSQL parses their bodies under the caller's
search_path. The _ord_ore wrappers compare eql_v3_internal.ore_block_256,
a composite type whose comparison operators are created in public. With
public off the path (a SECURITY DEFINER helper with SET search_path = '',
or a hardened role path) the bare < in the body resolved to pg_catalog's
built-in record comparison instead, ordering the ciphertext bytes with
no error.

The generator now names every wrapper operator with its schema, taken
from a new Term::operator_schema (public for ORE, pg_catalog for the
HMAC, bloom and OPE terms), and the bloom guard qualifies cardinality
too. Inlining and index engagement are unchanged: the inlined
expression carries the same operator either way. The four hand-written
json_entry ordering wrappers get the same treatment.

Tests: a codegen test checks every generated wrapper body across the
catalog uses no bare operator and names the right schema per term, and
a new SQLx suite runs every ORE-carrying domain's wrappers and
OPERATOR(public.<op>) forms under search_path = '' against the ORE
comparator, on pairs where it disagrees with byte order (red on the
previous installer, green now).

Refs: CIP-5153

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Jmn7p5byuYMWqoxDUAZQE
@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 63da8b3

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

This PR includes changesets to release 12 packages
Name Type
@cipherstash/eql Patch
stash Patch
@cipherstash/stack-prisma Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/prisma-example Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/stack Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit 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 Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

EQL comparison wrappers now qualify operators by schema, including generated wrappers and SQL functions across scalar types. Integration tests exercise comparison behavior with an empty PostgreSQL search_path. The Changeset and PostgreSQL skill document the update and usage constraints.

Changes: Encrypted comparison operator qualification

Layer / File(s) Summary
Select and emit qualified operators
packages/eql/crates/eql-domains/src/term.rs, packages/eql/crates/eql-codegen/src/context.rs, packages/eql/crates/eql-codegen/src/generate.rs, packages/eql/crates/eql-codegen/templates/functions/wrapper.sql.j2
Term metadata maps ORE operators to public and other term operators to pg_catalog. Code generation passes terms into wrapper construction and emits qualified operators.
Qualify comparison wrappers
packages/eql/src/v3/json/operators.sql, packages/eql/src/v3/scalars/*/*_functions.sql
SQL comparison wrappers qualify operators. ORE comparisons use public; other comparison operators use pg_catalog. Existing operand casts and comparison directions are retained.
Verify and document search-path behavior
packages/eql/tests/sqlx/tests/v3_search_path_independence_tests.rs, skills/stash-postgres/SKILL.md, .changeset/eql-wrapper-operators-search-path.md
Integration tests cover comparison results, malformed ORE terms, and HMAC equality with an empty search path. The skill and Changeset describe operator usage and the remaining public schema requirement for bare operators.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: coderdan, tobyhede

Merge Risk: Minimal · up to 63da8

This change makes encrypted-column comparison functions return correct results when public is not on the search_path. No concrete merge-blocking issue was found. Some fixture-dependent tests are left to CI.

Pre-merge checks: Passed 5

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the main change: schema-qualifying operators in comparison wrapper bodies.
Docstring Coverage Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 17 functions across 4 files. (89 skipped: 89 unsupported.)

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI; Enforced overrides: Organization
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fdc48cf9-3bb3-4479-803e-8bbd9be4f575
Commits

Reviewing files that changed from the base of the PR and between abe3502 and 63da8b3.

Files selected for processing: 93

  • .changeset/eql-wrapper-operators-search-path.md
  • packages/eql/crates/eql-codegen/src/context.rs
  • packages/eql/crates/eql-codegen/src/generate.rs
  • packages/eql/crates/eql-codegen/templates/functions/wrapper.sql.j2
  • packages/eql/crates/eql-domains/src/term.rs
  • packages/eql/src/v3/json/operators.sql
  • packages/eql/src/v3/scalars/bigint/bigint_eq_functions.sql
  • packages/eql/src/v3/scalars/bigint/bigint_ord_functions.sql
  • packages/eql/src/v3/scalars/bigint/bigint_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/bigint/bigint_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/bigint/json_entry_bigint_functions.sql
  • packages/eql/src/v3/scalars/bigint/query_bigint_eq_functions.sql
  • packages/eql/src/v3/scalars/bigint/query_bigint_ord_functions.sql
  • packages/eql/src/v3/scalars/bigint/query_bigint_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/bigint/query_bigint_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/date/date_eq_functions.sql
  • packages/eql/src/v3/scalars/date/date_ord_functions.sql
  • packages/eql/src/v3/scalars/date/date_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/date/date_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/date/query_date_eq_functions.sql
  • packages/eql/src/v3/scalars/date/query_date_ord_functions.sql
  • packages/eql/src/v3/scalars/date/query_date_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/date/query_date_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/double/double_eq_functions.sql
  • packages/eql/src/v3/scalars/double/double_ord_functions.sql
  • packages/eql/src/v3/scalars/double/double_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/double/double_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/double/json_entry_double_functions.sql
  • packages/eql/src/v3/scalars/double/query_double_eq_functions.sql
  • packages/eql/src/v3/scalars/double/query_double_ord_functions.sql
  • packages/eql/src/v3/scalars/double/query_double_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/double/query_double_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/integer/integer_eq_functions.sql
  • packages/eql/src/v3/scalars/integer/integer_ord_functions.sql
  • packages/eql/src/v3/scalars/integer/integer_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/integer/integer_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/integer/json_entry_integer_functions.sql
  • packages/eql/src/v3/scalars/integer/query_integer_eq_functions.sql
  • packages/eql/src/v3/scalars/integer/query_integer_ord_functions.sql
  • packages/eql/src/v3/scalars/integer/query_integer_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/integer/query_integer_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/numeric/json_entry_numeric_functions.sql
  • packages/eql/src/v3/scalars/numeric/numeric_eq_functions.sql
  • packages/eql/src/v3/scalars/numeric/numeric_ord_functions.sql
  • packages/eql/src/v3/scalars/numeric/numeric_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/numeric/numeric_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/numeric/query_numeric_eq_functions.sql
  • packages/eql/src/v3/scalars/numeric/query_numeric_ord_functions.sql
  • packages/eql/src/v3/scalars/numeric/query_numeric_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/numeric/query_numeric_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/real/json_entry_real_functions.sql
  • packages/eql/src/v3/scalars/real/query_real_eq_functions.sql
  • packages/eql/src/v3/scalars/real/query_real_ord_functions.sql
  • packages/eql/src/v3/scalars/real/query_real_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/real/query_real_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/real/real_eq_functions.sql
  • packages/eql/src/v3/scalars/real/real_ord_functions.sql
  • packages/eql/src/v3/scalars/real/real_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/real/real_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/smallint/json_entry_smallint_functions.sql
  • packages/eql/src/v3/scalars/smallint/query_smallint_eq_functions.sql
  • packages/eql/src/v3/scalars/smallint/query_smallint_ord_functions.sql
  • packages/eql/src/v3/scalars/smallint/query_smallint_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/smallint/query_smallint_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/smallint/smallint_eq_functions.sql
  • packages/eql/src/v3/scalars/smallint/smallint_ord_functions.sql
  • packages/eql/src/v3/scalars/smallint/smallint_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/smallint/smallint_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/text/json_entry_text_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_eq_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_match_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_ord_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_search_functions.sql
  • packages/eql/src/v3/scalars/text/query_text_search_ore_functions.sql
  • packages/eql/src/v3/scalars/text/text_eq_functions.sql
  • packages/eql/src/v3/scalars/text/text_match_functions.sql
  • packages/eql/src/v3/scalars/text/text_ord_functions.sql
  • packages/eql/src/v3/scalars/text/text_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/text/text_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/text/text_search_functions.sql
  • packages/eql/src/v3/scalars/text/text_search_ore_functions.sql
  • packages/eql/src/v3/scalars/timestamp/query_timestamp_eq_functions.sql
  • packages/eql/src/v3/scalars/timestamp/query_timestamp_ord_functions.sql
  • packages/eql/src/v3/scalars/timestamp/query_timestamp_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/timestamp/query_timestamp_ord_ore_functions.sql
  • packages/eql/src/v3/scalars/timestamp/timestamp_eq_functions.sql
  • packages/eql/src/v3/scalars/timestamp/timestamp_ord_functions.sql
  • packages/eql/src/v3/scalars/timestamp/timestamp_ord_ope_functions.sql
  • packages/eql/src/v3/scalars/timestamp/timestamp_ord_ore_functions.sql
  • packages/eql/tests/sqlx/tests/v3_search_path_independence_tests.rs
  • skills/stash-postgres/SKILL.md

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

@cipherstash-agent cipherstash-agent self-assigned this Nov 6, 2026
@blacksmith-sh

This comment has been minimized.

test:public_identifiers forbids private issue identifiers in tracked
files; describe the behaviour directly instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Jmn7p5byuYMWqoxDUAZQE
@tobyhede
tobyhede marked this pull request as ready for review November 10, 2026 01:48
@tobyhede
tobyhede requested a review from a team as a code owner November 10, 2026 01:48

@cipherstash-agent cipherstash-agent left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review attestation

Reviewed 63da8b3 · posted from Claude Code · claude-sonnet-5-5 · effort medium
Sources: code-review (medium) · claude-sonnet-5-5
Skills: code-review, review-cross-check, review-post

No findings.

Scope read: eql-codegen (context.rs, generate.rs, wrapper template), eql-domains/src/term.rs, json/operators.sql, the new SQLx suite, changeset and skill edit. The regenerated v3/scalars/* SQL was spot-checked via the generator tests, not line by line. No existing review threads; the one failed Blacksmith check (private-identifier scan) was on an earlier run and ci-required later passed.


Generated by Claude Code

@cipherstash-agent cipherstash-agent removed their assignment Nov 11, 2026
@tobyhede
tobyhede merged commit ef1ef1b into main Nov 11, 2026
50 checks passed
@tobyhede
tobyhede deleted the claude/project-thread-rckj8w branch November 11, 2026 01:23
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.

3 participants