Skip to content

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

Draft
cipherstash-agent wants to merge 2 commits into
mainfrom
claude/project-thread-rckj8w
Draft

cipherstash-agent wants to merge 2 commits into
mainfrom
claude/project-thread-rckj8w

Conversation

@cipherstash-agent

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

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

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

@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

This branch has not been deployed

No deployments
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