diff --git a/.changeset/eql-v4-bindings.md b/.changeset/eql-v4-bindings.md index 1ccfcdd3a..bf379e09d 100644 --- a/.changeset/eql-v4-bindings.md +++ b/.changeset/eql-v4-bindings.md @@ -2,6 +2,6 @@ '@cipherstash/eql': minor --- -**The `eql-bindings` crate has an `eql_bindings::v4` module: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces the storage-only types `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the plaintext types in `eql_bindings::encryption::plaintext`; the Go SDK's `encrypt/eql` table marks them producible. `IntegerEq` and `BigintEq` stay unproducible until vitaminc 0.6 settles the integer equality term. +**The `eql-bindings` crate has an `eql_bindings::v4` module, with the `stack-encrypt` feature: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt, so the module exists only with that feature and its types derive Stack Encrypt's traits without a further gate. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces the storage-only types `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the Rust primitives `i32`, `i64`, `bool`, `f32` and `f64`; the Go SDK's `encrypt/eql` table marks them producible. Stack Encrypt also produces the number equality types `IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`, whose `=` 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` (ADR-0002's semantic baseline). Every type it cannot produce yet names the issue that tracks its blocker. Nothing a released version shipped has changed. The v3 types, the v3 SQL and the TypeScript package are as they were; the `stack-encrypt` derives on the v3 text types and the `v3::targets` module were added after the last release and are moved to v4 here, not removed from a release. diff --git a/languages/golang/CHANGELOG.md b/languages/golang/CHANGELOG.md index 8eb992cf8..8f891e818 100644 --- a/languages/golang/CHANGELOG.md +++ b/languages/golang/CHANGELOG.md @@ -41,6 +41,10 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`. NULL. - **`encrypt.Range`**: what `Between` returns, the low and high bound's terms. +- **`IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`**: an `int32`, `int64`, + `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`. - **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. @@ -53,3 +57,5 @@ listed under **Breaking**. Releases are tagged `languages/golang/vX.Y.Z`. checked by decrypting them, and such a field stores no ciphertext. - A query with a nil cipher returns `ErrEncoding` from generated code as from the library. +- stashgen's refusal of an EQL type it cannot produce names the issue that + tracks the blocker, in place of telling you to wait for vitaminc 0.6. diff --git a/languages/golang/cmd/stashgen/README.md b/languages/golang/cmd/stashgen/README.md index c02ab6b8d..b86658e8b 100644 --- a/languages/golang/cmd/stashgen/README.md +++ b/languages/golang/cmd/stashgen/README.md @@ -159,11 +159,13 @@ The engine produces these EQL types today, each from one Go field type: | `encrypt_into` | Go field | |---|---| | `TextEq`, `Text`, `TextMatch` | `string` | -| `Integer` | `int32` | -| `Bigint` | `int64` | +| `Integer`, `IntegerEq` | `int32` | +| `Bigint`, `BigintEq` | `int64` | | `Boolean` | `bool` | -| `Real` | `float32` | -| `Double` | `float64` | +| `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 width must match: an `int` or `int64` field is not an `Integer`, and 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 1e59ffe81..9e99a13f0 100644 --- a/languages/golang/encrypt/README.md +++ b/languages/golang/encrypt/README.md @@ -399,14 +399,20 @@ The EQL types the engine produces, and the Go field types each takes: | `TextMatch` | `string` | `@@` (substring match, 3-grams, case-insensitive) | | `Text` | `string` | none: stored and read only | | `Integer` | `int32` | none | +| `IntegerEq` | `int32` | `=`, `<>` | | `Bigint` | `int64` | none | +| `BigintEq` | `int64` | `=`, `<>` | | `Boolean` | `bool` | none | | `Real` | `float32` | none | +| `RealEq` | `float32` | `=`, `<>` | | `Double` | `float64` | none | +| `DoubleEq` | `float64` | `=`, `<>` | A `TextMatch` value must be at least three characters long: shorter text has -no 3-grams to index and is refused. `IntegerEq` and `BigintEq` are held back -until the engine's integer equality term settles in vitaminc 0.6. The rest of +no 3-grams to index and is refused. The number equality types compare as +PostgreSQL compares the plain types: an integer equals itself whatever its +width, and for floats `-0.0 = 0.0` and `NaN = NaN`; the stored value still +decrypts to exactly what was written. The rest of the catalog (ordering, dates, decimals, JSON) is listed in `eql.Types` with the reason it is not produced yet, and `stashgen` refuses it with that reason. The guest builds the value: the generated code diff --git a/languages/golang/encrypt/doc.go b/languages/golang/encrypt/doc.go index 4a1cb6e92..c56609d1e 100644 --- a/languages/golang/encrypt/doc.go +++ b/languages/golang/encrypt/doc.go @@ -21,8 +21,11 @@ // } // // 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 an int32, -// Bigint an int64, Boolean a bool, Real a float32 and Double a float64. +// 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. The *Eq types answer +// Eq; 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 3b329782b..d0ea96ef9 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -10,55 +10,55 @@ import "database/sql/driver" // it does not, with the Reason. var Types = []Type{ {Name: "Integer", GoName: "Integer", Family: "integer", Suffix: "", Plaintext: "int32", SQLDomain: "public.eql_v4_integer", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, - {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: false, Reason: "integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "int32", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: true, Reason: ""}, + {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: "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: false, Reason: "integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {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: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, - {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: "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)"}, + {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: "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: ""}, - {Name: "TextOrdOre", GoName: "TextOrdOre", Family: "text", Suffix: "OrdOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ore", Indexes: []string{"eq", "ore"}, Query: "TextOrdOreQuery", QuerySQLDomain: "eql_v4.query_text_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "TextOrd", GoName: "TextOrd", Family: "text", Suffix: "Ord", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord", Indexes: []string{"eq", "ope"}, Query: "TextOrdQuery", QuerySQLDomain: "eql_v4.query_text_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "TextOrdOpe", GoName: "TextOrdOpe", Family: "text", Suffix: "OrdOpe", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ope", Indexes: []string{"eq", "ope"}, Query: "TextOrdOpeQuery", QuerySQLDomain: "eql_v4.query_text_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "TextSearchOre", GoName: "TextSearchOre", Family: "text", Suffix: "SearchOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_search_ore", Indexes: []string{"eq", "ore", "match"}, Query: "TextSearchOreQuery", QuerySQLDomain: "eql_v4.query_text_search_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "TextSearch", GoName: "TextSearch", Family: "text", Suffix: "Search", Plaintext: "string", SQLDomain: "public.eql_v4_text_search", Indexes: []string{"eq", "ope", "match"}, Query: "TextSearchQuery", QuerySQLDomain: "eql_v4.query_text_search", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "TextOrdOre", GoName: "TextOrdOre", Family: "text", Suffix: "OrdOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ore", Indexes: []string{"eq", "ore"}, Query: "TextOrdOreQuery", QuerySQLDomain: "eql_v4.query_text_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, + {Name: "TextOrd", GoName: "TextOrd", Family: "text", Suffix: "Ord", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord", Indexes: []string{"eq", "ope"}, Query: "TextOrdQuery", QuerySQLDomain: "eql_v4.query_text_ord", Producible: false, Reason: "no EQL type is built from the engine's text order terms yet (#1131)"}, + {Name: "TextOrdOpe", GoName: "TextOrdOpe", Family: "text", Suffix: "OrdOpe", Plaintext: "string", SQLDomain: "public.eql_v4_text_ord_ope", Indexes: []string{"eq", "ope"}, Query: "TextOrdOpeQuery", QuerySQLDomain: "eql_v4.query_text_ord_ope", Producible: false, Reason: "no EQL type is built from the engine's text order terms yet (#1131)"}, + {Name: "TextSearchOre", GoName: "TextSearchOre", Family: "text", Suffix: "SearchOre", Plaintext: "string", SQLDomain: "public.eql_v4_text_search_ore", Indexes: []string{"eq", "ore", "match"}, Query: "TextSearchOreQuery", QuerySQLDomain: "eql_v4.query_text_search_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, + {Name: "TextSearch", GoName: "TextSearch", Family: "text", Suffix: "Search", Plaintext: "string", SQLDomain: "public.eql_v4_text_search", Indexes: []string{"eq", "ope", "match"}, Query: "TextSearchQuery", QuerySQLDomain: "eql_v4.query_text_search", Producible: false, Reason: "no EQL type is built from the engine's text order terms yet (#1131)"}, {Name: "Boolean", GoName: "Boolean", Family: "boolean", Suffix: "", Plaintext: "bool", SQLDomain: "public.eql_v4_boolean", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, {Name: "Real", GoName: "Real", Family: "real", Suffix: "", Plaintext: "float32", SQLDomain: "public.eql_v4_real", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, - {Name: "RealEq", GoName: "RealEq", Family: "real", Suffix: "Eq", Plaintext: "float32", SQLDomain: "public.eql_v4_real_eq", Indexes: []string{"eq"}, Query: "RealEqQuery", QuerySQLDomain: "eql_v4.query_real_eq", Producible: false, Reason: "float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002)"}, - {Name: "RealOrdOre", GoName: "RealOrdOre", Family: "real", Suffix: "OrdOre", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ore", Indexes: []string{"ore"}, Query: "RealOrdOreQuery", QuerySQLDomain: "eql_v4.query_real_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "RealOrd", GoName: "RealOrd", Family: "real", Suffix: "Ord", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord", Indexes: []string{"ope"}, Query: "RealOrdQuery", QuerySQLDomain: "eql_v4.query_real_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "RealOrdOpe", GoName: "RealOrdOpe", Family: "real", Suffix: "OrdOpe", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ope", Indexes: []string{"ope"}, Query: "RealOrdOpeQuery", QuerySQLDomain: "eql_v4.query_real_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, + {Name: "RealEq", GoName: "RealEq", Family: "real", Suffix: "Eq", Plaintext: "float32", SQLDomain: "public.eql_v4_real_eq", Indexes: []string{"eq"}, Query: "RealEqQuery", QuerySQLDomain: "eql_v4.query_real_eq", Producible: true, Reason: ""}, + {Name: "RealOrdOre", GoName: "RealOrdOre", Family: "real", Suffix: "OrdOre", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ore", Indexes: []string{"ore"}, Query: "RealOrdOreQuery", QuerySQLDomain: "eql_v4.query_real_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, + {Name: "RealOrd", GoName: "RealOrd", Family: "real", Suffix: "Ord", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord", Indexes: []string{"ope"}, Query: "RealOrdQuery", QuerySQLDomain: "eql_v4.query_real_ord", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, + {Name: "RealOrdOpe", GoName: "RealOrdOpe", Family: "real", Suffix: "OrdOpe", Plaintext: "float32", SQLDomain: "public.eql_v4_real_ord_ope", Indexes: []string{"ope"}, Query: "RealOrdOpeQuery", QuerySQLDomain: "eql_v4.query_real_ord_ope", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, {Name: "Double", GoName: "Double", Family: "double", Suffix: "", Plaintext: "float64", SQLDomain: "public.eql_v4_double", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: true, Reason: ""}, - {Name: "DoubleEq", GoName: "DoubleEq", Family: "double", Suffix: "Eq", Plaintext: "float64", SQLDomain: "public.eql_v4_double_eq", Indexes: []string{"eq"}, Query: "DoubleEqQuery", QuerySQLDomain: "eql_v4.query_double_eq", Producible: false, Reason: "float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002)"}, - {Name: "DoubleOrdOre", GoName: "DoubleOrdOre", Family: "double", Suffix: "OrdOre", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ore", Indexes: []string{"ore"}, Query: "DoubleOrdOreQuery", QuerySQLDomain: "eql_v4.query_double_ord_ore", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "DoubleOrd", GoName: "DoubleOrd", Family: "double", Suffix: "Ord", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord", Indexes: []string{"ope"}, Query: "DoubleOrdQuery", QuerySQLDomain: "eql_v4.query_double_ord", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "DoubleOrdOpe", GoName: "DoubleOrdOpe", Family: "double", Suffix: "OrdOpe", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ope", Indexes: []string{"ope"}, Query: "DoubleOrdOpeQuery", QuerySQLDomain: "eql_v4.query_double_ord_ope", Producible: false, Reason: "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)"}, - {Name: "SteVecDocument", GoName: "SteVecDocument", Family: "json", Suffix: "Search", Plaintext: "", SQLDomain: "public.eql_v4_json_search", Indexes: []string{"json"}, Query: "SteVecQuery", QuerySQLDomain: "eql_v4.query_json", Producible: false, Reason: "the JSON index is a new operation in the engine"}, + {Name: "DoubleEq", GoName: "DoubleEq", Family: "double", Suffix: "Eq", Plaintext: "float64", SQLDomain: "public.eql_v4_double_eq", Indexes: []string{"eq"}, Query: "DoubleEqQuery", QuerySQLDomain: "eql_v4.query_double_eq", Producible: true, Reason: ""}, + {Name: "DoubleOrdOre", GoName: "DoubleOrdOre", Family: "double", Suffix: "OrdOre", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ore", Indexes: []string{"ore"}, Query: "DoubleOrdOreQuery", QuerySQLDomain: "eql_v4.query_double_ord_ore", Producible: false, Reason: "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)"}, + {Name: "DoubleOrd", GoName: "DoubleOrd", Family: "double", Suffix: "Ord", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord", Indexes: []string{"ope"}, Query: "DoubleOrdQuery", QuerySQLDomain: "eql_v4.query_double_ord", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, + {Name: "DoubleOrdOpe", GoName: "DoubleOrdOpe", Family: "double", Suffix: "OrdOpe", Plaintext: "float64", SQLDomain: "public.eql_v4_double_ord_ope", Indexes: []string{"ope"}, Query: "DoubleOrdOpeQuery", QuerySQLDomain: "eql_v4.query_double_ord_ope", Producible: false, Reason: "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)"}, + {Name: "SteVecDocument", GoName: "SteVecDocument", Family: "json", Suffix: "Search", Plaintext: "", SQLDomain: "public.eql_v4_json_search", Indexes: []string{"json"}, Query: "SteVecQuery", QuerySQLDomain: "eql_v4.query_json", Producible: false, Reason: "the JSON index is a new operation in the engine (#1060)"}, {Name: "Json", GoName: "JSON", Family: "json", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_json", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "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"}, } @@ -87,7 +87,7 @@ func (v *Integer) UnmarshalJSON(b []byte) error { } // IntegerEq is the EQL type stored in public.eql_v4_integer_eq: the integer family with the eq index. -// The engine cannot produce it yet: integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade. +// The engine produces it from a int32. type IntegerEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -134,7 +134,7 @@ func (v *IntegerEqQuery) UnmarshalJSON(b []byte) error { } // IntegerOrdOre is the EQL type stored in public.eql_v4_integer_ord_ore: the integer family with the ore index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type IntegerOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -181,7 +181,7 @@ func (v *IntegerOrdOreQuery) UnmarshalJSON(b []byte) error { } // IntegerOrd is the EQL type stored in public.eql_v4_integer_ord: the integer family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 IntegerOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -228,7 +228,7 @@ func (v *IntegerOrdQuery) UnmarshalJSON(b []byte) error { } // IntegerOrdOpe is the EQL type stored in public.eql_v4_integer_ord_ope: the integer family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 IntegerOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). type SmallintOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -511,7 +511,7 @@ func (v *Bigint) UnmarshalJSON(b []byte) error { } // BigintEq is the EQL type stored in public.eql_v4_bigint_eq: the bigint family with the eq index. -// The engine cannot produce it yet: integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade. +// The engine produces it from a int64. type BigintEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -558,7 +558,7 @@ func (v *BigintEqQuery) UnmarshalJSON(b []byte) error { } // BigintOrdOre is the EQL type stored in public.eql_v4_bigint_ord_ore: the bigint family with the ore index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type BigintOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -605,7 +605,7 @@ func (v *BigintOrdOreQuery) UnmarshalJSON(b []byte) error { } // BigintOrd is the EQL type stored in public.eql_v4_bigint_ord: the bigint family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 BigintOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -652,7 +652,7 @@ func (v *BigintOrdQuery) UnmarshalJSON(b []byte) error { } // BigintOrdOpe is the EQL type stored in public.eql_v4_bigint_ord_ope: the bigint family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 BigintOrdOpe []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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). 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: vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002). +// 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). type NumericOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1453,7 +1453,7 @@ func (v *TextMatchQuery) UnmarshalJSON(b []byte) error { } // TextOrdOre is the EQL type stored in public.eql_v4_text_ord_ore: the text family with the eq and ore indexes. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type TextOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1500,7 +1500,7 @@ func (v *TextOrdOreQuery) UnmarshalJSON(b []byte) error { } // TextOrd is the EQL type stored in public.eql_v4_text_ord: the text family with the eq and ope indexes. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: no EQL type is built from the engine's text order terms yet (#1131). type TextOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1547,7 +1547,7 @@ func (v *TextOrdQuery) UnmarshalJSON(b []byte) error { } // TextOrdOpe is the EQL type stored in public.eql_v4_text_ord_ope: the text family with the eq and ope indexes. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: no EQL type is built from the engine's text order terms yet (#1131). type TextOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1594,7 +1594,7 @@ func (v *TextOrdOpeQuery) UnmarshalJSON(b []byte) error { } // TextSearchOre is the EQL type stored in public.eql_v4_text_search_ore: the text family with the eq and ore and match indexes. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type TextSearchOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1641,7 +1641,7 @@ func (v *TextSearchOreQuery) UnmarshalJSON(b []byte) error { } // TextSearch is the EQL type stored in public.eql_v4_text_search: the text family with the eq and ope and match indexes. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: no EQL type is built from the engine's text order terms yet (#1131). type TextSearch []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1736,7 +1736,7 @@ func (v *Real) UnmarshalJSON(b []byte) error { } // RealEq is the EQL type stored in public.eql_v4_real_eq: the real family with the eq index. -// The engine cannot produce it yet: float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002). +// The engine produces it from a float32. type RealEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1783,7 +1783,7 @@ func (v *RealEqQuery) UnmarshalJSON(b []byte) error { } // RealOrdOre is the EQL type stored in public.eql_v4_real_ord_ore: the real family with the ore index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type RealOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1830,7 +1830,7 @@ func (v *RealOrdOreQuery) UnmarshalJSON(b []byte) error { } // RealOrd is the EQL type stored in public.eql_v4_real_ord: the real family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 RealOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1877,7 +1877,7 @@ func (v *RealOrdQuery) UnmarshalJSON(b []byte) error { } // RealOrdOpe is the EQL type stored in public.eql_v4_real_ord_ope: the real family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 RealOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1948,7 +1948,7 @@ func (v *Double) UnmarshalJSON(b []byte) error { } // DoubleEq is the EQL type stored in public.eql_v4_double_eq: the double family with the eq index. -// The engine cannot produce it yet: float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002). +// The engine produces it from a float64. type DoubleEq []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1995,7 +1995,7 @@ func (v *DoubleEqQuery) UnmarshalJSON(b []byte) error { } // DoubleOrdOre is the EQL type stored in public.eql_v4_double_ord_ore: the double family with the ore index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// The engine cannot produce it yet: EQL stores block ORE terms and the engine derives CLLW ORE (#1132). type DoubleOrdOre []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2042,7 +2042,7 @@ func (v *DoubleOrdOreQuery) UnmarshalJSON(b []byte) error { } // DoubleOrd is the EQL type stored in public.eql_v4_double_ord: the double family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 DoubleOrd []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2089,7 +2089,7 @@ func (v *DoubleOrdQuery) UnmarshalJSON(b []byte) error { } // DoubleOrdOpe is the EQL type stored in public.eql_v4_double_ord_ope: the double family with the ope index. -// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). +// 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 DoubleOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2136,7 +2136,7 @@ func (v *DoubleOrdOpeQuery) UnmarshalJSON(b []byte) error { } // SteVecDocument is the EQL type stored in public.eql_v4_json_search: the json family with the json index. -// The engine cannot produce it yet: the JSON index is a new operation in the engine. +// The engine cannot produce it yet: the JSON index is a new operation in the engine (#1060). type SteVecDocument []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 efc4c4319..755eb99a4 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", "Bigint", "Text", "TextEq", "TextMatch", "Boolean", "Real", "Double"} + want := []string{"Integer", "IntegerEq", "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", "Bigint": "int64", "Boolean": "bool", "Real": "float32", "Double": "float64"} { + 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"} { 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/eql_v4_test.go b/languages/golang/encrypt/eql_v4_test.go index ecd3c7aef..26d252325 100644 --- a/languages/golang/encrypt/eql_v4_test.go +++ b/languages/golang/encrypt/eql_v4_test.go @@ -24,8 +24,8 @@ import ( // eql_v4 columns and queries it. var profiles = []testusers.Profile{ - {ID: 1, Visits: 3, Balance: 1234567890123, Active: true, Ratio: 0.5, Score: 2.25, Note: "a private note", Name: "alice"}, - {ID: 2, Visits: 0, Balance: -1, Active: false, Ratio: -1.5, Score: 0, Note: "", Name: "bob smith"}, + {ID: 1, Visits: 3, Balance: 1234567890123, Active: true, Ratio: 0.5, Score: 2.25, Note: "a private note", Name: "alice", Age: 42, Rank: 7, Weight: 1.5, Price: 9.75}, + {ID: 2, Visits: 0, Balance: -1, Active: false, Ratio: -1.5, Score: 0, Note: "", Name: "bob smith", Age: 42, Rank: -7, Weight: -1.5, Price: 0}, } func fixturePath(name string) string { @@ -65,6 +65,10 @@ func stored(e testusers.EncryptedProfile) map[string][]byte { "score": e.Score, "note": e.Note, "name": e.Name, + "age": e.Age, + "rank": e.Rank, + "weight": e.Weight, + "price": e.Price, } } @@ -85,6 +89,10 @@ func TestV4FieldsSealToTheirEQLTypesAndOpenBack(t *testing.T) { "score": "c,i,v", "note": "c,i,v", "name": "bf,c,i,v", + "age": "c,hm,i,v", + "rank": "c,hm,i,v", + "weight": "c,hm,i,v", + "price": "c,hm,i,v", } for i, e := range encrypted { for column, value := range stored(e) { @@ -145,6 +153,22 @@ func TestV4QueriesMatchTheRustFixture(t *testing.T) { Plaintext string Query string } + Age struct { + Plaintext int32 + Query string + } + Rank struct { + Plaintext int64 + Query string + } + Weight struct { + Plaintext float32 + Query string + } + Price struct { + Plaintext float64 + Query string + } } if err := json.Unmarshal(raw, &fixture); err != nil { t.Fatal(err) @@ -162,6 +186,33 @@ func TestV4QueriesMatchTheRustFixture(t *testing.T) { if string(name.Query()) != fixture.Name.Query { t.Errorf("Name.Matches = %s\nwant %s", name.Query(), fixture.Name.Query) } + equality := []struct { + field string + query func() ([]byte, error) + want string + }{ + {"Age", func() ([]byte, error) { + return testusers.ProfileFields.Age.Eq(ctx, cipher, fixture.Age.Plaintext) + }, fixture.Age.Query}, + {"Rank", func() ([]byte, error) { + return testusers.ProfileFields.Rank.Eq(ctx, cipher, fixture.Rank.Plaintext) + }, fixture.Rank.Query}, + {"Weight", func() ([]byte, error) { + return testusers.ProfileFields.Weight.Eq(ctx, cipher, fixture.Weight.Plaintext) + }, fixture.Weight.Query}, + {"Price", func() ([]byte, error) { + return testusers.ProfileFields.Price.Eq(ctx, cipher, fixture.Price.Plaintext) + }, fixture.Price.Query}, + } + for _, eq := range equality { + got, err := eq.query() + if err != nil { + t.Fatal(err) + } + if eq.want == "" || string(got) != eq.want { + t.Errorf("%s.Eq = %s\nwant %s", eq.field, got, eq.want) + } + } // And a stored value holds every filter bit of a query for text it // contains, which is what `@@` tests, and not those of text it lacks. @@ -253,6 +304,10 @@ type fixturePlain struct { Score float64 `json:"score"` Note string `json:"note"` Name string `json:"name"` + Age int32 `json:"age"` + Rank int64 `json:"rank"` + Weight float32 `json:"weight"` + Price float64 `json:"price"` } // TestV4RowsMatchTheCommittedFixture checks the committed v4_rows.json @@ -282,7 +337,7 @@ func TestV4RowsMatchTheCommittedFixture(t *testing.T) { p := profiles[i] row := fixtureRow{ ID: p.ID, - Plaintext: fixturePlain{p.Visits, p.Balance, p.Active, p.Ratio, p.Score, p.Note, p.Name}, + Plaintext: plainOf(p), Stored: map[string]json.RawMessage{}, } for column, value := range stored(e) { @@ -337,6 +392,10 @@ func TestV4RowsMatchTheCommittedFixture(t *testing.T) { Score: []byte(row.Stored["score"]), Note: []byte(row.Stored["note"]), Name: []byte(row.Stored["name"]), + Age: []byte(row.Stored["age"]), + Rank: []byte(row.Stored["rank"]), + Weight: []byte(row.Stored["weight"]), + Price: []byte(row.Stored["price"]), } } opened, err := testusers.DecryptProfile(context.Background(), c.DefaultKeyset(), sealed) @@ -344,14 +403,17 @@ func TestV4RowsMatchTheCommittedFixture(t *testing.T) { t.Fatal(err) } for i, row := range committed.Rows { - p := opened[i] - got := fixturePlain{p.Visits, p.Balance, p.Active, p.Ratio, p.Score, p.Note, p.Name} - if got != row.Plaintext { + if got := plainOf(opened[i]); got != row.Plaintext { t.Errorf("row %d opens to %+v, want %+v", row.ID, got, row.Plaintext) } } } +// plainOf is a profile's plaintext as the fixture records it. +func plainOf(p testusers.Profile) fixturePlain { + return fixturePlain{p.Visits, p.Balance, p.Active, p.Ratio, p.Score, p.Note, p.Name, p.Age, p.Rank, p.Weight, p.Price} +} + // withoutCiphertexts renders a fixture with every stored value's `c` // removed: the part of the file that is the same on every run. func withoutCiphertexts(t *testing.T, f rowsFixture) []byte { diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 78fa33500..189086c89 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -709,7 +709,6 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", - "vitaminc-aead 0.6.0", "zeroize", ] diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 7613d9cfc..28ab41ca4 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -189,13 +189,17 @@ mod tests { producible, [ "Integer", + "IntegerEq", "Bigint", + "BigintEq", "Text", "TextEq", "TextMatch", "Boolean", "Real", - "Double" + "RealEq", + "Double", + "DoubleEq" ], "the engine's producible types, in catalog order" ); diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index 3ea30c155..da9ba5afc 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -1371,13 +1371,17 @@ fn targets_lists_what_the_build_holds() { producible, [ "Integer", + "IntegerEq", "Bigint", + "BigintEq", "Text", "TextEq", "TextMatch", "Boolean", "Real", - "Double" + "RealEq", + "Double", + "DoubleEq" ], "the engine's producible types, in catalog order" ); @@ -1772,7 +1776,7 @@ fn the_catalog_lists_each_types_operators() { assert_eq!(op, want, "{name}"); }; match name.as_str() { - "TextEq" => only([ + "TextEq" | "IntegerEq" | "BigintEq" | "RealEq" | "DoubleEq" => only([ ("operator", "eq"), ("fidelity", "exact"), ("arity", "one"), diff --git a/languages/golang/encrypt/internal/testusers/profile_stash.go b/languages/golang/encrypt/internal/testusers/profile_stash.go index 7254e0db8..5d3e4a48e 100644 --- a/languages/golang/encrypt/internal/testusers/profile_stash.go +++ b/languages/golang/encrypt/internal/testusers/profile_stash.go @@ -26,14 +26,18 @@ type EncryptedProfile struct { Score eql.Double Note eql.Text Name eql.TextMatch + Age eql.IntegerEq + Rank eql.BigintEq + Weight eql.RealEq + Price eql.DoubleEq } func (e EncryptedProfile) String() string { - return gensupport.Redacted("EncryptedProfile", map[string]any{"ID": e.ID}, "Visits", "Balance", "Active", "Ratio", "Score", "Note", "Name") + return gensupport.Redacted("EncryptedProfile", map[string]any{"ID": e.ID}, "Visits", "Balance", "Active", "Ratio", "Score", "Note", "Name", "Age", "Rank", "Weight", "Price") } func (e EncryptedProfile) LogValue() slog.Value { - return gensupport.RedactedLog(map[string]any{"ID": e.ID}, "Visits", "Balance", "Active", "Ratio", "Score", "Note", "Name") + return gensupport.RedactedLog(map[string]any{"ID": e.ID}, "Visits", "Balance", "Active", "Ratio", "Score", "Note", "Name", "Age", "Rank", "Weight", "Price") } // Stops compiling when Profile gains, loses, reorders or retypes a field. @@ -49,6 +53,10 @@ type profileShape struct { Score float64 Note string Name string + Age int32 + Rank int64 + Weight float32 + Price float64 } var profileDeclaration = gensupport.Declare("profiles"). @@ -59,7 +67,11 @@ var profileDeclaration = gensupport.Declare("profiles"). EncryptInto("ratio", gensupport.Float32, "Real"). EncryptInto("score", gensupport.Float64, "Double"). EncryptInto("note", gensupport.String, "Text"). - EncryptInto("name", gensupport.String, "TextMatch") + EncryptInto("name", gensupport.String, "TextMatch"). + EncryptInto("age", gensupport.Int32, "IntegerEq"). + EncryptInto("rank", gensupport.Int64, "BigintEq"). + EncryptInto("weight", gensupport.Float32, "RealEq"). + EncryptInto("price", gensupport.Float64, "DoubleEq") var profileCodec = gensupport.New(gensupport.Generated[Profile, EncryptedProfile]{ TypeName: "Profile", @@ -75,6 +87,10 @@ var profileCodec = gensupport.New(gensupport.Generated[Profile, EncryptedProfile "score": v.Score, "note": v.Note, "name": v.Name, + "age": v.Age, + "rank": v.Rank, + "weight": v.Weight, + "price": v.Price, } }, Seal: func(rec gensupport.Record) (EncryptedProfile, error) { @@ -90,6 +106,10 @@ var profileCodec = gensupport.New(gensupport.Generated[Profile, EncryptedProfile e.Score = eql.Double(rec["score"].EQL) e.Note = eql.Text(rec["note"].EQL) e.Name = eql.TextMatch(rec["name"].EQL) + e.Age = eql.IntegerEq(rec["age"].EQL) + e.Rank = eql.BigintEq(rec["rank"].EQL) + e.Weight = eql.RealEq(rec["weight"].EQL) + e.Price = eql.DoubleEq(rec["price"].EQL) return e, nil }, Open: func(e EncryptedProfile) gensupport.Record { @@ -102,6 +122,10 @@ var profileCodec = gensupport.New(gensupport.Generated[Profile, EncryptedProfile "score": {EQL: e.Score}, "note": {EQL: e.Note}, "name": {EQL: e.Name}, + "age": {EQL: e.Age}, + "rank": {EQL: e.Rank}, + "weight": {EQL: e.Weight}, + "price": {EQL: e.Price}, } }, Value: func(e EncryptedProfile, vals gensupport.Values) (Profile, error) { @@ -131,6 +155,18 @@ var profileCodec = gensupport.New(gensupport.Generated[Profile, EncryptedProfile if v.Name, err = gensupport.Get[string](vals, "name"); err != nil { return Profile{}, err } + if v.Age, err = gensupport.Get[int32](vals, "age"); err != nil { + return Profile{}, err + } + if v.Rank, err = gensupport.Get[int64](vals, "rank"); err != nil { + return Profile{}, err + } + if v.Weight, err = gensupport.Get[float32](vals, "weight"); err != nil { + return Profile{}, err + } + if v.Price, err = gensupport.Get[float64](vals, "price"); err != nil { + return Profile{}, err + } return v, nil }, }) @@ -156,6 +192,10 @@ var ProfileFields = struct { Score ProfileScoreField Note ProfileNoteField Name ProfileNameField + Age ProfileAgeField + Rank ProfileRankField + Weight ProfileWeightField + Price ProfilePriceField }{ Visits: ProfileVisitsField{gensupport.NewField[int32](profileDeclaration, "visits")}, Balance: ProfileBalanceField{gensupport.NewField[int64](profileDeclaration, "balance")}, @@ -164,6 +204,10 @@ var ProfileFields = struct { Score: ProfileScoreField{gensupport.NewField[float64](profileDeclaration, "score")}, Note: ProfileNoteField{gensupport.NewField[string](profileDeclaration, "note")}, Name: ProfileNameField{gensupport.NewField[string](profileDeclaration, "name")}, + Age: ProfileAgeField{gensupport.NewField[int32](profileDeclaration, "age")}, + Rank: ProfileRankField{gensupport.NewField[int64](profileDeclaration, "rank")}, + Weight: ProfileWeightField{gensupport.NewField[float32](profileDeclaration, "weight")}, + Price: ProfilePriceField{gensupport.NewField[float64](profileDeclaration, "price")}, } type ProfileVisitsField struct { @@ -234,3 +278,59 @@ func (f ProfileNameField) Encrypt(ctx context.Context, c *encrypt.Cipher, v stri func (f ProfileNameField) Matches(ctx context.Context, c *encrypt.Cipher, v string) (encrypt.EQLMatchQuery[eql.TextMatchQuery, string], error) { return gensupport.EQLMatches[eql.TextMatchQuery](ctx, f.field, c, v) } + +type ProfileAgeField struct { + field gensupport.Field[int32] +} + +func (f ProfileAgeField) Encrypt(ctx context.Context, c *encrypt.Cipher, v int32) (eql.IntegerEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.IntegerEq(out.EQL), err +} + +func (f ProfileAgeField) Eq(ctx context.Context, c *encrypt.Cipher, v int32) (eql.IntegerEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.IntegerEqQuery(term), err +} + +type ProfileRankField struct { + field gensupport.Field[int64] +} + +func (f ProfileRankField) Encrypt(ctx context.Context, c *encrypt.Cipher, v int64) (eql.BigintEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.BigintEq(out.EQL), err +} + +func (f ProfileRankField) Eq(ctx context.Context, c *encrypt.Cipher, v int64) (eql.BigintEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.BigintEqQuery(term), err +} + +type ProfileWeightField struct { + field gensupport.Field[float32] +} + +func (f ProfileWeightField) Encrypt(ctx context.Context, c *encrypt.Cipher, v float32) (eql.RealEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.RealEq(out.EQL), err +} + +func (f ProfileWeightField) Eq(ctx context.Context, c *encrypt.Cipher, v float32) (eql.RealEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.RealEqQuery(term), err +} + +type ProfilePriceField struct { + field gensupport.Field[float64] +} + +func (f ProfilePriceField) Encrypt(ctx context.Context, c *encrypt.Cipher, v float64) (eql.DoubleEq, error) { + out, err := f.field.Encrypt(ctx, c, v) + return eql.DoubleEq(out.EQL), err +} + +func (f ProfilePriceField) Eq(ctx context.Context, c *encrypt.Cipher, v float64) (eql.DoubleEqQuery, error) { + term, err := f.field.Term(ctx, c, "eq", v) + return eql.DoubleEqQuery(term), err +} diff --git a/languages/golang/encrypt/internal/testusers/profiles.go b/languages/golang/encrypt/internal/testusers/profiles.go index ab5a07c1d..35ccdffb0 100644 --- a/languages/golang/encrypt/internal/testusers/profiles.go +++ b/languages/golang/encrypt/internal/testusers/profiles.go @@ -16,4 +16,8 @@ type Profile struct { Score float64 `stash:"score,encrypt_into=Double"` Note string `stash:"note,encrypt_into=Text"` Name string `stash:"name,encrypt_into=TextMatch"` + Age int32 `stash:"age,encrypt_into=IntegerEq"` + Rank int64 `stash:"rank,encrypt_into=BigintEq"` + Weight float32 `stash:"weight,encrypt_into=RealEq"` + Price float64 `stash:"price,encrypt_into=DoubleEq"` } diff --git a/languages/golang/encrypt/query_test.go b/languages/golang/encrypt/query_test.go index 2313cea43..a5219f327 100644 --- a/languages/golang/encrypt/query_test.go +++ b/languages/golang/encrypt/query_test.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -389,10 +390,10 @@ func TestTheCatalogListsEachTypesOperators(t *testing.T) { } for _, target := range targets { switch { - case target.Name == "TextEq": + case slices.Contains([]string{"TextEq", "IntegerEq", "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("TextEq answers %+v, want %+v", target.Operators, want) + t.Fatalf("%s answers %+v, want %+v", target.Name, target.Operators, want) } case target.Name == "TextMatch": want := record.Operator{Name: "matches", Fidelity: record.Probabilistic, Index: record.Match} diff --git a/languages/golang/stashgen/refusal_test.go b/languages/golang/stashgen/refusal_test.go index a5e4938b5..13c3e5de4 100644 --- a/languages/golang/stashgen/refusal_test.go +++ b/languages/golang/stashgen/refusal_test.go @@ -92,7 +92,7 @@ func TestRefusals(t *testing.T) { {"an opaque struct with an interface", user("\t_ struct{} `stash:\"context=users,opaque\"`\n\tAny any"), stashgen.Request{Type: "User"}, "Any", "is not a type JSON carries"}, {"an opaque struct with a struct JSON would truncate", user("\t_ struct{} `stash:\"context=users,opaque\"`\n\tTx hidden") + "\ntype hidden struct {\n\tA int\n\tb int\n}\n", stashgen.Request{Type: "User"}, "Tx", "JSON would drop"}, {"an opaque struct with a map keyed by a struct", user("\t_ struct{} `stash:\"context=users,opaque\"`\n\tM map[hidden]int") + "\ntype hidden struct{ A int }\n", stashgen.Request{Type: "User"}, "M", "map key that is not a string or an integer"}, - {"an EQL type the engine cannot produce yet", user(ctx + "\tAge int32 `stash:\"age,encrypt_into=IntegerEq\"`"), stashgen.Request{Type: "User"}, "Age", "has no EQL type IntegerEq"}, + {"an EQL type the engine cannot produce yet", user(ctx + "\tAge int32 `stash:\"age,encrypt_into=IntegerOrd\"`"), stashgen.Request{Type: "User"}, "Age", "has no EQL type IntegerOrd"}, {"an index on a composite", user(ctx + "\tAttrs map[string]string `stash:\"attrs,encrypt,index=equality\"`"), stashgen.Request{Type: "User"}, "Attrs", "seals only as part of an opaque struct"}, {"the json index, not in the engine yet", user(ctx + "\tAttrs string `stash:\"attrs,index=json\"`"), stashgen.Request{Type: "User"}, "Attrs", "cannot derive the json index yet"}, {"an index option a query term cannot use", user(ctx + "\tEmail string `stash:\"email,encrypt,index=match(k=3)\"`"), stashgen.Request{Type: "User"}, "Email", "a query term uses only the default index options"}, diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index 34ebc4188..cadd1da48 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -1204,7 +1204,6 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", - "vitaminc-aead 0.6.0", "zeroize", ] diff --git a/packages/eql/crates/eql-bindings/CHANGELOG.md b/packages/eql/crates/eql-bindings/CHANGELOG.md index 4a55f2387..ca66bece0 100644 --- a/packages/eql/crates/eql-bindings/CHANGELOG.md +++ b/packages/eql/crates/eql-bindings/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`eql_bindings::v4` needs the `stack-encrypt` feature.** Only Stack + Encrypt produces an EQL v4 value (ADR-0002), so the module is compiled with + that feature alone, and its producible types derive `EncryptFrom` and + `DecryptInto` with no attribute of their own gated on it. +- **`IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq` are producible** + (`stack-encrypt` feature), from `i32`, `i64`, `f32` and `f64`. Their equality term follows ADR-0002's semantic + 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. - **`Target::match_options`** (`stack-encrypt` 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/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 90abbd35f..04be3fc3d 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -47,10 +47,6 @@ schemars = "1" # target takes and the kind names a plan's `"type"` key spells # (`encryption::targets`). stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false, features = ["dynamic"] } -# `Physical`, `Int`, `Float` and `FromScalar`: the scalar plaintexts in -# `encryption::plaintext` describe themselves through vitaminc's `Encrypt` -# and read back only their own width through `FromScalar`. -vitaminc-aead = { version = "0.6.0", optional = true } # The plaintext a target reads out of its `Protected` runtime value is held # in `Zeroizing` until it is sealed, so the copy is wiped like the original. zeroize = { version = "1", optional = true } @@ -62,7 +58,6 @@ hex = { version = "0.4", optional = true } default = [] stack-encrypt = [ "dep:stack-encrypt", - "dep:vitaminc-aead", "dep:zeroize", "dep:thiserror", "dep:base64", diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index 7f0a7af09..5115be0a5 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -106,25 +106,23 @@ The optional `stack-encrypt` feature enables `EncryptFrom` for `v4::text::TextEq`. Stack Encrypt writes EQL v4 only, so no `v3` type implements these traits. The derives are emitted by `eql-codegen`. -The other v4 types Stack Encrypt produces take a plaintext from -`eql_bindings::encryption::plaintext`, which seal as the primitive they wrap -and read back only a value of their own width: +The other v4 types Stack Encrypt produces take a Rust primitive as their +plaintext: | v4 type | Plaintext | | --- | --- | | `Text`, `TextMatch` | `String` | -| `Integer` | `Int32` | -| `Bigint` | `Int64` | -| `Boolean` | `Bool` | -| `Real` | `Float32` | -| `Double` | `Float64` | +| `Integer`, `IntegerEq` | `i32` | +| `Bigint`, `BigintEq` | `i64` | +| `Boolean` | `bool` | +| `Real`, `RealEq` | `f32` | +| `Double`, `DoubleEq` | `f64` | Each seals the leaf the value enum (`FfiValue`) seals for the same value, so -values written here and by the Go SDK open each other. `IntegerEq`, -`BigintEq`, `RealEq` and `DoubleEq` wait for vitaminc 0.6's release: the -engine derives integer and float equality in its shared domains, and terms -derived under vitaminc 0.5's per-width domains would stop matching after the -upgrade. +values written here and by the Go SDK open each other. On the equality +types, `=` behaves as Postgres's own does: an integer has one term whatever +its width, and for floats `-0.0 = 0.0` and `NaN = NaN`. The ciphertext keeps +the exact value. `TextMatch` builds its filter under a configuration pinned in this crate (`EqlMatch`: 3-grams of the lower-cased text, `k = 6`, a 2048-bit filter) for stored values and queries alike. The configuration is part of the stored @@ -134,9 +132,10 @@ where that matters. Write the stored column on the left, `col @@ query`: the other order finds nothing until #1171 is fixed. Text too short to make a 3-gram (empty strings, two-letter codes) cannot be stored in a `TextMatch` 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. Ordering -types wait for vitaminc 0.6's order encodings, and `Json` for a decision on -sealing a document as one leaf. +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. 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.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index c8f6a3e07..76c1a1f85 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -80,7 +80,6 @@ //! require::(); //! ``` -pub mod plaintext; pub mod targets; use base64::{engine::general_purpose::STANDARD, Engine as _}; diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs deleted file mode 100644 index 173681f07..000000000 --- a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Scalar plaintexts for the v4 number and boolean types. -//! -//! A Stack Encrypt target encrypts a plaintext through Vitamin C's -//! `Encrypt` and reads it back through `Decrypt`. These newtypes name the -//! plaintext of each storage-only EQL v4 type: [`Int32`] for `Integer`, -//! [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and [`Float32`] / -//! [`Float64`] for `Real` and `Double`. -//! -//! Each describes itself as the primitive it wraps, so it seals the leaf -//! vitaminc seals for that primitive, which is the leaf a data plan's field of -//! the same type seals. So a value written through these types and one -//! written by the Go SDK as a plain `int32` field open each other. -//! -//! Each reads back only its own width. vitaminc's primitive targets convert -//! an integer of any width that fits, and widen an `f32` to an `f64`; these -//! refuse a leaf of another width, so a column's type is the one it was -//! written with. -//! -//! None of them derives an equality term. `IntegerEq`, `BigintEq`, `RealEq` -//! and `DoubleEq` are held back until vitaminc 0.6 is released: the engine -//! already derives integer and float equality in 0.6's shared domains, and a -//! published build must not derive them under vitaminc 0.5's. -//! -//! ```rust,ignore -//! use eql_bindings::encryption::plaintext::Int32; -//! use eql_bindings::v4::integer::Integer; -//! -//! let stored: Integer = keyset.encrypt_as(&Int32(42), column).await?; -//! let opened: Int32 = cipher.decrypt_as(stored, expected).await?; -//! ``` -//! -//! None of them implements `Debug`, so a plaintext does not reach a log by -//! accident. - -use stack_encrypt::Encrypt; -use vitaminc_aead::{Float, FromScalar, Int, Logical, Physical, Sink}; -use zeroize::Zeroize; - -macro_rules! fixed_scalar { - ($(#[$doc:meta])* $name:ident($inner:ty), $read:pat => $value:expr, |$a:ident, $b:ident| $eq:expr) => { - $(#[$doc])* - #[derive(Clone, Copy, Zeroize)] - pub struct $name(pub $inner); - - /// Equal when the sealed payloads would be: by value for an integer, - /// by bit pattern for a float, so `-0.0 != 0.0` and a NaN equals - /// itself. That is the equality a round trip preserves. - impl PartialEq for $name { - fn eq(&self, other: &Self) -> bool { - let ($a, $b) = (self.0, other.0); - $eq - } - } - - impl Eq for $name {} - - impl From<$inner> for $name { - fn from(value: $inner) -> Self { - Self(value) - } - } - - impl From<$name> for $inner { - fn from(value: $name) -> Self { - value.0 - } - } - - /// Describes itself as the primitive it wraps. - impl Encrypt for $name { - fn visit(self, sink: S) -> Result { - self.0.visit(sink) - } - } - - /// Reads back a leaf of exactly this kind and width, and nothing - /// else: a value sealed as one kind never opens as another. - impl FromScalar for $name { - const EXPECTED: &'static str = stringify!($inner); - fn from_scalar(ty: Option, value: Physical<'_>) -> Option { - match (ty, value) { - (None, $read) => Some($name($value)), - _ => None, - } - } - } - }; -} - -fixed_scalar!( - /// The plaintext of `Integer`: a 32-bit signed integer. - Int32(i32), Physical::Int(Int::I32(v)) => v, |a, b| a == b -); -fixed_scalar!( - /// The plaintext of `Bigint`: a 64-bit signed integer. - Int64(i64), Physical::Int(Int::I64(v)) => v, |a, b| a == b -); -fixed_scalar!( - /// The plaintext of `Real`: a 32-bit float, sealed by its bit pattern, so - /// `-0.0` and a NaN's payload survive a round trip. - Float32(f32), Physical::Float(Float::F32(v)) => v, |a, b| a.to_bits() == b.to_bits() -); -fixed_scalar!( - /// The plaintext of `Double`: a 64-bit float, sealed by its bit pattern. - Float64(f64), Physical::Float(Float::F64(v)) => v, |a, b| a.to_bits() == b.to_bits() -); - -/// The plaintext of `Boolean`. -#[derive(Clone, Copy, PartialEq, Eq, Zeroize)] -pub struct Bool(pub bool); - -impl From for Bool { - fn from(value: bool) -> Self { - Self(value) - } -} - -impl From for bool { - fn from(value: Bool) -> Self { - value.0 - } -} - -/// Describes itself as a `bool`. -impl Encrypt for Bool { - fn visit(self, sink: S) -> Result { - self.0.visit(sink) - } -} - -/// Reads back a boolean leaf, and nothing else. -impl FromScalar for Bool { - const EXPECTED: &'static str = "bool"; - fn from_scalar(ty: Option, value: Physical<'_>) -> Option { - match (ty, value) { - (None, Physical::Bool(b)) => Some(Bool(b)), - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - //! What each type reads back. Sealing is the primitive's own; what is - //! written here is the reading, so these pin that each type takes only - //! its own kind and width. The round trip against the value enum - //! (`FfiValue`) is in the encryption test crate - //! (`tests/encryption/tests/v4_types.rs`). - use super::*; - - fn read(value: Physical<'_>) -> Option { - T::from_scalar(None, value) - } - - #[test] - fn integers_read_their_own_width() { - for v in [0, 1, -1, 42, i32::MIN, i32::MAX] { - assert!(read::(Physical::Int(Int::I32(v))) == Some(Int32(v))); - } - for v in [0, -1, 42, i64::MIN, i64::MAX] { - assert!(read::(Physical::Int(Int::I64(v))) == Some(Int64(v))); - } - } - - #[test] - fn a_scalar_of_another_kind_or_width_is_refused() { - // An Int64 leaf is not an Int32, even when its value would fit. - assert!(read::(Physical::Int(Int::I64(1))).is_none()); - assert!(read::(Physical::Int(Int::I32(1))).is_none()); - assert!(read::(Physical::Int(Int::U32(1))).is_none()); - // A float's leaf is not an integer's, nor a boolean's. - assert!(read::(Physical::Float(Float::F32(1.0))).is_none()); - assert!(read::(Physical::Int(Int::I32(1))).is_none()); - // An f32 leaf is not an f64, though it would widen exactly. - assert!(read::(Physical::Float(Float::F32(1.0))).is_none()); - assert!(read::(Physical::Float(Float::F64(1.0))).is_none()); - // A logical type on the leaf is not the bare scalar. - assert!(Int32::from_scalar(Some(Logical::Date), Physical::Int(Int::I32(1))).is_none()); - } - - #[test] - fn floats_read_their_bit_pattern() { - let nan32 = f32::from_bits(0x7fc0_1234); - for v in [0.0, -0.0, 1.5, f32::INFINITY, f32::NEG_INFINITY, nan32] { - let opened = read::(Physical::Float(Float::F32(v))).unwrap(); - assert_eq!(opened.0.to_bits(), v.to_bits()); - } - let nan64 = f64::from_bits(0x7ff8_0000_0000_beef); - for v in [0.0, -0.0, 1.5, f64::INFINITY, nan64] { - let opened = read::(Physical::Float(Float::F64(v))).unwrap(); - assert_eq!(opened.0.to_bits(), v.to_bits()); - } - } - - #[test] - fn booleans_read_both_values() { - assert!(read::(Physical::Bool(true)) == Some(Bool(true))); - assert!(read::(Physical::Bool(false)) == Some(Bool(false))); - assert!(read::(Physical::Null).is_none()); - } - - #[test] - fn float_equality_is_by_bit_pattern() { - assert!(Float32(-0.0) != Float32(0.0)); - assert!(Float64(-0.0) != Float64(0.0)); - assert!(Float32(f32::NAN) == Float32(f32::NAN)); - assert!(Float64(f64::NAN) == Float64(f64::NAN)); - assert!(Float64(f64::from_bits(0x7ff8_0000_0000_0001)) != Float64(f64::NAN)); - assert!(Int32(7) == Int32(7) && Int64(7) != Int64(8)); - } -} diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index f8d7b77fb..8c18610e6 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -91,7 +91,6 @@ use stack_encrypt::{ DecryptInto, EncryptFrom, Error, KeysetCipher, Label, NonEmpty, Pending, StackCipher, }; -use super::plaintext; use crate::v4::targets::{decrypt_named, encrypt_named, query_named, TARGETS}; use crate::Identifier; @@ -402,15 +401,13 @@ pub(crate) fn refuse(name: &str) -> TargetError { } mod sealed { - use super::plaintext::{Bool, Float32, Float64, Int32, Int64}; - pub trait Sealed {} impl Sealed for String {} - impl Sealed for Int32 {} - impl Sealed for Int64 {} - impl Sealed for Bool {} - impl Sealed for Float32 {} - impl Sealed for Float64 {} + impl Sealed for i32 {} + impl Sealed for i64 {} + impl Sealed for bool {} + impl Sealed for f32 {} + impl Sealed for f64 {} } /// A Rust plaintext an EQL type is produced from, read out of and written @@ -460,12 +457,12 @@ impl Plaintext for String { /// the declared kind's. macro_rules! scalar_plaintext { ($($ty:ident => $variant:ident),+ $(,)?) => {$( - impl Plaintext for plaintext::$ty { + impl Plaintext for $ty { const KIND: ValueKind = ValueKind::$variant; fn from_value(target: &'static str, value: FfiValue) -> Result { match value { - FfiValue::$variant(v) => Ok(plaintext::$ty(v)), + FfiValue::$variant(v) => Ok(v), other => Err(TargetError::Plaintext { target, expected: Self::KIND, @@ -475,17 +472,17 @@ macro_rules! scalar_plaintext { } fn into_value(self) -> FfiValue { - FfiValue::$variant(self.0) + FfiValue::$variant(self) } } )+}; } scalar_plaintext!( - Int32 => Int32, - Int64 => Int64, - Bool => Bool, - Float32 => Float32, - Float64 => Float64, + i32 => Int32, + i64 => Int64, + bool => Bool, + f32 => Float32, + f64 => Float64, ); /// Run a target's own plan over one runtime value and resolve to the EQL @@ -629,7 +626,7 @@ mod tests { assert!(matches!(refuse("Nope"), TargetError::Unknown { name } if name == "Nope")); assert!(matches!( refuse("TextOrdOre"), - TargetError::Unproducible { name: "TextOrdOre", reason } if reason.contains("vitaminc-ore") + TargetError::Unproducible { name: "TextOrdOre", reason } if reason.contains("block ORE") )); assert!(producible("TextEq").is_ok()); } diff --git a/packages/eql/crates/eql-bindings/src/lib.rs b/packages/eql/crates/eql-bindings/src/lib.rs index bd5c61b50..5b1e7249e 100644 --- a/packages/eql/crates/eql-bindings/src/lib.rs +++ b/packages/eql/crates/eql-bindings/src/lib.rs @@ -44,6 +44,12 @@ use ts_rs::TS; pub mod from_v2; pub mod sql; pub mod v3; +// EQL v4 holds the payloads Stack Encrypt writes (ADR-0002). Only Stack +// Encrypt produces a v4 value, so the module exists only with the +// `stack-encrypt` feature, and its types derive Stack Encrypt's traits +// unconditionally. (A `///` doc here would move `v4`'s own intra-doc links +// out of its scope.) +#[cfg(feature = "stack-encrypt")] pub mod v4; #[cfg(feature = "stack-encrypt")] diff --git a/packages/eql/crates/eql-bindings/src/v4/bigint.rs b/packages/eql/crates/eql-bindings/src/v4/bigint.rs index 6059c0931..9d1009024 100644 --- a/packages/eql/crates/eql-bindings/src/v4/bigint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/bigint.rs @@ -14,35 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Int64`](crate::encryption::plaintext::Int64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `Int64`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int64) -)] +/// +/// Plaintext input and decrypted output: [`i64`](i64). 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 = i64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Bigint { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -80,10 +63,18 @@ impl DomainType for Bigint { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`i64`](i64). 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 = i64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEq { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -248,10 +239,18 @@ impl DomainType for BigintOrdOpe { doc, doc = " See the [EQL bigint reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input: [`i64`](i64). 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 = i64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct BigintEqQuery { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/boolean.rs b/packages/eql/crates/eql-bindings/src/v4/boolean.rs index be25bdbf2..02be819d4 100644 --- a/packages/eql/crates/eql-bindings/src/v4/boolean.rs +++ b/packages/eql/crates/eql-bindings/src/v4/boolean.rs @@ -14,35 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL boolean reference](https://cipherstash.com/docs/reference/eql/booleans) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Bool`](crate::encryption::plaintext::Bool). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `Bool`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Bool) -)] +/// +/// Plaintext input and decrypted output: [`bool`](bool). 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 = bool)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Boolean { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/double.rs b/packages/eql/crates/eql-bindings/src/v4/double.rs index ee52d3ce3..f20074728 100644 --- a/packages/eql/crates/eql-bindings/src/v4/double.rs +++ b/packages/eql/crates/eql-bindings/src/v4/double.rs @@ -14,35 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL double reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Float64`](crate::encryption::plaintext::Float64). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `Float64`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Float64) -)] +/// +/// Plaintext input and decrypted output: [`f64`](f64). 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 = f64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Double { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -80,10 +63,18 @@ impl DomainType for Double { doc, doc = " See the [EQL double reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`f64`](f64). 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 = f64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct DoubleEq { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -248,10 +239,18 @@ impl DomainType for DoubleOrdOpe { doc, doc = " See the [EQL double reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input: [`f64`](f64). 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 = f64)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct DoubleEqQuery { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/integer.rs b/packages/eql/crates/eql-bindings/src/v4/integer.rs index 508595305..c6b0037dc 100644 --- a/packages/eql/crates/eql-bindings/src/v4/integer.rs +++ b/packages/eql/crates/eql-bindings/src/v4/integer.rs @@ -14,35 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Int32`](crate::encryption::plaintext::Int32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `Int32`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Int32) -)] +/// +/// Plaintext input and decrypted output: [`i32`](i32). 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 = i32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Integer { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -80,10 +63,18 @@ impl DomainType for Integer { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`i32`](i32). 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 = i32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEq { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -248,10 +239,18 @@ impl DomainType for IntegerOrdOpe { doc, doc = " See the [EQL integer reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input: [`i32`](i32). 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 = i32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct IntegerEqQuery { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub hm: Hmac256, } diff --git a/packages/eql/crates/eql-bindings/src/v4/mod.rs b/packages/eql/crates/eql-bindings/src/v4/mod.rs index 8fca2436d..2714884b1 100644 --- a/packages/eql/crates/eql-bindings/src/v4/mod.rs +++ b/packages/eql/crates/eql-bindings/src/v4/mod.rs @@ -6,7 +6,8 @@ //! [`crate::v3`], with `eql_v4_*` domain names and a `v` of `4`. //! //! **Only these structs derive Stack Encrypt's traits** (`EncryptFrom`, -//! `DecryptInto`, behind the `stack-encrypt` feature). Stack Encrypt's output +//! `DecryptInto`). Only Stack Encrypt produces a v4 value, so this module +//! exists only with the `stack-encrypt` feature. Stack Encrypt's output //! is not byte-for-byte compatible with cipherstash-client's, so no `eql_v3` //! struct claims it. Which v4 types derive them is //! `eql-codegen`'s `ENCRYPTION_DOMAINS`; the rest say why in @@ -33,11 +34,8 @@ pub mod real; pub mod smallint; /// Generated: the table of EQL types a Stack Encrypt data plan may name as /// a field target (`TARGETS`) and the by-name dispatch that runs a -/// producible type's own plan. Gated here, in the hand-written module list, -/// rather than inside the generated file. The descriptor type, the errors -/// and the public entry points are hand-written in -/// [`crate::encryption::targets`]. -#[cfg(feature = "stack-encrypt")] +/// producible type's own plan. The descriptor type, the errors and the +/// public entry points are hand-written in [`crate::encryption::targets`]. pub mod targets; pub mod text; pub mod timestamp; diff --git a/packages/eql/crates/eql-bindings/src/v4/real.rs b/packages/eql/crates/eql-bindings/src/v4/real.rs index 01f12b7da..e6d316bc6 100644 --- a/packages/eql/crates/eql-bindings/src/v4/real.rs +++ b/packages/eql/crates/eql-bindings/src/v4/real.rs @@ -14,35 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL real reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`Float32`](crate::encryption::plaintext::Float32). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `Float32`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr( - feature = "stack-encrypt", - stash(plaintext = crate::encryption::plaintext::Float32) -)] +/// +/// Plaintext input and decrypted output: [`f32`](f32). 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 = f32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Real { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -80,10 +63,18 @@ impl DomainType for Real { doc, doc = " See the [EQL real reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input and decrypted output: [`f32`](f32). 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 = f32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct RealEq { + #[stash(default)] pub v: SchemaVersion, + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -248,10 +239,18 @@ impl DomainType for RealOrdOpe { doc, doc = " See the [EQL real reference](https://cipherstash.com/docs/reference/eql/numbers) for SQL domain variants, operators, and query examples." )] +/// +/// Plaintext input: [`f32`](f32). 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 = f32)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct RealEqQuery { + #[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 02c37c36e..869afec83 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -1,10 +1,5 @@ // @generated by eql-codegen from the eql-domains catalog — do not edit //! The EQL types a Stack Encrypt data plan may name as a field target — every stored EQL v4 domain type in eql-domains::CATALOG order, as data a guest serializes (`TARGETS`), with the by-name dispatch that runs a producible type's own Rust plan. Generated from the catalog; the descriptor type, the errors and the public entry points stay hand-written in `crate::encryption::targets`, which documents the wire format. -use crate::encryption::plaintext::Bool; -use crate::encryption::plaintext::Float32; -use crate::encryption::plaintext::Float64; -use crate::encryption::plaintext::Int32; -use crate::encryption::plaintext::Int64; use crate::encryption::targets::{ open_target, refuse, refuse_query, run_target, Opener, Target, TargetError, }; @@ -37,10 +32,8 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("IntegerEqQuery"), query_sql_domain: Some("eql_v4.query_integer_eq"), - producible: false, - reason: Some( - "integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade", - ), + producible: true, + reason: None, }, Target { name: "IntegerOrdOre", @@ -53,7 +46,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_integer_ord_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -67,7 +60,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_integer_ord"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -81,7 +74,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_integer_ord_ope"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -95,7 +88,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -109,7 +102,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_eq"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -123,7 +116,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ore"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -137,7 +130,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -151,7 +144,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ope"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -175,10 +168,8 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("BigintEqQuery"), query_sql_domain: Some("eql_v4.query_bigint_eq"), - producible: false, - reason: Some( - "integer equality is produced once vitaminc 0.6 is released: its one integer domain is the one the engine derives in, and a term derived under vitaminc 0.5's per-width domains would stop matching after the upgrade", - ), + producible: true, + reason: None, }, Target { name: "BigintOrdOre", @@ -191,7 +182,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_bigint_ord_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -205,7 +196,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_bigint_ord"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -219,7 +210,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_bigint_ord_ope"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -233,7 +224,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -247,7 +238,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_eq"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -261,7 +252,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ore"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -275,7 +266,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -289,7 +280,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ope"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -303,7 +294,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -317,7 +308,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_eq"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -331,7 +322,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ore"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -345,7 +336,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -359,7 +350,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ope"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -373,7 +364,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -387,7 +378,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_eq"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -401,7 +392,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ore"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -415,7 +406,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -429,7 +420,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ope"), producible: false, reason: Some( - "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", + "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)", ), }, Target { @@ -479,7 +470,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -493,7 +484,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "no EQL type is built from the engine's text order terms yet (#1131)", ), }, Target { @@ -507,7 +498,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ope"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "no EQL type is built from the engine's text order terms yet (#1131)", ), }, Target { @@ -521,7 +512,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -535,7 +526,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "no EQL type is built from the engine's text order terms yet (#1131)", ), }, Target { @@ -571,10 +562,8 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("RealEqQuery"), query_sql_domain: Some("eql_v4.query_real_eq"), - producible: false, - reason: Some( - "float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002)", - ), + producible: true, + reason: None, }, Target { name: "RealOrdOre", @@ -587,7 +576,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_real_ord_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -601,7 +590,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_real_ord"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -615,7 +604,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_real_ord_ope"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -639,10 +628,8 @@ pub const TARGETS: &[Target] = &[ indexes: &["eq"], query: Some("DoubleEqQuery"), query_sql_domain: Some("eql_v4.query_double_eq"), - producible: false, - reason: Some( - "float equality is produced once vitaminc 0.6, which gives a float its canonical equality form, is released (ADR-0002)", - ), + producible: true, + reason: None, }, Target { name: "DoubleOrdOre", @@ -655,7 +642,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_double_ord_ore"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "EQL stores block ORE terms and the engine derives CLLW ORE (#1132)", ), }, Target { @@ -669,7 +656,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_double_ord"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -683,7 +670,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_double_ord_ope"), producible: false, reason: Some( - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", + "the engine does not yet derive this family's order terms over ADR-0002's order keys (#1236)", ), }, Target { @@ -696,7 +683,7 @@ pub const TARGETS: &[Target] = &[ query: Some("SteVecQuery"), query_sql_domain: Some("eql_v4.query_json"), producible: false, - reason: Some("the JSON index is a new operation in the engine"), + reason: Some("the JSON index is a new operation in the engine (#1060)"), }, Target { name: "Json", @@ -724,10 +711,16 @@ pub(crate) fn encrypt_named<'a, K: 'static>( ) -> Result, K>, TargetError> { match name { "Integer" => { - run_target::("Integer", keyset, column, plaintext) + run_target::("Integer", keyset, column, plaintext) + } + "IntegerEq" => { + run_target::("IntegerEq", keyset, column, plaintext) } "Bigint" => { - run_target::("Bigint", keyset, column, plaintext) + run_target::("Bigint", keyset, column, plaintext) + } + "BigintEq" => { + run_target::("BigintEq", keyset, column, plaintext) } "Text" => run_target::("Text", keyset, column, plaintext), "TextEq" => { @@ -737,11 +730,15 @@ pub(crate) fn encrypt_named<'a, K: 'static>( run_target::("TextMatch", keyset, column, plaintext) } "Boolean" => { - run_target::("Boolean", keyset, column, plaintext) + run_target::("Boolean", keyset, column, plaintext) } - "Real" => run_target::("Real", keyset, column, plaintext), + "Real" => run_target::("Real", keyset, column, plaintext), + "RealEq" => run_target::("RealEq", keyset, column, plaintext), "Double" => { - run_target::("Double", keyset, column, plaintext) + run_target::("Double", keyset, column, plaintext) + } + "DoubleEq" => { + run_target::("DoubleEq", keyset, column, plaintext) } _ => Err(refuse(name)), } @@ -756,10 +753,14 @@ pub(crate) fn decrypt_named<'a, K: 'static>( ) -> Result, TargetError> { match name { "Integer" => { - open_target::("Integer", opener, column, stored) + open_target::("Integer", opener, column, stored) } - "Bigint" => { - open_target::("Bigint", opener, column, stored) + "IntegerEq" => { + open_target::("IntegerEq", opener, column, stored) + } + "Bigint" => open_target::("Bigint", opener, column, stored), + "BigintEq" => { + open_target::("BigintEq", opener, column, stored) } "Text" => open_target::("Text", opener, column, stored), "TextEq" => open_target::("TextEq", opener, column, stored), @@ -767,11 +768,13 @@ pub(crate) fn decrypt_named<'a, K: 'static>( open_target::("TextMatch", opener, column, stored) } "Boolean" => { - open_target::("Boolean", opener, column, stored) + open_target::("Boolean", opener, column, stored) } - "Real" => open_target::("Real", opener, column, stored), - "Double" => { - open_target::("Double", opener, column, stored) + "Real" => open_target::("Real", opener, column, stored), + "RealEq" => open_target::("RealEq", opener, column, stored), + "Double" => open_target::("Double", opener, column, stored), + "DoubleEq" => { + open_target::("DoubleEq", opener, column, stored) } _ => Err(refuse(name)), } @@ -786,6 +789,15 @@ pub(crate) fn query_named<'a, K: 'static>( plaintext: FfiValue, ) -> Result, K>, TargetError> { match name { + "IntegerEq" => run_target::( + "IntegerEq", + keyset, + column, + plaintext, + ), + "BigintEq" => run_target::( + "BigintEq", keyset, column, plaintext, + ), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) } @@ -795,6 +807,12 @@ pub(crate) fn query_named<'a, K: 'static>( column, plaintext, ), + "RealEq" => { + run_target::("RealEq", keyset, column, plaintext) + } + "DoubleEq" => run_target::( + "DoubleEq", keyset, column, plaintext, + ), _ => Err(refuse_query(name)), } } diff --git a/packages/eql/crates/eql-bindings/src/v4/text.rs b/packages/eql/crates/eql-bindings/src/v4/text.rs index 0270227ed..2e549eca0 100644 --- a/packages/eql/crates/eql-bindings/src/v4/text.rs +++ b/packages/eql/crates/eql-bindings/src/v4/text.rs @@ -14,32 +14,18 @@ use ts_rs::TS; doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] +/// +/// Plaintext input and decrypted output: [`String`](String). 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 = String)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Text { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, } @@ -77,32 +63,18 @@ impl DomainType for Text { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] +/// +/// Plaintext input and decrypted output: [`String`](String). 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 = String)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TextEq { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub hm: Hmac256, @@ -141,32 +113,18 @@ impl DomainType for TextEq { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input and decrypted output: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input and decrypted output: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))] -#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] +/// +/// Plaintext input and decrypted output: [`String`](String). 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 = String)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TextMatch { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub c: Ciphertext, pub bf: BloomFilter, @@ -422,31 +380,18 @@ impl DomainType for TextSearch { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] +/// +/// Plaintext input: [`String`](String). 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 = String)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TextEqQuery { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub hm: Hmac256, } @@ -484,31 +429,18 @@ impl DomainType for TextEqQuery { doc, doc = " See the [EQL text reference](https://cipherstash.com/docs/reference/eql/text) for SQL domain variants, operators, and query examples." )] -#[cfg_attr(doc, doc = "")] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Plaintext input: [`String`](String). With the `stack-encrypt` feature, implements `EncryptFrom` to build match query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, not(feature = "stack-encrypt")), - doc = " Plaintext input: `String`. With the `stack-encrypt` feature, implements `EncryptFrom` to build match query terms. Query operands do not decrypt." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`." -)] -#[cfg_attr( - all(doc, feature = "stack-encrypt"), - doc = " See the [complete encryption example](crate::encryption#example)." -)] -#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] -#[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] +/// +/// Plaintext input: [`String`](String). Implements `EncryptFrom` to build match 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 = String)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TextMatchQuery { - #[cfg_attr(feature = "stack-encrypt", stash(default))] + #[stash(default)] pub v: SchemaVersion, - #[cfg_attr(feature = "stack-encrypt", stash(context_field))] + #[stash(context_field)] pub i: Identifier, pub bf: BloomFilter, } diff --git a/packages/eql/crates/eql-bindings/tests/v4_parity.rs b/packages/eql/crates/eql-bindings/tests/v4_inventory.rs similarity index 56% rename from packages/eql/crates/eql-bindings/tests/v4_parity.rs rename to packages/eql/crates/eql-bindings/tests/v4_inventory.rs index 41bdf2ae2..31bbb2471 100644 --- a/packages/eql/crates/eql-bindings/tests/v4_parity.rs +++ b/packages/eql/crates/eql-bindings/tests/v4_inventory.rs @@ -1,16 +1,18 @@ -//! The v4 counterpart of `catalog_parity.rs`: every v4 inventory entry must be -//! the catalog domain it claims, under the `eql_v4` names, and the version -//! boundary between the two generations must hold in both directions. A v3 -//! payload that a v4 type accepted would let a cipherstash-client value be -//! read as a Stack Encrypt one (ADR-0002), so these are pinned with literals -//! rather than through the helpers they check. +//! Every v4 inventory entry is the eql-domains catalog domain it claims, +//! under its `eql_v4` name and schema id, and a v4 type admits only `"v": 4`. +//! The expected names and ids are written as literals rather than through the +//! helpers they check. -use eql_bindings::{v3, v4}; +// `eql_bindings::v4` exists only with the `stack-encrypt` feature. +#![cfg(feature = "stack-encrypt")] + +use eql_bindings::v4; use eql_domains::{Term, CATALOG}; use serde_json::{json, Value}; /// The catalog's scalar domains under their v4 names, in catalog order. v4 has -/// no SteVec bindings, so only scalar domains appear. +/// no SteVec bindings, so only scalar domains appear. The catalog spells each +/// name with the `eql_v3_` prefix, which is swapped for v4's. fn expected_v4_domains() -> Vec { CATALOG .iter() @@ -23,7 +25,7 @@ fn expected_v4_domains() -> Vec { .map(|name| { let bare = name .strip_prefix(eql_domains::PUBLIC_TYPNAME_PREFIX) - .unwrap_or_else(|| panic!("{name} does not carry the v3 typname prefix")); + .unwrap_or_else(|| panic!("{name} does not carry the catalog typname prefix")); format!("{}{bare}", v4::PUBLIC_TYPNAME_PREFIX) }) .collect() @@ -55,23 +57,10 @@ fn stored_entries_name_public_v4_domains_and_v4_schema_ids() { #[test] fn query_entries_live_in_the_eql_v4_schema() { - let v3_queries: Vec = v3::all_query() - .iter() - .filter(|e| e.term_json_keys().is_some()) - .map(|e| e.domain().to_string()) - .collect(); - let v4_queries: Vec = v4::all_query() - .iter() - .map(|e| { - let sql = e.sql_domain(); - assert!(sql.starts_with("eql_v4.query_"), "{sql}"); - e.domain().to_string() - }) - .collect(); - assert_eq!( - v4_queries, v3_queries, - "each scalar query twin exists in v4 under the same name" - ); + for entry in v4::all_query() { + let sql = entry.sql_domain(); + assert!(sql.starts_with("eql_v4.query_"), "{sql}"); + } } #[test] @@ -79,8 +68,8 @@ fn term_keys_match_the_catalog() { let entries = v4::all(); for spec in CATALOG { for domain in spec.domains.iter().filter(|d| d.is_scalar()) { - let v3_name = spec.domain_name(domain); - let name = v3_name.replacen("eql_v3_", "eql_v4_", 1); + let catalog_name = spec.domain_name(domain); + let name = catalog_name.replacen("eql_v3_", "eql_v4_", 1); let entry = entries .iter() .find(|e| e.domain() == name) @@ -94,7 +83,7 @@ fn term_keys_match_the_catalog() { } } -fn text_eq(version: u8) -> Value { +fn text_eq(version: u16) -> Value { json!({ "v": version, "i": { "t": "users", "c": "email" }, @@ -103,24 +92,27 @@ fn text_eq(version: u8) -> Value { }) } -/// The boundary the move to v4 relies on: the payloads are otherwise -/// well-formed, so only the version can be what refuses them. +/// The payload is otherwise well-formed, so only the version can be what +/// refuses it. #[test] -fn each_generation_refuses_the_other_version() { +fn a_v4_type_admits_only_version_4() { assert!(serde_json::from_value::(text_eq(4)).is_ok()); - assert!(serde_json::from_value::(text_eq(3)).is_ok()); - - let err = serde_json::from_value::(text_eq(3)).unwrap_err(); - assert!(err.to_string().contains("version 3"), "{err}"); - let err = serde_json::from_value::(text_eq(4)).unwrap_err(); - assert!(err.to_string().contains("version 4"), "{err}"); + for version in [0, 3, 5] { + let err = serde_json::from_value::(text_eq(version)).unwrap_err(); + assert!( + err.to_string().contains(&format!( + "unsupported EQL schema version {version} (expected 4)" + )), + "{err}" + ); + } // And through the inventory, which is how a caller holding only a name // validates a payload. - let v4_entry = v4::all() + let entry = v4::all() .into_iter() .find(|e| e.domain() == "eql_v4_text_eq") .expect("eql_v4_text_eq in v4::all()"); - assert!(v4_entry.parse_value(&text_eq(4)).is_ok()); - assert!(v4_entry.parse_value(&text_eq(3)).is_err()); + assert!(entry.parse_value(&text_eq(4)).is_ok()); + assert!(entry.parse_value(&text_eq(3)).is_err()); } diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index 2c2be55a9..25c93dd96 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -334,20 +334,23 @@ 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: every storage-only domain whose plaintext -/// vitaminc 0.5 encodes, and the text equality domain. Integer equality is -/// held back until its PRF domain settles (see [`encryption_gap`]). Every other stored domain has a reason in -/// [`encryption_gap`], and a test holds the two together so a new domain +/// `stack-encrypt` derives: the storage and equality domains of every family +/// whose plaintext the engine's value enum carries, 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"), ("bigint", ""), + ("bigint", "eq"), ("text", ""), ("text", "eq"), ("text", "match"), ("boolean", ""), ("real", ""), + ("real", "eq"), ("double", ""), + ("double", "eq"), ]; /// Why no `eql_v3` struct derives Stack Encrypt's traits. @@ -357,16 +360,16 @@ 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 vitaminc 0.5 -/// has no scalar kind for. +/// derive and the by-name dispatch name. `None` for a family the engine's +/// value enum has no kind for. pub fn plaintext_of(family: &str) -> Option<(&'static str, &'static str)> { Some(match family { "text" => ("string", "String"), - "integer" => ("int32", "crate::encryption::plaintext::Int32"), - "bigint" => ("int64", "crate::encryption::plaintext::Int64"), - "boolean" => ("bool", "crate::encryption::plaintext::Bool"), - "real" => ("float32", "crate::encryption::plaintext::Float32"), - "double" => ("float64", "crate::encryption::plaintext::Float64"), + "integer" => ("int32", "i32"), + "bigint" => ("int64", "i64"), + "boolean" => ("bool", "bool"), + "real" => ("float32", "f32"), + "double" => ("float64", "f64"), _ => return None, }) } @@ -387,26 +390,20 @@ pub fn encryption_gap( return None; } Some(if !domain.is_scalar() { - "the JSON index is a new operation in the engine" + "the JSON index is a new operation in the engine (#1060)" } else if family.name == "json" { "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() { - "vitaminc 0.5's value enum has no kind for this family; the kind and its \ - encodings arrive in vitaminc 0.6 (ADR-0002)" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Ore | Term::Ope)) - { - "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)" - } else if matches!(family.name, "integer" | "bigint") && domain.terms == [Term::Hm] { - "integer equality is produced once vitaminc 0.6 is released: its one integer domain is \ - the one the engine derives in, and a term derived under vitaminc 0.5's per-width \ - domains would stop matching after the upgrade" - } else if matches!(family.name, "real" | "double") { - "float equality is produced once vitaminc 0.6, which gives a float its canonical \ - equality form, is released (ADR-0002)" + "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)" + } 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" { + "no EQL type is built from the engine's text order terms yet (#1131)" + } else if domain.terms.contains(&Term::Ope) { + "the engine does not yet derive this family's order terms over ADR-0002's order keys \ + (#1236)" } else { // Reached by an equality-only domain of a family whose plaintext is // specified but which is not listed: nothing in the catalog today. @@ -417,7 +414,9 @@ pub fn encryption_gap( /// Keep attributes in the generator so regeneration preserves the opt-in /// derives without enabling other domains before their operations and -/// plaintext contracts are supported ([`ENCRYPTION_DOMAINS`]). +/// plaintext contracts are supported ([`ENCRYPTION_DOMAINS`]). Only a v4 +/// struct gets them, and the v4 module exists only with the `stack-encrypt` +/// feature (`eql_bindings::v4`), so the attributes are not gated again. fn encryption_attrs( version: EqlVersion, family: &DomainFamily, @@ -427,16 +426,10 @@ fn encryption_attrs( if encryption_gap(version, family, domain).is_some() { return Default::default(); } - let decrypt = (!query).then( - || quote!(#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))]), - ); + let decrypt = (!query).then(|| quote!(#[derive(stack_encrypt::DecryptInto)])); let (_, rust) = plaintext_of(family.name).expect("a derived family names its plaintext"); let plaintext_ty: syn::Type = syn::parse_str(rust).expect("plaintext is a type path"); let shown = rust.rsplit("::").next().unwrap_or(rust); - // The plaintext line shows with or without the feature: it says what the - // type is built from, and the sentence itself names the feature. Only the - // link is feature-gated, since a plaintext from `crate::encryption` does - // not exist without it and the link would not resolve. let plaintext_doc = |named: &str| { if query { let terms = if domain.terms.contains(&Term::Bloom) { @@ -445,33 +438,29 @@ fn encryption_attrs( "equality" }; format!( - " Plaintext input: {named}. With the `stack-encrypt` feature, \ - implements `EncryptFrom<{shown}>` to build {terms} query terms. Query operands \ - do not decrypt." + " Plaintext input: {named}. Implements `EncryptFrom<{shown}>` to build {terms} \ + query terms. Query operands do not decrypt." ) } else { format!( - " Plaintext input and decrypted output: {named}. With the \ - `stack-encrypt` feature, implements `EncryptFrom<{shown}>` and \ - `DecryptInto<{shown}>`." + " Plaintext input and decrypted output: {named}. Implements \ + `EncryptFrom<{shown}>` and `DecryptInto<{shown}>`." ) } }; let plaintext = plaintext_doc(&format!("[`{shown}`]({rust})")); - let plaintext_unlinked = plaintext_doc(&format!("`{shown}`")); ( quote! { - #[cfg_attr(doc, doc = "")] - #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = #plaintext)] - #[cfg_attr(all(doc, not(feature = "stack-encrypt")), doc = #plaintext_unlinked)] - #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`.")] - #[cfg_attr(all(doc, feature = "stack-encrypt"), doc = " See the [complete encryption example](crate::encryption#example).")] - #[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::EncryptFrom))] + #[doc = ""] + #[doc = #plaintext] + #[doc = " Encryption context: `NonEmpty`, constructed with `Identifier::for_column(table, column)` and stored in `i`."] + #[doc = " See the [complete encryption example](crate::encryption#example)."] + #[derive(stack_encrypt::EncryptFrom)] #decrypt - #[cfg_attr(feature = "stack-encrypt", stash(plaintext = #plaintext_ty))] + #[stash(plaintext = #plaintext_ty)] }, - quote!(#[cfg_attr(feature = "stack-encrypt", stash(default))]), - quote!(#[cfg_attr(feature = "stack-encrypt", stash(context_field))]), + quote!(#[stash(default)]), + quote!(#[stash(context_field)]), ) } @@ -1062,7 +1051,7 @@ fn render_bindings(dir: &Path) -> Vec<(PathBuf, String)> { /// Render the v4 bindings: one module per family with a scalar domain (a /// mixed family's scalar domains under its own name, since v4 has no /// hand-written SteVec module to sit beside), and the Stack Encrypt target -/// table and dispatch (`targets.rs`, gated to the `stack-encrypt` feature by +/// table and dispatch (`targets.rs`, under the `stack-encrypt` feature with /// the hand-written `v4/mod.rs`). Stack Encrypt produces v4 only, so the /// target table lives here. fn render_v4_bindings(dir: &Path) -> Vec<(PathBuf, String)> { @@ -1729,21 +1718,17 @@ mod tests { } else if family.name == "json" { "tree of leaves" } else if plaintext_of(family.name).is_none() { - "has no kind for this family" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Ore | Term::Ope)) - { - "ordering terms" - } else if matches!(family.name, "integer" | "bigint") { - "integer equality is produced once vitaminc 0.6" + "no kind for this family" + } else if domain.terms.contains(&Term::Ore) { + "block ORE terms" + } else if domain.terms.contains(&Term::Ope) { + if family.name == "text" { + "text order terms" + } else { + "order keys (#1236)" + } } else { - assert!( - matches!(family.name, "real" | "double"), - "{full}: every other specified, unordered domain is derived" - ); - "float equality is produced once vitaminc 0.6" + panic!("{full}: every specified, unordered domain is derived: {reason}") }; assert!(reason.contains(expected), "{full}: {reason}"); } diff --git a/packages/eql/crates/eql-codegen/src/go_eql.rs b/packages/eql/crates/eql-codegen/src/go_eql.rs index 2a1d74f4c..e644e0a73 100644 --- a/packages/eql/crates/eql-codegen/src/go_eql.rs +++ b/packages/eql/crates/eql-codegen/src/go_eql.rs @@ -206,7 +206,7 @@ mod tests { // TextEq is producible and says so; TextOrdOre is not and says why. assert!(out.contains("// TextEq is the EQL type stored in public.eql_v4_text_eq")); assert!(out.contains("Producible: true, Reason: \"\"")); - assert!(out.contains("Name: \"TextOrdOre\"") && out.contains("ordering terms wait")); + assert!(out.contains("Name: \"TextOrdOre\"") && out.contains("block ORE terms")); // Every table row is one line: the shape gofmt leaves alone. for line in out.lines().filter(|l| l.starts_with("\t{Name: ")) { assert!(line.ends_with("},"), "{line}"); diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index be25592c2..ea6c26d9f 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -3,7 +3,7 @@ //! types a Stack Encrypt data plan may name as a field target, and the //! by-name dispatch that runs a named type's own Rust plan. Generated beside //! the v4 family modules by the same mechanism ([`crate::bindings`]), and -//! gated to the `stack-encrypt` feature by the hand-written `v4/mod.rs`. +//! under the `stack-encrypt` feature with the rest of `v4`. //! Stack Encrypt produces EQL v4 only (ADR-0002), so every target is an //! `eql_v4_*` domain. //! @@ -18,11 +18,12 @@ //! the derive, and [`target_gap`] is tested to agree with //! [`encryption_gap`](crate::bindings::encryption_gap) on every domain. //! -//! The reasons a type is not producible are the plan's -//! (`docs/plans/2026-10-04-plan-builder.md`, "EQL types"): every family but -//! text has no specified plaintext encoding; match and OPE terms are derived -//! but no EQL type is built from them; EQL's ORE term is block ORE where the -//! engine derives CLLW ORE; and the JSON index is a new engine operation. +//! 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 +//! 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 +//! index is a new engine operation (#1060). use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -393,12 +394,18 @@ mod tests { for producible in ["", "eq", "match"] { assert!(gap(&text, producible).is_none(), "text {producible:?}"); } - for ordered in ["ord", "ord_ore", "ord_ope", "search", "search_ore"] { + for ordered in ["ord", "ord_ope", "search"] { assert!( - gap(&text, ordered).unwrap().contains("ordering terms"), + gap(&text, ordered).unwrap().contains("text order terms"), "{ordered}" ); } + for block_ore in ["ord_ore", "search_ore"] { + assert!( + gap(&text, block_ore).unwrap().contains("block ORE"), + "{block_ore}" + ); + } assert!(gap(&text, "match").is_none(), "TextMatch is producible"); let family = |name: &str| { eql_domains::CATALOG @@ -406,25 +413,25 @@ mod tests { .find(|f| f.name == name) .expect("family") }; - for family_name in ["integer", "bigint"] { + for family_name in ["integer", "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"); assert!( - gap(family(family_name), "eq") - .unwrap() - .contains("integer equality is produced once vitaminc 0.6"), + gap(family, "ord").unwrap().contains("#1236"), + "{family_name}" + ); + assert!( + gap(family, "ord_ore").unwrap().contains("block ORE"), "{family_name}" ); - assert!(gap(family(family_name), "").is_none(), "{family_name}"); } - assert!(gap(family("integer"), "ord_ore") - .unwrap() - .contains("ordering terms")); - assert!(gap(family("real"), "eq") + assert!(gap(family("date"), "") .unwrap() - .contains("float equality is produced once vitaminc 0.6")); - assert!(gap(family("date"), "").unwrap().contains("has no kind")); + .contains("no kind for this family")); assert!(gap(family("smallint"), "eq") .unwrap() - .contains("has no kind")); + .contains("no kind for this family")); 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 5982c4622..e6667c821 100644 --- a/packages/eql/tests/encryption/fixtures/v4_queries.json +++ b/packages/eql/tests/encryption/fixtures/v4_queries.json @@ -1,8 +1,24 @@ { "_comment": "The v4 query operands eql-bindings derives for each plaintext under FakeDataKeySource's index key (keyset nil), read by the Go SDK's hermetic test; regenerate with EQL_UPDATE_FIXTURES=1, do not edit.", + "age": { + "plaintext": 42, + "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"age\"},\"hm\":\"ed60c97cff62963a142f9b7762de2e2b98e3d486a178b07f6e930318770fbecb\"}" + }, "name": { "plaintext": "alice", "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"name\"},\"bf\":[80,125,212,221,348,638,664,776,1141,1322,1400,1418,1532,1704,1762,1843,1865,1970]}" }, - "table": "profiles" + "price": { + "plaintext": 9.75, + "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"price\"},\"hm\":\"1b9f59331489691ca4eee0252b98b0863db3b8fbed9f0a0ba8df58f2bc7564ab\"}" + }, + "rank": { + "plaintext": 7, + "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"rank\"},\"hm\":\"54e9bd5b5c8620be147b65fe754240f64562d4ce62394b3e4390867f2ef483cc\"}" + }, + "table": "profiles", + "weight": { + "plaintext": 1.5, + "query": "{\"v\":4,\"i\":{\"t\":\"profiles\",\"c\":\"weight\"},\"hm\":\"96cc94054dd2785dd2592355d2478ca4470d29e09b47637a99f7a91066c93f8b\"}" + } } diff --git a/packages/eql/tests/encryption/fixtures/v4_rows.json b/packages/eql/tests/encryption/fixtures/v4_rows.json index 2da04aaf9..696a47f83 100644 --- a/packages/eql/tests/encryption/fixtures/v4_rows.json +++ b/packages/eql/tests/encryption/fixtures/v4_rows.json @@ -12,7 +12,11 @@ "ratio": 0.5, "score": 2.25, "note": "a private note", - "name": "alice" + "name": "alice", + "age": 42, + "rank": 7, + "weight": 1.5, + "price": 9.75 }, "stored": { "active": { @@ -21,7 +25,16 @@ "t": "profiles", "c": "active" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAA7z71FqWkCCWSkyPiBK7IQIAD0ug/nDu5R6UBEM9vJjtvDMBTuommRH359HgY7qUe5cAKm2WZaiFnjLqEGLDlJhvOJ4N4uQBzfyMNiLROxWw==" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAA7z71FqWkCCWSkyPiBK7IQIAD0ug/nDu5R6UBEM9vJjtvDMBTuommRH359HgY7qUe5cAKOFk1SWI53d53C11YlgdX5e7vkiqKTcLshM1vpgA==" + }, + "age": { + "v": 4, + "i": { + "t": "profiles", + "c": "age" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACaqBouRs68ubIb5N1OQbmcIABm7QQMIr1TNGIweTMmGzZsXGuC+7f6J8hofUigXNGM5QLxt+dtojhM2JhycNjwnNQhOYcyY0sCNy/ROzDWuyS1SoU=", + "hm": "ed60c97cff62963a142f9b7762de2e2b98e3d486a178b07f6e930318770fbecb" }, "balance": { "v": 4, @@ -29,7 +42,7 @@ "t": "profiles", "c": "balance" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAZJDEBcfVqv/o1B2CASJrAIAA9a/qD4wfv0t6fGVxDhKQOlQDIuYj5si13wnlmGENABAJ9hNn4JVUPAf0Oqgp+srYrqieBVnldyN2orjNNIl/8coQzlPV7" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAZJDEBcfVqv/o1B2CASJrAIAA9a/qD4wfv0t6fGVxDhKQOlQDIuYj5si13wnlmGENABAIQxQ+Pa4ne7yEPxdOVzjiQox/lvSNgN9H0vvDQircFxN4bzgSF" }, "name": { "v": 4, @@ -37,7 +50,7 @@ "t": "profiles", "c": "name" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAASxNA10KeHfMqSIglat8/GIADzC2+uswJkz9HL6DqUBBNDtGODBX5yNHLKryZ0Lgc24AJL13hLIs56eETD0CVxajCIWD1YDvt/qQ96PAk6BQX1+wqM", + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAASxNA10KeHfMqSIglat8/GIADzC2+uswJkz9HL6DqUBBNDtGODBX5yNHLKryZ0Lgc24ALhYPWuXmaNIoZbdwVw8PpS5u1QdTm071hn+f2eluPtcGZc", "bf": [ 80, 125, @@ -65,7 +78,25 @@ "t": "profiles", "c": "note" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACaWtPMkbUZL3WUSHDD8aD7IADB6CfVElii+gglc1gys6JmpNlLoIFn47AvStPnpJb63wK/3W5csrSgiLUwUES8hwXQnhc0UhgyZv43mkk3tmtP0bJps5Y8bFvyxaLs" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACaWtPMkbUZL3WUSHDD8aD7IADB6CfVElii+gglc1gys6JmpNlLoIFn47AvStPnpJb63wLcdjyabd75RThQ4CsEexovXy5QDLArZdZgRoBDmg/jShmurQATioUTUqTz" + }, + "price": { + "v": 4, + "i": { + "t": "profiles", + "c": "price" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABCExNfO+rr+B8wqj1M0aD4IADBdFcMOac9Yl8IAruRLW/XvOYbrEvf8/b/qUk9C02FfwJOmaRbHOuJeEThjicwd0FgANPkHMbZ2klQcOQkdnKaOEfu1X66", + "hm": "1b9f59331489691ca4eee0252b98b0863db3b8fbed9f0a0ba8df58f2bc7564ab" + }, + "rank": { + "v": 4, + "i": { + "t": "profiles", + "c": "rank" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABlWVkNzeHCBmpVMce0NgD6IAA4mPP/Krsok6Rmw3q0qARIDYCQpqM4GDID/zkILOkeKAIlW42N/yURtgBRtgsxh0No3cTCtq2rCdlAooi6j5FQ2W/QGzp9", + "hm": "54e9bd5b5c8620be147b65fe754240f64562d4ce62394b3e4390867f2ef483cc" }, "ratio": { "v": 4, @@ -73,7 +104,7 @@ "t": "profiles", "c": "ratio" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAuHszuxtkGGxdJ+yeeEgrtIACJ8iRTZnq+2wD3LmxdAqmDDHzDzzH5th5lG8EQq4qujQI0F4mjsLKrWLTCn4cnL+j9RJGWZUye1KEUEgFLy2w7jbg=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAuHszuxtkGGxdJ+yeeEgrtIACJ8iRTZnq+2wD3LmxdAqmDDHzDzzH5th5lG8EQq4qujQIhrxgAqpgRGwaL1PCunFuJD2OO1u8WbklfdpCWW4+4KCo=" }, "score": { "v": 4, @@ -81,7 +112,7 @@ "t": "profiles", "c": "score" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAbQulFdQ7itXP4biDOHDhxIAA4jkCscZEzyZymEuOdqFk34IZ2lMPnDiYFZgGjl+Qh0gKQ8PLCnxbnz0u/MiTy1keIajXDNTMgKHZwKVDhzp2lyFdkxNyG" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAbQulFdQ7itXP4biDOHDhxIAA4jkCscZEzyZymEuOdqFk34IZ2lMPnDiYFZgGjl+Qh0gJkRzmA2A/4kWGHplvyWJPXVEY4uE0sSqMBQq3Xa9AzCU6IOUs+" }, "visits": { "v": 4, @@ -89,7 +120,16 @@ "t": "profiles", "c": "visits" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAFDIc6s4iXDM+dzEA4ntTLIACXLoQBRqkNqM7yC8syykzCq7A4M32xA4/pWvh7C8atkgJnOr7F0ixiqCPrPH8Fr+u40q0M0yY0SAGJAzRZL9RtKmk=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAFDIc6s4iXDM+dzEA4ntTLIACXLoQBRqkNqM7yC8syykzCq7A4M32xA4/pWvh7C8atkgIkk++HnvHeJ43/6TKyHlZdACadIhQuCTI+w9ZxJdArk7s=" + }, + "weight": { + "v": 4, + "i": { + "t": "profiles", + "c": "weight" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACKITYga2qq0YHkFjTZcJ+4IACohdycH28XrjPCTXO5ZxudPnwg9RQaee1GS8RdT/bMKwJbEtW7gdbIYe0JMCtDM6uG3DoI7r2tUtL5PmBr3l4hQ+M=", + "hm": "96cc94054dd2785dd2592355d2478ca4470d29e09b47637a99f7a91066c93f8b" } } }, @@ -102,7 +142,11 @@ "ratio": -1.5, "score": 0, "note": "", - "name": "bob smith" + "name": "bob smith", + "age": 42, + "rank": -7, + "weight": -1.5, + "price": 0 }, "stored": { "active": { @@ -111,7 +155,16 @@ "t": "profiles", "c": "active" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABGXg/rWzFqWgcCw0KVmlV7IADK5ojyhDsQsCNphHB+3xjIrvP/87lbuOrEITfr0CumXgKas9j1WjeleoZNXxXCpQl5zTEbpuhNm3jz432uwQ==" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACeHWVW9n4VWX4jBWxAAXmOIADVxouCpKVuK55miYzKXtaxuk8M8yNirsNapVfaL5Cp9wKitSfLQl5U4d3sh9+zZZCKIhctq+xtpFjGdhDxZA==" + }, + "age": { + "v": 4, + "i": { + "t": "profiles", + "c": "age" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAC+UPfku+czy1jwxVlq5OIMIACI2l/JLpp27aUW/fdoZu76xFDx7IEJpBBS2tb5ex9WwwIGBFAxAyPvQY5viinEk+R6LXdmfp8QTvaT5FnDgUEcwNs=", + "hm": "ed60c97cff62963a142f9b7762de2e2b98e3d486a178b07f6e930318770fbecb" }, "balance": { "v": 4, @@ -119,7 +172,7 @@ "t": "profiles", "c": "balance" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAACHOuVBG9X5O4D9ApUC1DpIADAVYvyCPRFFVrNaWA5vGxA5PF75NkyEagzyfXSd1UIKwKPUkm2pn+4U7GMsAIcIoIwhG1aIpGmcAIvUtLfYiVA74iL35fU" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAB1ZVEXi2Es47JsbxxhOjhIIACjfYkd2iXoEsJoMxrjsVNgvKqKbMBlXgrMHFc2IFDM0wJPL2ijJkbRnr+iZO7L3/XU1OKT/U65J5K2YzKUYRA4lqodEgMF" }, "name": { "v": 4, @@ -127,7 +180,7 @@ "t": "profiles", "c": "name" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAlsDmtaCz7PVYXLYbtkdYiIADMhjWHFWFQffW6FoF5Kd0/vSO5UX+T+vZyisdueEEtugLKO7i9XY98Meyx+sRY3ODPp1DEZ2mua3DQV27QBeWr2rj1P3qmzA==", + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABmXH78GfaFYi06tdQL/YsdIABN+V8pc7G0+4120i61nbzJjVef8ywhG3ZiZFs2et0YTQK6SbrjSj5sJs1csvXCPLxXOdEB+uIlhtF2rrHObnDMj/pv4WzQVw==", "bf": [ 59, 178, @@ -177,7 +230,25 @@ "t": "profiles", "c": "note" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABtkZKQt8AupdgAkSYhBWuLIABushFe05tB4gQmcP9j+7VPX95cAW65PgvtgFajKobc6gJRmGKJ3XDJKTAOVvT7vrNPR4suqu/qGHnVB23ZOg==" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABobLCs537p+tT0KKzLIInaIAD6IFDEC396qG3xo6e/RUntWltagliYr8Rq9Mf7Z4MU6QLfQyQ8MD2pQIg6YERpyUul2FdLxB8stW8DbaoDUg==" + }, + "price": { + "v": 4, + "i": { + "t": "profiles", + "c": "price" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABt75+DHEYg2qfQw6iBiV8fIABOSEifNTOtcSRVx808u1FVucm5wkEm3QLEkkXB3twR3ALAOhm+xks8I2MSXvr3cz/sHA6l/1D+NnsxnUprAjpikdzGF9FI", + "hm": "b0b544178e92859e68e6435482637ae76bfd6603e503c4ded4881a89aaeb61d1" + }, + "rank": { + "v": 4, + "i": { + "t": "profiles", + "c": "rank" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADUOgpJDtn7zL6nzhYavTlAIADm+hoRGsKYDUzB0eIK2MVQdsL5G0YDtvAdNcFtbKCbkALNfWX2QT9N+k/T/1q432PwjSkPOYPEK5KqZb7kUrDj4E1QsXOg", + "hm": "7b1dbcfc1a19eec19cbda0117985973da7f24dc77c7e97f88f63a948f904d49d" }, "ratio": { "v": 4, @@ -185,7 +256,7 @@ "t": "profiles", "c": "ratio" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADJpOTT8kFqPHwznhWnYNgQIAC3GPCNDOMGBnh6gPSa5KTDr+VBQDNZAZr7kqKF16sx0QKivm/zonrrAnhWeznFBi6gEq+aGsOWkmzf0JjTPjdfykA=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACp4hotRiv5B8jrITj//bvPIAADksVFAKIzjTpgqZ8I3IP2HvR81DT2ZfPkkPw7T96z3wK1aKZuwGIC2kaPM/YUExB7UOjMQp29Z28yGvxb5virR7s=" }, "score": { "v": 4, @@ -193,7 +264,7 @@ "t": "profiles", "c": "score" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABAfBAgu3xTGBg6oVcxVdCnIADQvBhh5sMdxmdosyrWjFQngkmQ+312sbVAFpgs2UBpcQJExgoNvIsa/YdKXID8Dglp2fw7W87SIT5c/Lomaou5MY0ldsFn" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAw8GMAQWghCy9KJ1GKZqWpIACXBMEeoUUEIUIHGJFqbaz1Ohw0HY58iliBm01koQx74QL41ECR0FhZBugCejaBR17/+m3UvvbRUjy7TvSsipQLQbVUi5Ko" }, "visits": { "v": 4, @@ -201,7 +272,16 @@ "t": "profiles", "c": "visits" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABi/myUaRsBCFbuVTRBJLPiIADSnhrChMGg0gQaL8/2ghirKHXXC41P2aq5DghRI2FtWgLiTbX8qj/N69eB9aNPN6A0ilO2zy62YAxUlx97NnS+OMw=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAD/xGWx+0HoAi6vHLbwDANUIAAST1oa+P3JdVyt8+a0HqfuCp03GLjTY1gU+sQt2gjcCAIag/JipAzrJzQ94YcAjAQY+xnYBBY9uEZltgLKvTkEo4E=" + }, + "weight": { + "v": 4, + "i": { + "t": "profiles", + "c": "weight" + }, + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAChUfuxiOq+/Ehfu9sVsvfsIABWrl3a0t2Cnc1QBb2J/MLDdiba/6iLIIHITm82kj6p5QKXxLhV7DQNszAUEOjLJG4msDS5aafyn6jXgbUwoUhTD7k=", + "hm": "e2a35073033a519f51db4adc9ee92fd470cfd6eefc5f4d4c72c9ecc0a9e4e6b9" } } } diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index a22b78835..8f37248d3 100644 --- a/packages/eql/tests/encryption/tests/targets.rs +++ b/packages/eql/tests/encryption/tests/targets.rs @@ -359,8 +359,8 @@ mod given_a_name_the_engine_cannot_produce { let error = refused(targets::encrypt("TextOrdOre", &keyset, &email(), text("x"))); assert_eq!( error.to_string(), - "the engine cannot produce TextOrdOre yet: ordering terms wait for vitaminc-ore, the \ - order encodings in vitaminc 0.6 (ADR-0002)" + "the engine cannot produce TextOrdOre yet: EQL stores block ORE terms and the engine \ + derives CLLW ORE (#1132)" ); // Refused by name before the label or the value is looked at. let error = refused(targets::encrypt( @@ -479,7 +479,7 @@ mod the_table { "boolean" => Some(ValueKind::Bool), "real" => Some(ValueKind::Float32), "double" => Some(ValueKind::Float64), - // The rest have no scalar kind in vitaminc 0.5's value enum. + // The rest have no kind in the engine's value enum yet. _ => None, }; assert_eq!( @@ -553,11 +553,11 @@ mod the_table { } } - /// The types the engine produces in EQL v4 today: every storage-only - /// and equality-only domain whose plaintext vitaminc 0.5 encodes and, - /// for equality, derives a term for. + /// 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. #[test] - fn the_producible_types_are_the_unordered_ones_vitaminc_0_5_encodes() { + fn the_producible_types_are_the_unordered_ones_the_value_enum_carries() { let producible: Vec<&str> = targets::targets() .iter() .filter(|t| t.producible) @@ -567,13 +567,17 @@ mod the_table { producible, [ "Integer", + "IntegerEq", "Bigint", + "BigintEq", "Text", "TextEq", "TextMatch", "Boolean", "Real", - "Double" + "RealEq", + "Double", + "DoubleEq" ], ); for row in targets::targets().iter().filter(|t| !t.producible) { @@ -581,10 +585,10 @@ mod the_table { let expected = match (row.family, row.suffix) { ("json", "Search") => "JSON index", ("json", "") => "tree of leaves", - ("smallint" | "date" | "timestamp" | "numeric", _) => "has no kind", - (_, "Ord" | "OrdOre" | "OrdOpe" | "Search" | "SearchOre") => "ordering terms", - ("real" | "double", "Eq") => "float equality is produced once vitaminc 0.6", - ("integer" | "bigint", "Eq") => "integer equality is produced once vitaminc 0.6", + ("smallint" | "date" | "timestamp" | "numeric", _) => "no kind for this family", + (_, "OrdOre" | "SearchOre") => "derives CLLW ORE (#1132)", + ("text", "Ord" | "OrdOpe" | "Search") => "text order terms yet (#1131)", + (_, "Ord" | "OrdOpe") => "order keys (#1236)", (family, suffix) => panic!("{}: unexpected {family} {suffix}", row.name), }; assert!(reason.contains(expected), "{}: {reason}", row.name); diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs index 1daa7c5cb..70b5dd0cc 100644 --- a/packages/eql/tests/encryption/tests/v4_types.rs +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -1,9 +1,7 @@ //! The EQL v4 types Stack Encrypt produces besides `TextEq`: the -//! storage-only integer, bigint, boolean, float and text types, and -//! `TextMatch`. `Json` is not producible yet: a JSON value seals as a tree of -//! leaves and EQL's `c` holds one. `IntegerEq` and `BigintEq` are held back: -//! their equality term moves to vitaminc 0.6's shared integer PRF domain, and -//! terms stored before that would stop matching. +//! storage-only integer, 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 //! `encrypt_as` and the by-name dispatch a Go plan reaches), seal exactly the @@ -13,7 +11,6 @@ mod common; -use eql_bindings::encryption::plaintext::{Bool, Float32, Float64, Int32, Int64}; use eql_bindings::encryption::targets::{self as targets, TargetError}; use eql_bindings::encryption::{CiphertextVisitor, NativeCiphertextVisitor as NativeLeaf}; use eql_bindings::v3::terms::Ciphertext; @@ -135,14 +132,14 @@ async fn integers_round_trip_and_seal_the_value_enum_leaf() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); for value in [0, 42, -7, i32::MIN, i32::MAX] { - let stored: integer::Integer = keyset.encrypt_as(&Int32(value), column()).await.unwrap(); + let stored: integer::Integer = keyset.encrypt_as(&value, column()).await.unwrap(); assert_v4_envelope(&stored, &["v", "i", "c"]); - let opened: Int32 = cipher + let opened: i32 = cipher .decrypt_as(stored.clone(), column().into()) .await .unwrap(); - assert_eq!(opened.0, value); + assert_eq!(opened, value); assert!(matches!( as_value_enum(&cipher, stored.c.clone()).await, FfiValue::Int32(v) if v == value @@ -163,13 +160,13 @@ async fn integers_round_trip_and_seal_the_value_enum_leaf() { } for value in [0i64, -1, i64::MIN, i64::MAX] { - let stored: bigint::Bigint = keyset.encrypt_as(&Int64(value), column()).await.unwrap(); + let stored: bigint::Bigint = keyset.encrypt_as(&value, column()).await.unwrap(); assert_v4_envelope(&stored, &["v", "i", "c"]); - let opened: Int64 = cipher + let opened: i64 = cipher .decrypt_as(stored.clone(), column().into()) .await .unwrap(); - assert_eq!(opened.0, value); + assert_eq!(opened, value); assert!(matches!( as_value_enum(&cipher, stored.c.clone()).await, FfiValue::Int64(v) if v == value @@ -177,24 +174,113 @@ async fn integers_round_trip_and_seal_the_value_enum_leaf() { } } -/// Withdrawn until vitaminc 0.6: a later re-enable is a deliberate change to -/// this test, not a side effect of a regenerated table. +/// The `hm` the named type stores for `value`, and the one its query twin +/// derives. +async fn eq_terms( + keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, + name: &str, + value: FfiValue, +) -> (serde_json::Value, serde_json::Value) { + let stored: serde_json::Value = by_name(keyset, name, value.clone()).await; + let query = targets::query(name, keyset, &label(), value) + .unwrap() + .await + .unwrap(); + let query: serde_json::Value = serde_json::from_slice(&query).unwrap(); + (stored["hm"].clone(), query["hm"].clone()) +} + +/// The equality term follows ADR-0002's semantic baseline: integers of +/// either width share one term for one value, and a float's `-0.0` and every +/// NaN fold to one value each, as Postgres's own `float8 =` treats them. #[tokio::test] -async fn integer_equality_types_are_held_back_with_their_reason() { +async fn number_equality_terms_follow_the_semantic_baseline() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); - for (name, value) in [ - ("IntegerEq", FfiValue::Int32(1)), - ("BigintEq", FfiValue::Int64(1)), - ] { - let target = targets::target(name).expect("in the table"); - assert!(!target.producible, "{name}"); - let error = refused(targets::encrypt(name, &keyset, &label(), value)); - assert!( - matches!(&error, TargetError::Unproducible { reason, .. } if reason.contains("integer equality is produced once vitaminc 0.6")), - "{name}: {error}" - ); - } + let hm = |name: &'static str, value: FfiValue| { + let keyset = &keyset; + async move { + let (stored, query) = eq_terms(keyset, name, value).await; + assert!(stored.is_string(), "{name} stores an hm: {stored}"); + assert_eq!(stored, query, "{name}: the query probes the stored term"); + stored + } + }; + + assert_ne!( + hm("IntegerEq", FfiValue::Int32(42)).await, + hm("IntegerEq", FfiValue::Int32(43)).await + ); + assert_eq!( + hm("IntegerEq", FfiValue::Int32(42)).await, + hm("BigintEq", FfiValue::Int64(42)).await, + "one integer domain for every width" + ); + + assert_eq!( + hm("RealEq", FfiValue::Float32(-0.0)).await, + hm("RealEq", FfiValue::Float32(0.0)).await + ); + assert_eq!( + hm("RealEq", FfiValue::Float32(f32::from_bits(0xffc0_1234))).await, + hm("RealEq", FfiValue::Float32(f32::NAN)).await + ); + assert_eq!( + hm("DoubleEq", FfiValue::Float64(-0.0)).await, + hm("DoubleEq", FfiValue::Float64(0.0)).await + ); + assert_eq!( + hm( + "DoubleEq", + FfiValue::Float64(f64::from_bits(0xfff8_0000_0000_beef)) + ) + .await, + hm("DoubleEq", FfiValue::Float64(f64::NAN)).await + ); + assert_ne!( + hm("DoubleEq", FfiValue::Float64(1.5)).await, + hm("DoubleEq", FfiValue::Float64(2.5)).await + ); + assert_eq!( + hm("RealEq", FfiValue::Float32(1.5)).await, + hm("DoubleEq", FfiValue::Float64(1.5)).await, + "an f32 hashes as the f64 it widens to" + ); + // An integer and a float are different kinds, even at one value. + assert_ne!( + hm("BigintEq", FfiValue::Int64(1)).await, + hm("DoubleEq", FfiValue::Float64(1.0)).await + ); +} + +/// The typed entry point stores the same term as the by-name dispatch, and +/// the ciphertext keeps the value exactly: `-0.0` folds in the term only. +#[tokio::test] +async fn number_equality_types_round_trip_through_the_typed_entry_point() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + + let stored: integer::IntegerEq = keyset.encrypt_as(&-7i32, column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let named: serde_json::Value = by_name(&keyset, "IntegerEq", FfiValue::Int32(-7)).await; + assert_eq!(serde_json::to_value(&stored).unwrap()["hm"], named["hm"]); + let opened: i32 = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened, -7); + + 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(); + assert_eq!(opened, i64::MIN); + + let stored: real::RealEq = keyset.encrypt_as(&-0.0f32, column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let opened: f32 = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened.to_bits(), (-0.0f32).to_bits()); + + let stored: double::DoubleEq = keyset.encrypt_as(&f64::NAN, column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c", "hm"]); + let opened: f64 = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened.to_bits(), f64::NAN.to_bits()); } #[tokio::test] @@ -202,13 +288,13 @@ async fn booleans_and_floats_round_trip_as_storage_only_types() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); for value in [true, false] { - let stored: boolean::Boolean = keyset.encrypt_as(&Bool(value), column()).await.unwrap(); + let stored: boolean::Boolean = keyset.encrypt_as(&value, column()).await.unwrap(); assert_v4_envelope(&stored, &["v", "i", "c"]); - let opened: Bool = cipher + let opened: bool = cipher .decrypt_as(stored.clone(), column().into()) .await .unwrap(); - assert_eq!(opened.0, value); + assert_eq!(opened, value); assert!(matches!( as_value_enum(&cipher, stored.c).await, FfiValue::Bool(v) if v == value @@ -216,24 +302,24 @@ async fn booleans_and_floats_round_trip_as_storage_only_types() { } // Floats seal their bit pattern: -0.0 and a NaN's payload survive. for value in [1.5f32, -0.0, f32::NAN, f32::INFINITY] { - let stored: real::Real = keyset.encrypt_as(&Float32(value), column()).await.unwrap(); - let opened: Float32 = cipher + let stored: real::Real = keyset.encrypt_as(&value, column()).await.unwrap(); + let opened: f32 = cipher .decrypt_as(stored.clone(), column().into()) .await .unwrap(); - assert_eq!(opened.0.to_bits(), value.to_bits()); + assert_eq!(opened.to_bits(), value.to_bits()); assert!(matches!( as_value_enum(&cipher, stored.c).await, FfiValue::Float32(v) if v.to_bits() == value.to_bits() )); } for value in [2.25f64, -0.0, f64::NAN] { - let stored: double::Double = keyset.encrypt_as(&Float64(value), column()).await.unwrap(); - let opened: Float64 = cipher + let stored: double::Double = keyset.encrypt_as(&value, column()).await.unwrap(); + let opened: f64 = cipher .decrypt_as(stored.clone(), column().into()) .await .unwrap(); - assert_eq!(opened.0.to_bits(), value.to_bits()); + assert_eq!(opened.to_bits(), value.to_bits()); assert!(matches!( as_value_enum(&cipher, stored.c).await, FfiValue::Float64(v) if v.to_bits() == value.to_bits() @@ -248,39 +334,39 @@ async fn a_leaf_the_value_enum_sealed_opens_as_the_typed_plaintext() { let (cipher, _) = common::cipher().await; let keyset = cipher.default_keyset(); let c = sealed_by_value_enum(&keyset, FfiValue::Int32(-7)).await; - let opened: Int32 = cipher + let opened: i32 = cipher .decrypt_as(stored_as::(c), column().into()) .await .unwrap(); - assert_eq!(opened.0, -7); + assert_eq!(opened, -7); let c = sealed_by_value_enum(&keyset, FfiValue::Int64(i64::MAX)).await; - let opened: Int64 = cipher + let opened: i64 = cipher .decrypt_as(stored_as::(c), column().into()) .await .unwrap(); - assert_eq!(opened.0, i64::MAX); + assert_eq!(opened, i64::MAX); let c = sealed_by_value_enum(&keyset, FfiValue::Bool(true)).await; - let opened: Bool = cipher + let opened: bool = cipher .decrypt_as(stored_as::(c), column().into()) .await .unwrap(); - assert!(opened.0); + assert!(opened); let c = sealed_by_value_enum(&keyset, FfiValue::Float32(-0.0)).await; - let opened: Float32 = cipher + let opened: f32 = cipher .decrypt_as(stored_as::(c), column().into()) .await .unwrap(); - assert_eq!(opened.0.to_bits(), (-0.0f32).to_bits()); + assert_eq!(opened.to_bits(), (-0.0f32).to_bits()); let c = sealed_by_value_enum(&keyset, FfiValue::Float64(f64::NAN)).await; - let opened: Float64 = cipher + let opened: f64 = cipher .decrypt_as(stored_as::(c), column().into()) .await .unwrap(); - assert_eq!(opened.0.to_bits(), f64::NAN.to_bits()); + assert_eq!(opened.to_bits(), f64::NAN.to_bits()); } /// Every producible row of the generated table, through all three by-name @@ -470,16 +556,36 @@ async fn a_value_of_another_kind_is_refused_not_converted() { ); } - // A ciphertext moved into a type of another kind does not open: the tag - // is authenticated, and each plaintext reads only its own. - let narrow: integer::Integer = keyset.encrypt_as(&Int32(42), column()).await.unwrap(); + // A ciphertext moved into another type opens through that type's Rust + // plaintext as vitaminc reads it: an integer of another width that fits + // opens unchanged, one that does not fit is refused, and a value of + // another kind is refused. + let narrow: integer::Integer = keyset.encrypt_as(&42i32, column()).await.unwrap(); let moved = bigint::Bigint { v: narrow.v, - i: narrow.i, - c: narrow.c, + i: narrow.i.clone(), + c: narrow.c.clone(), }; - let opened: Result = cipher.decrypt_as(moved, column().into()).await; - assert!(opened.is_err()); + let opened: i64 = cipher.decrypt_as(moved, column().into()).await.unwrap(); + assert_eq!(opened, 42); + + let wide: bigint::Bigint = keyset.encrypt_as(&i64::MAX, column()).await.unwrap(); + let moved = integer::Integer { + v: narrow.v, + i: wide.i, + c: wide.c, + }; + let opened: Result = cipher.decrypt_as(moved, column().into()).await; + assert!(opened.is_err(), "i64::MAX does not fit an i32"); + + let flag: boolean::Boolean = keyset.encrypt_as(&true, column()).await.unwrap(); + let moved = integer::Integer { + v: narrow.v, + i: flag.i, + c: flag.c, + }; + let opened: Result = cipher.decrypt_as(moved, column().into()).await; + assert!(opened.is_err(), "a bool is not an integer"); } /// Needs an EQL v4 installation; uses fresh real ciphertext with a fake key @@ -517,23 +623,23 @@ fn postgres_stores_and_queries_every_producible_v4_type() { let row = runtime.block_on(async { ( keyset - .encrypt_as::<_, integer::Integer>(&Int32(age), column()) + .encrypt_as::<_, integer::Integer>(&age, column()) .await .unwrap(), keyset - .encrypt_as::<_, bigint::Bigint>(&Int64(balance), column()) + .encrypt_as::<_, bigint::Bigint>(&balance, column()) .await .unwrap(), keyset - .encrypt_as::<_, boolean::Boolean>(&Bool(id % 2 == 0), column()) + .encrypt_as::<_, boolean::Boolean>(&(id % 2 == 0), column()) .await .unwrap(), keyset - .encrypt_as::<_, real::Real>(&Float32(0.5), column()) + .encrypt_as::<_, real::Real>(&0.5f32, column()) .await .unwrap(), keyset - .encrypt_as::<_, double::Double>(&Float64(0.25), column()) + .encrypt_as::<_, double::Double>(&0.25f64, column()) .await .unwrap(), keyset @@ -618,46 +724,46 @@ fn postgres_stores_and_queries_every_producible_v4_type() { .unwrap(); let open = |i: usize| -> String { row.get(i) }; runtime.block_on(async { - let opened: Int32 = cipher + let opened: i32 = cipher .decrypt_as( serde_json::from_str::(&open(0)).unwrap(), column().into(), ) .await .unwrap(); - assert_eq!(opened.0, 7); - let opened: Int64 = cipher + assert_eq!(opened, 7); + let opened: i64 = cipher .decrypt_as( serde_json::from_str::(&open(1)).unwrap(), column().into(), ) .await .unwrap(); - assert_eq!(opened.0, 20); - let opened: Bool = cipher + assert_eq!(opened, 20); + let opened: bool = cipher .decrypt_as( serde_json::from_str::(&open(2)).unwrap(), column().into(), ) .await .unwrap(); - assert!(opened.0); - let opened: Float32 = cipher + assert!(opened); + let opened: f32 = cipher .decrypt_as( serde_json::from_str::(&open(3)).unwrap(), column().into(), ) .await .unwrap(); - assert_eq!(opened.0, 0.5); - let opened: Float64 = cipher + assert_eq!(opened, 0.5); + let opened: f64 = cipher .decrypt_as( serde_json::from_str::(&open(4)).unwrap(), column().into(), ) .await .unwrap(); - assert_eq!(opened.0, 0.25); + assert_eq!(opened, 0.25); let opened: String = cipher .decrypt_as( serde_json::from_str::(&open(5)).unwrap(), @@ -680,6 +786,153 @@ fn postgres_stores_and_queries_every_producible_v4_type() { ); } +/// Needs an EQL v4 installation. `=` on each number equality type finds +/// exactly the rows its query names, with `-0.0` and NaN treated as +/// Postgres's own `float8 =` treats them. +#[test] +#[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] +fn postgres_finds_number_equality_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_number_eq ( + id int PRIMARY KEY, + age public.eql_v4_integer_eq, + balance public.eql_v4_bigint_eq, + ratio public.eql_v4_real_eq, + score public.eql_v4_double_eq + );", + ) + .unwrap(); + + let json = |value: &dyn erased::Json| value.to_json(); + let rows = [ + (1, 42, 10i64, 0.5f32, 0.0f64), + (2, 7, -1, -0.0, f64::NAN), + (3, 42, i64::MAX, 0.0, -0.0), + ]; + for (id, age, balance, ratio, score) in rows { + let stored = runtime.block_on(async { + ( + keyset + .encrypt_as::<_, integer::IntegerEq>(&age, column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, bigint::BigintEq>(&balance, column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, real::RealEq>(&ratio, column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, double::DoubleEq>(&score, column()) + .await + .unwrap(), + ) + }); + db.execute( + "INSERT INTO v4_number_eq VALUES ($1, + $2::text::jsonb::public.eql_v4_integer_eq, + $3::text::jsonb::public.eql_v4_bigint_eq, + $4::text::jsonb::public.eql_v4_real_eq, + $5::text::jsonb::public.eql_v4_double_eq)", + &[ + &id, + &json(&stored.0), + &json(&stored.1), + &json(&stored.2), + &json(&stored.3), + ], + ) + .unwrap(); + } + + let mut ids = |column_name: &str, query_domain: &str, query: String| -> Vec { + db.query( + &format!( + "SELECT id FROM v4_number_eq WHERE {column_name} = $1::text::jsonb::eql_v4.{query_domain} ORDER BY id" + ), + &[&query], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect() + }; + let q = |stored: Result| stored.unwrap(); + let age = |v: i32| { + q(runtime.block_on(async { + keyset + .encrypt_as::<_, integer::IntegerEqQuery>(&v, column()) + .await + .map(|q| json(&q)) + })) + }; + let balance = |v: i64| { + q(runtime.block_on(async { + keyset + .encrypt_as::<_, bigint::BigintEqQuery>(&v, column()) + .await + .map(|q| json(&q)) + })) + }; + let ratio = |v: f32| { + q(runtime.block_on(async { + keyset + .encrypt_as::<_, real::RealEqQuery>(&v, column()) + .await + .map(|q| json(&q)) + })) + }; + let score = |v: f64| { + q(runtime.block_on(async { + keyset + .encrypt_as::<_, double::DoubleEqQuery>(&v, column()) + .await + .map(|q| json(&q)) + })) + }; + let (age_42, age_8) = (age(42), age(8)); + let (balance_max, balance_0) = (balance(i64::MAX), balance(0)); + let (ratio_zero, ratio_half) = (ratio(0.0), ratio(0.5)); + let (score_neg_zero, score_nan) = (score(-0.0), score(f64::from_bits(0xfff8_0000_0000_beef))); + + assert_eq!(ids("age", "query_integer_eq", age_42), [1, 3]); + assert_eq!(ids("age", "query_integer_eq", age_8), Vec::::new()); + assert_eq!(ids("balance", "query_bigint_eq", balance_max), [3]); + assert_eq!( + ids("balance", "query_bigint_eq", balance_0), + Vec::::new() + ); + assert_eq!( + ids("ratio", "query_real_eq", ratio_zero), + [2, 3], + "-0.0 = 0.0" + ); + assert_eq!(ids("ratio", "query_real_eq", ratio_half), [1]); + assert_eq!( + ids("score", "query_double_eq", score_neg_zero), + [1, 3], + "-0.0 = 0.0" + ); + assert_eq!( + ids("score", "query_double_eq", score_nan), + [2], + "NaN = NaN, whatever its payload" + ); +} + /// A stored value's JSON text, for binding as `$n::text::jsonb`. mod erased { pub trait Json { @@ -722,6 +975,22 @@ async fn the_v4_query_fixture_is_what_the_dispatch_derives() { "plaintext": "alice", "query": query("TextMatch", "name", FfiValue::String("alice".into())).await, }, + "age": { + "plaintext": 42, + "query": query("IntegerEq", "age", FfiValue::Int32(42)).await, + }, + "rank": { + "plaintext": 7, + "query": query("BigintEq", "rank", FfiValue::Int64(7)).await, + }, + "weight": { + "plaintext": 1.5, + "query": query("RealEq", "weight", FfiValue::Float32(1.5)).await, + }, + "price": { + "plaintext": 9.75, + "query": query("DoubleEq", "price", FfiValue::Float64(9.75)).await, + }, }); let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/v4_queries.json"); @@ -767,6 +1036,10 @@ mod go_rows { pub score: f64, pub note: String, pub name: String, + pub age: i32, + pub rank: i64, + pub weight: f32, + pub price: f64, } /// The committed rows, or the file `EQL_V4_ROWS` names: CI points it at @@ -810,43 +1083,43 @@ mod go_rows { row: &Row, ) { let p = &row.plaintext; - let visits: Int32 = cipher + let visits: i32 = cipher .decrypt_as( parsed::(row, "visits"), column("visits").into(), ) .await .unwrap(); - assert_eq!(visits.0, p.visits); - let balance: Int64 = cipher + assert_eq!(visits, p.visits); + let balance: i64 = cipher .decrypt_as( parsed::(row, "balance"), column("balance").into(), ) .await .unwrap(); - assert_eq!(balance.0, p.balance); - let active: Bool = cipher + assert_eq!(balance, p.balance); + let active: bool = cipher .decrypt_as( parsed::(row, "active"), column("active").into(), ) .await .unwrap(); - assert_eq!(active.0, p.active); - let ratio: Float32 = cipher + assert_eq!(active, p.active); + let ratio: f32 = cipher .decrypt_as(parsed::(row, "ratio"), column("ratio").into()) .await .unwrap(); - assert_eq!(ratio.0.to_bits(), p.ratio.to_bits()); - let score: Float64 = cipher + assert_eq!(ratio.to_bits(), p.ratio.to_bits()); + let score: f64 = cipher .decrypt_as( parsed::(row, "score"), column("score").into(), ) .await .unwrap(); - assert_eq!(score.0.to_bits(), p.score.to_bits()); + assert_eq!(score.to_bits(), p.score.to_bits()); let note: String = cipher .decrypt_as(parsed::(row, "note"), column("note").into()) .await @@ -860,6 +1133,38 @@ mod go_rows { .await .unwrap(); assert_eq!(name, p.name); + let age: i32 = cipher + .decrypt_as( + parsed::(row, "age"), + column("age").into(), + ) + .await + .unwrap(); + assert_eq!(age, p.age); + let rank: i64 = cipher + .decrypt_as( + parsed::(row, "rank"), + column("rank").into(), + ) + .await + .unwrap(); + assert_eq!(rank, p.rank); + let weight: f32 = cipher + .decrypt_as( + parsed::(row, "weight"), + column("weight").into(), + ) + .await + .unwrap(); + assert_eq!(weight.to_bits(), p.weight.to_bits()); + let price: f64 = cipher + .decrypt_as( + parsed::(row, "price"), + column("price").into(), + ) + .await + .unwrap(); + assert_eq!(price.to_bits(), p.price.to_bits()); } } @@ -877,8 +1182,9 @@ async fn rows_the_go_sdk_sealed_open_in_rust() { } /// The Go-sealed rows in PostgreSQL: each value passes its `eql_v4_*` domain -/// check, `@@` finds the row with the fixture's Go-derived query operand, -/// and what comes back out still opens. +/// check, `@@` and `=` find the rows with the fixture's query operands (which +/// the Go SDK's test derives byte for byte), and what comes back out still +/// opens. #[test] #[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] fn postgres_stores_and_queries_rows_the_go_sdk_sealed() { @@ -902,7 +1208,11 @@ fn postgres_stores_and_queries_rows_the_go_sdk_sealed() { ratio public.eql_v4_real, score public.eql_v4_double, note public.eql_v4_text, - name public.eql_v4_text_match + name public.eql_v4_text_match, + age public.eql_v4_integer_eq, + rank public.eql_v4_bigint_eq, + weight public.eql_v4_real_eq, + price public.eql_v4_double_eq );", ) .unwrap(); @@ -916,7 +1226,11 @@ fn postgres_stores_and_queries_rows_the_go_sdk_sealed() { $5::text::jsonb::public.eql_v4_real, $6::text::jsonb::public.eql_v4_double, $7::text::jsonb::public.eql_v4_text, - $8::text::jsonb::public.eql_v4_text_match)", + $8::text::jsonb::public.eql_v4_text_match, + $9::text::jsonb::public.eql_v4_integer_eq, + $10::text::jsonb::public.eql_v4_bigint_eq, + $11::text::jsonb::public.eql_v4_real_eq, + $12::text::jsonb::public.eql_v4_double_eq)", &[ &row.id, &v("visits"), @@ -926,6 +1240,10 @@ fn postgres_stores_and_queries_rows_the_go_sdk_sealed() { &v("score"), &v("note"), &v("name"), + &v("age"), + &v("rank"), + &v("weight"), + &v("price"), ], ) .unwrap(); @@ -962,6 +1280,48 @@ fn postgres_stores_and_queries_rows_the_go_sdk_sealed() { assert!(!expected.is_empty()); assert_eq!(ids, expected, "@@ {needle:?} over Go-sealed names"); + // `=` on each equality column, against the rows whose plaintext equals + // the query's. + let equal = |column_name: &str, query_domain: &str, holds: &dyn Fn(&go_rows::Plain) -> bool| { + let expected: Vec = fixture + .rows + .iter() + .filter(|r| holds(&r.plaintext)) + .map(|r| r.id) + .collect(); + assert!(!expected.is_empty(), "{column_name}: a row matches"); + (expected, column_name.to_owned(), query_domain.to_owned()) + }; + let plain = |key: &str| queries[key]["plaintext"].clone(); + let checks = [ + equal("age", "query_integer_eq", &|p| { + serde_json::json!(p.age) == plain("age") + }), + equal("rank", "query_bigint_eq", &|p| { + serde_json::json!(p.rank) == plain("rank") + }), + equal("weight", "query_real_eq", &|p| { + f64::from(p.weight) == plain("weight").as_f64().unwrap() + }), + equal("price", "query_double_eq", &|p| { + p.price == plain("price").as_f64().unwrap() + }), + ]; + for (expected, column_name, query_domain) in checks { + let ids: Vec = db + .query( + &format!( + "SELECT id FROM go_profiles WHERE {column_name} = $1::text::jsonb::eql_v4.{query_domain} ORDER BY id" + ), + &[&queries[column_name.as_str()]["query"].as_str().unwrap()], + ) + .unwrap() + .iter() + .map(|r| r.get(0)) + .collect(); + assert_eq!(ids, expected, "= over Go-sealed {column_name}"); + } + // Read back from the domains and open again. for row in &fixture.rows { let back = db