diff --git a/languages/golang/CHANGELOG.md b/languages/golang/CHANGELOG.md index 3c778ed82..fa7df990a 100644 --- a/languages/golang/CHANGELOG.md +++ b/languages/golang/CHANGELOG.md @@ -12,6 +12,37 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`. ### Added +- **Bind a whole jsonpath filter on a `JsonSearch` field as one parameter + (#1443).** Every `JsonSearch` field gets + `Fields..Match(ctx, cipher, predicate, vars, opts)`, which takes + what plaintext `jsonb_path_match(col, $1, $2)` takes: a jsonpath predicate + and its variables (any value `encoding/json` writes as an object, a + `json.RawMessage` of one, or `nil` for none). It returns an + `eql.JSONSearchPredicateQuery` for + `col @@ $1::eql_v4.query_json_predicate` or + `eql_v4.json_tree_match(col, $1::eql_v4.query_json_predicate)`. The engine + parses the predicate and compiles it into a tree of the operands `EqAt`, + `Selector`, `At.` and `Elements..Match` derive; the Go side + reads none of it. The grammar is paths of `.key`, `."quoted key"` and + `[*]`; `==`, `!=`, `<`, `<=`, `>`, `>=` against a variable or a JSON + literal; `exists(path)` and `exists(path ? (@.a == $x && @.b > $y))`; and + `&&`, `||`, `!(…)`. A construct it cannot answer is refused as an + `*encrypt.Diagnostic` wrapping `encrypt.ErrEncoding`, never compiled to a + tree that drops a leaf; an ordering at a path the tag does not order is + `encrypt.ErrTerm`. Each row answers as `jsonb_path_match` does over its + plaintext, save four documented cases (lax array unwrapping and wrapping, + a string compared with a number, and folded ordering terms). The tree is + evaluated inside a function, so no index engages: `EqAt`, `Contains` and + `At.` stay the indexed form. The embedded EQL guest names the + `eql_v4.query_json_predicate` domain, so `Checker.Operators` lists `match` + (input `predicate`) on every `JsonSearch` field, and the guest reports a + refused predicate as a malformed input rather than an internal failure. + +- **`Diagnostic.Construct` and `Diagnostic.At` (#1443).** A refused `Match` + predicate names the construct in the grammar's words (`` `like_regex` ``, + `a position (`[2]`)`) and the byte offset in the predicate where it + starts. Neither carries text from the predicate. + - **Test several members of one array element of a `JsonSearch` field (#1442).** A field whose `ordered=` paths name members beneath an array's wildcard (`$.contacts[*].relationship;$.contacts[*].calls`) gets @@ -148,6 +179,18 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`. ### Breaking +- **Every `encrypt/eql` type prints and logs redacted (#1443).** `String`, + `Format` (every verb, `%#v` included) and `LogValue` write the type's + name and `[redacted]` (`eql.TextEq{[redacted]}`, `eql.TextEq{}` when + empty), never the bytes: a stored value holds ciphertext and terms, and a + query value terms. Before, each type was a bare `[]byte` to `fmt` and + `slog`: `%s` and `%q` gave its JSON, and `fmt.Sprint(v)`, `%v` and `%x` + its bytes as numbers or hex. They now give the redacted text, with no + error. A caller that read the JSON with `%s` converts the value instead, `string(v)` or `json.RawMessage(v)`, or + calls `v.MarshalJSON()`; `Value` is unchanged too. There is no named + accessor method for the JSON: the value is the JSON. Generated by + eql-codegen for every type. + - **`eql.SteVecDocument` and `eql.SteVecQuery` are gone.** They named EQL v4's SteVec domains, which the engine never produced and the v4 bundle no longer carries: in v4, `public.eql_v4_json_search` is the diff --git a/languages/golang/cmd/stashgen/README.md b/languages/golang/cmd/stashgen/README.md index ae476f134..48a88915f 100644 --- a/languages/golang/cmd/stashgen/README.md +++ b/languages/golang/cmd/stashgen/README.md @@ -179,6 +179,7 @@ The field's `Fields` entry has a method per operation: | `Subtree(ctx, c, path, opts)` | `encrypt.JSONRead`: `Tokens()`, `Open` and `OpenOne` | `eql_v4.json_tree_subtree(col, $1::text[], 64)` | | `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)` | | `Elements.Visits.Match(ctx, c, predicates, opts)` | `eql.JSONSearchElementsQuery` | `eql_v4.json_tree_element_match(col, $1::eql_v4.query_json_elements)` | +| `Match(ctx, c, predicate, vars, opts)` | `eql.JSONSearchPredicateQuery` | `col @@ $1::eql_v4.query_json_predicate`, or `eql_v4.json_tree_match(col, $1::eql_v4.query_json_predicate)` | `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`. 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. @@ -220,6 +221,64 @@ Each comparison is of the member's ordering term, so `MemberEq` is an equality o `EqAt` at the member's full path is exact, of any element. `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. 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. + +`Match` on the field itself takes a whole filter as one parameter: a jsonpath predicate and its variables, the two things plaintext `jsonb_path_match(col, $1, $2)` takes. +It is generated on every `JsonSearch` field, ordered paths or none: the engine reads the predicate when `Match` is called, and refuses what the field cannot answer then. + +```go +q, err := entities.Fields.CustomFieldValues.Match(ctx, cipher, + `$."extra"."country" == $val && $."weight" < $max`, + map[string]any{"val": "nz", "max": 9.5}, encrypt.JSONReadOptions{}) +``` + +```sql +SELECT id FROM entity WHERE custom_field_values @@ $1::eql_v4.query_json_predicate +-- or, the same rows +SELECT id FROM entity WHERE eql_v4.json_tree_match(custom_field_values, $1::eql_v4.query_json_predicate) +``` + +`vars` are any value `encoding/json` writes as a JSON object (a map, a struct, or a `json.RawMessage` of one), or `nil` for none; a Go string is refused, as it is for a `Contains` needle. +The Go side reads nothing of the predicate: the engine parses it and compiles it into a tree of the operands the methods above derive, one leaf per comparison. +The grammar is the subset of SQL/JSON path that the stored terms can answer, as PostgreSQL reads it: + +- paths: `$`, then `.key`, `."quoted key"` and `[*]` steps; +- values: a variable (`$v`, `$"v"`) or a JSON literal (a string, a number, `true`, `false`, `null`); +- comparisons: `==`, `!=` (or `<>`), `<`, `<=`, `>`, `>=` between a path and a value, either side first; +- `exists(path)`, and `exists(path ? (@.m == $v && @.n > $w))`, conditions on `@` or `@.member` joined by `&&`; +- `&&`, `||`, `!(…)`, parentheses and `/* */` comments. Keywords are read in any case. + +An ordering (`<` and the rest) needs its path in `ordered=`, and so does every member of a filter of more than one condition, which compiles to the element match above. +Anything else is refused before any term is derived, as an `*encrypt.Diagnostic` wrapping `encrypt.ErrEncoding`. +`Construct()` names the construct in the grammar's own words (`` `like_regex` ``, `a position (`[2]`)`), and `At()` is the byte offset in the predicate where it starts; neither says anything of the predicate's text or the variables' values. +`Reason()` says why: + +- `json_predicate_syntax`: text the grammar does not read. +- `json_predicate_unsupported`: a jsonpath construct the stored terms cannot answer, such as `like_regex`, arithmetic, a method, `.**`, or a filter outside `exists`. +- `json_predicate_unknown_variable`: a `$name` that `vars` does not hold. +- `json_predicate_value_type`: an object or array as a value, or anything but a number or a string under an ordering or in a filter of more than one condition. +- `json_predicate_not_equal_at_wildcard`: `!=` at a path with `[*]`. +- `json_predicate_too_deep`: more than 32 levels of nesting, or a path deeper than a document. +- `json_path_position`: a position (`[2]`) in a path. +- `json_path_no_wildcard`: a filter on a path with no `[*]`. +- `json_path_wildcard`: a `[*]` in a filter's member. +- `json_syntax`: JSON text that does not parse, a literal in the predicate (`1e999`) or `vars` (`{`, or an empty or whitespace-only `json.RawMessage`); with `At()` for a literal, without it for `vars`. +- `json_input`: `vars` that are not a JSON object (`[1]`, `1`). + +An ordering at a path the tag does not order is `encrypt.ErrTerm` with the code `stack_encrypt::json_path_not_ordered`. + +Each row answers as `jsonb_path_match` answers over its plaintext, true, false or NULL, save four cases: + +- Lax mode's unwrapping of an array the path names no `[*]` for is not applied: `$.tags == "a"` over `{"tags": ["a"]}` is false here. Write `[*]`. +- Nor is its wrapping of a value that is not an array under `[*]`: `$.tags[*] == "a"` over `{"tags": "a"}` is false here. Keep an array an array. +- A string compared with a number, which jsonpath calls unknown, is false under `==` and true under `!=`. Keep one kind of value at a path. +- Orderings, and filters of more than one condition, compare the folded ordering terms: text case- and accent-folded, numbers as float64. So `exists($.c[*] ? (@.r == "sibling" && @.n > 5))` finds a `"Sibling"`; `==` anywhere else is exact. + +A filter of more than one condition reads each array to `opts.WildcardBound` elements, 64 when it is 0; a row whose array is longer fails the query with SQLSTATE 22023, as the element match does. + +`Match` costs an index: `eql_v4.json_tree_match` evaluates the tree inside a function, so it filters every row a scan gives it and no index engages. +The per-operator form engages the GIN index for `@>` (`EqAt`, `Contains`) and a btree on an ordered path's term (`At.`). +Where a query needs an index, write those leaves with the per-operator methods, or put one beside `Match` to narrow the rows: `WHERE col @> $1::eql_v4.query_json AND col @@ $2::eql_v4.query_json_predicate`. + `OpenOne` reads one value and refuses a read whose path has a wildcard before anything is opened; open a wildcard read with `Open`. 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`. `EqAt` and the orderings take a scalar: a string, a number, a bool, or `nil` for JSON `null`. diff --git a/languages/golang/cmd/stashgen/main_test.go b/languages/golang/cmd/stashgen/main_test.go index d5c7cb3fc..ad5037d8f 100644 --- a/languages/golang/cmd/stashgen/main_test.go +++ b/languages/golang/cmd/stashgen/main_test.go @@ -458,6 +458,18 @@ func TestRunGeneratesJSONSearchFields(t *testing.T) { if strings.Contains(written, "PrefsFieldElements") { t.Error("Prefs orders no member, and got Elements") } + // Every document field has Match, ordered paths or none: what a + // predicate may use is the engine's to decide when it compiles it. + for _, field := range []string{"ProfileField", "PrefsField"} { + for _, want := range []string{ + "func (f " + field + ") Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error)", + "gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts)", + } { + if !strings.Contains(written, want) { + t.Errorf("the generated file lacks %q", want) + } + } + } // The members of one array, named as At names their paths; an ordered // path that ends at its wildcard, or has none, is no array's member. written, stderr, code = generate("\tProfile json.RawMessage `stash:\"profile,encrypt_into=JsonSearch(ordered=$.extra.emergency_contacts[*].relationship;$.extra.emergency_contacts[*].previous_call_count;$.tags[*];$.age)\"`") @@ -570,6 +582,19 @@ func TestRunGeneratesJSONSearchFields(t *testing.T) { if _, err := os.Stat(filepath.Join(dir, "customer_stash.go")); !os.IsNotExist(err) { t.Fatal("a file was written after the refusal") } + // The same for any document field, ordered or not, and an engine that + // answers no match. + dir = writeModule(t, map[string]string{"model.go": source("\tProfile json.RawMessage `stash:\"profile,encrypt_into=JsonSearch\"`")}) + stderrBuf.Reset() + if code := run([]string{"-type", "Customer"}, dir, &bytes.Buffer{}, &stderrBuf, withoutOperator("match")); code != 1 { + t.Fatalf("exit %d, want 1\n%s", code, stderrBuf.String()) + } + if got := stderrBuf.String(); !strings.Contains(got, "Customer.Profile") || !strings.Contains(got, "the engine answers no match on JsonSearch") { + t.Fatalf("stderr = %q", got) + } + if _, err := os.Stat(filepath.Join(dir, "customer_stash.go")); !os.IsNotExist(err) { + t.Fatal("a file was written after the refusal") + } for _, c := range []struct{ name, fields, want string }{ {"a sealed null", "\tPrefs *json.RawMessage `stash:\"prefs,encrypt_into=JsonSearch,null=sealed\"`", "Customer.Prefs: a nil has an equality term and no other, so another index's column would show which values are absent; write null=sql"}, {"a nil document that does not say null=sql", "\tPrefs *json.RawMessage `stash:\"prefs,encrypt_into=JsonSearch\"`", "null=sql"}, diff --git a/languages/golang/encrypt/README.md b/languages/golang/encrypt/README.md index 1b3fcf047..2a97e856c 100644 --- a/languages/golang/encrypt/README.md +++ b/languages/golang/encrypt/README.md @@ -678,6 +678,36 @@ asked, naming it. A row whose array is longer than `JSONReadOptions.WildcardBound` (64 by default) fails the query with SQLSTATE 22023. +A whole filter binds as one parameter with the field's `Match`: a jsonpath +predicate and its variables, as plaintext `jsonb_path_match(col, $1, $2)` +takes them. The engine reads the predicate and compiles it into a tree of +the operands above; the Go side reads none of it. + +```go +q, err := users.Fields.Profile.Match(ctx, cipher, + `$."plan" == $plan && $."age" < $max`, + map[string]any{"plan": "pro", "max": 40}, encrypt.JSONReadOptions{}) +rows, err := db.QueryContext(ctx, `SELECT id FROM users WHERE profile @@ $1::eql_v4.query_json_predicate`, q) +``` + +`eql_v4.json_tree_match(profile, $1::eql_v4.query_json_predicate)` returns +the same rows. The variables are any value `encoding/json` writes as an +object, or `nil` for none. The grammar is paths of `.key`, `."key"` and +`[*]`; `==`, `!=`, `<`, `<=`, `>`, `>=` with a variable or a JSON literal; +`exists(path)` and `exists(path ? (@.a == $x && @.b > $y))`; `&&`, `||` and +`!(…)`. An ordering, and each member of a filter of more than one condition, +needs its path in `ordered=`. Anything else is refused as a `*Diagnostic` +wrapping `ErrEncoding`, whose `Reason()`, `Construct()` and `At()` give the +reason, the construct in the grammar's words and the byte offset in the +predicate, never its text or the values. Each row answers as +`jsonb_path_match` does over its plaintext, save four cases the stashgen +README lists: lax unwrapping and wrapping of arrays are not applied, a +string compared with a number is false under `==`, and orderings and +multi-condition filters compare folded terms. The tree is evaluated inside a +function, so no index engages: where a query needs one, write its leaves +with `EqAt`, `Contains` and `At.`, whose `@>` and `->` comparisons +use the GIN and btree indexes, or put one beside `Match` to narrow the rows. + ## Building The package embeds `wasm/stack_encrypt_guest.wasm`, a build artefact of the diff --git a/languages/golang/encrypt/checker.go b/languages/golang/encrypt/checker.go index 832b63426..430f7e752 100644 --- a/languages/golang/encrypt/checker.go +++ b/languages/golang/encrypt/checker.go @@ -258,7 +258,7 @@ func parseOperators(value any) ([]record.Operator, error) { op.Index, err = parseOperatorIndex(s) case "input": switch s { - case "value", "pair", "json", "path_json", "path_pair", "path", "path_bound", "path_predicates": + case "value", "pair", "json", "path_json", "path_pair", "path", "path_bound", "path_predicates", "predicate": op.Input = s default: return nil, fmt.Errorf("operator %d: unknown input %q", i, s) @@ -282,14 +282,14 @@ func parseOperators(value any) ([]record.Operator, error) { // of the names the engine's OpSpec::parse accepts, the scalar operators // (eq, lt, lte, gt, gte, between, matches) and a document's (contains, // contained_by, eq_at, the orderings at a path, between_at, selector, -// subtree, children, child_count, element_match). The catalog (se_targets) and se_query_term are -// read through this, parseFidelity and parseOperatorIndex, so the two -// readers refuse the same values. +// subtree, children, child_count, element_match, match). The catalog +// (se_targets) and se_query_term are read through this, parseFidelity and +// parseOperatorIndex, so the two readers refuse the same values. func parseOperatorName(s string) (string, error) { switch s { case "eq", "lt", "lte", "gt", "gte", "between", "matches", "contains", "contained_by", "eq_at", "lt_at", "lte_at", "gt_at", "gte_at", "between_at", - "selector", "subtree", "children", "child_count", "element_match": + "selector", "subtree", "children", "child_count", "element_match", "match": return s, nil } return "", fmt.Errorf("unknown operator %q", s) diff --git a/languages/golang/encrypt/doc.go b/languages/golang/encrypt/doc.go index 088449c61..75048beb5 100644 --- a/languages/golang/encrypt/doc.go +++ b/languages/golang/encrypt/doc.go @@ -258,6 +258,22 @@ // array is longer than [JSONReadOptions.WildcardBound] (64 by default) // fails the query with SQLSTATE 22023. // +// A whole filter binds as one parameter with the field's Match: a jsonpath +// predicate and its variables, as plaintext jsonb_path_match takes them. +// The engine parses the predicate and compiles it into a tree of the +// operands above; nothing in Go reads it. +// +// q, err := customers.Fields.Profile.Match(ctx, cipher, +// `$."name" == $name && $."age" < $max`, +// map[string]any{"name": "Alice", "max": 40}, encrypt.JSONReadOptions{}) +// // WHERE profile @@ $1::eql_v4.query_json_predicate +// +// A predicate the engine cannot answer is refused as a [Diagnostic] +// wrapping [ErrEncoding], whose Reason, Construct and At name the refusal +// and the byte offset in the predicate where it starts. The tree is +// evaluated inside eql_v4.json_tree_match, so no index engages: a query +// that needs one writes its leaves with EqAt, Contains and At.. +// // # Cancellation // // Every call takes a [context.Context], and a call whose context is diff --git a/languages/golang/encrypt/eql/eql.go b/languages/golang/encrypt/eql/eql.go index d6efdcb1a..82765a777 100644 --- a/languages/golang/encrypt/eql/eql.go +++ b/languages/golang/encrypt/eql/eql.go @@ -8,7 +8,11 @@ // the guest returns and never assembles an EQL value itself (ADR-0007, // amended 2026-10-06). Each type implements driver.Valuer and sql.Scanner // for one column, and json.Marshaler, which writes the value as the JSON -// it is. +// it is. It prints (fmt.Stringer, fmt.Formatter, for every verb) and logs +// (slog.LogValuer) as its type's name and never its bytes, so fmt.Sprint +// and %s do not give its JSON: a stored value holds ciphertext and terms, +// and a query value terms. The JSON is the value itself, a []byte: +// convert it (string(v), json.RawMessage(v)), or call MarshalJSON. // // Importing this package links the build of the engine that holds the EQL // types: eql_gen.go's init registers the embedded module with package @@ -28,6 +32,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "log/slog" ) // Type is one EQL type as the catalog describes it: what stashgen learns @@ -121,3 +127,26 @@ func unmarshal(dst *[]byte, b []byte) error { *dst = append([]byte(nil), b...) return nil } + +// redacted stands in for an EQL value's bytes in what it prints and logs. +const redacted = "[redacted]" + +// text is what an EQL value prints: its type's name, and [redacted] for +// its bytes, or {} when it is empty. +func text(b []byte, kind string) string { + if len(b) == 0 { + return "eql." + kind + "{}" + } + return "eql." + kind + "{" + redacted + "}" +} + +// format writes [text] for every verb fmt hands an EQL value. +func format(f fmt.State, b []byte, kind string) { + _, _ = io.WriteString(f, text(b, kind)) +} + +// logValue is what slog records of an EQL value: the [text] it prints, +// never its bytes. +func logValue(b []byte, kind string) slog.Value { + return slog.StringValue(text(b, kind)) +} diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index d29569c59..424662c8f 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -2,7 +2,11 @@ package eql -import "database/sql/driver" +import ( + "database/sql/driver" + "fmt" + "log/slog" +) // Types is every EQL type the catalog has, in catalog order: what the // engine's se_targets export lists, as Go. Producible says whether the @@ -86,6 +90,21 @@ func (v *Integer) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Integer) String() string { + return text(v, "Integer") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Integer) Format(f fmt.State, _ rune) { + format(f, v, "Integer") +} + +// LogValue is what slog records: String. +func (v Integer) LogValue() slog.Value { + return logValue(v, "Integer") +} + // IntegerEq is the EQL type stored in public.eql_v4_integer_eq: the integer family with the eq index. // The engine produces it from an int32. type IntegerEq []byte @@ -110,6 +129,21 @@ func (v *IntegerEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerEq) String() string { + return text(v, "IntegerEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerEq) Format(f fmt.State, _ rune) { + format(f, v, "IntegerEq") +} + +// LogValue is what slog records: String. +func (v IntegerEq) LogValue() slog.Value { + return logValue(v, "IntegerEq") +} + // IntegerEqQuery is the query value for IntegerEq: the operand eql_v4.query_integer_eq takes. type IntegerEqQuery []byte @@ -133,6 +167,21 @@ func (v *IntegerEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerEqQuery) String() string { + return text(v, "IntegerEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerEqQuery") +} + +// LogValue is what slog records: String. +func (v IntegerEqQuery) LogValue() slog.Value { + return logValue(v, "IntegerEqQuery") +} + // IntegerOrdOre is the EQL type stored in public.eql_v4_integer_ord_ore: the integer family with the ore_block index. // The engine produces it from an int32. type IntegerOrdOre []byte @@ -157,6 +206,21 @@ func (v *IntegerOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrdOre) String() string { + return text(v, "IntegerOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrdOre") +} + +// LogValue is what slog records: String. +func (v IntegerOrdOre) LogValue() slog.Value { + return logValue(v, "IntegerOrdOre") +} + // IntegerOrdOreQuery is the query value for IntegerOrdOre: the operand eql_v4.query_integer_ord_ore takes. type IntegerOrdOreQuery []byte @@ -180,6 +244,21 @@ func (v *IntegerOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrdOreQuery) String() string { + return text(v, "IntegerOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v IntegerOrdOreQuery) LogValue() slog.Value { + return logValue(v, "IntegerOrdOreQuery") +} + // IntegerOrd is the EQL type stored in public.eql_v4_integer_ord: the integer family with the ope index. // The engine produces it from an int32. type IntegerOrd []byte @@ -204,6 +283,21 @@ func (v *IntegerOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrd) String() string { + return text(v, "IntegerOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrd) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrd") +} + +// LogValue is what slog records: String. +func (v IntegerOrd) LogValue() slog.Value { + return logValue(v, "IntegerOrd") +} + // IntegerOrdQuery is the query value for IntegerOrd: the operand eql_v4.query_integer_ord takes. type IntegerOrdQuery []byte @@ -227,6 +321,21 @@ func (v *IntegerOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrdQuery) String() string { + return text(v, "IntegerOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrdQuery") +} + +// LogValue is what slog records: String. +func (v IntegerOrdQuery) LogValue() slog.Value { + return logValue(v, "IntegerOrdQuery") +} + // IntegerOrdOpe is the EQL type stored in public.eql_v4_integer_ord_ope: the integer family with the ope index. // The engine produces it from an int32. type IntegerOrdOpe []byte @@ -251,6 +360,21 @@ func (v *IntegerOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrdOpe) String() string { + return text(v, "IntegerOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrdOpe") +} + +// LogValue is what slog records: String. +func (v IntegerOrdOpe) LogValue() slog.Value { + return logValue(v, "IntegerOrdOpe") +} + // IntegerOrdOpeQuery is the query value for IntegerOrdOpe: the operand eql_v4.query_integer_ord_ope takes. type IntegerOrdOpeQuery []byte @@ -274,6 +398,21 @@ func (v *IntegerOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerOrdOpeQuery) String() string { + return text(v, "IntegerOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v IntegerOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "IntegerOrdOpeQuery") +} + // Smallint is the EQL type stored in public.eql_v4_smallint: the smallint family with no index. // The engine produces it from an int16. type Smallint []byte @@ -298,6 +437,21 @@ func (v *Smallint) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Smallint) String() string { + return text(v, "Smallint") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Smallint) Format(f fmt.State, _ rune) { + format(f, v, "Smallint") +} + +// LogValue is what slog records: String. +func (v Smallint) LogValue() slog.Value { + return logValue(v, "Smallint") +} + // SmallintEq is the EQL type stored in public.eql_v4_smallint_eq: the smallint family with the eq index. // The engine produces it from an int16. type SmallintEq []byte @@ -322,6 +476,21 @@ func (v *SmallintEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintEq) String() string { + return text(v, "SmallintEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintEq) Format(f fmt.State, _ rune) { + format(f, v, "SmallintEq") +} + +// LogValue is what slog records: String. +func (v SmallintEq) LogValue() slog.Value { + return logValue(v, "SmallintEq") +} + // SmallintEqQuery is the query value for SmallintEq: the operand eql_v4.query_smallint_eq takes. type SmallintEqQuery []byte @@ -345,6 +514,21 @@ func (v *SmallintEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintEqQuery) String() string { + return text(v, "SmallintEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintEqQuery") +} + +// LogValue is what slog records: String. +func (v SmallintEqQuery) LogValue() slog.Value { + return logValue(v, "SmallintEqQuery") +} + // SmallintOrdOre is the EQL type stored in public.eql_v4_smallint_ord_ore: the smallint family with the ore_block index. // The engine produces it from an int16. type SmallintOrdOre []byte @@ -369,6 +553,21 @@ func (v *SmallintOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrdOre) String() string { + return text(v, "SmallintOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrdOre") +} + +// LogValue is what slog records: String. +func (v SmallintOrdOre) LogValue() slog.Value { + return logValue(v, "SmallintOrdOre") +} + // SmallintOrdOreQuery is the query value for SmallintOrdOre: the operand eql_v4.query_smallint_ord_ore takes. type SmallintOrdOreQuery []byte @@ -392,6 +591,21 @@ func (v *SmallintOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrdOreQuery) String() string { + return text(v, "SmallintOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v SmallintOrdOreQuery) LogValue() slog.Value { + return logValue(v, "SmallintOrdOreQuery") +} + // SmallintOrd is the EQL type stored in public.eql_v4_smallint_ord: the smallint family with the ope index. // The engine produces it from an int16. type SmallintOrd []byte @@ -416,6 +630,21 @@ func (v *SmallintOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrd) String() string { + return text(v, "SmallintOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrd) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrd") +} + +// LogValue is what slog records: String. +func (v SmallintOrd) LogValue() slog.Value { + return logValue(v, "SmallintOrd") +} + // SmallintOrdQuery is the query value for SmallintOrd: the operand eql_v4.query_smallint_ord takes. type SmallintOrdQuery []byte @@ -439,6 +668,21 @@ func (v *SmallintOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrdQuery) String() string { + return text(v, "SmallintOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrdQuery") +} + +// LogValue is what slog records: String. +func (v SmallintOrdQuery) LogValue() slog.Value { + return logValue(v, "SmallintOrdQuery") +} + // SmallintOrdOpe is the EQL type stored in public.eql_v4_smallint_ord_ope: the smallint family with the ope index. // The engine produces it from an int16. type SmallintOrdOpe []byte @@ -463,6 +707,21 @@ func (v *SmallintOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrdOpe) String() string { + return text(v, "SmallintOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrdOpe") +} + +// LogValue is what slog records: String. +func (v SmallintOrdOpe) LogValue() slog.Value { + return logValue(v, "SmallintOrdOpe") +} + // SmallintOrdOpeQuery is the query value for SmallintOrdOpe: the operand eql_v4.query_smallint_ord_ope takes. type SmallintOrdOpeQuery []byte @@ -486,6 +745,21 @@ func (v *SmallintOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintOrdOpeQuery) String() string { + return text(v, "SmallintOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v SmallintOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "SmallintOrdOpeQuery") +} + // Bigint is the EQL type stored in public.eql_v4_bigint: the bigint family with no index. // The engine produces it from an int64. type Bigint []byte @@ -510,6 +784,21 @@ func (v *Bigint) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Bigint) String() string { + return text(v, "Bigint") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Bigint) Format(f fmt.State, _ rune) { + format(f, v, "Bigint") +} + +// LogValue is what slog records: String. +func (v Bigint) LogValue() slog.Value { + return logValue(v, "Bigint") +} + // BigintEq is the EQL type stored in public.eql_v4_bigint_eq: the bigint family with the eq index. // The engine produces it from an int64. type BigintEq []byte @@ -534,6 +823,21 @@ func (v *BigintEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintEq) String() string { + return text(v, "BigintEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintEq) Format(f fmt.State, _ rune) { + format(f, v, "BigintEq") +} + +// LogValue is what slog records: String. +func (v BigintEq) LogValue() slog.Value { + return logValue(v, "BigintEq") +} + // BigintEqQuery is the query value for BigintEq: the operand eql_v4.query_bigint_eq takes. type BigintEqQuery []byte @@ -557,6 +861,21 @@ func (v *BigintEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintEqQuery) String() string { + return text(v, "BigintEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintEqQuery") +} + +// LogValue is what slog records: String. +func (v BigintEqQuery) LogValue() slog.Value { + return logValue(v, "BigintEqQuery") +} + // BigintOrdOre is the EQL type stored in public.eql_v4_bigint_ord_ore: the bigint family with the ore_block index. // The engine produces it from an int64. type BigintOrdOre []byte @@ -581,6 +900,21 @@ func (v *BigintOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrdOre) String() string { + return text(v, "BigintOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrdOre") +} + +// LogValue is what slog records: String. +func (v BigintOrdOre) LogValue() slog.Value { + return logValue(v, "BigintOrdOre") +} + // BigintOrdOreQuery is the query value for BigintOrdOre: the operand eql_v4.query_bigint_ord_ore takes. type BigintOrdOreQuery []byte @@ -604,6 +938,21 @@ func (v *BigintOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrdOreQuery) String() string { + return text(v, "BigintOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v BigintOrdOreQuery) LogValue() slog.Value { + return logValue(v, "BigintOrdOreQuery") +} + // BigintOrd is the EQL type stored in public.eql_v4_bigint_ord: the bigint family with the ope index. // The engine produces it from an int64. type BigintOrd []byte @@ -628,6 +977,21 @@ func (v *BigintOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrd) String() string { + return text(v, "BigintOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrd) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrd") +} + +// LogValue is what slog records: String. +func (v BigintOrd) LogValue() slog.Value { + return logValue(v, "BigintOrd") +} + // BigintOrdQuery is the query value for BigintOrd: the operand eql_v4.query_bigint_ord takes. type BigintOrdQuery []byte @@ -651,6 +1015,21 @@ func (v *BigintOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrdQuery) String() string { + return text(v, "BigintOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrdQuery") +} + +// LogValue is what slog records: String. +func (v BigintOrdQuery) LogValue() slog.Value { + return logValue(v, "BigintOrdQuery") +} + // BigintOrdOpe is the EQL type stored in public.eql_v4_bigint_ord_ope: the bigint family with the ope index. // The engine produces it from an int64. type BigintOrdOpe []byte @@ -675,6 +1054,21 @@ func (v *BigintOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrdOpe) String() string { + return text(v, "BigintOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrdOpe") +} + +// LogValue is what slog records: String. +func (v BigintOrdOpe) LogValue() slog.Value { + return logValue(v, "BigintOrdOpe") +} + // BigintOrdOpeQuery is the query value for BigintOrdOpe: the operand eql_v4.query_bigint_ord_ope takes. type BigintOrdOpeQuery []byte @@ -698,6 +1092,21 @@ func (v *BigintOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintOrdOpeQuery) String() string { + return text(v, "BigintOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v BigintOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "BigintOrdOpeQuery") +} + // Date is the EQL type stored in public.eql_v4_date: the date family with no index. // The engine produces it from a date. type Date []byte @@ -722,6 +1131,21 @@ func (v *Date) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Date) String() string { + return text(v, "Date") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Date) Format(f fmt.State, _ rune) { + format(f, v, "Date") +} + +// LogValue is what slog records: String. +func (v Date) LogValue() slog.Value { + return logValue(v, "Date") +} + // DateEq is the EQL type stored in public.eql_v4_date_eq: the date family with the eq index. // The engine produces it from a date. type DateEq []byte @@ -746,6 +1170,21 @@ func (v *DateEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateEq) String() string { + return text(v, "DateEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateEq) Format(f fmt.State, _ rune) { + format(f, v, "DateEq") +} + +// LogValue is what slog records: String. +func (v DateEq) LogValue() slog.Value { + return logValue(v, "DateEq") +} + // DateEqQuery is the query value for DateEq: the operand eql_v4.query_date_eq takes. type DateEqQuery []byte @@ -769,6 +1208,21 @@ func (v *DateEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateEqQuery) String() string { + return text(v, "DateEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateEqQuery") +} + +// LogValue is what slog records: String. +func (v DateEqQuery) LogValue() slog.Value { + return logValue(v, "DateEqQuery") +} + // DateOrdOre is the EQL type stored in public.eql_v4_date_ord_ore: the date family with the ore_block index. // The engine produces it from a date. type DateOrdOre []byte @@ -793,6 +1247,21 @@ func (v *DateOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrdOre) String() string { + return text(v, "DateOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "DateOrdOre") +} + +// LogValue is what slog records: String. +func (v DateOrdOre) LogValue() slog.Value { + return logValue(v, "DateOrdOre") +} + // DateOrdOreQuery is the query value for DateOrdOre: the operand eql_v4.query_date_ord_ore takes. type DateOrdOreQuery []byte @@ -816,6 +1285,21 @@ func (v *DateOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrdOreQuery) String() string { + return text(v, "DateOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v DateOrdOreQuery) LogValue() slog.Value { + return logValue(v, "DateOrdOreQuery") +} + // DateOrd is the EQL type stored in public.eql_v4_date_ord: the date family with the ope index. // The engine produces it from a date. type DateOrd []byte @@ -840,6 +1324,21 @@ func (v *DateOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrd) String() string { + return text(v, "DateOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrd) Format(f fmt.State, _ rune) { + format(f, v, "DateOrd") +} + +// LogValue is what slog records: String. +func (v DateOrd) LogValue() slog.Value { + return logValue(v, "DateOrd") +} + // DateOrdQuery is the query value for DateOrd: the operand eql_v4.query_date_ord takes. type DateOrdQuery []byte @@ -863,6 +1362,21 @@ func (v *DateOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrdQuery) String() string { + return text(v, "DateOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateOrdQuery") +} + +// LogValue is what slog records: String. +func (v DateOrdQuery) LogValue() slog.Value { + return logValue(v, "DateOrdQuery") +} + // DateOrdOpe is the EQL type stored in public.eql_v4_date_ord_ope: the date family with the ope index. // The engine produces it from a date. type DateOrdOpe []byte @@ -887,6 +1401,21 @@ func (v *DateOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrdOpe) String() string { + return text(v, "DateOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "DateOrdOpe") +} + +// LogValue is what slog records: String. +func (v DateOrdOpe) LogValue() slog.Value { + return logValue(v, "DateOrdOpe") +} + // DateOrdOpeQuery is the query value for DateOrdOpe: the operand eql_v4.query_date_ord_ope takes. type DateOrdOpeQuery []byte @@ -910,6 +1439,21 @@ func (v *DateOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateOrdOpeQuery) String() string { + return text(v, "DateOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v DateOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "DateOrdOpeQuery") +} + // Timestamp is the EQL type stored in public.eql_v4_timestamp: the timestamp family with no index. // The engine produces it from a timestamp. type Timestamp []byte @@ -934,6 +1478,21 @@ func (v *Timestamp) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Timestamp) String() string { + return text(v, "Timestamp") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Timestamp) Format(f fmt.State, _ rune) { + format(f, v, "Timestamp") +} + +// LogValue is what slog records: String. +func (v Timestamp) LogValue() slog.Value { + return logValue(v, "Timestamp") +} + // TimestampEq is the EQL type stored in public.eql_v4_timestamp_eq: the timestamp family with the eq index. // The engine produces it from a timestamp. type TimestampEq []byte @@ -958,6 +1517,21 @@ func (v *TimestampEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampEq) String() string { + return text(v, "TimestampEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampEq) Format(f fmt.State, _ rune) { + format(f, v, "TimestampEq") +} + +// LogValue is what slog records: String. +func (v TimestampEq) LogValue() slog.Value { + return logValue(v, "TimestampEq") +} + // TimestampEqQuery is the query value for TimestampEq: the operand eql_v4.query_timestamp_eq takes. type TimestampEqQuery []byte @@ -981,6 +1555,21 @@ func (v *TimestampEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampEqQuery) String() string { + return text(v, "TimestampEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampEqQuery") +} + +// LogValue is what slog records: String. +func (v TimestampEqQuery) LogValue() slog.Value { + return logValue(v, "TimestampEqQuery") +} + // TimestampOrdOre is the EQL type stored in public.eql_v4_timestamp_ord_ore: the timestamp family with the ore_block index. // The engine produces it from a timestamp. type TimestampOrdOre []byte @@ -1005,6 +1594,21 @@ func (v *TimestampOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrdOre) String() string { + return text(v, "TimestampOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrdOre") +} + +// LogValue is what slog records: String. +func (v TimestampOrdOre) LogValue() slog.Value { + return logValue(v, "TimestampOrdOre") +} + // TimestampOrdOreQuery is the query value for TimestampOrdOre: the operand eql_v4.query_timestamp_ord_ore takes. type TimestampOrdOreQuery []byte @@ -1028,6 +1632,21 @@ func (v *TimestampOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrdOreQuery) String() string { + return text(v, "TimestampOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v TimestampOrdOreQuery) LogValue() slog.Value { + return logValue(v, "TimestampOrdOreQuery") +} + // TimestampOrd is the EQL type stored in public.eql_v4_timestamp_ord: the timestamp family with the ope index. // The engine produces it from a timestamp. type TimestampOrd []byte @@ -1052,6 +1671,21 @@ func (v *TimestampOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrd) String() string { + return text(v, "TimestampOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrd) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrd") +} + +// LogValue is what slog records: String. +func (v TimestampOrd) LogValue() slog.Value { + return logValue(v, "TimestampOrd") +} + // TimestampOrdQuery is the query value for TimestampOrd: the operand eql_v4.query_timestamp_ord takes. type TimestampOrdQuery []byte @@ -1075,6 +1709,21 @@ func (v *TimestampOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrdQuery) String() string { + return text(v, "TimestampOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrdQuery") +} + +// LogValue is what slog records: String. +func (v TimestampOrdQuery) LogValue() slog.Value { + return logValue(v, "TimestampOrdQuery") +} + // TimestampOrdOpe is the EQL type stored in public.eql_v4_timestamp_ord_ope: the timestamp family with the ope index. // The engine produces it from a timestamp. type TimestampOrdOpe []byte @@ -1099,6 +1748,21 @@ func (v *TimestampOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrdOpe) String() string { + return text(v, "TimestampOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrdOpe") +} + +// LogValue is what slog records: String. +func (v TimestampOrdOpe) LogValue() slog.Value { + return logValue(v, "TimestampOrdOpe") +} + // TimestampOrdOpeQuery is the query value for TimestampOrdOpe: the operand eql_v4.query_timestamp_ord_ope takes. type TimestampOrdOpeQuery []byte @@ -1122,6 +1786,21 @@ func (v *TimestampOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampOrdOpeQuery) String() string { + return text(v, "TimestampOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v TimestampOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "TimestampOrdOpeQuery") +} + // Numeric is the EQL type stored in public.eql_v4_numeric: the numeric family with no index. // The engine produces it from a decimal. type Numeric []byte @@ -1146,6 +1825,21 @@ func (v *Numeric) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Numeric) String() string { + return text(v, "Numeric") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Numeric) Format(f fmt.State, _ rune) { + format(f, v, "Numeric") +} + +// LogValue is what slog records: String. +func (v Numeric) LogValue() slog.Value { + return logValue(v, "Numeric") +} + // NumericEq is the EQL type stored in public.eql_v4_numeric_eq: the numeric family with the eq index. // The engine produces it from a decimal. type NumericEq []byte @@ -1170,6 +1864,21 @@ func (v *NumericEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericEq) String() string { + return text(v, "NumericEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericEq) Format(f fmt.State, _ rune) { + format(f, v, "NumericEq") +} + +// LogValue is what slog records: String. +func (v NumericEq) LogValue() slog.Value { + return logValue(v, "NumericEq") +} + // NumericEqQuery is the query value for NumericEq: the operand eql_v4.query_numeric_eq takes. type NumericEqQuery []byte @@ -1193,6 +1902,21 @@ func (v *NumericEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericEqQuery) String() string { + return text(v, "NumericEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericEqQuery") +} + +// LogValue is what slog records: String. +func (v NumericEqQuery) LogValue() slog.Value { + return logValue(v, "NumericEqQuery") +} + // NumericOrdOre is the EQL type stored in public.eql_v4_numeric_ord_ore: the numeric family with the ore_block index. // The engine produces it from a decimal. type NumericOrdOre []byte @@ -1217,6 +1941,21 @@ func (v *NumericOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrdOre) String() string { + return text(v, "NumericOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrdOre") +} + +// LogValue is what slog records: String. +func (v NumericOrdOre) LogValue() slog.Value { + return logValue(v, "NumericOrdOre") +} + // NumericOrdOreQuery is the query value for NumericOrdOre: the operand eql_v4.query_numeric_ord_ore takes. type NumericOrdOreQuery []byte @@ -1240,6 +1979,21 @@ func (v *NumericOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrdOreQuery) String() string { + return text(v, "NumericOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v NumericOrdOreQuery) LogValue() slog.Value { + return logValue(v, "NumericOrdOreQuery") +} + // NumericOrd is the EQL type stored in public.eql_v4_numeric_ord: the numeric family with the ope index. // The engine produces it from a decimal. type NumericOrd []byte @@ -1264,6 +2018,21 @@ func (v *NumericOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrd) String() string { + return text(v, "NumericOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrd) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrd") +} + +// LogValue is what slog records: String. +func (v NumericOrd) LogValue() slog.Value { + return logValue(v, "NumericOrd") +} + // NumericOrdQuery is the query value for NumericOrd: the operand eql_v4.query_numeric_ord takes. type NumericOrdQuery []byte @@ -1287,6 +2056,21 @@ func (v *NumericOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrdQuery) String() string { + return text(v, "NumericOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrdQuery") +} + +// LogValue is what slog records: String. +func (v NumericOrdQuery) LogValue() slog.Value { + return logValue(v, "NumericOrdQuery") +} + // NumericOrdOpe is the EQL type stored in public.eql_v4_numeric_ord_ope: the numeric family with the ope index. // The engine produces it from a decimal. type NumericOrdOpe []byte @@ -1311,6 +2095,21 @@ func (v *NumericOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrdOpe) String() string { + return text(v, "NumericOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrdOpe") +} + +// LogValue is what slog records: String. +func (v NumericOrdOpe) LogValue() slog.Value { + return logValue(v, "NumericOrdOpe") +} + // NumericOrdOpeQuery is the query value for NumericOrdOpe: the operand eql_v4.query_numeric_ord_ope takes. type NumericOrdOpeQuery []byte @@ -1334,6 +2133,21 @@ func (v *NumericOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericOrdOpeQuery) String() string { + return text(v, "NumericOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v NumericOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "NumericOrdOpeQuery") +} + // Text is the EQL type stored in public.eql_v4_text: the text family with no index. // The engine produces it from a string. type Text []byte @@ -1358,6 +2172,21 @@ func (v *Text) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Text) String() string { + return text(v, "Text") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Text) Format(f fmt.State, _ rune) { + format(f, v, "Text") +} + +// LogValue is what slog records: String. +func (v Text) LogValue() slog.Value { + return logValue(v, "Text") +} + // TextEq is the EQL type stored in public.eql_v4_text_eq: the text family with the eq index. // The engine produces it from a string. type TextEq []byte @@ -1382,6 +2211,21 @@ func (v *TextEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextEq) String() string { + return text(v, "TextEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextEq) Format(f fmt.State, _ rune) { + format(f, v, "TextEq") +} + +// LogValue is what slog records: String. +func (v TextEq) LogValue() slog.Value { + return logValue(v, "TextEq") +} + // TextEqQuery is the query value for TextEq: the operand eql_v4.query_text_eq takes. type TextEqQuery []byte @@ -1405,6 +2249,21 @@ func (v *TextEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextEqQuery) String() string { + return text(v, "TextEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextEqQuery") +} + +// LogValue is what slog records: String. +func (v TextEqQuery) LogValue() slog.Value { + return logValue(v, "TextEqQuery") +} + // TextMatch is the EQL type stored in public.eql_v4_text_match: the text family with the match index. // The engine produces it from a string. type TextMatch []byte @@ -1429,6 +2288,21 @@ func (v *TextMatch) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextMatch) String() string { + return text(v, "TextMatch") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextMatch) Format(f fmt.State, _ rune) { + format(f, v, "TextMatch") +} + +// LogValue is what slog records: String. +func (v TextMatch) LogValue() slog.Value { + return logValue(v, "TextMatch") +} + // TextMatchQuery is the query value for TextMatch: the operand eql_v4.query_text_match takes. type TextMatchQuery []byte @@ -1452,6 +2326,21 @@ func (v *TextMatchQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextMatchQuery) String() string { + return text(v, "TextMatchQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextMatchQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextMatchQuery") +} + +// LogValue is what slog records: String. +func (v TextMatchQuery) LogValue() slog.Value { + return logValue(v, "TextMatchQuery") +} + // TextOrdOre is the EQL type stored in public.eql_v4_text_ord_ore: the text family with the eq and ore_block indexes. // The engine produces it from a string. type TextOrdOre []byte @@ -1476,6 +2365,21 @@ func (v *TextOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrdOre) String() string { + return text(v, "TextOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "TextOrdOre") +} + +// LogValue is what slog records: String. +func (v TextOrdOre) LogValue() slog.Value { + return logValue(v, "TextOrdOre") +} + // TextOrdOreQuery is the query value for TextOrdOre: the operand eql_v4.query_text_ord_ore takes. type TextOrdOreQuery []byte @@ -1499,6 +2403,21 @@ func (v *TextOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrdOreQuery) String() string { + return text(v, "TextOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v TextOrdOreQuery) LogValue() slog.Value { + return logValue(v, "TextOrdOreQuery") +} + // TextOrd is the EQL type stored in public.eql_v4_text_ord: the text family with the eq and ope indexes. // The engine produces it from a string. type TextOrd []byte @@ -1523,6 +2442,21 @@ func (v *TextOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrd) String() string { + return text(v, "TextOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrd) Format(f fmt.State, _ rune) { + format(f, v, "TextOrd") +} + +// LogValue is what slog records: String. +func (v TextOrd) LogValue() slog.Value { + return logValue(v, "TextOrd") +} + // TextOrdQuery is the query value for TextOrd: the operand eql_v4.query_text_ord takes. type TextOrdQuery []byte @@ -1546,6 +2480,21 @@ func (v *TextOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrdQuery) String() string { + return text(v, "TextOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextOrdQuery") +} + +// LogValue is what slog records: String. +func (v TextOrdQuery) LogValue() slog.Value { + return logValue(v, "TextOrdQuery") +} + // TextOrdOpe is the EQL type stored in public.eql_v4_text_ord_ope: the text family with the eq and ope indexes. // The engine produces it from a string. type TextOrdOpe []byte @@ -1570,6 +2519,21 @@ func (v *TextOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrdOpe) String() string { + return text(v, "TextOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "TextOrdOpe") +} + +// LogValue is what slog records: String. +func (v TextOrdOpe) LogValue() slog.Value { + return logValue(v, "TextOrdOpe") +} + // TextOrdOpeQuery is the query value for TextOrdOpe: the operand eql_v4.query_text_ord_ope takes. type TextOrdOpeQuery []byte @@ -1593,6 +2557,21 @@ func (v *TextOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextOrdOpeQuery) String() string { + return text(v, "TextOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v TextOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "TextOrdOpeQuery") +} + // TextSearchOre is the EQL type stored in public.eql_v4_text_search_ore: the text family with the eq and ore_block and match indexes. // The engine produces it from a string. type TextSearchOre []byte @@ -1617,6 +2596,21 @@ func (v *TextSearchOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextSearchOre) String() string { + return text(v, "TextSearchOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextSearchOre) Format(f fmt.State, _ rune) { + format(f, v, "TextSearchOre") +} + +// LogValue is what slog records: String. +func (v TextSearchOre) LogValue() slog.Value { + return logValue(v, "TextSearchOre") +} + // TextSearchOreQuery is the query value for TextSearchOre: the operand eql_v4.query_text_search_ore takes. type TextSearchOreQuery []byte @@ -1640,6 +2634,21 @@ func (v *TextSearchOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextSearchOreQuery) String() string { + return text(v, "TextSearchOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextSearchOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextSearchOreQuery") +} + +// LogValue is what slog records: String. +func (v TextSearchOreQuery) LogValue() slog.Value { + return logValue(v, "TextSearchOreQuery") +} + // TextSearch is the EQL type stored in public.eql_v4_text_search: the text family with the eq and ope and match indexes. // The engine produces it from a string. type TextSearch []byte @@ -1664,6 +2673,21 @@ func (v *TextSearch) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextSearch) String() string { + return text(v, "TextSearch") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextSearch) Format(f fmt.State, _ rune) { + format(f, v, "TextSearch") +} + +// LogValue is what slog records: String. +func (v TextSearch) LogValue() slog.Value { + return logValue(v, "TextSearch") +} + // TextSearchQuery is the query value for TextSearch: the operand eql_v4.query_text_search takes. type TextSearchQuery []byte @@ -1687,6 +2711,21 @@ func (v *TextSearchQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextSearchQuery) String() string { + return text(v, "TextSearchQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextSearchQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextSearchQuery") +} + +// LogValue is what slog records: String. +func (v TextSearchQuery) LogValue() slog.Value { + return logValue(v, "TextSearchQuery") +} + // Boolean is the EQL type stored in public.eql_v4_boolean: the boolean family with no index. // The engine produces it from a bool. type Boolean []byte @@ -1711,6 +2750,21 @@ func (v *Boolean) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Boolean) String() string { + return text(v, "Boolean") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Boolean) Format(f fmt.State, _ rune) { + format(f, v, "Boolean") +} + +// LogValue is what slog records: String. +func (v Boolean) LogValue() slog.Value { + return logValue(v, "Boolean") +} + // Real is the EQL type stored in public.eql_v4_real: the real family with no index. // The engine produces it from a float32. type Real []byte @@ -1730,9 +2784,24 @@ func (v Real) MarshalJSON() ([]byte, error) { return marshal(v, "Real") } -// UnmarshalJSON reads the EQL value from JSON, as it is. -func (v *Real) UnmarshalJSON(b []byte) error { - return unmarshal((*[]byte)(v), b) +// UnmarshalJSON reads the EQL value from JSON, as it is. +func (v *Real) UnmarshalJSON(b []byte) error { + return unmarshal((*[]byte)(v), b) +} + +// String names the type and hides the value. +func (v Real) String() string { + return text(v, "Real") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Real) Format(f fmt.State, _ rune) { + format(f, v, "Real") +} + +// LogValue is what slog records: String. +func (v Real) LogValue() slog.Value { + return logValue(v, "Real") } // RealEq is the EQL type stored in public.eql_v4_real_eq: the real family with the eq index. @@ -1759,6 +2828,21 @@ func (v *RealEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealEq) String() string { + return text(v, "RealEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealEq) Format(f fmt.State, _ rune) { + format(f, v, "RealEq") +} + +// LogValue is what slog records: String. +func (v RealEq) LogValue() slog.Value { + return logValue(v, "RealEq") +} + // RealEqQuery is the query value for RealEq: the operand eql_v4.query_real_eq takes. type RealEqQuery []byte @@ -1782,6 +2866,21 @@ func (v *RealEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealEqQuery) String() string { + return text(v, "RealEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealEqQuery") +} + +// LogValue is what slog records: String. +func (v RealEqQuery) LogValue() slog.Value { + return logValue(v, "RealEqQuery") +} + // RealOrdOre is the EQL type stored in public.eql_v4_real_ord_ore: the real family with the ore_block index. // The engine produces it from a float32. type RealOrdOre []byte @@ -1806,6 +2905,21 @@ func (v *RealOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrdOre) String() string { + return text(v, "RealOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "RealOrdOre") +} + +// LogValue is what slog records: String. +func (v RealOrdOre) LogValue() slog.Value { + return logValue(v, "RealOrdOre") +} + // RealOrdOreQuery is the query value for RealOrdOre: the operand eql_v4.query_real_ord_ore takes. type RealOrdOreQuery []byte @@ -1829,6 +2943,21 @@ func (v *RealOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrdOreQuery) String() string { + return text(v, "RealOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v RealOrdOreQuery) LogValue() slog.Value { + return logValue(v, "RealOrdOreQuery") +} + // RealOrd is the EQL type stored in public.eql_v4_real_ord: the real family with the ope index. // The engine produces it from a float32. type RealOrd []byte @@ -1853,6 +2982,21 @@ func (v *RealOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrd) String() string { + return text(v, "RealOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrd) Format(f fmt.State, _ rune) { + format(f, v, "RealOrd") +} + +// LogValue is what slog records: String. +func (v RealOrd) LogValue() slog.Value { + return logValue(v, "RealOrd") +} + // RealOrdQuery is the query value for RealOrd: the operand eql_v4.query_real_ord takes. type RealOrdQuery []byte @@ -1876,6 +3020,21 @@ func (v *RealOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrdQuery) String() string { + return text(v, "RealOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealOrdQuery") +} + +// LogValue is what slog records: String. +func (v RealOrdQuery) LogValue() slog.Value { + return logValue(v, "RealOrdQuery") +} + // RealOrdOpe is the EQL type stored in public.eql_v4_real_ord_ope: the real family with the ope index. // The engine produces it from a float32. type RealOrdOpe []byte @@ -1900,6 +3059,21 @@ func (v *RealOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrdOpe) String() string { + return text(v, "RealOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "RealOrdOpe") +} + +// LogValue is what slog records: String. +func (v RealOrdOpe) LogValue() slog.Value { + return logValue(v, "RealOrdOpe") +} + // RealOrdOpeQuery is the query value for RealOrdOpe: the operand eql_v4.query_real_ord_ope takes. type RealOrdOpeQuery []byte @@ -1923,6 +3097,21 @@ func (v *RealOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealOrdOpeQuery) String() string { + return text(v, "RealOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v RealOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "RealOrdOpeQuery") +} + // Double is the EQL type stored in public.eql_v4_double: the double family with no index. // The engine produces it from a float64. type Double []byte @@ -1947,6 +3136,21 @@ func (v *Double) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v Double) String() string { + return text(v, "Double") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v Double) Format(f fmt.State, _ rune) { + format(f, v, "Double") +} + +// LogValue is what slog records: String. +func (v Double) LogValue() slog.Value { + return logValue(v, "Double") +} + // DoubleEq is the EQL type stored in public.eql_v4_double_eq: the double family with the eq index. // The engine produces it from a float64. type DoubleEq []byte @@ -1971,6 +3175,21 @@ func (v *DoubleEq) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleEq) String() string { + return text(v, "DoubleEq") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleEq) Format(f fmt.State, _ rune) { + format(f, v, "DoubleEq") +} + +// LogValue is what slog records: String. +func (v DoubleEq) LogValue() slog.Value { + return logValue(v, "DoubleEq") +} + // DoubleEqQuery is the query value for DoubleEq: the operand eql_v4.query_double_eq takes. type DoubleEqQuery []byte @@ -1994,6 +3213,21 @@ func (v *DoubleEqQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleEqQuery) String() string { + return text(v, "DoubleEqQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleEqQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleEqQuery") +} + +// LogValue is what slog records: String. +func (v DoubleEqQuery) LogValue() slog.Value { + return logValue(v, "DoubleEqQuery") +} + // DoubleOrdOre is the EQL type stored in public.eql_v4_double_ord_ore: the double family with the ore_block index. // The engine produces it from a float64. type DoubleOrdOre []byte @@ -2018,6 +3252,21 @@ func (v *DoubleOrdOre) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrdOre) String() string { + return text(v, "DoubleOrdOre") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrdOre) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrdOre") +} + +// LogValue is what slog records: String. +func (v DoubleOrdOre) LogValue() slog.Value { + return logValue(v, "DoubleOrdOre") +} + // DoubleOrdOreQuery is the query value for DoubleOrdOre: the operand eql_v4.query_double_ord_ore takes. type DoubleOrdOreQuery []byte @@ -2041,6 +3290,21 @@ func (v *DoubleOrdOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrdOreQuery) String() string { + return text(v, "DoubleOrdOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrdOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrdOreQuery") +} + +// LogValue is what slog records: String. +func (v DoubleOrdOreQuery) LogValue() slog.Value { + return logValue(v, "DoubleOrdOreQuery") +} + // DoubleOrd is the EQL type stored in public.eql_v4_double_ord: the double family with the ope index. // The engine produces it from a float64. type DoubleOrd []byte @@ -2065,6 +3329,21 @@ func (v *DoubleOrd) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrd) String() string { + return text(v, "DoubleOrd") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrd) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrd") +} + +// LogValue is what slog records: String. +func (v DoubleOrd) LogValue() slog.Value { + return logValue(v, "DoubleOrd") +} + // DoubleOrdQuery is the query value for DoubleOrd: the operand eql_v4.query_double_ord takes. type DoubleOrdQuery []byte @@ -2088,6 +3367,21 @@ func (v *DoubleOrdQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrdQuery) String() string { + return text(v, "DoubleOrdQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrdQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrdQuery") +} + +// LogValue is what slog records: String. +func (v DoubleOrdQuery) LogValue() slog.Value { + return logValue(v, "DoubleOrdQuery") +} + // DoubleOrdOpe is the EQL type stored in public.eql_v4_double_ord_ope: the double family with the ope index. // The engine produces it from a float64. type DoubleOrdOpe []byte @@ -2112,6 +3406,21 @@ func (v *DoubleOrdOpe) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrdOpe) String() string { + return text(v, "DoubleOrdOpe") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrdOpe) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrdOpe") +} + +// LogValue is what slog records: String. +func (v DoubleOrdOpe) LogValue() slog.Value { + return logValue(v, "DoubleOrdOpe") +} + // DoubleOrdOpeQuery is the query value for DoubleOrdOpe: the operand eql_v4.query_double_ord_ope takes. type DoubleOrdOpeQuery []byte @@ -2135,6 +3444,21 @@ func (v *DoubleOrdOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleOrdOpeQuery) String() string { + return text(v, "DoubleOrdOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleOrdOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleOrdOpeQuery") +} + +// LogValue is what slog records: String. +func (v DoubleOrdOpeQuery) LogValue() slog.Value { + return logValue(v, "DoubleOrdOpeQuery") +} + // JSON is the EQL type stored in public.eql_v4_json: the json family with no index. // The engine cannot produce it yet: a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document becomes one sealed leaf is not decided. type JSON []byte @@ -2159,6 +3483,21 @@ func (v *JSON) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSON) String() string { + return text(v, "JSON") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSON) Format(f fmt.State, _ rune) { + format(f, v, "JSON") +} + +// LogValue is what slog records: String. +func (v JSON) LogValue() slog.Value { + return logValue(v, "JSON") +} + // JSONSearch is the EQL type stored in public.eql_v4_json_search: the json_tree family with the json index. // The engine produces it from a json. type JSONSearch []byte @@ -2183,6 +3522,21 @@ func (v *JSONSearch) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONSearch) String() string { + return text(v, "JSONSearch") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONSearch) Format(f fmt.State, _ rune) { + format(f, v, "JSONSearch") +} + +// LogValue is what slog records: String. +func (v JSONSearch) LogValue() slog.Value { + return logValue(v, "JSONSearch") +} + // JSONSearchQuery is the query value for JSONSearch: the operand eql_v4.query_json takes. type JSONSearchQuery []byte @@ -2206,6 +3560,21 @@ func (v *JSONSearchQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONSearchQuery) String() string { + return text(v, "JSONSearchQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONSearchQuery) Format(f fmt.State, _ rune) { + format(f, v, "JSONSearchQuery") +} + +// LogValue is what slog records: String. +func (v JSONSearchQuery) LogValue() slog.Value { + return logValue(v, "JSONSearchQuery") +} + // JSONSearchLeafQuery is the json_tree family's ordering operand at a path, compared with an entry `->` extracts: the value eql_v4.query_json_leaf holds. type JSONSearchLeafQuery []byte @@ -2229,6 +3598,21 @@ func (v *JSONSearchLeafQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONSearchLeafQuery) String() string { + return text(v, "JSONSearchLeafQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONSearchLeafQuery) Format(f fmt.State, _ rune) { + format(f, v, "JSONSearchLeafQuery") +} + +// LogValue is what slog records: String. +func (v JSONSearchLeafQuery) LogValue() slog.Value { + return logValue(v, "JSONSearchLeafQuery") +} + // JSONSearchElementsQuery is the json_tree family's element-scoped predicate, which eql_v4.json_tree_element_match tests: the value eql_v4.query_json_elements holds. type JSONSearchElementsQuery []byte @@ -2252,6 +3636,21 @@ func (v *JSONSearchElementsQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONSearchElementsQuery) String() string { + return text(v, "JSONSearchElementsQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONSearchElementsQuery) Format(f fmt.State, _ rune) { + format(f, v, "JSONSearchElementsQuery") +} + +// LogValue is what slog records: String. +func (v JSONSearchElementsQuery) LogValue() slog.Value { + return logValue(v, "JSONSearchElementsQuery") +} + // JSONSearchPredicateQuery is the json_tree family's compiled predicate, a boolean tree of the other operands, which eql_v4.json_tree_match and `@@` evaluate: the value eql_v4.query_json_predicate holds. type JSONSearchPredicateQuery []byte @@ -2275,6 +3674,21 @@ func (v *JSONSearchPredicateQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONSearchPredicateQuery) String() string { + return text(v, "JSONSearchPredicateQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONSearchPredicateQuery) Format(f fmt.State, _ rune) { + format(f, v, "JSONSearchPredicateQuery") +} + +// LogValue is what slog records: String. +func (v JSONSearchPredicateQuery) LogValue() slog.Value { + return logValue(v, "JSONSearchPredicateQuery") +} + // JSONEntry is the json_tree family's entry: one node of a document, as `->` and the walks return it: the value public.eql_v4_json_entry holds. type JSONEntry []byte @@ -2298,6 +3712,21 @@ func (v *JSONEntry) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v JSONEntry) String() string { + return text(v, "JSONEntry") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v JSONEntry) Format(f fmt.State, _ rune) { + format(f, v, "JSONEntry") +} + +// LogValue is what slog records: String. +func (v JSONEntry) LogValue() slog.Value { + return logValue(v, "JSONEntry") +} + // IntegerRangeOreQuery is a range query value over the ore_block index of the integer family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_integer_range_ore takes in eql_v4.between. It ranges over IntegerOrdOre. type IntegerRangeOreQuery []byte @@ -2322,6 +3751,21 @@ func (v *IntegerRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerRangeOreQuery) String() string { + return text(v, "IntegerRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v IntegerRangeOreQuery) LogValue() slog.Value { + return logValue(v, "IntegerRangeOreQuery") +} + // IntegerRangeOpeQuery is a range query value over the ope index of the integer family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_integer_range_ope takes in eql_v4.between. It ranges over IntegerOrd, IntegerOrdOpe. type IntegerRangeOpeQuery []byte @@ -2346,6 +3790,21 @@ func (v *IntegerRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v IntegerRangeOpeQuery) String() string { + return text(v, "IntegerRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v IntegerRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "IntegerRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v IntegerRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "IntegerRangeOpeQuery") +} + // SmallintRangeOreQuery is a range query value over the ore_block index of the smallint family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_smallint_range_ore takes in eql_v4.between. It ranges over SmallintOrdOre. type SmallintRangeOreQuery []byte @@ -2370,6 +3829,21 @@ func (v *SmallintRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintRangeOreQuery) String() string { + return text(v, "SmallintRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v SmallintRangeOreQuery) LogValue() slog.Value { + return logValue(v, "SmallintRangeOreQuery") +} + // SmallintRangeOpeQuery is a range query value over the ope index of the smallint family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_smallint_range_ope takes in eql_v4.between. It ranges over SmallintOrd, SmallintOrdOpe. type SmallintRangeOpeQuery []byte @@ -2394,6 +3868,21 @@ func (v *SmallintRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v SmallintRangeOpeQuery) String() string { + return text(v, "SmallintRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v SmallintRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "SmallintRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v SmallintRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "SmallintRangeOpeQuery") +} + // BigintRangeOreQuery is a range query value over the ore_block index of the bigint family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_bigint_range_ore takes in eql_v4.between. It ranges over BigintOrdOre. type BigintRangeOreQuery []byte @@ -2418,6 +3907,21 @@ func (v *BigintRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintRangeOreQuery) String() string { + return text(v, "BigintRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v BigintRangeOreQuery) LogValue() slog.Value { + return logValue(v, "BigintRangeOreQuery") +} + // BigintRangeOpeQuery is a range query value over the ope index of the bigint family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_bigint_range_ope takes in eql_v4.between. It ranges over BigintOrd, BigintOrdOpe. type BigintRangeOpeQuery []byte @@ -2442,6 +3946,21 @@ func (v *BigintRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v BigintRangeOpeQuery) String() string { + return text(v, "BigintRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v BigintRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "BigintRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v BigintRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "BigintRangeOpeQuery") +} + // DateRangeOreQuery is a range query value over the ore_block index of the date family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_date_range_ore takes in eql_v4.between. It ranges over DateOrdOre. type DateRangeOreQuery []byte @@ -2466,6 +3985,21 @@ func (v *DateRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateRangeOreQuery) String() string { + return text(v, "DateRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v DateRangeOreQuery) LogValue() slog.Value { + return logValue(v, "DateRangeOreQuery") +} + // DateRangeOpeQuery is a range query value over the ope index of the date family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_date_range_ope takes in eql_v4.between. It ranges over DateOrd, DateOrdOpe. type DateRangeOpeQuery []byte @@ -2490,6 +4024,21 @@ func (v *DateRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DateRangeOpeQuery) String() string { + return text(v, "DateRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DateRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "DateRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v DateRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "DateRangeOpeQuery") +} + // TimestampRangeOreQuery is a range query value over the ore_block index of the timestamp family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_timestamp_range_ore takes in eql_v4.between. It ranges over TimestampOrdOre. type TimestampRangeOreQuery []byte @@ -2514,6 +4063,21 @@ func (v *TimestampRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampRangeOreQuery) String() string { + return text(v, "TimestampRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v TimestampRangeOreQuery) LogValue() slog.Value { + return logValue(v, "TimestampRangeOreQuery") +} + // TimestampRangeOpeQuery is a range query value over the ope index of the timestamp family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_timestamp_range_ope takes in eql_v4.between. It ranges over TimestampOrd, TimestampOrdOpe. type TimestampRangeOpeQuery []byte @@ -2538,6 +4102,21 @@ func (v *TimestampRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TimestampRangeOpeQuery) String() string { + return text(v, "TimestampRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TimestampRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "TimestampRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v TimestampRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "TimestampRangeOpeQuery") +} + // NumericRangeOreQuery is a range query value over the ore_block index of the numeric family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_numeric_range_ore takes in eql_v4.between. It ranges over NumericOrdOre. type NumericRangeOreQuery []byte @@ -2562,6 +4141,21 @@ func (v *NumericRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericRangeOreQuery) String() string { + return text(v, "NumericRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v NumericRangeOreQuery) LogValue() slog.Value { + return logValue(v, "NumericRangeOreQuery") +} + // NumericRangeOpeQuery is a range query value over the ope index of the numeric family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_numeric_range_ope takes in eql_v4.between. It ranges over NumericOrd, NumericOrdOpe. type NumericRangeOpeQuery []byte @@ -2586,6 +4180,21 @@ func (v *NumericRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v NumericRangeOpeQuery) String() string { + return text(v, "NumericRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v NumericRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "NumericRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v NumericRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "NumericRangeOpeQuery") +} + // TextRangeOreQuery is a range query value over the ore_block index of the text family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_text_range_ore takes in eql_v4.between. It ranges over TextOrdOre, TextSearchOre. type TextRangeOreQuery []byte @@ -2610,6 +4219,21 @@ func (v *TextRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextRangeOreQuery) String() string { + return text(v, "TextRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v TextRangeOreQuery) LogValue() slog.Value { + return logValue(v, "TextRangeOreQuery") +} + // TextRangeOpeQuery is a range query value over the ope index of the text family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_text_range_ope takes in eql_v4.between. It ranges over TextOrd, TextOrdOpe, TextSearch. type TextRangeOpeQuery []byte @@ -2634,6 +4258,21 @@ func (v *TextRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v TextRangeOpeQuery) String() string { + return text(v, "TextRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v TextRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "TextRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v TextRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "TextRangeOpeQuery") +} + // RealRangeOreQuery is a range query value over the ore_block index of the real family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_real_range_ore takes in eql_v4.between. It ranges over RealOrdOre. type RealRangeOreQuery []byte @@ -2658,6 +4297,21 @@ func (v *RealRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealRangeOreQuery) String() string { + return text(v, "RealRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v RealRangeOreQuery) LogValue() slog.Value { + return logValue(v, "RealRangeOreQuery") +} + // RealRangeOpeQuery is a range query value over the ope index of the real family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_real_range_ope takes in eql_v4.between. It ranges over RealOrd, RealOrdOpe. type RealRangeOpeQuery []byte @@ -2682,6 +4336,21 @@ func (v *RealRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v RealRangeOpeQuery) String() string { + return text(v, "RealRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v RealRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "RealRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v RealRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "RealRangeOpeQuery") +} + // DoubleRangeOreQuery is a range query value over the ore_block index of the double family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_double_range_ore takes in eql_v4.between. It ranges over DoubleOrdOre. type DoubleRangeOreQuery []byte @@ -2706,6 +4375,21 @@ func (v *DoubleRangeOreQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } +// String names the type and hides the value. +func (v DoubleRangeOreQuery) String() string { + return text(v, "DoubleRangeOreQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleRangeOreQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleRangeOreQuery") +} + +// LogValue is what slog records: String. +func (v DoubleRangeOreQuery) LogValue() slog.Value { + return logValue(v, "DoubleRangeOreQuery") +} + // DoubleRangeOpeQuery is a range query value over the ope index of the double family: both bounds of one BETWEEN, // included, as the one operand eql_v4.query_double_range_ope takes in eql_v4.between. It ranges over DoubleOrd, DoubleOrdOpe. type DoubleRangeOpeQuery []byte @@ -2729,3 +4413,18 @@ func (v DoubleRangeOpeQuery) MarshalJSON() ([]byte, error) { func (v *DoubleRangeOpeQuery) UnmarshalJSON(b []byte) error { return unmarshal((*[]byte)(v), b) } + +// String names the type and hides the value. +func (v DoubleRangeOpeQuery) String() string { + return text(v, "DoubleRangeOpeQuery") +} + +// Format prints String for every verb fmt hands it, %#v included. +func (v DoubleRangeOpeQuery) Format(f fmt.State, _ rune) { + format(f, v, "DoubleRangeOpeQuery") +} + +// LogValue is what slog records: String. +func (v DoubleRangeOpeQuery) LogValue() slog.Value { + return logValue(v, "DoubleRangeOpeQuery") +} diff --git a/languages/golang/encrypt/eql/eql_test.go b/languages/golang/encrypt/eql/eql_test.go index 815b50797..a7cbe8e4c 100644 --- a/languages/golang/encrypt/eql/eql_test.go +++ b/languages/golang/encrypt/eql/eql_test.go @@ -1,7 +1,11 @@ package eql_test import ( + "bytes" "encoding/json" + "fmt" + "log/slog" + "strings" "testing" "github.com/cipherstash/stack/languages/golang/encrypt/eql" @@ -100,3 +104,50 @@ func TestValuesRoundTripThroughTheDriverAndJSON(t *testing.T) { t.Fatalf("null unmarshals to an empty value: %v, %v", q, err) } } + +// An EQL value prints and logs its type's name and never its bytes, by +// every verb, through %#v and slog, and inside a struct. +func TestValuesPrintAndLogRedacted(t *testing.T) { + doc := []byte(`{"v":4,"i":{"t":"users","c":"email"},"c":"stack-encrypt:1:SECRET==","hm":"cafe"}`) + v := eql.TextEq(doc) + q := eql.JSONSearchPredicateQuery(`{"contains":{"sv":[{"s":"SECRET"}]}}`) + for _, verb := range []string{"%v", "%+v", "%#v", "%s", "%q", "%x", "%X", "%d"} { + for _, c := range []struct { + value any + want string + }{ + {v, "eql.TextEq{[redacted]}"}, + {q, "eql.JSONSearchPredicateQuery{[redacted]}"}, + {eql.TextEq(nil), "eql.TextEq{}"}, + } { + if got := fmt.Sprintf(verb, c.value); got != c.want { + t.Errorf("%s prints %s, want %s", verb, got, c.want) + } + } + if got := fmt.Sprintf(verb, struct{ Email eql.TextEq }{v}); strings.Contains(got, "SECRET") || strings.Contains(strings.ToUpper(got), "5345435245") { + t.Errorf("%s of a struct prints the value: %s", verb, got) + } + } + if v.String() != "eql.TextEq{[redacted]}" { + t.Errorf("String %s", v.String()) + } + // slog records what String gives, the type's name with it. + var logged bytes.Buffer + slog.New(slog.NewJSONHandler(&logged, nil)).Info("row", "email", v, "q", q, "none", eql.TextEq(nil)) + for _, want := range []string{`"email":"eql.TextEq{[redacted]}"`, `"q":"eql.JSONSearchPredicateQuery{[redacted]}"`, `"none":"eql.TextEq{}"`} { + if !strings.Contains(logged.String(), want) { + t.Errorf("slog records %s, without %s", logged.String(), want) + } + } + if strings.Contains(logged.String(), "SECRET") { + t.Errorf("slog records the value: %s", logged.String()) + } + // The JSON is the value itself, as a conversion, the driver and + // encoding/json give it. + if got, err := v.Value(); err != nil || got != string(doc) { + t.Errorf("Value = %v, %v", got, err) + } + if string(v) != string(doc) || !bytes.Equal(json.RawMessage(v), doc) { + t.Error("a conversion does not give the JSON") + } +} diff --git a/languages/golang/encrypt/errors.go b/languages/golang/encrypt/errors.go index 68960418a..15b9dc931 100644 --- a/languages/golang/encrypt/errors.go +++ b/languages/golang/encrypt/errors.go @@ -90,13 +90,15 @@ var ErrStorageVersion = vcffi.ErrUnsupportedVersion // returns after the sentinel's text), Help, URL, Severity, Fields (the // structured fields, by name) and Causes (the errors behind it, outermost // first). Accessors read the fields a caller branches on: ExpectedKeyset -// and FoundKeyset on an [ErrForeignKeyset] (each a [KeysetID]'s bytes), and -// Field and Reason on a refused plan, record or value. +// and FoundKeyset on an [ErrForeignKeyset] (each a [KeysetID]'s bytes); +// Field and Reason on a refused plan, record or value; and Construct and At +// on a refused Match predicate, the construct in the grammar's words and +// the byte offset in the predicate where it starts. // // What one may carry is fixed: keyset ids and names, field names, counts, -// index kinds, ZeroKMS request kinds and HTTP statuses. Never plaintext, -// key material, tokens, ciphertext or term bytes, or the values of an -// encryption context. +// index kinds, ZeroKMS request kinds and HTTP statuses, and a refused +// predicate's construct and offset. Never plaintext, key material, +// tokens, ciphertext or term bytes, or the values of an encryption context. // // Errors the client raises itself carry none: a closed client ([ErrState]), // a guest that did not return, [ErrMemoryLock], and an argument refused diff --git a/languages/golang/encrypt/example/json/README.md b/languages/golang/encrypt/example/json/README.md index 02f7fc1b9..2070f729a 100644 --- a/languages/golang/encrypt/example/json/README.md +++ b/languages/golang/encrypt/example/json/README.md @@ -2,8 +2,8 @@ This program stores a Go `Profile` struct as one encrypted EQL v4 JSON column. It finds a row by containment, compares an ordered path, finds a row where one -visit passes two tests, decrypts a whole profile, and opens only the visit -times from another query. +visit passes two tests, finds a row by one jsonpath predicate, decrypts a whole +profile, and opens only the visit times from another query. ## Run @@ -51,7 +51,23 @@ q, err := Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.ElementPredi ``` Ada has a visit at home and a visit after 15, but no one visit that is both, -so only Bea's row matches. Each comparison is of the ordering term, so text +so only Bea's row matches. + +`Fields.Profile.Match` takes a whole filter as one jsonpath predicate and its +variables, as plaintext `jsonb_path_match` does, and binds as one parameter: + +```go +q, err := Fields.Profile.Match(ctx, cipher, + `$."age" < $max && exists($."visits"[*] ? (@."place" == $place && @."at" > $after))`, + map[string]any{"max": 30, "place": "home", "after": 15}, encrypt.JSONReadOptions{}) +// SELECT id FROM customers WHERE profile @@ $1::eql_v4.query_json_predicate +``` + +The engine compiles the predicate into the operands the other methods give: +the age comparison is `At.Age`'s, and the filter is the visit match above. +It refuses a construct it cannot answer, naming it and its byte offset in the +predicate. The tree is evaluated inside a function, so no index engages; the +per-operator queries are the fast path. Each comparison is of the ordering term, so text compares case- and accent-folded. A row with more visits than `JSONReadOptions.WildcardBound` (64 by default) fails the query with SQLSTATE 22023. A containment needle made from a diff --git a/languages/golang/encrypt/example/json/customer_stash.go b/languages/golang/encrypt/example/json/customer_stash.go index ef515f48e..7643b0152 100644 --- a/languages/golang/encrypt/example/json/customer_stash.go +++ b/languages/golang/encrypt/example/json/customer_stash.go @@ -207,6 +207,46 @@ func (f ProfileField) ChildCount(ctx context.Context, c *encrypt.Cipher, path st return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f ProfileField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} + // ProfileFieldElements names the arrays of Profile whose elements have ordered // members. Each accessor's Match tests the members of one element together. type ProfileFieldElements struct { diff --git a/languages/golang/encrypt/example/json/main.go b/languages/golang/encrypt/example/json/main.go index abfd2836c..a04d2b4bc 100644 --- a/languages/golang/encrypt/example/json/main.go +++ b/languages/golang/encrypt/example/json/main.go @@ -121,6 +121,25 @@ func run(ctx context.Context) error { } fmt.Printf("customer with a visit at home after 15: %d\n", homeLateID) + // The same test and an age ordering as one jsonpath predicate and its + // variables, bound as one parameter. The engine compiles the predicate + // into the operands above, so the age is the ordering's term and the + // filter is the visit match. The tree is evaluated in a function, which + // engages no index: the per-operator queries above are the fast path. + predicate, err := Fields.Profile.Match(ctx, cipher, + `$."age" < $max && exists($."visits"[*] ? (@."place" == $place && @."at" > $after))`, + map[string]any{"max": 30, "place": "home", "after": 15}, encrypt.JSONReadOptions{}) + if err != nil { + return fmt.Errorf("prepare predicate: %w", err) + } + var matchedID int64 + if err := conn.QueryRowContext(ctx, + `SELECT id FROM customers WHERE profile @@ $1::eql_v4.query_json_predicate`, predicate, + ).Scan(&matchedID); err != nil { + return fmt.Errorf("find by predicate: %w", err) + } + fmt.Printf("customer under 30 with a visit at home after 15: %d\n", matchedID) + // Read only the visit times of one row. The database returns encrypted // entries; Open checks them against the prepared read and returns JSON text. read, err := Fields.Profile.Subtree(ctx, cipher, "$.visits[*].at", encrypt.JSONReadOptions{}) diff --git a/languages/golang/encrypt/gensupport/json.go b/languages/golang/encrypt/gensupport/json.go index 8beddd45e..608dfa3cd 100644 --- a/languages/golang/encrypt/gensupport/json.go +++ b/languages/golang/encrypt/gensupport/json.go @@ -70,6 +70,16 @@ func JSONText(v any) ([]byte, error) { return nil, fmt.Errorf("%w: a %T does not write as JSON", encrypt.ErrEncoding, v) } +// isGoString reports whether v is a Go string, a type defined over one, or +// a pointer to either, that encoding/json writes as one JSON string: one +// with no MarshalJSON or MarshalText of its own. A containment needle and +// Match's vars refuse it, since JSON text in a Go string is the likely +// mistake. +func isGoString(v any) bool { + t := throughPointer(v) + return t != nil && t.Kind() == reflect.String && writesItself(v) == "" +} + // writesItself names the encoding/json interface v implements to write // itself, or "" for none. func writesItself(v any) string { @@ -82,6 +92,13 @@ func writesItself(v any) string { return "" } +// isNilPointer reports whether v is a nil pointer of any type, which +// carries no value whatever type it points to. +func isNilPointer(v any) bool { + rv := reflect.ValueOf(v) + return rv.Kind() == reflect.Pointer && rv.IsNil() +} + // throughPointer is v's dynamic type, or what it points to when it is a // pointer, or nil for nil: the one place the JSON operands look at a type // by kind, since a type switch cannot name every type defined over []byte @@ -181,7 +198,7 @@ func (f Field[T]) jsonOperand(v any) ([]byte, error) { func JSONQuery[Q ~[]byte, T any](ctx context.Context, f Field[T], c *encrypt.Cipher, operator string, needle any) (Q, error) { // A Go string is one JSON string, and a document of members never // contains one: it is almost always JSON text meant as the needle. - if t := throughPointer(needle); t != nil && t.Kind() == reflect.String && writesItself(needle) == "" { + if isGoString(needle) { return nil, fmt.Errorf("%w: gensupport: %s: a %T needle is one JSON string, not JSON text; pass JSON text as json.RawMessage, or a map or struct", encrypt.ErrEncoding, f.name, needle) } if needle == nil { @@ -306,6 +323,60 @@ func JSONElementMatch[Q ~[]byte, T any](ctx context.Context, f Field[T], c *encr return Q(term.Terms[0]), nil } +// JSONMatch derives match on a JSON field: the one operand +// eql_v4.json_tree_match and `@@` take, a jsonpath predicate compiled into +// a boolean tree of the other operators' operands. The predicate crosses +// as it is: the engine parses and compiles it, and nothing here reads it. +// +// vars are the predicate's variables ($name), the jsonb that +// jsonb_path_match takes third: any value [JSONText] writes as a JSON +// object, such as a map, a struct or a json.RawMessage of one. nil, a nil +// pointer of any type, a nil map, or a json.RawMessage of the literal null +// sends none. Any other json.RawMessage crosses as it is, so an empty or +// whitespace-only one is refused by the engine as not JSON, as "{" is. A Go +// string or a non-nil pointer to one is refused, as [JSONQuery] refuses a +// needle: it writes one JSON string, and JSON text in a Go string is the +// likely mistake. +// +// What the engine refuses (text it does not read, a construct the index +// cannot answer, a variable vars do not hold, vars that are not an +// object, an ordering at a path the field does not order) is its +// [encrypt.Diagnostic]. opts.WildcardBound 0 sends no bound, and the +// engine's default, 64, applies. +func JSONMatch[Q ~[]byte, T any](ctx context.Context, f Field[T], c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (Q, error) { + if f.err != nil { + return nil, f.err + } + input := vcvalue.Object{{Key: "predicate", Value: predicate}} + if vars != nil && !isNilPointer(vars) { + if isGoString(vars) { + return nil, fmt.Errorf("%w: gensupport: %s: Match takes vars as a JSON object, and a %T is one JSON string; pass JSON text as json.RawMessage, or a map or struct", encrypt.ErrEncoding, f.name, vars) + } + text, err := f.jsonOperand(vars) + if err != nil { + return nil, err + } + // The literal null, which encoding/json writes for a nil map, slice + // or pointer: no variables. Anything else, empty text included, is + // the engine's to read. + if !bytes.Equal(bytes.TrimSpace(text), []byte("null")) { + if text == nil { + // A nil json.RawMessage is empty text, not an absent field. + text = []byte{} + } + input = append(input, vcvalue.Field{Key: "vars", Value: text}) + } + } + if opts.WildcardBound != 0 { + input = append(input, vcvalue.Field{Key: "wildcard_bound", Value: opts.WildcardBound}) + } + term, err := f.jsonTerm(ctx, c, "match", "eql", 1, input) + if err != nil { + return nil, err + } + return Q(term.Terms[0]), nil +} + // orderedMembers says which members of an array's elements the field // orders. func orderedMembers(members []string) string { diff --git a/languages/golang/encrypt/guest/src/ops.rs b/languages/golang/encrypt/guest/src/ops.rs index b152e312e..3b2c65e25 100644 --- a/languages/golang/encrypt/guest/src/ops.rs +++ b/languages/golang/encrypt/guest/src/ops.rs @@ -985,4 +985,61 @@ mod tests { Err(STATUS_ENCODING) ); } + + /// `match` on a JsonSearch field through the query exports: a + /// predicate compiles to the predicate operand, and one the grammar + /// refuses is the caller's input (`STATUS_ENCODING`), never the + /// guest's own failure. + #[cfg(feature = "eql")] + #[test] + fn a_compiled_predicate_derives_and_a_refused_one_is_the_callers_input() { + use futures::executor::block_on; + use stack_encrypt::StackCipher; + use stack_kms::FakeDataKeySource; + + let cipher = block_on(StackCipher::builder().kms(FakeDataKeySource::new()).init()) + .expect("a cipher"); + let keyset = cipher.default_keyset(); + let plan = encoded(object(vec![( + "profile", + object(vec![ + ("context", Tree::Array(vec![text("users"), text("profile")])), + ("type", text("json")), + ("null", text("sql")), + ("target", text("JsonSearch")), + ]), + )])); + let input = |predicate: &str| { + encoded(object(vec![ + ("predicate", text(predicate)), + ("vars", text(r#"{"v": "nz"}"#)), + ])) + }; + + let accepted = input("$.country == $v && exists($.tags[*])"); + validate::query_term(&accepted, &plan, b"profile", b"match") + .expect("a field answers match"); + let term = + block_on(query_term(&keyset, &accepted, &plan, b"profile", b"match")).expect("derives"); + let Tree::Object(term) = Tree::from_frame(&term).expect("a value") else { + panic!("a query term is an object") + }; + let text = |key: &str| match term.iter().find(|(k, _)| k == key) { + Some((_, Tree::String(s))) => s.as_bytes().to_vec(), + other => panic!("{key}: {other:?}"), + }; + assert_eq!(text("operator"), b"match"); + assert_eq!(text("domain"), b"eql_v4.query_json_predicate"); + assert_eq!(text("encoding"), b"eql"); + + let refused = input(r#"$.country like_regex "n""#); + assert_eq!( + validate::query_term(&refused, &plan, b"profile", b"match"), + Err(STATUS_ENCODING) + ); + assert_eq!( + block_on(query_term(&keyset, &refused, &plan, b"profile", b"match")), + Err(STATUS_ENCODING) + ); + } } diff --git a/languages/golang/encrypt/guest/src/status.rs b/languages/golang/encrypt/guest/src/status.rs index 3a34694a9..7cfbdabd0 100644 --- a/languages/golang/encrypt/guest/src/status.rs +++ b/languages/golang/encrypt/guest/src/status.rs @@ -162,8 +162,9 @@ pub fn status_for_error(error: &stack_encrypt::Error) -> u32 { /// A dynamic-path error as a status code. /// /// The split the library draws is the one the ABI needs: `Context`, `Term`, -/// `Plan`, `UntypedIndex`, `Source` and `Record` are each a statement about -/// the caller's input, decided before any key is minted or retrieved, so they are +/// `Plan`, `UntypedIndex`, `Source`, `Record` and `Predicate` are each a +/// statement about the caller's input, decided before any key is minted or +/// retrieved, so they are /// [`STATUS_ENCODING`] — named one by one, because that verdict is the /// host's to act on and must be given deliberately. `Cipher` defers to /// [`status_for_error`]; `Internal` is the library's own invariant failing @@ -184,7 +185,11 @@ pub fn status_for_dynamic(error: &stack_encrypt::dynamic::Error) -> u32 { | Error::Plan { .. } | Error::UntypedIndex { .. } | Error::Source { .. } - | Error::Record { .. } => STATUS_ENCODING, + | Error::Record { .. } + // A `match` predicate refused at an offset in its text: a construct + // the grammar does not read or the index cannot answer, a variable + // its vars do not hold, a value its comparison cannot take. + | Error::Predicate { .. } => STATUS_ENCODING, Error::Cipher(e) => status_for_error(e), // A target refusal is a statement about the plan, the label or the // value (an unknown or unproducible type, an extended plan, a value @@ -563,6 +568,15 @@ mod tests { reason: Reason::NoCiphertextNode, }, ), + ( + "a refused predicate", + Error::Predicate { + field: age(), + reason: Reason::JsonPredicateUnsupported, + construct: "`like_regex`", + at: 4, + }, + ), ] { assert_eq!( status_for_dynamic(&err), diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 065982246..c61b7083e 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -114,17 +114,20 @@ fn describe(target: &eql_bindings::encryption::targets::Target) -> TargetDescrip Some(options) => descriptor.with_match_options(options), None => descriptor, }; - // A document's orderings at a path bind as its leaf operand, and its - // element predicate (`element_match`) as its elements operand. Its - // compiled predicate (`match`) is not named: the Go SDK does not take - // that operator yet. + // A document's orderings at a path bind as its leaf operand, its + // element predicate (`element_match`) as its elements operand, and its + // compiled predicate (`match`) as its predicate operand. let descriptor = match target.leaf_query_sql_domain() { Some(domain) => descriptor.with_leaf_query_sql_domain(domain), None => descriptor, }; - match target.elements_query_sql_domain() { + let descriptor = match target.elements_query_sql_domain() { Some(domain) => descriptor.with_elements_query_sql_domain(domain), None => descriptor, + }; + match target.predicate_query_sql_domain() { + Some(domain) => descriptor.with_predicate_query_sql_domain(domain), + None => descriptor, } } @@ -277,13 +280,12 @@ mod tests { /// The JSON document's query domains this guest names, and so the JSON /// operators the Go SDK is offered: the leaf operand, for the orderings - /// at a path, and the element predicate, for `element_match`; not the - /// compiled predicate, whose operator (`match`) the Go side does not - /// parse yet. Naming it is a change to the Go SDK's operator list: - /// update this test with the Go side that takes the operator. + /// at a path; the element predicate, for `element_match`; and the + /// compiled predicate, for `match`. Every operator a field lists is one + /// the Go SDK's operator list (`parseOperatorName`) and stashgen read. #[cfg(feature = "eql")] #[test] - fn a_document_names_the_leaf_and_element_domains_and_not_the_predicate_domain() { + fn a_document_names_the_leaf_element_and_predicate_domains() { use stack_encrypt::dynamic::{self, record, OpSpec}; let targets = resolver().targets(); let document = targets @@ -298,9 +300,17 @@ mod tests { document.elements_query_sql_domain.as_deref(), Some("eql_v4.query_json_elements") ); - assert_eq!(document.predicate_query_sql_domain, None); + assert_eq!( + document.predicate_query_sql_domain.as_deref(), + Some("eql_v4.query_json_predicate") + ); + assert!( + document.operators().contains(&OpSpec::Match), + "the type lists match: {:?}", + document.operators() + ); // The operators a field answers: the orderings and `element_match` - // only where it orders a path, and `match` nowhere. + // only where it orders a path, and `match` on every field. let field_operators = |ordered: &[&str]| { let wire = serde_json::json!({ "profile": { @@ -319,12 +329,12 @@ mod tests { .collect::>() }; let ordered = field_operators(&["$.contacts[*].relationship"]); - assert!(ordered.contains(&OpSpec::ElementMatch), "{ordered:?}"); - assert!(!ordered.contains(&OpSpec::Match), "{ordered:?}"); - let unordered = field_operators(&[]); - for absent in [OpSpec::ElementMatch, OpSpec::Match] { - assert!(!unordered.contains(&absent), "{absent:?}: {unordered:?}"); + for present in [OpSpec::ElementMatch, OpSpec::Match] { + assert!(ordered.contains(&present), "{present:?}: {ordered:?}"); } + let unordered = field_operators(&[]); + assert!(unordered.contains(&OpSpec::Match), "{unordered:?}"); + assert!(!unordered.contains(&OpSpec::ElementMatch), "{unordered:?}"); } #[cfg(feature = "eql")] diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index 67610f7b6..eb475761f 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -1844,7 +1844,8 @@ fn the_catalog_lists_each_types_operators() { ("input", "value"), ]), // A document's operators under the index's defaults: the - // orderings at a path only for a field that orders one. + // orderings at a path only for a field that orders one, and + // the compiled predicate, which needs none. "JsonSearch" => { let json = |operator: &'static str, arity: &'static str, input: &'static str| { vec![ @@ -1865,6 +1866,7 @@ fn the_catalog_lists_each_types_operators() { json("subtree", "one", "path_bound"), json("children", "one", "path"), json("child_count", "one", "path"), + json("match", "one", "predicate"), ], "{name}" ); diff --git a/languages/golang/encrypt/internal/testusers/customer_stash.go b/languages/golang/encrypt/internal/testusers/customer_stash.go index 0538e943c..68fd7ca6c 100644 --- a/languages/golang/encrypt/internal/testusers/customer_stash.go +++ b/languages/golang/encrypt/internal/testusers/customer_stash.go @@ -213,6 +213,46 @@ func (f CustomerProfileField) ChildCount(ctx context.Context, c *encrypt.Cipher, return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f CustomerProfileField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} + // CustomerProfileFieldElements names the arrays of Profile whose elements have // ordered members. Each accessor's Match tests the members of one element // together. @@ -391,3 +431,43 @@ func (f CustomerPrefsField) Children(ctx context.Context, c *encrypt.Cipher, pat func (f CustomerPrefsField) ChildCount(ctx context.Context, c *encrypt.Cipher, path string) (encrypt.JSONToken, error) { return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } + +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f CustomerPrefsField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} diff --git a/languages/golang/encrypt/internal/testusers/entities.go b/languages/golang/encrypt/internal/testusers/entities.go index 447c5ca41..63d2b0fdb 100644 --- a/languages/golang/encrypt/internal/testusers/entities.go +++ b/languages/golang/encrypt/internal/testusers/entities.go @@ -3,24 +3,38 @@ package testusers //go:generate go run github.com/cipherstash/stack/languages/golang/cmd/stashgen -type Entity -name Entity // Entity holds its custom field values as an encrypted JSON document (the -// EQL type JsonSearch). Each emergency contact's relationship, call count -// and age order, so the generated EntityFields.CustomFieldValues has -// Elements.ExtraEmergencyContacts, whose Match tests them on one contact +// EQL type JsonSearch). Its weight orders, and so does each emergency +// contact's relationship, call count and age, so the generated +// EntityFields.CustomFieldValues has At.Weight and +// Elements.ExtraEmergencyContacts, whose Match tests a contact's members // together. A contact's name is not ordered. type Entity struct { _ struct{} `stash:"context=entities"` ID int64 `stash:"id,passthrough"` - CustomFieldValues CustomFieldValues `stash:"custom_field_values,encrypt_into=JsonSearch(ordered=$.extra.emergency_contacts[*].relationship;$.extra.emergency_contacts[*].previous_call_count;$.extra.emergency_contacts[*].age)"` + CustomFieldValues CustomFieldValues `stash:"custom_field_values,encrypt_into=JsonSearch(ordered=$.weight;$.extra.emergency_contacts[*].relationship;$.extra.emergency_contacts[*].previous_call_count;$.extra.emergency_contacts[*].age)"` } -// CustomFieldValues is an entity's custom field values document. +// CustomFieldValues is an entity's custom field values document. Every +// member but Extra is left out of the document when it is nil, so a +// document holds only the values an entity has. type CustomFieldValues struct { - Extra CustomFieldExtra `json:"extra"` + Score *float64 `json:"score,omitempty"` + Weight *float64 `json:"weight,omitempty"` + Address *CustomAddress `json:"address,omitempty"` + Extra CustomFieldExtra `json:"extra"` } -// CustomFieldExtra holds an entity's emergency contacts. +// CustomAddress is an entity's address. A nil Postcode is JSON null. +type CustomAddress struct { + Postcode *string `json:"postcode"` +} + +// CustomFieldExtra holds an entity's country, preferred languages and +// emergency contacts. type CustomFieldExtra struct { - EmergencyContacts []EmergencyContact `json:"emergency_contacts"` + Country string `json:"country,omitempty"` + PreferredLanguages []string `json:"preferred_languages,omitempty"` + EmergencyContacts []EmergencyContact `json:"emergency_contacts"` } // EmergencyContact is one emergency contact of an entity. diff --git a/languages/golang/encrypt/internal/testusers/entity_stash.go b/languages/golang/encrypt/internal/testusers/entity_stash.go index deb8b4567..6a62ceb82 100644 --- a/languages/golang/encrypt/internal/testusers/entity_stash.go +++ b/languages/golang/encrypt/internal/testusers/entity_stash.go @@ -42,7 +42,7 @@ type entityShape struct { var entityDeclaration = gensupport.Declare("entities"). Passthrough("id"). EncryptInto("custom_field_values", gensupport.JSON, "JsonSearch"). - Options("custom_field_values", gensupport.TargetOptions{Ordered: []string{"$.extra.emergency_contacts[*].relationship", "$.extra.emergency_contacts[*].previous_call_count", "$.extra.emergency_contacts[*].age"}}) + Options("custom_field_values", gensupport.TargetOptions{Ordered: []string{"$.weight", "$.extra.emergency_contacts[*].relationship", "$.extra.emergency_contacts[*].previous_call_count", "$.extra.emergency_contacts[*].age"}}) var entityCodec = gensupport.New(gensupport.Generated[Entity, EncryptedEntity]{ TypeName: "Entity", @@ -119,6 +119,7 @@ func newEntityCustomFieldValuesField(f gensupport.Field[CustomFieldValues]) Enti return EntityCustomFieldValuesField{ field: f, At: EntityCustomFieldValuesFieldAt{ + Weight: EntityCustomFieldValuesFieldPath{field: f, path: "$.weight"}, ExtraEmergencyContactsRelationship: EntityCustomFieldValuesFieldPath{field: f, path: "$.extra.emergency_contacts[*].relationship"}, ExtraEmergencyContactsPreviousCallCount: EntityCustomFieldValuesFieldPath{field: f, path: "$.extra.emergency_contacts[*].previous_call_count"}, ExtraEmergencyContactsAge: EntityCustomFieldValuesFieldPath{field: f, path: "$.extra.emergency_contacts[*].age"}, @@ -199,6 +200,46 @@ func (f EntityCustomFieldValuesField) ChildCount(ctx context.Context, c *encrypt return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f EntityCustomFieldValuesField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} + // EntityCustomFieldValuesFieldElements names the arrays of CustomFieldValues // whose elements have ordered members. Each accessor's Match tests the members // of one element together. @@ -239,6 +280,7 @@ func (a EntityCustomFieldValuesFieldArray) Match(ctx context.Context, c *encrypt // EntityCustomFieldValuesFieldAt names the declared ordered paths of // CustomFieldValues. Use an accessor's Selector with its ordering query. type EntityCustomFieldValuesFieldAt struct { + Weight EntityCustomFieldValuesFieldPath // $.weight ExtraEmergencyContactsRelationship EntityCustomFieldValuesFieldPath // $.extra.emergency_contacts[*].relationship ExtraEmergencyContactsPreviousCallCount EntityCustomFieldValuesFieldPath // $.extra.emergency_contacts[*].previous_call_count ExtraEmergencyContactsAge EntityCustomFieldValuesFieldPath // $.extra.emergency_contacts[*].age diff --git a/languages/golang/encrypt/internal/testusers/subscriber_stash.go b/languages/golang/encrypt/internal/testusers/subscriber_stash.go index 301737800..ea396eb67 100644 --- a/languages/golang/encrypt/internal/testusers/subscriber_stash.go +++ b/languages/golang/encrypt/internal/testusers/subscriber_stash.go @@ -226,6 +226,46 @@ func (f SubscriberProfileField) ChildCount(ctx context.Context, c *encrypt.Ciphe return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f SubscriberProfileField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} + // SubscriberProfileFieldElements names the arrays of Profile whose elements // have ordered members. Each accessor's Match tests the members of one element // together. @@ -408,6 +448,46 @@ func (f SubscriberPrefsField) ChildCount(ctx context.Context, c *encrypt.Cipher, return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f SubscriberPrefsField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} + // SubscriberLabelsField provides document queries and reads for the Labels // field. type SubscriberLabelsField struct { @@ -483,3 +563,43 @@ func (f SubscriberLabelsField) Children(ctx context.Context, c *encrypt.Cipher, func (f SubscriberLabelsField) ChildCount(ctx context.Context, c *encrypt.Cipher, path string) (encrypt.JSONToken, error) { return gensupport.JSONNodeToken(ctx, f.field, c, "child_count", path) } + +// Match derives the operand for a jsonpath predicate over the document, as +// plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ +// $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, +// $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): +// a map, a struct or a json.RawMessage that writes a JSON object, or nil for +// none. +// +// The engine reads the predicate, and nothing in Go does: paths of .key, +// ."quoted key" and [*] steps; ==, !=, <, <=, > and >= between a path and a +// variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n +// > $w)) for several members of one array element; &&, || and !(…). Anything +// else is refused, before any term is derived, as an *encrypt.Diagnostic +// wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal +// and the byte offset in predicate where it starts. An ordering at a path the +// tag does not order, or a member of a filter of more than one condition that +// it does not order, is refused wrapping encrypt.ErrTerm. +// +// The tree is evaluated inside eql_v4.json_tree_match, which engages no index: +// every row the scan gives it is opened and tested. The per-operator methods +// engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's +// term (At.), so a query that needs an index writes those leaves with +// them, or narrows the rows with one beside Match. +// +// The answer is jsonb_path_match's over the plaintext, true, false or NULL, +// save four cases. Lax mode's unwrapping of an array the path names no [*] for +// is not applied: write [*]. Nor is its wrapping of a value that is not an +// array under [*]. A string compared with a number is false under == and true +// under !=, where jsonpath says unknown. Orderings, and filters of more than +// one condition, compare folded ordering terms: text case- and accent-folded, +// numbers as float64, so such a filter's == is of folded values; == elsewhere +// is exact. +// +// A filter of more than one condition reads each array to opts.WildcardBound +// elements, 64 when it is 0. A row whose array is longer fails the query with +// SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: +// raise the bound past the longest array. +func (f SubscriberLabelsField) Match(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (eql.JSONSearchPredicateQuery, error) { + return gensupport.JSONMatch[eql.JSONSearchPredicateQuery](ctx, f.field, c, predicate, vars, opts) +} diff --git a/languages/golang/encrypt/json_match_postgres_test.go b/languages/golang/encrypt/json_match_postgres_test.go new file mode 100644 index 000000000..b0d65fd26 --- /dev/null +++ b/languages/golang/encrypt/json_match_postgres_test.go @@ -0,0 +1,236 @@ +package encrypt_test + +import ( + "context" + "encoding/json" + "errors" + "os" + "slices" + "testing" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/encrypt/internal/testusers" + "github.com/jackc/pgx/v5/pgconn" +) + +func f64(v float64) *float64 { return &v } +func str(v string) *string { return &v } + +// predicateEntities are documents for the seven filters of a customer's +// AIP-160 request, each written as plaintext jsonb_path_match takes it: +// scores, countries (one in another case), weights (one at the bound), +// postcodes present, JSON null and absent, preferred languages, and +// emergency contacts where one contact is a sibling called more than five +// times (2, 5) or the two halves are on different contacts (1, 6). +var predicateEntities = []testusers.Entity{ + {ID: 1, CustomFieldValues: testusers.CustomFieldValues{ + Score: f64(3.14), Weight: f64(5), Address: &testusers.CustomAddress{Postcode: str("6011")}, + Extra: testusers.CustomFieldExtra{Country: "nz", PreferredLanguages: []string{"csharp", "go"}, EmergencyContacts: []testusers.EmergencyContact{ + {Name: "Ann", Relationship: "Sibling", PreviousCallCount: 2, Age: 30}, + {Name: "Al", Relationship: "Friend", PreviousCallCount: 9, Age: 50}, + }}, + }}, + {ID: 2, CustomFieldValues: testusers.CustomFieldValues{ + Score: f64(2), Weight: f64(12.5), Address: &testusers.CustomAddress{}, + Extra: testusers.CustomFieldExtra{Country: "au", PreferredLanguages: []string{"rust"}, EmergencyContacts: []testusers.EmergencyContact{ + {Name: "Bea", Relationship: "Friend", PreviousCallCount: 1, Age: 41}, + {Name: "Ben", Relationship: "Sibling", PreviousCallCount: 7, Age: 25}, + }}, + }}, + {ID: 3, CustomFieldValues: testusers.CustomFieldValues{ + Weight: f64(9.5), + Extra: testusers.CustomFieldExtra{Country: "nz", PreferredLanguages: []string{"python", "rust"}, EmergencyContacts: []testusers.EmergencyContact{}}, + }}, + {ID: 4, CustomFieldValues: testusers.CustomFieldValues{ + Score: f64(3.14), Address: &testusers.CustomAddress{Postcode: str("2000")}, + Extra: testusers.CustomFieldExtra{EmergencyContacts: []testusers.EmergencyContact{ + {Name: "Cy", Relationship: "Friend", PreviousCallCount: 1, Age: 60}, + }}, + }}, + {ID: 5, CustomFieldValues: testusers.CustomFieldValues{ + Score: f64(1), Weight: f64(1.25), Address: &testusers.CustomAddress{Postcode: str("0632")}, + Extra: testusers.CustomFieldExtra{Country: "NZ", PreferredLanguages: []string{"csharp", "python"}, EmergencyContacts: []testusers.EmergencyContact{ + {Name: "Di", Relationship: "Sibling", PreviousCallCount: 6, Age: 33}, + }}, + }}, + {ID: 6, CustomFieldValues: testusers.CustomFieldValues{ + Score: f64(3.14), Weight: f64(9.49), + Extra: testusers.CustomFieldExtra{Country: "nz", PreferredLanguages: []string{"go"}, EmergencyContacts: []testusers.EmergencyContact{ + {Name: "Ed", Relationship: "Parent", PreviousCallCount: 12, Age: 70}, + {Name: "Flo", Relationship: "Sibling", PreviousCallCount: 0, Age: 40}, + }}, + }}, +} + +// Match inside PostgreSQL with EQL v4 installed, for the seven filters of +// the request: for each, the @@ form and the json_tree_match form return +// the rows the query written per operator returns, and the rows +// jsonb_path_match returns over the plaintexts. Skipped unless +// DATABASE_URL is set. +func TestJSONPredicateMatchInPostgres(t *testing.T) { + url := os.Getenv("DATABASE_URL") + if url == "" { + t.Skip("DATABASE_URL is not set") + } + ctx := context.Background() + c := deterministicEQLClient(t) + cipher := c.DefaultKeyset() + conn := eqlPostgresConn(t, ctx, url) + if _, err := conn.ExecContext(ctx, `CREATE TEMP TABLE entity (id bigint PRIMARY KEY, custom_field_values public.eql_v4_json_search NOT NULL, plain jsonb NOT NULL)`); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _, _ = conn.ExecContext(context.Background(), `DROP TABLE IF EXISTS entity`) }) + encrypted, err := testusers.EncryptEntity(ctx, cipher, predicateEntities) + if err != nil { + t.Fatal(err) + } + for i, e := range encrypted { + plain, err := json.Marshal(predicateEntities[i].CustomFieldValues) + if err != nil { + t.Fatal(err) + } + if _, err := conn.ExecContext(ctx, `INSERT INTO entity VALUES ($1, $2, $3)`, e.ID, e.CustomFieldValues, string(plain)); err != nil { + t.Fatal(err) + } + } + ids := func(q string, args ...any) ([]int64, error) { + t.Helper() + rows, err := conn.QueryContext(ctx, q+` ORDER BY id`, args...) + if err != nil { + return nil, err + } + defer rows.Close() + var out []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + out = append(out, id) + } + return out, rows.Err() + } + // must is an operand as the text it binds as, cast to its domain. + must := func(q []byte, err error) string { + t.Helper() + if err != nil { + t.Fatal(err) + } + return string(q) + } + f := testusers.EntityFields.CustomFieldValues + selector := func(path string) string { + t.Helper() + s, err := f.Selector(ctx, cipher, path) + if err != nil { + t.Fatal(err) + } + return string(s.Selector()) + } + weight, err := f.At.Weight.Selector(ctx, cipher) + if err != nil { + t.Fatal(err) + } + languages := "$.extra.preferred_languages[*]" + + for _, tc := range []struct { + name string + predicate string + vars map[string]any + // The same filter written per operator, and its parameters. + perOperator string + args []any + want []int64 + }{ + { + name: "1 equality", predicate: `$."score" == $val`, vars: map[string]any{"val": 3.14}, + perOperator: `SELECT id FROM entity WHERE custom_field_values @> $1::eql_v4.query_json`, + args: []any{must(f.EqAt(ctx, cipher, "$.score", 3.14))}, + want: []int64{1, 4, 6}, + }, + { + name: "2 nested equality", predicate: `$."extra"."country" == $val`, vars: map[string]any{"val": "nz"}, + perOperator: `SELECT id FROM entity WHERE custom_field_values @> $1::eql_v4.query_json`, + args: []any{must(f.EqAt(ctx, cipher, "$.extra.country", "nz"))}, + want: []int64{1, 3, 6}, + }, + { + name: "3 range on an ordered path", predicate: `$."weight" < $val`, vars: map[string]any{"val": 9.5}, + perOperator: `SELECT id FROM entity WHERE (custom_field_values -> $1::text) < $2::eql_v4.query_json_leaf`, + args: []any{string(weight.Selector()), must(f.At.Weight.Lt(ctx, cipher, 9.5))}, + want: []int64{1, 5, 6}, + }, + { + name: "4 presence, not JSON null", predicate: `$."address"."postcode" != null`, vars: map[string]any{}, + perOperator: `SELECT id FROM entity WHERE eql_v4.json_tree_path_exists(custom_field_values, $1::text) AND NOT (custom_field_values @> $2::eql_v4.query_json)`, + args: []any{selector("$.address.postcode"), must(f.EqAt(ctx, cipher, "$.address.postcode", nil))}, + want: []int64{1, 4, 5}, + }, + { + name: "5 array membership with NOT", predicate: `!(exists($."extra"."preferred_languages"[*] ? (@ == $val1)))`, vars: map[string]any{"val1": "csharp"}, + perOperator: `SELECT id FROM entity WHERE NOT (custom_field_values @> $1::eql_v4.query_json)`, + args: []any{must(f.EqAt(ctx, cipher, languages, "csharp"))}, + want: []int64{2, 3, 4, 6}, + }, + { + name: "6 boolean composition", + predicate: `((exists($."extra"."preferred_languages"[*] ? (@ == $val1)) + || exists($."extra"."preferred_languages"[*] ? (@ == $val2))) + && !(exists($."extra"."preferred_languages"[*] ? (@ == $val3))))`, + vars: map[string]any{"val1": "csharp", "val2": "rust", "val3": "python"}, + perOperator: `SELECT id FROM entity WHERE (custom_field_values @> $1::eql_v4.query_json OR custom_field_values @> $2::eql_v4.query_json) AND NOT (custom_field_values @> $3::eql_v4.query_json)`, + args: []any{must(f.EqAt(ctx, cipher, languages, "csharp")), must(f.EqAt(ctx, cipher, languages, "rust")), must(f.EqAt(ctx, cipher, languages, "python"))}, + want: []int64{1, 2}, + }, + { + name: "7 element-scoped", + predicate: `exists($."extra"."emergency_contacts"[*] ? ((@."relationship" == $val1 && @."previous_call_count" > $val2)))`, + vars: map[string]any{"val1": "Sibling", "val2": 5}, + perOperator: `SELECT id FROM entity WHERE eql_v4.json_tree_element_match(custom_field_values, $1::eql_v4.query_json_elements)`, + args: []any{must(f.Elements.ExtraEmergencyContacts.Match(ctx, cipher, encrypt.ElementPredicates{ + encrypt.MemberEq("relationship", "Sibling"), encrypt.MemberGt("previous_call_count", 5), + }, encrypt.JSONReadOptions{}))}, + want: []int64{2, 5}, + }, + } { + q, err := f.Match(ctx, cipher, tc.predicate, tc.vars, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatalf("%s: %v", tc.name, err) + } + vars, err := json.Marshal(tc.vars) + if err != nil { + t.Fatal(err) + } + for _, form := range []struct { + name string + sql string + args []any + }{ + {"@@", `SELECT id FROM entity WHERE custom_field_values @@ $1::eql_v4.query_json_predicate`, []any{q}}, + {"json_tree_match", `SELECT id FROM entity WHERE eql_v4.json_tree_match(custom_field_values, $1::eql_v4.query_json_predicate)`, []any{q}}, + {"per operator", tc.perOperator, tc.args}, + {"jsonb_path_match over the plaintext", `SELECT id FROM entity WHERE jsonb_path_match(plain, $1::jsonpath, $2::jsonb)`, []any{tc.predicate, string(vars)}}, + } { + got, err := ids(form.sql, form.args...) + if err != nil { + t.Fatalf("%s, %s: %v", tc.name, form.name, err) + } + if !slices.Equal(got, tc.want) { + t.Errorf("%s, %s: rows %v, want %v", tc.name, form.name, got, tc.want) + } + } + } + + // The element-scoped filter reads each array to the bound: one shorter + // than an array fails the query, rather than answer over part of it. + short, err := f.Match(ctx, cipher, `exists($.extra.emergency_contacts[*] ? (@.relationship == $r && @.previous_call_count > $n))`, + map[string]any{"r": "Sibling", "n": 5}, encrypt.JSONReadOptions{WildcardBound: 1}) + if err != nil { + t.Fatal(err) + } + _, err = ids(`SELECT id FROM entity WHERE custom_field_values @@ $1::eql_v4.query_json_predicate`, short) + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) || pgErr.Code != "22023" { + t.Errorf("a bound of 1 over two contacts: %v, want SQLSTATE 22023", err) + } +} diff --git a/languages/golang/encrypt/json_match_test.go b/languages/golang/encrypt/json_match_test.go new file mode 100644 index 000000000..2ac529c86 --- /dev/null +++ b/languages/golang/encrypt/json_match_test.go @@ -0,0 +1,392 @@ +package encrypt_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "slices" + "strings" + "testing" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/encrypt/eql" + "github.com/cipherstash/stack/languages/golang/encrypt/internal/testusers" + "github.com/cipherstash/vitaminc/bindings/go/vcvalue" +) + +// The compiled predicate of a document field (match): the generated Match +// on testusers.Entity's custom field values, which order $.weight and each +// emergency contact's relationship, previous_call_count and age. + +// predicateNode is one node of eql_v4.query_json_predicate: an object with +// one key, and, for an and, or or not, its children. +type predicateNode map[string]json.RawMessage + +func decodeNode(t *testing.T, raw json.RawMessage) (string, json.RawMessage) { + t.Helper() + var node predicateNode + if err := json.Unmarshal(raw, &node); err != nil || len(node) != 1 { + t.Fatalf("a node is an object with one key: %s (%v)", raw, err) + } + for k, v := range node { + return k, v + } + panic("unreachable") +} + +// shape writes a compiled tree with its leaves named by kind: +// and(contains, cmp.lt), and collects the leaves in order. +func shape(t *testing.T, raw json.RawMessage, leaves *[]json.RawMessage) string { + t.Helper() + key, value := decodeNode(t, raw) + switch key { + case "and", "or": + var children []json.RawMessage + if err := json.Unmarshal(value, &children); err != nil { + t.Fatalf("%s holds a list: %v", key, err) + } + parts := make([]string, len(children)) + for i, c := range children { + parts[i] = shape(t, c, leaves) + } + return key + "(" + strings.Join(parts, ", ") + ")" + case "not": + return "not(" + shape(t, value, leaves) + ")" + case "cmp": + bound, operand := decodeNode(t, value) + *leaves = append(*leaves, operand) + return "cmp." + bound + case "contains", "exists", "elements": + *leaves = append(*leaves, value) + return key + } + t.Fatalf("unknown node %q", key) + return "" +} + +// sameOperand says two operands are one: the same JSON, save the order of +// a containment needle's selectors (sv) and elements (el), which the engine +// shuffles on every derivation. +func sameOperand(t *testing.T, a, b []byte) bool { + t.Helper() + canonical := func(raw []byte) string { + var v any + if err := json.Unmarshal(raw, &v); err != nil { + t.Fatalf("%s: %v", raw, err) + } + out, err := json.Marshal(unshuffled(v)) + if err != nil { + t.Fatal(err) + } + return string(out) + } + return canonical(a) == canonical(b) +} + +// unshuffled sorts every sv and el list in v by its entries' JSON. +func unshuffled(v any) any { + switch v := v.(type) { + case map[string]any: + for k, child := range v { + v[k] = unshuffled(child) + if list, ok := v[k].([]any); ok && (k == "sv" || k == "el") { + slices.SortFunc(list, func(x, y any) int { + a, _ := json.Marshal(x) + b, _ := json.Marshal(y) + return strings.Compare(string(a), string(b)) + }) + } + } + return v + case []any: + for i := range v { + v[i] = unshuffled(v[i]) + } + return v + } + return v +} + +func compiled(t *testing.T, q eql.JSONSearchPredicateQuery) (string, []json.RawMessage) { + t.Helper() + var leaves []json.RawMessage + return shape(t, json.RawMessage(q), &leaves), leaves +} + +// A compiled predicate is a tree of what the other operators derive on the +// same field: each leaf is the operand its own method gives, and the tree +// is the predicate's boolean shape with the guards that make an absent +// path and a JSON null answer as jsonpath answers them. +func TestMatchCompilesToTheOperandsOfTheOtherOperators(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + cipher := c.DefaultKeyset() + f := testusers.EntityFields.CustomFieldValues + must := func(q []byte, err error) json.RawMessage { + t.Helper() + if err != nil { + t.Fatal(err) + } + return q + } + q, err := f.Match(ctx, cipher, `$."extra"."country" == $val && $."weight" < $max`, + map[string]any{"val": "nz", "max": 9.5}, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatal(err) + } + if v, err := q.Value(); err != nil || !strings.HasPrefix(fmt.Sprint(v), `{"and":`) { + t.Fatalf("Match binds as its JSON: %v", err) + } + got, leaves := compiled(t, q) + // == is the containment needle; < is a node there, not null, whose + // ordering term compares. + if want := "and(contains, exists, not(contains), cmp.lt)"; got != want { + t.Fatalf("the tree is %s, want %s", got, want) + } + selector, err := f.At.Weight.Selector(ctx, cipher) + if err != nil { + t.Fatal(err) + } + for i, want := range []json.RawMessage{ + must(f.EqAt(ctx, cipher, "$.extra.country", "nz")), + must(json.Marshal(selector.Selector())), + must(f.EqAt(ctx, cipher, "$.weight", nil)), + must(f.At.Weight.Lt(ctx, cipher, 9.5)), + } { + if !sameOperand(t, leaves[i], want) { + t.Errorf("leaf %d is %s, not its operator's %s", i, leaves[i], want) + } + } + + // The other shapes: != is a node there that is not the value; exists + // is the selector; a filter of one condition is a needle; ! and || + // keep their places. + for _, tc := range []struct { + predicate string + vars any + want string + }{ + {`$.address.postcode != null`, nil, "and(exists, not(contains))"}, + {`exists($.address.postcode)`, nil, "exists"}, + {`!(exists($.extra.preferred_languages[*] ? (@ == $v)))`, map[string]string{"v": "csharp"}, "not(contains)"}, + {`$.score == 3.14 || !($.weight >= 2)`, nil, "or(contains, not(and(exists, not(contains), cmp.gte)))"}, + {`exists($.extra.emergency_contacts[*] ? (@.relationship == $r && @.previous_call_count > $n))`, map[string]any{"r": "Sibling", "n": 5}, "elements"}, + {`$.extra.emergency_contacts[*].age <= 40`, nil, "elements"}, + } { + q, err := f.Match(ctx, cipher, tc.predicate, tc.vars, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatalf("%s: %v", tc.predicate, err) + } + if got, _ := compiled(t, q); got != tc.want { + t.Errorf("%s compiles to %s, want %s", tc.predicate, got, tc.want) + } + } + + // A filter of more than one condition is the element match of the + // array, read to the same bound. + opts := encrypt.JSONReadOptions{WildcardBound: 4} + q, err = f.Match(ctx, cipher, `exists($.extra.emergency_contacts[*] ? (@.relationship == $r && @.previous_call_count > $n))`, + map[string]any{"r": "Sibling", "n": 5}, opts) + if err != nil { + t.Fatal(err) + } + _, leaves = compiled(t, q) + elements := must(f.Elements.ExtraEmergencyContacts.Match(ctx, cipher, encrypt.ElementPredicates{ + encrypt.MemberEq("relationship", "Sibling"), encrypt.MemberGt("previous_call_count", 5), + }, opts)) + if !sameOperand(t, leaves[0], elements) { + t.Errorf("the filter is %s, not the element match %s", leaves[0], elements) + } +} + +// vars are any value encoding/json writes as an object: a map, a struct, a +// json.RawMessage of one; nil, or a value that writes null, is none. Each +// derives the operand its JSON text does. +func TestMatchTakesItsVarsAsAnyJSONObject(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + cipher := c.DefaultKeyset() + f := testusers.EntityFields.CustomFieldValues + predicate := `$.extra.country == $val && $.weight < $max` + derive := func(predicate string, vars any) eql.JSONSearchPredicateQuery { + t.Helper() + q, err := f.Match(ctx, cipher, predicate, vars, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatalf("%T: %v", vars, err) + } + return q + } + want := derive(predicate, map[string]any{"val": "nz", "max": 9.5}) + type varsStruct struct { + Val string `json:"val"` + Max float64 `json:"max"` + } + for _, vars := range []any{ + json.RawMessage(`{"val": "nz", "max": 9.5}`), + varsStruct{Val: "nz", Max: 9.5}, + &varsStruct{Val: "nz", Max: 9.5}, + map[string]json.RawMessage{"val": json.RawMessage(`"nz"`), "max": json.RawMessage(`9.5`)}, + } { + if got := derive(predicate, vars); !sameOperand(t, json.RawMessage(got), json.RawMessage(want)) { + t.Errorf("%T derives another operand", vars) + } + } + // Literals in place of variables derive the same tree, with no vars, + // with vars that write null (nil, a nil map, a nil pointer of any type, + // a string type's included, the literal null), or with an empty object. + literal := `$.extra.country == "nz" && $.weight < 9.5` + type text string + var nilMap map[string]any + var nilStruct *varsStruct + var nilMapPointer *map[string]any + var nilString *string + var nilText *text + for _, vars := range []any{nil, nilMap, nilStruct, nilMapPointer, nilString, nilText, json.RawMessage(`null`), json.RawMessage(" null\n"), map[string]any{}} { + if got := derive(literal, vars); !sameOperand(t, json.RawMessage(got), json.RawMessage(want)) { + t.Errorf("%#v: literals derive another operand", vars) + } + } +} + +// What Match refuses, each an *encrypt.Diagnostic wrapping the sentinel +// its status maps to: a grammar refusal is ErrEncoding with its reason, +// the construct in the grammar's words and the byte offset in the +// predicate; an ordering at a path the field does not order is ErrTerm. +// None of them holds a variable's value or the predicate's text. +func TestMatchRefusalsNameTheConstructAndItsOffset(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + cipher := c.DefaultKeyset() + f := testusers.EntityFields.CustomFieldValues + vars := map[string]any{"v": "secret-nz", "flag": true} + deep := strings.Repeat("(", 33) + "$.a == 1" + strings.Repeat(")", 33) + for _, tc := range []struct { + name, predicate string + vars any + reason string + construct string + at string // the text the offset points at + }{ + {"text the grammar does not read", `$.extra.country = $v`, vars, "json_predicate_syntax", "text the grammar does not read", "= $v"}, + {"like_regex", `$.extra.country like_regex "secret"`, vars, "json_predicate_unsupported", "`like_regex`", "like_regex"}, + {"arithmetic", `$.weight + 1 == 2`, vars, "json_predicate_unsupported", "arithmetic (`+`, `-`, `*`, `/`, `%`)", "+ 1"}, + {"a method", `$.weight.size() == 1`, vars, "json_predicate_unsupported", "a method (`.name()`)", ".size()"}, + {"a filter outside exists", `$.extra.preferred_languages[*] ? (@ == $v) == 1`, vars, "json_predicate_unsupported", "a filter outside `exists`", "? (@"}, + {"|| in a filter", `exists($.extra.emergency_contacts[*] ? (@.relationship == $v || @.age > 1))`, vars, "json_predicate_unsupported", "`||` in a filter", "|| @.age"}, + {"an unknown variable", `$.extra.country == $missing`, vars, "json_predicate_unknown_variable", "a variable", "$missing"}, + {"a value an ordering cannot take", `$.weight < $flag`, vars, "json_predicate_value_type", "a value", "$flag"}, + {"an object", `$.extra.country == $obj`, map[string]any{"obj": map[string]any{"secret-nz": 1}}, "json_predicate_value_type", "a value", "$obj"}, + {"!= at a wildcard", `$.extra.preferred_languages[*] != $v`, vars, "json_predicate_not_equal_at_wildcard", "`!=` at a path with a wildcard", "!= $v"}, + {"too deep", deep, nil, "json_predicate_too_deep", "nesting", "$.a"}, + {"a position", `$.extra.preferred_languages[2] == $v`, vars, "json_path_position", "a position (`[2]`)", "[2]"}, + {"a filter with no wildcard", `exists($.address ? (@.postcode == $v))`, vars, "json_path_no_wildcard", "a filter on a path with no wildcard", "$.address ?"}, + {"a wildcard in a member", `exists($.extra.emergency_contacts[*] ? (@.calls[*] == $v))`, vars, "json_path_wildcard", "a wildcard in a filter's member", "@.calls"}, + {"a number JSON cannot hold", `$.weight == 1e999`, vars, "json_syntax", "a value", "1e999"}, + } { + _, err := f.Match(ctx, cipher, tc.predicate, tc.vars, encrypt.JSONReadOptions{}) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &d) { + t.Errorf("%s: %v, want ErrEncoding with a Diagnostic", tc.name, err) + continue + } + want := strings.Index(tc.predicate, tc.at) + if at, ok := d.At(); d.Reason() != tc.reason || d.Construct() != tc.construct || !ok || at != want { + t.Errorf("%s: reason %q, construct %q, at %d (%v); want %q, %q, %d: %v", tc.name, d.Reason(), d.Construct(), at, ok, tc.reason, tc.construct, want, err) + } + if d.Field() != "custom_field_values" || d.Code != "stack_encrypt::dynamic_predicate" { + t.Errorf("%s: field %q, code %q", tc.name, d.Field(), d.Code) + } + if strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), tc.predicate) { + t.Errorf("%s: the error holds the caller's text: %v", tc.name, err) + } + } + + // The input itself: vars that are not an object, or not JSON. + for _, tc := range []struct { + name string + vars any + reason string + }{ + {"vars that are a list", []string{"secret-nz"}, "json_input"}, + {"vars that are a number", 1, "json_input"}, + {"vars that are not JSON", json.RawMessage(`{"v": "secret-nz"`), "json_syntax"}, + {"vars that are an empty json.RawMessage", json.RawMessage{}, "json_syntax"}, + {"vars that are a nil json.RawMessage", json.RawMessage(nil), "json_syntax"}, + {"vars that are whitespace", json.RawMessage(" \t\n "), "json_syntax"}, + } { + _, err := f.Match(ctx, cipher, `$.extra.country == $v`, tc.vars, encrypt.JSONReadOptions{}) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &d) || d.Reason() != tc.reason { + t.Errorf("%s: %v, want ErrEncoding with reason %s", tc.name, err, tc.reason) + } + if err != nil && strings.Contains(err.Error(), "secret") { + t.Errorf("%s: the error holds a value: %v", tc.name, err) + } + } + + // An ordering at a path the field does not order is the index's + // refusal, as the orderings at a path give it. + for _, predicate := range []string{ + `$.score < 3`, + `exists($.extra.emergency_contacts[*] ? (@.name == $v && @.age > 1))`, + } { + _, err := f.Match(ctx, cipher, predicate, vars, encrypt.JSONReadOptions{}) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrTerm) || !errors.As(err, &d) || d.Code != "stack_encrypt::json_path_not_ordered" { + t.Errorf("%s: %v, want ErrTerm json_path_not_ordered", predicate, err) + } + } + + // Refused before the engine is asked: vars that are a Go string, which + // writes one JSON string, and no cipher. + for _, bad := range []any{`{"v": "secret-nz"}`, new(string)} { + _, err := f.Match(ctx, cipher, `$.extra.country == $v`, bad, encrypt.JSONReadOptions{}) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || errors.As(err, &d) || !strings.Contains(err.Error(), "json.RawMessage") { + t.Errorf("%T vars: %v, want ErrEncoding from the SDK", bad, err) + } + if strings.Contains(err.Error(), "secret") { + t.Errorf("%T vars: the error holds a value: %v", bad, err) + } + } + if _, err := f.Match(ctx, nil, `$.weight < 1`, nil, encrypt.JSONReadOptions{}); err == nil { + t.Fatal("Match with no cipher") + } + + // A bound of 0 is the engine's default through Match, so the engine's + // own refusal of 0 is reached through QueryTerm alone. + input := vcvalue.Object{{Key: "predicate", Value: `exists($.a)`}, {Key: "wildcard_bound", Value: uint32(0)}} + _, err := cipher.QueryTerm(ctx, contactsPlan(), "custom", "match", input) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &d) || d.Reason() != "json_wildcard_bound_zero" { + t.Errorf("a bound of 0: %v", err) + } +} + +// A compiled predicate is an EQL value: it binds as its JSON, and prints +// and logs as its type and nothing of its terms. +func TestMatchOperandPrintsAndLogsRedacted(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + q, err := testusers.EntityFields.CustomFieldValues.Match(ctx, c.DefaultKeyset(), `$.extra.country == "nz"`, nil, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatal(err) + } + if v, err := q.Value(); err != nil || !strings.Contains(fmt.Sprint(v), `"contains"`) { + t.Fatalf("Value is the JSON: %v", err) + } + var logged bytes.Buffer + slog.New(slog.NewJSONHandler(&logged, nil)).Info("query", "q", q) + for _, verb := range []string{"%v", "%+v", "%#v", "%s", "%q", "%x", "%d"} { + out := fmt.Sprintf(verb, q) + if out != "eql.JSONSearchPredicateQuery{[redacted]}" { + t.Errorf("%s prints %s", verb, out) + } + } + if strings.Contains(logged.String(), "contains") || !strings.Contains(logged.String(), `"q":"eql.JSONSearchPredicateQuery{[redacted]}"`) { + t.Errorf("slog records %s", logged.String()) + } +} diff --git a/languages/golang/encrypt/json_tree_test.go b/languages/golang/encrypt/json_tree_test.go index dc6d57840..37023f35f 100644 --- a/languages/golang/encrypt/json_tree_test.go +++ b/languages/golang/encrypt/json_tree_test.go @@ -154,7 +154,7 @@ func TestJSONOperatorsDeriveTaggedTerms(t *testing.T) { "contains": "json", "contained_by": "json", "eq_at": "path_json", "lt_at": "path_json", "lte_at": "path_json", "gt_at": "path_json", "gte_at": "path_json", "between_at": "path_pair", "selector": "path", "subtree": "path_bound", - "children": "path", "child_count": "path", "element_match": "path_predicates", + "children": "path", "child_count": "path", "element_match": "path_predicates", "match": "predicate", } if !reflect.DeepEqual(inputs, want) { t.Fatalf("operators = %v", inputs) diff --git a/languages/golang/encrypt/query_test.go b/languages/golang/encrypt/query_test.go index b2dc90dc0..64a018d4c 100644 --- a/languages/golang/encrypt/query_test.go +++ b/languages/golang/encrypt/query_test.go @@ -459,7 +459,8 @@ func TestTheCatalogListsEachTypesOperators(t *testing.T) { } case target.Name == "JsonSearch": // A document's operators, under the index's defaults: no - // ordering without an ordered path. Each through the JSON index. + // ordering without an ordered path, and the compiled predicate, + // which needs none. Each through the JSON index. var names []string for _, op := range target.Operators { if op.Index != record.JSONIndex || op.Fidelity != record.Exact { @@ -467,7 +468,7 @@ func TestTheCatalogListsEachTypesOperators(t *testing.T) { } names = append(names, op.Name) } - if want := []string{"contains", "contained_by", "eq_at", "selector", "subtree", "children", "child_count"}; !slices.Equal(names, want) { + if want := []string{"contains", "contained_by", "eq_at", "selector", "subtree", "children", "child_count", "match"}; !slices.Equal(names, want) { t.Fatalf("JsonSearch answers %v, want %v", names, want) } case len(target.Operators) != 0: diff --git a/languages/golang/encrypt/targets_test.go b/languages/golang/encrypt/targets_test.go index 3b48f8274..451fcc7ab 100644 --- a/languages/golang/encrypt/targets_test.go +++ b/languages/golang/encrypt/targets_test.go @@ -117,7 +117,7 @@ func TestParseQueryTermTakesOnlyWhatTheEngineWrites(t *testing.T) { } for _, name := range []string{"eq", "lt", "lte", "gt", "gte", "between", "matches", "contains", "contained_by", "eq_at", "lt_at", "lte_at", "gt_at", "gte_at", "between_at", - "selector", "subtree", "children", "child_count", "element_match"} { + "selector", "subtree", "children", "child_count", "element_match", "match"} { got, err := parseQueryTerm(term(name, "exact", "ore")) if err != nil || got.Operator != name || got.Fidelity != record.Exact || got.Index != record.Ore { t.Errorf("%s: %+v, %v", name, got, err) diff --git a/languages/golang/internal/guest/diagnostic.go b/languages/golang/internal/guest/diagnostic.go index 603fd6277..26ddf3403 100644 --- a/languages/golang/internal/guest/diagnostic.go +++ b/languages/golang/internal/guest/diagnostic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "math" "github.com/cipherstash/vitaminc/bindings/go/vcffi" "github.com/cipherstash/vitaminc/bindings/go/vcvalue" @@ -23,8 +24,10 @@ import ( // What it may carry is the rule written on stack-profile's ErrorPayload: // keyset ids and names, field names, counts, index kinds, ZeroKMS request // kinds and HTTP statuses, workspace ids, CRNs and regions, profile file -// paths. Never plaintext, key material, tokens, ciphertext or term bytes, -// or a context's values: a stored context's length and parts at most. +// paths, and for a refused predicate the construct in the grammar's words +// and a byte offset. Never plaintext, key material, tokens, ciphertext or +// term bytes, or a context's values: a stored context's length and parts +// at most. // // A guest built before se_last_error, or one whose error does not decode, // returns the bare sentinel instead: the detail is never allowed to hide @@ -126,6 +129,88 @@ func (d *Diagnostic) Reason() string { return reason } +// Construct is what a refused predicate (a JSONSearch field's Match) names +// as refused, in the grammar's own words ("`like_regex`", "a position +// (`[2]`)", "a variable"), or "" for an error that names none. It is a +// fixed string of the grammar, never text from the predicate. +func (d *Diagnostic) Construct() string { + construct, _ := d.Fields["construct"].(string) + return construct +} + +// At is the byte offset into the caller's text where a refusal starts: +// for a refused Match predicate, where the refused construct begins in the +// predicate string. The offset is read whatever integer width the value +// transport carried it in, and from a float that holds a whole number. ok +// is false for an error that gives no offset, or one that is negative, not +// a whole number, or past int. +func (d *Diagnostic) At() (offset int, ok bool) { + return nonNegativeInt(d.Fields["at"]) +} + +// nonNegativeInt reads a decoded field as a non-negative int: any Go +// integer type, or a float holding a whole number exactly (below 2^53). +func nonNegativeInt(v any) (int, bool) { + var signed int64 + switch n := v.(type) { + case int: + signed = int64(n) + case int8: + signed = int64(n) + case int16: + signed = int64(n) + case int32: + signed = int64(n) + case int64: + signed = n + case uint: + return unsignedInt(uint64(n)) + case uint8: + return unsignedInt(uint64(n)) + case uint16: + return unsignedInt(uint64(n)) + case uint32: + return unsignedInt(uint64(n)) + case uint64: + return unsignedInt(n) + case float32: + return wholeFloat(float64(n)) + case float64: + return wholeFloat(n) + default: + return 0, false + } + if signed < 0 || signed > math.MaxInt { + return 0, false + } + return int(signed), true +} + +func unsignedInt(n uint64) (int, bool) { + if n > math.MaxInt { + return 0, false + } + return int(n), true +} + +// wholeFloat reads f as an int: a whole number from 0, below 2^53 (where +// a float64 stops holding every integer exactly) and at most math.MaxInt. +func wholeFloat(f float64) (int, bool) { + n, ok := wholeFloatUpTo(f, math.MaxInt) + return int(n), ok +} + +// wholeFloatUpTo reads f as a whole number from 0, below 2^53 and at most +// limit, which is math.MaxInt for the int wholeFloat returns: a bound of +// 2^31-1 where int is 32 bits, so a larger float is refused rather than +// overflowing the conversion. +func wholeFloatUpTo(f float64, limit int64) (int64, bool) { + if f < 0 || f >= 1<<53 || f > float64(limit) || f != math.Trunc(f) { + return 0, false + } + return int64(f), true +} + // parseUUID parses a canonical hyphenated UUID. func parseUUID(text string) ([16]byte, bool) { var id [16]byte diff --git a/languages/golang/internal/guest/diagnostic_test.go b/languages/golang/internal/guest/diagnostic_test.go index 7b5b0e0fd..28f60b687 100644 --- a/languages/golang/internal/guest/diagnostic_test.go +++ b/languages/golang/internal/guest/diagnostic_test.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "errors" + "math" + "strconv" "strings" "testing" @@ -283,6 +285,66 @@ func TestKeysetAccessorsAnswerOnlyForAForeignKeyset(t *testing.T) { } } +// A refused predicate's construct and offset read as they were decoded, +// the offset in whatever integer width the transport carried it, or as a +// float holding a whole number. An error with neither, or an offset that +// is negative, fractional, past int or not a number, gives none. +func TestConstructAndAtReadARefusedPredicate(t *testing.T) { + d := &guest.Diagnostic{Fields: map[string]any{"construct": "`like_regex`", "at": uint64(4)}} + if d.Construct() != "`like_regex`" { + t.Errorf("Construct() = %q", d.Construct()) + } + for _, good := range []any{ + int(4), int8(4), int16(4), int32(4), int64(4), + uint(4), uint8(4), uint16(4), uint32(4), uint64(4), + float32(4), float64(4), + } { + d := &guest.Diagnostic{Fields: map[string]any{"at": good}} + if at, ok := d.At(); !ok || at != 4 { + t.Errorf("At() of %#v = %d, %v", good, at, ok) + } + } + if at, ok := (&guest.Diagnostic{Fields: map[string]any{"at": uint32(0)}}).At(); !ok || at != 0 { + t.Errorf("an offset of 0: %d, %v", at, ok) + } + for _, bad := range []any{nil, "4", true, int32(-1), int64(-4), float64(-1), float64(4.5), float32(0.5), math.NaN(), math.Inf(1), float64(1 << 53), uint64(1) << 63} { + d := &guest.Diagnostic{Fields: map[string]any{"at": bad}} + if _, ok := d.At(); ok { + t.Errorf("At read %#v", bad) + } + } + none := &guest.Diagnostic{Fields: map[string]any{}} + if _, ok := none.At(); ok || none.Construct() != "" { + t.Error("Construct or At answered for an error that gives none") + } +} + +// A float offset is read only up to math.MaxInt: where int is 32 bits, a +// whole float past 2^31-1 is refused rather than overflowing the int it +// converts to. The bound is tested at a 32-bit limit on every platform, and +// At at the platform's own int, which CI also runs under GOARCH=386. +func TestAFloatOffsetIsReadOnlyUpToMaxInt(t *testing.T) { + for _, tc := range []struct { + f float64 + ok bool + }{ + {0, true}, + {math.MaxInt32, true}, + {math.MaxInt32 + 1, false}, + {3e9, false}, + {1 << 53, false}, + } { + n, ok := guest.WholeFloatUpTo(tc.f, math.MaxInt32) + if ok != tc.ok || (ok && n != int64(tc.f)) { + t.Errorf("WholeFloatUpTo(%v, MaxInt32) = %d, %v; want ok %v", tc.f, n, ok, tc.ok) + } + } + at, ok := (&guest.Diagnostic{Fields: map[string]any{"at": float64(3e9)}}).At() + if want := 3e9 <= math.MaxInt; ok != want || (ok && int64(at) != 3e9) { + t.Errorf("At() of 3e9 = %d, %v; want ok %v where int is %d bits", at, ok, want, strconv.IntSize) + } +} + // With no detail to give, the failure is the bare sentinel, exactly as // before the detail existed: a missing explanation never hides the failure. func TestWithoutDetailTheFailureIsTheBareSentinel(t *testing.T) { diff --git a/languages/golang/internal/guest/export_test.go b/languages/golang/internal/guest/export_test.go index e7f741479..468ff7bd0 100644 --- a/languages/golang/internal/guest/export_test.go +++ b/languages/golang/internal/guest/export_test.go @@ -12,3 +12,9 @@ import ( func (e Exports) Diagnose(ctx context.Context, m api.Module, kind error) error { return e.diagnose(ctx, m, kind) } + +// WholeFloatUpTo is wholeFloatUpTo, for the external tests: the bound a +// float offset is read to, at a limit of the test's choosing. +func WholeFloatUpTo(f float64, limit int64) (int64, bool) { + return wholeFloatUpTo(f, limit) +} diff --git a/languages/golang/internal/record/record.go b/languages/golang/internal/record/record.go index 05992135d..06a570453 100644 --- a/languages/golang/internal/record/record.go +++ b/languages/golang/internal/record/record.go @@ -620,8 +620,8 @@ type Operator struct { // Name is the operator's name, as a query names it: eq, lt, lte, gt, // gte, between or matches; for a field that stores a JSON document, // contains, contained_by, eq_at, lt_at, lte_at, gt_at, gte_at, - // between_at, selector, subtree, children, child_count or - // element_match. + // between_at, selector, subtree, children, child_count, element_match + // or match. Name string // Fidelity is how far the server's answer can be trusted. Fidelity Fidelity @@ -632,7 +632,7 @@ type Operator struct { Index Output // Input is the shape of the operator's input, as the engine names it: // value (one value of the field's kind), pair (two, low then high), - // json, path_json, path_pair, path, path_bound or path_predicates - // (objects of named inputs, for a document's operators). + // json, path_json, path_pair, path, path_bound, path_predicates or + // predicate (objects of named inputs, for a document's operators). Input string } diff --git a/languages/golang/stashgen/emit.go b/languages/golang/stashgen/emit.go index 45f64c7de..ba27227d2 100644 --- a/languages/golang/stashgen/emit.go +++ b/languages/golang/stashgen/emit.go @@ -1011,7 +1011,7 @@ var jsonOperators = map[string]bool{ "contains": true, "contained_by": true, "eq_at": true, "selector": true, "subtree": true, "children": true, "child_count": true, "lt_at": true, "lte_at": true, "gt_at": true, "gte_at": true, "between_at": true, - "element_match": true, + "element_match": true, "match": true, } // planJSON checks a document field's operators against the methods the @@ -1029,6 +1029,13 @@ func planJSON(f *genFile, g *genField) error { if !document { return fieldErr(f.typeName, g.GoName, "the engine answers no document operator on %s, and the generator writes document fields of JsonSearch only", g.EQLType) } + // The engine answers match on every document field: what a predicate + // may use is decided when it is compiled, not by the tag. A document + // field with none is the engine and the generator disagreeing. This is + // the one rule: jsonEntry writes Match on every field it passes. + if !answers(g, "match") { + return fieldErr(f.typeName, g.GoName, "the engine answers no match on %s: the generator and its engine disagree, which is a bug in this stashgen", g.EQLType) + } g.jsonPaths = nil seen := map[string]string{} given := map[string]bool{} @@ -1177,6 +1184,7 @@ func (w *writer) jsonEntry(g genField) { leaf := strings.TrimSuffix(needle, "Query") + "LeafQuery" entry := "eql." + EQLGoName("JsonEntry") elementsQuery := strings.TrimSuffix(needle, "Query") + "ElementsQuery" + predicateQuery := strings.TrimSuffix(needle, "Query") + "PredicateQuery" // planJSON refuses ordered paths the engine answers no ordering on, // and arrays it answers no element_match on. ordered := len(g.jsonPaths) > 0 @@ -1282,6 +1290,8 @@ func (w *writer) jsonEntry(g genField) { w.p("\treturn gensupport.JSONNodeToken(ctx, f.field, c, \"child_count\", path)") w.p("}") } + // planJSON refuses a document field the engine answers no match on. + w.jsonMatch(recv, predicateQuery) if arrays { w.jsonElements(g, field, elementsQuery) } @@ -1354,3 +1364,24 @@ func (w *writer) jsonElements(g genField, field, query string) { w.p("\treturn gensupport.JSONElementMatch[%s](ctx, a.field, c, a.path, predicates, opts)", query) w.p("}") } + +// jsonMatch writes a document field's Match, which compiles a jsonpath +// predicate into the operand eql_v4.json_tree_match and `@@` evaluate. +func (w *writer) jsonMatch(recv, query string) { + w.nl() + for i, para := range []string{ + "Match derives the operand for a jsonpath predicate over the document, as plaintext jsonb_path_match(col, predicate, vars) takes one: bind it as col @@ $1::eql_v4.query_json_predicate, or eql_v4.json_tree_match(col, $1::eql_v4.query_json_predicate). vars are the predicate's variables ($name): a map, a struct or a json.RawMessage that writes a JSON object, or nil for none.", + "The engine reads the predicate, and nothing in Go does: paths of .key, .\"quoted key\" and [*] steps; ==, !=, <, <=, > and >= between a path and a variable or a JSON literal; exists(path), and exists(path ? (@.m == $v && @.n > $w)) for several members of one array element; &&, || and !(…). Anything else is refused, before any term is derived, as an *encrypt.Diagnostic wrapping encrypt.ErrEncoding whose Reason, Construct and At name the refusal and the byte offset in predicate where it starts. An ordering at a path the tag does not order, or a member of a filter of more than one condition that it does not order, is refused wrapping encrypt.ErrTerm.", + "The tree is evaluated inside eql_v4.json_tree_match, which engages no index: every row the scan gives it is opened and tested. The per-operator methods engage the GIN index for @> (EqAt, Contains) and a btree on an ordered path's term (At.), so a query that needs an index writes those leaves with them, or narrows the rows with one beside Match.", + "The answer is jsonb_path_match's over the plaintext, true, false or NULL, save four cases. Lax mode's unwrapping of an array the path names no [*] for is not applied: write [*]. Nor is its wrapping of a value that is not an array under [*]. A string compared with a number is false under == and true under !=, where jsonpath says unknown. Orderings, and filters of more than one condition, compare folded ordering terms: text case- and accent-folded, numbers as float64, so such a filter's == is of folded values; == elsewhere is exact.", + "A filter of more than one condition reads each array to opts.WildcardBound elements, 64 when it is 0. A row whose array is longer fails the query with SQLSTATE 22023 (invalid_parameter_value) rather than answer over part of it: raise the bound past the longest array.", + } { + if i > 0 { + w.p("//") + } + w.b.WriteString(wrapComment(para, 80)) + } + w.p("%sMatch(ctx context.Context, c *encrypt.Cipher, predicate string, vars any, opts encrypt.JSONReadOptions) (%s, error) {", recv, query) + w.p("\treturn gensupport.JSONMatch[%s](ctx, f.field, c, predicate, vars, opts)", query) + w.p("}") +} diff --git a/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go b/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go index 02fbd21db..576ea03fb 100644 --- a/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go +++ b/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go @@ -241,6 +241,11 @@ func JSONElementMatch[Q ~[]byte, T any](context.Context, Field[T], *encrypt.Ciph return nil, nil } +// JSONMatch derives match on a JSON field. +func JSONMatch[Q ~[]byte, T any](context.Context, Field[T], *encrypt.Cipher, string, any, encrypt.JSONReadOptions) (Q, error) { + return nil, nil +} + // JSONSelect prepares `->` at a path on a JSON field. func JSONSelect[E ~[]byte, T any](context.Context, Field[T], *encrypt.Cipher, string) (encrypt.JSONSelection[E], error) { return encrypt.JSONSelection[E]{}, nil diff --git a/mise.toml b/mise.toml index b90273322..1164e8e88 100644 --- a/mise.toml +++ b/mise.toml @@ -381,10 +381,22 @@ set -euo pipefail mise -C packages/eql run build psql "$EQL_TEST_DATABASE_URL" -v ON_ERROR_STOP=1 -q -f packages/eql/release/cipherstash-encrypt-v4.sql cd languages/golang +# The PostgreSQL tests, named once: the -run pattern and the check below +# are both derived from this list. +TESTS=( + TestEQLTypesRoundTripAndQueryInPostgres + TestEQLOrderMatchesPlaintextInPostgres + TestQueriesRunAgainstPostgres + TestJSONDocumentsQueryAndReadInPostgres + TestJSONTypedDocumentsInPostgres + TestJSONElementMatchInPostgres + TestJSONPredicateMatchInPostgres +) +pattern="^($(IFS='|'; echo "${TESTS[*]}"))$" # The tests skip without DATABASE_URL and without their guest builds; -v and # the grep turn either skip into a failure here, where both are provided. DATABASE_URL="$EQL_TEST_DATABASE_URL" CGO_ENABLED=0 go test -count=1 -v \ - -run '^(TestEQLTypesRoundTripAndQueryInPostgres|TestEQLOrderMatchesPlaintextInPostgres|TestQueriesRunAgainstPostgres|TestJSONDocumentsQueryAndReadInPostgres|TestJSONTypedDocumentsInPostgres|TestJSONElementMatchInPostgres)$' ./encrypt \ + -run "$pattern" ./encrypt \ | tee "${TMPDIR:-/tmp}/go-test-postgres.log" if grep -q -- '--- SKIP' "${TMPDIR:-/tmp}/go-test-postgres.log"; then echo "error: a PostgreSQL test skipped; build the deterministic guests first" >&2 @@ -392,7 +404,7 @@ if grep -q -- '--- SKIP' "${TMPDIR:-/tmp}/go-test-postgres.log"; then fi # A -run pattern that matches no test (one renamed, say) prints "no tests # to run" and PASS, so each test has to be seen to pass by name. -for test in TestEQLTypesRoundTripAndQueryInPostgres TestEQLOrderMatchesPlaintextInPostgres TestQueriesRunAgainstPostgres TestJSONDocumentsQueryAndReadInPostgres TestJSONTypedDocumentsInPostgres TestJSONElementMatchInPostgres; do +for test in "${TESTS[@]}"; do if ! grep -q -- "^--- PASS: $test " "${TMPDIR:-/tmp}/go-test-postgres.log"; then echo "error: $test did not run and pass; was it renamed?" >&2 exit 1 diff --git a/packages/eql/crates/eql-codegen/src/go_eql.rs b/packages/eql/crates/eql-codegen/src/go_eql.rs index ef1771d76..1b21ef257 100644 --- a/packages/eql/crates/eql-codegen/src/go_eql.rs +++ b/packages/eql/crates/eql-codegen/src/go_eql.rs @@ -92,8 +92,13 @@ fn table_row(row: &Row) -> String { ) } -/// The four methods every EQL type and query type carries, block-bodied so -/// gofmt leaves them as written. +/// The methods every EQL type and query type carries, block-bodied so +/// gofmt leaves them as written: the driver's and encoding/json's, which +/// read and write the value as the JSON it is, and fmt's and slog's, which +/// print and log the type's name and never the value, since a stored value +/// holds ciphertext and terms and a query value terms. String, Format +/// (every verb, `%#v` included) and LogValue all write one text, the +/// helper `text`'s. fn methods(out: &mut String, go: &str, domain: &str) { let _ = write!( out, @@ -104,7 +109,13 @@ fn methods(out: &mut String, go: &str, domain: &str) { \n// MarshalJSON is the EQL value as the JSON it is, or null when empty.\n\ func (v {go}) MarshalJSON() ([]byte, error) {{\n\treturn marshal(v, {q})\n}}\n\ \n// UnmarshalJSON reads the EQL value from JSON, as it is.\n\ - func (v *{go}) UnmarshalJSON(b []byte) error {{\n\treturn unmarshal((*[]byte)(v), b)\n}}\n", + func (v *{go}) UnmarshalJSON(b []byte) error {{\n\treturn unmarshal((*[]byte)(v), b)\n}}\n\ + \n// String names the type and hides the value.\n\ + func (v {go}) String() string {{\n\treturn text(v, {q})\n}}\n\ + \n// Format prints String for every verb fmt hands it, %#v included.\n\ + func (v {go}) Format(f fmt.State, _ rune) {{\n\tformat(f, v, {q})\n}}\n\ + \n// LogValue is what slog records: String.\n\ + func (v {go}) LogValue() slog.Value {{\n\treturn logValue(v, {q})\n}}\n", q = quote(go), ); } @@ -115,7 +126,7 @@ pub fn render_go_eql() -> String { let mut out = String::new(); let _ = writeln!( out, - "{GO_GENERATED_MARKER}\n\npackage eql\n\nimport \"database/sql/driver\"\n" + "{GO_GENERATED_MARKER}\n\npackage eql\n\nimport (\n\t\"database/sql/driver\"\n\t\"fmt\"\n\t\"log/slog\"\n)\n" ); out.push_str( "// Types is every EQL type the catalog has, in catalog order: what the\n\ @@ -371,6 +382,31 @@ mod tests { assert!(!out.contains("from a int"), "a int32 or a int64"); } + /// Every type the file declares prints and logs through the redacting + /// helpers, so none prints its bytes: one String, Format and LogValue + /// per `type X []byte`, each the helpers' one text. No GoString: fmt + /// reaches Format first for `%#v`. + #[test] + fn every_type_prints_and_logs_redacted() { + let out = render_go_eql(); + let types: Vec<&str> = out + .lines() + .filter_map(|l| l.strip_prefix("type ")?.strip_suffix(" []byte")) + .collect(); + assert!(types.len() > 50, "{} types", types.len()); + for go in &types { + for method in [ + format!("func (v {go}) String() string {{\n\treturn text(v, {go:?})\n}}"), + format!("func (v {go}) Format(f fmt.State, _ rune) {{\n\tformat(f, v, {go:?})\n}}"), + format!("func (v {go}) LogValue() slog.Value {{\n\treturn logValue(v, {go:?})\n}}"), + ] { + assert_eq!(out.matches(&method).count(), 1, "{method}"); + } + } + assert!(!out.contains("GoString")); + assert!(out.contains("import (\n\t\"database/sql/driver\"\n\t\"fmt\"\n\t\"log/slog\"\n)\n")); + } + #[test] fn rendering_is_deterministic() { assert_eq!(render_go_eql(), render_go_eql());