Skip to content

[4/6] docs(eql-mapper): cleanup after the v3 rewrite — stale strings and notes#428

Open
freshtonic wants to merge 12 commits into
queue/eql-v3/typecheckfrom
queue/eql-v3/transform
Open

[4/6] docs(eql-mapper): cleanup after the v3 rewrite — stale strings and notes#428
freshtonic wants to merge 12 commits into
queue/eql-v3/typecheckfrom
queue/eql-v3/transform

Conversation

@freshtonic

@freshtonic freshtonic commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📚 eql-v3 PR  ·  4 of 6

Part of a queue. The PRs merge in FIFO order — the numbered order below, #1 first. Merging one supersedes the PRs after it until the author runs git queue sync (rebases the rest onto the merged base) and git queue submit (retargets their PRs).

✅🟢 #422 queue/eql-v3/setup-skillsmain
✅🟢 #423 queue/eql-v3/upgrade-depsqueue/eql-v3/setup-skills
♻️🟢 #424 queue/eql-v3/typecheckqueue/eql-v3/upgrade-deps
✅🟢 #428 queue/eql-v3/transformqueue/eql-v3/typecheck  👈 this PR
✅🟢 #426 queue/eql-v3/showcasequeue/eql-v3/transform
⏳🟢 #427 queue/eql-v3/integrationqueue/eql-v3/showcase

✅ approved · ♻️ changes requested · ⏳ review pending  |  🟣 merged · 🟢 open · ⚫ closed  —  status as of the last git queue submit.
🥞 Managed by git-queue — do not edit this list by hand.

About this queue

Migrates CipherStash Proxy from EQL v2 to EQL v3 (cipherstash-client 0.34.1-alpha.4 → 0.42.0, EQL 2.3.0-pre.3 → 3.0.2), replacing the opaque eql_v2_encrypted composite type with 53 typed jsonb domains that encode both scalar type and searchable capability in the column type itself.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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 Plus

Run ID: 10cd24f2-4cb5-4fc1-90ba-1c02df22fc9c

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 queue/eql-v3/transform

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.

@freshtonic
freshtonic force-pushed the queue/eql-v3/transform branch from 717589f to c251fb8 Compare July 22, 2026 22:20
@freshtonic freshtonic changed the title [4/4] docs(eql-mapper): cleanup after the v3 rewrite — stale strings and notes [4/6] docs(eql-mapper): cleanup after the v3 rewrite — stale strings and notes Jul 22, 2026
@freshtonic
freshtonic requested a review from tobyhede July 22, 2026 23:40
@tobyhede

Copy link
Copy Markdown
Contributor

Review — v3 rewrite pipeline

Careful, well-documented implementation. ADR-0003 is genuinely load-bearing, the domain-capability logic is exhaustively unit-tested, and the mem::replace-against-Null NodeKey-preservation trick is applied consistently. Verified correct: post-order rule composition (operands are cast before the enclosing predicate wraps them), is_query_operand correctly excluding containment (@>/<@ operands cast to the public column domain, comparison/match operands to the eql_v3.query_* twin), and the eq_termord_term fallback / ord_term vs ord_term_ore selection.

Nice catch on the LIKE capability bypass — the old arm just did unify_node_with_type(expr_val, native), so LIKE on an Eq-only encrypted column was silently accepted. Also confirmed routing native LHS through the T: TokenMatch-bounded rule is safe (satisfy_bounds rule 1: Native satisfies all bounds), so WHERE native_col LIKE 'x' is not regressed.

🔴 Blocking-ish

1. The JsonAccessor selector assumption is unresolved and untestable as written. cast_literals_as_encrypted.rs carries its own NOTE: "the encrypted replacement is the selector string itself. If it is instead a jsonb payload, this needs the selector extracted rather than emitted verbatim." The ->/->> test can't settle it — the replacement is the stub '<encrypted-selector(medications)>', which is indistinguishable from a jsonb payload. If the encrypt pipeline returns a payload, eql_v3."->"(notes, <payload>) is wrong SQL. Please confirm against the real encrypt path before merge.

2. No execution anywhere. CONTEXT.md says it: "End-to-end validation against a live database with EQL v3 installed is still pending." Every test is assert_eq!(statement.to_string(), "…"). Each emitted name (eql_v3.ord_term, match_term, query_text_ord, the public.eql_v3_* domains, match_term(a) @> match_term(b)) is verified only against a reading of cipherstash-encrypt.sql. One wrong name = runtime failure this suite cannot see. Compounded by CI: gh pr checks shows only CodeRabbit ("reviews are disabled for this base branch") — no build or test job on the queue base.

🟡 Test gaps (all verified absent)

  • NOT LIKE / NOT ILIKE — zero occurrences. The Expr::UnaryOp{Not, Nested(..)} negation branch in rewrite_eql_match_ops.rs is entirely unexercised.
  • ILIKE — zero occurrences (only LIKE and @@ have rewrite-output tests).
  • UPDATE SET stored-value castupdate / update_with_returning_clause exist but only type-check and assert typed.projection; neither calls .transform(). So of ADR-0003's two stored-value contexts, only INSERT has cast-target coverage. This is the load-bearing new concept in the ADR — worth pinning.
  • native LIKE — no regression test locking in that WHERE native_col LIKE 'x' still type-checks (it does; I verified).
  • Transform-time capability errors — the "does not support operator" / "does not support fuzzy match" branches in both new rules are untested.

🟡 Question — double precision domain naming

suffix() computes prefix_len = "eql_v3_".len() + token.as_domain_str().len(), which relies on the token being a single underscore-free word (as from_domain_name's split('_').next() assumes). That holds for every token except possibly double: if the real domain is eql_v3_double_precision_ord, suffix() returns "precision_ord", every stores_* predicate returns false, and the column silently reports as non-orderable — no error, just a missing rewrite. If the domain is eql_v3_double_ord, no issue. Worth a test either way; the invariant is currently a comment, not an assertion.

🟠 Process

Please retitle. docs(eql-mapper): cleanup … stale strings and notes describes ~125 of 1004 added lines. The rest is ~618 lines of functional code — two new transformation rules (rewrite_eql_comparison_ops.rs +123, rewrite_eql_match_ops.rs +137), helpers.rs +93 (is_query_operand, v3_cast_target, eql_v3_term_call), domain-capability methods in types.rs, a new @@ operator across parser macro + operator table + inference, and the LIKE capability fix — plus ~261 lines of tests. feat/refactor would be accurate; as-is this undersells the change to reviewers and to any changelog tooling.

Relatedly, no CHANGELOG entry despite user-visible behavior changes (LIKE is now capability-checked and will reject columns that previously slipped through; @@ is newly supported). Confirm whether the queue handles that at merge.

…ation

Starts the v3 functional-index rewrite (ADR-0001) by pinning the pipeline
design and landing its hardest, most correctness-sensitive piece: choosing
the eql_v3 term-extraction function for an operator on a given column.

- ADR-0003 records the v3 rewrite pipeline: the stored-value vs query-operand
  context split (column domain vs query twin cast targets), operator ->
  term-function rewriting, JSON containment retained (not deleted), and the
  rule inventory. All operand/term details verified against the installed
  cipherstash-encrypt.sql, not the docs.

- DomainIdentity gains stores_hm/op/ob/bf and eq_term_fn/ord_term_fn/
  match_term_fn/query_twin. These encode the verified selection rules:
  * eq_term only where hm is stored; =/<> otherwise fall back to the ordering
    term (an ord-only scalar like integer_ord compares via ord_term, exactly
    as eql_v3.eq does).
  * ord_term (op) vs ord_term_ore (ob) chosen from the domain typname.
  * text is the hm exception (text_ord* stores hm alongside its order term).
  * query_twin names the term-only operand domain (eql_v3.query_<bare>).

Unit-tested against the SEM-term table. The rewrite rule that consumes these
lands next.

Refs CIP-3599.

Stable-Commit-Id: q-4fed4c4vg8sehf76wwrn5yaysf
…ex form

First working slice of the v3 rewrite pipeline (ADR-0003). Scalar
comparisons on encrypted columns now emit the functional-index form, and
all cast targets move from the opaque eql_v2_encrypted to real v3 domains.

- New RewriteEqlComparisonOps: `col <op> x` -> `eql_v3.<term>(col) <op>
  eql_v3.<term>(x)`, the term function chosen from the column's domain
  identity (eq_term / ord_term / ord_term_ore). A domain with no term for
  the operator is a capability error.
- Cast rules retargeted (helpers::cast_to_v3_domain): a query operand casts
  to the eql_v3.query_* twin, a stored value (INSERT/UPDATE) to the
  public.eql_v3_* column domain. Role is detected from the AST context
  (post-order traversal + full NodePath ancestry): a value under a
  comparison predicate is an operand, otherwise stored.
- CastLiteralsAsEncrypted gains node_types so it can name the target domain;
  EqlTerm gains eql_value(); helpers gain eql_v3_term_call / is_comparison_op.

Example:
  WHERE salary > $1
    -> WHERE eql_v3.ord_term(salary) > eql_v3.ord_term($1::JSONB::eql_v3.query_text_ord)

Transitional: JSON (->, @>, jsonb_*) and aggregate (min/max) rewriting still
emit the eql_v2.* function wrappers over v3-cast operands; those wrappers are
retargeted in the next slices. Tests updated to the current output; the
params fixture gains `EQL: Eq` since a bare (capability-less) v3 column is
storage-only and rightly rejects `=`.

eql-mapper 85 passing; workspace check, clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-62kqdp86gcbb66tvwhy69tgn5s
Continues the v3 rewrite (ADR-0003). min/max and the jsonb_* passthrough
functions on encrypted columns now rewrite to their eql_v3 counterparts.

- RewriteStandardSqlFnsOnEqlTypes resolves a pg_catalog function to its
  eql_v3 counterpart via the new get_eql_v3_function_name, and rewrites only
  when one is declared. This fixes the long-standing count bug: count has no
  eql_v3 counterpart (it runs natively on the encrypted value), so it is no
  longer rewritten to a non-existent eql_v3.count.
- sql_decls: the min/max/jsonb_path_*/jsonb_array_* target declarations move
  from eql_v2 to eql_v3. The containment declarations (jsonb_array,
  jsonb_contains, jsonb_contained_by) stay under eql_v2 until the containment
  slice.

  min(salary) -> eql_v3.min(salary)
  jsonb_path_query(col, '$.x') -> eql_v3.jsonb_path_query(col, '$.x'::...)
  count(col) -> count(col)   (unchanged)

eql-mapper 85 passing; workspace check, clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-092hd8jb1d31ck4va4n7r2gzet
Continues the v3 rewrite (ADR-0003). `@>`/`<@` on encrypted JSON columns
now rewrite to the eql_v3 containment functions. Containment is retained in
v3, scoped to JSON (ADR-0002 amendment) — `@>`/`<@` on scalar encrypted
columns still raise.

- RewriteContainmentOps emits eql_v3.jsonb_contains / jsonb_contained_by.
- sql_decls: the jsonb_array / jsonb_contains / jsonb_contained_by target
  declarations move from eql_v2 to eql_v3.

  notes @> needle -> eql_v3.jsonb_contains(notes, needle)
  needle <@ notes -> eql_v3.jsonb_contained_by(needle, notes)

No eql_v2.* function names remain in the mapper's rewrite output. eql-mapper
85 passing; clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-6dne3hbwwfsj7q42yqf4da8rqv
LIKE/ILIKE on an encrypted column previously only unified the result with
Native, so it bypassed the trait system entirely — a column with no match
capability silently accepted LIKE. They now apply the `~~`/`~~*` (and
negated) operator rules, so the encrypted LHS must implement TokenMatch and
the pattern takes its Tokenized associated type.

Adds tests: LIKE on a TokenMatch column type-checks; LIKE on an Eq-only
encrypted column is a capability error.

(The v3 rewrite of LIKE/@@ to `eql_v3.match_term(a) @> eql_v3.match_term(b)`
is a separate follow-up; this fixes the type-checking gap.)

eql-mapper 87 passing; clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-429ag1vxhrefpb1vy71sbkzg34
Continues the v3 rewrite (ADR-0003). LIKE/ILIKE on encrypted columns now
rewrite to the fuzzy-match functional form.

- New RewriteEqlMatchOps: `col LIKE pat` -> `eql_v3.match_term(col) @>
  eql_v3.match_term(pat)`; `NOT LIKE` wraps it in `NOT (...)`. Fuzzy match
  compares bloom-filter terms with @> (containment), so the operator becomes
  @> between the two match_term calls, mirroring eql_v3.matches. A column
  whose domain stores no bloom filter has no match_term -> capability error.
- Operand role detection (helpers::is_query_operand) now also treats a value
  under a LIKE/ILIKE predicate as a query operand, so the pattern casts to the
  query twin.

  email LIKE 'a%'
    -> eql_v3.match_term(email) @> eql_v3.match_term('<ct>'::JSONB::eql_v3.query_text_match)

`@@` (BinaryOperator::AtAt) is the remaining fuzzy-match surface; it needs the
operator declaration to parse first (separate slice).

eql-mapper 88 passing; clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-0gdjjy9gdt34jne9yhvs1kjw9m
Continues the v3 rewrite (ADR-0003). `->`/`->>` on encrypted JSON columns
are functionalised, since managed Postgres (Supabase) forbids the
CREATE OPERATOR DDL the native operators would need (ADR-0001).

- RewriteContainmentOps (now the general encrypted-JSON binary-op rule) also
  rewrites `col -> sel` -> `eql_v3."->"(col, sel)` and
  `col ->> sel` -> `eql_v3."->>"(col, sel)`. Operator-symbol function names are
  quoted; ordinary names are not.
- CastLiteralsAsEncrypted passes a JSON field selector (an EqlTerm::JsonAccessor,
  the RHS of ->/->>) as encrypted *text* rather than a jsonb-domain cast, to
  match eql_v3."->"(json_search, text).

  notes -> 'medications' -> eql_v3."->"(notes, '<encrypted-selector>')

ASSUMPTION TO CONFIRM (flagged in code): the encrypted selector replacement is
the selector string itself. If the encrypt pipeline instead produces a jsonb
payload for JSON selectors, the selector must be extracted rather than emitted
verbatim — a one-line change once confirmed.

eql-mapper 88 passing; clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-03j8aeq7we1c74jhkrgrny3rt4
Completes the fuzzy-match surface (ADR-0003). `@@` now parses in the
operator DSL and rewrites like LIKE/ILIKE.

- eql-mapper-macros: the binary-operator parser accepts `@@` (it previously
  only accepted `@>` after `@`), so `@@` can be declared.
- sql_decls: declare `<T>(T @@ <T as TokenMatch>::Tokenized) -> Native where
  T: TokenMatch` — `@@` requires the match capability.
- RewriteEqlMatchOps now also handles `BinaryOp::AtAt`:
    col @@ pat -> eql_v3.match_term(col) @> eql_v3.match_term(pat)
  and `is_query_operand` treats an `@@` operand as a query operand so the
  pattern casts to the query twin.

eql-mapper 89 passing; eql-mapper-macros 3 passing; clippy, fmt clean.

Refs CIP-3599.

Stable-Commit-Id: q-7s39kd5jdyhfjh1qyhkg2s858f
Validates the schema! EQL("<domain>") explicit-domain form end to end: a
block-ORE ordering column (eql_v3_integer_ord_ore) rewrites through
ord_term_ore (not ord_term) with the query_integer_ord_ore twin. This
exercises the ord-vs-ord_ore distinction that the inert domain identity
(ADR-0002) exists to carry.

  seq > 5  ->  eql_v3.ord_term_ore(seq) > eql_v3.ord_term_ore(...::eql_v3.query_integer_ord_ore)

eql-mapper 90 passing.

Refs CIP-3599, CIP-3600.

Stable-Commit-Id: q-0pjttkst7g5xwva2ag41v1nk6j
… double domain

Fills the rewrite-output test gaps called out in review of the v3 rewrite
pipeline (#428):

- ILIKE and NOT LIKE / NOT ILIKE — the negated match arm in
  RewriteEqlMatchOps had no rewrite-output coverage (only positive
  LIKE/@@ did).
- UPDATE SET stored-value cast — of ADR-0003's two stored-value
  contexts only INSERT had cast-target coverage; UPDATE now pins
  `SET salary = 'ENCRYPTED'::JSONB::public.eql_v3_text`.
- native LIKE still type-checks — regression lock for routing LIKE/ILIKE
  through the TokenMatch-bounded rule (Native satisfies all bounds).
- the `double` token does not swallow its capability suffix — pins the
  eql_v3_double_ord invariant that suffix()/stores_* only documents in a
  comment.

Stable-Commit-Id: q-47bk2x9aqbw7j8v04j81ecbec4
The queue changes user-visible behaviour (v3 typed jsonb domains replace
eql_v2_encrypted, cipherstash-client 0.42.0 / EQL 3.0.2, LIKE/ILIKE now
capability-checked, `@@` newly supported) but touched no CHANGELOG entry.
Add one [Unreleased] section covering the migration, per CLAUDE.md and the
review notes on #424/#428.

Stable-Commit-Id: q-0s2fx8fb1q2563t7b45x9crjmf
Cleanup pass (CIP-3601), now that the v3 rewrite is in place.

- eql-mapper-macros: the EqlTrait / operator parse-error messages listed only
  Eq, Ord, TokenMatch, JsonLike but Contain is a valid keyword (retained in v3
  for JSON containment). Both messages now include Contain.
- eql-mapper CONTEXT.md: the "migration in flight" note claimed the code still
  emits the v2 surface; it now emits v3 (no eql_v2.* names remain in output),
  with end-to-end validation still pending.

Note on the default()/all() reconciliation (the other CIP-3601 item): no
production change is needed. schema_delta's collect_ddl never fabricates EQL
traits (new/added columns are Native), so its EqlTraits::default() only appears
in test assertions, consistent with the schema! macro's bare `EQL` (a
capability-less, storage-only v3 column). The former mismatch was the loader's
hardcoded all(), which CIP-3598 replaced with domain-derived capability.

eql-mapper 90 passing; eql-mapper-macros 3 passing; fmt clean.

Refs CIP-3601.

Stable-Commit-Id: q-56sd3v5nxbdh5b4kpepyc1mm31
@freshtonic
freshtonic force-pushed the queue/eql-v3/typecheck branch from 3cad2c5 to 1a81cf2 Compare July 23, 2026 06:08
@freshtonic
freshtonic force-pushed the queue/eql-v3/transform branch from c251fb8 to 081333f Compare July 23, 2026 06:08
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