Skip to content

feat(eql-bindings): generate EQL v4 bindings and derive Stack Encrypt traits on v4 only - #1162

Open
coderdan wants to merge 3 commits into
feat/eql-v4-bundlefrom
feat/eql-v4-bindings
Open

coderdan wants to merge 3 commits into
feat/eql-v4-bundlefrom
feat/eql-v4-bindings

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

Summary

EQL (Encrypt Query Language) is the SQL we install into PostgreSQL so it can search encrypted values. Under ADR-0002 (#1139) the same SQL is installed twice: eql_v3 for values written by cipherstash-client (the TypeScript SDK's engine) and eql_v4 for values written by Stack Encrypt (the engine the Go SDK runs). Stack Encrypt's output is not byte-for-byte compatible with cipherstash-client's, so a v3 type that claims to be producible by Stack Encrypt is wrong. This PR generates Rust payload types for v4 and moves all Stack Encrypt support onto them. The SQL and the TypeScript package do not change.

Changes

  • eql-codegen: the bindings generator takes the EQL version, as the SQL renderer already does. It writes the scalar domains again into eql_bindings::v4 (eql_v4_* names, a SchemaVersion pinned to 4, a v4 schema id, an inventory). encryption_gap refuses every v3 domain, so only v4 structs derive EncryptFrom/DecryptInto. A test asserts no v3 struct derives either.
  • Target table: the Stack Encrypt target table moves from v3/targets.rs to v4/targets.rs and names eql_v4 domains. Go's encrypt/eql/eql_gen.go is regenerated from it.
  • Tests and fixtures: the encryption test crate's PostgreSQL task installs the v4 bundle. The Go tests and stack-encrypt's test fixtures name the v4 domains. The cross-language TextEq fixture now carries "v":4, with the same equality term.
  • Docs: the eql-bindings README and packages/eql/AGENTS.md describe the v4 module. The pending @cipherstash/eql changesets are updated, and one is added for the v4 module.

Verification

  • cargo test for eql-codegen, eql-bindings and the encryption test crate passes, plus types:check, typescript:check, codegen:parity and test:crates.
  • The encryption crate's PostgreSQL test passes against a fresh eql_v4 install: store, =/<>, index use, decrypt.
  • All Go packages pass (mise run go:test, go:lint) with rebuilt guests. stack-encrypt's 541 tests pass. protect-ffi compiles.
  • Published crates.io eql-bindings (3.0.3–3.0.6) has no stack-encrypt feature, so moving the derives breaks nothing released.

Related

Refs #1142, #1145. Stacked on #1161.

https://claude.ai/code/session_01CA4pFfaN4DPNSTLnJVNxsu

@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a629897

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 Minor
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 Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Only developers with an assigned seat can use this organization's usage-based review budget, and seats here are assigned manually. Ask an admin to assign a seat, or change the review continuation mode in Billing.

Next included review available in 12 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 87db7964-8913-408c-8efa-34e149001a70

📥 Commits

Reviewing files that changed from the base of the PR and between 76dfac9 and a629897.


📒 Files selected for processing (58)
  • .changeset/eql-plan-field-targets.md
  • .changeset/eql-text-equality-transcoding.md
  • .changeset/eql-v4-bindings.md
  • languages/golang/cmd/stashgen/README.md
  • languages/golang/encrypt/README.md
  • languages/golang/encrypt/eql/eql_gen.go
  • languages/golang/encrypt/eql/eql_test.go
  • languages/golang/encrypt/eql_test.go
  • languages/golang/encrypt/guest/src/ops.rs
  • languages/golang/encrypt/records.go
  • languages/golang/encrypt/targets_test.go
  • packages/eql/AGENTS.md
  • packages/eql/crates/eql-bindings/README.md
  • packages/eql/crates/eql-bindings/src/encryption.rs
  • packages/eql/crates/eql-bindings/src/encryption/example.rs
  • packages/eql/crates/eql-bindings/src/encryption/targets.rs
  • packages/eql/crates/eql-bindings/src/lib.rs
  • packages/eql/crates/eql-bindings/src/v3/bigint.rs
  • packages/eql/crates/eql-bindings/src/v3/boolean.rs
  • packages/eql/crates/eql-bindings/src/v3/date.rs
  • packages/eql/crates/eql-bindings/src/v3/domain_type.rs
  • packages/eql/crates/eql-bindings/src/v3/double.rs
  • packages/eql/crates/eql-bindings/src/v3/integer.rs
  • packages/eql/crates/eql-bindings/src/v3/json_storage.rs
  • packages/eql/crates/eql-bindings/src/v3/mod.rs
  • packages/eql/crates/eql-bindings/src/v3/numeric.rs
  • packages/eql/crates/eql-bindings/src/v3/real.rs
  • packages/eql/crates/eql-bindings/src/v3/smallint.rs
  • packages/eql/crates/eql-bindings/src/v3/text.rs
  • packages/eql/crates/eql-bindings/src/v3/timestamp.rs
  • packages/eql/crates/eql-bindings/src/v4/bigint.rs
  • packages/eql/crates/eql-bindings/src/v4/boolean.rs
  • packages/eql/crates/eql-bindings/src/v4/date.rs
  • packages/eql/crates/eql-bindings/src/v4/double.rs
  • packages/eql/crates/eql-bindings/src/v4/integer.rs
  • packages/eql/crates/eql-bindings/src/v4/inventory.rs
  • packages/eql/crates/eql-bindings/src/v4/json.rs
  • packages/eql/crates/eql-bindings/src/v4/mod.rs
  • packages/eql/crates/eql-bindings/src/v4/numeric.rs
  • packages/eql/crates/eql-bindings/src/v4/real.rs
  • packages/eql/crates/eql-bindings/src/v4/smallint.rs
  • packages/eql/crates/eql-bindings/src/v4/targets.rs
  • packages/eql/crates/eql-bindings/src/v4/text.rs
  • packages/eql/crates/eql-bindings/src/v4/timestamp.rs
  • packages/eql/crates/eql-bindings/tests/v4_parity.rs
  • packages/eql/crates/eql-codegen/src/bindings.rs
  • packages/eql/crates/eql-codegen/src/go_eql.rs
  • packages/eql/crates/eql-codegen/src/targets.rs
  • packages/eql/crates/eql-codegen/tests/bindings_parity.rs
  • packages/eql/crates/eql-codegen/tests/cli.rs
  • packages/eql/mise.toml
  • packages/eql/tasks/githooks/pre-commit
  • packages/eql/tests/encryption/fixtures/text_eq_query.json
  • packages/eql/tests/encryption/tests/targets.rs
  • packages/eql/tests/encryption/tests/text_eq.rs
  • packages/stack-encrypt/fuzz/fuzz_targets/check_record.rs
  • packages/stack-encrypt/src/dynamic/record.rs
  • packages/stack-encrypt/src/dynamic/target.rs

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

No mutants were generated for the changed lines.

@coderdan coderdan 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 of this PR's own changes (feat/eql-v4-bundle...feat/eql-v4-bindings). The codegen and the trait move hold up: the generated v4 domain names match what the {{prefix}} SQL renders, and moving the derives off v3 breaks no released crate. crates.io eql-bindings 0.1.0 through 3.0.6 publishes no features, and protect-ffi does not enable stack-encrypt. One release-coherence problem, two gaps in the gates and tests, and two small doc issues.

What I ran on the head commit (c0f074f): cargo test for eql-codegen, for eql-bindings --all-features and for eql-encryption-tests (all pass); clippy -D warnings and cargo fmt --check on those crates (clean); cargo publish -p eql-bindings --dry-run --all-features (builds against registry stack-encrypt 0.2.0); and cargo check --locked in protect-ffi's workspace (passes).


1. Major: the next EQL release would publish a v4 producer with no v4 SQL to install it into

Where: .changeset/eql-v4-bindings.md:2, packages/eql/crates/eql-bindings/src/sql.rs:7, .github/workflows/_build-eql-sql.yml:95-96, packages/eql/crates/eql-bindings/src/v4/mod.rs:15

Problem: This PR turns the stack-encrypt feature into a producer of "v": 4 payloads, and they only pass the public.eql_v4_* domain checks. The two pending changesets already release that feature, and the new changeset makes it a minor bump. Through the lockstep version sync, the next release publishes eql-bindings 3.1.0 with this behaviour. No released artefact carries the v4 SQL:

  • eql_bindings::sql::INSTALL_SQL / UNINSTALL_SQL embed only sql/cipherstash-encrypt*.sql, which is v3.
  • _build-eql-sql.yml uploads only cipherstash-encrypt.sql and cipherstash-encrypt-uninstall.sql.
  • @cipherstash/eql/sql and stash eql install install only v3.

None of #1161 or #1163–#1166 touches those paths. So a crates.io user who enables stack-encrypt gets payloads that every released install rejects at INSERT, while the published README tells them to use public.eql_v4_text_eq.

Why it matters: The release would also contradict its own plan. ADR-0002 (docs/adr/0002-…md:226) says "@cipherstash/eql 4.x ships both bundles", and v4/mod.rs:15 says the rest follows in 4.0. This change would ship the v4 types in a 3.x minor instead.

Suggested fix: Pick one and write it down in the PR:

  • (a) Ship the v4 bundle in the same release: a sql::INSTALL_SQL_V4 / UNINSTALL_SQL_V4, the v4 files in the crate's sql/ and the release manifest, and the release upload.
  • (b) Keep these changesets from releasing until the bundle ships, for example by holding the stack or marking the stack-encrypt surface unstable.

If (b), say so in the changesets so the Version Packages PR does not pick them up by accident.

2. Minor: types:check, types:generate and the pre-commit hook do not cover src/v4

Where: packages/eql/mise.toml:359, :396, :401; packages/eql/tasks/githooks/pre-commit:18

Problem: eql-codegen bindings now writes crates/eql-bindings/src/v4/*, but these three places still name only src/v3:

  • types:check runs git diff --exit-code and git ls-files --others on src/v3 only. It regenerates v4 in place and then passes even when the committed v4 files were stale.
  • types:generate snapshots and restores only src/v3, so if the run fails, v4 is left rewritten. The task's "atomic on failure" comment no longer holds.
  • The pre-commit hook regex has no crates/eql-bindings/src/v4/, so a commit that edits only a generated v4 file runs no gate.

CI still catches v4 drift, through bindings_parity.rs under test:crates (cargo test -p eql-codegen). But types:check is the gate AGENTS.md documents, and it now reports fresh on a stale tree. #1163–#1166 do not change this.

Suggested fix: Add crates/eql-bindings/src/v4 to the diff and untracked lists, to the snapshot/restore trap, and to the hook regex.

3. Minor: no test checks the version boundary that the change relies on

Where: packages/eql/crates/eql-bindings/src/v4/mod.rs:84-99, packages/eql/tests/encryption/tests/targets.rs:271

Problem: The new changeset and the module docs say v4's SchemaVersion "rejects any other version, so a v3 payload fails at the type boundary instead of at INSERT". Nothing tests that:

  • No test deserializes a well-formed "v": 3 TextEq, with c and a 64-hex hm, into v4::text::TextEq and expects a refusal. None tests the reverse (v: 4 into v3) either.
  • The one v: 3 input in the stack-encrypt path, refuses_bytes_that_are_not_the_type, is {"v":3,…,"hm":"00"}. It is refused because c is missing, not because of the version, so it would still pass if v4's SchemaVersion accepted 3. This matters most for decrypt, because earlier builds of this tree wrote Stack Encrypt payloads with v: 3 and the same stack-encrypt:1: prefix.
  • v4 also has no counterpart to v3's catalog_parity.rs, which would pin each v4 inventory entry's sql_domain, its query twin's schema (eql_v4.), schema_id under /eql/v4/ and its term keys.

Suggested fix:

  • Add a well-formed v: 3 stored payload to refuses_bytes_that_are_not_the_type.
  • Add a unit test in eql-bindings that v4::text::TextEq refuses v: 3 and v3::text::TextEq refuses v: 4.
  • Add a small v4 parity test that walks v4::all() and v4::all_query().

4. Nit: broken intra-doc link without the feature

Where: packages/eql/crates/eql-bindings/src/v4/mod.rs:12

Problem: [targets] points to a module that exists only under #[cfg(feature = "stack-encrypt")]. Running cargo doc -p eql-bindings with default features gives a new warning: "unresolved link to targets". docs.rs builds with the feature, so the published docs are fine. A future -D warnings doc gate would fail on it.

Suggested fix: Write it as plain code text, or as [targets](crate::encryption::targets) behind the feature.

5. Nit: the Go READMEs describe a setup that now fails on insert

Where: languages/golang/encrypt/README.md:276, :281; languages/golang/cmd/stashgen/README.md:47

Problem: These still tell users to store eql.TextEq in a public.eql_v3_text_eq column and query through eql_v3.query_text_eq. After this PR the guest writes "v": 4, which the v3 domain check rejects. #1165 fixes both files, so this only matters if the stack does not merge together. If it might not, move those README edits into this PR.

@coderdan
coderdan force-pushed the feat/eql-v4-bindings branch from c0f074f to 24b9228 Compare October 12, 2026 11:58
@coderdan

Copy link
Copy Markdown
Contributor Author

Rebased onto the updated #1161, then pushed 24b9228, which answers items 2–5 of the review above. Item 1 is still open and needs your decision (see the end).

  1. types:check, types:generate and pre-commit. src/v4 is in the diff and untracked lists, in the snapshot/restore trap, and in the hook regex. mise run types:check passes. Separately: the hook's regex is written relative to the subtree (^crates/…) while git diff --cached prints paths relative to the repo (packages/eql/crates/…), so I don't think the hook matches anything since the import. That predates this PR and is not changed here.

  2. The version boundary. There's a new tests/v4_parity.rs, the v4 counterpart of catalog_parity.rs. It checks four things:

    • v4::all() is the scalar catalog, as eql_v4_*, in catalog order;
    • every query twin is in eql_v4;
    • the term keys match the catalog;
    • each generation's TextEq refuses the other's version, both directly and through the inventory.

    It found a real bug: the inventory's PhantomData<T> handles don't forward schema_id, so every v4 entry reported a /eql/v3/ $id. That is fixed with a type-level schema_id_base_static(), which v4 overrides. The encryption suite also gained refuses_a_stored_payload_of_the_other_version: a real v4 payload with only v set back to 3 is refused as Stored, and the error names version 3.

  3. Intra-doc link. It is plain code text now. RUSTDOCFLAGS=-D warnings cargo doc -p eql-bindings with default features is clean.

  4. Go READMEs. These now name public.eql_v4_text_eq and eql_v4.query_text_eq. test(golang): prove the EQL v4 types from Go, against Rust and PostgreSQL #1165's fuller rewrite of the same lines was rebased over them.

Item 1, the release with no v4 SQL to install into, is a decision I've left for you.

  • Option (a), shipping the bundle, means the stamp, manifests, INSTALL_SQL_V4, the release upload, and the eql_v4_ detectors ADR-0002 now lists. That is a PR of its own.
  • Option (b) means holding these changesets. Changesets has no per-changeset hold, so in practice it means not merging the stack past feat(eql): build the eql_v4 bundle beside eql_v3 #1161 until (a) exists.

I'd lean (b) for now. The Go alpha gets its v4 SQL from the Go release (#1166/#1167), not from npm or crates.io.

Also on this restack: #1163 gained a commit that fixes two eql-bindings unit tests that broke there under --all-features when Text became producible.

@coderdan
coderdan marked this pull request as ready for review October 12, 2026 13:20
@coderdan
coderdan requested a review from a team as a code owner October 12, 2026 13:20
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T13:28:21.455023Z 24b9228 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-12T13:25:48.668163Z 24b9228 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tobyhede tobyhede left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review attestation: files reviewed - the 3 PR commits (487b5b6, ef1878b, 24b9228): eql-codegen bindings/targets, eql-bindings v3/v4 modules, encryption/targets.rs, encryption tests, mise.toml, Go encrypt/eql, stack-encrypt fixtures, changesets, AGENTS.md. Models: Claude Sonnet 5.5 (claude-sonnet-5-5), single pass, no verifier.


Generated by Claude Code

'@cipherstash/eql': minor
---

**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`, and has its own `SchemaVersion`, which rejects any other version. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table names v4 domains. The `v3` types and the TypeScript package are unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changeset says "The v3 types ... are unchanged", but this PR removes public API from v3: eql_bindings::v3::targets (the whole module moved to v4) and the EncryptFrom/DecryptInto impls on v3::text::TextEq and TextEqQuery. Any consumer using the stack-encrypt feature with v3 types stops compiling. If no release carried the feature, say so here and say these were removed. If one did, this needs a breaking bump rather than minor.


Generated by Claude Code

/// ciphertext prefix. A real, well-formed v4 payload with only its version
/// changed is refused, so the version alone is what refuses it.
#[tokio::test]
async fn refuses_a_stored_payload_of_the_other_version() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test pins the new behaviour: a stored Stack Encrypt payload with "v": 3 is refused on decrypt. Its doc comment says earlier builds wrote exactly that. Any data written by the pre-change Go guest or v3::TextEq path becomes undecryptable, and nothing offers a re-version or migration path. If that data is known not to exist outside tests, record it in the changeset. If it might exist, decrypt needs a read-only compatibility path for v: 3 payloads with the stack-encrypt:1: prefix.


Generated by Claude Code

/// boundary instead of at INSERT. The inner value is private; the only
/// constructible instance is `4`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, TS)]
pub struct SchemaVersion(#[ts(type = "4")] u16);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SchemaVersion is a near copy-paste of the v3 type in lib.rs: the struct, Deserialize, manual JsonSchema and Default differ only in the constant 3 vs 4 and the schema name. The two will drift, for example if the error message or schema description is edited in one. A single SchemaVersion<const V: u16>, or a macro, would remove the duplication.


Generated by Claude Code

/// A v4 struct's `DomainType::schema_id` overrides: the trait's defaults name
/// the v3 schema base. Both forms, since the inventory's `PhantomData` handles
/// reach the type-level one.
fn schema_id_override(version: EqlVersion) -> TokenStream {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The v4 override emits two near-identical fns (schema_id_base_static and schema_id) into every generated impl, which is about 150 impls. The cause is that DomainType::schema_id defaults to the v3 base. Adding an object-safe schema_id_base(&self) to the trait, defaulting to v3, and having the default schema_id use it would mean v4 types override one method. It would also avoid the "boxed PhantomData fell back to v3" bug this commit had to fix with a second override.


Generated by Claude Code

// compiled type behind it.
for row in targets::targets()
.iter()
.filter(|r| r.family != "json" || r.suffix.is_empty())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The filter r.family != "json" || r.suffix.is_empty() skips every json-family row that has a suffix, not only the SteVec document. A future json row such as a generated v4 JsonSearch would be silently exempt from the "is a compiled domain type" check. Filter on the SteVec shape or the exact row name instead. Also, TARGETS now advertises public.eql_v4_json* SteVec rows (and eql_v4.query_json) that have no Rust type in v4, and the Go eql_gen.go names them. Check that the Go side cannot select a target the Rust side cannot back.


Generated by Claude Code

Comment thread packages/eql/AGENTS.md

Adding a scalar encrypted-domain type is one row in the Rust catalog `eql-domains::CATALOG` (`crates/eql-domains/src/lib.rs`): a `DomainFamily` giving the type `name` (e.g. `bigint`), its `ScalarKind` (the `kind` field), the `Domain`s mapping each generated (bare) domain name to its fixed index `Term`s (`eq => [Hm]`, `ord`/`ord_ore => [Ore]`), and the `Fixture` value list. Term capabilities are fixed in the `Term` enum's `impl` methods (with unit tests): `Hm` provides equality, and `Ore` provides equality plus ordering. There is no TOML manifest and no Python — the catalog is the source of truth, validated by the compiler (an undefined term or unknown scalar is a compile error) plus catalog `#[test]`s. `mise run build` runs `cargo run -p eql-codegen`, which regenerates the scalar SQL surface into `src/v3/scalars/<T>/` from `CATALOG` at the start of every build; that surface includes supported comparison wrappers plus blockers for native `json` operators that would otherwise be reachable through domain fallback. `cargo run -p eql-codegen` regenerates every type at once (the same call `mise run build` uses; there is no per-type codegen task). The generated `*_types.sql` / `*_functions.sql` / `*_operators.sql` / `*_aggregates.sql` files are **committed in place** under `src/v3/scalars/<T>/` and drift-gated by `mise run codegen:parity` (regenerate in place + `git diff` + untracked check — the same regenerate-and-diff pattern `types:check` uses for the committed bindings). They are still machine-generated: change the catalog and rebuild, never hand-edit (CI fails on drift). The per-type plaintext fixture lists the SQLx matrix consumes are **not** a generated file — they are materialised from each `CATALOG` row at compile time as `eql_domains::INT4_VALUES` / `INT2_VALUES` (the `int_values!` macro) and read directly by `ScalarType::FIXTURE_VALUES`; a Rust source of truth no longer round-trips through a committed generated `.rs`. Generated SQL carries a `-- AUTOMATICALLY GENERATED FILE` header (the project-wide marker `docs:validate` greps on); change the catalog and rebuild, never hand-edit. Hand-written SQL beyond the fixed surface goes in `src/v3/scalars/<T>/<T>_extensions.sql` with no auto-generated header and explicit `-- REQUIRE:` edges — that file IS committed. `json`'s **SteVec** domains never enter this pipeline, but its **scalar storage domain does**: `generate_all` iterates `eql_domains::families_with_scalar_domains()` and renders each family's scalar domains (so `public.eql_v3_json` is generated into `src/v3/scalars/json/`), while `list-types` and the SQLx matrix still iterate `eql_domains::scalar_families()`, which excludes the whole non-scalar `json` family — so `json` stays out of the operator/coverage matrix by construction (a storage-only domain carries no operator matrix), even though its ciphertext-only storage domain now has a generated SQL surface.

**EQL v4 bindings:** the generator writes the scalar domains a second time into `crates/eql-bindings/src/v4/` (`eql_v4_*` names, `v: 4`, its own `SchemaVersion`, an `inventory.rs`), plus the Stack Encrypt target table `v4/targets.rs`. **Only v4 structs derive Stack Encrypt's `EncryptFrom`/`DecryptInto`**: Stack Encrypt's output is not byte-for-byte compatible with cipherstash-client's, so `encryption_gap` refuses every v3 domain and a codegen test asserts no v3 struct derives either. v4 has no SteVec bindings, payload enums, TypeScript export or published JSON Schema yet. The hand-written `v4/mod.rs` re-exports v3's term newtypes and `DomainType`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new paragraph adds v4, but the unchanged text below it still describes types:check as diffing "all three (crates/eql-bindings/src/v3 + bindings/ + schema/)". types:check and types:generate now also cover src/v4, and the pre-commit hook triggers on it. The stale sentence is exactly the drift the repo's meta-file rule warns about, so update it in this PR.


Generated by Claude Code

pub mod text;
pub mod timestamp;

pub use crate::SchemaVersion;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pub use crate::SchemaVersion is now v3::SchemaVersion while v4 defines a different type with the same name, and eql_bindings::SchemaVersion at the crate root is still the v3 one. A consumer who writes use eql_bindings::SchemaVersion with a v4 struct gets a type mismatch that looks like the wrong crate version. Consider renaming the root one SchemaVersionV3 (keeping an alias) or documenting at the root that it is v3-only.


Generated by Claude Code

@cipherstash-agent cipherstash-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review attestation

Reviewed head 24b9228 with Claude Code /code-review medium, then cross-checked each finding against the diff.

🔴 Blocking

  • The changeset misstates the v3 impact. .changeset/eql-v4-bindings.md says the v3 types are "unchanged", but v3 loses its EncryptFrom / DecryptInto derives and the public eql_bindings::v3::targets module. That is a breaking change shipped as minor. The existing changesets were also edited in place, so the v3 → v4 change is in no CHANGELOG (inline comment).

🟠 Should fix

  • No migration guidance for stored "v": 3 Stack Encrypt payloads, which are now rejected (inline comment on targets.rs).
  • The Go README gives no v4 install step, and no skills/ file was touched. Check stash-cli and stash-indexing (inline comment).

🟡 Nits

  • Stale comment in eql-codegen/src/targets.rs (~L148). It still names eql_v3.query_json. The json SteVec rows name SteVecQuery / eql_v4.query_json, which have no v4 compiled type, and the parity test special-cases this.
  • Shared term types. v4 shares v3's term types (inline comment on v4/mod.rs).

Generated by Claude Code

'@cipherstash/eql': minor
---

**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`, and has its own `SchemaVersion`, which rejects any other version. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table names v4 domains. The `v3` types and the TypeScript package are unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Blocking: the changeset says "The v3 types … are unchanged", but this PR removes public v3 API.

Verified in the diff:

  • v3::text::* lose the EncryptFrom / DecryptInto derives.
  • The public eql_bindings::v3::targets module is removed. It is now v4::targets.

A downstream crate using keyset.encrypt_as::<v3::...> or v3::targets::* with the stack-encrypt feature stops compiling on a minor bump. Either keep the v3 derives and module for one release, or ship this as a documented breaking change. In both cases the changeset text must say what was removed.

Also, the PR edits eql-plan-field-targets.md and eql-text-equality-transcoding.md in place. If those already shipped, the v3 → v4 change for existing Stack Encrypt and Go users appears in no CHANGELOG. This changeset should carry it.


Generated by Claude Code

}
}

/// Earlier builds wrote Stack Encrypt payloads with `"v": 3` and the same

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 No migration path for stored "v": 3 Stack Encrypt payloads. This comment records that earlier builds wrote "v": 3 payloads. After this change SchemaVersion rejects them, so targets::decrypt and Go DecryptContact error on every row written by a prior release. Columns typed eql_v3_* also won't accept the new v:4 values. No re-encrypt or backfill guidance appears in the changeset or READMEs. If no such data exists in the wild, say so in the changeset.


Generated by Claude Code

A field tagged `encrypt_into=TextEq` is stored as one EQL value, the JSON
PostgreSQL holds in a `public.eql_v3_text_eq` column, as an `eql.TextEq`
from package `encrypt/eql`. The guest builds the value: the generated code
PostgreSQL holds in a `public.eql_v4_text_eq` column, as an `eql.TextEq`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 The README now says Stack Encrypt writes public.eql_v4_*, but gives no install step. A Go user with only stash eql install (v3) gets type "public.eql_v4_text_eq" does not exist. Name the v4 bundle and how to install it.

Per AGENTS.md, skills/stash-cli and skills/stash-indexing ship to customers. This PR touches no skills/ file. Check whether they need eql_v4 guidance (or confirm the v4 bundle PR below covers it).


Generated by Claude Code

pub mod text;
pub mod timestamp;

pub use crate::v3::domain_type::{DomainType, SQL_SCHEMA};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Nit (non-blocking): v4 re-exports v3's terms and SQL_SCHEMA, so v4 Ciphertext / Hmac256 are the same types as v3's. The version boundary lives only in the envelope's SchemaVersion. A later v3-only change to terms.rs validation silently changes v4 behaviour. The module doc says the two outputs aren't byte-compatible, yet they share term types. Consider a note, or v4-owned terms.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24b92287d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 148 to +150
encoded as hex. Serde handles the final EQL JSON envelope only. SQL consumes
these payloads through the existing `public.eql_v3_text_eq` and
`eql_v3.query_text_eq` domains and their equality extractor index. Writers and
these payloads through the `public.eql_v4_text_eq` and
`eql_v4.query_text_eq` domains and their equality extractor index. Writers and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Ship the v4 SQL bundle with the v4 bindings

Released consumers cannot install the domains required by this new API: tasks/release/prepare-bindings-assets.sh:36-58 still copies only the v3 cipherstash-encrypt.sql into the crate/npm packages, and .github/workflows/_build-eql-sql.yml:94-96 uploads only those v3 artifacts. Consequently, code using v4::text::TextEq produces payloads for public.eql_v4_text_eq, but neither the published crate's eql_bindings::sql nor the GitHub release provides the installer that creates that domain; include the v4 installer/uninstaller in the release assets before exposing these bindings.

AGENTS.md reference: packages/eql/AGENTS.md:L254-L254

Useful? React with 👍 / 👎.

… traits on v4 only

Stack Encrypt's output is not byte-for-byte compatible with
cipherstash-client's, which writes the eql_v3 domains, so an eql_v3
struct deriving EncryptFrom or DecryptInto claims a compatibility that
does not exist. ADR-0002 gives Stack Encrypt its own name, eql_v4.

eql-codegen now writes the scalar domains a second time into
eql_bindings::v4: eql_v4_* names, a SchemaVersion pinned to 4, a schema
id under eql/v4, and an inventory. encryption_gap refuses every v3 domain,
so TextEq and TextEqQuery derive their traits only in v4, and a codegen
test asserts no v3 struct derives either. The Stack Encrypt target table
moves to v4/targets.rs and names eql_v4 domains; the Go package
encrypt/eql is regenerated from it. v4 has no SteVec bindings, payload
enums, TypeScript export or JSON Schema yet.

No published eql-bindings carried the stack-encrypt feature, so nothing
released loses a trait. The encryption test crate's PostgreSQL test now
installs the v4 bundle; the Go guest and the stack-encrypt fixtures name
the v4 domains.

Refs #1142
The guest's eql-feature round-trip test still asserted "v": 3 after
Stack Encrypt moved to writing EQL v4, failing the WASI/Go CI job.
Also update two comments that named the v3 envelope and the
eql_v3.query_<name> operand.
…he version boundary

The inventory boxes PhantomData<T> handles, and their DomainType impl did
not forward schema_id, so every v4 entry reported an /eql/v3/ $id. A
type-level schema_id_base_static() now carries each type's base through
the handle, and the generated v4 types override it.

That surfaced in a new v4 parity test, the counterpart of
catalog_parity.rs: v4::all() is the scalar catalog under eql_v4 names in
catalog order, each query twin lives in eql_v4, term keys match the
catalog, and each generation's TextEq refuses the other's version, both
directly and through the inventory. The encryption suite refuses a real
v4 payload whose version alone was set back to 3.

types:generate snapshots and restores src/v4 as well as src/v3,
types:check diffs it, and the pre-commit hook triggers on it. The v4
module doc no longer links a feature-gated module, and the Go READMEs
name the eql_v4 column and query operand the guest now writes.
@coderdan
coderdan force-pushed the feat/eql-v4-bindings branch from 24b9228 to a629897 Compare October 12, 2026 13:36
@coderdan
coderdan removed this pull request from stack #1168 October 12, 2026 13:40
@coderdan
coderdan added this pull request to stack #1174 October 12, 2026 13:40

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.

3 participants