diff --git a/.changeset/eql-v4-bindings.md b/.changeset/eql-v4-bindings.md index e29c9aa91..546a460d7 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. +**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`, 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. 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/cmd/stashgen/README.md b/languages/golang/cmd/stashgen/README.md index b2c521da5..227e1c855 100644 --- a/languages/golang/cmd/stashgen/README.md +++ b/languages/golang/cmd/stashgen/README.md @@ -149,7 +149,19 @@ The same input always gives the same file: fields keep their declared order, and `stashgen` checks each declaration with the engine, and holds no copy of the engine's rules: it runs the WASI guest the SDK embeds and asks it, one field at a time, so the error names the field. It asks the engine for the EQL types it holds: each name, its plaintext type, its indexes, its query form, and whether the engine produces it today. The command links the build of the engine that holds the EQL types (`encrypt/eql`), so it can answer for every type; a generated file imports `encrypt/eql` only when it names one. -The engine produces `TextEq` today; `encrypt_into` with any other type is refused with the type's name. +The engine produces these EQL types today, each from one Go field type: + +| `encrypt_into` | Go field | +|---|---| +| `TextEq`, `Text` | `string` | +| `Integer` | `int32` | +| `Bigint` | `int64` | +| `Boolean` | `bool` | +| `Real` | `float32` | +| `Double` | `float64` | + +The width must match: an `int` or `int64` field is not an `Integer`, and a `float64` field is not a `Real`. +`encrypt_into` with any other type is refused with the type's name and the reason it is not produced yet. Separate columns work today for four indexes: `equality`, `match`, `ore` and `ope`. A field with `encrypt_into` is stored under its table and column, which is what an EQL value records in its `i`. diff --git a/languages/golang/encrypt/eql/eql.go b/languages/golang/encrypt/eql/eql.go index 24b3d7c17..28103f89c 100644 --- a/languages/golang/encrypt/eql/eql.go +++ b/languages/golang/encrypt/eql/eql.go @@ -18,9 +18,9 @@ // // The types and the [Types] table are generated from the EQL catalog by // eql-codegen (`mise run types:generate` in packages/eql); eql_gen.go is -// committed and drift-gated. The engine produces [TextEq] today; every -// other type is listed with the reason it cannot be produced yet, and -// stashgen refuses it. +// committed and drift-gated. [Types] says which types the engine produces +// today; every other type is listed with the reason it cannot be produced +// yet, and stashgen refuses it. package eql import ( diff --git a/languages/golang/encrypt/eql/eql_gen.go b/languages/golang/encrypt/eql/eql_gen.go index e1bc5688b..d7b185e15 100644 --- a/languages/golang/encrypt/eql/eql_gen.go +++ b/languages/golang/encrypt/eql/eql_gen.go @@ -9,61 +9,61 @@ import "database/sql/driver" // engine produces the type today; stashgen refuses encrypt_into for one // it does not, with the Reason. var Types = []Type{ - {Name: "Integer", GoName: "Integer", Family: "integer", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_integer", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerEq", GoName: "IntegerEq", Family: "integer", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_integer_eq", Indexes: []string{"eq"}, Query: "IntegerEqQuery", QuerySQLDomain: "eql_v4.query_integer_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrdOre", GoName: "IntegerOrdOre", Family: "integer", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord_ore", Indexes: []string{"ore"}, Query: "IntegerOrdOreQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrd", GoName: "IntegerOrd", Family: "integer", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord", Indexes: []string{"ope"}, Query: "IntegerOrdQuery", QuerySQLDomain: "eql_v4.query_integer_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "IntegerOrdOpe", GoName: "IntegerOrdOpe", Family: "integer", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_integer_ord_ope", Indexes: []string{"ope"}, Query: "IntegerOrdOpeQuery", QuerySQLDomain: "eql_v4.query_integer_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Smallint", GoName: "Smallint", Family: "smallint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_smallint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Bigint", GoName: "Bigint", Family: "bigint", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_bigint", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintEq", GoName: "BigintEq", Family: "bigint", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_bigint_eq", Indexes: []string{"eq"}, Query: "BigintEqQuery", QuerySQLDomain: "eql_v4.query_bigint_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrdOre", GoName: "BigintOrdOre", Family: "bigint", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord_ore", Indexes: []string{"ore"}, Query: "BigintOrdOreQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrd", GoName: "BigintOrd", Family: "bigint", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord", Indexes: []string{"ope"}, Query: "BigintOrdQuery", QuerySQLDomain: "eql_v4.query_bigint_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "BigintOrdOpe", GoName: "BigintOrdOpe", Family: "bigint", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_bigint_ord_ope", Indexes: []string{"ope"}, Query: "BigintOrdOpeQuery", QuerySQLDomain: "eql_v4.query_bigint_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Date", GoName: "Date", Family: "date", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_date", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Timestamp", GoName: "Timestamp", Family: "timestamp", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_timestamp", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Numeric", GoName: "Numeric", Family: "numeric", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_numeric", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {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: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Text", GoName: "Text", Family: "text", Suffix: "", Plaintext: "string", SQLDomain: "public.eql_v4_text", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL"}, + {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: "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable 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: "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: "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable 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: "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: false, Reason: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {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 a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms"}, - {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: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {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: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {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 a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms"}, - {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: "the engine derives match and OPE terms, but no EQL type is built from them yet"}, - {Name: "Boolean", GoName: "Boolean", Family: "boolean", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_boolean", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Real", GoName: "Real", Family: "real", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_real", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealEq", GoName: "RealEq", Family: "real", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_real_eq", Indexes: []string{"eq"}, Query: "RealEqQuery", QuerySQLDomain: "eql_v4.query_real_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrdOre", GoName: "RealOrdOre", Family: "real", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_real_ord_ore", Indexes: []string{"ore"}, Query: "RealOrdOreQuery", QuerySQLDomain: "eql_v4.query_real_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrd", GoName: "RealOrd", Family: "real", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_real_ord", Indexes: []string{"ope"}, Query: "RealOrdQuery", QuerySQLDomain: "eql_v4.query_real_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "RealOrdOpe", GoName: "RealOrdOpe", Family: "real", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_real_ord_ope", Indexes: []string{"ope"}, Query: "RealOrdOpeQuery", QuerySQLDomain: "eql_v4.query_real_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "Double", GoName: "Double", Family: "double", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_double", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleEq", GoName: "DoubleEq", Family: "double", Suffix: "Eq", Plaintext: "", SQLDomain: "public.eql_v4_double_eq", Indexes: []string{"eq"}, Query: "DoubleEqQuery", QuerySQLDomain: "eql_v4.query_double_eq", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrdOre", GoName: "DoubleOrdOre", Family: "double", Suffix: "OrdOre", Plaintext: "", SQLDomain: "public.eql_v4_double_ord_ore", Indexes: []string{"ore"}, Query: "DoubleOrdOreQuery", QuerySQLDomain: "eql_v4.query_double_ord_ore", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrd", GoName: "DoubleOrd", Family: "double", Suffix: "Ord", Plaintext: "", SQLDomain: "public.eql_v4_double_ord", Indexes: []string{"ope"}, Query: "DoubleOrdQuery", QuerySQLDomain: "eql_v4.query_double_ord", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, - {Name: "DoubleOrdOpe", GoName: "DoubleOrdOpe", Family: "double", Suffix: "OrdOpe", Plaintext: "", SQLDomain: "public.eql_v4_double_ord_ope", Indexes: []string{"ope"}, Query: "DoubleOrdOpeQuery", QuerySQLDomain: "eql_v4.query_double_ord_ope", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, + {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: false, Reason: "the engine derives match terms, but no EQL type is built from them yet"}, + {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: "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: "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (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: "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: "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (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: "Json", GoName: "JSON", Family: "json", Suffix: "", Plaintext: "", SQLDomain: "public.eql_v4_json", Indexes: nil, Query: "", QuerySQLDomain: "", Producible: false, Reason: "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is"}, + {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"}, } // Integer is the EQL type stored in public.eql_v4_integer: the integer family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int32. type Integer []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade. 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). type SmallintOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -487,7 +487,7 @@ func (v *SmallintOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Bigint is the EQL type stored in public.eql_v4_bigint: the bigint family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a int64. type Bigint []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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade. 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// 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). type NumericOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1335,7 +1335,7 @@ func (v *NumericOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Text is the EQL type stored in public.eql_v4_text: the text family with no index. -// The engine cannot produce it yet: the storage-only text domain follows once TextEq is proven end to end in PostgreSQL. +// The engine produces it from a string. type Text []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1406,7 +1406,7 @@ func (v *TextEqQuery) UnmarshalJSON(b []byte) error { } // TextMatch is the EQL type stored in public.eql_v4_text_match: the text family with the match index. -// The engine cannot produce it yet: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: the engine derives match terms, but no EQL type is built from them yet. type TextMatch []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: EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: the engine derives match and OPE terms, but no EQL type is built from them yet. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type TextSearch []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1688,7 +1688,7 @@ func (v *TextSearchQuery) UnmarshalJSON(b []byte) error { } // Boolean is the EQL type stored in public.eql_v4_boolean: the boolean family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a bool. type Boolean []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1712,7 +1712,7 @@ func (v *Boolean) UnmarshalJSON(b []byte) error { } // Real is the EQL type stored in public.eql_v4_real: the real family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a float32. type Real []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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type RealOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -1924,7 +1924,7 @@ func (v *RealOrdOpeQuery) UnmarshalJSON(b []byte) error { } // Double is the EQL type stored in public.eql_v4_double: the double family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine produces it from a float64. type Double []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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). 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: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002). type DoubleOrdOpe []byte // Value is the EQL value as the driver takes it: the JSON text, or NULL when empty. @@ -2183,7 +2183,7 @@ func (v *SteVecQuery) UnmarshalJSON(b []byte) error { } // JSON is the EQL type stored in public.eql_v4_json: the json family with no index. -// The engine cannot produce it yet: how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is. +// The engine cannot produce it yet: a JSON value seals as a tree of leaves and EQL's `c` holds one; how a document becomes one sealed leaf is not decided. type JSON []byte // 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 426158bd7..92ac9ea5a 100644 --- a/languages/golang/encrypt/eql/eql_test.go +++ b/languages/golang/encrypt/eql/eql_test.go @@ -7,7 +7,7 @@ import ( "github.com/cipherstash/stack/languages/golang/encrypt/eql" ) -func TestTypesTableNamesTextEqAsTheOneProducibleType(t *testing.T) { +func TestTypesTableNamesTheProducibleTypes(t *testing.T) { var producible []string for _, typ := range eql.Types { if typ.Producible { @@ -19,8 +19,19 @@ func TestTypesTableNamesTextEqAsTheOneProducibleType(t *testing.T) { t.Errorf("%s: an unproducible type has a reason", typ.Name) } } - if len(producible) != 1 || producible[0] != "TextEq" { - t.Fatalf("producible = %v, want [TextEq]", producible) + want := []string{"Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"} + if len(producible) != len(want) { + t.Fatalf("producible = %v, want %v", producible, want) + } + for i := range want { + if producible[i] != want[i] { + 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"} { + if typ, ok := eql.Lookup(name); !ok || typ.Plaintext != plaintext { + t.Errorf("%s: Plaintext = %q, want %q", name, typ.Plaintext, plaintext) + } } textEq, ok := eql.Lookup("TextEq") if !ok || textEq.GoName != "TextEq" || textEq.Plaintext != "string" || textEq.Query != "TextEqQuery" || textEq.SQLDomain != "public.eql_v4_text_eq" || len(textEq.Indexes) != 1 || textEq.Indexes[0] != "eq" { diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 09815fb6a..f9ce384e9 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -709,8 +709,10 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", + "vitaminc-aead", "vitaminc-aead-value", "vitaminc-prf", + "vitaminc-protected", "zeroize", ] diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 4afc43340..83df35d40 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -179,7 +179,11 @@ mod tests { .filter(|t| t.producible) .map(|t| t.name.as_str()) .collect(); - assert_eq!(producible, ["TextEq"], "the engine produces TextEq only"); + assert_eq!( + producible, + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], + "the engine's producible types, in catalog order" + ); assert!(matches!( resolver().resolve("TextOrdOre"), Err(TargetError::Unproducible { .. }) diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index 5a2f86639..53e0a42f4 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -1366,7 +1366,11 @@ fn targets_lists_what_the_build_holds() { .filter(|i| producible(i)) .filter_map(name_of) .collect(); - assert_eq!(producible, ["TextEq"], "the engine produces TextEq only"); + assert_eq!( + producible, + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], + "the engine's producible types, in catalog order" + ); } // ============================================================================= diff --git a/languages/golang/stashgen/engine.go b/languages/golang/stashgen/engine.go index baa20710f..f48cb5ac1 100644 --- a/languages/golang/stashgen/engine.go +++ b/languages/golang/stashgen/engine.go @@ -41,6 +41,11 @@ type EQLType struct { // Reason says why not when it does not. Producible bool Reason string + + // wire is the engine's exact plaintext kind (int32, not just an + // integer), when the engine named it. Plaintext is the coarse kind a + // field's Go type is compared with first; wire then holds the width. + wire record.Kind } // GuestEngine is the engine the SDK embeds: the WASI guest in package @@ -70,7 +75,7 @@ func (e *guestEngine) EQLTypes(ctx context.Context) ([]EQLType, error) { } out := make([]EQLType, 0, len(targets)) for _, t := range targets { - eqlType := EQLType{Name: t.Name, Plaintext: kindOfWire(t.Plaintext), Query: t.Query, Producible: t.Producible, Reason: t.Reason} + eqlType := EQLType{Name: t.Name, Plaintext: kindOfWire(t.Plaintext), Query: t.Query, Producible: t.Producible, Reason: t.Reason, wire: t.Plaintext} for _, term := range t.Indexes { if name, ok := indexOfOutput[term]; ok { eqlType.Indexes = append(eqlType.Indexes, name) @@ -161,6 +166,13 @@ func lowerDeclaration(d Declaration, eqlTypes []EQLType) (*record.Plan, error) { if eqlType.Plaintext != KindOther && eqlType.Plaintext != f.GoType.Kind { return nil, &FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, eqlType.Plaintext, f.GoType, f.GoType.Kind)} } + // The width too: Integer seals an int32 and Bigint an int64, Real + // a float32 and Double a float64. A narrower Go integer widens + // to the kind the generated code seals it as (int8 and int16 + // seal as int32), so the comparison is on that kind. + if eqlType.wire != record.Untyped && rf.Kind != eqlType.wire { + return nil, &FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals %s, and %s seals as %s", f.EQLType, eqlType.wire, f.GoType, rf.Kind)} + } // An EQL value is stored under a table and a column: the struct's // context is the table and the field's name the column, so a // context of two or more segments leaves the field no column, diff --git a/languages/golang/stashgen/enginetest/enginetest.go b/languages/golang/stashgen/enginetest/enginetest.go index 5a817c87a..31e8b8419 100644 --- a/languages/golang/stashgen/enginetest/enginetest.go +++ b/languages/golang/stashgen/enginetest/enginetest.go @@ -1,6 +1,7 @@ // Package enginetest is a static stand-in for the Rust engine in the -// generator's tests. It knows what the engine produces today: the EQL type -// TextEq, and the four indexes with the Go kinds each applies to. +// generator's tests. It knows what the engine produces today: the EQL types +// in eqlTypes, each with the width of value it seals, and the four indexes +// with the Go kinds each applies to. // // It is a test double, not a copy of the rules the SDK ships: the SDK's // generator asks the embedded guest. When the engine learns a type or an @@ -10,6 +11,7 @@ package enginetest import ( "context" "fmt" + "strings" "github.com/cipherstash/stack/languages/golang/stashgen" ) @@ -19,16 +21,47 @@ type Static struct{} var _ stashgen.Engine = Static{} -// EQLTypes returns TextEq, the one EQL type the engine produces today. +// eqlTypes are the EQL types the engine produces today, with the width of +// value each seals ("" when the kind has no width), mirroring the producible +// rows of encrypt/eql's Types. +var eqlTypes = []struct { + stashgen.EQLType + seals string +}{ + {stashgen.EQLType{Name: "Integer", Plaintext: stashgen.KindInt, Producible: true}, "int32"}, + {stashgen.EQLType{Name: "Bigint", Plaintext: stashgen.KindInt, Producible: true}, "int64"}, + {stashgen.EQLType{Name: "Text", Plaintext: stashgen.KindString, Producible: true}, ""}, + {stashgen.EQLType{Name: "TextEq", Plaintext: stashgen.KindString, Indexes: []stashgen.IndexName{stashgen.IndexEquality}, Query: "TextEqQuery", Producible: true}, ""}, + {stashgen.EQLType{Name: "Boolean", Plaintext: stashgen.KindBool, Producible: true}, ""}, + {stashgen.EQLType{Name: "Real", Plaintext: stashgen.KindFloat, Producible: true}, "float32"}, + {stashgen.EQLType{Name: "Double", Plaintext: stashgen.KindFloat, Producible: true}, "float64"}, +} + +// EQLTypes returns the EQL types the engine produces today. func (Static) EQLTypes(context.Context) ([]stashgen.EQLType, error) { - return []stashgen.EQLType{ - {Name: "TextEq", Plaintext: stashgen.KindString, Indexes: []stashgen.IndexName{stashgen.IndexEquality}, Query: "TextEqQuery", Producible: true}, - }, nil + out := make([]stashgen.EQLType, len(eqlTypes)) + for i, t := range eqlTypes { + out[i] = t.EQLType + } + return out, nil +} + +// sealsAs is the width the generated code seals a Go integer or float as: +// int8, int16 and int32 as int32, int and int64 as int64. +func sealsAs(t stashgen.GoType) string { + switch t.Basic { + case "int8", "int16", "int32": + return "int32" + case "int", "int64": + return "int64" + case "float32", "float64": + return t.Basic + } + return "" } // Check applies the engine's rules as they stand today. func (e Static) Check(ctx context.Context, d stashgen.Declaration) error { - eqlTypes, _ := e.EQLTypes(ctx) for _, f := range d.Fields { if f.Verb == stashgen.VerbOmit { continue @@ -39,17 +72,23 @@ func (e Static) Check(ctx context.Context, d stashgen.Declaration) error { } } if f.Verb == stashgen.VerbEncryptInto { - var found *stashgen.EQLType - for i := range eqlTypes { - if eqlTypes[i].Name == f.EQLType { - found = &eqlTypes[i] + var names []string + found := -1 + for i, t := range eqlTypes { + names = append(names, t.Name) + if t.Name == f.EQLType { + found = i } } - if found == nil { - return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("the engine cannot produce the EQL type %s yet; it produces TextEq", f.EQLType)} + if found < 0 { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("the engine cannot produce the EQL type %s yet; it produces %s", f.EQLType, strings.Join(names, ", "))} + } + t := eqlTypes[found] + if t.Plaintext != f.GoType.Kind { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, t.Plaintext, f.GoType, f.GoType.Kind)} } - if found.Plaintext != f.GoType.Kind { - return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals a %s, and %s is %s", f.EQLType, found.Plaintext, f.GoType, f.GoType.Kind)} + if t.seals != "" && sealsAs(f.GoType) != t.seals { + return &stashgen.FieldError{Type: d.Type, Field: f.GoName, Reason: fmt.Sprintf("%s seals %s, and %s seals as %s", f.EQLType, t.seals, f.GoType, sealsAs(f.GoType))} } } for _, idx := range f.Indexes { diff --git a/languages/golang/stashgen/lower_test.go b/languages/golang/stashgen/lower_test.go index 140776870..6a5101128 100644 --- a/languages/golang/stashgen/lower_test.go +++ b/languages/golang/stashgen/lower_test.go @@ -3,6 +3,8 @@ package stashgen import ( "strings" "testing" + + "github.com/cipherstash/stack/languages/golang/internal/record" ) // A struct that names an EQL type beside a context_field has no table of @@ -28,3 +30,48 @@ func TestLoweringRefusesAnEQLTypeBesideAContextField(t *testing.T) { t.Fatalf("lowerDeclaration = %+v, %v", plan, err) } } + +// An EQL type seals one width of its kind. A Go field of the same kind and +// another width is refused here, naming both, rather than by the engine's +// generic refusal; a narrower integer that seals as the type's width passes. +func TestLoweringRefusesAnotherWidthOfTheKind(t *testing.T) { + eql := []EQLType{ + {Name: "Integer", Plaintext: KindInt, Producible: true, wire: record.Int32}, + {Name: "Bigint", Plaintext: KindInt, Producible: true, wire: record.Int64}, + {Name: "Real", Plaintext: KindFloat, Producible: true, wire: record.Float32}, + {Name: "Double", Plaintext: KindFloat, Producible: true, wire: record.Float64}, + } + field := func(basic string, kind Kind, eqlType string) Declaration { + return Declaration{Type: "Row", Context: "rows", Fields: []Field{ + {Name: "v", GoName: "V", GoType: GoType{Name: basic, Kind: kind, Basic: basic}, Verb: VerbEncryptInto, EQLType: eqlType}, + }} + } + for _, c := range []struct { + basic, eqlType, want string + kind Kind + }{ + {"int", "Integer", "Integer seals int32, and int seals as int64", KindInt}, + {"int64", "Integer", "Integer seals int32, and int64 seals as int64", KindInt}, + {"int32", "Bigint", "Bigint seals int64, and int32 seals as int32", KindInt}, + {"float64", "Real", "Real seals float32, and float64 seals as float64", KindFloat}, + {"float32", "Double", "Double seals float64, and float32 seals as float32", KindFloat}, + } { + _, err := lowerDeclaration(field(c.basic, c.kind, c.eqlType), eql) + fe, ok := err.(*FieldError) + if !ok || fe.Field != "V" || fe.Reason != c.want { + t.Errorf("%s into %s: lowerDeclaration = %v, want %q", c.basic, c.eqlType, err, c.want) + } + } + for _, c := range []struct { + basic, eqlType string + kind Kind + }{ + {"int32", "Integer", KindInt}, {"int16", "Integer", KindInt}, {"int8", "Integer", KindInt}, + {"int64", "Bigint", KindInt}, {"int", "Bigint", KindInt}, + {"float32", "Real", KindFloat}, {"float64", "Double", KindFloat}, + } { + if _, err := lowerDeclaration(field(c.basic, c.kind, c.eqlType), eql); err != nil { + t.Errorf("%s into %s: %v", c.basic, c.eqlType, err) + } + } +} diff --git a/languages/golang/stashgen/refusal_test.go b/languages/golang/stashgen/refusal_test.go index b962c0084..2ab440c56 100644 --- a/languages/golang/stashgen/refusal_test.go +++ b/languages/golang/stashgen/refusal_test.go @@ -80,6 +80,8 @@ func TestRefusals(t *testing.T) { {"a _ field with no context", user("\t_ struct{}\n\tEmail string `stash:\"email,encrypt\"`"), stashgen.Request{Type: "User"}, "_", "carries the context tag"}, {"an index that does not apply to the field type", user(ctx + "\tAge int32 `stash:\"age,encrypt,index=match\"`"), stashgen.Request{Type: "User"}, "Age", "match applies to a string, not to int32"}, {"an EQL type that does not apply to the field type", user(ctx + "\tAge int64 `stash:\"age,encrypt_into=TextEq\"`"), stashgen.Request{Type: "User"}, "Age", "TextEq seals a string, and int64 is int"}, + {"an EQL type of another width", user(ctx + "\tAge int64 `stash:\"age,encrypt_into=Integer\"`"), stashgen.Request{Type: "User"}, "Age", "Integer seals int32, and int64 seals as int64"}, + {"a float of another width", user(ctx + "\tScore float64 `stash:\"score,encrypt_into=Real\"`"), stashgen.Request{Type: "User"}, "Score", "Real seals float32, and float64 seals as float64"}, {"a field type the engine cannot seal", user(ctx + "\tDone chan int `stash:\"done,encrypt\"`"), stashgen.Request{Type: "User"}, "Done", "seals only as part of an opaque struct"}, {"a composite sealed outside an opaque struct", user(ctx + "\tAt time.Time `stash:\"at,encrypt\"`"), stashgen.Request{Type: "User"}, "At", "a time.Time seals only as part of an opaque struct"}, {"a slice sealed outside an opaque struct", user(ctx + "\tTags []string `stash:\"tags,encrypt\"`"), stashgen.Request{Type: "User"}, "Tags", "seals only as part of an opaque struct"}, diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index e053231f3..f2cfa8072 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -1204,8 +1204,10 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", + "vitaminc-aead 0.5.1", "vitaminc-aead-value", "vitaminc-prf", + "vitaminc-protected 0.5.1", "zeroize", ] diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 602ff7851..2d1ea219d 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -55,6 +55,11 @@ vitaminc-prf = { version = "0.5.0", optional = true } # nothing from stack-encrypt's own `dynamic` feature; the same line # stack-encrypt and the Go guest build against. vitaminc-aead-value = { version = "0.5.1", optional = true } +# `DecipherVisitor` and `Protected`: the scalar plaintexts in +# `encryption::plaintext` seal and open vitaminc's tagged leaves themselves, +# because vitaminc 0.5 has no native `Encrypt` for the scalar types. +vitaminc-aead = { version = "0.5.1", optional = true } +vitaminc-protected = { version = "0.5.1", 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 } @@ -68,6 +73,8 @@ stack-encrypt = [ "dep:stack-encrypt", "dep:vitaminc-prf", "dep:vitaminc-aead-value", + "dep:vitaminc-aead", + "dep:vitaminc-protected", "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 f4bb4c87e..86c2a9a8c 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -104,8 +104,29 @@ evolution plan. The optional `stack-encrypt` feature enables `EncryptFrom` for `v4::text::TextEq` and `v4::text::TextEqQuery`, and `DecryptInto` for `v4::text::TextEq`. Stack Encrypt writes EQL v4 only, so no `v3` type -implements these traits. Other v4 domains do not yet expose these conversions. -The derives are emitted by `eql-codegen`. +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`, because vitaminc 0.5 has no native +encryption for the scalar types: + +| v4 type | Plaintext | +| --- | --- | +| `Text` | `String` | +| `Integer` | `Int32` | +| `Bigint` | `Int64` | +| `Boolean` | `Bool` | +| `Real` | `Float32` | +| `Double` | `Float64` | + +Each seals as vitaminc's tagged leaf, the bytes the value enum (`FfiValue`) +seals for the same value, so values written here and by the Go SDK open each +other. `IntegerEq` and `BigintEq` wait for vitaminc 0.6, whose integer +equality term uses one PRF domain for every width; terms derived under +vitaminc 0.5's per-width domains would stop matching after that upgrade. +Ordering types wait for vitaminc 0.6's order encodings, float +equality for its canonical float form, 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 @@ -162,7 +183,7 @@ assumed. Outcomes against today's sources: | Writer | Reader and query producer | Outcome | | --- | --- | --- | -| Rust (`stack-encrypt` profile) | Rust (`stack-encrypt` profile) | **Proven.** `tests/encryption/tests/text_eq.rs`: JSON round trip, both canonical directions, PostgreSQL insert and equality query. | +| Rust (`stack-encrypt` profile) | Rust (`stack-encrypt` profile) | **Proven.** `tests/encryption/tests/text_eq.rs`: JSON round trip, both canonical directions, PostgreSQL insert and equality query. `tests/encryption/tests/v4_types.rs`: the same for the other producible v4 types, in both directions between the typed plaintexts and the value enum. | | JS / FFI (`cipherstash-client` profile) | Rust | **Not supported.** `c` lacks the `stack-encrypt:1:` prefix and is refused loudly on decryption. A Rust probe against such a column is accepted by SQL and matches nothing. | | Rust (`stack-encrypt` profile) | JS / FFI | **Not supported.** The JS reader does not know the `stack-encrypt:1:` prefix and the two profiles' equality terms never match. | @@ -214,7 +235,8 @@ crate would need stack-encrypt API newer than the crates.io release the published crate names, and the guest already depends on both. A type is producible exactly when its generated struct carries the `stack-encrypt` derives (`ENCRYPTION_DOMAINS` in `eql-codegen`), because the dispatch runs the derived -plan; `TextEq` is the only one today. +plan; `targets::targets()` lists them, each with `producible` and, when it is +`false`, the reason. ### Developing the `stack-encrypt` feature diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index 3588f30f6..0134178a3 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -78,6 +78,7 @@ //! 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 new file mode 100644 index 000000000..610a7ad9b --- /dev/null +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -0,0 +1,289 @@ +//! Scalar plaintexts for the v4 number and boolean types. +//! +//! A Stack Encrypt target encrypts a plaintext through Vitamin C's +//! `Encrypt` and `Decrypt` traits, and vitaminc 0.5 implements +//! `Encrypt` for `String` but not for `i32`, `i64`, `bool`, `f32` or `f64`. +//! These newtypes fill that gap for the storage-only EQL v4 types: [`Int32`] +//! for `Integer`, [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and +//! [`Float32`] / [`Float64`] for `Real` and `Double`. +//! +//! Each seals as vitaminc's tagged leaf, `[tag] ++ payload` from the frozen +//! table in `vitaminc_aead_value::tags`, which is exactly what the value enum +//! (`FfiValue`) seals for the same value. So a value written through these +//! types and one written by the Go SDK as a plain `int32` field open each +//! other. +//! +//! None of them derives an equality term. `IntegerEq` and `BigintEq` are held +//! back until vitaminc 0.6, whose integer equality term uses one PRF domain +//! for every width; a term derived here under vitaminc 0.5's per-width +//! domains would stop matching after that upgrade. +//! +//! ```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::{Cipher, Decipher, Decrypt, Encrypt, IntoAad, Unspecified}; +use vitaminc_aead::DecipherVisitor; +use vitaminc_aead_value::tagged::{ + TaggedBoolFalse, TaggedBoolTrue, TaggedFloat32, TaggedFloat64, TaggedInt32, TaggedInt64, +}; +use vitaminc_aead_value::tags; +use vitaminc_protected::{Controlled, Protected}; +use zeroize::Zeroize; + +/// Reads one tagged scalar leaf back, refusing any other tag or width: a +/// value sealed as one kind never opens as another. +struct LeafVisitor { + tag: u8, + parse: fn(&[u8]) -> Option, +} + +impl<'c, T: Send> DecipherVisitor<'c> for LeafVisitor { + type Value = T; + + fn visit_bytes_vec(self, data: Protected>) -> Result { + match data.risky_ref().split_first() { + Some((&tag, payload)) if tag == self.tag => (self.parse)(payload).ok_or(Unspecified), + _ => Err(Unspecified), + } + } +} + +/// Bytes a [`LeafVisitor`] reads back: one tag, then exactly `N` payload +/// bytes. Anything else is not this kind. +fn leaf_payload(payload: &[u8]) -> Option<[u8; N]> { + payload.try_into().ok() +} + +macro_rules! fixed_scalar { + ($(#[$doc:meta])* $name:ident($inner:ty), $tagged:ty, $tag:expr, $width:literal, |$bytes:ident| $decode: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 + } + } + + impl Encrypt for $name { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + <$tagged>::from(self.0).encrypt_with_aad(cipher, aad) + } + } + + impl<'c> Decrypt<'c> for $name { + fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok + where + D: Decipher<'c>, + A: IntoAad<'a>, + { + decipher.decrypt_any(Self::visitor(), aad) + } + } + + impl $name { + /// The reader [`Decrypt`] opens this type's leaf with. + fn visitor() -> LeafVisitor { + LeafVisitor { + tag: $tag, + parse: |payload| { + let $bytes = leaf_payload::<$width>(payload)?; + Some($name($decode)) + }, + } + } + } + }; +} + +fixed_scalar!( + /// The plaintext of `Integer`: a 32-bit signed integer. + Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b), |a, b| a == b +); +fixed_scalar!( + /// The plaintext of `Bigint`: a 64-bit signed integer. + Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b), |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), TaggedFloat32, tags::FLOAT32, 4, |b| f32::from_bits(u32::from_le_bytes(b)), + |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), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)), + |a, b| a.to_bits() == b.to_bits() +); + +/// The plaintext of `Boolean`. A boolean is the tag itself: no payload. +#[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 + } +} + +impl Encrypt for Bool { + fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result + where + C: Cipher, + A: IntoAad<'a>, + { + if self.0 { + TaggedBoolTrue::tag_only().encrypt_with_aad(cipher, aad) + } else { + TaggedBoolFalse::tag_only().encrypt_with_aad(cipher, aad) + } + } +} + +/// Opens either boolean tag, and nothing else. +struct BoolVisitor; + +impl<'c> DecipherVisitor<'c> for BoolVisitor { + type Value = Bool; + + fn visit_bytes_vec(self, data: Protected>) -> Result { + match data.risky_ref().as_slice() { + [tags::BOOL_TRUE] => Ok(Bool(true)), + [tags::BOOL_FALSE] => Ok(Bool(false)), + _ => Err(Unspecified), + } + } +} + +impl<'c> Decrypt<'c> for Bool { + fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok + where + D: Decipher<'c>, + A: IntoAad<'a>, + { + decipher.decrypt_any(BoolVisitor, aad) + } +} + +#[cfg(test)] +mod tests { + //! The leaf each type reads back, byte for byte. Sealing goes through + //! vitaminc's own `Tagged*` types; what is hand-written here is the + //! reading, so these pin its tag, width and byte order. The round trip + //! against the value enum (`FfiValue`) is in the encryption test crate + //! (`tests/encryption/tests/v4_types.rs`). + use super::*; + + fn leaf(tag: u8, payload: &[u8]) -> Protected> { + let mut bytes = vec![tag]; + bytes.extend_from_slice(payload); + Protected::new(bytes) + } + + fn read<'c, V: DecipherVisitor<'c>>(visitor: V, bytes: Protected>) -> Option { + visitor.visit_bytes_vec(bytes).ok() + } + + #[test] + fn integers_read_their_own_little_endian_leaf() { + let (int32, int64) = (Int32::visitor, Int64::visitor); + for v in [0, 1, -1, 42, i32::MIN, i32::MAX] { + assert!(read(int32(), leaf(tags::INT32, &v.to_le_bytes())) == Some(Int32(v))); + } + for v in [0, -1, 42, i64::MIN, i64::MAX] { + assert!(read(int64(), leaf(tags::INT64, &v.to_le_bytes())) == Some(Int64(v))); + } + // Little-endian, not big: 1 is 01 00 00 00. + assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0, 0])) == Some(Int32(1))); + } + + #[test] + fn a_leaf_of_another_tag_or_width_is_refused() { + let (int32, int64) = (Int32::visitor, Int64::visitor); + // An Int64 leaf is not an Int32, even when its value would fit. + assert!(read(int32(), leaf(tags::INT64, &1i64.to_le_bytes())).is_none()); + assert!(read(int64(), leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); + // The right tag with the wrong width. + assert!(read(int32(), leaf(tags::INT32, &1i64.to_le_bytes())).is_none()); + assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0])).is_none()); + assert!(read(int64(), leaf(tags::INT64, &1i32.to_le_bytes())).is_none()); + // No tag at all. + assert!(read(int32(), Protected::new(Vec::new())).is_none()); + // A float's leaf is not an integer's. + assert!(read(int32(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); + } + + #[test] + fn floats_read_their_bit_pattern() { + let (f32s, f64s) = (Float32::visitor, Float64::visitor); + 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(f32s(), leaf(tags::FLOAT32, &v.to_bits().to_le_bytes())).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(f64s(), leaf(tags::FLOAT64, &v.to_bits().to_le_bytes())).unwrap(); + assert_eq!(opened.0.to_bits(), v.to_bits()); + } + assert!(read(f64s(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); + assert!(read(f32s(), leaf(tags::FLOAT32, &1f64.to_le_bytes())).is_none()); + } + + #[test] + fn booleans_are_the_tag_alone() { + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE])) == Some(Bool(true))); + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_FALSE])) == Some(Bool(false))); + // A payload after the tag, or another tag, is not a boolean. + assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE, 0])).is_none()); + assert!(read(BoolVisitor, leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); + assert!(read(BoolVisitor, Protected::new(Vec::new())).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 b3c6d9944..764acb045 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -86,6 +86,7 @@ use stack_encrypt::{ }; use vitaminc_aead_value::{FfiValue, ValueKind}; +use super::plaintext; use crate::v4::targets::{decrypt_named, encrypt_named, query_named, TARGETS}; use crate::Identifier; @@ -383,8 +384,15 @@ 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 {} } /// A Rust plaintext an EQL type is produced from, read out of and written @@ -392,7 +400,7 @@ mod sealed { /// plaintext types the catalog's producible families name, and the generated /// dispatch picks one per type. `Zeroize`, because [`from_value`](Self::from_value) /// copies the plaintext out of the runtime value's `Protected` buffer and -/// the copy is wiped when it is dropped (the dispatch holds it in +/// the copy is wiped when it is dropped (`run_target` holds it in /// [`zeroize::Zeroizing`]), as the original is. pub trait Plaintext: sealed::Sealed + Sized + MaybeSend + zeroize::Zeroize + 'static { /// The kind of value this plaintext is. @@ -429,6 +437,39 @@ impl Plaintext for String { } } +/// The scalar plaintexts read exactly their own kind: a JSON number is not +/// narrowed or widened into another width, because the stored bytes must be +/// the declared kind's. +macro_rules! scalar_plaintext { + ($($ty:ident => $variant:ident),+ $(,)?) => {$( + impl Plaintext for plaintext::$ty { + const KIND: ValueKind = ValueKind::$variant; + + fn from_value(target: &'static str, value: FfiValue) -> Result { + match value { + FfiValue::$variant(v) => Ok(plaintext::$ty(v)), + other => Err(TargetError::Plaintext { + target, + expected: Self::KIND, + found: other.kind(), + }), + } + } + + fn into_value(self) -> FfiValue { + FfiValue::$variant(self.0) + } + } + )+}; +} +scalar_plaintext!( + Int32 => Int32, + Int64 => Int64, + Bool => Bool, + Float32 => Float32, + Float64 => Float64, +); + /// Run a target's own plan over one runtime value and resolve to the EQL /// value's JSON bytes. The generated dispatch calls this with the type and /// its plaintext; it is the one place the typed `encrypt_as` is reached from @@ -541,28 +582,13 @@ mod tests { #[test] fn a_producible_type_without_a_query_twin_answers_no_query() { - // No such type in the catalog today (`Text` is not producible), so - // the rule is pinned on a synthetic row; `refuse_query` on the live - // table falls through to `refuse`. - let storage_only = Target { - name: "Text", - family: "text", - suffix: "", - plaintext: Some("string"), - sql_domain: "public.eql_v4_text", - indexes: &[], - query: None, - query_sql_domain: None, - producible: true, - reason: None, - }; - let leaked: &'static Target = Box::leak(Box::new(storage_only)); + // `Text` is producible and storage-only, so it has no query twin. assert!(matches!( - no_query(Some(leaked)), + no_query(target("Text")), Some(TargetError::NoQuery { name: "Text" }) )); assert_eq!( - no_query(Some(leaked)).unwrap().to_string(), + refuse_query("Text").to_string(), "Text answers no query: it is a storage-only type" ); assert!( @@ -570,11 +596,11 @@ mod tests { "TextEq answers a query" ); assert!( - no_query(target("Text")).is_none(), - "an unproducible type is refused as that" + no_query(target("Json")).is_none(), + "an unproducible type without a query twin is refused as unproducible" ); assert!(matches!( - refuse_query("Text"), + refuse_query("Json"), TargetError::Unproducible { .. } )); assert!(matches!(refuse_query("Nope"), TargetError::Unknown { .. })); @@ -585,7 +611,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("CLLW") + TargetError::Unproducible { name: "TextOrdOre", reason } if reason.contains("vitaminc-ore") )); assert!(producible("TextEq").is_ok()); } diff --git a/packages/eql/crates/eql-bindings/src/v4/bigint.rs b/packages/eql/crates/eql-bindings/src/v4/bigint.rs index 98e4aa73a..6059c0931 100644 --- a/packages/eql/crates/eql-bindings/src/v4/bigint.rs +++ b/packages/eql/crates/eql-bindings/src/v4/bigint.rs @@ -14,10 +14,35 @@ 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) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Bigint { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/boolean.rs b/packages/eql/crates/eql-bindings/src/v4/boolean.rs index ca4a1c71a..be25bdbf2 100644 --- a/packages/eql/crates/eql-bindings/src/v4/boolean.rs +++ b/packages/eql/crates/eql-bindings/src/v4/boolean.rs @@ -14,10 +14,35 @@ 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) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Boolean { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", 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 bd73e4043..ee52d3ce3 100644 --- a/packages/eql/crates/eql-bindings/src/v4/double.rs +++ b/packages/eql/crates/eql-bindings/src/v4/double.rs @@ -14,10 +14,35 @@ 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) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Double { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/integer.rs b/packages/eql/crates/eql-bindings/src/v4/integer.rs index f320c8794..508595305 100644 --- a/packages/eql/crates/eql-bindings/src/v4/integer.rs +++ b/packages/eql/crates/eql-bindings/src/v4/integer.rs @@ -14,10 +14,35 @@ 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) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Integer { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/real.rs b/packages/eql/crates/eql-bindings/src/v4/real.rs index 6d6ef3b88..01f12b7da 100644 --- a/packages/eql/crates/eql-bindings/src/v4/real.rs +++ b/packages/eql/crates/eql-bindings/src/v4/real.rs @@ -14,10 +14,35 @@ 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) +)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Real { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } diff --git a/packages/eql/crates/eql-bindings/src/v4/targets.rs b/packages/eql/crates/eql-bindings/src/v4/targets.rs index cfaa39d8b..e89ab754b 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -1,5 +1,10 @@ // @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, }; @@ -15,70 +20,68 @@ pub const TARGETS: &[Target] = &[ name: "Integer", family: "integer", suffix: "", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "IntegerEq", family: "integer", suffix: "Eq", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_eq", indexes: &["eq"], query: Some("IntegerEqQuery"), query_sql_domain: Some("eql_v4.query_integer_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade", ), }, Target { name: "IntegerOrdOre", family: "integer", suffix: "OrdOre", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord_ore", indexes: &["ore"], query: Some("IntegerOrdOreQuery"), query_sql_domain: Some("eql_v4.query_integer_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "IntegerOrd", family: "integer", suffix: "Ord", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord", indexes: &["ope"], query: Some("IntegerOrdQuery"), query_sql_domain: Some("eql_v4.query_integer_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "IntegerOrdOpe", family: "integer", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("int32"), sql_domain: "public.eql_v4_integer_ord_ope", indexes: &["ope"], query: Some("IntegerOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_integer_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -92,7 +95,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -106,7 +109,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -120,7 +123,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -134,7 +137,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -148,77 +151,75 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_smallint_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Bigint", family: "bigint", suffix: "", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "BigintEq", family: "bigint", suffix: "Eq", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_eq", indexes: &["eq"], query: Some("BigintEqQuery"), query_sql_domain: Some("eql_v4.query_bigint_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF domain; producing it before that release would leave stored terms unmatchable after the upgrade", ), }, Target { name: "BigintOrdOre", family: "bigint", suffix: "OrdOre", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord_ore", indexes: &["ore"], query: Some("BigintOrdOreQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "BigintOrd", family: "bigint", suffix: "Ord", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord", indexes: &["ope"], query: Some("BigintOrdQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "BigintOrdOpe", family: "bigint", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("int64"), sql_domain: "public.eql_v4_bigint_ord_ope", indexes: &["ope"], query: Some("BigintOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_bigint_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -232,7 +233,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -246,7 +247,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -260,7 +261,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -274,7 +275,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -288,7 +289,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_date_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -302,7 +303,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -316,7 +317,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -330,7 +331,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -344,7 +345,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -358,7 +359,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_timestamp_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -372,7 +373,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -386,7 +387,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -400,7 +401,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -414,7 +415,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -428,7 +429,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_numeric_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5's value enum has no kind for this family; the kind and its encodings arrive in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -440,10 +441,8 @@ pub const TARGETS: &[Target] = &[ indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL", - ), + producible: true, + reason: None, }, Target { name: "TextEq", @@ -468,7 +467,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_match"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "the engine derives match terms, but no EQL type is built from them yet", ), }, Target { @@ -482,7 +481,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ore"), producible: false, reason: Some( - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -496,7 +495,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -510,7 +509,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_ord_ope"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -524,7 +523,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search_ore"), producible: false, reason: Some( - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -538,161 +537,155 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: Some("eql_v4.query_text_search"), producible: false, reason: Some( - "the engine derives match and OPE terms, but no EQL type is built from them yet", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Boolean", family: "boolean", suffix: "", - plaintext: None, + plaintext: Some("bool"), sql_domain: "public.eql_v4_boolean", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "Real", family: "real", suffix: "", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "RealEq", family: "real", suffix: "Eq", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_eq", indexes: &["eq"], query: Some("RealEqQuery"), query_sql_domain: Some("eql_v4.query_real_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrdOre", family: "real", suffix: "OrdOre", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord_ore", indexes: &["ore"], query: Some("RealOrdOreQuery"), query_sql_domain: Some("eql_v4.query_real_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrd", family: "real", suffix: "Ord", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord", indexes: &["ope"], query: Some("RealOrdQuery"), query_sql_domain: Some("eql_v4.query_real_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "RealOrdOpe", family: "real", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("float32"), sql_domain: "public.eql_v4_real_ord_ope", indexes: &["ope"], query: Some("RealOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_real_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "Double", family: "double", suffix: "", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double", indexes: &[], query: None, query_sql_domain: None, - producible: false, - reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", - ), + producible: true, + reason: None, }, Target { name: "DoubleEq", family: "double", suffix: "Eq", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_eq", indexes: &["eq"], query: Some("DoubleEqQuery"), query_sql_domain: Some("eql_v4.query_double_eq"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "vitaminc 0.5 derives no equality term for a float; the canonical float form arrives in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrdOre", family: "double", suffix: "OrdOre", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord_ore", indexes: &["ore"], query: Some("DoubleOrdOreQuery"), query_sql_domain: Some("eql_v4.query_double_ord_ore"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrd", family: "double", suffix: "Ord", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord", indexes: &["ope"], query: Some("DoubleOrdQuery"), query_sql_domain: Some("eql_v4.query_double_ord"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { name: "DoubleOrdOpe", family: "double", suffix: "OrdOpe", - plaintext: None, + plaintext: Some("float64"), sql_domain: "public.eql_v4_double_ord_ope", indexes: &["ope"], query: Some("DoubleOrdOpeQuery"), query_sql_domain: Some("eql_v4.query_double_ord_ope"), producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "ordering terms wait for vitaminc-ore, the order encodings in vitaminc 0.6 (ADR-0002)", ), }, Target { @@ -718,7 +711,7 @@ pub const TARGETS: &[Target] = &[ query_sql_domain: None, producible: false, reason: Some( - "how this family encodes a plaintext for the stack-encrypt producer profile is not specified; only the text family's encoding is", + "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", ), }, ]; @@ -732,9 +725,23 @@ pub(crate) fn encrypt_named<'a, K: 'static>( plaintext: FfiValue, ) -> Result, K>, TargetError> { match name { + "Integer" => { + run_target::("Integer", keyset, column, plaintext) + } + "Bigint" => { + run_target::("Bigint", keyset, column, plaintext) + } + "Text" => run_target::("Text", keyset, column, plaintext), "TextEq" => { run_target::("TextEq", keyset, column, plaintext) } + "Boolean" => { + run_target::("Boolean", keyset, column, plaintext) + } + "Real" => run_target::("Real", keyset, column, plaintext), + "Double" => { + run_target::("Double", keyset, column, plaintext) + } _ => Err(refuse(name)), } } @@ -747,7 +754,21 @@ pub(crate) fn decrypt_named<'a, K: 'static>( stored: &[u8], ) -> Result, TargetError> { match name { + "Integer" => { + open_target::("Integer", opener, column, stored) + } + "Bigint" => { + open_target::("Bigint", opener, column, stored) + } + "Text" => open_target::("Text", opener, column, stored), "TextEq" => open_target::("TextEq", opener, column, stored), + "Boolean" => { + open_target::("Boolean", opener, column, stored) + } + "Real" => open_target::("Real", opener, column, stored), + "Double" => { + open_target::("Double", opener, column, stored) + } _ => Err(refuse(name)), } } diff --git a/packages/eql/crates/eql-bindings/src/v4/text.rs b/packages/eql/crates/eql-bindings/src/v4/text.rs index ce20abc8a..28ae34d1b 100644 --- a/packages/eql/crates/eql-bindings/src/v4/text.rs +++ b/packages/eql/crates/eql-bindings/src/v4/text.rs @@ -14,10 +14,32 @@ 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))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Text { + #[cfg_attr(feature = "stack-encrypt", stash(default))] pub v: SchemaVersion, + #[cfg_attr(feature = "stack-encrypt", stash(context_field))] pub i: Identifier, pub c: Ciphertext, } @@ -57,8 +79,12 @@ impl DomainType for Text { )] #[cfg_attr(doc, doc = "")] #[cfg_attr( - doc, - doc = " Plaintext input and decrypted output: [`String`]. With the `stack-encrypt` feature, implements `EncryptFrom` and `DecryptInto`." + 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"), @@ -376,8 +402,12 @@ impl DomainType for TextSearch { )] #[cfg_attr(doc, doc = "")] #[cfg_attr( - doc, - doc = " Plaintext input: [`String`]. With the `stack-encrypt` feature, implements `EncryptFrom` to build equality query terms. Query operands do not decrypt." + 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"), diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index bb96b4fc4..e0a100094 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -333,17 +333,43 @@ fn render_query_struct(eql: EqlVersion, family: &DomainFamily, domain: &Domain) } } -/// The `(family, domain)` pairs whose generated structs carry the opt-in -/// `stack-encrypt` derives. Text equality is the first; 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)] = &[("text", "eq")]; +/// 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 +/// cannot arrive without either a derive or a reason. +pub const ENCRYPTION_DOMAINS: &[(&str, &str)] = &[ + ("integer", ""), + ("bigint", ""), + ("text", ""), + ("text", "eq"), + ("boolean", ""), + ("real", ""), + ("double", ""), +]; /// Why no `eql_v3` struct derives Stack Encrypt's traits. pub const V3_ENCRYPTION_GAP: &str = "Stack Encrypt produces EQL v4 only: its output is not byte-for-byte compatible with \ cipherstash-client's, which writes the eql_v3 domains"; +/// 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. +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"), + _ => return None, + }) +} + /// Why a stored domain carries no `stack-encrypt` derive yet, or `None` when /// it does. Derived from the catalog's own facts (family, terms), not from a /// per-domain list, so it answers for a domain added tomorrow. No `eql_v3` @@ -359,22 +385,34 @@ pub fn encryption_gap( if ENCRYPTION_DOMAINS.contains(&(family.name, domain.name)) { return None; } - Some(if family.name != "text" { - "only the text family's plaintext encoding is specified for the stack-encrypt \ - producer profile; the numeric, date, boolean and JSON encodings are plan section 2 \ - and have not been settled" - } else if domain.terms.iter().any(|t| !matches!(t, Term::Hm)) { - "ordering and match terms need stack-encrypt operations that have not landed \ - (block-ORE and OPE term derivation, bloom-filter match terms)" - } else if domain.terms.is_empty() { - "the storage-only text domain follows once TextEq is proven end to end in \ - PostgreSQL and the interoperability matrix is recorded" + Some(if !domain.is_scalar() { + "the JSON index is a new operation in the engine" + } 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 domain.terms.contains(&Term::Bloom) { + "the engine derives match terms, but no EQL type is built from them yet" + } else if matches!(family.name, "integer" | "bigint") && domain.terms == [Term::Hm] { + "the integer equality term moves to vitaminc 0.6's shared integer-orderable/v1 PRF \ + domain; producing it before that release would leave stored terms unmatchable after \ + the upgrade" + } else if matches!(family.name, "real" | "double") { + "vitaminc 0.5 derives no equality term for a float; the canonical float form \ + arrives in vitaminc 0.6 (ADR-0002)" } else { - // Reached by an equality-only text domain other than `eq`: nothing in - // the catalog today, but the branch above would otherwise call it - // storage-only, which it is not. - "an equality-only text domain carries the same term TextEq does and derives the \ - same way; it is not listed in ENCRYPTION_DOMAINS yet" + // Reached by an equality-only domain of a family whose plaintext is + // specified but which is not listed: nothing in the catalog today. + "this domain derives the way its family's other domains do; it is not listed in \ + ENCRYPTION_DOMAINS yet" }) } @@ -393,22 +431,40 @@ fn encryption_attrs( let decrypt = (!query).then( || quote!(#[cfg_attr(feature = "stack-encrypt", derive(stack_encrypt::DecryptInto))]), ); - let plaintext = if query { - " Plaintext input: [`String`]. With the `stack-encrypt` feature, implements \ - `EncryptFrom` to build equality query terms. Query operands do not decrypt." - } else { - " Plaintext input and decrypted output: [`String`]. With the `stack-encrypt` \ - feature, implements `EncryptFrom` and `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 { + format!( + " Plaintext input: {named}. With the `stack-encrypt` feature, \ + implements `EncryptFrom<{shown}>` to build equality 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}>`." + ) + } }; + let plaintext = plaintext_doc(&format!("[`{shown}`]({rust})")); + let plaintext_unlinked = plaintext_doc(&format!("`{shown}`")); ( quote! { #[cfg_attr(doc, doc = "")] - #[cfg_attr(doc, doc = #plaintext)] + #[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))] #decrypt - #[cfg_attr(feature = "stack-encrypt", stash(plaintext = String))] + #[cfg_attr(feature = "stack-encrypt", stash(plaintext = #plaintext_ty))] }, quote!(#[cfg_attr(feature = "stack-encrypt", stash(default))]), quote!(#[cfg_attr(feature = "stack-encrypt", stash(context_field))]), @@ -1664,16 +1720,28 @@ mod tests { // catalog fact that selects it. Some(reason) => { let full = domain.full_name(family.name); - let expected = if family.name != "text" { - "only the text family" - } else if domain.terms.iter().any(|t| !matches!(t, Term::Hm)) { - "ordering and match terms" + let expected = if !domain.is_scalar() { + "JSON index" + } 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 domain.terms.contains(&Term::Bloom) { + "match terms" + } else if matches!(family.name, "integer" | "bigint") { + "integer-orderable/v1" } else { assert!( - domain.terms.is_empty(), - "{full}: an hm-only text domain other than eq is not in the catalog" + matches!(family.name, "real" | "double"), + "{full}: every other specified, unordered domain is derived" ); - "storage-only text domain" + "equality term for a float" }; assert!(reason.contains(expected), "{full}: {reason}"); } @@ -1711,7 +1779,7 @@ mod tests { } #[test] - fn an_equality_only_text_domain_is_not_called_storage_only() { + fn an_unlisted_equality_only_domain_says_it_is_unlisted() { // Not in the catalog, which is why the branch needs a synthetic // domain: the only text domain reaching the hm-only branch today has // no terms, so a wrong reason there would survive every catalog walk. @@ -1725,7 +1793,10 @@ mod tests { shape: eql_domains::Shape::Scalar, }; let reason = encryption_gap(EqlVersion::V4, &family, &hm_only).unwrap(); - assert!(reason.contains("equality-only"), "{reason}"); + assert!( + reason.contains("not listed in ENCRYPTION_DOMAINS"), + "{reason}" + ); assert!(!reason.contains("storage-only"), "{reason}"); } } diff --git a/packages/eql/crates/eql-codegen/src/go_eql.rs b/packages/eql/crates/eql-codegen/src/go_eql.rs index c9ecb05b7..2a1d74f4c 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("CLLW")); + assert!(out.contains("Name: \"TextOrdOre\"") && out.contains("ordering terms wait")); // 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 976df60c6..d9e32fd1d 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -29,7 +29,7 @@ use quote::{format_ident, quote}; use eql_domains::{Domain, DomainFamily, Shape, Term}; -use crate::bindings::{encryption_gap, format_rs, stored_payload_domains}; +use crate::bindings::{encryption_gap, format_rs, plaintext_of, stored_payload_domains}; use crate::render::EqlVersion; /// The version every target is produced into. @@ -54,47 +54,18 @@ pub fn index_key(term: Term) -> &'static str { /// payload keys. const JSON_INDEX_KEY: &str = "json"; -/// The plaintext a family's domains are produced from, as a vitaminc -/// `ValueKind` name (what a plan field's `"type"` key spells) and the Rust -/// type the generated dispatch names. `None` while the family's plaintext -/// encoding for the stack-encrypt producer profile is unspecified, which is -/// every family but text today. +/// The plaintext a family's domains are produced from: see +/// [`bindings::plaintext_of`](crate::bindings::plaintext_of). pub fn plaintext(family: &DomainFamily) -> Option<(&'static str, &'static str)> { plaintext_of(family.name) } -/// [`plaintext`], by family name: what a rendered row carries. -fn plaintext_of(family: &str) -> Option<(&'static str, &'static str)> { - (family == "text").then_some(("string", "String")) -} - /// Why the engine cannot produce a stored domain's EQL type today, or -/// `None` when it can. `None` exactly when the domain carries the -/// `stack-encrypt` derives ([`encryption_gap`]), because the dispatch runs -/// the derived plan; the reasons are the plan's, keyed on catalog facts so -/// a domain added tomorrow gets an answer. +/// `None` when it can: exactly when the v4 domain carries the +/// `stack-encrypt` derives, because the dispatch runs the derived plan, so +/// the one source of reasons is [`encryption_gap`]. pub fn target_gap(family: &DomainFamily, domain: &Domain) -> Option<&'static str> { - encryption_gap(TARGET_VERSION, family, domain)?; - Some(if matches!(domain.shape, Shape::SteVec) { - "the JSON index is a new operation in the engine" - } else if family.name != "text" { - "how this family encodes a plaintext for the stack-encrypt producer profile is not \ - specified; only the text family's encoding is" - } else if domain.terms.contains(&Term::Ore) { - "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are \ - different algorithms" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Bloom | Term::Ope)) - { - "the engine derives match and OPE terms, but no EQL type is built from them yet" - } else if domain.terms.is_empty() { - "the storage-only text domain follows once TextEq is proven end to end in PostgreSQL" - } else { - "an equality-only text domain derives the same way TextEq does; it is not listed in \ - ENCRYPTION_DOMAINS yet" - }) + encryption_gap(TARGET_VERSION, family, domain) } /// One row of the generated table, computed from the catalog: what both @@ -245,7 +216,8 @@ fn render_targets_from(rows: &[Row]) -> String { let ty = format_ident!("{strukt}"); let (_, rust) = plaintext_of(r.family).expect("a producible family has a specified plaintext"); - let source = format_ident!("{rust}"); + // Named by its last segment: the file imports every plaintext path. + let source = format_ident!("{}", rust.rsplit("::").next().unwrap_or(rust)); (name.clone(), quote!(super::#module::#ty), quote!(#source)) }; let encrypt_arms: TokenStream = producible @@ -272,6 +244,22 @@ fn render_targets_from(rows: &[Row]) -> String { Some(quote! { #name => run_target::<#ty, #source, K>(#name, keyset, column, plaintext), }) }) .collect(); + // One `use` per plaintext a producible type names by path (`String` is + // in the prelude), so the arms stay short enough to read. + let mut imports: Vec<&str> = producible + .iter() + .filter_map(|r| plaintext_of(r.family).map(|(_, rust)| rust)) + .filter(|rust| rust.contains("::")) + .collect(); + imports.sort_unstable(); + imports.dedup(); + let imports: TokenStream = imports + .iter() + .map(|rust| { + let path: syn::Path = syn::parse_str(rust).expect("plaintext is a type path"); + quote!(use #path;) + }) + .collect(); let helpers = if producible.is_empty() { quote!() } else { @@ -291,6 +279,7 @@ fn render_targets_from(rows: &[Row]) -> String { use vitaminc_aead_value::FfiValue; + #imports use crate::encryption::targets::{#helpers refuse, refuse_query, Opener, Target, TargetError}; use crate::Identifier; use stack_encrypt::{KeysetCipher, NonEmpty, Pending}; @@ -384,63 +373,57 @@ mod tests { ); } - /// Every reason is the plan's reason for that domain, selected by the - /// catalog fact that applies: a wrong branch order would hand the ORE - /// reason to `text_search` or the encoding reason to a text domain. + /// A target's reason is its v4 domain's derive reason: one source, so + /// the table and the derives cannot disagree about why. #[test] - fn every_unproducible_target_has_the_plan_reason_for_its_domain() { + fn every_unproducible_target_has_its_derive_reason() { for (family, domain) in stored_payload_domains() { - let full = domain.full_name(family.name); - let Some(reason) = target_gap(family, domain) else { - continue; - }; - let expected = if matches!(domain.shape, Shape::SteVec) { - "JSON index" - } else if family.name != "text" { - "encodes a plaintext" - } else if domain.terms.contains(&Term::Ore) { - "block ORE" - } else if domain - .terms - .iter() - .any(|t| matches!(t, Term::Bloom | Term::Ope)) - { - "match and OPE terms" - } else { - assert!( - domain.terms.is_empty(), - "{full}: an hm-only text domain other than eq is not in the catalog" - ); - "storage-only text domain" - }; - assert!(reason.contains(expected), "{full}: {reason}"); + assert_eq!( + target_gap(family, domain), + encryption_gap(TARGET_VERSION, family, domain), + "{}", + domain.full_name(family.name) + ); } - // Spelled out for the four plan rows, so the table reads without - // the derivation above. + // Spelled out, so the table reads without the derivation. + let gap = |family: &eql_domains::DomainFamily, name: &str| { + target_gap(family, family.domain_by_name(name).expect("domain")) + }; let text = eql_domains::TEXT; - let by_name = |name: &str| text.domain_by_name(name).expect("text domain"); - assert!(target_gap(&text, by_name("ord_ore")) - .unwrap() - .contains("CLLW")); - assert!(target_gap(&text, by_name("search_ore")) - .unwrap() - .contains("CLLW")); - assert!(target_gap(&text, by_name("match")) - .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("ord")) - .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("ord_ope")) + for producible in ["", "eq"] { + assert!(gap(&text, producible).is_none(), "text {producible:?}"); + } + for ordered in ["ord", "ord_ore", "ord_ope", "search", "search_ore"] { + assert!( + gap(&text, ordered).unwrap().contains("ordering terms"), + "{ordered}" + ); + } + assert!(gap(&text, "match").unwrap().contains("match terms")); + let family = |name: &str| { + eql_domains::CATALOG + .iter() + .find(|f| f.name == name) + .expect("family") + }; + for family_name in ["integer", "bigint"] { + assert!( + gap(family(family_name), "eq") + .unwrap() + .contains("integer-orderable/v1"), + "{family_name}" + ); + assert!(gap(family(family_name), "").is_none(), "{family_name}"); + } + assert!(gap(family("integer"), "ord_ore") .unwrap() - .contains("no EQL type")); - assert!(target_gap(&text, by_name("search")) + .contains("ordering terms")); + assert!(gap(family("real"), "eq").unwrap().contains("float")); + assert!(gap(family("date"), "").unwrap().contains("has no kind")); + assert!(gap(family("smallint"), "eq") .unwrap() - .contains("no EQL type")); - assert!( - target_gap(&text, by_name("eq")).is_none(), - "TextEq is producible" - ); + .contains("has no kind")); + assert!(gap(family("json"), "").unwrap().contains("tree of leaves")); } #[test] @@ -500,47 +483,64 @@ mod tests { }) { let name = domain.rust_struct_name(family.name); let module = family.name; + let (_, plaintext) = plaintext_of(family.name).expect("a derived family's plaintext"); + let plaintext = plaintext.rsplit("::").next().unwrap(); let arm = |helper: &str, ty: &str| { format!( - "\"{name}\" => {{ {helper}::(\"{name}\", " + "\"{name}\" => {{ {helper}::(\"{name}\", " ) }; let flat_arm = |helper: &str, ty: &str| { - format!("\"{name}\" => {helper}::(\"{name}\", ") + format!( + "\"{name}\" => {helper}::(\"{name}\", " + ) }; + // rustfmt breaks long arms at different points, so compare with + // all whitespace gone. + let tight: String = out.split_whitespace().collect(); let count = |helper: &str, ty: &str| { - out.matches(&arm(helper, ty)).count() + out.matches(&flat_arm(helper, ty)).count() + let strip = |p: String| p.split_whitespace().collect::(); + tight.matches(&strip(arm(helper, ty))).count() + + tight.matches(&strip(flat_arm(helper, ty))).count() }; assert_eq!(count("run_target", &name), 1, "{name}: one encrypt arm"); assert_eq!(count("open_target", &name), 1, "{name}: one decrypt arm"); assert_eq!( count("run_target", &format!("{name}Query")), - 1, - "{name}: one query arm" + usize::from(!domain.terms.is_empty()), + "{name}: a query arm exactly when it has a query twin" ); } // No unproducible type has an arm: the only `=>` arms are the // producible ones and the three fall-throughs. + let with_twin = ENCRYPTION_DOMAINS + .iter() + .filter(|(f, d)| { + let family = eql_domains::CATALOG.iter().find(|x| x.name == *f).unwrap(); + !family.domain_by_name(d).unwrap().terms.is_empty() + }) + .count(); let arms = out.matches("\" => ").count(); - assert_eq!(arms, ENCRYPTION_DOMAINS.len() * 3, "arms: {out}"); + assert_eq!( + arms, + ENCRYPTION_DOMAINS.len() * 2 + with_twin, + "arms: {out}" + ); assert_eq!(out.matches("_ => Err(refuse(name))").count(), 2); assert_eq!(out.matches("_ => Err(refuse_query(name))").count(), 1); } - /// A producible type with no query twin — a storage-only domain, once - /// its derive lands — renders encrypt and decrypt arms and no query arm, - /// rather than aborting the generator: the query dispatch's fall-through - /// refuses it as answering no query. Not in the catalog today, so the - /// row is flipped by hand. + /// A producible type with no query twin — the storage-only `Text` — + /// renders encrypt and decrypt arms and no query arm: the query + /// dispatch's fall-through refuses it as answering no query. #[test] fn a_producible_type_without_a_query_twin_gets_no_query_arm() { - let mut rows = rows(); + let rows = rows(); let text = rows - .iter_mut() + .iter() .find(|r| r.name == "Text") .expect("the storage-only text domain"); - assert!(text.query.is_none() && text.reason.is_some()); - text.reason = None; + assert!(text.query.is_none() && text.reason.is_none()); let out: String = render_targets_from(&rows) .split_whitespace() .collect::>() @@ -562,10 +562,5 @@ mod tests { !out.contains("TextQuery"), "no query arm for a type with no twin: {out}" ); - assert_eq!( - out.matches("\" => ").count(), - (ENCRYPTION_DOMAINS.len() + 1) * 3 - 1, - "every producible type has three arms but the twinless one, which has two" - ); } } diff --git a/packages/eql/mise.toml b/packages/eql/mise.toml index 233078020..5fe00f278 100644 --- a/packages/eql/mise.toml +++ b/packages/eql/mise.toml @@ -330,9 +330,11 @@ psql "$EQL_TEST_DATABASE_URL" -v ON_ERROR_STOP=1 -f release/cipherstash-encrypt- # `--ignored` selects by attribute, and cargo exits 0 on an empty selection, so # a test that loses its `#[ignore]` or is renamed would leave this task passing # on nothing. List what the filter selects before running it. -listing=$(cargo test --locked -p eql-encryption-tests --test text_eq -- --ignored --list | sed -n 's/: test$//p') -[ -n "$listing" ] || { echo "error: '--ignored' selects no test in text_eq; the PostgreSQL test has gone missing." >&2; exit 1; } -cargo test --locked -p eql-encryption-tests --test text_eq -- --ignored +for suite in text_eq v4_types; do + listing=$(cargo test --locked -p eql-encryption-tests --test "$suite" -- --ignored --list | sed -n 's/: test$//p') + [ -n "$listing" ] || { echo "error: '--ignored' selects no test in $suite; the PostgreSQL test has gone missing." >&2; exit 1; } + cargo test --locked -p eql-encryption-tests --test "$suite" -- --ignored +done """ [tasks."check:encryption:wasi"] diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index 39c06e5a1..97c7b1189 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: EQL stores a block ORE term and the engine \ - derives a CLLW ORE term; the two are different algorithms" + "the engine cannot produce TextOrdOre yet: ordering terms wait for vitaminc-ore, the \ + order encodings in vitaminc 0.6 (ADR-0002)" ); // Refused by name before the label or the value is looked at. let error = refused(targets::encrypt( @@ -472,14 +472,24 @@ mod the_table { }) .collect(); assert_eq!(row.suffix, suffix, "{name}: suffix"); + let kind = match family.name { + "text" => Some(ValueKind::String), + "integer" => Some(ValueKind::Int32), + "bigint" => Some(ValueKind::Int64), + "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. + _ => None, + }; assert_eq!( row.plaintext, - (family.name == "text").then_some("string"), - "{name}: only text's plaintext encoding is specified" + kind.map(ValueKind::name), + "{name}: the family's plaintext kind" ); assert_eq!( row.plaintext_kind(), - (family.name == "text").then_some(ValueKind::String), + kind, "{name}: the plaintext name is a vitaminc kind" ); if matches!(domain.shape, Shape::SteVec) { @@ -543,8 +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. #[test] - fn text_eq_is_the_one_producible_type_today() { + fn the_producible_types_are_the_unordered_ones_vitaminc_0_5_encodes() { let producible: Vec<&str> = targets::targets() .iter() .filter(|t| t.producible) @@ -552,18 +565,19 @@ mod the_table { .collect(); assert_eq!( producible, - ["TextEq"], - "the plan's list: the engine produces TextEq only" + ["Integer", "Bigint", "Text", "TextEq", "Boolean", "Real", "Double"], ); for row in targets::targets().iter().filter(|t| !t.producible) { let reason = row.reason.expect("an unproducible type has a reason"); let expected = match (row.family, row.suffix) { ("json", "Search") => "JSON index", - (family, _) if family != "text" => "encodes a plaintext", - (_, "OrdOre" | "SearchOre") => "CLLW", - (_, "Match" | "Ord" | "OrdOpe" | "Search") => "no EQL type is built", - (_, "") => "storage-only", - (_, suffix) => panic!("{}: unexpected text suffix {suffix}", row.name), + ("json", "") => "tree of leaves", + ("smallint" | "date" | "timestamp" | "numeric", _) => "has no kind", + (_, "Ord" | "OrdOre" | "OrdOpe" | "Search" | "SearchOre") => "ordering terms", + ("text", "Match") => "match terms", + ("real" | "double", "Eq") => "float", + ("integer" | "bigint", "Eq") => "integer-orderable/v1", + (family, suffix) => panic!("{}: unexpected {family} {suffix}", row.name), }; assert!(reason.contains(expected), "{}: {reason}", row.name); } @@ -589,7 +603,7 @@ mod the_table { "reason": null }) ); - let row = targets::target("Integer").unwrap(); + let row = targets::target("Date").unwrap(); let json = serde_json::to_value(row).unwrap(); assert_eq!( json["plaintext"], diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs new file mode 100644 index 000000000..126df4ac5 --- /dev/null +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -0,0 +1,560 @@ +//! The EQL v4 types Stack Encrypt produces besides `TextEq`: the +//! storage-only integer, bigint, boolean, float and text types. `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. +//! +//! 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 +//! leaf vitaminc's value enum seals for the same value (what the Go SDK writes +//! for a plain field of that kind) and open the leaf the value enum sealed, +//! and refuse a value of another kind. + +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; +use eql_bindings::v4::{bigint, boolean, double, integer, real, text}; +use eql_bindings::Identifier; +use serde::{de::DeserializeOwned, Serialize}; +use stack_encrypt::target::transcode::{Reader, Visitor}; +use stack_encrypt::{CipherText, Label, NonEmpty}; +use vitaminc_aead_value::{FfiValue, ValueKind}; + +fn column() -> NonEmpty { + Identifier::for_column("users", "age").unwrap() +} + +fn label() -> Label { + Label::new(["users", "age"]).unwrap() +} + +/// The refusal a dispatch returned; `Pending` has no `Debug`. +fn refused(result: Result) -> TargetError { + match result { + Err(error) => error, + Ok(_) => panic!("the dispatch accepted what it should have refused"), + } +} + +/// The JSON a stored v4 value serializes to: version 4, the column, and the +/// ciphertext with the producer marker. +fn assert_v4_envelope(stored: &T, keys: &[&str]) { + let json = serde_json::to_value(stored).unwrap(); + assert_eq!(json["v"], 4, "Stack Encrypt writes EQL v4"); + assert_eq!(json["i"], serde_json::json!({"t": "users", "c": "age"})); + assert!(json["c"].as_str().unwrap().starts_with("stack-encrypt:1:")); + let mut found: Vec<&str> = json + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + found.sort_unstable(); + let mut expected = keys.to_vec(); + expected.sort_unstable(); + assert_eq!(found, expected); +} + +/// What the stored ciphertext opens to when read as vitaminc's value enum: +/// the leaf the Go SDK would have sealed for the same value. +async fn as_value_enum( + cipher: &stack_encrypt::StackCipher, + c: Ciphertext, +) -> FfiValue { + let native = c.read(NativeLeaf).unwrap(); + cipher.decrypt(native, column()).await.unwrap() +} + +/// The other direction: the `c` a plain field of `value`'s kind gets when the +/// value enum seals it, as a Go SDK field without an EQL type does. +async fn sealed_by_value_enum( + keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, + value: FfiValue, +) -> Ciphertext { + match keyset.encrypt(value, column()).await.unwrap() { + CipherText::Single(leaf) => CiphertextVisitor.sealed(leaf).unwrap(), + _ => panic!("a scalar seals as one leaf"), + } +} + +/// A storage-only v4 value of type `T` around `c`, for this test's column. +fn stored_as(c: Ciphertext) -> T { + serde_json::from_value(serde_json::json!({ + "v": 4, + "i": {"t": "users", "c": "age"}, + "c": c, + })) + .unwrap() +} + +/// Run one named target over `value` and read the stored JSON back as `T`. +async fn by_name( + keyset: &stack_encrypt::KeysetCipher<'_, common::Observed>, + name: &str, + value: FfiValue, +) -> T { + let bytes = targets::encrypt(name, keyset, &label(), value) + .unwrap() + .await + .unwrap(); + serde_json::from_slice(&bytes).unwrap() +} + +/// Whether two scalar values are the same value, bit for bit for floats. +fn same(a: &FfiValue, b: &FfiValue) -> bool { + match (a, b) { + (FfiValue::Int32(a), FfiValue::Int32(b)) => a == b, + (FfiValue::Int64(a), FfiValue::Int64(b)) => a == b, + (FfiValue::Bool(a), FfiValue::Bool(b)) => a == b, + (FfiValue::Float32(a), FfiValue::Float32(b)) => a.to_bits() == b.to_bits(), + (FfiValue::Float64(a), FfiValue::Float64(b)) => a.to_bits() == b.to_bits(), + (FfiValue::String(a), FfiValue::String(b)) => a.risky_ref() == b.risky_ref(), + _ => false, + } +} + +/// A sample plaintext of each kind a producible type takes. +fn sample(kind: ValueKind) -> FfiValue { + match kind { + ValueKind::Int32 => FfiValue::Int32(-42), + ValueKind::Int64 => FfiValue::Int64(i64::MIN + 1), + ValueKind::Bool => FfiValue::Bool(true), + ValueKind::Float32 => FfiValue::Float32(-0.0), + ValueKind::Float64 => FfiValue::Float64(f64::NAN), + ValueKind::String => FfiValue::String("a private note".into()), + other => panic!("no sample for {other:?}: add one with the type that takes it"), + } +} + +#[tokio::test] +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(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + + let opened: Int32 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c.clone()).await, + FfiValue::Int32(v) if v == value + )); + + // The by-name dispatch runs the same plan. + let named: integer::Integer = by_name(&keyset, "Integer", FfiValue::Int32(value)).await; + let opened = targets::decrypt( + "Integer", + &cipher, + &label(), + &serde_json::to_vec(&named).unwrap(), + ) + .unwrap() + .await + .unwrap(); + assert!(matches!(opened, FfiValue::Int32(v) if v == value)); + } + + for value in [0i64, -1, i64::MIN, i64::MAX] { + let stored: bigint::Bigint = keyset.encrypt_as(&Int64(value), column()).await.unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: Int64 = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c.clone()).await, + FfiValue::Int64(v) if v == value + )); + } +} + +/// Withdrawn until vitaminc 0.6: a later re-enable is a deliberate change to +/// this test, not a side effect of a regenerated table. +#[tokio::test] +async fn integer_equality_types_are_held_back_with_their_reason() { + 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-orderable/v1")), + "{name}: {error}" + ); + } +} + +#[tokio::test] +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(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: Bool = cipher + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, value); + assert!(matches!( + as_value_enum(&cipher, stored.c).await, + FfiValue::Bool(v) if v == value + )); + } + // 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 + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.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 + .decrypt_as(stored.clone(), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), value.to_bits()); + assert!(matches!( + as_value_enum(&cipher, stored.c).await, + FfiValue::Float64(v) if v.to_bits() == value.to_bits() + )); + } +} + +/// The direction `LeafVisitor` and `BoolVisitor` implement: a leaf the value +/// enum sealed, as a plain Go field writes it, opens as the typed plaintext. +#[tokio::test] +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 + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, -7); + + let c = sealed_by_value_enum(&keyset, FfiValue::Int64(i64::MAX)).await; + let opened: Int64 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0, i64::MAX); + + let c = sealed_by_value_enum(&keyset, FfiValue::Bool(true)).await; + let opened: Bool = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert!(opened.0); + + let c = sealed_by_value_enum(&keyset, FfiValue::Float32(-0.0)).await; + let opened: Float32 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), (-0.0f32).to_bits()); + + let c = sealed_by_value_enum(&keyset, FfiValue::Float64(f64::NAN)).await; + let opened: Float64 = cipher + .decrypt_as(stored_as::(c), column().into()) + .await + .unwrap(); + assert_eq!(opened.0.to_bits(), f64::NAN.to_bits()); +} + +/// Every producible row of the generated table, through all three by-name +/// entry points: a swapped type or plaintext in the generator fails here. +#[tokio::test] +async fn every_producible_type_dispatches_by_name() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let producible: Vec<_> = targets::targets().iter().filter(|t| t.producible).collect(); + assert!(!producible.is_empty()); + for target in producible { + let name = target.name; + let kind = target + .plaintext_kind() + .expect("a producible type names its kind"); + let stored = targets::encrypt(name, &keyset, &label(), sample(kind)) + .unwrap() + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&stored).unwrap(); + assert_eq!(json["v"], 4, "{name}"); + let opened = targets::decrypt(name, &cipher, &label(), &stored) + .unwrap() + .await + .unwrap(); + assert!(same(&opened, &sample(kind)), "{name}: opened another value"); + + match target.query { + None => { + let error = refused(targets::query(name, &keyset, &label(), sample(kind))); + assert!( + matches!(error, TargetError::NoQuery { .. }), + "{name}: {error}" + ); + } + Some(_) => { + let query = targets::query(name, &keyset, &label(), sample(kind)) + .unwrap() + .await + .unwrap(); + let query: serde_json::Value = serde_json::from_slice(&query).unwrap(); + assert!( + query.get("c").is_none(), + "{name}: a query carries no ciphertext" + ); + if target.indexes.contains(&"eq") { + assert_eq!( + query["hm"], json["hm"], + "{name}: the query probes the stored term" + ); + } + } + } + } +} + +#[tokio::test] +async fn storage_only_text_round_trips_through_both_entry_points() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + let stored: text::Text = keyset + .encrypt_as(&"a private note".to_owned(), column()) + .await + .unwrap(); + assert_v4_envelope(&stored, &["v", "i", "c"]); + let opened: String = cipher.decrypt_as(stored, column().into()).await.unwrap(); + assert_eq!(opened, "a private note"); + + let named: text::Text = by_name(&keyset, "Text", FfiValue::String("note".into())).await; + let opened: String = cipher.decrypt_as(named, column().into()).await.unwrap(); + assert_eq!(opened, "note"); + // A storage-only type answers no query. + let error = refused(targets::query( + "Text", + &keyset, + &label(), + FfiValue::String("note".into()), + )); + assert!( + matches!(error, TargetError::NoQuery { name: "Text" }), + "{error}" + ); +} + +#[tokio::test] +async fn a_value_of_another_kind_is_refused_not_converted() { + let (cipher, _) = common::cipher().await; + let keyset = cipher.default_keyset(); + for (name, value, expected) in [ + ("Integer", FfiValue::Int64(1), ValueKind::Int32), + ("Bigint", FfiValue::Int32(1), ValueKind::Int64), + ("Boolean", FfiValue::Int32(1), ValueKind::Bool), + ("Real", FfiValue::Float64(1.0), ValueKind::Float32), + ("Text", FfiValue::Int32(1), ValueKind::String), + ] { + let error = refused(targets::encrypt(name, &keyset, &label(), value)); + assert!( + matches!(error, TargetError::Plaintext { target, expected: e, .. } if target == name && e == expected), + "{name}: {error}" + ); + } + + // 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(); + let moved = bigint::Bigint { + v: narrow.v, + i: narrow.i, + c: narrow.c, + }; + let opened: Result = cipher.decrypt_as(moved, column().into()).await; + assert!(opened.is_err()); +} + +/// Needs an EQL v4 installation; uses fresh real ciphertext with a fake key +/// source. +#[test] +#[ignore = "requires EQL_TEST_DATABASE_URL pointing to an EQL v4 database"] +fn postgres_stores_and_queries_every_producible_v4_type() { + 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_types ( + id int PRIMARY KEY, + age public.eql_v4_integer, + balance public.eql_v4_bigint, + active public.eql_v4_boolean, + ratio public.eql_v4_real, + score public.eql_v4_double, + note public.eql_v4_text + );", + ) + .unwrap(); + + let json = |value: &dyn erased::Json| value.to_json(); + for (id, age, balance) in [(1, 42, 10i64), (2, 7, 20), (3, 42, 30)] { + let row = runtime.block_on(async { + ( + keyset + .encrypt_as::<_, integer::Integer>(&Int32(age), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, bigint::Bigint>(&Int64(balance), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, boolean::Boolean>(&Bool(id % 2 == 0), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, real::Real>(&Float32(0.5), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, double::Double>(&Float64(0.25), column()) + .await + .unwrap(), + keyset + .encrypt_as::<_, text::Text>(&"note".to_owned(), column()) + .await + .unwrap(), + ) + }); + db.execute( + "INSERT INTO v4_types VALUES ($1, + $2::text::jsonb::public.eql_v4_integer, + $3::text::jsonb::public.eql_v4_bigint, + $4::text::jsonb::public.eql_v4_boolean, + $5::text::jsonb::public.eql_v4_real, + $6::text::jsonb::public.eql_v4_double, + $7::text::jsonb::public.eql_v4_text)", + &[ + &id, + &json(&row.0), + &json(&row.1), + &json(&row.2), + &json(&row.3), + &json(&row.4), + &json(&row.5), + ], + ) + .unwrap(); + } + + // Every stored value reads back from its domain and opens. + let row = db + .query_one( + "SELECT age::text, balance::text, active::text, ratio::text, score::text, note::text + FROM v4_types WHERE id = 2", + &[], + ) + .unwrap(); + let open = |i: usize| -> String { row.get(i) }; + runtime.block_on(async { + let opened: Int32 = cipher + .decrypt_as( + serde_json::from_str::(&open(0)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 7); + let opened: Int64 = cipher + .decrypt_as( + serde_json::from_str::(&open(1)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 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 + .decrypt_as( + serde_json::from_str::(&open(3)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 0.5); + let opened: Float64 = cipher + .decrypt_as( + serde_json::from_str::(&open(4)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened.0, 0.25); + let opened: String = cipher + .decrypt_as( + serde_json::from_str::(&open(5)).unwrap(), + column().into(), + ) + .await + .unwrap(); + assert_eq!(opened, "note"); + }); + + // A storage-only column has no equality operator: it is blocked, not + // silently compared as jsonb. + let err = db + .query("SELECT id FROM v4_types WHERE age = age", &[]) + .unwrap_err(); + let message = err.as_db_error().map(|e| e.message()).unwrap_or_default(); + assert!( + message.contains("is not supported for public.eql_v4_integer"), + "{message}" + ); +} + +/// A stored value's JSON text, for binding as `$n::text::jsonb`. +mod erased { + pub trait Json { + fn to_json(&self) -> String; + } + impl Json for T { + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } + } +}