From 52ae33b6c14646dede9c88308d3fdb35bf7131fd Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 30 Oct 2026 13:57:22 +1100 Subject: [PATCH 1/3] feat(eql-codegen): print and log every Go EQL type redacted A Go EQL value is the JSON its column stores: a stored value holds ciphertext and terms, and a query value holds terms. Until now each type was a bare []byte to fmt and slog, so %s or a log line wrote those bytes out. The compiled predicate Match returns (#1443) is the first operand a caller is likely to log whole, so every generated type now gets String, Format and LogValue, written by eql-codegen beside Value and Scan so no type can be missed. All three write one text, the type's name and [redacted] (or {} when empty); Format answers every verb, %#v included, so no GoString is generated. Value, MarshalJSON and a []byte or string conversion still give the JSON. The helpers (text, format, logValue) are hand-written in eql.go, so the generated methods stay one-line block bodies gofmt leaves alone. A codegen test checks every `type X []byte` the file declares has all three. --- languages/golang/encrypt/eql/eql.go | 40 +- languages/golang/encrypt/eql/eql_gen.go | 2379 ++++++++++++++++- languages/golang/encrypt/eql/eql_test.go | 67 + packages/eql/crates/eql-codegen/src/go_eql.rs | 50 +- 4 files changed, 2530 insertions(+), 6 deletions(-) diff --git a/languages/golang/encrypt/eql/eql.go b/languages/golang/encrypt/eql/eql.go index d6efdcb1a..76d49448e 100644 --- a/languages/golang/encrypt/eql/eql.go +++ b/languages/golang/encrypt/eql/eql.go @@ -8,7 +8,13 @@ // 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. To print or log the JSON on purpose, call +// Reveal at that call: slog.Debug("stored", "email", v.Reveal()). In code, +// 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 +34,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "log/slog" ) // Type is one EQL type as the catalog describes it: what stashgen learns @@ -121,3 +129,33 @@ 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)) +} + +// reveal is an EQL value's JSON as text, for the Reveal its type carries: +// a string, so fmt and every slog handler print it as text and not as a +// list of byte values. Empty when the value is. +func reveal(b []byte) string { + return string(b) +} diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index d29569c59..a5f55a8d5 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,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Integer) Reveal() string { + return reveal(v) +} + // 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 +135,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerEq) Reveal() string { + return reveal(v) +} + // IntegerEqQuery is the query value for IntegerEq: the operand eql_v4.query_integer_eq takes. type IntegerEqQuery []byte @@ -133,6 +179,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerEqQuery) Reveal() string { + return reveal(v) +} + // 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 +224,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrdOre) Reveal() string { + return reveal(v) +} + // IntegerOrdOreQuery is the query value for IntegerOrdOre: the operand eql_v4.query_integer_ord_ore takes. type IntegerOrdOreQuery []byte @@ -180,6 +268,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +313,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrd) Reveal() string { + return reveal(v) +} + // IntegerOrdQuery is the query value for IntegerOrd: the operand eql_v4.query_integer_ord takes. type IntegerOrdQuery []byte @@ -227,6 +357,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +402,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrdOpe) Reveal() string { + return reveal(v) +} + // IntegerOrdOpeQuery is the query value for IntegerOrdOpe: the operand eql_v4.query_integer_ord_ope takes. type IntegerOrdOpeQuery []byte @@ -274,6 +446,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +491,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Smallint) Reveal() string { + return reveal(v) +} + // 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 +536,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintEq) Reveal() string { + return reveal(v) +} + // SmallintEqQuery is the query value for SmallintEq: the operand eql_v4.query_smallint_eq takes. type SmallintEqQuery []byte @@ -345,6 +580,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintEqQuery) Reveal() string { + return reveal(v) +} + // 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 +625,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrdOre) Reveal() string { + return reveal(v) +} + // SmallintOrdOreQuery is the query value for SmallintOrdOre: the operand eql_v4.query_smallint_ord_ore takes. type SmallintOrdOreQuery []byte @@ -392,6 +669,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +714,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrd) Reveal() string { + return reveal(v) +} + // SmallintOrdQuery is the query value for SmallintOrd: the operand eql_v4.query_smallint_ord takes. type SmallintOrdQuery []byte @@ -439,6 +758,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +803,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrdOpe) Reveal() string { + return reveal(v) +} + // SmallintOrdOpeQuery is the query value for SmallintOrdOpe: the operand eql_v4.query_smallint_ord_ope takes. type SmallintOrdOpeQuery []byte @@ -486,6 +847,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +892,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Bigint) Reveal() string { + return reveal(v) +} + // 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 +937,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintEq) Reveal() string { + return reveal(v) +} + // BigintEqQuery is the query value for BigintEq: the operand eql_v4.query_bigint_eq takes. type BigintEqQuery []byte @@ -557,6 +981,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintEqQuery) Reveal() string { + return reveal(v) +} + // 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 +1026,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrdOre) Reveal() string { + return reveal(v) +} + // BigintOrdOreQuery is the query value for BigintOrdOre: the operand eql_v4.query_bigint_ord_ore takes. type BigintOrdOreQuery []byte @@ -604,6 +1070,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +1115,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrd) Reveal() string { + return reveal(v) +} + // BigintOrdQuery is the query value for BigintOrd: the operand eql_v4.query_bigint_ord takes. type BigintOrdQuery []byte @@ -651,6 +1159,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +1204,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrdOpe) Reveal() string { + return reveal(v) +} + // BigintOrdOpeQuery is the query value for BigintOrdOpe: the operand eql_v4.query_bigint_ord_ope takes. type BigintOrdOpeQuery []byte @@ -698,6 +1248,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +1293,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Date) Reveal() string { + return reveal(v) +} + // 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 +1338,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateEq) Reveal() string { + return reveal(v) +} + // DateEqQuery is the query value for DateEq: the operand eql_v4.query_date_eq takes. type DateEqQuery []byte @@ -769,6 +1382,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateEqQuery) Reveal() string { + return reveal(v) +} + // 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 +1427,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrdOre) Reveal() string { + return reveal(v) +} + // DateOrdOreQuery is the query value for DateOrdOre: the operand eql_v4.query_date_ord_ore takes. type DateOrdOreQuery []byte @@ -816,6 +1471,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +1516,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrd) Reveal() string { + return reveal(v) +} + // DateOrdQuery is the query value for DateOrd: the operand eql_v4.query_date_ord takes. type DateOrdQuery []byte @@ -863,6 +1560,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +1605,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrdOpe) Reveal() string { + return reveal(v) +} + // DateOrdOpeQuery is the query value for DateOrdOpe: the operand eql_v4.query_date_ord_ope takes. type DateOrdOpeQuery []byte @@ -910,6 +1649,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +1694,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Timestamp) Reveal() string { + return reveal(v) +} + // 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 +1739,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampEq) Reveal() string { + return reveal(v) +} + // TimestampEqQuery is the query value for TimestampEq: the operand eql_v4.query_timestamp_eq takes. type TimestampEqQuery []byte @@ -981,6 +1783,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampEqQuery) Reveal() string { + return reveal(v) +} + // 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 +1828,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrdOre) Reveal() string { + return reveal(v) +} + // TimestampOrdOreQuery is the query value for TimestampOrdOre: the operand eql_v4.query_timestamp_ord_ore takes. type TimestampOrdOreQuery []byte @@ -1028,6 +1872,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +1917,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrd) Reveal() string { + return reveal(v) +} + // TimestampOrdQuery is the query value for TimestampOrd: the operand eql_v4.query_timestamp_ord takes. type TimestampOrdQuery []byte @@ -1075,6 +1961,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +2006,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrdOpe) Reveal() string { + return reveal(v) +} + // TimestampOrdOpeQuery is the query value for TimestampOrdOpe: the operand eql_v4.query_timestamp_ord_ope takes. type TimestampOrdOpeQuery []byte @@ -1122,6 +2050,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +2095,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Numeric) Reveal() string { + return reveal(v) +} + // 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 +2140,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericEq) Reveal() string { + return reveal(v) +} + // NumericEqQuery is the query value for NumericEq: the operand eql_v4.query_numeric_eq takes. type NumericEqQuery []byte @@ -1193,6 +2184,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericEqQuery) Reveal() string { + return reveal(v) +} + // 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 +2229,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrdOre) Reveal() string { + return reveal(v) +} + // NumericOrdOreQuery is the query value for NumericOrdOre: the operand eql_v4.query_numeric_ord_ore takes. type NumericOrdOreQuery []byte @@ -1240,6 +2273,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +2318,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrd) Reveal() string { + return reveal(v) +} + // NumericOrdQuery is the query value for NumericOrd: the operand eql_v4.query_numeric_ord takes. type NumericOrdQuery []byte @@ -1287,6 +2362,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +2407,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrdOpe) Reveal() string { + return reveal(v) +} + // NumericOrdOpeQuery is the query value for NumericOrdOpe: the operand eql_v4.query_numeric_ord_ope takes. type NumericOrdOpeQuery []byte @@ -1334,6 +2451,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +2496,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Text) Reveal() string { + return reveal(v) +} + // 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 +2541,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextEq) Reveal() string { + return reveal(v) +} + // TextEqQuery is the query value for TextEq: the operand eql_v4.query_text_eq takes. type TextEqQuery []byte @@ -1405,6 +2585,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextEqQuery) Reveal() string { + return reveal(v) +} + // 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 +2630,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextMatch) Reveal() string { + return reveal(v) +} + // TextMatchQuery is the query value for TextMatch: the operand eql_v4.query_text_match takes. type TextMatchQuery []byte @@ -1452,6 +2674,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextMatchQuery) Reveal() string { + return reveal(v) +} + // 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 +2719,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrdOre) Reveal() string { + return reveal(v) +} + // TextOrdOreQuery is the query value for TextOrdOre: the operand eql_v4.query_text_ord_ore takes. type TextOrdOreQuery []byte @@ -1499,6 +2763,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +2808,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrd) Reveal() string { + return reveal(v) +} + // TextOrdQuery is the query value for TextOrd: the operand eql_v4.query_text_ord takes. type TextOrdQuery []byte @@ -1546,6 +2852,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +2897,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrdOpe) Reveal() string { + return reveal(v) +} + // TextOrdOpeQuery is the query value for TextOrdOpe: the operand eql_v4.query_text_ord_ope takes. type TextOrdOpeQuery []byte @@ -1593,6 +2941,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +2986,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextSearchOre) Reveal() string { + return reveal(v) +} + // TextSearchOreQuery is the query value for TextSearchOre: the operand eql_v4.query_text_search_ore takes. type TextSearchOreQuery []byte @@ -1640,6 +3030,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextSearchOreQuery) Reveal() string { + return reveal(v) +} + // 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 +3075,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextSearch) Reveal() string { + return reveal(v) +} + // TextSearchQuery is the query value for TextSearch: the operand eql_v4.query_text_search takes. type TextSearchQuery []byte @@ -1687,6 +3119,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextSearchQuery) Reveal() string { + return reveal(v) +} + // 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 +3164,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Boolean) Reveal() string { + return reveal(v) +} + // 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 @@ -1735,6 +3209,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Real) Reveal() string { + return reveal(v) +} + // RealEq is the EQL type stored in public.eql_v4_real_eq: the real family with the eq index. // The engine produces it from a float32. type RealEq []byte @@ -1759,6 +3254,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealEq) Reveal() string { + return reveal(v) +} + // RealEqQuery is the query value for RealEq: the operand eql_v4.query_real_eq takes. type RealEqQuery []byte @@ -1782,6 +3298,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealEqQuery) Reveal() string { + return reveal(v) +} + // 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 +3343,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrdOre) Reveal() string { + return reveal(v) +} + // RealOrdOreQuery is the query value for RealOrdOre: the operand eql_v4.query_real_ord_ore takes. type RealOrdOreQuery []byte @@ -1829,6 +3387,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +3432,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrd) Reveal() string { + return reveal(v) +} + // RealOrdQuery is the query value for RealOrd: the operand eql_v4.query_real_ord takes. type RealOrdQuery []byte @@ -1876,6 +3476,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +3521,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrdOpe) Reveal() string { + return reveal(v) +} + // RealOrdOpeQuery is the query value for RealOrdOpe: the operand eql_v4.query_real_ord_ope takes. type RealOrdOpeQuery []byte @@ -1923,6 +3565,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +3610,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v Double) Reveal() string { + return reveal(v) +} + // 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 +3655,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleEq) Reveal() string { + return reveal(v) +} + // DoubleEqQuery is the query value for DoubleEq: the operand eql_v4.query_double_eq takes. type DoubleEqQuery []byte @@ -1994,6 +3699,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleEqQuery) Reveal() string { + return reveal(v) +} + // 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 +3744,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrdOre) Reveal() string { + return reveal(v) +} + // DoubleOrdOreQuery is the query value for DoubleOrdOre: the operand eql_v4.query_double_ord_ore takes. type DoubleOrdOreQuery []byte @@ -2041,6 +3788,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrdOreQuery) Reveal() string { + return reveal(v) +} + // 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 +3833,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrd) Reveal() string { + return reveal(v) +} + // DoubleOrdQuery is the query value for DoubleOrd: the operand eql_v4.query_double_ord takes. type DoubleOrdQuery []byte @@ -2088,6 +3877,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrdQuery) Reveal() string { + return reveal(v) +} + // 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 +3922,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrdOpe) Reveal() string { + return reveal(v) +} + // DoubleOrdOpeQuery is the query value for DoubleOrdOpe: the operand eql_v4.query_double_ord_ope takes. type DoubleOrdOpeQuery []byte @@ -2135,6 +3966,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleOrdOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4011,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSON) Reveal() string { + return reveal(v) +} + // 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 +4056,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONSearch) Reveal() string { + return reveal(v) +} + // JSONSearchQuery is the query value for JSONSearch: the operand eql_v4.query_json takes. type JSONSearchQuery []byte @@ -2206,6 +4100,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONSearchQuery) Reveal() string { + return reveal(v) +} + // 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 +4144,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONSearchLeafQuery) Reveal() string { + return reveal(v) +} + // 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 +4188,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONSearchElementsQuery) Reveal() string { + return reveal(v) +} + // 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 +4232,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONSearchPredicateQuery) Reveal() string { + return reveal(v) +} + // 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 +4276,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v JSONEntry) Reveal() string { + return reveal(v) +} + // 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 +4321,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4366,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v IntegerRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4411,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4456,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v SmallintRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4501,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4546,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v BigintRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4591,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4636,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DateRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4681,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4726,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TimestampRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4771,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4816,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v NumericRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4861,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4906,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v TextRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +4951,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +4996,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v RealRangeOpeQuery) Reveal() string { + return reveal(v) +} + // 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 +5041,27 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleRangeOreQuery) Reveal() string { + return reveal(v) +} + // 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 +5085,24 @@ 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") +} + +// Reveal is the value's JSON as text, ciphertext and terms included, for a +// program that means to print or log what String hides. +func (v DoubleRangeOpeQuery) Reveal() string { + return reveal(v) +} diff --git a/languages/golang/encrypt/eql/eql_test.go b/languages/golang/encrypt/eql/eql_test.go index 815b50797..f65a5d537 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,66 @@ 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") + } + // Reveal is the named call that prints the JSON, as text through fmt + // and through slog's handlers. + if got := v.Reveal(); got != string(doc) { + t.Errorf("Reveal = %s, want the JSON", got) + } + if got := fmt.Sprint(v.Reveal()); got != string(doc) { + t.Errorf("fmt prints Reveal as %s", got) + } + logged.Reset() + slog.New(slog.NewTextHandler(&logged, &slog.HandlerOptions{Level: slog.LevelDebug})).Debug("row", "email", v.Reveal()) + if !strings.Contains(logged.String(), "SECRET") { + t.Errorf("slog does not record the revealed JSON: %s", logged.String()) + } + if eql.TextEq(nil).Reveal() != "" { + t.Error("an empty value reveals text") + } +} diff --git a/packages/eql/crates/eql-codegen/src/go_eql.rs b/packages/eql/crates/eql-codegen/src/go_eql.rs index ef1771d76..502a4adbf 100644 --- a/packages/eql/crates/eql-codegen/src/go_eql.rs +++ b/packages/eql/crates/eql-codegen/src/go_eql.rs @@ -92,8 +92,14 @@ 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. Reveal is the one way to print the value: the program +/// asks for it by name at the call that prints or logs it. fn methods(out: &mut String, go: &str, domain: &str) { let _ = write!( out, @@ -104,7 +110,16 @@ 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\ + \n// Reveal is the value's JSON as text, ciphertext and terms included, for a\n\ + // program that means to print or log what String hides.\n\ + func (v {go}) Reveal() string {{\n\treturn reveal(v)\n}}\n", q = quote(go), ); } @@ -115,7 +130,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 +386,33 @@ 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, and one Reveal, the + /// named call that gives the JSON. 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}}"), + format!("func (v {go}) Reveal() string {{\n\treturn reveal(v)\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()); From 41b14e7e225113d8bb23ae46683a92d4e94228cd Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 30 Oct 2026 13:57:33 +1100 Subject: [PATCH 2/3] feat(go): bind a jsonpath predicate on a JsonSearch field as one parameter An application that compiles a filter language into SQL targets jsonb_path_match(col, $1, $2): the predicate and its variables, two parameters. On an encrypted column the same filter was one Go call per leaf and a hand-assembled WHERE, one cast per parameter. The engine's match operator (#1441) compiles the predicate into one operand; this is the Go call on top (#1443). Fields..Match(ctx, cipher, predicate, vars, opts) is generated on every JsonSearch field and returns eql.JSONSearchPredicateQuery, which binds as col @@ $1::eql_v4.query_json_predicate or eql_v4.json_tree_match(col, $1::...). The predicate crosses as text and vars as JSON text written by gensupport.JSONText, so Go adds no grammar: nil, or a value that writes null, sends no vars, and a Go string is refused as a Contains needle is. opts.WildcardBound bounds the filters that compile to the element match, as Elements..Match takes it. The guest now names eql_v4.query_json_predicate, so the engine lists match (input predicate) on every JsonSearch field; the checker and stashgen read it, and stashgen refuses a document field the engine answers no match on. The guest maps Error::Predicate to STATUS_ENCODING: a refused predicate is the caller's input, not an internal failure. Diagnostic gains Construct and At, which read the construct and the byte offset the refusal carries. testusers.Entity grows the RC2 model's members (score, weight, address, country, preferred languages; $.weight ordered). The PostgreSQL test runs the seven filters of the customer's request and checks @@, json_tree_match, the per-operator query and jsonb_path_match over the plaintexts return the same rows; it joins go:test:postgres. Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5 --- languages/golang/cmd/stashgen/main_test.go | 25 ++ languages/golang/encrypt/checker.go | 10 +- languages/golang/encrypt/errors.go | 12 +- .../encrypt/example/json/customer_stash.go | 45 ++ languages/golang/encrypt/gensupport/json.go | 96 +++- languages/golang/encrypt/guest/src/ops.rs | 57 +++ languages/golang/encrypt/guest/src/status.rs | 20 +- languages/golang/encrypt/guest/src/targets.rs | 44 +- .../golang/encrypt/guest/tests/native_ops.rs | 4 +- .../internal/testusers/customer_stash.go | 90 ++++ .../encrypt/internal/testusers/entities.go | 30 +- .../internal/testusers/entity_stash.go | 49 +- .../internal/testusers/subscriber_stash.go | 135 ++++++ .../encrypt/json_match_postgres_test.go | 253 +++++++++++ languages/golang/encrypt/json_match_test.go | 422 ++++++++++++++++++ languages/golang/encrypt/json_tree_test.go | 2 +- languages/golang/encrypt/query_test.go | 5 +- languages/golang/encrypt/targets_test.go | 2 +- languages/golang/internal/guest/diagnostic.go | 89 +++- .../golang/internal/guest/diagnostic_test.go | 62 +++ .../golang/internal/guest/export_test.go | 6 + languages/golang/internal/record/record.go | 8 +- languages/golang/stashgen/emit.go | 33 +- .../testdata/stubsdk/encrypt/eql/eql.go | 4 + .../stubsdk/encrypt/gensupport/gensupport.go | 5 + mise.toml | 16 +- 26 files changed, 1470 insertions(+), 54 deletions(-) create mode 100644 languages/golang/encrypt/json_match_postgres_test.go create mode 100644 languages/golang/encrypt/json_match_test.go diff --git a/languages/golang/cmd/stashgen/main_test.go b/languages/golang/cmd/stashgen/main_test.go index 39c65ef0c..86d1e714a 100644 --- a/languages/golang/cmd/stashgen/main_test.go +++ b/languages/golang/cmd/stashgen/main_test.go @@ -468,6 +468,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)\"`") @@ -598,6 +610,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/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/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/customer_stash.go b/languages/golang/encrypt/example/json/customer_stash.go index 3f68604e4..9dfdf67dc 100644 --- a/languages/golang/encrypt/example/json/customer_stash.go +++ b/languages/golang/encrypt/example/json/customer_stash.go @@ -207,6 +207,51 @@ 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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/gensupport/json.go b/languages/golang/encrypt/gensupport/json.go index e5c0f5190..844b75390 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 { @@ -181,7 +191,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 { @@ -312,6 +322,90 @@ func JSONElementMatch[Q ~[]byte, T, E any](ctx context.Context, f Field[T], c *e 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. They answer +// as the same Go value bound as jsonb_path_match's vars through pgx or +// database/sql answers over plaintext: +// +// - A nil value (nil, or a nil map, slice, pointer or json.RawMessage) +// is SQL NULL there, and jsonb_path_match is NULL for every row. Match +// returns an empty operand, which binds as NULL, so `@@` and +// json_tree_match are NULL for every row too. The predicate is still +// compiled, so text the grammar does not read is still refused; a +// variable it names is not, as plaintext does not look for one. +// - A value that writes the JSON null (a json.RawMessage of null, or a +// pointer to a nil map) is refused as not an object, as plaintext +// refuses "vars" that are not an object. +// - 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 opts.WildcardBound != 0 { + input = append(input, vcvalue.Field{Key: "wildcard_bound", Value: opts.WildcardBound}) + } + if isNilValue(vars) { + // SQL NULL vars: compile with none for what plaintext refuses + // before it reads vars, and answer with the empty operand. + if _, err := f.jsonTerm(ctx, c, "match", "eql", 1, input); err != nil && !isUnknownVariable(err) { + return nil, err + } + return nil, nil + } + 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 + } + input = append(input, vcvalue.Field{Key: "vars", Value: text}) + term, err := f.jsonTerm(ctx, c, "match", "eql", 1, input) + if err != nil { + return nil, err + } + return Q(term.Terms[0]), nil +} + +// isNilValue reports whether v is nil, or a nil map, slice, pointer or +// interface value: what database/sql and pgx bind as SQL NULL. +func isNilValue(v any) bool { + if v == nil { + return true + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Map, reflect.Slice, reflect.Pointer, reflect.Interface: + return rv.IsNil() + } + return false +} + +// isUnknownVariable reports whether err is the engine's refusal of a +// $name that vars do not hold. +func isUnknownVariable(err error) bool { + var d *encrypt.Diagnostic + return errors.As(err, &d) && d.Reason() == "json_predicate_unknown_variable" +} + // 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 853b5aec5..6c16c05d5 100644 --- a/languages/golang/encrypt/internal/testusers/customer_stash.go +++ b/languages/golang/encrypt/internal/testusers/customer_stash.go @@ -213,6 +213,51 @@ 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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. @@ -411,6 +456,51 @@ func (f CustomerPrefsField) ChildCount(ctx context.Context, c *encrypt.Cipher, p 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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) +} + // CustomerPaths names the ordered members of the arrays of Customer's // documents, nested as the JSON nests them, for the element match: // encrypt.AtPath(CustomerPaths.Profile.Visits.At) makes the predicates 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 effa24d6c..bb6cdbbe2 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,51 @@ 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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. @@ -260,6 +306,7 @@ func (a EntityCustomFieldValuesFieldArray[E]) Member(member string) encrypt.Memb // 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 35c02e194..1743e8183 100644 --- a/languages/golang/encrypt/internal/testusers/subscriber_stash.go +++ b/languages/golang/encrypt/internal/testusers/subscriber_stash.go @@ -226,6 +226,51 @@ 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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. @@ -428,6 +473,51 @@ 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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 { @@ -504,6 +594,51 @@ func (f SubscriberLabelsField) 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, and an empty +// map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind +// it over plaintext: the operand is empty, binds as NULL, and matches no row, +// as jsonb_path_match does with NULL vars. +// +// 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. +// +// A query returns jsonb_path_match's rows over the plaintext, and each row +// answers as it does, true, false or NULL, save five 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. An ordering of +// a JSON null is NULL where jsonpath says false, except under an odd number of +// !, which returns the same rows; there the operand also asks that the node is +// not null, which tells the database which rows hold null at the path. +// +// 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) +} + // SubscriberPaths names the ordered members of the arrays of Subscriber's // documents, nested as the JSON nests them, for the element match: // encrypt.AtPath(SubscriberPaths.Profile.Visits.At) makes the predicates 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..3df24e73b --- /dev/null +++ b/languages/golang/encrypt/json_match_postgres_test.go @@ -0,0 +1,253 @@ +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.JSONReadOptions{}, + encrypt.AtPath(testusers.EntityPaths.CustomFieldValues.Extra.EmergencyContacts.Relationship).Eq("Sibling"), + encrypt.AtPath(testusers.EntityPaths.CustomFieldValues.Extra.EmergencyContacts.PreviousCallCount).Gt(5), + ))}, + 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) + } + + // nil vars bind as SQL NULL, as they do for plaintext jsonb_path_match: + // no row matches, under @@ and json_tree_match alike. + none, err := f.Match(ctx, cipher, `$.extra.country == $val`, nil, encrypt.JSONReadOptions{}) + if err != nil { + t.Fatal(err) + } + for _, q := range []string{ + `SELECT id FROM entity WHERE custom_field_values @@ $1::eql_v4.query_json_predicate`, + `SELECT id FROM entity WHERE eql_v4.json_tree_match(custom_field_values, $1::eql_v4.query_json_predicate)`, + } { + got, err := ids(q, none) + if err != nil || len(got) != 0 { + t.Errorf("nil vars: %s = %v (%v), want no rows", q, got, 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..6edc85ba3 --- /dev/null +++ b/languages/golang/encrypt/json_match_test.go @@ -0,0 +1,422 @@ +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 whose ordering term + // compares. With no ! above it, it sends no null needle. + if want := "and(contains, exists, 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.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`, map[string]any{}, "and(exists, not(contains))"}, + {`exists($.address.postcode)`, map[string]any{}, "exists"}, + {`!(exists($.extra.preferred_languages[*] ? (@ == $v)))`, map[string]string{"v": "csharp"}, "not(contains)"}, + {`$.score == 3.14 || !($.weight >= 2)`, map[string]any{}, "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`, map[string]any{}, "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, opts, + encrypt.AtPath(testusers.EntityPaths.CustomFieldValues.Extra.EmergencyContacts.Relationship).Eq("Sibling"), + encrypt.AtPath(testusers.EntityPaths.CustomFieldValues.Extra.EmergencyContacts.PreviousCallCount).Gt(5), + )) + 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. Each derives the operand its JSON text does. A +// nil value is what pgx and database/sql bind as SQL NULL, and plaintext +// jsonb_path_match is NULL for every row with NULL vars: Match returns the +// empty operand, which binds as NULL, whatever variables the predicate +// names. +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 an empty + // object. + literal := `$.extra.country == "nz" && $.weight < 9.5` + if got := derive(literal, map[string]any{}); !sameOperand(t, json.RawMessage(got), json.RawMessage(want)) { + t.Error("literals derive another operand") + } + // A nil value of any kind is SQL NULL: the empty operand, which binds as + // NULL, for a predicate with variables or without. + 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(nil), []byte(nil), []any(nil)} { + for _, p := range []string{predicate, literal} { + got := derive(p, vars) + if got != nil { + t.Errorf("%#v: %s derives an operand, want the empty one", vars, p) + } + if v, err := got.Value(); err != nil || v != nil { + t.Errorf("%#v: the empty operand binds as %v (%v), want NULL", vars, v, err) + } + } + } +} + +// 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 whitespace", json.RawMessage(" \t\n "), "json_syntax"}, + // Not nil, but the JSON null: not an object, as plaintext refuses it. + {"vars that are the JSON null", json.RawMessage(`null`), "json_input"}, + {"vars that are the JSON null with space", json.RawMessage(" null\n"), "json_input"}, + {"vars that point to a nil map", func() any { var m map[string]any; return &m }(), "json_input"}, + } { + _, 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) + } + } + + // With nil vars the predicate is still compiled: what the grammar does + // not read, or the index cannot answer, is refused as plaintext refuses + // a jsonpath it cannot parse before it reads vars. + for _, tc := range []struct{ predicate, reason string }{ + {`$.extra.country = $v`, "json_predicate_syntax"}, + {`$.extra.country like_regex "nz"`, "json_predicate_unsupported"}, + } { + _, err := f.Match(ctx, cipher, tc.predicate, nil, encrypt.JSONReadOptions{}) + var d *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &d) || d.Reason() != tc.reason { + t.Errorf("nil vars, %s: %v, want reason %s", tc.predicate, err, tc.reason) + } + } + + // 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"`, map[string]any{}, 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 ed586b904..65a6e4944 100644 --- a/languages/golang/stashgen/emit.go +++ b/languages/golang/stashgen/emit.go @@ -1012,7 +1012,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 @@ -1030,6 +1030,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{} @@ -1320,6 +1327,7 @@ func (w *writer) jsonEntry(g genField, pathsVar string) { 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 @@ -1425,6 +1433,8 @@ func (w *writer) jsonEntry(g genField, pathsVar string) { 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, pathsVar) } @@ -1587,3 +1597,24 @@ func (w *writer) paths(f *genFile) { w.p("\treturn %s", local) 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, and an empty map when there are none. A nil vars is SQL NULL, as pgx and database/sql bind it over plaintext: the operand is empty, binds as NULL, and matches no row, as jsonb_path_match does with NULL vars.", + "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.", + "A query returns jsonb_path_match's rows over the plaintext, and each row answers as it does, true, false or NULL, save five 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. An ordering of a JSON null is NULL where jsonpath says false, except under an odd number of !, which returns the same rows; there the operand also asks that the node is not null, which tells the database which rows hold null at the path.", + "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/eql/eql.go b/languages/golang/stashgen/testdata/stubsdk/encrypt/eql/eql.go index eab634afd..385ac8b02 100644 --- a/languages/golang/stashgen/testdata/stubsdk/encrypt/eql/eql.go +++ b/languages/golang/stashgen/testdata/stubsdk/encrypt/eql/eql.go @@ -32,5 +32,9 @@ type JSONSearchLeafQuery []byte // JSONSearchElementsQuery is the element match's operand on a JSONSearch. type JSONSearchElementsQuery []byte +// JSONSearchPredicateQuery is a compiled jsonpath predicate's operand on a +// JSONSearch. +type JSONSearchPredicateQuery []byte + // JSONEntry is one encrypted entry of a JSONSearch document. type JSONEntry []byte diff --git a/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go b/languages/golang/stashgen/testdata/stubsdk/encrypt/gensupport/gensupport.go index 2c939e257..bae751a8b 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, E any](context.Context, Field[T], *encrypt.C 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 From 8579e95ac01ed1a2933ca37dbfa5006cafe707c2 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 30 Oct 2026 13:57:39 +1100 Subject: [PATCH 3/3] docs(go): show Match in the references and the JSON example The stashgen README's "Encrypted JSON documents" table gains a Match row with both SQL forms, and a section on the grammar, the refusals and what Diagnostic's Reason, Construct and At give, the four places the answer parts from jsonb_path_match, the 22023 bound, and the index the per-operator form engages that json_tree_match does not. encrypt/README.md and doc.go say the same in short. The typed-document example finds Bea by one predicate that combines an ordering with the visit filter. The Go module's CHANGELOG records Match, the Diagnostic accessors and the redacted EQL types (#1443). --- languages/golang/CHANGELOG.md | 50 +++++++++++++++ languages/golang/cmd/stashgen/README.md | 61 +++++++++++++++++++ languages/golang/encrypt/README.md | 32 ++++++++++ languages/golang/encrypt/doc.go | 16 +++++ .../golang/encrypt/example/json/README.md | 23 +++++-- languages/golang/encrypt/example/json/main.go | 19 ++++++ 6 files changed, 197 insertions(+), 4 deletions(-) diff --git a/languages/golang/CHANGELOG.md b/languages/golang/CHANGELOG.md index b06df306f..018d1b9d5 100644 --- a/languages/golang/CHANGELOG.md +++ b/languages/golang/CHANGELOG.md @@ -12,6 +12,43 @@ 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, or a + `json.RawMessage` of one; an empty map when there are none). A nil `vars` + (nil, or a nil map, slice or pointer) answers as pgx and `database/sql` + bind it over plaintext, as SQL `NULL`: the operand is empty, binds as + `NULL`, and matches no row. `vars` that write the JSON `null` are refused + as not an object, as plaintext refuses them. 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 five documented cases (lax array unwrapping and wrapping, + a string compared with a number, folded ordering terms, and an ordering + of a `null` that is NULL rather than false outside an odd number of `!`, + which returns the same rows). 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 @@ -158,6 +195,19 @@ 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 printed or logged the JSON with `%s` calls the new + `Reveal()` at that call (`slog.Debug("stored", "email", v.Reveal())`), + which returns the JSON as a string; code that reads the JSON converts the + value, `string(v)` or `json.RawMessage(v)`, or calls `v.MarshalJSON()`. + `Value` is unchanged. 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 02e227448..3e42ca569 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, opts, predicates...)` | `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. @@ -238,6 +239,66 @@ Each comparison is of the member's ordering term, so `Eq` is an equality of fold `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), and an empty map when the predicate has none; a Go string is refused, as it is for a `Contains` needle. +They answer as the same Go value bound as `jsonb_path_match`'s `vars` through pgx or `database/sql` answers over plaintext. A nil value (`nil`, or a nil map, slice or pointer) is SQL `NULL` there, and `jsonb_path_match` is `NULL` for every row: `Match` returns an empty operand, which binds as `NULL`, so the query matches no row. The predicate is still compiled, so text the grammar does not read is still refused. `vars` that write the JSON `null`, such as `json.RawMessage("null")` or a pointer to a nil map, are refused as `json_input`, as plaintext refuses vars that are not an object. +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`, `null`). + +An ordering at a path the tag does not order is `encrypt.ErrTerm` with the code `stack_encrypt::json_path_not_ordered`. + +A query returns the rows `jsonb_path_match` returns over the plaintext, and each row answers as it does, true, false or NULL, save five 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. +- An ordering of a JSON `null` is NULL where jsonpath says false, unless it sits under an odd number of `!`. Both pick the same rows, so a `WHERE` is unaffected; a `SELECT` of the boolean itself can show NULL. Under an odd number of `!`, where the rows would differ, the operand also asks that the node is not `null` (a `null` needle, which tells the database which rows hold `null` at that path); elsewhere it sends none. + +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/encrypt/README.md b/languages/golang/encrypt/README.md index c6fa259a3..bea059c96 100644 --- a/languages/golang/encrypt/README.md +++ b/languages/golang/encrypt/README.md @@ -687,6 +687,38 @@ 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 five cases the stashgen +README lists: lax unwrapping and wrapping of arrays are not applied, a +string compared with a number is false under `==`, orderings and +multi-condition filters compare folded terms, and an ordering of a `null` +is NULL rather than false outside an odd number of `!`, which returns the +same rows. 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/doc.go b/languages/golang/encrypt/doc.go index 658720b79..c127dc85f 100644 --- a/languages/golang/encrypt/doc.go +++ b/languages/golang/encrypt/doc.go @@ -265,6 +265,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/example/json/README.md b/languages/golang/encrypt/example/json/README.md index f4651d12e..abf760d0e 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 @@ -53,8 +53,23 @@ q, err := Fields.Profile.Elements.Visits.Match(ctx, cipher, encrypt.JSONReadOpti `Paths.Profile.Visits.At` has the Go type of `Visit.At`, `int64`, so `Gt("15")` does not compile, and neither does a predicate on another array's member. 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 +visit that is both, 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/main.go b/languages/golang/encrypt/example/json/main.go index e50fd9cc2..88aa33fad 100644 --- a/languages/golang/encrypt/example/json/main.go +++ b/languages/golang/encrypt/example/json/main.go @@ -123,6 +123,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{})