Repository navigation
Conversation
|
Important Review skippedToo many files! This PR contains 848 files, which is 748 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files.
|
🦋 Changeset detectedLatest commit: f3e9f92 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
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 |
Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 436 | 0 | 203 | 0 |
cargo-mutants exited with no surviving mutants recorded. The run may not have completed cleanly; check the workflow run logs.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a05affaaa5
ℹ️ 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".
freshtonic
left a comment
There was a problem hiding this comment.
I request changes for two release problems on the crates.io side. The code itself is in good shape, and every check I ran passes.
How I reviewed this
The diff is large because generated code is committed. I reviewed the generators and their templates, then I ran the drift checks to prove that the committed output matches them. I did not read each generated file.
- EQL codegen:
eql-codegenrenders the version-neutral SQL source two times (eql_v3andeql_v4) from theeql-domainscatalogue. It also generates the Rust payload types andeql_gen.go. - Go:
stashgengenerates the*_stash.gofiles and the goldens.
Checks run (in a worktree at a05affaaa)
- EQL:
cargo test -p eql-codegen(includes the Go parity test): pass.mise run types:check,codegen:parity,test:schemas:parity: pass.mise run build, with the installer completeness gate and the v4 isolation gate: pass.git statuswas clean after all of these.
- Rust and Go:
cargo fmt --check: pass.cargo clippy --workspace --all-features --all-targets -D warnings: pass.cargo test -p stack-guest-abi -p stack-encrypt --all-features: pass.
- Go guests and SDK:
- All five WASI guests build, and the import-surface checks pass.
go vet ./...andgo test ./...inlanguages/golang: pass.go generate ./...leaves no diff.
- I built the v3 bundle and compared it with the
@cipherstash/eql@3.0.6tarball. Onlylints()and comments changed, so the claim "v3 SQL unchanged" holds. git merge-treeagainst currentmain: no conflicts.cargo publish --dry-run -p stack-encrypt-derive -p stack-encryptagainst crates.io: fails (see below).
Blocking
- stack-encrypt 0.3.0 cannot publish. The root release-plz line publishes every stack-* version that crates.io does not have, so merging this starts a publish of stack-encrypt 0.3.0. stack-encrypt requires
stack-kms0.1.0, and that is the only stack-kms version on crates.io. Butsrc/lib.rsre-exportsstack_kms::{diagnostic, ErrorPayload}, and these were added after the 0.1.0 release with no version bump. The packaged build fails withE0432: unresolved imports stack_kms::diagnostic, stack_kms::ErrorPayload.- Fix: bump stack-kms (for example to 0.2.0, with a CHANGELOG entry), and make stack-encrypt require that version.
- Note that the publish dry run on
main(tests-crates.yml) packages all five crates together, so stack-encrypt resolves the tree's stack-kms there. That step does not catch a stack-kms that crates.io lacks under the same version number.
- The stack-encrypt CHANGELOG has no 0.3.0 section.
Cargo.tomlsays 0.3.0, but about 850 lines, including a### Breakingsection, are still under## [Unreleased]. The root release-plz line only publishes and never rewrites the changelog, so 0.3.0 would ship under "Unreleased", as 0.1.0 did. Please rename the section to## [0.3.0] - <date>in this PR.
Non-blocking (see inline)
SchemaVersionis now generic. Rust does not apply a default const parameter in an expression, soSchemaVersion::CURRENTandSchemaVersion::default()stop compiling (E0284). The changeset ships this as a minor bump and says "plainSchemaVersionstill means v3".- The stored and single-bound query domain templates accept
"v": null. The range template already guards against this. - stack-encrypt will publish with an exact pin on
cllw-ore =0.5.2-rc.1. - There are some small nits: stale vitaminc version numbers in comments, a Go wipe path, an ADR reference, and the position of a
use.
Before you merge
eql-bindings now requires stack-encrypt 0.3.0. Its crates.io publish (publish_all_features) succeeds only after release-crates has published stack-encrypt 0.3.0, so blocker 1 must be fixed first. Otherwise the EQL crate publish fails too.
Checked and fine
- SQL renderer: it refuses literal
eql_v3/eql_v4names, unknown placeholders and unmatched blocks. - Range domains and
between: correct, with STRICT/IMMUTABLE accessors and an inlinablebetween. On non-superuser installs, the ORE range domains are disabled with the same NOT VALID CHECK as the single-bound ORE domains. - v4
ore_block_v2SQL: the layout offsets and the GF(2^128) doubling are correct, and errors do not echo the input bytes. - stack-guest-abi
stepandvalue: no new raw reads of host lengths, handles are never reused,MAX_OPENis enforced, and secrets areZeroizingwithOpaqueDebug. - Go step loop: every early exit after a pause aborts the operation and wipes the buffers.
maxPausedis tied to the Rust constant by a test. - Go public surface:
QueryTerm,VerifyandTargetLimitfollow the existingSeal/Openpattern, andMatchQuerynever prints its text. - CI: actions are pinned by SHA, both copies of the path filters were updated, and no release workflow changed.
- Changesets: the EQL changesets are minor, which is correct apart from the
SchemaVersionpoint.chacha20-0-10-2.mdmatches the lockfile moves. - Skills: no skill update is needed.
stash eql installstill installs v3 only, and no skill covers the Go SDK or Stack Encrypt.
| @@ -1,7 +1,7 @@ | |||
| [package] | |||
| name = "stack-encrypt" | |||
| description = "Encrypt Rust values under per-value ZeroKMS data keys via the vitaminc cipher traits" | |||
| version = "0.2.0" | |||
| version = "0.3.0" | |||
There was a problem hiding this comment.
Blocking. Merging this starts a crates.io publish of stack-encrypt 0.3.0. That publish fails. This crate requires stack-kms 0.1.0 (the only stack-kms on crates.io), but src/lib.rs re-exports stack_kms::{diagnostic, ErrorPayload}. These came after the 0.1.0 release, and the version was not bumped. cargo publish --dry-run -p stack-encrypt-derive -p stack-encrypt fails with E0432 at src/lib.rs:300.
Please bump stack-kms (for example to 0.2.0, with a CHANGELOG entry) and require that version here. Then release-plz publishes stack-kms first.
Also blocking: CHANGELOG.md still has these changes under ## [Unreleased]. The root release-plz line does not rewrite the changelog, so 0.3.0 would ship with an "Unreleased" heading. Please rename it to ## [0.3.0] - <date>.
There was a problem hiding this comment.
Fixed in #1403, commit 9590f83. The gap was wider than stack-kms. Run against crates.io on current next, cargo publish --dry-run -p stack-kms fails with stack_auth::diagnostic and stack_auth::tls unresolved, and -p stack-auth fails with stack_profile::diagnostic unresolved. Each crate changed after its 0.43.0 / 0.1.0 release with no version bump.
The commit:
- moves stack-auth and stack-profile to 0.44.0 (stack-auth's per-JWT token cache change is breaking) and stack-kms to 0.2.0, and makes stack-encrypt require stack-kms 0.2.0;
- adds a 0.44.0 CHANGELOG section for stack-auth and stack-profile, and a first CHANGELOG for stack-kms;
- renames stack-encrypt's
## [Unreleased]to## [0.3.0] - 2026-10-27; - updates the lockfiles that record these crates (root, both Go guests, EQL, stack-encrypt fuzz);
- adds an
@cipherstash/authpatch changeset, because that package is built from stack-auth.
cargo publish --dry-run -p stack-profile -p stack-auth -p stack-kms -p stack-encrypt-derive -p stack-encrypt now packages and verifies all five.
I'm leaving this open for @coderdan to confirm: this also publishes stack-auth and stack-profile 0.44.0, and the CHANGELOG dates assume the merge to main happens on 2026-10-27.
Not blocking. The points in this review are still open, but they need not stop the merge.
… index once The benchmarks of the derivable-token format (#1363) found every read that enumerates slower than the gated walk: whole subtrees by 38 to 46%, a 300-element array by 78%, json_tree_child_count on a small node by 59%, with a per-hit cost inside the walk far above the hashes. None of it was the format. The post-install step pins search_path on every function without the eql-inline-critical marker, and a pinned SQL function is never inlined, so each of the seven chain primitives ran as a full function call, about fourteen per level of the walk, 160 µs a level on the wide corpus. json_tree_hit_token could not inline even unpinned: a FROM clause, then STRICT with a CASE body, then arguments the planner had flattened into expensive expressions. The two maps copied every entry to build and were built twice by child_count. And every reference to the argument decompressed the whole document again. The seven primitives carry the marker. hit_token is FROM-less and not STRICT, and the walk derives a hit's token in the probe beneath it from the working table's columns. json_tree_index replaces json_tree_by_parent and json_tree_by_self: one pass, tag to position, set-returning so it inlines as an aggregate subquery computed once. Each read decompresses the document once into a MATERIALIZED CTE. No stored byte changes. Measured on the benchmark's own pass queries (500 documents per scenario, in-server time per document, interleaved, medians), the SQL as the PR first had it against the bundle built from the tree, on the same rows on one cluster, identical rows from both: read/first_name 0.10 ms -> 0.11 ms +8.9% read/company.name 1.27 ms -> 0.39 ms -69.5% read/user.contact.emergency_contact.name 1.27 ms -> 0.37 ms -70.5% read/orders[2] 4.26 ms -> 0.83 ms -80.5% read/orders[2].status 1.26 ms -> 0.37 ms -70.3% read/company 2.06 ms -> 0.49 ms -76.2% read/user 2.77 ms -> 0.59 ms -78.6% read/orders (fallback) 8.64 ms -> 0.84 ms -90.3% read/orders (walked) 24.03 ms -> 3.57 ms -85.1% read/$ (sealed root) 1.26 ms -> 0.39 ms -69.1% read/$ (root open) 28.21 ms -> 4.20 ms -85.1% wildcard/orders[*].total N=8 2.41 ms -> 0.60 ms -75.0% wildcard/orders[*].total N=64 1.79 ms -> 0.50 ms -71.9% wildcard/orders[*].total N=256 2.81 ms -> 0.66 ms -76.5% wildcard/orders[*].items[*].price N=8 3.57 ms -> 0.95 ms -73.3% wildcard/orders[*].items[*].price N=64 25.03 ms -> 4.08 ms -83.7% children/orders (nested) 1.35 ms -> 0.42 ms -68.6% child_count/orders (nested) 2.39 ms -> 0.31 ms -87.1% children/attrs 12.33 ms -> 5.12 ms -58.4% child_count/attrs 14.27 ms -> 1.20 ms -91.6% read/attrs (walked) 40.74 ms -> 7.22 ms -82.3% children/list 12.59 ms -> 5.08 ms -59.7% child_count/list 14.23 ms -> 1.21 ms -91.5% read/list (walked) 55.61 ms -> 8.22 ms -85.2% The oracle's tamper SQL reads the index's new shape. The Postgres suite (11 json_tree tests), documented-sql and the eight SQL tasks pass. The cipherstash/benches harness still names the two removed maps as floors and moves to json_tree_index before the next full run. Refs #1363 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
The cap on what one subtree read may send was set where nothing would ever reach it. A wildcard sends its bound plus one tokens under each node it sits below, so two levels at the default bound of 64 send 4,161, the largest read any measurement made. The server looks every token up in every row before it knows whether the budget is blown, so a wide read costs its tokens even when the whole document comes back instead, and the entries-times-tokens LIMIT on the walk scales with the cap. 8,192 admits every one-level wildcard up to 8,191 elements, two levels up to a bound of 90, the default included, and never a third; 1,024 was considered and would have refused the default two-level read. Owner's decision, 2026-10-25, recorded in the design doc with the reasons. The same note records the decision not to chain the sealed root's top-level entries from its token: a walk from the root is a capability over the whole document's shape, so it would add leakage, and it waits for a customer to ask. Refs #1363 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
tests/fixtures/json_tree_vectors.json is written by the pinned-vectors test under STACK_ENCRYPT_UPDATE_JSON_TREE_VECTORS=1, in serde_json's pretty form, and Biome formats every JSON file it is not told to skip: the Lint (Biome) job on #1363 failed with one error, the formatter wanting the fixture's one-element arrays on one line. Reformatting it would be undone by the next regeneration, so it joins the other generated fixtures the config already skips. Refs #1363 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
The crates job on #1363 failed on rustfmt alone: one assertion in the cap test fits on a line. No behaviour changes. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
proptest 1.12, from main's dependency bumps, moves to rand_core 0.10, where the trait that provides next_u32, next_u64 and fill_bytes is Rng and RngCore is a deprecated alias that no longer brings them into scope. The two leakage-test modules imported RngCore, so every job that compiles stack-encrypt's tests failed. Import Rng instead, and require proptest 1.12, the first release whose Rng provides those methods. Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
…1031 chore: merge main into next
…ains public.eql_v4_json_search and public.eql_v4_json_entry are domains over jsonb, so PostgreSQL's own jsonb functions accept them and run over the stored envelope: jsonb_path_match(col, ...) returns false for every row, with no error, and an envelope key such as v returns the envelope's field. EQL cannot make these calls raise. An overload taking the domain as its first argument is a second candidate for every call whose first argument is untyped, so installing EQL would make jsonb_typeof($1) and its kind ambiguous on plain jsonb across the whole database, and the native function still wins whenever any other argument is untyped. A base type of its own would close it, but only a superuser can create one. The JSON tree leakage reference now says so, and lists what to query with instead. The new tests pin it against a real sealed document: the jsonpath functions find nothing on a document or an entry, however their arguments are written; extraction reads the envelope's keys; the array functions raise natively; and with EQL installed, a plain-jsonb call with an untyped first argument still resolves to the native function, with no overload of a native jsonb function in public. Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
The stack-auth and stack-kms fuzz lockfiles had not been updated since those crates moved to vitaminc 0.8, so they recorded none of its 0.8 crates. cargo-fuzz has no --locked flag, so the regression job resolved the missing versions afresh on every run, built against whatever was newest (vitaminc 0.8.1 by then, where the workspace pins 0.8.0), and passed. Regenerate both lockfiles, with every crate the workspace lockfile also pins at the workspace's version, and add a fuzz-lockfiles job that resolves each packages/*/fuzz crate with cargo metadata --locked. It runs on pull requests, the nightly schedule and manual dispatch. Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
ci(fuzz): check the fuzz crates' lockfiles with --locked
…ockers docs(eql): say what native jsonb functions do on the v4 JSON tree domains
…ee document An equality and a range at two paths under one array could only be asked as `@>` plus an ordering over `json_tree_path_query`, which two different elements can answer: a superset of the plaintext jsonpath `exists($.a[*] ? (@.x == $v && @.y > $w))`. `json_tree_subtree` already computed `k`, the place in `tokens` of the token that found each row, and dropped it. `json_tree_subtree_indexed` returns it, and `json_tree_subtree` is now its projection, so the two reads have one implementation. A client expands a wildcard in element order to one bound, so reads of two fields of one array agree on `k` for one element. `eql_v4.json_tree_element_match(col, p eql_v4.query_json_elements)` takes one operand: per field, its read tokens (probe included) and its bounds as `query_json_leaf` operands. It opens the document once, finds the node each token names by its self tag (the node the subtree read returns at `d = 0`, with nothing beneath it read), pairs the members' nodes by `k`, and compares with the existing entry/leaf operators. A bound is checked by the same validator as the `query_json_leaf` CHECK. A token that names a node at another selector than its member's bounds is the probe's hit on an array longer than the bound, and raises `invalid_parameter_value` instead of answering over part of the array. The domain joins the catalog as the json_tree family's ElementsQuery role, so the eql-bindings inventory and the Go `eql` package pick it up from the generator (`eql.JSONSearchElementsQuery`). Refs #1440
…ment field
`element_match` takes `{"path", "wildcard_bound"?, "predicates"}`: an
array's elements by a path with a wildcard, and predicates on fields of
an element by a member path relative to it. Its operand is
`eql_v4.query_json_elements`: per predicate, the `subtree` tokens of the
member's full path at the bound, and the leaf operands the orderings
derive for its values. `eq` is one leaf as both `gte` and `lte`, so its
equality is the ordering term's (folded text, numbers as float64), and
`between` is `between_at`'s two. Each member's tokens are the subtree
key of its full path, so the k-th token of every member names a field of
one element, and the last the probe. No new derivation.
Refused before anything is derived: a path with no wildcard, a
`wildcard_bound` of 0, a position in the path or a member, a wildcard in
a member, an empty predicate list, an unknown `op`, a value no ordering
takes, and a member whose full path the field does not order. The operator is listed only on a
field that orders a path and whose type names the elements domain
(`TargetDescriptor::with_elements_query_sql_domain`), so a host that
does not name it, such as the Go guest today, sees no new operator.
The heap hygiene suite watches the operator too. Its spy now searches
for the data key where its lock holds it, and records the key in place:
it used to copy the key onto the stack to compare, on every free during a
watch, and a future built later at that depth carried the copy into its
box. On this branch that made the `json_open (subtree)` watch fail on the
spy's own copy, in `load_keysets`' future box (macOS arm64, debug); with
the copy gone the watch passes and `load_keysets` is unchanged.
Refs #1440
…PostgreSQL Seals contact lists with the JSON index, derives `element_match` through a data plan over eql-bindings' target table, and queries the column: two contacts each holding half of the predicate are returned by the superset query (`@>` and an ordering over `json_tree_path_query`) and not by `json_tree_element_match`; one contact holding both is returned by both; a folded spelling ties under the element predicate and not under exact containment; a bound below the longest array raises; and `json_tree_subtree` equals the indexed read without `k` under every budget, the sealed-root fallback at `k = 0`. The rest of the SQL's contract is pinned over the same documents and operands: the operand domain's CHECK member by member, over a derived operand and its parts; one element must satisfy every member under the sealed root and the open one; each bound, `between` and `eq` compare as the entry operators do, and a field with no ordering term or no field satisfies none; a probe's hit raises, including a token whose node was moved to the element's selector; and `k` is the place of the token whose node a row sits in. EQL tests run over real encrypted data, and the SQLx suite has no way to produce a JSON tree document, so these live here. Refs #1440
A filter compiler that targets `jsonb_path_match(col, $1, $2)` binds one predicate and its values. On `public.eql_v4_json_search` each question is its own operator, so the same filter had to be assembled per leaf. `eql_v4.query_json_predicate` is a boolean tree, one key per node (`and`, `or`, `not`, `contains`, `exists`, `cmp`, `elements`), whose leaves are the operands the family's operators already take. Its CHECK validates each leaf with that operand's own validator: the needle's, a new `json_tree_is_valid_bounds` that the element member's CHECK now calls too, and the elements predicate's. `eql_v4.json_tree_match` evaluates the tree with SQL three-valued logic, so it answers what the hand-written WHERE of those operators answers, NULL included, and `@@` on `(eql_v4_json_search, eql_v4.query_json_predicate)` is the operator form. The jsonpath `@@` blocker stays; an untyped operand now matches both and is refused as ambiguous rather than reaching native jsonb. The document is decompressed once per row and indexed once when the tree has an element leaf: the element match body moves into `json_tree_element_match_opened`, which both functions call. A `contains` leaf applies `json_tree_contains`'s rule to the projection read once. Children are read in order and the read stops at the first that decides the answer, so an element leaf the answer does not depend on cannot raise. The domain joins the catalog as the json_tree family's PredicateQuery role, so the eql-bindings v4 inventory and the Go type `eql.JSONSearchPredicateQuery` come from the generator. Refs #1441 Claude-Session: https://claude.ai/code/session_01MrxG724qYKcAWJdcABBFNY
…ent operand
`match` takes what `jsonb_path_match` takes, `{"predicate", "vars"?,
"wildcard_bound"?}`, and derives the `eql_v4.query_json_predicate`
operand. A hand-written parser reads the subset of SQL/JSON path
predicates the terms can answer: comparisons of a path (`$.a.b`,
`$.a[*].b`, `$."a b"`) with a variable or a literal, `exists(path)`,
`exists(path ? (@.x == $v && @.y > $w))`, `&&`, `||`, `!` and
parentheses. Everything else is refused at the byte it starts, as
`Error::Predicate`, naming the construct in the grammar's words and
never quoting the text, whose keys and values can be customer data.
Each comparison is compiled onto an existing operator's input and
derived by that operator's code: `==` to `eq_at`'s needle, `!=` to a
`selector` and not the needle, an ordering to a `selector` and the
ordering's leaf, so an absent path answers false as jsonpath has it; an
ordering at a wildcard and
a filter of more than one condition to `element_match`, a filter of one
equality to `eq_at`. No new derivation. `!=` at a wildcard, which asks
whether some element differs, is refused.
An ordering under an odd number of `!` also asks that its node is not a
JSON null, the `null` needle, since there jsonpath's false of a null and
the ordering's NULL pick different rows. Under no `!`, or an even number,
`and` and `or` cannot turn a child's NULL into true, so the needle would
change no row and only tell the database which rows hold null at the
path; it is not sent, and an ordering of a null answers NULL there.
New reasons: `json_predicate_syntax`, `json_predicate_unsupported`,
`json_predicate_unknown_variable`, `json_predicate_value_type`,
`json_predicate_not_equal_at_wildcard` and `json_predicate_too_deep`.
The operator is listed only on a type that names the domain
(`TargetDescriptor::with_predicate_query_sql_domain`), so a host that
does not, such as the Go guest today, sees no new operator.
Refs #1441
Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
A Go EQL value is the JSON its column stores: a stored value holds ciphertext and terms, and a query value holds terms. Until now each type was a bare []byte to fmt and slog, so %s or a log line wrote those bytes out. The compiled predicate Match returns (#1443) is the first operand a caller is likely to log whole, so every generated type now gets String, Format and LogValue, written by eql-codegen beside Value and Scan so no type can be missed. All three write one text, the type's name and [redacted] (or {} when empty); Format answers every verb, %#v included, so no GoString is generated. Value, MarshalJSON and a []byte or string conversion still give the JSON. The helpers (text, format, logValue) are hand-written in eql.go, so the generated methods stay one-line block bodies gofmt leaves alone. A codegen test checks every `type X []byte` the file declares has all three.
…ield A filter such as "a contact who is a sibling and was called more than five times" had no Go method. An EqAt and an ordering at the contacts' paths can each be answered by a different contact, so the application had to read the array back and re-apply the predicate itself. The engine and EQL already answer it: element_match derives one eql_v4.query_json_elements operand, and json_tree_element_match is true of a row where one element passes every predicate. This wires it into the Go SDK. The EQL guest's resolver now names the elements domain, so the engine lists element_match (input path_predicates) on a field that orders a path, and the checker and stashgen read the operator. The compiled predicate domain stays unnamed: match is #1443's. stashgen groups the tag's ordered paths by the array their last wildcard names (record.TargetOptions.ElementArrays) and writes one accessor per array under Elements, named as At names a path, with Match(ctx, cipher, opts, predicates...). An array with no ordered member beneath it gets none, and a root array ($[*]) is Root. The members are compile-time names. stashgen writes Paths (NPaths with -name), a tree nested as the JSON nests it, of encrypt.Path[E, T] values: Paths.Profile.Visits.Place. E is a type stashgen writes for each array, and T is the member's Go type, read from the document's struct as encoding/json reads it, or any where the type names none (a json.RawMessage, say). encrypt.AtPath(path) gives Eq, Lt, Lte, Gt, Gte and Between, each taking a T and returning an encrypt.ElementPredicate[E], and an array's Match takes only its own E. So a value of another type, or a predicate on another array's member, does not compile. Paths holds fields and no methods, so no JSON key is reserved. For a filter built at run time, the array accessor's Member("place") gives the same comparisons with any values. An array gets no accessor, and a notice naming its ordered paths, when its keys give no accessor name, when another array's keys give the same one, or when Paths cannot name its members: a key that gives no Go name on its own, or one name that would stand for two paths. A tag that generated before still generates, with its At unchanged. Match writes each value with gensupport.JSONText as the orderings do, and returns an eql.JSONSearchElementsQuery. The member check runs in the SDK, against the members the declaration orders beneath the array, before the guest is called: the engine's json_path_not_ordered names no member, so the first error a caller sees names the member, the array and the members it orders. It wraps ErrTerm, as the engine's refusal does; no predicates, a zero predicate, a zero Path and a member with a wildcard wrap ErrEncoding. Every other refusal is the engine's Diagnostic. A predicate prints and logs its member and comparison, never its values, and copies the bytes of a json.RawMessage or []byte value. Tests: the hermetic suite pins the operand against the subtree reads and the orderings of the same members, the run-time form against the typed one, every pre-engine refusal, and each engine refusal's sentinel and code through the guest. A type-checking test loads the generated testusers package with a probe file and holds the two compile errors, a value of another type and another array's predicate; the stashgen test does the same against a generated module, and pins the member types and every case that gets no accessor. The PostgreSQL suite (go:test:postgres) finds the row where one contact passes both predicates, by path and by name, and not the row where two contacts each pass one, which the EqAt and ordering superset returns, and raises SQLSTATE 22023 past the bound. The guest test now pins the elements domain named and match not listed. Closes #1442 Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
…meter An application that compiles a filter language into SQL targets jsonb_path_match(col, $1, $2): the predicate and its variables, two parameters. On an encrypted column the same filter was one Go call per leaf and a hand-assembled WHERE, one cast per parameter. The engine's match operator (#1441) compiles the predicate into one operand; this is the Go call on top (#1443). Fields.<Field>.Match(ctx, cipher, predicate, vars, opts) is generated on every JsonSearch field and returns eql.JSONSearchPredicateQuery, which binds as col @@ $1::eql_v4.query_json_predicate or eql_v4.json_tree_match(col, $1::...). The predicate crosses as text and vars as JSON text written by gensupport.JSONText, so Go adds no grammar: nil, or a value that writes null, sends no vars, and a Go string is refused as a Contains needle is. opts.WildcardBound bounds the filters that compile to the element match, as Elements.<Array>.Match takes it. The guest now names eql_v4.query_json_predicate, so the engine lists match (input predicate) on every JsonSearch field; the checker and stashgen read it, and stashgen refuses a document field the engine answers no match on. The guest maps Error::Predicate to STATUS_ENCODING: a refused predicate is the caller's input, not an internal failure. Diagnostic gains Construct and At, which read the construct and the byte offset the refusal carries. testusers.Entity grows the RC2 model's members (score, weight, address, country, preferred languages; $.weight ordered). The PostgreSQL test runs the seven filters of the customer's request and checks @@, json_tree_match, the per-operator query and jsonb_path_match over the plaintexts return the same rows; it joins go:test:postgres. Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
…stgreSQL Documents sealed by Stack Encrypt's JSON index, predicates compiled by `match` through a data plan over eql-bindings' target table, and `eql_v4.json_tree_match` and `@@` over the column against PostgreSQL's `jsonb_path_match` over the plaintexts: the same rows, and the same answer row by row save NULL where jsonpath says false of an ordering over a null that sends no `null` needle. And against the WHERE written from the operands each leaf's own operator derives, NULL included. The rest of the SQL's contract is pinned over the same documents and derived operands: the domain's CHECK node kind by node kind, over a compiled tree and its parts; for every predicate, the WHERE written from the compiled tree's own leaves; three-valued logic under `and`, `or` and `not`, and an ordering leaf alone; a `contains` leaf as `@>`, the root scalar's `alt` included; an `elements` leaf as the element match, a leaf the answer does not depend on left unread; and `@@` with a typed predicate, a blocked jsonpath, an untyped operand and NULLs. EQL tests run over real encrypted data, and the SQLx suite has no way to produce a JSON tree document, so these live here. Refs #1441 Claude-Session: https://claude.ai/code/session_01MrxG724qYKcAWJdcABBFNY
The stashgen README's "Encrypted JSON documents" table gets the Match row and its SQL, with when Elements and Paths are generated, how encrypt.AtPath makes a predicate from a path, what a path's type checks, the run-time Member form, what is refused before the engine, which arrays get no accessor, the folded equality and the 22023 bound. Its list of what stashgen writes, and -name, name Paths. The SDK README and package doc show the call, and the CHANGELOG records it under Unreleased. The typed-document example orders each visit's at and place, and finds the customer with one visit at home after 15, with the paths from Paths: Ada has a visit at home and a visit after 15, but no one visit that is both. Its generated file is regenerated from the new tag. Refs #1442 Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
The stashgen README's "Encrypted JSON documents" table gains a Match row with both SQL forms, and a section on the grammar, the refusals and what Diagnostic's Reason, Construct and At give, the four places the answer parts from jsonb_path_match, the 22023 bound, and the index the per-operator form engages that json_tree_match does not. encrypt/README.md and doc.go say the same in short. The typed-document example finds Bea by one predicate that combines an ordering with the visit filter. The Go module's CHANGELOG records Match, the Diagnostic accessors and the redacted EQL types (#1443).
…nt-match feat(eql): test equality and ranges on one array element of a json_tree document
feat(eql): one-parameter jsonpath predicates on json_tree documents (json_tree_match, @@, match)
feat(go): test several members of one array element of a JsonSearch field
feat(go): bind a jsonpath predicate on a JsonSearch field as one parameter
Limit.FetchOr returned any page it was given, so a page of 1<<63 or more reached database/sql as a uint64 it refuses, and PostgreSQL's LIMIT is a signed 64-bit value. It now cuts the page to math.MaxInt64, as Rust's Limit::fetch_or cuts it to MAX_ROWS. Stats.Rows suggested greatest(reltuples, 0). reltuples is -1 for a table never vacuumed or analysed, and 0 promises a fetch of exactly want, so the doc now says to read it with a reltuples >= 0 filter and to page when the count is unknown, as Stats::declared says in Rust. The PostgreSQL test analyses the table and reads it that way. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
decodeRequest refused such a request without wiping the bearer token in its headers or its body, and the step's wipe never reaches a request it did not append. It now wipes both, as every other refusal there does. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
The CHECK compared VALUE->>'v' with the bundle's version. With "v": null the comparison is NULL and a NULL CHECK passes, so the payload fitted both the eql_v3_* and the eql_v4_* domains. They now use the COALESCE form the range domains use. The coexistence test casts a null-version payload to a stored and a query domain of each version. The range templates name the ADR they cite as Stack Encrypt ADR-0009: packages/eql/docs/adr has no 0009. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
stack-encrypt 0.3.0 could not publish: it needs stack_kms::diagnostic and ErrorPayload, which the only stack-kms on crates.io (0.1.0) lacks. The same holds one level down: stack-kms needs stack_auth::diagnostic and tls, and stack-auth needs stack_profile::diagnostic, none of which the published 0.43.0 crates have. Each crate changed after its last release without a version bump, and the root release-plz line publishes a version only when crates.io lacks it. stack-auth and stack-profile move to 0.44.0 (stack-auth's OIDC token cache change is breaking), stack-kms to 0.2.0, and stack-encrypt requires it. Each gets a CHANGELOG section, stack-kms its first CHANGELOG, and stack-encrypt's Unreleased section becomes 0.3.0. The lockfiles that record these path crates move with them. @cipherstash/auth is built from stack-auth, so it carries a patch changeset. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
stack-encrypt's manifest no longer names the workspace vitaminc version in a comment, where it went stale, and stack-guest-abi's call tests import HostValue with their other imports. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
mise run build renders the null-version CHECK and the ADR citation into the committed per-family SQL under src/v3/scalars. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
cargo-lock-freshness holds every committed Cargo.lock to the versions the tree's manifests declare. Only the path crates' version lines move. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
fix: review findings on the next-to-main release candidate (#1229)
Summary
This is the integration branch for a release candidate of EQL v4 and of Stack Encrypt's Rust and Go support. EQL is the SQL that lets PostgreSQL store and search encrypted values; EQL v4 is a second copy of it for values written by Stack Encrypt, the Rust encryption engine behind the Go SDK. The open pull requests for both are merged here first, so they can be tested together before any of them reaches
main.The release version is not decided yet.
Why a branch, not main
Some of this work depends on vitaminc and ore.rs code that is not yet on crates.io, so it pins those crates to a git commit.
stack-encryptandeql-bindingsare published crates, andcargo publishdrops those git pins, so nothing on this branch can be released until vitaminc 0.6 is published (cipherstash/vitaminc#389, which waits on cipherstash/vitaminc#463). Once it is, the git pins become crates.io versions and this branch merges tomainas a normal release.What lands here, in order
feat/aead-remove-legacy-traitsto one commit on vitamincmain, and takes vitaminc's breaking changes since then.Rules for this branch
rev, neverbranch. A branch is deleted when its stack merges.packages/eqlandpackages/stack-encrypt/fuzz. Two commits mean two copies of vitaminc's traits, and the code stops compiling.Changes
Verification
nextwas created frommainat 2a70a47. Nothing has been built yet, because nothing has changed.Related