Skip to content

Commit 52802ef

Browse files
committed
docs(go): show the element match in the references and the JSON example
The stashgen README's "Encrypted JSON documents" table gets the Match row and its SQL, with when Elements is generated, how a member is written, what is refused before the engine, the folded equality and the 22023 bound. 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: 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
1 parent 6ea438c commit 52802ef

8 files changed

Lines changed: 190 additions & 15 deletions

File tree

‎languages/golang/CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`.
1212

1313
### Added
1414

15+
- **Test several members of one array element of a `JsonSearch` field
16+
(#1442).** A field whose `ordered=` paths name members beneath an array's
17+
wildcard (`$.contacts[*].relationship;$.contacts[*].calls`) gets
18+
`Fields.<Field>.Elements.<Array>.Match`, one accessor per array, named as
19+
`At` names a path (`Elements.Contacts`). `Match` takes
20+
`encrypt.ElementPredicates` made with `encrypt.MemberEq`, `MemberLt`,
21+
`MemberLte`, `MemberGt`, `MemberGte` and `MemberBetween`, and returns an
22+
`eql.JSONSearchElementsQuery` for
23+
`eql_v4.json_tree_element_match(col, $1::eql_v4.query_json_elements)`,
24+
which finds a row where one element passes every predicate. An `EqAt` and
25+
an ordering at the array's paths can each be answered by a different
26+
element. Each comparison is of the member's ordering term, so `MemberEq`
27+
compares text case- and accent-folded and integers past 2^53 can tie;
28+
`EqAt` stays exact. A member the field does not order is refused before
29+
the engine is asked, with `encrypt.ErrTerm`, naming the member and the
30+
array; no predicates, a zero `ElementPredicate` and a member with a
31+
wildcard are refused with `encrypt.ErrEncoding`. A row whose array is
32+
longer than `JSONReadOptions.WildcardBound` (64 by default) fails the
33+
query with SQLSTATE 22023. An `ElementPredicate` prints and logs its
34+
member and comparison, never its values. The embedded EQL guest now names
35+
the `eql_v4.query_json_elements` domain, so `Checker.Operators` lists
36+
`element_match` (input `path_predicates`) on a field that orders a path.
37+
1538
- **A two-letter word is searchable in `TextMatch`, `TextSearch` and
1639
`TextSearchOre` (#1309).** The EQL v4 match types index each two-letter
1740
word whole, beside the 3-grams of the text, so a two-letter surname such

‎languages/golang/cmd/stashgen/README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ The field's `Fields` entry has a method per operation:
178178
| `Selector(ctx, c, path)` | `encrypt.JSONSelection`: `Selector()` and `Open` | `col -> $1::text` |
179179
| `Subtree(ctx, c, path, opts)` | `encrypt.JSONRead`: `Tokens()`, `Open` and `OpenOne` | `eql_v4.json_tree_subtree(col, $1::text[], 64)` |
180180
| `Children(ctx, c, path)`, `ChildCount(ctx, c, path)` | `encrypt.JSONToken` | `eql_v4.json_tree_children(col, $1::text)`, `eql_v4.json_tree_child_count(col, $1::text)` |
181+
| `Elements.Visits.Match(ctx, c, predicates, opts)` | `eql.JSONSearchElementsQuery` | `eql_v4.json_tree_element_match(col, $1::eql_v4.query_json_elements)` |
181182

182183
`At` has one accessor per ordered path, named for its keys (`$.visits[*].at` is `At.VisitsAt`, `$.a.1` is `At.A1`); two paths with one name, or one path given twice, stop `stashgen`.
183184
An ordering compares the value `->` extracts at its path, and EQL's `<` compares ordering terms alone, so take the selector from the same accessor (`At.Age.Selector`) rather than spelling the path again.
@@ -193,6 +194,32 @@ SELECT id FROM customers ORDER BY (
193194
```
194195

195196
with `$1` from `At.VisitsAt.Selector(ctx, c)` and `$2` from `At.VisitsAt.Gt(ctx, c, v)`; for the latest visit, order by `ORDER BY 1 DESC NULLS LAST` inside and `DESC` outside.
197+
198+
Two queries at a wildcard path, an `EqAt` and an ordering say, can each be answered by a different element.
199+
To test several members of one element together, use `Match` under `Elements`.
200+
`Elements` has one accessor per array whose elements have an ordered member, named for the array's keys as `At` names a path: with `ordered=$.visits[*].at;$.visits[*].place`, `$.visits[*]` is `Elements.Visits`, with the members `at` and `place`.
201+
A root array (`ordered=$[*].name`) is `Elements.Root`.
202+
An ordered path that ends at its wildcard (`$.tags[*]`), or has none, gives no accessor, and a field without one has no `Elements`.
203+
Two arrays whose keys give one name (`$.a_b[*]` and `$.aB[*]` are both `AB`), and an array whose keys give no name, get no accessor: `stashgen` still writes the field and its `At`, and prints a notice naming the ordered paths, which the generated file repeats in a comment.
204+
205+
```go
206+
// rows with one visit that is at home and after 15
207+
q, err := customers.Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredicates{
208+
encrypt.MemberEq("place", "home"),
209+
encrypt.MemberGt("at", 15),
210+
}, encrypt.JSONReadOptions{})
211+
```
212+
213+
```sql
214+
SELECT id FROM customers WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)
215+
```
216+
217+
The predicates are `encrypt.MemberEq`, `MemberLt`, `MemberLte`, `MemberGt`, `MemberGte` and `MemberBetween` (inclusive), each with a member written as it is below the array in the tag (`place`, or `address.city`) and Go values, as the orderings take.
218+
A member the field does not order is refused before the engine is asked, with `encrypt.ErrTerm`, naming the member, the array and the members it orders; no predicates, and a member with a wildcard, are refused with `encrypt.ErrEncoding`.
219+
Each comparison is of the member's ordering term, so `MemberEq` is an equality of folded values: text compares case- and accent-folded (`"home"` equals `"Home"`), and integers past 2^53 can tie.
220+
`EqAt` at the member's full path is exact, of any element.
221+
`Match` reads each array to `opts.WildcardBound` elements, 64 when it is 0, and a row whose array is longer fails the query with SQLSTATE 22023 rather than answer over part of it: set the bound past the longest array.
222+
The function runs on each row a scan gives it, so narrow the rows first with an indexed predicate, such as `@>` on the same values.
196223
`OpenOne` reads one value and refuses a read whose path has a wildcard before anything is opened; open a wildcard read with `Open`.
197224
A needle and a value are Go values: a `json.RawMessage` of JSON text crosses as it is, and anything else is marshalled with `encoding/json`.
198225
`EqAt` and the orderings take a scalar: a string, a number, a bool, or `nil` for JSON `null`.

‎languages/golang/encrypt/README.md‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ The EQL types the engine produces, and the Go field types each takes:
520520
| `Numeric` | `encrypt.Decimal` | none |
521521
| `NumericEq` | `encrypt.Decimal` | `=`, `<>` (`1.5 = 1.50`) |
522522
| `NumericOrd`, `NumericOrdOpe`, `NumericOrdOre` | `encrypt.Decimal` | `<`, `<=`, `>`, `>=`, `ORDER BY` |
523-
| `JsonSearch` (`eql.JSONSearch`) | struct, map, slice, or `json.RawMessage` | containment, equality at a path, ordering at declared paths, selection, subtree reads; see [Encrypted JSON documents](#encrypted-json-documents) |
523+
| `JsonSearch` (`eql.JSONSearch`) | struct, map, slice, or `json.RawMessage` | containment, equality at a path, ordering at declared paths, several members of one array element, selection, subtree reads; see [Encrypted JSON documents](#encrypted-json-documents) |
524524

525525
Any built-in Go integer field takes an integer EQL type (ADR-0002): the field
526526
is sealed at the type's width, and opens back into the field's type. A type
@@ -658,6 +658,26 @@ element of each row, so an ordering over every element is written over
658658
`eql_v4.json_tree_path_query(col, $sel)`, its matches (the stashgen README
659659
shows the SQL).
660660

661+
Two such queries can each be answered by a different element. To test
662+
several members of one array element together, use `Match` on the array's
663+
accessor under `Elements`. A field has one for each array whose elements
664+
have an ordered member (`ordered=$.visits[*].at;$.visits[*].place`):
665+
666+
```go
667+
q, err := users.Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredicates{
668+
encrypt.MemberEq("place", "home"),
669+
encrypt.MemberGt("at", 15),
670+
}, encrypt.JSONReadOptions{})
671+
rows, err := db.QueryContext(ctx, `SELECT id FROM users WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)`, q)
672+
```
673+
674+
Each comparison is of the member's ordering term, so `MemberEq` compares
675+
text case- and accent-folded, and integers past 2^53 can tie; `EqAt` is
676+
exact. A member the field does not order is refused before the engine is
677+
asked, naming it. A row whose array is longer than
678+
`JSONReadOptions.WildcardBound` (64 by default) fails the query with
679+
SQLSTATE 22023.
680+
661681
## Building
662682

663683
The package embeds `wasm/stack_encrypt_guest.wasm`, a build artefact of the

‎languages/golang/encrypt/doc.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,22 @@
242242
// A selection or a subtree read opens values as JSON text ([JSONValue],
243243
// [JSONRead.Open]). [DecodeJSON] reads one into a Go type.
244244
//
245+
// Two queries at an array's paths can each be answered by a different
246+
// element. To test several members of one element together, use Match on
247+
// the array's accessor under Elements, which a field has for each array
248+
// whose elements have an ordered member:
249+
//
250+
// q, err := customers.Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredicates{
251+
// encrypt.MemberEq("place", "home"),
252+
// encrypt.MemberGt("at", 15),
253+
// }, encrypt.JSONReadOptions{})
254+
// // WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)
255+
//
256+
// Each comparison is of the member's ordering term, so [MemberEq] compares
257+
// text case- and accent-folded, and integers past 2^53 can tie. A row whose
258+
// array is longer than [JSONReadOptions.WildcardBound] (64 by default)
259+
// fails the query with SQLSTATE 22023.
260+
//
245261
// # Cancellation
246262
//
247263
// Every call takes a [context.Context], and a call whose context is

‎languages/golang/encrypt/example/json/README.md‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Typed encrypted JSON document example
22

33
This program stores a Go `Profile` struct as one encrypted EQL v4 JSON column.
4-
It finds a row by containment, compares an ordered path, decrypts a whole
5-
profile, and opens only the visit times from another query.
4+
It finds a row by containment, compares an ordered path, finds a row where one
5+
visit passes two tests, decrypts a whole profile, and opens only the visit
6+
times from another query.
67

78
## Run
89

@@ -35,8 +36,25 @@ its `stash` tags. After changing the struct or tags, run this from
3536
go generate ./encrypt/example/json
3637
```
3738

38-
`Profile` uses `encoding/json` for the document shape. The `ordered=$.age`
39-
tag adds an ordering term for that path. A containment needle made from a
39+
`Profile` uses `encoding/json` for the document shape. The
40+
`ordered=$.age;$.visits[*].at;$.visits[*].place` tag adds an ordering term
41+
for each of those paths. Because `at` and `place` are ordered members of
42+
each visit, the generated `Fields.Profile.Elements.Visits.Match` tests both
43+
on one visit:
44+
45+
```go
46+
q, err := Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredicates{
47+
encrypt.MemberEq("place", "home"),
48+
encrypt.MemberGt("at", 15),
49+
}, encrypt.JSONReadOptions{})
50+
// SELECT id FROM customers WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)
51+
```
52+
53+
Ada has a visit at home and a visit after 15, but no one visit that is both,
54+
so only Bea's row matches. Each comparison is of the ordering term, so text
55+
compares case- and accent-folded. A row with more visits than
56+
`JSONReadOptions.WildcardBound` (64 by default) fails the query with SQLSTATE
57+
22023. A containment needle made from a
4058
struct includes every field without `omitempty`, including zero values;
4159
`main.go` uses a map to name only the member being queried. The prepared
4260
subtree read returns JSON text, which `encrypt.DecodeJSON[int64]` decodes.

‎languages/golang/encrypt/example/json/customer_stash.go‎

Lines changed: 54 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎languages/golang/encrypt/example/json/main.go‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func run(ctx context.Context) error {
5050
defer client.Close()
5151
cipher := client.DefaultKeyset()
5252
customers := []Customer{
53-
{ID: 1, Profile: Profile{Name: "Ada", Age: 35, Visits: []Visit{{At: 10}, {At: 20}}}},
54-
{ID: 2, Profile: Profile{Name: "Bea", Age: 28, Visits: []Visit{{At: 30}}}},
53+
{ID: 1, Profile: Profile{Name: "Ada", Age: 35, Visits: []Visit{{At: 10, Place: "home"}, {At: 20, Place: "work"}}}},
54+
{ID: 2, Profile: Profile{Name: "Bea", Age: 28, Visits: []Visit{{At: 30, Place: "home"}}}},
5555
}
5656
sealed, err := Encrypt(ctx, cipher, customers)
5757
if err != nil {
@@ -101,6 +101,26 @@ func run(ctx context.Context) error {
101101
}
102102
fmt.Printf("customer older than 30: %d\n", olderID)
103103

104+
// Test two members of one visit together: a visit at home after 15.
105+
// Ada has a visit at home and a visit after 15, but not one visit that
106+
// is both, so only Bea's row matches. Match compares ordering terms, so
107+
// "Home" would equal "home": text compares case- and accent-folded.
108+
homeLate, err := Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredicates{
109+
encrypt.MemberEq("place", "home"),
110+
encrypt.MemberGt("at", 15),
111+
}, encrypt.JSONReadOptions{})
112+
if err != nil {
113+
return fmt.Errorf("prepare visit match: %w", err)
114+
}
115+
var homeLateID int64
116+
if err := conn.QueryRowContext(ctx,
117+
`SELECT id FROM customers WHERE eql_v4.json_tree_element_match(profile, $1::eql_v4.query_json_elements)`,
118+
homeLate,
119+
).Scan(&homeLateID); err != nil {
120+
return fmt.Errorf("find a visit at home after 15: %w", err)
121+
}
122+
fmt.Printf("customer with a visit at home after 15: %d\n", homeLateID)
123+
104124
// Read only the visit times of one row. The database returns encrypted
105125
// entries; Open checks them against the prepared read and returns JSON text.
106126
read, err := Fields.Profile.Subtree(ctx, cipher, "$.visits[*].at", encrypt.JSONReadOptions{})

‎languages/golang/encrypt/example/json/model.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package main
66

77
// Visit is one visit stored inside a customer's encrypted profile.
88
type Visit struct {
9-
At int64 `json:"at"`
9+
At int64 `json:"at"`
10+
Place string `json:"place"`
1011
}
1112

12-
// Profile is a typed JSON document. Its Age is searchable by order.
13+
// Profile is a typed JSON document. Its Age, and each visit's At and Place,
14+
// are searchable by order.
1315
type Profile struct {
1416
Name string `json:"name"`
1517
Age int64 `json:"age"`
@@ -20,5 +22,5 @@ type Profile struct {
2022
type Customer struct {
2123
_ struct{} `stash:"context=customers"`
2224
ID int64 `stash:"id,passthrough"`
23-
Profile Profile `stash:"profile,encrypt_into=JsonSearch(ordered=$.age)"`
25+
Profile Profile `stash:"profile,encrypt_into=JsonSearch(ordered=$.age;$.visits[*].at;$.visits[*].place)"`
2426
}

0 commit comments

Comments
 (0)