Repository navigation
feat(eql)!: EQL v4 text search queries carry only the term their operator reads; produce TextSearch - #1274
Conversation
🦋 Changeset detectedLatest commit: 213d998 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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 |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 0 | 0 | 2 | 0 |
Every mutant in the changed lines was caught by a test.
82ab168 to
31563a5
Compare
a77b70b to
ae2df35
Compare
coderdan
left a comment
There was a problem hiding this comment.
Review of ae2df35 against feat/eql-v4-ordered-ope-types. No blocking defect found; one design question and a few small points, ranked below. Inline comments carry the detail.
Findings (most severe first)
- Medium, design. A
TextSearchcolumn can never hold text shorter than one 3-gram ("","Al"), because the stored side reuses the match visitor's query-side refusal. So the headline case,WHERE name = 'Al', is now expressible but can never match a row, and a GoEncryptbatch fails on one empty name. The engine's own doctrine stores such text as the empty filter, and the SQLmatchesguard already keeps an empty filter from matching everything (verified below). Now that stored and query filters are built by different structs, the stored side could allow the empty filter. See the inline comment onv4_text_search.rs. - Low. "A missing term never reads as NULL" has two gaps: the helper only tests key presence, so
"hm": nullpasses the CHECK and compares as NULL (0 rows, no error); and the refusal is raised when the operand is evaluated, so under a generic plan a bound parameter whose qual is never reached (WHERE flag AND name = $1with noflagrow, or an empty table with no index) returns 0 rows with no error. Neither returns a wrong row; both are missed diagnostics. Inline onfunctions.sqland the ADR. - Low.
eql-bindingsis 3.0.6 andTextSearchQuery/TextSearchOreQuerychange public field types toOption, which is semver-breaking for a Rust consumer, while the CHANGELOG files it under Changed and the changeset isminor. Inline on the CHANGELOG. - Nit. The PR body says
stored_rows::every_frozen_row_opens_and_seals_as_it_didfails on this base; it passes here (682/682 under nextest) and in CI's crates job, so that paragraph is stale. - Nit. Heap hygiene runs the
TextSearchquery through the first index only (eq), so the order and match projections are not watched. Inline onheap_hygiene.rs.
Verified sound
- Built both bundles, from the base branch tip (
origin/feat/eql-v4-ordered-ope-types) and from this head:release/cipherstash-encrypt.sqland its uninstaller are byte-identical; the v4 diff is exactly the helper, the two relaxed CHECKs and the six extractor bodies. TheREQUIREedge added to the two query function files did not reorder the installer. - Installed the head v4 bundle on a scratch PostgreSQL 15.12 and exercised it: with hash/btree/gin indexes on
eq_term/ord_term/match_term,=,<,>=and@@all take an Index Scan (or Bitmap Index Scan) with a one-term literal operand and with a$1parameter underforce_generic_plan; EXPLAIN VERBOSE shows the extractor inlined ((name)::jsonb ->> 'hm' = require_query_term(...) ->> 'hm'), so the plpgsql helper does not block inlining.=,<>,<,>=,@@, both commutator forms,eql_v4.lt/eq/gte/matchesand the bare extractors all raise22023with the documented message when their term is absent, and the message never contains the operand's other term values. The stored domain still rejects a value missing any ofhm/op/bf; the v4 query domain rejects no-term,c-present, andv=3; the ORE twin rejects an emptyoband acceptshmalone. An emptybfquery matches 0 rows, not all.eql_v4.lints()is clean. Helper is IMMUTABLE STRICT PARALLEL SAFE; extractors stayLANGUAGE sqlSTRICT. - Renderer version blocks: directive parsing, nesting/unmatched/unclosed errors, literal-name refusal inside a skipped block, and the "other version unchanged" property are all tested; the template emits directives alone on a line so they are never read as placeholders.
- Codegen divergence points (
optional_term_keys,required_term,V3_SCALARS_BLOCKERrequire, projection rendering, dispatch arms,no_indexfall-through) match the hand-written SQL and the generated Rust;cargo test -p eql-codegen,-p eql-bindings --all-features,-p eql-encryption-testsandmise run types:checkpass locally. - Producer side: each projection derives exactly one term and converts into the twin;
queryrefuses an index the type lacks before deriving; the=/<query on two-letter text carrieshm/oponly and@@on it is refused.run_projectedzeroizes the plaintext asrun_targetdoes. stack-encrypt:mise x --env test -- cargo nextest run -p stack-encrypt --all-features682 passed; clippy-D warningsclean.TargetResolver::querychange is under Breaking in its CHANGELOG. Go:go test ./stashgen/...passes, thedirectorygolden case is registered and matches the generator;go generatedrift is checked in CI, which is fully green at this head.- ADR-0002 amendment matches the code. Skills do not mention the Go SDK or
stashgen, and the v3types.TextSearchguidance instash-encryptionis unaffected, so no skill change is owed. Only v3 JSON schemas and TS bindings are committed, so nothing is stale there.
Not run: the SQLx coexistence test (no Docker on this machine); CI's PG17 shards ran it and passed.
31563a5 to
f3ddc0f
Compare
ae2df35 to
e09295b
Compare
f3ddc0f to
238aee3
Compare
e09295b to
4e44e5b
Compare
coderdan
left a comment
There was a problem hiding this comment.
Re-review of e09295b against feat/eql-v4-ordered-ope-types at f3ddc0f. Range-diff against ae2df35 (old base c1bdf66): the base rewrite dropped the two ordered-OPE commits that used to ride under this PR; every change left in the range-diff is the author's own fix-up, and all four commits still apply cleanly on the new base. No blocking defect. Two low findings and one nit below; the five earlier findings are accounted for first.
Prior findings (review 5566064727)
- Medium, short text could not be stored in
TextSearch— fixed. The stored struct'sbfis the newv4::terms::SearchBloomFilter, whoseSearchMatchVisitorreturns the empty filter whereMatchVisitorrefuses;TextMatchand every query keepBloomFilter.stored_term_newtypein the generator emits it only for a v4 search domain's stored struct, with a test pinning each struct's field type and that v3 never names it. Verified end to end: the encryption-tests suite, the Postgres-backedv4_text_searchtest against a scratch PG 15.12 running the head v4 bundle (rows"Al"and""stored with"bf": [], found by=/</lt(), found by no@@needle), and the Go test. - Low,
"hm": nullread as NULL and the raise is at evaluation — fixed.require_query_termnow treats JSONnulland an empty array as missing; the helper doc, ADR amendment and changeset say "wherever the extractor reads the operand" and spell out the literal-vs-generic-plan difference. Verified on the head bundle:hm: nullon=,op: nullon<,bf: []andbf: nullon@@, oneql_v4.matches()and on the commutator form all raise 22023 naming the term; the ORE twin raises the same forhm: nullandbf: [], and its CHECK still refusesob: nullandob: [](23514). - Low,
Optionfields filed under Changed — fixed, and the decision to keepminoris sound. The entry is under a new### Breakingheading stating the field changes, and the@cipherstash/eql@3.0.6tag'scrates/eql-bindings/src/holdsv3/andfrom_v2/only, so no published crate has thev4module; the changeset says so. - Nit, stale
stored_rowsclaim in the PR body — partly fixed. The old paragraph is now labelled "Original verification", but "Review notes" still says CI is likely to show the frozen-rows failure in the crates and mutants jobs; both are green at this head. - Nit, heap hygiene watched
eqonly — fixed. The test reads the producible text types from the target table and runs each query through every index, which is how it surfaced the pre-existing OPE leak (see finding 2 below).
New findings (most severe first)
- Low. "No
@@query finds a stored empty filter" holds for query operands, but not for the stored-vs-stored forms, which this PR does not touch:matches(a, b)over twoeql_v4_text_searchvalues keeps the shared guarda @> b AND (cardinality(b) > 0 OR cardinality(a) = 0), so two short-text rows match each other. Inline onv4_text_search.rs. - Low. The new heap exclusion is a substring match on
stack_encrypt::sem::OpeVisitorover the whole backtrace, so every unwiped free beneathvisit_blockis excluded, not only the droppedStringclone, and the per-run count is printed but never asserted. Inline onheap_hygiene.rs. No issue is filed for the stack-encrypt leak itself yet; the exclusion comment would be the natural place to point at one. - Nit. PR body "Review notes", as in prior finding 4.
Verified sound at this head
- Built both bundles from the base tip and from this head with
mise run build:release/cipherstash-encrypt.sql, its uninstaller and the v4 uninstaller arecmp-identical; the v4 installer's diff is exactly the helper, the two relaxed CHECKs and the six extractor bodies. The committed bundles and release manifests are unchanged against the base. - On the head v4 bundle (PG 15.12): hash/btree/gin indexes on
eq_term/ord_term/match_termare used for=,<and@@with a one-term literal and with$1underforce_generic_plan; EXPLAIN VERBOSE showsrequire_query_terminline inside theIndex Cond, so the plpgsql helper still does not block inlining theLANGUAGE sqlextractors. A stored"bf": []is accepted by the domain CHECK; a non-empty@@operand never returns such a row;eql_v4.lints()is clean. SearchMatchVisitorfalls back toMatchVisitorfor non-empty positions, so the i16 bound check is kept;SearchBloomFilterdelegatesschema_name/schema_id/json_schematoBloomFilter, so the schema is one definition; no v4 TS export exists, and only v3 schemas are committed, sotypes:checkpasses with nothing stale.cargo test -p eql-codegen,-p eql-bindings --all-features,-p eql-encryption-tests(including the Postgres-backedv4_text_search,v4_typesandv4_ordertests against the scratch database, andheap_hygienewith--nocapture: exactly one excluded free per OPE derivation, none elsewhere),mise run types:check. No stack-encrypt source changed since ae2df35, and CI's crates, mutants, Go and PG17 shard jobs are all green at e09295b.- The
OpeVisitorleak claim checks out against the code:Orderable::encrypt_intakes&self, andOpeVisitor(T, OrderTarget)owns the plainStringclone that Owned-modeziphands it, so it drops unwiped at the end ofvisit_block.
Not run: the SQLx coexistence suite (credentialed fixtures); CI's PG17 shards ran it.
coderdan
left a comment
There was a problem hiding this comment.
Re-review of the update to 4e44e5b.
Compared with e09295b, only commit 2 changed. It removes the stack_encrypt::sem::OpeVisitor exclusion from packages/eql/tests/encryption/tests/heap_hygiene.rs, leaving only vitaminc's aws-lc seal, and updates the module doc to match. The other three commits are identical apart from the rebase.
This resolves the second LOW in review 5566371952. The exclusion matched on a whole-backtrace substring and its count was never asserted, so it could have absorbed other frees under visit_block. The underlying leak is now fixed at its source in #1235 (ba29e03): order visitors carry the plaintext as Carried<T> and wipe it on drop. The widened heap-hygiene test now holds every order term of TextOrd, TextOrdOpe and TextSearch to the no-residue rule. It still needs #1235's fix in this branch's base to pass, and CI is green at this head.
Still open from 5566371952:
- LOW: comparing two stored values with
@@/eql_v4.matchesmatches when both filters are empty. - Nit: the stale "Review notes" sentence in the PR body.
238aee3 to
68cec8c
Compare
4e44e5b to
4430c0c
Compare
68cec8c to
3e96092
Compare
4430c0c to
9048302
Compare
|
Restacked onto the new #1266 and force-pushed. The conflicts were the smallint additions; generated files were regenerated. The review fixes are doc-only (the |
3e96092 to
548c546
Compare
9048302 to
30d3a35
Compare
coderdan
left a comment
There was a problem hiding this comment.
Incremental re-review of the restack to 30d3a35 (onto #1266's 548c546). Range-diff against 4430c0c: commits 2 and 4 are identical; commits 1 (the SQL) and 3 (the bindings) changed.
Substantive change, response to review 5566371952.
- LOW, stored-vs-stored
@@matches two empty filters: addressed by documentation at this head, as agreed in the thread. The ADR-0002 amendment, the eql-bindings README and the CHANGELOG now scope "no@@finds a stored empty filter" to query operands, and say a@@between two stored values (a self-join, ormatches(col, <stored value as jsonb>)) keeps the shared guard, as in v3, and that no producer builds that predicate. The three SQL functions are unchanged. Closed. - The OPE heap-exclusion removal was already in at
4430c0c;heap_hygiene.rsis unchanged here.
Rebase adaptations. The stashgen README table, the Go README, doc.go and eql_test.go lists carry the base's smallint entries beside TextSearch; the eql-bindings README sentence on Date/Timestamp/Numeric follows the base's wording; the bindings.rs and targets.rs doc comments take the base's "no Rust plaintext type" phrasing; tests/targets.rs drops the smallint reason match. None of the smallint types belongs in this PR's text-search surface and none is misplaced. No fixture changed, and the v3 bundle is not in the diff. mise run types:check at this head: no drift.
Still open: nit, the PR body's "Review notes" still says CI is likely to show the frozen-rows failure in the crates and mutants jobs until the base is rebased; both have been green for several heads, this one included.
Checks (no credentials): CI green at this head. At the top of the stack (#1280's 2189d5a, which contains these four commits): cargo test --locked -p eql-codegen -p eql-bindings --all-features and -p eql-encryption-tests pass (v4_text_search 5 passed, 1 ignored without a database); mise run types:check no drift; cargo nextest run -p stack-encrypt --all-features 695 passed; go test ./stashgen/... ./cmd/stashgen/... ok; go vet clean.
548c546 to
17171df
Compare
30d3a35 to
61eb180
Compare
17171df to
bea3dc2
Compare
61eb180 to
422f4a1
Compare
422f4a1 to
bd9091a
Compare
bea3dc2 to
9ce4789
Compare
bd9091a to
92b0e1c
Compare
9ce4789 to
596d160
Compare
|
Rebased onto |
…perator reads
eql_v4.query_text_search required hm, op and bf on every query value. bf
is the bloom filter built from 3-grams, so text shorter than one 3-gram has
none, and `col = 'Al'` or `col < 'Al'` could not be written against an
eql_v4_text_search column. That is why Stack Encrypt could not produce the
type.
In v4 the search query domains (query_text_search and its _ore twin) now
require at least one of their terms. Each extractor over them reads its
term through eql_v4_internal.require_query_term, which raises
invalid_parameter_value naming the domain, the term and the operators that
compare it when the term is absent, so `=` on a value with no hm, `<` with
no op and `@@` with no bf are refused rather than reading NULL and
matching nothing. The extractors stay LANGUAGE sql, so the wrappers still
inline and the eq_term/ord_term/match_term indexes still engage.
v3 is cipherstash-client's format and does not change. The SQL source is
rendered once per version and had no way to say "only in v4", so the
renderer gains version blocks: lines between `-- {{#v3}}`/`-- {{/v3}}`
or `-- {{#v4}}`/`-- {{/v4}}` render for that version only, and the
directive lines for neither. The generator emits them for the two search
query domains, and the helper sits in a v4 block of
src/v3/scalars/functions.sql. release/cipherstash-encrypt.sql and its
uninstaller are byte-identical to the build before this change.
ADR-0002 gains a dated amendment recording the decision.
…s operator TargetResolver::query took a type name, a label and a value, so a resolver had to build the type's whole query twin, every term of it, for every operator. EQL v4's TextSearch needs to emit only the term its operator reads: a match term cannot be made from text shorter than one 3-gram, and `=` or `<` on such text must not be refused for want of one. query_term already knows the answering index (OpSpec::answered_by over the type's indexes). It now passes it through record::query to TargetResolver::query as an &IndexSpec after the type's name. A resolver whose twins carry every term ignores it. eql-bindings' encryption::targets::query takes it the same way and refuses an index the type does not carry (TargetError::NoIndex) before deriving anything. The Go guest forwards it; an index the type lacks can only come from an engine bug, so the guest reports it as internal. BREAKING CHANGE: TargetResolver::query and dynamic::record::query take an &IndexSpec; eql_bindings::encryption::targets::query takes one after the name.
…perator's term TextSearch was unproducible because its query twin carried hm, op and bf on every query, and text shorter than one 3-gram has no bf: `=` or `<` on a two-letter name would have been refused. EQL v4's search query domain now takes any one term, so the twin can carry just the one its operator reads. In v4, TextSearchQuery (and the unproducible TextSearchOreQuery) hold each term as an Option, skipped in the JSON when None. The generator emits one projection per term, TextSearchEqualityQuery, TextSearchOrderQuery and TextSearchMatchQuery, each deriving EncryptFrom<String> over the envelope and its one term and converting into TextSearchQuery. The by-name query dispatch, which now receives the answering index, runs the projection for that index, so a match term is derived only for `matches`. The twin itself carries no derive. The stored TextSearch carries all three terms, the same bytes TextEq, TextOrd and TextMatch store, so text too short for a 3-gram is refused on write with EmptyTermText, exactly as TextMatch refuses it: a stored row with an empty filter would be one no match query finds. TextSearch joins ENCRYPTION_DOMAINS, the regenerated target table marks it producible, and the Go encrypt/eql table follows. The engine answers eq, lt, lte, gt, gte and matches on it. Tests: each index's query carries its term alone and equal to the stored one; "Al" is queried by eq and ope and refused for match; the typed projections serialize to what the dispatch writes; and, against an EQL v4 database, `=`, `<>`, `<`, `>=`, `@@` and the function forms find the plaintexts' rows, a one-term operand is a query_text_search and a termless one is not, and each operator and extractor refuses an operand missing its term with invalid_parameter_value naming the term and never the operand.
…ng one term A directory Entry model holds one TextSearch field. Generated over the deterministic EQL guest, its stored value carries bf, c, hm, i, op and v and opens back; Eq, Lt and Gte return a query with only hm or op, the stored term, for "Alice" and for the two-letter "Al"; Matches returns one with only bf, which Verify checks; and Matches on "Al", like storing "Qz" or "", is refused with ErrTerm without quoting the value. stashgen gets a golden case for the same field over its static engine, which now lists TextSearch with its three indexes, so the generated methods (Eq, Lt, Lte, Gt, Gte, Matches) are pinned and compile against the stub SDK.
92b0e1c to
213d998
Compare
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: 213d998b3a
ℹ️ 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".
| -- {{#v4}} | ||
| AND (VALUE ? 'hm' OR VALUE ? 'ob' OR VALUE ? 'bf') | ||
| -- {{/v4}} |
There was a problem hiding this comment.
Add an upgrade note for the payload change
This changes which terms query_text_search_ore requires and introduces new runtime errors when an operator reads an omitted term, but the commit adds neither a numbered upgrade note under docs/upgrading/ nor a changeset cross-link. Callers that construct query payloads manually need the compatibility, verification, and rollback guidance before adopting this release.
AGENTS.md reference: packages/eql/AGENTS.md:L299-L306
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Carried in #1280, since that PR changes an eql_v4 payload too (the block ORE term layout) and the two notes belong in one file: packages/eql/docs/upgrading/v3.1.md at 45a79a0, U-001 for this change (which terms a text search query must carry, the invalid_parameter_value raise and where it fires, what a hand-built payload must do, verification, rollback) and U-002 for the block ORE scheme. Both changesets cross-link their note.
|
Rebased onto
Verified at the tip: every |
Summary
EQL is the SQL that lets PostgreSQL search encrypted columns. A column stores an encrypted value plus "index terms": an equality term (
hm) for=, an order term (op) for<andORDER BY, and a bloom filter (bf) for fuzzy text matching (@@). A query sends a matching "query value" carrying terms but no ciphertext.TextSearchis the EQL type that does all three in one column. Until now EQL v4 (the version Stack Encrypt and the Go SDK write) required every query value to carry all three terms. The bloom filter is built from 3-letter pieces of the text, so a name likeAlhas none, andWHERE name = 'Al'could not be written. That is why the Go generator (stashgen) refusedTextSearchfields.This PR lets an EQL v4 text search query carry only the term its operator needs, and makes
TextSearchproducible from Rust and Go. EQL v3 (cipherstash-client's format) is untouched: its SQL bundle is byte-for-byte the same.Changes
EQL SQL (v4 only)
eql_v4.query_text_searchandeql_v4.query_text_search_orenow require at least one of their terms instead of all of them (plus the existingv,i, noc, version checks; a presentobis still a non-empty array).eq_term,ord_term,ord_term_ore,match_term, so every operator and function form) reads its term through a neweql_v4_internal.require_query_term, which raisesinvalid_parameter_valuenaming the domain, the missing term and the operators that compare it. It never echoes the query value. A term that is JSONnullor an empty array counts as missing. It raises wherever the extractor reads the operand (a literal folds at plan time; a generic plan whose predicate is never reached returns no row and no error), instead of reading as NULL (silently no rows) or as an empty filter. The extractors stayLANGUAGE sql, so indexes still engage (checked withEXPLAIN).eql_v3andeql_v4gains version blocks, lines between-- {{#v3}}/-- {{/v3}}or-- {{#v4}}/-- {{/v4}}. A block is kept for its own version and dropped from the other, and the directive lines are dropped from both. There was no mechanism for this before; this is the smallest one that keeps a single source. Malformed blocks (nested, unmatched, unclosed) fail the build. The generator emits blocks for the two search query domains, and the helper sits in a v4 block ofsrc/v3/scalars/functions.sql.packages/eql/AGENTS.mddocuments version blocks.Stack Encrypt (
stack-encrypt, breaking)TargetResolver::queryanddynamic::record::querynow receive the index that answers the query's operator (&IndexSpec), whichquery_termalready knew. A type whose query carries every term ignores it.eql-bindings
TextSearchQueryandTextSearchOreQueryhold each term as anOption, skipped in JSON whenNone.TextSearchEqualityQuery,TextSearchOrderQuery,TextSearchMatchQueryeach derive only their one term and convert intoTextSearchQuery. The by-name dispatch (encryption::targets::query, which now takes the index and refuses one the type lacks withTargetError::NoIndex) runs the projection for that index.TextSearchis producible. A stored value carries all three terms. Decision: short text is stored. Text too short for a 3-gram ("","Al") gets the empty filter"bf": []:=and<find it, no@@query does. Itsbffield is a newv4::terms::SearchBloomFilter(same JSON and JSON Schema asBloomFilter), which takes the empty filter;TextMatchand every query keepBloomFilterand keep refusing such text. The stored domain's CHECK acceptsbf: [], andmatchescannot find an empty stored filter because a query's filter is never empty (the projection refuses short text and the SQL refuses"bf": []).TextSearchQuery/TextSearchOreQuery'sOptionterms are listed under Breaking in the crate CHANGELOG; no releasedeql-bindingshas thev4module (3.0.6 does not), and the changeset staysminor.Go
encrypt/eqltable:TextSearchproducible; the guest forwards the index. ATextSearchfield getsEq,Lt,Lte,Gt,GteandMatches. An empty or two-letter value is stored (empty filter);Matcheson it isErrTerm.Verification
Re-run after the review fixes, rebased on
feat/eql-v4-ordered-ope-typesatf3ddc0fe1:cmp-identical to the base's.mise run test:encryption:postgrespasses on a fresh v4 database and on one with v3 and v4 installed (PostgreSQL from mise): short text stored and found by=/<, never by@@;"hm": nulland"bf": []operands refused with 22023.heap_hygienenow watches every producible text type through every index. It found a pre-existing unwiped free of the plaintext in stack-encrypt's OPE derivation (sem::OpeVisitor::visit_block) on everyopterm; excluded by backtrace like the aws-lc one, counted and printed, not fixed here.cargo metadata --lockedclean at every commit (root,packages/eql, both Go guests, stack-encrypt fuzz). eql-bindings/encryption/codegen tests,types:check,codegen:parity, fmt, clippy-D warnings(EQL workspace minus the credentialed sqlx crate), rustdoc-D warnings,mise run doc, guests,wasm:guest:test(68 + 71),go:test,go:lint,go generateclean. Not re-run: the SQLx suite (needs credentialed fixtures), root-workspace nextest and mutants (no root-workspace code changed by the fixes).Original verification:
Run locally on macOS (PostgreSQL 17.5 from mise, no Docker):
release/cipherstash-encrypt.sqland its uninstaller are byte-identical (cmp) to the build before this PR. Only the v4 bundle changes.packages/eql/tests/encryption/tests/v4_text_search.rs(real Stack Encrypt values, fake KMS), run withmise run test:encryption:postgresagainst a fresh v4 database, and again with v3 and v4 both installed: queries with onlyhm, onlyopor onlybfare accepted and one with none is refused (23514).=,<>,<,>=,@@andeql_v4.lt(...)find the rows the plaintexts say, two-letter text included on=/</>. Every operator and extractor refuses an operand missing its term with22023, naming the term and not quoting the operand. The ORE twin follows the same CHECK rule. All four suites of that task passed.a_v4_search_query_needs_one_term_and_a_v3_one_needs_all(v3 still needs all three terms; v4 needs one; each operator refuses a missing term). Run locally only against hand-made fixture rows: the real fixtures need CipherStash credentials I don't have, so CI is its first run against real cipherstash-client terms.eql_v4.lints()is clean on the new bundle.cargo test --locked -p eql-bindings --all-features,eql-encryption-tests(incl. heap hygiene),eql-codegen;mise run types:checkandmise run codegen:parityclean; clippy-D warningsand fmt on both workspaces;mise run docandmise run test:docclean.mise x --env test -- cargo nextest run --workspace --all-features: 1220 passed, 1 failed:stack_encrypt::stored_rows::every_frozen_row_opens_and_seals_as_it_did. It fails the same way on this PR's base,feat/eql-v4-ordered-ope-typesat82ab168e6, without these commits: the base's order-term commits are older than the ones on its own base branch, where this test passes in CI. Not regenerated (it's a frozen fixture).cargo mutants --in-diff): 2 mutants, both unviable, none survived. Run with the frozen-rows test filtered out of the baseline because of the base failure above.wasm:guest:build,:eql, both:deterministic),mise run wasm:guest:test(68 + 71 pass),mise run go:test(all packages ok),mise run go:lint(0 issues),go generate ./...produces no drift. New Go testTestV4TextSearchStoresEveryTermAndQueriesCarryOneand astashgengolden casedirectory.Related
Refs #1131
Review notes
packages/eql/crates/eql-codegen/src/render.rs(version blocks), then the template changes intemplates/query_types.sql.j2andtemplates/functions/extractor.sql.j2, thensrc/v3/scalars/functions.sql.TextSearchOrestays unproducible for its own reason (block ORE). Only its SQL CHECK and struct shape change.