From d5184c2a1d6c7ecd94779348e57e3fc821b493c9 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 19 Oct 2026 00:47:46 +1100 Subject: [PATCH 1/2] feat(eql-bindings): produce Smallint and SmallintEq from i16 The engine and the Go SDK both carry the int16 kind now, so the smallint family's storage and equality types need only eql-bindings to name a plaintext: i16 joins the sealed Plaintext set and plaintext_of, and the two domains join ENCRYPTION_DOMAINS. SmallintEq's term is the integer equality term, one per value whatever the width, so a smallint shares it with an integer of the same value under the same column. The date, timestamp and numeric families no longer say the engine has no kind for them, which stopped being true when the kinds landed. What they wait for is a choice of Rust plaintext type, and the reason says so. Tests: round trips through both entry points, the shared equality term, a value of another width refused by name and an i32 that does not fit refused on open, and in PostgreSQL `=` on SmallintEq (including both extremes) with the storage-only column blocked. v4_queries.json gains a SmallintEq query for the Go SDK. --- .changeset/eql-v4-smallint.md | 5 + languages/golang/CHANGELOG.md | 4 + languages/golang/cmd/stashgen/README.md | 3 +- languages/golang/encrypt/README.md | 5 +- languages/golang/encrypt/doc.go | 11 +- languages/golang/encrypt/eql/eql_gen.go | 80 ++++----- languages/golang/encrypt/eql/eql_test.go | 4 +- languages/golang/encrypt/guest/src/targets.rs | 2 + .../golang/encrypt/guest/tests/native_ops.rs | 4 +- languages/golang/encrypt/query_test.go | 2 +- packages/eql/crates/eql-bindings/CHANGELOG.md | 6 + packages/eql/crates/eql-bindings/README.md | 6 +- .../eql-bindings/src/encryption/targets.rs | 2 + .../crates/eql-bindings/src/v4/smallint.rs | 24 +++ .../eql/crates/eql-bindings/src/v4/targets.rs | 79 +++++---- .../eql/crates/eql-codegen/src/bindings.rs | 16 +- .../eql/crates/eql-codegen/src/targets.rs | 20 ++- .../tests/encryption/fixtures/v4_queries.json | 4 + .../eql/tests/encryption/tests/targets.rs | 13 +- .../eql/tests/encryption/tests/v4_types.rs | 162 +++++++++++++++++- 20 files changed, 346 insertions(+), 106 deletions(-) create mode 100644 .changeset/eql-v4-smallint.md diff --git a/.changeset/eql-v4-smallint.md b/.changeset/eql-v4-smallint.md new file mode 100644 index 000000000..5438f7c9e --- /dev/null +++ b/.changeset/eql-v4-smallint.md @@ -0,0 +1,5 @@ +--- +'@cipherstash/eql': minor +--- + +**Stack Encrypt produces the EQL v4 types `Smallint` and `SmallintEq`** (`eql-bindings`, `stack-encrypt` feature), from `i16`. `SmallintEq`'s equality term is the integer one, so a smallint and an integer of the same value under one column share it. The `date`, `timestamp` and `numeric` types now say they wait for a choice of Rust plaintext type, not for an engine kind. diff --git a/languages/golang/CHANGELOG.md b/languages/golang/CHANGELOG.md index 9d656a1aa..0a9e6ce93 100644 --- a/languages/golang/CHANGELOG.md +++ b/languages/golang/CHANGELOG.md @@ -70,6 +70,10 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`. `float32` or `float64` field tagged `encrypt_into=` one of them gets `Eq`. `=` compares as PostgreSQL compares the plain types: an integer has one term whatever its width, and for floats `-0.0 = 0.0` and `NaN = NaN`. +- **`Smallint` and `SmallintEq`**: an `int16` field (or any Go integer + type, range checked as for `Integer`) tagged `encrypt_into=` one of them + seals at 16 bits; `SmallintEq` gets `Eq`, with the same equality term as + an integer of the same value under the same column. - **stashgen notices an index no generated method queries**: an `ore` or `ope` index on text, bytes or a float (the engine answers the orderings over integers only), or the second of two order indexes on an integer. diff --git a/languages/golang/cmd/stashgen/README.md b/languages/golang/cmd/stashgen/README.md index 344838649..cdbfb8155 100644 --- a/languages/golang/cmd/stashgen/README.md +++ b/languages/golang/cmd/stashgen/README.md @@ -159,13 +159,14 @@ The engine produces these EQL types today, each from one Go field type: | `encrypt_into` | Go field | |---|---| | `TextEq`, `Text`, `TextMatch` | `string` | +| `Smallint`, `SmallintEq` | `int16`, or any Go integer type | | `Integer`, `IntegerEq` | `int32`, or any Go integer type | | `Bigint`, `BigintEq` | `int64`, or any Go integer type | | `Boolean` | `bool` | | `Real`, `RealEq` | `float32` | | `Double`, `DoubleEq` | `float64` | -The `Eq` types answer `Eq`; `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double` are stored and read only, with no query method. +The `Eq` types answer `Eq`; `Text`, `Smallint`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double` are stored and read only, with no query method. Any built-in Go integer field takes an integer type (ADR-0002) and seals at the type's width: an `int16` field always fits an `Integer`, and an `int64` field is range checked value by value, `Encrypt` refusing a value that does not fit with `encrypt.ErrEncoding`. `encrypt.Int128` and `encrypt.Uint128` are not converted. A float's width must match: a `float64` field is not a `Real`. A `TextMatch` field refuses text shorter than three characters, such as an empty string or a two-letter code, because it has no 3-grams to index; its query refuses such text too. diff --git a/languages/golang/encrypt/README.md b/languages/golang/encrypt/README.md index fe6021930..026b270e6 100644 --- a/languages/golang/encrypt/README.md +++ b/languages/golang/encrypt/README.md @@ -434,6 +434,8 @@ The EQL types the engine produces, and the Go field types each takes: | `TextEq` | `string` | `=`, `<>` | | `TextMatch` | `string` | `@@` (substring match, 3-grams, case-insensitive) | | `Text` | `string` | none: stored and read only | +| `Smallint` | `int16`, or any Go integer type (see below) | none | +| `SmallintEq` | the same | `=`, `<>` | | `Integer` | `int32`, or any Go integer type (see below) | none | | `IntegerEq` | the same | `=`, `<>` | | `Bigint` | `int64`, or any Go integer type | none | @@ -446,7 +448,8 @@ The EQL types the engine produces, and the Go field types each takes: Any built-in Go integer field takes an integer EQL type (ADR-0002): the field is sealed at the type's width, and opens back into the field's type. A type -every value of which fits (an `int16` or a `uint16` field for `Integer`) +every value of which fits (an `int16` or a `uint16` field for `Integer`, an +`int8` for `Smallint`) always seals; a wider one (an `int64` or a `uint32` field for `Integer`) is range checked value by value, and `Encrypt` refuses a value that does not fit with `ErrEncoding`, naming the field's kind and not the value. diff --git a/languages/golang/encrypt/doc.go b/languages/golang/encrypt/doc.go index 6fc832d00..b3a37b470 100644 --- a/languages/golang/encrypt/doc.go +++ b/languages/golang/encrypt/doc.go @@ -21,14 +21,15 @@ // } // // encrypt_into names the EQL type the field is stored as. The Go field type -// must match it: TextEq, TextMatch and Text take a string, Integer and -// IntegerEq an int32, Bigint and BigintEq an int64, Boolean a bool, Real and -// RealEq a float32, and Double and DoubleEq a float64. An integer EQL type +// must match it: TextEq, TextMatch and Text take a string, Smallint and +// SmallintEq an int16, Integer and IntegerEq an int32, Bigint and BigintEq +// an int64, Boolean a bool, Real and RealEq a float32, and Double and +// DoubleEq a float64. An integer EQL type // also takes any other built-in Go integer type, sealed at the type's width: // one every value of which fits it (an int16 into Integer) always seals, and // a wider one (an int64 into Integer) is range checked, a value that does not -// fit refused with [ErrEncoding]. The *Eq types answer Eq; Integer, Bigint, -// Boolean, Real, Double and Text are stored and read only. +// fit refused with [ErrEncoding]. The *Eq types answer Eq; Smallint, +// Integer, Bigint, Boolean, Real, Double and Text are stored and read only. // // 3. Run the generator. It writes user_stash.go beside the struct: // diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index d0ea96ef9..6942ed5f2 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -14,31 +14,31 @@ var Types = []Type{ {Name: "IntegerOrdOre", GoName: "IntegerOrdOre", Family: "integer", Suffix: "OrdOre", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ore", Indexes: []string{"ore"}, Query: "IntegerOrdOreQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, {Name: "IntegerOrd", GoName: "IntegerOrd", Family: "integer", Suffix: "Ord", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord", Indexes: []string{"ope"}, Query: "IntegerOrdQuery", QuerySQLDomain: "eql_v4.query_integer_ord", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, {Name: "IntegerOrdOpe", GoName: "IntegerOrdOpe", Family: "integer", Suffix: "OrdOpe", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_ord_ope", Indexes: []string{"ope"}, Query: "IntegerOrdOpeQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ope", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, - {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "SmallintEq", GoName: "SmallintEq", Family: "smallint", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_smallint_eq", Indexes: []string{"eq"}, Query: "SmallintEqQuery", QuerySQLDomain: "eql_v4.query_smallint_eq", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "SmallintOrdOre", GoName: "SmallintOrdOre", Family: "smallint", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ore", Indexes: []string{"ore"}, Query: "SmallintOrdOreQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ore", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "SmallintOrd", GoName: "SmallintOrd", Family: "smallint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord", Indexes: []string{"ope"}, Query: "SmallintOrdQuery", QuerySQLDomain: "eql_v4.query_smallint_ord", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "SmallintOrdOpe", GoName: "SmallintOrdOpe", Family: "smallint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_smallint_ord_ope", Indexes: []string{"ope"}, Query: "SmallintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ope", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, + {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "int16", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, + {Name: "SmallintEq", GoName: "SmallintEq", Family: "smallint", Suffix: "Eq", Plaintext: "int16", SQLDomain: "public.eql_v4_smallint_eq", Indexes: []string{"eq"}, Query: "SmallintEqQuery", QuerySQLDomain: "eql_v4.query_smallint_eq", Producible: true, Reason: ""}, + {Name: "SmallintOrdOre", GoName: "SmallintOrdOre", Family: "smallint", Suffix: "OrdOre", Plaintext: "int16", SQLDomain: "public.eql_v4_smallint_ord_ore", Indexes: []string{"ore"}, Query: "SmallintOrdOreQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, + {Name: "SmallintOrd", GoName: "SmallintOrd", Family: "smallint", Suffix: "Ord", Plaintext: "int16", SQLDomain: "public.eql_v4_smallint_ord", Indexes: []string{"ope"}, Query: "SmallintOrdQuery", QuerySQLDomain: "eql_v4.query_smallint_ord", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, + {Name: "SmallintOrdOpe", GoName: "SmallintOrdOpe", Family: "smallint", Suffix: "OrdOpe", Plaintext: "int16", SQLDomain: "public.eql_v4_smallint_ord_ope", Indexes: []string{"ope"}, Query: "SmallintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_smallint_ord_ope", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, {Name: "Bigint", GoName: "Bigint", Family: "bigint", Suffix: "", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: true, Reason: ""}, {Name: "BigintOrdOre", GoName: "BigintOrdOre", Family: "bigint", Suffix: "OrdOre", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ore", Indexes: []string{"ore"}, Query: "BigintOrdOreQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, {Name: "BigintOrd", GoName: "BigintOrd", Family: "bigint", Suffix: "Ord", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord", Indexes: []string{"ope"}, Query: "BigintOrdQuery", QuerySQLDomain: "eql_v4.query_bigint_ord", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, {Name: "BigintOrdOpe", GoName: "BigintOrdOpe", Family: "bigint", Suffix: "OrdOpe", Plaintext: "int64", SQLDomain: "public.eql_v4_bigint_ord_ope", Indexes: []string{"ope"}, Query: "BigintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ope", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, - {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "DateEq", GoName: "DateEq", Family: "date", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_date_eq", Indexes: []string{"eq"}, Query: "DateEqQuery", QuerySQLDomain: "eql_v4.query_date_eq", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "DateOrdOre", GoName: "DateOrdOre", Family: "date", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ore", Indexes: []string{"ore"}, Query: "DateOrdOreQuery", QuerySQLDomain: "eql_v4.query_date_ord_ore", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "DateOrd", GoName: "DateOrd", Family: "date", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_date_ord", Indexes: []string{"ope"}, Query: "DateOrdQuery", QuerySQLDomain: "eql_v4.query_date_ord", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "DateOrdOpe", GoName: "DateOrdOpe", Family: "date", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ope", Indexes: []string{"ope"}, Query: "DateOrdOpeQuery", QuerySQLDomain: "eql_v4.query_date_ord_ope", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "TimestampEq", GoName: "TimestampEq", Family: "timestamp", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_eq", Indexes: []string{"eq"}, Query: "TimestampEqQuery", QuerySQLDomain: "eql_v4.query_timestamp_eq", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "TimestampOrdOre", GoName: "TimestampOrdOre", Family: "timestamp", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ore", Indexes: []string{"ore"}, Query: "TimestampOrdOreQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ore", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "TimestampOrd", GoName: "TimestampOrd", Family: "timestamp", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord", Indexes: []string{"ope"}, Query: "TimestampOrdQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "TimestampOrdOpe", GoName: "TimestampOrdOpe", Family: "timestamp", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ope", Indexes: []string{"ope"}, Query: "TimestampOrdOpeQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ope", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "NumericEq", GoName: "NumericEq", Family: "numeric", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_numeric_eq", Indexes: []string{"eq"}, Query: "NumericEqQuery", QuerySQLDomain: "eql_v4.query_numeric_eq", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "NumericOrdOre", GoName: "NumericOrdOre", Family: "numeric", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ore", Indexes: []string{"ore"}, Query: "NumericOrdOreQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ore", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "NumericOrd", GoName: "NumericOrd", Family: "numeric", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord", Indexes: []string{"ope"}, Query: "NumericOrdQuery", QuerySQLDomain: "eql_v4.query_numeric_ord", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, - {Name: "NumericOrdOpe", GoName: "NumericOrdOpe", Family: "numeric", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ope", Indexes: []string{"ope"}, Query: "NumericOrdOpeQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ope", Producible: false, Reason: "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)"}, + {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "DateEq", GoName: "DateEq", Family: "date", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_date_eq", Indexes: []string{"eq"}, Query: "DateEqQuery", QuerySQLDomain: "eql_v4.query_date_eq", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "DateOrdOre", GoName: "DateOrdOre", Family: "date", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ore", Indexes: []string{"ore"}, Query: "DateOrdOreQuery", QuerySQLDomain: "eql_v4.query_date_ord_ore", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "DateOrd", GoName: "DateOrd", Family: "date", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_date_ord", Indexes: []string{"ope"}, Query: "DateOrdQuery", QuerySQLDomain: "eql_v4.query_date_ord", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "DateOrdOpe", GoName: "DateOrdOpe", Family: "date", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_date_ord_ope", Indexes: []string{"ope"}, Query: "DateOrdOpeQuery", QuerySQLDomain: "eql_v4.query_date_ord_ope", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "TimestampEq", GoName: "TimestampEq", Family: "timestamp", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_eq", Indexes: []string{"eq"}, Query: "TimestampEqQuery", QuerySQLDomain: "eql_v4.query_timestamp_eq", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "TimestampOrdOre", GoName: "TimestampOrdOre", Family: "timestamp", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ore", Indexes: []string{"ore"}, Query: "TimestampOrdOreQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ore", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "TimestampOrd", GoName: "TimestampOrd", Family: "timestamp", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord", Indexes: []string{"ope"}, Query: "TimestampOrdQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "TimestampOrdOpe", GoName: "TimestampOrdOpe", Family: "timestamp", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_timestamp_ord_ope", Indexes: []string{"ope"}, Query: "TimestampOrdOpeQuery", QuerySQLDomain: "eql_v4.query_timestamp_ord_ope", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "NumericEq", GoName: "NumericEq", Family: "numeric", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_numeric_eq", Indexes: []string{"eq"}, Query: "NumericEqQuery", QuerySQLDomain: "eql_v4.query_numeric_eq", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "NumericOrdOre", GoName: "NumericOrdOre", Family: "numeric", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ore", Indexes: []string{"ore"}, Query: "NumericOrdOreQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ore", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "NumericOrd", GoName: "NumericOrd", Family: "numeric", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord", Indexes: []string{"ope"}, Query: "NumericOrdQuery", QuerySQLDomain: "eql_v4.query_numeric_ord", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, + {Name: "NumericOrdOpe", GoName: "NumericOrdOpe", Family: "numeric", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_numeric_ord_ope", Indexes: []string{"ope"}, Query: "NumericOrdOpeQuery", QuerySQLDomain: "eql_v4.query_numeric_ord_ope", Producible: false, Reason: "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)"}, {Name: "Text", GoName: "Text", Family: "text", Suffix: "", Plaintext: "string", SQLDomain: "public.eql_v4_text", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, {Name: "TextEq", GoName: "TextEq", Family: "text", Suffix: "Eq", Plaintext: "string", SQLDomain: "public.eql_v4_text_eq", Indexes: []string{"eq"}, Query: "TextEqQuery", QuerySQLDomain: "eql_v4.query_text_eq", Producible: true, Reason: ""}, {Name: "TextMatch", GoName: "TextMatch", Family: "text", Suffix: "Match", Plaintext: "string", SQLDomain: "public.eql_v4_text_match", Indexes: []string{"match"}, Query: "TextMatchQuery", QuerySQLDomain: "eql_v4.query_text_match", Producible: true, Reason: ""}, @@ -275,7 +275,7 @@ func (v *IntegerOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Smallint is the EQL type stored in public.eql_v4_smallint: the smallint family with no index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine produces it from a int16. type Smallint []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -299,7 +299,7 @@ func (v *Smallint) UnmarshalJSON(b []byte) error { } // SmallintEq is the EQL type stored in public.eql_v4_smallint_eq: the smallint family with the eq index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine produces it from a int16. type SmallintEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -346,7 +346,7 @@ func (v *SmallintEqQuery) UnmarshalJSON(b []byte) error { } // SmallintOrdOre is the EQL type stored in public.eql_v4_smallint_ord_ore: the smallint family with the ore index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type SmallintOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -393,7 +393,7 @@ func (v *SmallintOrdOreQuery) UnmarshalJSON(b []byte) error { } // SmallintOrd is the EQL type stored in public.eql_v4_smallint_ord: the smallint family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236). type SmallintOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -440,7 +440,7 @@ func (v *SmallintOrdQuery) UnmarshalJSON(b []byte) error { } // SmallintOrdOpe is the EQL type stored in public.eql_v4_smallint_ord_ope: the smallint family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236). type SmallintOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -699,7 +699,7 @@ func (v *BigintOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Date is the EQL type stored in public.eql_v4_date: the date family with no index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type Date []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -723,7 +723,7 @@ func (v *Date) UnmarshalJSON(b []byte) error { } // DateEq is the EQL type stored in public.eql_v4_date_eq: the date family with the eq index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type DateEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -770,7 +770,7 @@ func (v *DateEqQuery) UnmarshalJSON(b []byte) error { } // DateOrdOre is the EQL type stored in public.eql_v4_date_ord_ore: the date family with the ore index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type DateOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -817,7 +817,7 @@ func (v *DateOrdOreQuery) UnmarshalJSON(b []byte) error { } // DateOrd is the EQL type stored in public.eql_v4_date_ord: the date family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type DateOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -864,7 +864,7 @@ func (v *DateOrdQuery) UnmarshalJSON(b []byte) error { } // DateOrdOpe is the EQL type stored in public.eql_v4_date_ord_ope: the date family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type DateOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -911,7 +911,7 @@ func (v *DateOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Timestamp is the EQL type stored in public.eql_v4_timestamp: the timestamp family with no index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type Timestamp []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -935,7 +935,7 @@ func (v *Timestamp) UnmarshalJSON(b []byte) error { } // TimestampEq is the EQL type stored in public.eql_v4_timestamp_eq: the timestamp family with the eq index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type TimestampEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -982,7 +982,7 @@ func (v *TimestampEqQuery) UnmarshalJSON(b []byte) error { } // TimestampOrdOre is the EQL type stored in public.eql_v4_timestamp_ord_ore: the timestamp family with the ore index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type TimestampOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1029,7 +1029,7 @@ func (v *TimestampOrdOreQuery) UnmarshalJSON(b []byte) error { } // TimestampOrd is the EQL type stored in public.eql_v4_timestamp_ord: the timestamp family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type TimestampOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1076,7 +1076,7 @@ func (v *TimestampOrdQuery) UnmarshalJSON(b []byte) error { } // TimestampOrdOpe is the EQL type stored in public.eql_v4_timestamp_ord_ope: the timestamp family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type TimestampOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1123,7 +1123,7 @@ func (v *TimestampOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Numeric is the EQL type stored in public.eql_v4_numeric: the numeric family with no index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type Numeric []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1147,7 +1147,7 @@ func (v *Numeric) UnmarshalJSON(b []byte) error { } // NumericEq is the EQL type stored in public.eql_v4_numeric_eq: the numeric family with the eq index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type NumericEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1194,7 +1194,7 @@ func (v *NumericEqQuery) UnmarshalJSON(b []byte) error { } // NumericOrdOre is the EQL type stored in public.eql_v4_numeric_ord_ore: the numeric family with the ore index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type NumericOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1241,7 +1241,7 @@ func (v *NumericOrdOreQuery) UnmarshalJSON(b []byte) error { } // NumericOrd is the EQL type stored in public.eql_v4_numeric_ord: the numeric family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type NumericOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1288,7 +1288,7 @@ func (v *NumericOrdQuery) UnmarshalJSON(b []byte) error { } // NumericOrdOpe is the EQL type stored in public.eql_v4_numeric_ord_ope: the numeric family with the ope index. -// The engine cannot produce it yet: the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144). +// The engine cannot produce it yet: eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130). type NumericOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. diff --git a/languages/golang/encrypt/eql/eql_test.go b/languages/golang/encrypt/eql/eql_test.go index 755eb99a4..c7ee88ef0 100644 --- a/languages/golang/encrypt/eql/eql_test.go +++ b/languages/golang/encrypt/eql/eql_test.go @@ -19,7 +19,7 @@ func TestTypesTableNamesTheProducibleTypes(t *testing.T) { t.Errorf("%s: an unproducible type has a reason", typ.Name) } } - want := []string{"Integer", "IntegerEq", "Bigint", "BigintEq", "Text", "TextEq", "TextMatch", "Boolean", "Real", "RealEq", "Double", "DoubleEq"} + want := []string{"Integer", "IntegerEq", "Smallint", "SmallintEq", "Bigint", "BigintEq", "Text", "TextEq", "TextMatch", "Boolean", "Real", "RealEq", "Double", "DoubleEq"} if len(producible) != len(want) { t.Fatalf("producible = %v, want %v", producible, want) } @@ -28,7 +28,7 @@ func TestTypesTableNamesTheProducibleTypes(t *testing.T) { t.Fatalf("producible = %v, want %v", producible, want) } } - for name, plaintext := range map[string]string{"Integer": "int32", "IntegerEq": "int32", "Bigint": "int64", "BigintEq": "int64", "Boolean": "bool", "Real": "float32", "RealEq": "float32", "Double": "float64", "DoubleEq": "float64"} { + for name, plaintext := range map[string]string{"Integer": "int32", "IntegerEq": "int32", "Smallint": "int16", "SmallintEq": "int16", "Bigint": "int64", "BigintEq": "int64", "Boolean": "bool", "Real": "float32", "RealEq": "float32", "Double": "float64", "DoubleEq": "float64"} { if typ, ok := eql.Lookup(name); !ok || typ.Plaintext != plaintext { t.Errorf("%s: Plaintext = %q, want %q", name, typ.Plaintext, plaintext) } diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 1f7c11ced..1f4443eff 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -191,6 +191,8 @@ mod tests { [ "Integer", "IntegerEq", + "Smallint", + "SmallintEq", "Bigint", "BigintEq", "Text", diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index da9ba5afc..0abd0e60f 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -1372,6 +1372,8 @@ fn targets_lists_what_the_build_holds() { [ "Integer", "IntegerEq", + "Smallint", + "SmallintEq", "Bigint", "BigintEq", "Text", @@ -1776,7 +1778,7 @@ fn the_catalog_lists_each_types_operators() { assert_eq!(op, want, "{name}"); }; match name.as_str() { - "TextEq" | "IntegerEq" | "BigintEq" | "RealEq" | "DoubleEq" => only([ + "TextEq" | "IntegerEq" | "SmallintEq" | "BigintEq" | "RealEq" | "DoubleEq" => only([ ("operator", "eq"), ("fidelity", "exact"), ("arity", "one"), diff --git a/languages/golang/encrypt/query_test.go b/languages/golang/encrypt/query_test.go index a5219f327..4510cfc2e 100644 --- a/languages/golang/encrypt/query_test.go +++ b/languages/golang/encrypt/query_test.go @@ -390,7 +390,7 @@ func TestTheCatalogListsEachTypesOperators(t *testing.T) { } for _, target := range targets { switch { - case slices.Contains([]string{"TextEq", "IntegerEq", "BigintEq", "RealEq", "DoubleEq"}, target.Name): + case slices.Contains([]string{"TextEq", "IntegerEq", "SmallintEq", "BigintEq", "RealEq", "DoubleEq"}, target.Name): want := []record.Operator{{Name: "eq", Fidelity: record.Exact, Index: record.Equality}} if len(target.Operators) != 1 || target.Operators[0] != want[0] { t.Fatalf("%s answers %+v, want %+v", target.Name, target.Operators, want) diff --git a/packages/eql/crates/eql-bindings/CHANGELOG.md b/packages/eql/crates/eql-bindings/CHANGELOG.md index 199d7e626..f8f0de918 100644 --- a/packages/eql/crates/eql-bindings/CHANGELOG.md +++ b/packages/eql/crates/eql-bindings/CHANGELOG.md @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 baseline, so `=` compares as PostgreSQL's own integer and `float8 =` do: one term per integer value whatever its width, `-0.0 = 0.0` and `NaN = NaN`. The ciphertext keeps the exact value. +- **`Smallint` and `SmallintEq` are producible** (`stack-encrypt` feature), + from `i16`. `SmallintEq`'s equality term is the integer one: a smallint + and an integer or bigint of the same value under one column share it. + The `date`, `timestamp` and `numeric` families say why they are still + unproducible: the engine has their kinds, and their Rust plaintext types + are not chosen yet. - **`Target::match_options`** (`targets` feature): the options a type's match index is built under, `EqlMatch`'s, for a type that carries one (`TextMatch`), and `None` otherwise. Stack Encrypt answers `matches` diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index 179b34330..1c7f16db9 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -112,6 +112,7 @@ plaintext: | v4 type | Plaintext | | --- | --- | | `Text`, `TextMatch` | `String` | +| `Smallint`, `SmallintEq` | `i16` | | `Integer`, `IntegerEq` | `i32` | | `Bigint`, `BigintEq` | `i64` | | `Boolean` | `bool` | @@ -134,8 +135,9 @@ other order finds nothing until #1171 is fixed. Text too short to make a column yet, and a `TextMatchQuery` for it is refused too: one filter type builds both, and an empty filter as a query would match every row. The ordering types wait for the engine's order keys (#1236), text's order types -(#1131) and block ORE (#1132), and `Json` for a decision on sealing a -document as one leaf. +(#1131) and block ORE (#1132), `Date`, `Timestamp` and `Numeric` for a +choice of their Rust plaintext types (#1130), and `Json` for a decision on +sealing a document as one leaf. The generated API docs include a complete, executable example in the `eql_bindings::encryption` module, from cipher setup through decryption. Its diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index 61f71170f..0e5ac90d0 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -408,6 +408,7 @@ pub(crate) fn refuse(name: &str) -> TargetError { mod sealed { pub trait Sealed {} impl Sealed for String {} + impl Sealed for i16 {} impl Sealed for i32 {} impl Sealed for i64 {} impl Sealed for bool {} @@ -486,6 +487,7 @@ macro_rules! scalar_plaintext { )+}; } scalar_plaintext!( + i16 => Int16, i32 => Int32, i64 => Int64, bool => Bool, diff --git a/packages/eql/crates/eql-bindings/src/v4/smallint.rs b/packages/eql/crates/eql-bindings/src/v4/smallint.rs index 5341a7810..424182bdf 100644 --- a/packages/eql/crates/eql-bindings/src/v4/smallint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/smallint.rs @@ -14,10 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL smallint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`i16`](i16). Implements `EncryptFrom` and `DecryptInto`. +/// Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`. +/// See the [complete encryption example](crate::encryption#example). +#[derive(stack_encrypt::EncryptFrom, stack_encrypt::DecryptInto)] +#[stash(plaintext = i16)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Smallint { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -55,10 +63,18 @@ impl DomainType for Smallint { doc, doc = " See the [EQL smallint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`i16`](i16). Implements `EncryptFrom` and `DecryptInto`. +/// Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`. +/// See the [complete encryption example](crate::encryption#example). +#[derive(stack_encrypt::EncryptFrom, stack_encrypt::DecryptInto)] +#[stash(plaintext = i16)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SmallintEq { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -223,10 +239,18 @@ impl DomainType for SmallintOrdOpe { doc, doc = " See the [EQL smallint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input: [`i16`](i16). Implements `EncryptFrom` to build equality query terms. Query operands do not decrypt. +/// Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`. +/// See the [complete encryption example](crate::encryption#example). +#[derive(stack_encrypt::EncryptFrom)] +#[stash(plaintext = i16)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SmallintEqQuery { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/targets.rs b/packages/eql/crates/eql-bindings/src/v4/targets.rs index b1895e2ae..846b7bf36 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -81,70 +81,66 @@ pub const TARGETS: &[Target] = &[ name: "Smallint", family: "smallint", suffix: "", - plaintext: None, + plaintext: Some("int16"), sql_domain: "public.eql_v4_smallint", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", - ), + producible: true, + reason: None, }, Target { name: "SmallintEq", family: "smallint", suffix: "Eq", - plaintext: None, + plaintext: Some("int16"), sql_domain: "public.eql_v4_smallint_eq", indexes: &["eq"], query: Some("SmallintEqQuery"), query_sql_domain: Some("eql_v4.query_smallint_eq"), - producible: false, - reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", - ), + producible: true, + reason: None, }, Target { name: "SmallintOrdOre", family: "smallint", suffix: "OrdOre", - plaintext: None, + plaintext: Some("int16"), sql_domain: "public.eql_v4_smallint_ord_ore", indexes: &["ore"], query: Some("SmallintOrdOreQuery"), query_sql_domain: Some("eql_v4.query_smallint_ord_ore"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { name: "SmallintOrd", family: "smallint", suffix: "Ord", - plaintext: None, + plaintext: Some("int16"), sql_domain: "public.eql_v4_smallint_ord", indexes: &["ope"], query: Some("SmallintOrdQuery"), query_sql_domain: Some("eql_v4.query_smallint_ord"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { name: "SmallintOrdOpe", family: "smallint", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("int16"), sql_domain: "public.eql_v4_smallint_ord_ope", indexes: &["ope"], query: Some("SmallintOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_smallint_ord_ope"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -224,7 +220,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -238,7 +234,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_eq"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -252,7 +248,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ore"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -266,7 +262,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -280,7 +276,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ope"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -294,7 +290,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -308,7 +304,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_eq"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -322,7 +318,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ore"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -336,7 +332,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -350,7 +346,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ope"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -364,7 +360,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -378,7 +374,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_eq"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -392,7 +388,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ore"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -406,7 +402,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -420,7 +416,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ope"), producible: false, reason: Some( - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 specifies the kind and its encodings (ADR-0002, #1144)", + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK carry the kind, and which Rust type a date, timestamp or decimal is produced from is not decided (#1130)", ), }, Target { @@ -716,6 +712,15 @@ pub(crate) fn encrypt_named<'a, K: 'static>( "IntegerEq" => { run_target::("IntegerEq", keyset, column, plaintext) } + "Smallint" => { + run_target::("Smallint", keyset, column, plaintext) + } + "SmallintEq" => run_target::( + "SmallintEq", + keyset, + column, + plaintext, + ), "Bigint" => { run_target::("Bigint", keyset, column, plaintext) } @@ -758,6 +763,12 @@ pub(crate) fn decrypt_named<'a, K: 'static>( "IntegerEq" => { open_target::("IntegerEq", opener, column, stored) } + "Smallint" => { + open_target::("Smallint", opener, column, stored) + } + "SmallintEq" => { + open_target::("SmallintEq", opener, column, stored) + } "Bigint" => open_target::("Bigint", opener, column, stored), "BigintEq" => { open_target::("BigintEq", opener, column, stored) @@ -795,6 +806,12 @@ pub(crate) fn query_named<'a, K: 'static>( column, plaintext, ), + "SmallintEq" => run_target::( + "SmallintEq", + keyset, + column, + plaintext, + ), "BigintEq" => run_target::( "BigintEq", keyset, column, plaintext, ), diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index 6219c1c64..2259d99ab 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -335,12 +335,14 @@ fn render_query_struct(eql: EqlVersion, family: &DomainFamily, domain: &Domain) /// The `(family, domain)` pairs whose generated v4 structs carry the opt-in /// `stack-encrypt` derives: the storage and equality domains of every family -/// whose plaintext the engine's value enum carries, and text match. Every +/// whose plaintext eql-bindings names (`plaintext_of`), and text match. Every /// other stored domain has a reason in [`encryption_gap`], and a test holds the two together so a new domain /// cannot arrive without either a derive or a reason. pub const ENCRYPTION_DOMAINS: &[(&str, &str)] = &[ ("integer", ""), ("integer", "eq"), + ("smallint", ""), + ("smallint", "eq"), ("bigint", ""), ("bigint", "eq"), ("text", ""), @@ -360,11 +362,12 @@ pub const V3_ENCRYPTION_GAP: &str = /// The plaintext a family's v4 domains are produced from: the vitaminc /// `ValueKind` name a plan field's `"type"` key spells, and the Rust type the -/// derive and the by-name dispatch name. `None` for a family the engine's -/// value enum has no kind for. +/// derive and the by-name dispatch name. `None` for a family eql-bindings +/// names no Rust plaintext type for yet. pub fn plaintext_of(family: &str) -> Option<(&'static str, &'static str)> { Some(match family { "text" => ("string", "String"), + "smallint" => ("int16", "i16"), "integer" => ("int32", "i32"), "bigint" => ("int64", "i64"), "boolean" => ("bool", "bool"), @@ -395,8 +398,9 @@ pub fn encryption_gap( "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" } else if plaintext_of(family.name).is_none() { - "the engine's value enum and the Go SDK have no kind for this family yet; vitaminc 0.6 \ - specifies the kind and its encodings (ADR-0002, #1144)" + "eql-bindings has no Rust plaintext type for this family yet: the engine and the Go SDK \ + carry the kind, and which Rust type a date, timestamp or decimal is produced from is \ + not decided (#1130)" } else if domain.terms.contains(&Term::Ore) { "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)" } else if domain.terms.contains(&Term::Ope) && family.name == "text" { @@ -1718,7 +1722,7 @@ mod tests { } else if family.name == "json" { "tree of leaves" } else if plaintext_of(family.name).is_none() { - "no kind for this family" + "no Rust plaintext type for this family" } else if domain.terms.contains(&Term::Ore) { "block ORE terms" } else if domain.terms.contains(&Term::Ope) { diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index 99af51c0d..d63ece55b 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -19,7 +19,7 @@ //! [`encryption_gap`](crate::bindings::encryption_gap) on every domain. //! //! The reasons a type is not producible, each naming the issue that tracks -//! it: the engine's value enum has no kind for the family (#1144); the +//! it: no Rust plaintext type is chosen for the family yet (#1130); the //! engine's order terms are not yet derived over ADR-0002's order keys //! (#1236) or, for text, no EQL type is built from them (#1131); EQL's ORE //! term is block ORE where the engine derives CLLW ORE (#1132); and the JSON @@ -413,7 +413,7 @@ mod tests { .find(|f| f.name == name) .expect("family") }; - for family_name in ["integer", "bigint", "real", "double"] { + for family_name in ["integer", "smallint", "bigint", "real", "double"] { let family = family(family_name); assert!(gap(family, "").is_none(), "{family_name}"); assert!(gap(family, "eq").is_none(), "{family_name}Eq is producible"); @@ -426,12 +426,16 @@ mod tests { "{family_name}" ); } - assert!(gap(family("date"), "") - .unwrap() - .contains("no kind for this family")); - assert!(gap(family("smallint"), "eq") - .unwrap() - .contains("no kind for this family")); + for family_name in ["date", "timestamp", "numeric"] { + for domain in ["", "eq", "ord"] { + assert!( + gap(family(family_name), domain) + .unwrap() + .contains("no Rust plaintext type for this family"), + "{family_name} {domain:?}" + ); + } + } assert!(gap(family("json"), "").unwrap().contains("tree of leaves")); } diff --git a/packages/eql/tests/encryption/fixtures/v4_queries.json b/packages/eql/tests/encryption/fixtures/v4_queries.json index 4ff1207ab..5b20273f4 100644 --- a/packages/eql/tests/encryption/fixtures/v4_queries.json +++ b/packages/eql/tests/encryption/fixtures/v4_queries.json @@ -21,6 +21,10 @@ "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"rank\"},\"hm\":\"54e9bd5b5c8620be147b65fe754240f64562d4ce62394b3e4390867f2ef483cc\"}" }, "table": "profiles", + "tally": { + "plaintext": -12, + "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"tally\"},\"hm\":\"7aeb7ba10d7f88d1c6ec1e4d819e20aa0460e2467eaf1c5829437126ecc72db3\"}" + }, "weight": { "plaintext": 1.5, "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"weight\"},\"hm\":\"96cc94054dd2785dd2592355d2478ca4470d29e09b47637a99f7a91066c93f8b\"}" diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index 473571847..1d4cc4f48 100644 --- a/packages/eql/tests/encryption/tests/targets.rs +++ b/packages/eql/tests/encryption/tests/targets.rs @@ -480,12 +480,13 @@ mod the_table { assert_eq!(row.suffix, suffix, "{name}: suffix"); let kind = match family.name { "text" => Some(ValueKind::String), + "smallint" => Some(ValueKind::Int16), "integer" => Some(ValueKind::Int32), "bigint" => Some(ValueKind::Int64), "boolean" => Some(ValueKind::Bool), "real" => Some(ValueKind::Float32), "double" => Some(ValueKind::Float64), - // The rest have no kind in the engine's value enum yet. + // The rest have no Rust plaintext type in eql-bindings yet. _ => None, }; assert_eq!( @@ -560,10 +561,10 @@ mod the_table { } /// The types the engine produces in EQL v4 today: the storage and - /// equality domains of every family whose plaintext the engine's value - /// enum carries, and text match. + /// equality domains of every family whose plaintext eql-bindings names, + /// and text match. #[test] - fn the_producible_types_are_the_unordered_ones_the_value_enum_carries() { + fn the_producible_types_are_the_unordered_ones_with_a_rust_plaintext() { let producible: Vec<&str> = targets::targets() .iter() .filter(|t| t.producible) @@ -574,6 +575,8 @@ mod the_table { [ "Integer", "IntegerEq", + "Smallint", + "SmallintEq", "Bigint", "BigintEq", "Text", @@ -591,7 +594,7 @@ mod the_table { let expected = match (row.family, row.suffix) { ("json", "Search") => "JSON index", ("json", "") => "tree of leaves", - ("smallint" | "date" | "timestamp" | "numeric", _) => "no kind for this family", + ("date" | "timestamp" | "numeric", _) => "no Rust plaintext type for this family", (_, "OrdOre" | "SearchOre") => "derives CLLW ORE (#1132)", ("text", "Ord" | "OrdOpe" | "Search") => "text order terms yet (#1131)", (_, "Ord" | "OrdOpe") => "order keys (#1236)", diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs index 176715456..7819884d3 100644 --- a/packages/eql/tests/encryption/tests/v4_types.rs +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -1,6 +1,6 @@ //! The EQL v4 types Stack Encrypt produces besides `TextEq`: the -//! storage-only integer, bigint, boolean, float and text types, the integer -//! and float equality types, and `TextMatch`. `Json` is not producible yet: a +//! storage-only integer, smallint, bigint, boolean, float and text types, the +//! integer and float equality types, and `TextMatch`. `Json` is not producible yet: a //! JSON value seals as a tree of leaves and EQL's `c` holds one. //! //! Each type must round-trip through both entry points (the typed @@ -14,7 +14,7 @@ mod common; use eql_bindings::encryption::targets::{self as targets, TargetError}; use eql_bindings::encryption::{CiphertextVisitor, NativeCiphertextVisitor as NativeLeaf}; use eql_bindings::v3::terms::Ciphertext; -use eql_bindings::v4::{bigint, boolean, double, integer, real, text}; +use eql_bindings::v4::{bigint, boolean, double, integer, real, smallint, text}; use eql_bindings::Identifier; use serde::{de::DeserializeOwned, Serialize}; use stack_encrypt::dynamic::{ValueFrame, ValueKind}; @@ -116,6 +116,7 @@ async fn by_name( /// A sample plaintext of each kind a producible type takes. fn sample(kind: ValueKind) -> ValueFrame { match kind { + ValueKind::Int16 => frame::(i16::MIN), ValueKind::Int32 => frame::(-42), ValueKind::Int64 => frame::(i64::MIN + 1), ValueKind::Bool => frame::(true), @@ -158,6 +159,31 @@ async fn integers_round_trip_and_seal_the_leaf_their_frame_seals() { assert!(same(&opened, &frame(value))); } + for value in [0i16, -1, i16::MIN, i16::MAX] { + let stored: smallint::Smallint = keyset.encrypt_as(&value, column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: i16 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened, value); + assert!(same( + &as_frame(&cipher, stored.c.clone()).await, + &frame(value) + )); + let named: smallint::Smallint = by_name(&keyset, "Smallint", frame::(value)).await; + let opened = targets::decrypt( + "Smallint", + &cipher, + &label(), + &serde_json::to_vec(&named).unwrap(), + ) + .unwrap() + .await + .unwrap(); + assert!(same(&opened, &frame(value))); + } + for value in [0i64, -1, i64::MIN, i64::MAX] { let stored: bigint::Bigint = keyset.encrypt_as(&value, column()).await.unwrap(); assert_v4_envelope(&stored, &["v", "i", "c"]); @@ -215,6 +241,15 @@ async fn number_equality_terms_follow_the_semantic_baseline() { hm("BigintEq", frame::(42)).await, "one integer domain for every width" ); + assert_eq!( + hm("SmallintEq", frame::(42)).await, + hm("IntegerEq", frame::(42)).await, + "a smallint too" + ); + assert_ne!( + hm("SmallintEq", frame::(-1)).await, + hm("SmallintEq", frame::(1)).await + ); assert_eq!( hm("RealEq", frame::(-0.0)).await, @@ -266,6 +301,13 @@ async fn number_equality_types_round_trip_through_the_typed_entry_point() { let opened: i32 = cipher.decrypt_as(stored, column().into()).await.unwrap(); assert_eq!(opened, -7); + let stored: smallint::SmallintEq = keyset.encrypt_as(&i16::MIN, column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let named: serde_json::Value = by_name(&keyset, "SmallintEq", frame::(i16::MIN)).await; + assert_eq!(serde_json::to_value(&stored).unwrap()["hm"], named["hm"]); + let opened: i16 = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened, i16::MIN); + let stored: bigint::BigintEq = keyset.encrypt_as(&i64::MIN, column()).await.unwrap(); assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); let opened: i64 = cipher.decrypt_as(stored, column().into()).await.unwrap(); @@ -524,6 +566,8 @@ async fn a_value_of_another_kind_is_refused_not_converted() { let keyset = cipher.default_keyset(); for (name, value, expected) in [ ("Integer", frame::(1), ValueKind::Int32), + ("Smallint", frame::(1), ValueKind::Int16), + ("SmallintEq", frame::(1), ValueKind::Int16), ("Bigint", frame::(1), ValueKind::Int64), ("Boolean", frame::(1), ValueKind::Bool), ("Real", frame::(1.0), ValueKind::Float32), @@ -558,6 +602,22 @@ async fn a_value_of_another_kind_is_refused_not_converted() { let opened: Result = cipher.decrypt_as(moved, column().into()).await; assert!(opened.is_err(), "i64::MAX does not fit an i32"); + let moved = smallint::Smallint { + v: narrow.v, + i: narrow.i.clone(), + c: narrow.c.clone(), + }; + let opened: i16 = cipher.decrypt_as(moved, column().into()).await.unwrap(); + assert_eq!(opened, 42, "an i32 that fits opens as an i16"); + let big: integer::Integer = keyset.encrypt_as(&70_000i32, column()).await.unwrap(); + let moved = smallint::Smallint { + v: narrow.v, + i: big.i, + c: big.c, + }; + let opened: Result = cipher.decrypt_as(moved, column().into()).await; + assert!(opened.is_err(), "70000 does not fit an i16"); + let flag: boolean::Boolean = keyset.encrypt_as(&true, column()).await.unwrap(); let moved = integer::Integer { v: narrow.v, @@ -913,6 +973,98 @@ fn postgres_finds_number_equality_rows() { ); } +/// Needs an EQL v4 installation. A `Smallint` stores and reads back, and +/// `=` on a `SmallintEq` column finds the rows its query names, including +/// the extremes of the width. +#[test] +#[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] +fn postgres_stores_and_finds_smallint_rows() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (cipher, _) = runtime.block_on(common::cipher()); + let keyset = cipher.default_keyset(); + let mut db = postgres::Client::connect( + &std::env::var("EQL_TEST_DATABASE_URL").expect("EQL_TEST_DATABASE_URL"), + postgres::NoTls, + ) + .unwrap(); + db.batch_execute( + "CREATE TEMP TABLE v4_smallint ( + id int PRIMARY KEY, + plain public.eql_v4_smallint, + searchable public.eql_v4_smallint_eq + );", + ) + .unwrap(); + let json = |value: &dyn erased::Json| value.to_json(); + for (id, value) in [(1, i16::MIN), (2, 7i16), (3, i16::MAX), (4, 7)] { + let (plain, searchable) = runtime.block_on(async { + ( + keyset + .encrypt_as::<_, smallint::Smallint>(&value, column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, smallint::SmallintEq>(&value, column()) + .await + .unwrap(), + ) + }); + db.execute( + "INSERT INTO v4_smallint VALUES ($1, + $2::text::jsonb::public.eql_v4_smallint, + $3::text::jsonb::public.eql_v4_smallint_eq)", + &[&id, &json(&plain), &json(&searchable)], + ) + .unwrap(); + } + let mut ids = |value: i16| -> Vec { + let query: smallint::SmallintEqQuery = runtime + .block_on(async { keyset.encrypt_as(&value, column()).await }) + .unwrap(); + db.query( + "SELECT id FROM v4_smallint WHERE searchable = $1::text::jsonb::eql_v4.query_smallint_eq ORDER BY id", + &[&json(&query)], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect() + }; + assert_eq!(ids(7), [2, 4]); + assert_eq!(ids(i16::MIN), [1]); + assert_eq!(ids(i16::MAX), [3]); + assert_eq!(ids(8), Vec::::new()); + + let back: String = db + .query_one("SELECT plain::text FROM v4_smallint WHERE id = 1", &[]) + .unwrap() + .get(0); + let opened: i16 = runtime + .block_on(async { + cipher + .decrypt_as( + serde_json::from_str::(&back).unwrap(), + column().into(), + ) + .await + }) + .unwrap(); + assert_eq!(opened, i16::MIN); + + // The storage-only column has no equality operator. + let err = db + .query("SELECT id FROM v4_smallint WHERE plain = plain", &[]) + .unwrap_err(); + let message = err.as_db_error().map(|e| e.message()).unwrap_or_default(); + assert!( + message.contains("is not supported for public.eql_v4_smallint"), + "{message}" + ); +} + /// A stored value's JSON text, for binding as `$n::text::jsonb`. mod erased { pub trait Json { @@ -975,6 +1127,10 @@ async fn the_v4_query_fixture_is_what_the_dispatch_derives() { "plaintext": "alice@example.com", "query": query("TextEq", "email", frame("alice@example.com")).await, }, + "tally": { + "plaintext": -12, + "query": query("SmallintEq", "tally", frame::(-12)).await, + }, }); let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/v4_queries.json"); From 337e8821647be7be9f58992fa27d2d6475e647d8 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 19 Oct 2026 00:47:46 +1100 Subject: [PATCH 2/2] test(golang,eql): seal the smallint EQL v4 types from Go, and store Go's rows in PostgreSQL A Tally test model, under Profile's table so its query shares v4_queries.json without touching Profile's frozen rows, holds an int16 Smallint, an int16 SmallintEq and an int32 SmallintEq. The rows seal to their EQL types and open back; an int32 outside the smallint range, above or below, is refused with ErrEncoding naming the field on the codec write, the single-field Encrypt and the Eq query; and Count.Eq derives the Rust fixture's query byte for byte. TestV4SmallintRowsMatchTheCommittedFixture checks v4_smallint_rows.json, which the Rust suite opens at 16 bits and inserts into eql_v4 columns, where = with the fixture's query finds the rows holding its value. The file is rewritten only when something other than `c` moves, so on its own it carries the ciphertexts of the last regeneration, not today's. EQL_V4_SMALLINT_ROWS_OUT and EQL_V4_SMALLINT_ROWS hand today's rows from Go to Rust, as EQL_V4_ROWS_OUT and EQL_V4_ROWS do for v4_rows.json, and the Go workflow's hand-off step now passes both pairs. So a guest change that moves only a smallint ciphertext, which Go's own round trip cannot see because the equality term is the same at every width, fails Rust's 16-bit open in the pull request that makes it. CI runs only the two open tests on the handed-off rows; PostgreSQL stores and queries the committed rows. The step's cargo filter is now --exact, so it names only the tests that read what it hands them. --- .github/workflows/tests-golang.yml | 18 +- .../golang/encrypt/eql_v4_smallint_test.go | 232 ++++++++++++++++++ .../encrypt/internal/testusers/tally.go | 15 ++ .../encrypt/internal/testusers/tally_stash.go | 160 ++++++++++++ .../eql/tests/encryption/fixtures/README.md | 5 +- .../encryption/fixtures/v4_smallint_rows.json | 115 +++++++++ .../eql/tests/encryption/tests/v4_types.rs | 155 ++++++++++++ 7 files changed, 692 insertions(+), 8 deletions(-) create mode 100644 languages/golang/encrypt/eql_v4_smallint_test.go create mode 100644 languages/golang/encrypt/internal/testusers/tally.go create mode 100644 languages/golang/encrypt/internal/testusers/tally_stash.go create mode 100644 packages/eql/tests/encryption/fixtures/v4_smallint_rows.json diff --git a/.github/workflows/tests-golang.yml b/.github/workflows/tests-golang.yml index 7e5280432..6ef27e601 100644 --- a/.github/workflows/tests-golang.yml +++ b/.github/workflows/tests-golang.yml @@ -248,17 +248,21 @@ jobs: echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB" mise run go:test - # The Rust suite opens the committed v4_rows.json, whose ciphertexts the - # Go SDK sealed when the file was last regenerated. This also hands it - # the rows the Go SDK seals in this run, so a Go guest and a native - # Rust build that stop agreeing on today's ciphertexts fail here rather - # than when someone next regenerates the file. The toolchain is the + # The Rust suite opens the committed v4_rows.json and + # v4_smallint_rows.json, whose ciphertexts the Go SDK sealed when each + # file was last regenerated. This also hands it the rows the Go SDK + # seals in this run, so a Go guest and a native Rust build that stop + # agreeing on today's ciphertexts fail here rather than when someone + # next regenerates a file. Both filters are exact, so each test named + # here is one that reads the rows handed to it. The toolchain is the # root's pin, as for the guests. - name: Rows the Go SDK seals today open in Rust run: | rows="$RUNNER_TEMP/v4_rows.json" - (cd languages/golang && CGO_ENABLED=0 EQL_V4_ROWS_OUT="$rows" go test -count=1 -run TestV4RowsMatchTheCommittedFixture ./encrypt) - EQL_V4_ROWS="$rows" cargo test --locked --manifest-path packages/eql/Cargo.toml -p eql-encryption-tests --test v4_types rows_the_go_sdk_sealed_open_in_rust + smallint_rows="$RUNNER_TEMP/v4_smallint_rows.json" + (cd languages/golang && CGO_ENABLED=0 EQL_V4_ROWS_OUT="$rows" EQL_V4_SMALLINT_ROWS_OUT="$smallint_rows" go test -count=1 -run '^(TestV4RowsMatchTheCommittedFixture|TestV4SmallintRowsMatchTheCommittedFixture)$' ./encrypt) + test -s "$rows" && test -s "$smallint_rows" + EQL_V4_ROWS="$rows" EQL_V4_SMALLINT_ROWS="$smallint_rows" cargo test --locked --manifest-path packages/eql/Cargo.toml -p eql-encryption-tests --test v4_types -- --exact rows_the_go_sdk_sealed_open_in_rust smallint_rows_the_go_sdk_sealed_open_in_rust # What is encrypted is fixed before the program ships: every generated # file is committed, and this fails when `go generate` would change diff --git a/languages/golang/encrypt/eql_v4_smallint_test.go b/languages/golang/encrypt/eql_v4_smallint_test.go new file mode 100644 index 000000000..59dff0686 --- /dev/null +++ b/languages/golang/encrypt/eql_v4_smallint_test.go @@ -0,0 +1,232 @@ +package encrypt_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "math" + "os" + "strings" + "testing" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/encrypt/internal/testusers" +) + +// The smallint EQL v4 types, sealed through generated code over the +// deterministic guest build with the EQL types: an int16 field reaches +// Smallint and SmallintEq, opens back, and its Eq query is the one +// eql-bindings' own dispatch derives (v4_queries.json, key tally). +func TestV4SmallintFieldsSealAndOpenBack(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + cipher := c.DefaultKeyset() + + tallies := []testusers.Tally{ + {ID: 1, Small: math.MinInt16, Count: -12, Wide: 7}, + {ID: 2, Small: math.MaxInt16, Count: 0, Wide: math.MinInt16}, + } + encrypted, err := testusers.EncryptTally(ctx, cipher, tallies) + if err != nil { + t.Fatal(err) + } + for i, e := range encrypted { + for column, value := range map[string][]byte{"small": e.Small, "tally": e.Count, "wide": e.Wide} { + want := "c,hm,i,v" + if column == "small" { + want = "c,i,v" + } + if got := keysOf(t, column, value); got != want { + t.Errorf("row %d %s: keys %s, want %s", i, column, got, want) + } + } + } + decrypted, err := testusers.DecryptTally(ctx, cipher, encrypted) + if err != nil { + t.Fatal(err) + } + for i := range tallies { + if decrypted[i] != tallies[i] { + t.Errorf("row %d: %+v, want %+v", i, decrypted[i], tallies[i]) + } + } + + // An int32 field in a smallint type seals at 16 bits: a value that does + // not fit is refused, not truncated, on every path, and the error names + // the field. Both bounds, as widened_test.go pins them for IntegerEq. + for _, v := range []int32{math.MaxInt16 + 1, math.MinInt16 - 1} { + refusals := map[string]error{} + _, refusals["EncryptTally"] = testusers.EncryptTally(ctx, cipher, []testusers.Tally{{ID: 3, Wide: v}}) + _, refusals["Wide.Encrypt"] = testusers.TallyFields.Wide.Encrypt(ctx, cipher, v) + _, refusals["Wide.Eq"] = testusers.TallyFields.Wide.Eq(ctx, cipher, v) + for path, err := range refusals { + if !errors.Is(err, encrypt.ErrEncoding) { + t.Errorf("%s(%d): %v, want ErrEncoding", path, v, err) + } else if !strings.Contains(err.Error(), "wide") { + t.Errorf("%s(%d): %v, want the field named", path, v, err) + } + } + } + // The bounds themselves fit. + for _, v := range []int32{math.MaxInt16, math.MinInt16} { + if _, err := testusers.TallyFields.Wide.Eq(ctx, cipher, v); err != nil { + t.Errorf("Wide.Eq(%d): %v", v, err) + } + } + + raw, err := os.ReadFile(fixturePath("v4_queries.json")) + if err != nil { + t.Fatal(err) + } + var fixture struct { + Tally struct { + Plaintext int16 + Query string + } + } + if err := json.Unmarshal(raw, &fixture); err != nil { + t.Fatal(err) + } + q, err := testusers.TallyFields.Count.Eq(ctx, cipher, fixture.Tally.Plaintext) + if err != nil { + t.Fatal(err) + } + if fixture.Tally.Query == "" || string(q) != fixture.Tally.Query { + t.Errorf("Count.Eq = %s\nwant %s", q, fixture.Tally.Query) + } +} + +// tallyRows are the rows v4_smallint_rows.json holds: both extremes of the +// width, and an int32 field at the extremes too. +var tallyRows = []testusers.Tally{ + {ID: 1, Small: math.MinInt16, Count: -12, Wide: math.MinInt16}, + {ID: 2, Small: math.MaxInt16, Count: 7, Wide: math.MaxInt16}, + {ID: 3, Small: 0, Count: -12, Wide: -1}, +} + +type smallintRowsFixture struct { + Comment string `json:"_comment"` + Seed string `json:"seed"` + Table string `json:"table"` + Rows []smallintRow `json:"rows"` +} + +type smallintRow struct { + ID int64 `json:"id"` + Plaintext smallintPlain `json:"plaintext"` + Stored map[string]json.RawMessage `json:"stored"` +} + +type smallintPlain struct { + Small int16 `json:"small"` + Tally int16 `json:"tally"` + Wide int32 `json:"wide"` +} + +func storedTally(e testusers.EncryptedTally) map[string][]byte { + return map[string][]byte{"small": e.Small, "tally": e.Count, "wide": e.Wide} +} + +// TestV4SmallintRowsMatchTheCommittedFixture checks the committed +// v4_smallint_rows.json against what the generated code seals today, as +// TestV4RowsMatchTheCommittedFixture does for v4_rows.json: everything but +// each `c` must match, and the committed `c` values must open. +// EQL_UPDATE_FIXTURES rewrites the file when something other than the +// ciphertexts moved, so a change that moves only `c` leaves the committed +// file as it was. EQL_V4_SMALLINT_ROWS_OUT names a file to write today's +// rows to, ciphertexts and all: CI hands it to the Rust suite +// (EQL_V4_SMALLINT_ROWS), so Rust opens the smallint ciphertexts the Go SDK +// seals now, not only the committed ones. +func TestV4SmallintRowsMatchTheCommittedFixture(t *testing.T) { + c := deterministicEQLClient(t) + ctx := context.Background() + encrypted, err := testusers.EncryptTally(ctx, c.DefaultKeyset(), tallyRows) + if err != nil { + t.Fatal(err) + } + fixture := smallintRowsFixture{ + Comment: "Tally rows sealed by the Go SDK's generated code over the deterministic EQL guest with the seed below; the Rust suite opens and queries them. Regenerate with EQL_UPDATE_FIXTURES=1 go test ./encrypt -run TestV4SmallintRows, do not edit.", + Seed: string(testSeed[:]), + Table: "profiles", + } + for i, e := range encrypted { + r := tallyRows[i] + row := smallintRow{ID: r.ID, Plaintext: smallintPlain{r.Small, r.Count, r.Wide}, Stored: map[string]json.RawMessage{}} + for column, value := range storedTally(e) { + row.Stored[column] = json.RawMessage(value) + } + fixture.Rows = append(fixture.Rows, row) + } + rendered, err := json.MarshalIndent(fixture, "", " ") + if err != nil { + t.Fatal(err) + } + rendered = append(rendered, '\n') + if out := os.Getenv("EQL_V4_SMALLINT_ROWS_OUT"); out != "" { + if err := os.WriteFile(out, rendered, 0o644); err != nil { + t.Fatal(err) + } + } + path := fixturePath("v4_smallint_rows.json") + var committed smallintRowsFixture + committedBytes, err := os.ReadFile(path) + if err == nil { + err = json.Unmarshal(committedBytes, &committed) + } + same := err == nil && bytes.Equal(smallintWithoutCiphertexts(t, committed), smallintWithoutCiphertexts(t, fixture)) + if os.Getenv("EQL_UPDATE_FIXTURES") != "" && !same { + if err := os.WriteFile(path, rendered, 0o644); err != nil { + t.Fatal(err) + } + t.Logf("%s: rewritten", path) + committed, same, err = fixture, true, nil + } + if err != nil { + t.Fatalf("%v: run with EQL_UPDATE_FIXTURES=1 to write it", err) + } + if !same { + t.Errorf("%s differs from what the generated code seals today beyond the ciphertexts; regenerate it with EQL_UPDATE_FIXTURES=1", path) + } + + sealed := make([]testusers.EncryptedTally, len(committed.Rows)) + for i, row := range committed.Rows { + sealed[i] = testusers.EncryptedTally{ID: row.ID, Small: []byte(row.Stored["small"]), Count: []byte(row.Stored["tally"]), Wide: []byte(row.Stored["wide"])} + } + opened, err := testusers.DecryptTally(ctx, c.DefaultKeyset(), sealed) + if err != nil { + t.Fatal(err) + } + for i, row := range committed.Rows { + if got := (smallintPlain{opened[i].Small, opened[i].Count, opened[i].Wide}); got != row.Plaintext { + t.Errorf("row %d opens to %+v, want %+v", row.ID, got, row.Plaintext) + } + } +} + +func smallintWithoutCiphertexts(t *testing.T, f smallintRowsFixture) []byte { + t.Helper() + out := f + out.Rows = nil + for _, row := range f.Rows { + stripped := smallintRow{ID: row.ID, Plaintext: row.Plaintext, Stored: map[string]json.RawMessage{}} + for column, value := range row.Stored { + var fields map[string]json.RawMessage + if err := json.Unmarshal(value, &fields); err != nil { + t.Fatal(err) + } + delete(fields, "c") + b, err := json.Marshal(fields) + if err != nil { + t.Fatal(err) + } + stripped.Stored[column] = b + } + out.Rows = append(out.Rows, stripped) + } + b, err := json.Marshal(out) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/languages/golang/encrypt/internal/testusers/tally.go b/languages/golang/encrypt/internal/testusers/tally.go new file mode 100644 index 000000000..cb377b93d --- /dev/null +++ b/languages/golang/encrypt/internal/testusers/tally.go @@ -0,0 +1,15 @@ +package testusers + +//go:generate go run github.com/cipherstash/stack/languages/golang/cmd/stashgen -type Tally -name Tally + +// Tally stores the smallint EQL v4 types, under Profile's table so its +// query shares v4_queries.json (key tally). It is a model of its own +// because Profile's rows are frozen in stored_v4_rows_v1.json: a new +// Profile field would change what those rows store. +type Tally struct { + _ struct{} `stash:"context=profiles"` + ID int64 `stash:"id,passthrough"` + Small int16 `stash:"small,encrypt_into=Smallint"` + Count int16 `stash:"tally,encrypt_into=SmallintEq"` + Wide int32 `stash:"wide,encrypt_into=SmallintEq"` +} diff --git a/languages/golang/encrypt/internal/testusers/tally_stash.go b/languages/golang/encrypt/internal/testusers/tally_stash.go new file mode 100644 index 000000000..286d89dac --- /dev/null +++ b/languages/golang/encrypt/internal/testusers/tally_stash.go @@ -0,0 +1,160 @@ +// Code generated by stashgen. DO NOT EDIT. + +package testusers + +import ( + "context" + "log/slog" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/encrypt/eql" + "github.com/cipherstash/stack/languages/golang/encrypt/gensupport" +) + +// Stops compiling when the library does not accept this version of generated file. +const _ = gensupport.GeneratedVersion1 + +// Tally prints its sealed fields in the clear: it has no String or LogValue +// method. Write them, or run stashgen with -redact. + +type EncryptedTally struct { + ID int64 + Small eql.Smallint + Count eql.SmallintEq + Wide eql.SmallintEq +} + +func (e EncryptedTally) String() string { + return gensupport.Redacted("EncryptedTally", map[string]any{"ID": e.ID}, "Small", "Count", "Wide") +} + +func (e EncryptedTally) LogValue() slog.Value { + return gensupport.RedactedLog(map[string]any{"ID": e.ID}, "Small", "Count", "Wide") +} + +// Stops compiling when Tally gains, loses, reorders or retypes a field. +var _ = tallyShape(Tally{}) + +type tallyShape struct { + _ struct{} + ID int64 + Small int16 + Count int16 + Wide int32 +} + +var tallyDeclaration = gensupport.Declare("profiles"). + Passthrough("id"). + EncryptInto("small", gensupport.Int16, "Smallint"). + EncryptInto("tally", gensupport.Int16, "SmallintEq"). + EncryptInto("wide", gensupport.Int16, "SmallintEq") + +var tallyCodec = gensupport.New(gensupport.Generated[Tally, EncryptedTally]{ + TypeName: "Tally", + Declaration: tallyDeclaration, + PrintsPlaintext: true, + Source: func(v Tally) gensupport.Values { + return gensupport.Values{ + "id": v.ID, + "small": v.Small, + "tally": v.Count, + "wide": v.Wide, + } + }, + Seal: func(rec gensupport.Record) (EncryptedTally, error) { + var e EncryptedTally + var err error + if e.ID, err = gensupport.Passthrough[int64](rec, "id"); err != nil { + return EncryptedTally{}, err + } + e.Small = eql.Smallint(rec["small"].EQL) + e.Count = eql.SmallintEq(rec["tally"].EQL) + e.Wide = eql.SmallintEq(rec["wide"].EQL) + return e, nil + }, + Open: func(e EncryptedTally) gensupport.Record { + return gensupport.Record{ + "id": {Value: e.ID}, + "small": {EQL: e.Small}, + "tally": {EQL: e.Count}, + "wide": {EQL: e.Wide}, + } + }, + Value: func(e EncryptedTally, vals gensupport.Values) (Tally, error) { + var v Tally + var err error + if v.ID, err = gensupport.Get[int64](vals, "id"); err != nil { + return Tally{}, err + } + if v.Small, err = gensupport.Get[int16](vals, "small"); err != nil { + return Tally{}, err + } + if v.Count, err = gensupport.Get[int16](vals, "tally"); err != nil { + return Tally{}, err + } + if v.Wide, err = gensupport.Get[int32](vals, "wide"); err != nil { + return Tally{}, err + } + return v, nil + }, +}) + +// EncryptTally seals each Tally, with one ZeroKMS request for each 500 sealed +// values, plus one the first time a keyset is used. The result has one element +// for each input, in the same order. +func EncryptTally(ctx context.Context, cipher *encrypt.Cipher, values []Tally) ([]EncryptedTally, error) { + return tallyCodec.Encrypt(ctx, cipher, values) +} + +// DecryptTally opens each EncryptedTally, with one ZeroKMS request for each 500 +// sealed values, plus one the first time a keyset is used. +func DecryptTally(ctx context.Context, d encrypt.Decrypter, encrypted []EncryptedTally) ([]Tally, error) { + return tallyCodec.Decrypt(ctx, d, encrypted) +} + +var TallyFields = struct { + Small TallySmallField + Count TallyCountField + Wide TallyWideField +}{ + Small: TallySmallField{gensupport.NewField[int16](tallyDeclaration, "small")}, + Count: TallyCountField{gensupport.NewField[int16](tallyDeclaration, "tally")}, + Wide: TallyWideField{gensupport.NewField[int32](tallyDeclaration, "wide")}, +} + +type TallySmallField struct { + field gensupport.Field[int16] +} + +func (f TallySmallField) Encrypt(ctx context.Context, c *encrypt.Cipher, v int16) (eql.Smallint, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.Smallint(out.EQL), err +} + +type TallyCountField struct { + field gensupport.Field[int16] +} + +func (f TallyCountField) Encrypt(ctx context.Context, c *encrypt.Cipher, v int16) (eql.SmallintEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.SmallintEq(out.EQL), err +} + +func (f TallyCountField) Eq(ctx context.Context, c *encrypt.Cipher, v int16) (eql.SmallintEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.SmallintEqQuery(term), err +} + +type TallyWideField struct { + field gensupport.Field[int32] +} + +func (f TallyWideField) Encrypt(ctx context.Context, c *encrypt.Cipher, v int32) (eql.SmallintEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.SmallintEq(out.EQL), err +} + +func (f TallyWideField) Eq(ctx context.Context, c *encrypt.Cipher, v int32) (eql.SmallintEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.SmallintEqQuery(term), err +} diff --git a/packages/eql/tests/encryption/fixtures/README.md b/packages/eql/tests/encryption/fixtures/README.md index 536bffd24..4fb784ea0 100644 --- a/packages/eql/tests/encryption/fixtures/README.md +++ b/packages/eql/tests/encryption/fixtures/README.md @@ -5,12 +5,15 @@ | `v4_rows.json` | the Go SDK's `TestV4RowsMatchTheCommittedFixture` | yes, with `EQL_UPDATE_FIXTURES=1` | | `v4_queries.json` | the by-name dispatch in `tests/v4_types.rs` | yes, with `EQL_UPDATE_FIXTURES=1` | | `text_eq_query.json`, `ste_vec_selectors.json` | `tests/text_eq.rs`, `tests/ste_vec_selectors.rs` | yes, see each test | +| `v4_smallint_rows.json` | the Go SDK's `TestV4SmallintRowsMatchTheCommittedFixture` | yes, with `EQL_UPDATE_FIXTURES=1` | | `stored_v4_rows_v1.json` | copied from `v4_rows.json` at the next build | **never: frozen** | ## `stored_v4_rows_v1.json` (FROZEN) `Profile` rows the Go SDK's generated code stored with the `next` build, one -value of every producible EQL v4 type. `v4_rows.json` proves +value of every EQL v4 type that build produced. Types made producible since +are sealed into their own regenerable row fixtures (see the table above); a +later frozen file covers them once it is cut. `v4_rows.json` proves the Go SDK and Rust agree *today* and is regenerated when the encoding moves. This file is the opposite: it is never regenerated or edited, so a change that breaks rows already stored fails instead of being regenerated away. diff --git a/packages/eql/tests/encryption/fixtures/v4_smallint_rows.json b/packages/eql/tests/encryption/fixtures/v4_smallint_rows.json new file mode 100644 index 000000000..a5ccfee5c --- /dev/null +++ b/packages/eql/tests/encryption/fixtures/v4_smallint_rows.json @@ -0,0 +1,115 @@ +{ + "_comment": "Tally rows sealed by the Go SDK's generated code over the deterministic EQL guest with the seed below; the Rust suite opens and queries them. Regenerate with EQL_UPDATE_FIXTURES=1 go test ./encrypt -run TestV4SmallintRows, do not edit.", + "seed": "encrypt round-trip tests seed v1", + "table": "profiles", + "rows": [ + { + "id": 1, + "plaintext": { + "small": -32768, + "tally": -12, + "wide": -32768 + }, + "stored": { + "small": { + "v": 4, + "i": { + "t": "profiles", + "c": "small" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAD4LCrufuRD/FQY4TZFe6AfIACHiUuB0CJjJr+LbNxAwTa6nFzZsohMvYKuG0eEpQBCbgKf9AiLZjYMlO0juGQzOeX6KmJJlGAIEDEAdoDnsA72" + }, + "tally": { + "v": 4, + "i": { + "t": "profiles", + "c": "tally" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADvGG8yu5085HD9Jm4QqutRIABdpCObhZDRQkCMnI2LxRF5VXligtVWiYIKNuwkApKOhgKuPx0Xh4na+BtvDwsTOE3PUvpIH16sh6KGbP67mou4", + "hm": "7aeb7ba10d7f88d1c6ec1e4d819e20aa0460e2467eaf1c5829437126ecc72db3" + }, + "wide": { + "v": 4, + "i": { + "t": "profiles", + "c": "wide" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACG2/FDPiPfbcgt2KR0n9s0IACH7cDAisfp79UHd5R5MnIYNc9nJHmEe9GA03PUrox54AKI8b2I7Oopv7wKeCbETVPc8n0KoR/eNqNGepjAv0fR", + "hm": "9f5bd412c1ec1d9e4f15369c64ce122ec845647246ca07955d7203fc96555624" + } + } + }, + { + "id": 2, + "plaintext": { + "small": 32767, + "tally": 7, + "wide": 32767 + }, + "stored": { + "small": { + "v": 4, + "i": { + "t": "profiles", + "c": "small" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABY92Lf6tGcgnCxPlh/N7fAIABg4YLpuiC4kQ3TFpot377ZfERfsdbGLyE3T8AOdj5eywIpGUbElbuwIdq+vZDbLOtnOSgRN1qzJYNZ97ZPnO0s" + }, + "tally": { + "v": 4, + "i": { + "t": "profiles", + "c": "tally" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADJrvFsTc/RhAG3RKXlEdLZIADuWnjVDRcyIKSqfa1PHPcHJ5XGatPoW/q7+Bp+5kS53gIpPvaJ0mvH0fzO9cI0UQ/fCUAX4eVmBQGZFKyyZbG5", + "hm": "047ab2a408d78f27bcc929f822c158467c53876cde604a483c2b5cb7f2e39aab" + }, + "wide": { + "v": 4, + "i": { + "t": "profiles", + "c": "wide" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACMOoUNfVb6x7EVb4wzgrTKIADuyS3Tll1IIW6pgUv4Cu2iDViH88RHa/FjC5gzNC5jYwJ790dlZc8MjWj1aN3o4Uo0LA0JAnxXgIckJoJGNPSN", + "hm": "8b20978328a93915cf589e8c74fb9028c80b58e461c51314df8d51fe9a36c077" + } + } + }, + { + "id": 3, + "plaintext": { + "small": 0, + "tally": -12, + "wide": -1 + }, + "stored": { + "small": { + "v": 4, + "i": { + "t": "profiles", + "c": "small" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABYn3RZE90JwspcrV/psve/IACtxdWMfY3cnahhwVmZ+IPpuLhGNVGQnLoU7NkXgJlQlwItsLZHfwkaJ2TywKUBlwsCRmQow0ldmAvx6BDhKpgw" + }, + "tally": { + "v": 4, + "i": { + "t": "profiles", + "c": "tally" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAA1m/PV0eKPMIRerBIYOsnNIABDbUnDV8SPoZNssOuZHnrbBGKhHmWWtLI7MMqCzLZPrQK5dHwUPznYOntuuLR4pnGbLPuPJK4y/0iieiRNHtfv", + "hm": "7aeb7ba10d7f88d1c6ec1e4d819e20aa0460e2467eaf1c5829437126ecc72db3" + }, + "wide": { + "v": 4, + "i": { + "t": "profiles", + "c": "wide" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAApfEecjt2IZoP2fHX11pmUIABuRiSfGGi7UIxZHX7tMXh1g5Wzr6lMP48A3l7vIgRwwQI8A1k4j6jffo1R1lwun47UMgH9i4hpRwFfA51gNg2W", + "hm": "ffbbb89c5fc4e850a711ab9547c6eaca24b68f408346591d8032a60488130f87" + } + } + } + ] +} diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs index 7819884d3..0e4be75a5 100644 --- a/packages/eql/tests/encryption/tests/v4_types.rs +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -1065,6 +1065,161 @@ fn postgres_stores_and_finds_smallint_rows() { ); } +/// `fixtures/v4_smallint_rows.json`: `Tally` rows the Go SDK's generated +/// code sealed over the deterministic EQL guest +/// (`TestV4SmallintRowsMatchTheCommittedFixture` writes it). +mod go_tally { + use super::*; + use std::collections::BTreeMap; + + #[derive(serde::Deserialize)] + pub struct Fixture { + pub seed: String, + pub table: String, + pub rows: Vec, + } + #[derive(serde::Deserialize)] + pub struct Row { + pub id: i32, + pub plaintext: Plain, + pub stored: BTreeMap, + } + #[derive(serde::Deserialize)] + pub struct Plain { + pub small: i16, + pub tally: i16, + pub wide: i32, + } + + /// The committed rows, or the file `EQL_V4_SMALLINT_ROWS` names: CI + /// points it at rows the Go SDK sealed in the same job, so today's Go + /// ciphertexts reach Rust, not only the committed ones. + pub fn fixture() -> Fixture { + let path = std::env::var_os("EQL_V4_SMALLINT_ROWS").map_or_else( + || { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures/v4_smallint_rows.json") + }, + std::path::PathBuf::from, + ); + let raw = std::fs::read_to_string(path).expect( + "fixtures/v4_smallint_rows.json is written by the Go SDK's \ + TestV4SmallintRowsMatchTheCommittedFixture", + ); + serde_json::from_str(&raw).unwrap() + } + + /// Open every field of `row` as its v4 type and check it against what + /// Go sealed. The `int32` field `wide` was sealed at 16 bits, so it + /// opens as an `i16` here. + pub async fn open_row( + cipher: &stack_encrypt::StackCipher, + row: &Row, + ) { + let column = |name: &str| Identifier::for_column("profiles", name).unwrap(); + let parsed = |name: &str| row.stored[name].clone(); + let small: i16 = cipher + .decrypt_as( + serde_json::from_value::(parsed("small")).unwrap(), + column("small").into(), + ) + .await + .unwrap(); + assert_eq!(small, row.plaintext.small, "row {} small", row.id); + let tally: i16 = cipher + .decrypt_as( + serde_json::from_value::(parsed("tally")).unwrap(), + column("tally").into(), + ) + .await + .unwrap(); + assert_eq!(tally, row.plaintext.tally, "row {} tally", row.id); + let wide: i16 = cipher + .decrypt_as( + serde_json::from_value::(parsed("wide")).unwrap(), + column("wide").into(), + ) + .await + .unwrap(); + assert_eq!(i32::from(wide), row.plaintext.wide, "row {} wide", row.id); + } +} + +/// What the Go SDK sealed as `Smallint` and `SmallintEq` parses as each +/// type and opens in Rust, at 16 bits, field by field. +#[tokio::test] +async fn smallint_rows_the_go_sdk_sealed_open_in_rust() { + let fixture = go_tally::fixture(); + assert_eq!(fixture.table, "profiles"); + assert!(fixture.rows.len() > 1); + let cipher = go_rows::cipher(&fixture.seed).await; + for row in &fixture.rows { + go_tally::open_row(&cipher, row).await; + } +} + +/// The Go-sealed smallint rows in PostgreSQL: each value passes its domain +/// check, and `=` with the fixture's `SmallintEq` query (which the Go SDK's +/// test derives byte for byte) finds the rows holding that value. +#[test] +#[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] +fn postgres_stores_and_queries_smallint_rows_the_go_sdk_sealed() { + let fixture = go_tally::fixture(); + let mut db = postgres::Client::connect( + &std::env::var("EQL_TEST_DATABASE_URL").expect("EQL_TEST_DATABASE_URL"), + postgres::NoTls, + ) + .unwrap(); + db.batch_execute( + "CREATE TEMP TABLE go_tallies ( + id int PRIMARY KEY, + small public.eql_v4_smallint, + tally public.eql_v4_smallint_eq, + wide public.eql_v4_smallint_eq + );", + ) + .unwrap(); + for row in &fixture.rows { + let v = |column: &str| row.stored[column].to_string(); + db.execute( + "INSERT INTO go_tallies VALUES ($1, + $2::text::jsonb::public.eql_v4_smallint, + $3::text::jsonb::public.eql_v4_smallint_eq, + $4::text::jsonb::public.eql_v4_smallint_eq)", + &[&row.id, &v("small"), &v("tally"), &v("wide")], + ) + .unwrap(); + } + let queries: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string( + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/v4_queries.json"), + ) + .unwrap(), + ) + .unwrap(); + let plaintext = queries["tally"]["plaintext"].as_i64().unwrap(); + let expected: Vec = fixture + .rows + .iter() + .filter(|r| i64::from(r.plaintext.tally) == plaintext) + .map(|r| r.id) + .collect(); + assert!( + !expected.is_empty() && expected.len() < fixture.rows.len(), + "the fixture splits the rows" + ); + let ids: Vec = db + .query( + "SELECT id FROM go_tallies WHERE tally = $1::text::jsonb::eql_v4.query_smallint_eq ORDER BY id", + &[&queries["tally"]["query"].as_str().unwrap()], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect(); + assert_eq!(ids, expected, "= over Go-sealed tally"); +} + /// A stored value's JSON text, for binding as `$n::text::jsonb`. mod erased { pub trait Json {