Repository navigation
Conversation
A json.RawMessage field tagged encrypt_into=EncryptedJson, with optional ordered paths and sealed_root, is sealed by the engine from the field's data plan and gets generated methods for every json_tree query and read: Contains, ContainedBy, EqAt, Selector, Subtree, Children, ChildCount, and an At accessor per ordered path with Lt, Lte, Gt, Gte and Between. Selector and subtree reads return handles (encrypt.JSONSelection, encrypt.JSONRead) that keep the request they were prepared from and open the rows the database returns only as answers to it. TestJSONDocumentsQueryAndReadInPostgres runs every method against the EQL v4 bundle from the tree and joins go:test:postgres. Refs #1347, #1351, #1352 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
|
|
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,
|
|
Automated code review of this pull request at
|
- A json.RawMessage is a document only under encrypt_into; sealed or indexed any other way it stays bytes, so existing models generate as before. - An ordered path's accessor has its own Selector, so an ordering and the value it compares come from one path; the README says why. - JSONRead keeps the subtree input it was prepared from and sends it back when it opens; OpenOne refuses a wildcard path before opening anything. - JSONSelection.Open makes no request for a page with no entries and refuses more than one value for a row; a zero JSONSelector or JSONToken is refused rather than bound as ''. - gensupport's JSON helpers check the term's encoding, fidelity (record.Exact) and part count in one place. - stashgen: imports are re-checked after planning adds encoding/json; a document field needs the engine to answer selector on it; an ordered path given twice, or with no name, is refused by name; a key of digits names its accessor; index=json points at encrypt_into=EncryptedJson. - Tests check rows.Err() and no longer assert a driver value's type. Refs #1347 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
|
Review response to the findings listed above (fixed in 3ae7166 unless noted):
Verified on this branch: |
Summary
This pull request gives Go applications encrypted JSON documents (
EncryptedJson, EQL'sjson_treetype; EQL is the Encrypt Query Language, the SQL that stores and queries encrypted values) throughstashgen, the Go SDK's code generator. Ajson.RawMessagefield taggedencrypt_into=EncryptedJsonis sealed by Stack Encrypt (the Rust engine, run in Go as a WebAssembly guest) from the field's data plan (the declaration the generator emits and the engine resolves), and gets generated methods for every query and read the previous pull request added. It is slices D and E of the plan in #1353.Changes
Runtime (
languages/golang/encrypt)json.go:JSONSelector,JSONToken,JSONTokens(atext[]value),JSONValue(Found,JSON),JSONReadOptions(WildcardBound),JSONRange, and the two read handles:JSONSelection(a->read;Selector()to bind,Opento open each row's entry) andJSONRead(a subtree read;Tokens()to bind,Open/OpenOnefor each row's entries). A handle holds the request that prepared it; the engine re-derives from that request, never from the rows.gensupport: thejsonkind, a declaration'sTargetOptions, and the helpers generated code calls (JSONQuery,JSONQueryAt,JSONBetweenAt,JSONSelect,JSONSubtree,JSONNodeToken).term.go:JSON()/JSONTerm(the old SteVec placeholder, never producible) markedDeprecated.Generator (
languages/golang/stashgen)encrypt_into=EncryptedJson(ordered=$.a;$.b[*].c,sealed_root=false).;separates ordered paths because,separates tag options. A path in a tag is a plain JSONPath (dotted keys,[*]); a quoted key is refused. The generator hands the options to the engine, which validates them as it resolves the plan, so a declaration the engine refuses (a position in an ordered path, options on a type that takes none) fails at generation.json.RawMessage/*json.RawMessagemap to thejsonkind. A pointer must saynull=sql(a nil document is SQLNULL;null=sealedis refused); JSONnullis a document. Other Go types forEncryptedJson, and the Go spellingEncryptedJSONin a tag, are refused with a hint.Encrypt,Contains,ContainedBy,EqAt,Selector,Subtree,Children,ChildCount, and, for a field that orders paths, anAtstruct with one accessor per ordered path (Lt,Lte,Gt,Gte,Between, and the path's ownSelector). Ajson.RawMessagesealed any other way thanencrypt_intostays bytes. The accessors are generated from the options the engine accepted, so an unordered path has none.Tests
encrypt/internal/testusers/customers.go(+ generatedcustomer_stash.go): a model with an ordered document and an optional one.encrypt/json_fields_test.go: round trip (includingnull=sqland a JSONnull), and every generated method derives the operand the data plan does.encrypt/json_postgres_test.go(TestJSONDocumentsQueryAndReadInPostgres): rows stored by generated code in PostgreSQL with the EQL v4 bundle from the tree; stored values read back; containment both ways (including an array element),eq_atat a wildcard path, containment on thenull=sqlcolumn;ltandbetweenat$.ageandgtat the wildcard path$.visits[*].at;->selector reads opened per row (absence included); subtree reads of a wildcard and of an array; and children / child count by node token. Added tomise run go:test:postgres, which runs each test by name.cmd/stashgen/main_test.go(TestRunGeneratesEncryptedJSONFields): the generated surface, and nine refusals (sealed null, pointer withoutnull=sql, a position in an ordered path, a quoted key, an unknown option, the Go spelling of the type, two paths naming one accessor, a string field, options on a scalar type).Docs:
cmd/stashgen/README.mdandencrypt/README.mdgain an "Encrypted JSON documents" section; Go CHANGELOG.Assumptions
The five owner decisions in #1347 are open; this stack takes the plan's recommended answer to each:
json.RawMessageand*json.RawMessage. A struct-typed document is not supported yet.json.RawMessage).Atstruct named after each path ($.visits[*].at→VisitsAt).eq_at, orderings, selector and subtree reads, children tokens).Also assumed:
ordered=with;), andsealed_root=falseas the only other option.Children/ChildCountreturn the node token to bind; the child rows are read in SQL (the opener for child rows is not in this stack).Not done here
json_tree_childrenrows.ORDER BYon an ordered path is not exercised in PostgreSQL here; the orderings are tested as comparisons.Verification
After the review fixes (3ae7166) and merging the updated base (534d32f), with the guests rebuilt from it:
mise run go:test,go:lint(0 issues),go generate ./...(no drift) andgo:test:postgresagainst PostgreSQL 17 with the tree's EQL v4 bundle (all four tests by name, includingTestJSONDocumentsQueryAndReadInPostgres), all passed. The original verification, macOS arm64, Go 1.26.10, Rust 1.94.1:mise run go:test(gofmt check,go vet, and the module's tests against the deterministic guests): passed, includingTestACustomersDocumentsRoundTrip,TestACustomersFieldsDeriveEachQuery,TestRunGeneratesEncryptedJSONFields, and the stub-contract testsTestStubAgreesWithGensupport/TestStubAgreesWithEncrypt(the generator's test stub gains the new symbols).mise run go:test:postgresagainst PostgreSQL 17 in Docker, with the EQL v4 bundle built from the tree: passed, all four tests by name, includingTestJSONDocumentsQueryAndReadInPostgres.mise run go:lint(golangci-lint): 0 issues. The two internal uses of the now-deprecatedencrypt.JSON/encrypt.JSONTermcarry anolint:staticcheckwith the reason.go generate ./...leaves the tree unchanged (the CI drift check).git diff --check: clean.types:checkfrom the previous pull request stand; the guests used are the ones built from it.Related
Stack
Merge order is bottom to top; each pull request is based on the one below it.
EncryptedJsonstored through a data plan (engine, EQL catalog, guest, Go runtime)https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu