Eql v3#392
Conversation
Reconcile the eql-bindings release-plz setup with the canonical cipherstash-suite pattern (the org reference; see its RELEASING.md and commit 02a989405): - Run `release` before `release-pr` (needs: release). The previous parallel jobs would reproduce the recursive-release-PR bug the suite documents: release-pr running first sees the just-merged `chore: release` commit as unreleased and opens another PR. - Use release-plz's native crates.io Trusted Publishing (id-token: write) instead of rust-lang/crates-io-auth-action. No CARGO_REGISTRY_TOKEN; GitHub ops use the default GITHUB_TOKEN. - Sign release commits and tags with GPG via crazy-max/ghaction-import-gpg (GPG_PRIVATE_KEY secret), matching the suite. - Add cliff.toml (git-cliff changelog template, conventional-commit parsers, `review` skip) and point release-plz.toml at it; align the workspace block (git_tag_enable, git_release_enable, publish, semver_check = false). - Match the suite's generated crate CHANGELOG.md header. - Static concurrency group + workflow-level permissions. EQL-local conventions kept deliberately (consistent within EQL, not the suite): blacksmith runner and mise toolchain, and the eql-bindings-v* tag exclusions on the SQL-surface workflows. CIP-3224
Rename eql_v3 `timestamptz` scalar type to `timestamp`
…indings-crate ci(release): publish eql-bindings to crates.io via release-plz
…rated inventory/TS/JSON
…en coverage eql_v3.ste_vec_entry / eql_v3.ste_vec_query rename to eql_v3.jsonb_entry / eql_v3.jsonb_query, bringing them onto the same family+suffix convention every scalar type uses (eql_v3.json, the document domain, keeps its established name — the one documented exception). Domain::full_name/domain_name is shape-aware for exactly that one case; every other domain, scalar or SteVec, resolves through the same join. Renamed across the real SQL DDL (src/v3/jsonb/), the catalog, hand-written bindings, generated TS/JSON Schema artifacts, tests, and docs — ste_vec_* function/scheme names (to_ste_vec_query, ste_vec_contains, is_valid_ste_vec_*_payload, etc.) are unchanged. Also folds in fixes surfaced by review: - SteVecEntry.a now renders `a?: boolean | null` in the generated TS binding (was incorrectly required) via #[ts(optional = nullable)]. - New Domain::rust_struct_name centralizes the Shape -> Rust struct identifier mapping that eql-codegen's inventory renderer and a test each carried their own copy of. - Real-crypto SQLx coverage for SteVecQuery/SteVecQueryEntry (previously only the document/entry shapes were exercised against actual ciphertext), plus an assertion that both SteVecTerm variants (hm, oc) occur in real fixture data. - Conformance test pinning untagged SteVecTerm resolution when both hm/oc are present, and TS property-order pinning for the SteVec structs. - ScalarKind::Jsonb.rust_type() pinning test, at parity with every sibling kind. - codegen-level assertion that dump_catalog/list-types exclude the jsonb family.
- catalog_parity: add jsonb schema-required drift gate pinning the published json/jsonb_entry/jsonb_query schema `required` sets (+ entry hm-XOR-oc anyOf, query element no-`c`) against the hand-written is_valid_ste_vec_*_payload SQL CHECK contract — the SQL-CHECK↔JSON-Schema link the scalar gate skips (#1). - spec: guard DomainFamily::is_eq_only() with is_scalar() so the non-scalar jsonb family is no longer misreported as eq-only (latent footgun for wiring jsonb into the eq-only-sensitive matrix macros) + regression test (#3). - catalog_parity: route schema_required_keys_match_catalog_terms and schemas_are_strict through the scalar_families() DRY helper (schemas_are_strict also drops its O(n·m) name re-lookup + fail-open .unwrap_or(true)); the ts_property_order re-inline stays as the documented exception (#4). - support.rs: derive the cross-variant exclusion list from the non-scalar catalog families instead of hardcoding 'json'/'jsonb_entry'/'jsonb_query' — a future rename or second non-scalar family stays covered automatically (#2).
…ry types The entry/query domain rename (ste_vec_entry/ste_vec_query → jsonb_entry/ jsonb_query) missed two consumers, failing the 'Validate (Postgres 17)' job: - tasks/test/clean_install_v3.sh cast the containment needle to the dropped type `eql_v3.ste_vec_query` → "type does not exist". Now `eql_v3.jsonb_query`. (The `to_ste_vec_query` FUNCTION keeps its name; only the domain types were renamed.) Verified: `mise run build && mise run test:clean_install_v3` passes. - tasks/test/splinter.sh allowlist descriptions + a section comment referenced the old `eql_v3.ste_vec_entry` type name (informational field 5 / comment only — splinter matches on fields 1-4, so no behavior change).
Shard PG17 3/4 failed on real_ste_vec_row_parses_into_document_and_entries:
Error: unknown field 'k', expected one of 'v', 'i', 'sv'
The real cipherstash SteVec document envelope carries the k form discriminator
(k:"sv") — required by the canonical eql-payload-v2.3 SteVecPayload
(required: [v,k,i,sv]) and emitted on every real payload. SteVecDocument modelled
only {v,i,sv} and is #[serde(deny_unknown_fields)], so it hard-rejected the real
payload the eql_v3.json SQL CHECK accepted at INSERT (the CHECK only mandates
v/i/sv and tolerates extras). The test caught a genuine binding gap.
Fix: add a field pinned to "sv" via a new SteVecForm discriminator type
(mirrors how SchemaVersion pins v: rejects any other value, so a scalar k:"ct"
payload can't be read back as a document). SteVecQuery needs no k — to_ste_vec_query
builds a locally-constructed {sv:[...]} needle. eql_v3 never reads k; it is
passthrough form metadata preserved on round-trip.
Also:
- Investigation note in mod.rs: why the flat-scalar structs are {v,i,c} with no k
(k is OPTIONAL on the ct form per the canonical schema; a latent gap flagged if
a producer ever emits k:"ct" on a scalar — no test parses real scalar
ciphertext into the bindings today).
- Conformance: document round-trip now carries k:"sv"; envelope negatives cover
missing k; new wrong-k ("ct") negative; strict-schema test pins k const "sv".
- catalog_parity drift gate: json required set is now {v,k,i,sv}.
- Regenerated bindings/schema (SteVecDocument.ts gains k; new SteVecForm.ts;
json.json required updated).
- nit: shape_and_terms_are_consistent no longer chains the JSONB const (it is in
CATALOG post-flip).
…name freshtonic's PR #336 review flagged this as a non-blocking naming nit: the min/max aggregate state functions kept the pre-rename `ste_vec_entry_*_sfunc` names after the domain rename (ste_vec_entry → jsonb_entry), and splinter.sh's to_ste_vec_query allowlist description still pointed at the dropped `eql_v3.ste_vec_query` type. Rename the sfuncs to jsonb_entry_min_sfunc/jsonb_entry_max_sfunc and fix the stale description. Verified: mise run clean && mise run build; mise run test:clean_install_v3; mise run test:splinter (unmatched=0); cargo build --workspace.
…families Corrects CLAUDE.md and docs/reference/catalog-driven-architecture.md, which still described jsonb as lacking a generated SQL surface — it has had a permanently hand-written one under src/v3/jsonb/ since #267, deliberately skipped by eql-codegen via the SteVec Shape variants, not a "not yet" gap. Also: adds behavioral SQLx coverage for eql_v3.jsonb_contains / jsonb_contained_by (previously only structurally tested, despite being documented public API for hand-rolled GIN queries) and clarifying Doxygen notes on their intentional unreachability from the typed operators; deletes two orphaned files with no remaining references (src/v3/jsonb/jsonb_test.sql, excluded from the build and superseded by the SQLx suite; tests/ste_vec.sql, which referenced the removed eql_v2_encrypted type); and corrects a stale release/cipherstash-encrypt-v3.sql installer filename across four Unreleased CHANGELOG entries to match the current cipherstash-encrypt.sql artifact name.
An unrotated container log filled the shared Docker VM's disk to 100%, blocking postgres:up. Caps each test-postgres container's json-file log at 30MB (10MB x 3 files) instead of unbounded growth.
…ELOG bullet split has_ore_cllw(jsonb_entry) previously had only incidental true-path coverage via jsonb_entry_int4_fixture_shape (asserting fixture rows have oc); adds a dedicated positive/negative test for both branches. CHANGELOG.md:25 had two Added bullets merged onto one line (missing newline before the PR #307 entry), predating this branch — the second bullet wasn't rendering as its own list item.
…op dead Default PR #336 review flagged Shape::{SteVecDocument,SteVecEntry,SteVecQuery} as behaviorally identical everywhere except one hardcoded-literal match arm in Domain::rust_struct_name. Collapses to Shape::{Scalar,SteVec}, deriving the struct name from Domain.name instead ("entry"->SteVecEntry, "query"-> SteVecQuery, "json"->SteVecDocument as the one irregular case already special-cased by full_name). Domain::full_name's non-exhaustive shape check and its hand-duplicated copy in tests.rs are folded into the same identity comparison as a byproduct. Also removes SteVecForm's unused `impl Default` (crates/eql-bindings) — confirmed dead via grep, no #[serde(default)] or call site references it. Verified: cargo test -p eql-domains -p eql-codegen -p eql-bindings (256 passed), mise run codegen:parity (byte-identical), existing rust_struct_name_is_shape_aware / jsonb_domain_names_follow_the_family_suffix_convention tests already pin the exact output strings so no new test was needed. Two other review findings (a hand-written DomainType-impl macro duplicating codegen's generated pattern, and two SQLx tests replaying the same fixture) were investigated and left as-is: both are real but fixing them would add new shared infrastructure or conflate distinct test assertions for marginal benefit, per the project's anti-overengineering convention.
Wraps 4 assert!/assert_eq! calls that exceeded the line-length limit, introduced by the earlier jsonb_contains/has_ore_cllw test additions.
feat(eql_v3): jsonb (SteVec) encrypted-JSONB payload bindings (Rust/TS/JSON Schema)
…tors into eql_v3_internal
Includes the eql_v3.ore_cllw(ste_vec_entry) extractor's RETURNS/cast and the ste_vec_entry min/max sfunc local variable declarations in src/v3/jsonb/, since those reference the now-moved eql_v3_internal.ore_cllw composite type even though the extractor function itself stays public eql_v3 (Task 4 still moves the sfunc/wrapper *functions* themselves).
Cuts the EQL 3.0.0-alpha.4 lockstep release: npm `@cipherstash/eql`, crate `eql-bindings`, and the bundled SQL all ship at this identity. Carries CIP-3469 (#390): SteVec ordering moves from CLLW-ORE (`oc`) to CLLW-OPE (`op`). `SteVecTerm` becomes `{ hm } | { op }`, the `eql_v3_internal.ore_cllw` type and its superuser-only operator class are gone, and `from_v2` fails closed on `oc` entries with UnconvertibleOreTerm. Entry ordering now extracts `eql_v3.ord_ope_term(entry)` under native bytea comparison, so a plain functional btree index engages on managed Postgres. Unblocks protectjs-ffi#129, whose 7 remaining failures are all `FromV2(MissingTerm { key: "hm|oc" })` against alpha.3. Generated by `pnpm run version`: changesets computed alpha.4 (prerelease mode absorbs the major bump), sync-lockstep-versions.mjs propagated it to Cargo.toml, and release:prepare_bindings_assets rebuilt the exact-version SQL into both packages. Verified: bundled SQL carries 0 `ore_cllw` and 300 `ord_ope_term` references, crate and npm SQL are byte-identical, and all four version artifacts agree.
The CIP-3472 rename sweep rewrote the match arms of norm() in v3_jsonb_operator_surface_tests, but those arms match what Postgres' format_type() RETURNS, not names in our source. Two things changed at once: the domains gained the eql_v3_ prefix, and json stopped being a reserved word, so format_type no longer quotes it. Output is now bare eql_v3_json / eql_v3_jsonb_entry (schema omitted, public being on search_path), where the arms still looked for the quoted json and bare jsonb_entry. Every operand normalised to itself, so all four surface assertions saw unprefixed names and failed on 3 of 4 PG17 shards. Match the real output, and keep the bare query_jsonb arm so the assertions survive a search_path change that would unqualify it.
…blic-eql-v3-types-with-eql_v3_ feat(types)!: prefix all public EQL v3 types with `eql_v3_` (CIP-3472)
`public.<T>_ord` and `public.text_ord` now carry the `op` term, extract via
`eql_v3.ord_ope_term(col)` returning `eql_v3_internal.ope_cllw`, and order by
native bytea comparison. Nothing is removed: `_ord_ore` keeps block-ORE and its
`eql_v3.ord_term` extractor, `text_search` stays `[hm, ob, bf]`, and `_ord` /
`_ord_ope` are now the byte-identical twins (previously `_ord` / `_ord_ore`).
Why. `ope_cllw` is a DOMAIN over bytea, so an ordered functional btree resolves
to `bytea_ops` — the base type's default opclass, whose opfamily already holds
the comparison operators the planner must match. The block-ORE path depends on a
hand-written opclass for a composite type, created by a DO block that silently
skips on insufficient_privilege. When that opclass is absent (the ordinary case
on managed Postgres), `CREATE INDEX ... btree(eql_v3.ord_term(col))` does not
fail: Postgres binds `record_ops`, and the resulting index never engages because
the ORE operators are not members of that family. `_ord` removes the failure
mode rather than reporting it.
The catalog is the only source of truth for this: three lines in
`crates/eql-domains/src/lib.rs`. Everything under `src/v3/scalars`,
`crates/eql-bindings/{src/v3,bindings,schema}` and `packages/eql/src/generated`
is regenerated output and was not hand-edited (verified idempotent).
The SQLx harness no longer assumes the ordering SEM is block-ORE. The extractor
name and the term-identity oracle are derived from the domain's catalog `Term`
(`Variant::ordering_term`), so `_ord` routes through `ord_ope_term`/`op` and
`_ord_ore` through `ord_term`/`ob` from one source.
Two behaviour changes, both verified against real ciphertext and documented in
U-003:
* `public.text_ord` now ACCEPTS the empty string and sorts it first. OPE
encrypts "" to a well-formed one-byte term; block-ORE produces an
unorderable empty array (`ob: []`) that `text_ord_ore` still rejects (#262).
* `real`/`double` `_ord` columns now distinguish -0.0 from +0.0 (`-0.0 < +0.0`,
`-0.0 <> +0.0`). `orderable-bytes` canonicalizes the sign of zero for ORE;
CLLW-OPE does not. This makes `_ord` consistent with `_eq`, which already
distinguished them (see #387).
BREAKING CHANGE: `eql_v3.ord_term(public.<T>_ord)` no longer exists — use
`eql_v3.ord_ope_term`, or retype the column to `public.<T>_ord_ore`. Rows in an
`_ord` column must carry `op` rather than `ob`. See docs/upgrading/v3.0.md U-003.
Note: `float_special::negative_zero_and_positive_zero_compare_equal_under_eq`
fails at this commit. It fails identically on the parent (verified by rebuilding
the baseline SQL against a fresh database), and no CI job runs that file today.
The follow-up commit wires it into CI behind a self-expiring known-failure
marker and files the bug as #387.
…re markers `tests/sqlx/tests/encrypted_domain/float_special.rs` had never executed in CI, from the commit that created it (76f7db6). Two independent locks: the sharded job compiles the archive with default features, so the `#[cfg(feature = "proptest-e2e")]` module was never built; and the `e2e` job (10fd7e4) runs `cargo test --features proptest-e2e e2e_oracle`, a name filter that `float_special::*` does not match. The only task that runs it, `test:sqlx`, is invoked by no workflow. Its NaN / +-0.0 / +-Inf encoder tripwires guarded nothing. `test:sqlx:e2e` now names both proptest-e2e suites explicitly. A bare unfiltered run would duplicate the sharded fixture suite, which is why the filter exists; the fix is to name the second suite, not to drop the filter. Turning the suite on surfaces a real bug (#387): `-0.0` and `+0.0` hash to different `hm` terms, because cipherstash-client feeds `f64::to_be_bytes()` — sign bit included — into the HMAC. `WHERE col = 0.0` misses rows stored as `-0.0`. It is not caused by, and does not depend on, the `_ord` ordering SEM. Rather than `#[ignore]` it — which hides the failure and keeps hiding it after the fix lands — `known_failure()` inverts the assertion. The test is written the way it SHOULD pass; the marker passes while the bug reproduces (printing the issue URL on every run) and FAILS the moment the assertion starts passing. A marker cannot outlive its bug. The other half of the contract is `mise run test:known-failures` (new CI job, credential-free): every `ISSUE_*` constant must name an issue that exists, is still OPEN, and is actually referenced by a test. So a failing test can only be suppressed with an open, identified issue, and only until that issue closes.
…heir cipher
`eql_v3.ord_term` now serves the default `_ord` domain (CLLW-OPE, returning
`eql_v3_internal.ope_cllw`), and block-ORE moves behind the qualified
`eql_v3.ord_term_ore` on `_ord_ore`. `eql_v3.ord_ope_term` is removed; its
overloads fold into `ord_term`.
Why. The previous commit flipped `_ord` from block-ORE to CLLW-OPE but left the
extractor names keyed off the *term*, so the default domain inherited the
qualified name (`ord_ope_term`) while the escape hatch kept the unqualified one.
That inverted the surface: `ord_term(public.<T>_ord)` — the name every caller
reaches for — vanished from the overload set, and the rename landed on the
common path instead of the specialised one. Naming the extractor after the
domain it serves restores the intended shape:
_ord -> ord_term (default; ope_cllw)
_ord_ope -> ord_term
_ord_ore -> ord_term_ore (escape hatch; ore_block_256)
Callers on `_ord` keep their index DDL verbatim; only the return type and the
payload term (`ob` -> `op`) move, so the index is rebuilt rather than rewritten.
Callers on `_ord_ore` do a pure DDL rename with no re-encryption or reindex.
The catalog remains the sole source of truth: this is two lines in
`Term::extractor()` (crates/eql-domains/src/term.rs). Everything under
src/v3/scalars, crates/eql-bindings/{src/v3,bindings,schema} and
packages/eql/src/generated is regenerated output, verified byte-identical to a
clean regeneration and free of incidental drift. `ope_extractor_never_collides_
with_ore_extractor` continues to pin the distinctness the dedupe relies on.
Also corrects two term-newtype docs that the SEM swap had falsified:
`OpeCllw` claimed to back only `_ord_ope`, and `OreBlock256` still claimed to
back `_ord`. Both feed the committed TypeScript and JSON Schema.
BREAKING CHANGE: `eql_v3.ord_term(public.<T>_ord_ore)` is renamed to
`eql_v3.ord_term_ore`, and `eql_v3.ord_ope_term` no longer exists (use
`eql_v3.ord_term`). `eql_v3.ord_term(public.<T>_ord)` keeps its name but now
returns `eql_v3_internal.ope_cllw` over the `op` term. See docs/upgrading/v3.0.md
U-003.
Note: DB-backed suites were not run locally and are left to CI.
`public.text_search` now carries `[hm, op, bf]` and orders via `eql_v3.ord_term` (returning `eql_v3_internal.ope_cllw`). The block-ORE shape `[hm, ob, bf]` is preserved under a new domain, `public.text_search_ore`, whose ordering extractor is `eql_v3.ord_term_ore`. Equality (`hm`/`eq_term`) and bloom containment (`bf`/`match_term`) are unchanged on both, and the two domains expose an identical operator surface. Why. This completes the move begun in 020afff for `_ord`, and the reason is the same one — not operator capability. The catalog previously recorded that `_search` "deliberately excludes `Ope`" because "its operator surface would not grow". True, and the wrong axis: `ore_block_256` is a composite whose btree operator class is hand-written and installed by a DO block that silently skips on insufficient_privilege — the ordinary case on managed Postgres. Absent that opclass, an ordered functional index over `text_search` still CREATEs (Postgres binds `record_ops`) and then never engages. `ope_cllw` is a DOMAIN over bytea, so the index binds `bytea_ops`, the base type's default opclass. `text_search` removes the failure mode rather than reporting it. Empty-string behaviour follows the term, exactly as it did for `_ord`: `op` has no non-empty-array CHECK clause, so `text_search` now ACCEPTS `""` and sorts it first, while `text_search_ore` still rejects it (#262). The catalog is the only semantic edit — two `Domain` rows in `crates/eql-domains/src/lib.rs`. The one non-mechanical consequence is `capability_label` in `crates/eql-codegen/src/bindings.rs`, a name-dispatched `match` with a `panic!` catch-all that gates codegen until a new bare domain name is deliberately labelled. Everything under `src/v3/scalars`, `crates/eql-bindings/{src/v3,bindings,schema}` and `packages/eql/src/generated` is regenerated output. The SQLx matrix wires `search_ore` as a first-class variant rather than leaving it codegen-only: it is now the sole block-ORE-with-bloom domain, so its containment, ORE injectivity and empty-string rejection each get an arm. `search` needed no matrix change — its routing was already catalog-derived, and it re-points from `ord_term_ore`/`ob` to `ord_term`/`op` on its own. No fixture regeneration: `scalar_fixture!(text, ...)` already encrypts with [Unique, Ore, Match, Ope], so the committed text fixture carries all four term keys and both new term sets are subsets of it. Also refreshes SUPABASE.md, which still described `ord_term` as returning `ob` — stale since 85c41a4, and squarely aimed at the audience this change benefits. BREAKING CHANGE: `public.text_search` rows must carry `op` rather than `ob`, and `eql_v3.ord_term_ore(public.text_search)` no longer exists — use `eql_v3.ord_term`, or retype the column to `public.text_search_ore` for the zero-re-encryption path. `public.text_search` now accepts the empty string. See docs/upgrading/v3.0.md U-004. Note: DB-backed SQLx suites were not run locally and are left to CI. The public-surface golden was regenerated against Postgres 17.
…ctor rename
The clean-install smoke test still indexed eql_v3.ord_term_ore(c) on a
public.integer_ord column — ambiguous ('function is not unique') now that
the _ord extractor is eql_v3.ord_term and ord_term_ore belongs to the
_ord_ore domains. Smoke both paths: ord_term on integer_ord (CLLW-OPE,
native bytea opclass — needs nothing installed) and ord_term_ore on
integer_ord_ore, which keeps the D4 proof (fails outright if the ported
operator_class is absent).
Also fixes two doc stragglers from the rename: the database-indexes
superuser callout still recommended the removed eql_v3.ord_ope_term and
described the pre-#389 poison set, and the U-003 TL;DR named text_search
where its own compatibility table (and ore_fallback.sql) say
text_search_ore.
…ever poisoned The NULL-insert probe and the demote-reinstall scenario hardcoded public.integer_ord, which this branch moves off block-ORE: it is no longer in the poison set, so 'must raise' assertions failed CI (shards 2/4 and 4/4). Both probes now exercise public.integer_ord_ore, and the reinstall scenario derives its stored payload from the catalog's ord_ore terms (ob) instead of ord's (now op).
…urrected OreCllw The rebase over eql_v3 (which now carries CIP-3469's SteVec CLLW-OPE switch) auto-merged this branch's OpeCllw doc edits over the top of that change, re-adding the deleted OreCllw newtype and a doc contrast against it. Remove the newtype again, fold the branch's text_search addition and the SteVec entry facts into one OpeCllw doc (all ordering surfaces now extract via eql_v3.ord_term), and regenerate the TS/JSON outputs.
The rebase's later commits re-applied their pre-CIP-3469 context over
conflict resolutions made in earlier ones:
- tests/sqlx/src/{jsonb_entry,matrix,scalar_domains}.rs regressed the
SteVec entry ordering seam to the removed eql_v3.ore_cllw — the CI
failure on every jsonb_entry matrix ordering/aggregate test
- the public-surface golden resurrected eql_v3.ore_cllw(entry) /
has_ore_cllw(entry) rows and lost the ord_term(entry) overload
- docs/upgrading/v3.0.md lost the SteVec U-004 note and the U-003
scope-caveat resolution entirely; restored with his notes renumbered
to U-005/U-006 and every TL;DR / compat-table / changeset cross-ref
and anchor updated to match
`ord_term_only_operand_orders_via_the_ore_operator` stored `IndexKind::Ore`
payloads into `public.integer_ord`. Since `_ord` moved to CLLW-OPE its domain
CHECK requires `op`, so the `ob`-bearing payload is rejected:
value for domain integer_ord violates check constraint "integer_ord_check"
Encrypt with `IndexKind::Ope` and rename to `..._via_the_ope_operator` — the
test asserted "orders via the ORE operator" about a domain that no longer uses
ORE. `_ord_ore` remains the block-ORE surface and is exercised elsewhere.
The file is behind `#![cfg(feature = "proptest-e2e")]`, which `mise run
test:sqlx` enables, so this was a red in CI.
`JsonbEntryInteger` overrides `ord_extractor_expr` because a SteVec entry's
ordering term is structural (`op`), not derivable from its `PG_TYPE`
("integer"). But integer's `_ord` is itself OPE-backed, so under
`Variant::Ord` the catalog-derived default returns the same `ord_term` string
as the override — deleting the override broke no test.
Assert `Variant::OrdOre` too, where the two diverge: the default consults the
catalog, finds `Term::Ore`, and emits `ord_term_ore`. Verified by deleting the
override and observing the failure (`left: ord_term_ore`, `right: ord_term`).
The `_ord` and `text_search` moves to CLLW-OPE invalidated prose written when those domains were block-ORE. Corrections: - `.changeset/eql-3468.md`: the poisoned set is `_ord_ore` / `text_search_ore` (+ query twins), not `_ord` / `text_search`; the count is 20, not 38 (the 9 `_ord` domains and their twins left the set — verified against the generated `ore_fallback.sql`); the broken ORE index recipe is `ord_term_ore(col)`. - `.changeset/ord-defaults-to-ope.md`: dropped the claim that `text_search` remains `[hm, ob, bf]` — it contradicted `search-defaults-to-ope.md`, and both land in one CHANGELOG. - `docs/upgrading/v3.0.md`: the `text_search` note is U-006, not U-005. - `docs/reference/eql-functions.md`: `min`/`max` compare via the variant's ordering term, not "the ORE block term". - `docs/reference/permissions.md`: the pgcrypto `extensions` grant is needed only by the block-ORE variants; CLLW-OPE compares native bytea. - `docs/reference/database-indexes.md`: the opclass audit targets the `_ord_ore` index, not the OPE one defined above it. - `src/v3/jsonb/operators.sql`: `eq_term` coalesces `hm`/`op`, not `hm`/`oc`. - `operator_class.sql`, `splinter.sh`: `ord_term_ore` lives in `eql_v3`, and returns `eql_v3_internal.ore_block_256`.
…ed `oc`
`JsonbEntryInteger::placeholder_payload()` still emitted
`{"s":…,"c":…,"oc":"00"}`, a CLLW-ORE entry term that CIP-3469 removed. The
`public.jsonb_entry` CHECK now validates `hm` XOR `op` and rejects `oc`, so
every matrix test casting the placeholder failed:
value for domain jsonb_entry violates check constraint "jsonb_entry_check"
(`matrix_jsonb_entry_integer_entry_{eq,lt,gt}_supported_null`, red on CI.)
The accompanying unit test asserted `hm ^ oc`, so it enshrined the wrong
contract and passed while the live domain rejected the payload. It now asserts
`hm ^ op`, matching the CHECK. Two doc comments describing the entry's ordering
term as CLLW-ORE are corrected too.
CIP-3469 replaced the SteVec entry ordering term but left four doc comments describing the v3 surface as `hm` XOR `oc`. Doc-only; the `from_v2` references to `oc` are correct and untouched — that code exists to reject `oc`.
The suite is `#![cfg(feature = "proptest-e2e")]`, so the default-feature shards compiled it to zero tests, while the e2e job filtered only `e2e_oracle` and `float_special` by name. Its three assertions had never executed — the exact orphaning this branch fixes for `float_special`, left in place for the sibling file the branch itself edited onto the new OPE `_ord`. It is a whole test TARGET rather than a module, so no name filter reaches it; invoke it with `--test`. The comment above the task already warned that every `proptest-e2e`-gated suite needs its own line here.
…die silently `grep` exits 1 when it matches nothing — precisely the case this check exists to report. Under `set -euo pipefail` that status propagated out of the pipeline and killed the script at the assignment, before the "referenced by no test" diagnostic could print. The gate exited 1 with no output, so the single failure mode it was written for produced no explanation. Swallow only grep's status; `wc` still counts the (empty) input. Verified by registering an unreferenced probe constant: the gate now prints the diagnostic and exits 1.
`_ord` moved to CLLW-OPE, which does not canonicalize the sign of zero, so `=` on `public.<T>_ord` now splits `-0.0` from `+0.0` for real/double. Nothing pinned that. The changeset justified it as consistency with `_eq` — but `_eq` only behaves that way because of #387, an open bug: when #387 is fixed the rationale evaporates and, as it stood, no test would have turned red. Pin it with the same `known_failure(#387)` marker `_eq` uses. CLLW-OPE derives `op` from the same raw `f64::to_be_bytes()` (sign bit included) that #387 feeds into `hm`, so a root-cause fix canonicalizes both terms at once: this test then turns RED and forces the marker — and the changeset's wording — to be revisited rather than quietly outliving its premise. Also assert the contrast unconditionally: `=` on the block-ORE `_ord_ore` already agrees with IEEE, because `orderable-bytes` canonicalizes. That is the domain a float column needing `±0.0` equality should use, and the changeset now says so.
feat(v3)!: back the `_ord` domains with CLLW-OPE instead of block-ORE
|
Important Review skippedToo many files! This PR contains 876 files, which is 726 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (199)
📒 Files selected for processing (876)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Brings eql_v3 up to date with main (release-plz.yml registration stub, #383). Conflict: .github/workflows/release-plz.yml (add/add). Resolved in favour of eql_v3, which carries the full publish workflow. main's copy is a registration stub whose own comment says "Replace with the full workflow when the eql_v3 release line merges to main" — this is that merge. The resulting tree is byte-identical to eql_v3 prior to this merge; the stub is the only content main contributed and eql_v3 supersedes it wholesale.
freshtonic
left a comment
There was a problem hiding this comment.
Review: EQL 3.0 (eql_v3 → main)
Reviewed the hand-written surface only. Generated artefacts were excluded by marker (AUTOMATICALLY GENERATED FILE / @generated) and by path (release/, src/v3/scalars/, crates/eql-bindings/{sql,bindings,schema}/, packages/eql/{src/generated,sql}/, pnpm-lock.yaml) — 654 of 949 changed files, leaving ~295 to read. I also took the branch's prior per-PR reviews as given and did not re-litigate settled design; the focus is merge-level and release-level risk, plus invariant violations.
Worth flagging up front: CodeRabbit skipped this PR entirely ("876 files exceed the limit of 150"), so nothing automated has looked at it.
Overall this is high-quality work. The footgun invariants the architecture exists to protect are intact, and the test suite is unusually well-guarded. Three things need attention, one of them before 3.0.0 can be cut at all.
🔴 Blocking (before versioning, not necessarily before merge) — Changesets pre-mode reaches main, so the production path can never emit 3.0.0
.changeset/pre.json (mode: "pre", tag: "alpha") exists on eql_v3 and not on origin/main. This merge carries it onto main. release.yml classifies any push to main as mode=production and runs pnpm run version → changeset version, which in pre-mode bumps to the next prerelease.
docs/development/releasing.md documents this exact footgun:
Footgun — exit pre-mode before a final release.
.changeset/pre.jsonlives oneql_v3. If it reachesmain,changeset versionthere will emit alpha-suffixed "final" versions. Runchangeset pre exit(and merge that) before cutting a production release frommain.
I confirmed it empirically rather than inferring it — ran changeset version in a detached worktree at this branch's head:
- As-is (pre.json present):
3.0.0-alpha.4→3.0.0-alpha.5, all 43 changesets left unconsumed, no3.0.0CHANGELOG section. Changesets itself warns: "You are in prerelease mode … you should revert these changes and runchangeset pre exit." - After
changeset pre exit: →3.0.0, all 43 changesets consumed, full3.0.0changelog section written.
The 43 changesets (6 major incl. eql-v2-removed.md, 30 minor, 7 patch) do aggregate correctly to 3.0.0 — but only once pre-mode is exited.
Not auto-catastrophic: publishing is gated behind the human-reviewed "Version Packages" PR, and the production path uses changeset publish, which in pre-mode tags npm alpha — so latest isn't poisoned. The failure mode is "3.0.0 never ships / another alpha publishes from the production path," not "GA is corrupted."
Fix: changeset pre exit and commit — either in this PR or immediately after merge, before the Version Packages PR is versioned.
🟠 Major — docs/upgrading/v3.0.md doesn't cover the eql_v2 removal, and says so
The guide's own header:
This guide currently covers the
eql_v3envelope-version bump; the release owner extends it with notes for the other pending breaking changes (notably theeql_v2schema removal) as the release is prepared.
grep -cE 'eql_v2_encrypted|config_add' docs/upgrading/v3.0.md → 0. U-001…U-006 and the compatibility table cover only intra-v3 changes (envelope v:3, query-domain renames, ORE→OPE) — i.e. migrations for pre-release alpha adopters, not for an actual EQL 2.x production user.
So a v2 user opening the canonical upgrade guide for the release whose headline change is "the entire eql_v2 schema is removed" finds nothing about their eql_v2_encrypted columns, the removed config_add_table/config_add_column/config_add_index API, or the removed eql_v2.* operators.
No data-loss trap — the v3 installer globs src/v3 only and leaves an existing eql_v2 schema intact, and the uninstaller drops only eql_v3/eql_v3_internal. This is a documentation gap, not a destructive one. Defensible while on the -alpha line; must land before 3.0.0 GA.
🟠 Major — encrypted empty JSONB array (sv: []) breaks the array accessors
eql_v3.ste_vec() builds its jsonb[] with array_agg(elem), which returns SQL NULL (not '{}') over zero rows. The document CHECK accepts sv: [] (jsonb_typeof(val->'sv')='array' holds, and NOT EXISTS over an empty array is trivially true), so this is a storable value.
Reproduced against a real PostgreSQL 16 with the surface built from this branch (mise run build → release/cipherstash-encrypt.sql), payload {"v":"3","i":{...},"a":true,"sv":[]}:
| Call | Actual | Expected (native jsonb parity) |
|---|---|---|
cast to public.eql_v3_json |
accepted (t) |
accepted |
eql_v3.jsonb_array_length(doc) |
NULL |
0 |
eql_v3.jsonb_array_elements(doc) |
ERROR: upper bound of FOR loop cannot be null |
0 rows |
eql_v3.jsonb_array_elements_text(doc) |
same ERROR | 0 rows |
Both errors come from FOR idx IN 1..array_length(sv, 1) with sv NULL (src/v3/jsonb/functions.sql:446, and the same shape in jsonb_array_elements_text).
Note the obvious fix is not sufficient: coalesce(array_agg(elem), '{}') still yields NULL from jsonb_array_length, because array_length('{}'::jsonb[], 1) is itself NULL in Postgres. I verified a fix that does hold:
jsonb_array_length:RETURN coalesce(array_length(sv, 1), 0);jsonb_array_elements/_text:FOREACH item IN ARRAY coalesce(sv, '{}'::jsonb[]) LOOP
→ length 0, elements 0 rows, matching native jsonb semantics.
Reachability caveat, stated honestly: the SQL layer is definitely wrong for this input, but the trigger requires the client to emit a: true with sv: [] for an encrypted []. a is the documented array marker (eql_v3_internal.is_ste_vec_array), and sv: [] documents already appear in v3_jsonb_operator_surface_tests.rs:485 (without a), so it looks constructible — please confirm whether cipherstash-client produces it. Worth a regression test either way.
🟡 Minor — edge_cases.rs _ord arm tests a stale requirement
tests/sqlx/tests/encrypted_domain/property/edge_cases.rs:154-164 — check_rejects_payload_missing_ob asserts "the _ord and _ord_ore domain CHECKs both require ob". Stale after the CLLW-OPE flip: the catalog gives _ord Term::Ope (crates/eql-domains/src/lib.rs:215-216), so its CHECK requires op, not ob. The no_ob fixture happens to omit both, so _ord still rejects it — for the wrong reason. A payload carrying op but no ob would be accepted by _ord and this test would never notice.
Not a can't-fail test (a fully-broken _ord CHECK is still caught), and the catalog-derived matrix check covers the real per-variant requirement. Suggest splitting into a missing-op case for _ord and missing-ob for _ord_ore, with corrected comments.
Nits
.gitignore:7-11,246-248— stale ignore entries for build variants deleted in this PR (deps.txt,deps-supabase.txt,src/deps-protect.txt, …).tasks/build.shnow only producesdeps-v3.txt/deps-ordered-v3.txt. Harmless, cosmetic..github/workflows/release.yml:17-20,48— the workflow holdingid-token: write(npm OIDC) uses floating tags (actions/checkout@v4,jdx/mise-action@v3) whiletest-eql.ymlSHA-pins. The highest-privilege workflow deserves the tightest pinning. Also, workflow-levelpermissionsare inherited by jobs that don't need them (classifyonly runsgit log); prefercontents: readat the top with per-job escalation.README.md:205— performance table still captioned "EQL 2.3 on PostgreSQL 17" on a 3.0 release. Honestly labelled, just stale provenance.docs/development/2026-07-04-release-tasks-implementation-plan.md(1538 lines) — superseded internal scratch. It carries a clear "SUPERSEDED (historical record)" banner and isn't linked anywhere, so this is defensible; consider deleting rather than shipping.
Verified clean (checked, nothing found)
- All four encrypted-domain footguns hold. Programmatically audited all 1,565 generated blockers: every one is
LANGUAGE plpgsql, none isSTRICT. No domain-over-domain (all domains resolve to a base type). The single operator class isFOR TYPE eql_v3_internal.ore_block_256, a composite type, not a domain. EveryLANGUAGE sqlextractor/wrapper isIMMUTABLE, single-SELECT, and free of aSETclause, so inlining survives. - Self-containment. No
eql_v2.<symbol>anywhere insrc/v3outside doc-comment prose. - Codegen determinism. The only
HashSet/HashMapin the generator is used forcontains()membership, never iterated into output — so "identical CATALOG ⇒ byte-identical SQL" holds.mise run buildon this branch produced a zero-byte diff. - Rust workspace.
cargo check --workspaceclean;eql-domains(93),eql-codegen,eql-bindings,eql-tests-macros(22) test suites all pass.serde(flatten)/deny_unknown_fieldsare correctly kept off the same structs.from_v2fails closed onoc(CLLW-ORE) entries. - No silently-skipped tests. The
proptest-e2egate is genuinely exercised — the dedicatede2ejob runsmise run test:sqlx:e2e, which names each gated suite explicitly, andci-requiredneedsit. - No synthetic ciphertexts. Hand-built hex payloads appear only in structural CHECK/blocker/comparator-wiring tests; every crypto ordering/equality claim rides real generated fixtures.
- CI gating is sound.
ci-requiredlists every job inneedsand passes only onsuccess|skipped— afailure/cancelledcannot be masked. The drift gates (codegen-parity.sh,schemas-parity.sh) correctly fail on untracked files too, which is the usual blind spot. - v2 removal is coherent. No live reference to any deleted task/artifact (
tasks/uninstall.sql,tasks/pin_search_path.sql,src/version.template) — the only hits are removal-note comments. No doc instructs installing a build variant that no longer exists. - Uninstall is correct by design.
eql_v3/eql_v3_internaldrop CASCADE takes thepublicORE operators with them; thepublic.eql_v3_*column domains deliberately survive so application tables do, and reinstall is idempotent.
Summary: the changeset pre exit step must happen before 3.0.0 is versioned, and the v3.0 upgrade guide needs its eql_v2-removal section before GA. The empty-sv accessor bug is a real, reproduced defect worth fixing regardless of whether the client currently emits that payload. Everything else is nits. Nice work.
`.changeset/pre.json` carried `mode: "pre"` (tag `alpha`), and `release.yml` classifies any push to `main` as `mode=production`, running `changeset version`. In pre-mode that bumps to the next *prerelease*: merging `eql_v3` into `main` would have produced `3.0.0-alpha.5`, leaving all 43 changesets unconsumed and no `3.0.0` CHANGELOG section — the production path could never emit 3.0.0. `changeset pre exit` flips `mode` to `"exit"`; the next `changeset version` then emits final versions, consumes the changesets, and removes `pre.json`. Verified in a scratch worktree: the 6 major / 30 minor / 7 patch changesets aggregate to `3.0.0` with a full changelog section. This is the step `docs/development/releasing.md` calls out: > Footgun — exit pre-mode before a final release. Run `changeset pre exit` > (and merge that) before cutting a production release from `main`. Note: `eql_v3` can no longer cut alphas without re-entering pre-mode (`changeset pre enter alpha`).
EQL 3.0 replaces the untyped
eql_v2_encryptedcomposite column type with a family of typed, per-capability encrypted domain types, and removes theeql_v2schema entirely. The database no longer owns the encryption configuration model — the client (CipherStash Proxy / ProtectJS) does — and EQL ships as a single self-contained SQL surface plus first-class Rust and TypeScript packages generated from one catalog.This is a major, breaking release. Nothing in
eql_v2survives.The core change: typed columns instead of one opaque type
In v2 every encrypted column was
eql_v2_encrypted, regardless of what it held or how it could be searched. Capability was a property of the payload, discovered at query time.In v3 capability is a property of the type. A column is declared as the domain matching exactly what it supports, and the domain's
CHECKenforces that the payload actually carries the required index terms — so a missing term fails on insert or cast, not later at query time.Capability suffixes
c_eq=<>hm(HMAC-256)_ord=<><<=>>=,MIN/MAXop(CLLW-OPE)_ord_ope_ord(byte-identical twin)op_ord_ore_ordob(block-ORE)_match@><@bf(bloom filter)_searchhm+op+bf_search_orehm+ob+bfOn the integer-like families the order term is exact, so
_ord/_ord_oreget equality fromop/obdirectly. On text the order term is not equality-lossless, sotext_ordandtext_ord_oreadditionally carryhmand always route=/<>through the exact HMAC.51 column-type domains across 10 scalar families —
smallint,integer,bigint,real,double,numeric,date,timestamp(each: storage /_eq/_ord/_ord_ope/_ord_ore),text(adds_match,_search,_search_ore), andboolean(storage-only) — pluseql_v3_jsonandeql_v3_jsonb_entry.booleanis deliberately storage-only: a two-value column has so little cardinality that even an HMAC equality index would leak the plaintext distribution.Every operator also has a callable function equivalent (
eql_v3.eq,eql_v3.lt,eql_v3.contains, …). Supabase/PostgREST exposes the database through RPC that calls functions, not operators —WHERE col = $1isn't expressible there, buteql_v3.eq(col, $1)is. A CI gate (v3_operator_equivalents_tests.rs) fails if any supported operator loses its wrapper.Schema layout
Three namespaces, split along a deliberate line:
public— the column-type domains, prefixedeql_v3_. They live inpublicbecause dropping an EQL-owned schema must never drop application columns. The prefix means EQL types no longer shadow built-in names (integer,text,json), so an unqualified reference can't resolve to the wrong type throughsearch_path, and future EQL versions can coexist in one database.eql_v3— the public API: query-operand domains, index-term extractors (eq_term/ord_term/ord_term_ore/match_term), comparison wrappers, aggregates, the JSONB access surface,version(), andlints().eql_v3_internal— internal only: the SEM index-term types (hmac_256,bloom_filter,ope_cllw,ore_block_256), operator blockers, aggregate state functions, and CHECK validators. Hidden so they don't clutter type pickers that enumerate every type in every visible schema.EQL never grants permissions automatically. The installer issues no
GRANT/REVOKE; access is strictly opt-in. Seedocs/reference/permissions.md.Ordering: CLLW-OPE by default, block-ORE opt-in
The biggest operational change.
_ordis now backed by CLLW-OPE (opterm), not block-ORE.eql_v3.ord_term(col)returnseql_v3_internal.ope_cllw, a domain overbytea. Because it's a bytea domain it inherits the default btree operator class, so a plain functional index engages structurally, and the whole comparison chain stays inlinable:Block-ORE needs a hand-written operator class on a composite type, and
CREATE OPERATOR CLASSrequires superuser. On cloud-hosted Supabase and most managed Postgres that class silently didn't exist —CREATE INDEXsucceeded by bindingrecord_ops, then never engaged. Ordered queries fell back to seq scans with no error.So v3 does two things:
_ord/text_searchuse OPE.CHECK. First cast or insert raisesfeature_not_supported(0A000) naming the domain and pointing at the alternatives. The constraint isNOT VALID, so ORE data written under an earlier superuser install stays readable. Superuser installs are unchanged.Block-ORE remains fully supported behind
_ord_ore/text_search_ore, extracted viaeql_v3.ord_term_ore(col).Query-operand domains
Every term-bearing domain has a paired
eql_v3.query_<name>twin (40 of them) carrying{v, i, <terms>}— the storage envelope minus the ciphertextc, enforced by the domain CHECK.Query operands travel through PostgREST query strings, URL logs, proxies, and SQL logs. They should carry index terms only — never a decryptable ciphertext. A
c-bearing operand is rejected.Encrypted JSONB (SteVec)
public.eql_v3_jsondocuments are searchable without decryption: containment (@>,<@), path access (->,->>,jsonb_path_query,jsonb_array_*), entry equality on extracted leaves, and entry-level ordered range.Entries carry
hmXORop. Entry ordering extractseql_v3.ord_term(entry)— the same bytea-domain path as the scalars, so ordered JSONB indexes also work without superuser. The oldoc(CLLW-ORE) term, its composite type, comparator, operators, and operator class are removed;oc-bearing documents must be re-encrypted.Comparisons are leaf-level only. Root-document
=,<,>and every nativejsonboperator reachable through domain fallback (?,?|,@?,#>,||, …) raise rather than silently falling through to plaintext-jsonb semantics.Wire format:
v: 3Every v3 domain CHECK pins
VALUE->>'v' = '3'. The bindings accept exactly3and reject legacy2at the type boundary. A client emittingv: 2cannot insert into a v3 column.eql_bindings::from_v2/from_v2_queryconvert v2.3 payloads to v3, failing closed (MissingTerm,UnconvertibleOreTerm) rather than guessing — the target domain is explicit input, since every v2 index term is optional on the wire and capability can't be inferred.One catalog, three languages
Scalar domains are defined as rows in
eql-domains::CATALOG(Rust).cargo run -p eql-codegenmaterialises from it:eql-bindingscrate (payload structs,DomainPayload/QueryPayloadenums, the domain inventory),@cipherstash/eqlnpm package.Adding a scalar type is one catalog row. Drift between the three is CI-gated (
types:check). The previous Python codegen toolchain is gone — building and testing EQL no longer needs Python.Both language packages bundle the exact SQL installer they were generated against (
eql_bindings::sql, or the npm package's./sqlsubpath export), so consumers pin wire types and DDL together. Releases are cut in lockstep from Changesets; from 3.0.0 the changelog ispackages/eql/CHANGELOG.md.Removed
The
eql_v2schema and everything in it, with no v3 replacement for:eql_v2_encryptedand its operators (=,<>,LIKE/ILIKE,@>/<@, ORE comparisons)eql_v2_configuration,add_search_config,add_column,migrate_config,diff_config,create_encrypted_columnsencryptindexmigration machineryGROUP BY/grouped_valueon the encrypted column typeSearchable-encryption capability is provided by the v3 domain families. Configuration is now owned by the client.
Build variants are gone too: one artifact,
release/cipherstash-encrypt.sql(+ uninstaller), under the name the combined build used — existing install URLs keep working. The Supabase and Protect variants no longer exist because there is now a single surface that installs everywhere.Breaking changes
Full detail and migration recipes in
docs/upgrading/v3.0.md:v: 3eql_v3.query_<name>op, notoc)_ordis backed by CLLW-OPE — existing_ordindexes must be rebuilttext_searchis backed by CLLW-OPETwo behaviour changes ride along with U-005:
text_ordnow accepts the empty string (OPE encodes""to a well-formed term; block-ORE produced an unorderableob: []), andreal/double_ordcolumns now distinguish-0.0from+0.0. A float column needing IEEE±0.0semantics should be typed_ord_ore, which canonicalizes. Both splits are pinned byknown_failuremarkers against #387.Testing
18 SQLx suites, plus a property-test harness asserting SQL operator results agree with a plaintext oracle across a generated input space — a pure-Rust catalog suite, a fixture suite over committed real ciphertexts, and an e2e suite (behind the
proptest-e2efeature) that batch-encrypts fresh plaintexts through ZeroKMS every run.Dedicated gates cover the things that silently rot:
eql_v2-freeness of the artifact and its dependency closure, public/internal schema placement, operator↔function equivalence, uninstall completeness, privilege behaviour, and Rust/TS/JSON-Schema drift.Two performance regressions found during the release benches were fixed by converting hot functions the planner can never inline from
LANGUAGE sqlto plpgsql (#353, #354), restoring or beating v2 numbers on ORE ordered scans and the JSONB needle cast.Branch state
Brought up to date with
mainvia merge (6d7e87c9), not rebase. The only conflict was.github/workflows/release-plz.yml— resolved in favour ofeql_v3, which carries the full publish workflow; main's copy is the registration stub added in #383, whose own comment says to replace it when this branch lands. The merge changed no content: the resulting tree is byte-identical to the branch tip before it.A rebase was attempted first and abandoned.
--rebase-mergescannot reproduce the hand-made resolutions in 32 of this branch's 50 merge commits, and silently reverted part of theeql_v3_internalschema split across 166 commits.