Repository navigation
Conversation
…nswers, so a generator holds no copy of the table dynamic::field_operators lists the operators a field of a data plan answers, each with the index that answers it: for a field declared with indexes, what OpSpec::answered_by finds under its kind; for a field that names an EQL type, what the type's catalog entry lists. query_term reads the same table, so what a generator is told a field answers is what a query on it finds. operator_to_value writes one entry in the catalog's wire form, which TargetDescriptor::to_value now shares. Part of #1189
…ers, with Verify and TargetLimit on a matches query stashgen asks the engine which operators each sealed field answers, through a new guest export, se_operators, and writes one method for each: Eq, Lt, Lte, Gt, Gte, Between and Matches, each taking the field's own Go type. A field has no method for an operator it does not answer, so such a query does not compile. The generator holds no copy of which index answers which operator; the static test engine is held to the guest by a test that generates the same declarations through both and compares the files. Eq returns an EqualityTerm (or, on an encrypt_into field, the EQL type's query value), an ordering an OreTerm or OpeTerm, and Between an encrypt.Range of the two bounds' terms. Matches returns an encrypt.MatchQuery, whose answer can hold false positives, so it carries Verify, one guest call for a page of decrypted values, and TargetLimit. Every query type implements driver.Valuer. MatchQuery keeps the text it was derived from, because Verify cuts its tokens again in the engine; it never prints it. se_query, Cipher.Query and gensupport's per-index term methods are gone: eq on a target field is its twin, and a query names an operator. se_term stays, now behind the unexported Cipher.derive, because a record call's term failure does not say which field it came from and the host asks se_term again to name it. The order tests read the terms Encrypt stores, since a query method reaches only the index that answers its operator. Part of #1189
…SQL, through database/sql and pgx The test stores generated rows in separate columns, finds one by its equality term, and runs a matches query whose filter containment PostgreSQL tests with @>, then decrypts the rows it returned and verifies them: the verified rows are the Rust query fixture's. It uses the deterministic guest's keys and skips unless DATABASE_URL is set; the Linux Go job now starts PostgreSQL 17 and sets it. pgx is a test-only import. Part of #1189
…decrypt-and-verify step, and say se_query is gone The README and the package documentation describe one method per operator, what each returns, why a matches query carries Verify and TargetLimit and an exact one neither, and that a MatchQuery holds its text because Go cannot wipe a string. The ADR-0007 amendment records se_operators, the removal of se_query, and the one reason se_term stays. Part of #1189
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 4 | 0 | 6 | 0 |
Every mutant in the changed lines was caught by a test.
Summary
Stacked on #1215. This is the Go SDK half of ADR-0009 (appendix B): the generated code names an operator, not an index. PR 4 of 4, the top of the chain.
Fieldsentry hasEq,Lt,Lte,Gt,Gte,BetweenorMatches, as the engine says the field answers, each taking the field's own Go type (Fields.Age.Lt(ctx, cipher, uint32(40)),Fields.Age.Between(ctx, cipher, lo, hi)). A field has no method for an operator it does not answer, so the query does not compile.Queryand the per-indexEquality,Match,Ore,Opemethods are gone.dynamic::field_operators(&plan, field, &resolver)andse_operators(plan, field), read in Go byChecker.Operators. They return the same{operator, fidelity, arity, index}objects as the catalog, from the same table thatquery_termreads, so the generator is told what a query on the field finds. stashgen asks for every sealed field,encrypt_intofields included, and holds no copy of the rules.enginetest.Staticis still a stand-in. A test now generates the same declarations through it and through the guest and checks that the files match.Eqreturns anencrypt.EqualityTerm, or the EQL type's query value (eql.TextEqQuery) on anencrypt_intofield. The orderings return anencrypt.OreTerm, or anencrypt.OpeTermwhen the field declares no ORE.Betweenreturns anencrypt.Range[T]with the two bounds' terms.Matchesreturns anencrypt.MatchQuery[T]. Every one implementsdriver.Valuer; aRangebinds as two parameters, each bound aValuer.VerifyandTargetLimit, and an exact one carries neither.MatchQuery.Verify(ctx, values []T)checks a page of decrypted values in one guest call.MatchQuery.TargetLimit(ctx, want, stats)sizes the fetch. Both use no key. The generator only writesMatchQueryfor an operator the engine reports as probabilistic. If the engine ever reports a combination the SDK has no Go type for (a probabilistic operator on an EQL type, for example), stashgen refuses the field and names it, rather than leaving a method out.MatchQueryholds its text.Verifycuts the query's tokens again in the engine, so the query keeps the text it was derived from. Go cannot wipe a string, so the text stays in memory until the garbage collector reclaims it. The README and the package docs say so. AMatchQuerynever prints the text:Format,String,GoStringandLogValueshow only the field and the size of the term, and a test checks everyfmtverb and bothsloghandlers.se_queryis removed. On a target field,eqgives the twin.Cipher.Query, the export, its guest ops and its tests are gone.se_termstays. Generated code no longer uses it, andCipher.Deriveis now the unexportedderive. One caller still needs it: when a record call fails on a term, the error does not say which row and field caused it, andlocateTermFailureasksse_termagain, one term at a time, to find them.se_query_termcannot do this job. It reaches only the index that answers an operator (ORE, when a field declares ORE and OPE), and it refuses text with no token, which storage accepts. The ADR-0007 amendment records this.encrypt/internal/testusers(plus a newProfiletype whose declaration matches the Rust query fixture),testpolicy,example, the stashgen goldens and the stub SDK. The order property tests now read the ORE and OPE terms thatEncryptstores, because a query method reaches only one index, and only on integers.The width rules hold because each method takes the field's Go type:
Lton auint32field takes auint32, andMatchesontype Email stringreturns aMatchQuery[Email]whoseVerifytakes[]Email.Behaviour to note
OpSpec::answered_byorders integers only, as the typedop::Ordereddoes). Such a field still stores its term, but no longer gets anOreorOpemethod.expected_tokensprior. Without a measured histogram,TargetLimitis unbounded andLimit.FetchOr(page)pages. The README says so.match_positions(bytea) → int[]SQL function and@>for the containment test. This is what the PostgreSQL test runs.What waits
Matches, orBetween, on an EQL type needs the EQL v4 types (EQL bindings: generate the v4 Rust types, and derive Stack Encrypt's traits on v4 only #1179). Today the engine produces onlyTextEq, which answerseq. stashgen is ready for more operators: when the catalog lists them, an exact one gets a method automatically, while a probabilistic one is refused until an EQL query type carriesVerify.se_termgoes once the record exports name the field behind a term failure themselves.database/sqlwith the pgx driver only, enough to run theMatchesquery.Tests
dynamic::query): every operator a field is said to answer derives a term through that index, and every other operator is refused asoperator_not_answered, for equality, match, ORE+OPE and text-with-ORE fields. Also covered: a missing field, the wire form of an operator, and a target field answering what its type lists.ops::operatorsfor each field of the record plan, with every listed operator validating and every other refused; a missing field and a malformed plan; in theeqlbuild, aTextEqfield answerseq. The in-crateTextEqround trip now queries throughquery_termwitheq.Checker.Operatorsfor each field of the fixture plan.Profile's generatedEq,Lt,Lte,Gt,Gte,BetweenandMatchesagainst the Rust query fixture's terms.MatchQuery.Verifyagainst the fixture's verified page, andMatchQuery.TargetLimitagainst all six fixture limits.MatchQueryrefuses both methods.QueryTermrefusals before state.encrypt/postgres_test.go): the test writesProfilerows throughdatabase/sqland pgx, finds one row byEq, then runsMatcheswithTargetLimitand@>, decrypts the rows, and checks thatVerifykeeps exactly the fixture's verified names and that the server returned every true match. It skips unlessDATABASE_URLis set. The Linux Go job now startspostgres:17and sets it.Test plan
cargo fmt --all --check,cargo clippy --workspace --all-targets --all-features -- -D warnings(Rust 1.94.1).cargo nextest run --workspace --all-features: 1159 passed.mise run test:doc,mise run doc.mise run wasm:guest:test(fmt, clippy on host andwasm32-wasip1for all four feature sets, rustdoc, nextest with and withouteql).mise run wasm:guest:build,:eql,:deterministic,:eql:deterministic,wasm:auth-guest:build, thenscripts/go-binding-test.sh languages/golang(gofmt, vet, tests; the 386 run is Linux only) andmise run go:lint.go generate ./...inlanguages/golangleaves the tree clean.DATABASE_URLset).--in-diffover stack-auth and stack-encrypt: 10 mutants, 4 caught, 6 unviable, none missed.vitest --config scripts/vitest.config.mjs): 74 of 75 files pass. The other file,check-auth-npm-changeset, fails to import@changesets/parsefrom this worktree's linkednode_modules, and is not touched here.Every checkbox of #1189 is done:
VerifyandTargetLimiton probabilistic queries only, each query keepingdriver.Valuerdoc.goMatchestest, which runs locally and in the Linux Go job once CI runsCloses #1189