Repository navigation
Conversation
🦋 Changeset detectedLatest commit: 12f9e83 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 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 |
|---|---|---|---|
| 62 | 0 | 32 | 0 |
Every mutant in the changed lines was caught by a test.
1701131 to
7938f8f
Compare
|
Rebased onto #1327, which now takes clww-ore 0.6.0 and ore-rs 0.10.0 from crates.io instead of git. The commits on this branch are unchanged apart from their hashes. Rerun after the rebase: root nextest (1267 passed, 1 skipped), clippy, the EQL crate tests, all four Go guests and |
|
Two pushes since the last note:
|
7601d3f to
368f303
Compare
|
Rebased onto #1327 again, after its review changes: stack-encrypt now re-exports vitaminc for bindings, and the Go guest takes vitaminc only through those re-exports. There was one conflict, in the stack-encrypt CHANGELOG, and both entries are kept. Rerun after the rebase: root nextest (1267 passed, 1 skipped), clippy, |
368f303 to
0deb2b1
Compare
A plan field may now carry "null": "sealed" or "sql" (#1298), so an optional value can be encrypted without a sentinel. - "sealed" seals a null like a value, under the field's label, as vitaminc 0.8's typed null of the field's type, so its ciphertext is as long as a present value's and the stored record does not show which rows are absent. It opens as a null. The field must declare a type of fixed width (a bool's plain null is already as long as its value) and derive no term, since a null has none yet. - "sql" stores nothing for a null, or for a row with no entry for the field: the stored record has no entry, so the host writes SQL NULL. A stored record with no entry for the field opens it as a null. Rows of a batch are run by one lowered plan per set of fields they leave out, and the groups' requests are merged into the one call. A field with no "null" behaves as before: every row must have it, and a typed field refuses a null. Refs #1298. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
…a value A storage-only EQL v4 type (Date, Integer, Text, ...) carries no term, so an absent value has nothing that would show it but the length of `c`. Each now also derives EncryptFrom and DecryptInto for an Option of its plaintext, and the by-name dispatch the Go guest runs reads and opens a storage-only type through that Option: a value seals exactly the leaf the plaintext seals (Some walks the value itself), and a null seals as vitaminc 0.8's typed null, as long as a value, and opens as a null. A type with a term still refuses a null. eql-bindings' own Date, Timestamp and Decimal plaintexts declare their fixed width to vitaminc, so their None is padded too. The codegen emits the second plaintext attribute and the Option dispatch; the generated files are regenerated with `mise run types:generate`. Whether a field may hold a null is its plan's to say (#1298): the engine refuses a null for a target field without "null". Refs #1298. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
A sealed field may now be declared over a pointer to one scalar (`DOB *encrypt.Date` with `stash:"dob,encrypt_into=Date"`, which #1293 reproduced being refused as a composite), and Decrypt gives nil back for a nil (#1298). A new tag part, `null=sealed` or `null=sql`, says how nil is stored, and reaches the engine as the data plan's "null": - sealed: nil is sealed as a typed null as long as a value, so the column does not show which rows are absent. stashgen picks it for a field with no index whose type has a fixed width, sealed with encrypt or into a storage-only EQL type. - sql: nothing is stored, so the field's columns hold SQL NULL and its member in the generated type is a pointer. A field with an index, or of text or bytes, has to say so: a null has no term yet, and text shows its length, so stashgen will not choose a visible NULL silently. gensupport gains Declaration.Null, Deref and GetPtr; the record model and the encrypt record path carry the mode and accept a SQLNull field left out. A stored null read into a field that is not a pointer is a Decrypt error, from the engine's kind check. -model does not map a null=sql field yet, and stashgen says so. The reproduction from #1293 is folded into cmd/stashgen's tests, with the guest-built skip the other engine test has. Closes #1293. Refs #1298. Co-authored-by: Jonathon Walker <1989282+jonathon-walker@users.noreply.github.com> Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
The domain check of eql_v4_date takes a value whose c is a typed null, as it takes a date, and it reads back and opens as None (#1298). Runs in mise run test:encryption:postgres with the file's other database tests. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
…y through a nullable plan The two survivors of cargo mutants --in-diff on the null modes (#1298): no test refused "null": "sealed" on an EQL type with a term, and none read a stored null through a plan with no "null", where the field's kind check must refuse it rather than skip. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
vitaminc-normalize 0.8 derives an equality term for a null: one term per context, shared by every null, typed or plain. #1327 dropped it to keep the old behaviour; this keeps it (#1298). - stack-encrypt: `EqualityTerm` of a `None`, and `IndexSpec::Equality` on a null, now derive that term. No other index has one. - Data plans: `"null": "sealed"` takes a field whose only index is equality, on the field or on the EQL type it names. The null is sealed as long as a value and stores the null term beside it, so the absent rows are one equality group, as any repeated value is, and the column holds no SQL NULL. An `eq` query whose input is a null derives that term (`query_term`, `record::query`); a null query of any other field or operator is the new `Reason::NullQuery`. - eql-bindings: a type whose only term is equality's (`DateEq`, `IntegerEq`, ...) and its query twin take an `Option` of the plaintext, as storage-only types do, and the by-name dispatch encrypts, opens and queries them through it. - Go: stashgen defaults to `null=sealed` for a fixed-width field whose only index is equality, and generates `EqNull`, the `eq` query of nil, through the new `gensupport.Field.NullTerm`. Verified in PostgreSQL: `=` with the query of a null finds exactly the rows of a DateEq column whose value was a sealed null. Refs #1298. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
…d equality The three survivors of cargo mutants on the null term: record::query's refusal of a null was reached by no test, since query_term refuses the same input before it. Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
…ed-width kind The length test covered a date, a float64 and a bool, so half of `ValueKind::sealed_null`'s arms ran in no test and the CRAP gate failed on it (CC 18, 47.6% covered). It now seals a value and a null of every kind the function maps, each integer and float width, a decimal, a date and a timestamp, and checks each pair is the same length and opens back. A decimal given in 64 bits is included: its null is padded to 128 bits, so the value had to be too. It is, and it opens in 128 bits. Refs #1298 Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
… an absent value Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
0deb2b1 to
12f9e83
Compare
Summary
The Go SDK can now encrypt an optional field. A field declared over a pointer, such as
DOB *encrypt.Datetaggedstash:"dob,encrypt_into=Date", generates code, andDecryptreturnsnilfor anil. Until now stashgen refused such a field as a composite, as #1293 showed. stashgen is the code generator that readsstash:"…"struct tags.There are two ways to store
nil, and they differ in what someone reading the database can see:null=sealed):nilis encrypted like a value. vitaminc 0.8 pads it to the length of a value of the field's type, so a row with no value looks like any other row.EqNull, theeqquery ofnil.null=sql): nothing is stored, and the field's columns areNULL. Anyone reading the table can see which rows have no value.null=sqlexplicitly. A null has no order or match term, and text shows its length anyway.NULLsilently.Stacked on #1327 (the vitaminc 0.8 / ORE upgrade, now on the released clww-ore 0.6.0 and ore-rs 0.10.0). When #1327 merges, this PR's base becomes
next.Changes
Stack Encrypt (Rust engine)
EqualityTermof aNone) and the dynamic one (IndexSpec::Equalityon a null). No other index has a term for a null."null": "sealed"or"null": "sql"(NullMode,FieldPlan::with_null)."sealed"seals a null as a typed null of the field's declared type, and stores the null's equality term beside it when the field has an equality index. It is refused on:SealedNullIndex);SealedNullType);NullPassthrough)."sql"leaves the field out of the stored record. A source may omit the field, and a stored record without it opens it as null.eqquery whose input is a null derives the null term, throughquery_termorrecord::query, on a"sealed"field. Any other null query is refused with the newNullQuery; a"sql"field's nulls are SQLNULL, whichIS NULLfinds."null"behaves exactly as before.eql-bindings (EQL v4 types in Rust)
EncryptFromandDecryptIntoforOption<plaintext>: the storage-only types (Date,Integer,Text, …) and the types whose only term is equality (DateEq,IntegerEq,TextEq, …). The equality types' query twins also takeOption<plaintext>. These come from a codegen change; the generated files were regenerated.encryption::targets) encrypts, opens and queries these types through thatOption.hmwhere the type has one, and opens as a null.hm.Date,TimestampandDecimalplaintexts now declare their fixed width to vitaminc, so theirNoneis padded too.Go SDK
*Tfor a sealed scalar field;null=sealed|sql, pickssealedwhere it hides the absence (fixed width, equality the only index), and otherwise refuses with a reason;EqNull(ctx, c)on a sealed nullable field that answerseq, through the newgensupport.Field.NullTerm;**T, pointers to structs and slices, andnull=on a field that is not a pointer or on a passthrough.null=sqlfield, the generated member is a pointer (*eql.DateEq). Seal and Open skip the field when it is nil, and Value reads it with the newgensupport.GetPtr.gensupportgainsDeclaration.Null,Deref,GetPtrandField.NullTerm.internal/recordand the encrypt record path carry the mode and accept anull=sqlfield that is left out.Decrypterror, from the engine's kind check.-modeldoes not map anull=sqlfield yet, and stashgen says so instead of generating code that would dereference nil.Docs
encrypt.Dateandencrypt.Ciphertextdocs, the stack-encrypt and Go CHANGELOGs, and a@cipherstash/eqlpatch changeset (eql-bindings ships at the npm package's version).Verification
cargo nextest run --workspace --all-featurespasses (1267 tests), and clippy-D warningsis clean. New engine tests:eqquery of null derives it; null queries on a"sql"field, on an order operator, or through another index are refused;"sql"batch of four rows with mixed nulls stores no entry for the left-out field, makes one key request and opens nulls;"sql"field.cargo test -p eql-codegenpasses, with the dispatch tests updated; so do-p eql-bindings --all-featuresand-p eql-encryption-tests.mise run types:checkshows no drift.DatestoresNoneas long as a value on the typed and by-name paths, and each path opens the other's;DateEqnull has the nullhm, which the query of aNonederives, and a type with an order term refuses a null;Datesealed null,DateEqSQL null).mise run test:encryption:postgres, which includes the new database test:public.eql_v4_dateandpublic.eql_v4_date_eqaccept a value whosecis a sealed null, and it reads back and opens asNone. Go SDK: stashgen refuses a pointer field such as*encrypt.Date, so an optional value cannot be encrypted #1298 had listed this as unverified.WHERE due = <query of null>::eql_v4.query_date_eqreturns exactly the rows whose value was absent.mise run go:testandmise run go:lintpass, andgo generate ./...changes no committed file.cmd/stashgen: the test(stashgen): reproduce nullable DOB generation failure #1293 reproduction, now skipped when the guest is not built, passes forencrypt.Dateand*encrypt.Date.encrypt:TestOptionalFieldsStoreNilAsTheirTagsSayround-trips aPersonwith both modes over the deterministic EQL guest.DOB's,Born's andAge's ciphertexts are as long as present ones.Born(DateEq) andAge(index=equality) shares one equality term that no value has, andEqNullderives it.null=sqlmembers are nil, and every nil decrypts to nil.encrypt.Dateis an error.stashgen: 12 new refusal cases.cargo mutants --in-difffor stack-encrypt:"null". Each now has a test, and a rerun of the two catches both.record::query's null check, which no test reached. A new test catches all three on a rerun.Related
Closes #1298.
Supersedes #1293. Its test is folded into
cmd/stashgen/main_test.go, credited to its author, with the guest skip added.Stacked on #1327. Uses cipherstash/vitaminc#497 and cipherstash/vitaminc#499 (vitaminc 0.8.0).
Review notes
packages/stack-encrypt/src/dynamic/record.rs(NullMode,source_row,by_absentandin_row_order, and the groupedencrypt_with/decrypt_with), thenlanguages/golang/stashgen/read.go(nullMode) and the generatedencrypt/internal/testusers/person_stash.go.null=sql, and that is the intended design, not a gap to fill later. vitaminc-ore 0.8 can place nulls at one end of an order space, but a sealed null there buys nothing:NULLshows.LIMITs, and fails when the column is not selected.NULL, which shows every absent row with no inference.null=sqlonly, as Go SDK: stashgen refuses a pointer field such as*encrypt.Date, so an optional value cannot be encrypted #1298 scoped. eql-bindings'Textdoes acceptOption<String>, because every storage-only type is generated alike, but the engine refuses a sealed null on a variable-width type.KindOf::typed_null's default mutant is excluded in chore(stack-encrypt)!: move to vitaminc 0.8, clww-ore 0.6 and ore-rs 0.10 #1327 as equivalent. It is reachable now, sinceValueFrame::typed_nullframes reach it, and it is still equivalent."null"is refused when opened (FieldType), so reading a nullable column through a declaration that is not nullable fails loudly.https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3