From 3d3f4a7adff3e50468430ad1cdb9d37809e872ea Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 21 May 2026 18:02:05 +1000 Subject: [PATCH 01/12] feat: eql-types canonical types crate (prototype) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prototype of a single-source-of-truth crate for EQL payload types: one Rust definition per shape, generating TypeScript (ts-rs) and JSON Schema (schemars). Two tiers — frozen eql_v2_encrypted v2.3 contract, and the capability-encoded eql_v2_int4 variant family. Draft for review; not wired into the build or CI. --- crates/eql-types/.gitignore | 2 + crates/eql-types/Cargo.toml | 11 ++ crates/eql-types/README.md | 48 +++++ crates/eql-types/bindings/EncryptedPayload.ts | 36 ++++ crates/eql-types/bindings/EqlEncrypted.ts | 8 + crates/eql-types/bindings/Identifier.ts | 16 ++ crates/eql-types/bindings/Int4.ts | 19 ++ crates/eql-types/bindings/Int4Eq.ts | 27 +++ crates/eql-types/bindings/Int4Ord.ts | 28 +++ crates/eql-types/bindings/Int4Tagged.ts | 10 + crates/eql-types/bindings/SteVecElement.ts | 18 ++ crates/eql-types/bindings/SteVecPayload.ts | 20 ++ crates/eql-types/bindings/SteVecTerm.ts | 9 + crates/eql-types/schema/EqlEncrypted.json | 182 ++++++++++++++++++ crates/eql-types/schema/Int4Eq.json | 56 ++++++ crates/eql-types/schema/Int4Tagged.json | 125 ++++++++++++ crates/eql-types/src/int4.rs | 125 ++++++++++++ crates/eql-types/src/lib.rs | 47 +++++ crates/eql-types/src/v2_3.rs | 102 ++++++++++ crates/eql-types/tests/conformance.rs | 96 +++++++++ 20 files changed, 985 insertions(+) create mode 100644 crates/eql-types/.gitignore create mode 100644 crates/eql-types/Cargo.toml create mode 100644 crates/eql-types/README.md create mode 100644 crates/eql-types/bindings/EncryptedPayload.ts create mode 100644 crates/eql-types/bindings/EqlEncrypted.ts create mode 100644 crates/eql-types/bindings/Identifier.ts create mode 100644 crates/eql-types/bindings/Int4.ts create mode 100644 crates/eql-types/bindings/Int4Eq.ts create mode 100644 crates/eql-types/bindings/Int4Ord.ts create mode 100644 crates/eql-types/bindings/Int4Tagged.ts create mode 100644 crates/eql-types/bindings/SteVecElement.ts create mode 100644 crates/eql-types/bindings/SteVecPayload.ts create mode 100644 crates/eql-types/bindings/SteVecTerm.ts create mode 100644 crates/eql-types/schema/EqlEncrypted.json create mode 100644 crates/eql-types/schema/Int4Eq.json create mode 100644 crates/eql-types/schema/Int4Tagged.json create mode 100644 crates/eql-types/src/int4.rs create mode 100644 crates/eql-types/src/lib.rs create mode 100644 crates/eql-types/src/v2_3.rs create mode 100644 crates/eql-types/tests/conformance.rs diff --git a/crates/eql-types/.gitignore b/crates/eql-types/.gitignore new file mode 100644 index 00000000..4fffb2f8 --- /dev/null +++ b/crates/eql-types/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/crates/eql-types/Cargo.toml b/crates/eql-types/Cargo.toml new file mode 100644 index 00000000..deb841da --- /dev/null +++ b/crates/eql-types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "eql-types" +version = "0.1.0" +edition = "2021" +description = "Canonical wire types for EQL payloads — single source of truth for Rust, TypeScript (ts-rs), and JSON Schema (schemars)." + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +ts-rs = { version = "10", features = ["serde-json-impl"] } +schemars = "0.8" diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md new file mode 100644 index 00000000..1c18cefe --- /dev/null +++ b/crates/eql-types/README.md @@ -0,0 +1,48 @@ +# eql-types (prototype) + +Canonical wire types for EQL payloads — **one Rust definition per payload +shape**, intended as the single source of truth for: + +- **Rust** — consumed directly by `cipherstash-client` / `protect-ffi` +- **TypeScript** — generated via [`ts-rs`] into [`bindings/`](bindings/) +- **JSON Schema** — generated via [`schemars`] into [`schema/`](schema/) + +> **Status: prototype / draft for discussion.** Not wired into the EQL build +> or CI. See the pull request description for full context. + +## Why + +Type information is lost at every hop of `EQL → cipherstash-client → +protect-ffi → stack`. protect-ffi hand-writes its TypeScript types; they drift +from the Rust they describe; stack widens them further. The result is bugs +like the `protect-dynamodb` search-term check that validates a payload shape +EQL v2.3 never actually defined. A generated, single-source crate removes the +hand-copying. + +## Two tiers + +| Module | Tier | Rule | +|--------|------|------| +| [`src/v2_3.rs`](src/v2_3.rs) | `eql_v2_encrypted` v2.3 wire contract | **FROZEN** — in production; mirrors `eql-payload-v2.3.schema.json`; must not change | +| [`src/int4.rs`](src/int4.rs) | `eql_v2_int4` variant family (#225) | **Design freedom** — capability-encoded types | + +## Capability-encoded types + +`eql_v2_encrypted` is one type with every index term optional, so consumers +must guess at runtime which terms are present. The `int4` family instead has +one type per capability — `Int4` / `Int4Eq` / `Int4Ord` — each carrying its +index terms as **required** fields. The capability is the type identity; +`Option` never appears. + +## Develop + +```sh +cargo test +``` + +Runs the conformance round-trip tests and regenerates `bindings/` (TypeScript) +and `schema/` (JSON Schema). Both directories are checked in so reviewers can +see the codegen output without running anything. + +[`ts-rs`]: https://github.com/Aleph-Alpha/ts-rs +[`schemars`]: https://graham.cool/schemars/ diff --git a/crates/eql-types/bindings/EncryptedPayload.ts b/crates/eql-types/bindings/EncryptedPayload.ts new file mode 100644 index 00000000..3c037f71 --- /dev/null +++ b/crates/eql-types/bindings/EncryptedPayload.ts @@ -0,0 +1,36 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; + +/** + * Scalar storage payload (`k = "ct"`). + * + * FROZEN imperfection: `hm`/`bf`/`ob` are independently optional. A consumer + * cannot tell from the type which terms are present — it must inspect at + * runtime. This is precisely the gap the `protect-dynamodb` bug fell into. + * The fix, for *new* types, is [`crate::int4`]. + */ +export type EncryptedPayload = { +/** + * Schema version — always [`crate::EQL_SCHEMA_VERSION`]. + */ +v: number, +/** + * Table/column identifier. + */ +i: Identifier, +/** + * mp_base85 ciphertext. Required. + */ +c: string, +/** + * HMAC-SHA256 equality term — present iff a `unique` index is configured. + */ +hm?: string, +/** + * Bloom filter term — present iff a `match` index is configured. + */ +bf?: Array, +/** + * Block ORE term — present iff an `ore` index is configured. + */ +ob?: Array, }; diff --git a/crates/eql-types/bindings/EqlEncrypted.ts b/crates/eql-types/bindings/EqlEncrypted.ts new file mode 100644 index 00000000..3e7bd8ed --- /dev/null +++ b/crates/eql-types/bindings/EqlEncrypted.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedPayload } from "./EncryptedPayload"; +import type { SteVecPayload } from "./SteVecPayload"; + +/** + * `eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`. + */ +export type EqlEncrypted = { "k": "ct" } & EncryptedPayload | { "k": "sv" } & SteVecPayload; diff --git a/crates/eql-types/bindings/Identifier.ts b/crates/eql-types/bindings/Identifier.ts new file mode 100644 index 00000000..5e976dbe --- /dev/null +++ b/crates/eql-types/bindings/Identifier.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Table + column identifier — wire shape `{"t": "...", "c": "..."}`. + * + * Shared by every payload in both tiers. + */ +export type Identifier = { +/** + * Table name. + */ +t: string, +/** + * Column name. + */ +c: string, }; diff --git a/crates/eql-types/bindings/Int4.ts b/crates/eql-types/bindings/Int4.ts new file mode 100644 index 00000000..ffddccf3 --- /dev/null +++ b/crates/eql-types/bindings/Int4.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; + +/** + * `eql_v2_int4` — storage only. Carries `c`; every operator is blocked. + */ +export type Int4 = { +/** + * Schema version. + */ +v: number, +/** + * Table/column identifier. + */ +i: Identifier, +/** + * mp_base85 ciphertext. Required by the domain's CHECK constraint. + */ +c: string, }; diff --git a/crates/eql-types/bindings/Int4Eq.ts b/crates/eql-types/bindings/Int4Eq.ts new file mode 100644 index 00000000..de28bece --- /dev/null +++ b/crates/eql-types/bindings/Int4Eq.ts @@ -0,0 +1,27 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; + +/** + * `eql_v2_int4_eq` — HMAC equality (`=`, `<>`). + * + * `hm` is a required field. There is no `Option`: the type *is* the + * equality capability. A payload without `hm` cannot be deserialized into + * this type — the Rust analogue of the SQL domain's CHECK constraint. + */ +export type Int4Eq = { +/** + * Schema version. + */ +v: number, +/** + * Table/column identifier. + */ +i: Identifier, +/** + * mp_base85 ciphertext. Required. + */ +c: string, +/** + * HMAC-SHA256 equality term. Required. + */ +hm: string, }; diff --git a/crates/eql-types/bindings/Int4Ord.ts b/crates/eql-types/bindings/Int4Ord.ts new file mode 100644 index 00000000..c0e15301 --- /dev/null +++ b/crates/eql-types/bindings/Int4Ord.ts @@ -0,0 +1,28 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; + +/** + * `eql_v2_int4_ord` — equality + ORE-block range (`=` `<>` `<` `<=` `>` `>=`). + * + * Deliberately carries no `hm`: ORE over a full-domain `int4` is lossless, so + * the order term `ob` doubles as an exact equality term. + * (`eql_v2_int4_ord_ore` in #225 is the same shape under a scheme-explicit + * name — structurally identical, so it is not a separate Rust type.) + */ +export type Int4Ord = { +/** + * Schema version. + */ +v: number, +/** + * Table/column identifier. + */ +i: Identifier, +/** + * mp_base85 ciphertext. Required. + */ +c: string, +/** + * Block ORE term. Required — serves both range and equality. + */ +ob: Array, }; diff --git a/crates/eql-types/bindings/Int4Tagged.ts b/crates/eql-types/bindings/Int4Tagged.ts new file mode 100644 index 00000000..09a90e43 --- /dev/null +++ b/crates/eql-types/bindings/Int4Tagged.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; + +/** + * **Proposed.** Self-describing int4 payload — `x` is the capability tag. + * + * Generates a clean TypeScript discriminated union (`switch (p.x)` with + * exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`. + */ +export type Int4Tagged = { "x": "int4", v: number, i: Identifier, c: string, } | { "x": "int4_eq", v: number, i: Identifier, c: string, hm: string, } | { "x": "int4_ord", v: number, i: Identifier, c: string, ob: Array, }; diff --git a/crates/eql-types/bindings/SteVecElement.ts b/crates/eql-types/bindings/SteVecElement.ts new file mode 100644 index 00000000..3446c7a7 --- /dev/null +++ b/crates/eql-types/bindings/SteVecElement.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * One STE-vector element. + */ +export type SteVecElement = { +/** + * Tokenized selector — deterministic per (path, key). + */ +s: string, +/** + * Per-entry mp_base85 ciphertext. Required. + */ +c: string, +/** + * Array marker — true when the selector points at a JSON array context. + */ +a?: boolean, } & ({ hm: string, } | { oc: string, }); diff --git a/crates/eql-types/bindings/SteVecPayload.ts b/crates/eql-types/bindings/SteVecPayload.ts new file mode 100644 index 00000000..eeae7992 --- /dev/null +++ b/crates/eql-types/bindings/SteVecPayload.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Identifier } from "./Identifier"; +import type { SteVecElement } from "./SteVecElement"; + +/** + * STE-vector storage payload (`k = "sv"`). + */ +export type SteVecPayload = { +/** + * Schema version. + */ +v: number, +/** + * Table/column identifier. + */ +i: Identifier, +/** + * Per-selector encrypted entries; root document ciphertext at `sv[0].c`. + */ +sv: Array, }; diff --git a/crates/eql-types/bindings/SteVecTerm.ts b/crates/eql-types/bindings/SteVecTerm.ts new file mode 100644 index 00000000..fe6778f4 --- /dev/null +++ b/crates/eql-types/bindings/SteVecTerm.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * SteVec element term. FROZEN as **untagged** — this is the v2.3 wire shape. + * + * A consumer must narrow with `'hm' in term`; there is no literal + * discriminant. A *new* type would tag this — see [`crate::int4`]. + */ +export type SteVecTerm = { hm: string, } | { oc: string, }; diff --git a/crates/eql-types/schema/EqlEncrypted.json b/crates/eql-types/schema/EqlEncrypted.json new file mode 100644 index 00000000..8df7b1ad --- /dev/null +++ b/crates/eql-types/schema/EqlEncrypted.json @@ -0,0 +1,182 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EqlEncrypted", + "description": "`eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`.", + "oneOf": [ + { + "description": "Scalar ciphertext payload.", + "type": "object", + "required": [ + "c", + "i", + "k", + "v" + ], + "properties": { + "bf": { + "description": "Bloom filter term — present iff a `match` index is configured.", + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "c": { + "description": "mp_base85 ciphertext. Required.", + "type": "string" + }, + "hm": { + "description": "HMAC-SHA256 equality term — present iff a `unique` index is configured.", + "type": [ + "string", + "null" + ] + }, + "i": { + "description": "Table/column identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "k": { + "type": "string", + "enum": [ + "ct" + ] + }, + "ob": { + "description": "Block ORE term — present iff an `ore` index is configured.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "v": { + "description": "Schema version — always [`crate::EQL_SCHEMA_VERSION`].", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + { + "description": "STE-vector payload (jsonb / structured values).", + "type": "object", + "required": [ + "i", + "k", + "sv", + "v" + ], + "properties": { + "i": { + "description": "Table/column identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "k": { + "type": "string", + "enum": [ + "sv" + ] + }, + "sv": { + "description": "Per-selector encrypted entries; root document ciphertext at `sv[0].c`.", + "type": "array", + "items": { + "$ref": "#/definitions/SteVecElement" + } + }, + "v": { + "description": "Schema version.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + } + ], + "definitions": { + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + } + }, + "SteVecElement": { + "description": "One STE-vector element.", + "type": "object", + "anyOf": [ + { + "description": "HMAC term — boolean leaves, and array / object root placeholders.", + "type": "object", + "required": [ + "hm" + ], + "properties": { + "hm": { + "type": "string" + } + } + }, + { + "description": "CLLW ORE term — string / number leaves.", + "type": "object", + "required": [ + "oc" + ], + "properties": { + "oc": { + "type": "string" + } + } + } + ], + "required": [ + "c", + "s" + ], + "properties": { + "a": { + "description": "Array marker — true when the selector points at a JSON array context.", + "type": [ + "boolean", + "null" + ] + }, + "c": { + "description": "Per-entry mp_base85 ciphertext. Required.", + "type": "string" + }, + "s": { + "description": "Tokenized selector — deterministic per (path, key).", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/crates/eql-types/schema/Int4Eq.json b/crates/eql-types/schema/Int4Eq.json new file mode 100644 index 00000000..9bde81c0 --- /dev/null +++ b/crates/eql-types/schema/Int4Eq.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Int4Eq", + "description": "`eql_v2_int4_eq` — HMAC equality (`=`, `<>`).\n\n`hm` is a required field. There is no `Option`: the type *is* the equality capability. A payload without `hm` cannot be deserialized into this type — the Rust analogue of the SQL domain's CHECK constraint.", + "type": "object", + "required": [ + "c", + "hm", + "i", + "v" + ], + "properties": { + "c": { + "description": "mp_base85 ciphertext. Required.", + "type": "string" + }, + "hm": { + "description": "HMAC-SHA256 equality term. Required.", + "type": "string" + }, + "i": { + "description": "Table/column identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "v": { + "description": "Schema version.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "definitions": { + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/crates/eql-types/schema/Int4Tagged.json b/crates/eql-types/schema/Int4Tagged.json new file mode 100644 index 00000000..86c54cd7 --- /dev/null +++ b/crates/eql-types/schema/Int4Tagged.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Int4Tagged", + "description": "**Proposed.** Self-describing int4 payload — `x` is the capability tag.\n\nGenerates a clean TypeScript discriminated union (`switch (p.x)` with exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`.", + "oneOf": [ + { + "description": "`x: \"int4\"` — storage only.", + "type": "object", + "required": [ + "c", + "i", + "v", + "x" + ], + "properties": { + "c": { + "type": "string" + }, + "i": { + "$ref": "#/definitions/Identifier" + }, + "v": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "x": { + "type": "string", + "enum": [ + "int4" + ] + } + } + }, + { + "description": "`x: \"int4_eq\"` — HMAC equality.", + "type": "object", + "required": [ + "c", + "hm", + "i", + "v", + "x" + ], + "properties": { + "c": { + "type": "string" + }, + "hm": { + "type": "string" + }, + "i": { + "$ref": "#/definitions/Identifier" + }, + "v": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "x": { + "type": "string", + "enum": [ + "int4_eq" + ] + } + } + }, + { + "description": "`x: \"int4_ord\"` — equality + ORE-block range.", + "type": "object", + "required": [ + "c", + "i", + "ob", + "v", + "x" + ], + "properties": { + "c": { + "type": "string" + }, + "i": { + "$ref": "#/definitions/Identifier" + }, + "ob": { + "type": "array", + "items": { + "type": "string" + } + }, + "v": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "x": { + "type": "string", + "enum": [ + "int4_ord" + ] + } + } + } + ], + "definitions": { + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/crates/eql-types/src/int4.rs b/crates/eql-types/src/int4.rs new file mode 100644 index 00000000..ab1f0fd5 --- /dev/null +++ b/crates/eql-types/src/int4.rs @@ -0,0 +1,125 @@ +//! # `eql_v2_int4` variant family — NEW (targets EQL 2.4) +//! +//! Where [`crate::v2_3`] is frozen, this module has design freedom. It mirrors +//! the SQL domain family from `encrypt-query-language#225`. +//! +//! ## The idea: capability-encoded types +//! +//! `eql_v2_encrypted` is one mega-type with every index term optional — so a +//! consumer must runtime-check "do I have an `hm`?". The int4 family instead +//! splits storage into one type per **capability**: +//! +//! | Rust type | SQL domain | Required keys | Operators | +//! |-------------|---------------------|---------------|----------------------------| +//! | [`Int4`] | `eql_v2_int4` | `c` | none (storage only) | +//! | [`Int4Eq`] | `eql_v2_int4_eq` | `c`, `hm` | `=` `<>` | +//! | [`Int4Ord`] | `eql_v2_int4_ord` | `c`, `ob` | `=` `<>` `<` `<=` `>` `>=` | +//! +//! The capability is the **type identity**. There are no optional index-term +//! fields: hold an [`Int4Eq`] and `hm` is present — guaranteed by the Rust +//! type, and (on the SQL side) by the domain's `CHECK` constraint. The runtime +//! guard the `protect-dynamodb` bug reached for becomes impossible to need. +//! +//! `Option` does not appear in this module. + +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +/// `eql_v2_int4` — storage only. Carries `c`; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct Int4 { + /// Schema version. + pub v: u16, + /// Table/column identifier. + pub i: Identifier, + /// mp_base85 ciphertext. Required by the domain's CHECK constraint. + pub c: String, +} + +/// `eql_v2_int4_eq` — HMAC equality (`=`, `<>`). +/// +/// `hm` is a required field. There is no `Option`: the type *is* the +/// equality capability. A payload without `hm` cannot be deserialized into +/// this type — the Rust analogue of the SQL domain's CHECK constraint. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct Int4Eq { + /// Schema version. + pub v: u16, + /// Table/column identifier. + pub i: Identifier, + /// mp_base85 ciphertext. Required. + pub c: String, + /// HMAC-SHA256 equality term. Required. + pub hm: String, +} + +/// `eql_v2_int4_ord` — equality + ORE-block range (`=` `<>` `<` `<=` `>` `>=`). +/// +/// Deliberately carries no `hm`: ORE over a full-domain `int4` is lossless, so +/// the order term `ob` doubles as an exact equality term. +/// (`eql_v2_int4_ord_ore` in #225 is the same shape under a scheme-explicit +/// name — structurally identical, so it is not a separate Rust type.) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct Int4Ord { + /// Schema version. + pub v: u16, + /// Table/column identifier. + pub i: Identifier, + /// mp_base85 ciphertext. Required. + pub c: String, + /// Block ORE term. Required — serves both range and equality. + pub ob: Vec, +} + +// =========================================================================== +// PROPOSAL (beyond #225) — a self-describing wire discriminator +// =========================================================================== +// +// On the wire, an int4 payload is discriminated only by *which key is present* +// (`hm` vs `ob`). The SQL domain name carries the rest — but once the JSON +// leaves SQL (into protect-ffi, into TypeScript, into a log line) that +// information is gone and a consumer is back to sniffing keys: the same +// untagged failure mode that produced the original protect-dynamodb bug. +// +// While the int4 family is still pre-release, a one-field capability tag `x` +// makes every payload self-describing and gives Rust / TS / SQL a single +// literal discriminant. This is the tagged-union lesson applied to a type we +// are still free to change. + +/// **Proposed.** Self-describing int4 payload — `x` is the capability tag. +/// +/// Generates a clean TypeScript discriminated union (`switch (p.x)` with +/// exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +#[serde(tag = "x")] +pub enum Int4Tagged { + /// `x: "int4"` — storage only. + #[serde(rename = "int4")] + Storage { + v: u16, + i: Identifier, + c: String, + }, + /// `x: "int4_eq"` — HMAC equality. + #[serde(rename = "int4_eq")] + Eq { + v: u16, + i: Identifier, + c: String, + hm: String, + }, + /// `x: "int4_ord"` — equality + ORE-block range. + #[serde(rename = "int4_ord")] + Ord { + v: u16, + i: Identifier, + c: String, + ob: Vec, + }, +} diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs new file mode 100644 index 00000000..5382ebfb --- /dev/null +++ b/crates/eql-types/src/lib.rs @@ -0,0 +1,47 @@ +//! # eql-types — canonical EQL payload types (prototype) +//! +//! One Rust definition per EQL payload shape — the single source of truth for: +//! +//! - **Rust** — consumed directly by `cipherstash-client` / `protect-ffi` +//! - **TypeScript** — generated via `ts-rs` (run `cargo test`, see `bindings/`) +//! - **JSON Schema** — generated via `schemars` (run `cargo test`, see `schema/`) +//! +//! ## Two tiers +//! +//! - [`v2_3`] — **FROZEN.** The `eql_v2_encrypted` wire contract, in production +//! use by customers. Mirrors `eql-payload-v2.3.schema.json`, imperfections +//! included. Nothing here may change. +//! - [`int4`] — **NEW** (targets EQL 2.4). Design freedom. Demonstrates +//! *capability-encoded types* — the pattern that removes the runtime +//! index-term guessing `eql_v2_encrypted` forces onto every consumer. +//! +//! ## Codegen rules (learned from the ts-rs spike) +//! +//! 1. **Field names ARE wire names** — no `#[serde(rename)]` on fields. ts-rs +//! silently drops a `rename` that is bundled into an attribute it can't +//! parse (`skip_serializing_if`); having no rename removes the footgun. +//! 2. Every `Option` field carries `#[ts(optional)]`, so it generates +//! `field?: T` rather than a required `field: T | null`. +//! 3. `serde`, `ts-rs`, and `schemars` derives travel together on every type. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +pub mod int4; +pub mod v2_3; + +/// EQL wire-format version. Hard-coded to `2` for every v2.x payload. +pub const EQL_SCHEMA_VERSION: u16 = 2; + +/// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. +/// +/// Shared by every payload in both tiers. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct Identifier { + /// Table name. + pub t: String, + /// Column name. + pub c: String, +} diff --git a/crates/eql-types/src/v2_3.rs b/crates/eql-types/src/v2_3.rs new file mode 100644 index 00000000..6c7957f7 --- /dev/null +++ b/crates/eql-types/src/v2_3.rs @@ -0,0 +1,102 @@ +//! # EQL v2.3 wire types — FROZEN +//! +//! `eql_v2_encrypted` is in production use by customers. The shapes here are +//! the v2.3 wire contract and MUST NOT change — not field names, not +//! optionality, not enum tagging. They mirror `eql-payload-v2.3.schema.json` +//! exactly, including its imperfections: +//! +//! - [`EncryptedPayload`] carries `hm`/`bf`/`ob` as independent optionals +//! ("any subset" — a column with several indexes carries several terms). +//! - [`SteVecTerm`] is an **untagged** enum — a consumer must sniff keys. +//! +//! New design work goes in sibling modules (see [`crate::int4`]), never here. + +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +/// `eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +#[serde(tag = "k")] +pub enum EqlEncrypted { + /// Scalar ciphertext payload. + #[serde(rename = "ct")] + Ct(EncryptedPayload), + /// STE-vector payload (jsonb / structured values). + #[serde(rename = "sv")] + Sv(SteVecPayload), +} + +/// Scalar storage payload (`k = "ct"`). +/// +/// FROZEN imperfection: `hm`/`bf`/`ob` are independently optional. A consumer +/// cannot tell from the type which terms are present — it must inspect at +/// runtime. This is precisely the gap the `protect-dynamodb` bug fell into. +/// The fix, for *new* types, is [`crate::int4`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct EncryptedPayload { + /// Schema version — always [`crate::EQL_SCHEMA_VERSION`]. + pub v: u16, + /// Table/column identifier. + pub i: Identifier, + /// mp_base85 ciphertext. Required. + pub c: String, + /// HMAC-SHA256 equality term — present iff a `unique` index is configured. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub hm: Option, + /// Bloom filter term — present iff a `match` index is configured. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub bf: Option>, + /// Block ORE term — present iff an `ore` index is configured. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub ob: Option>, +} + +/// STE-vector storage payload (`k = "sv"`). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct SteVecPayload { + /// Schema version. + pub v: u16, + /// Table/column identifier. + pub i: Identifier, + /// Per-selector encrypted entries; root document ciphertext at `sv[0].c`. + pub sv: Vec, +} + +/// One STE-vector element. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +pub struct SteVecElement { + /// Tokenized selector — deterministic per (path, key). + pub s: String, + /// Per-entry mp_base85 ciphertext. Required. + pub c: String, + /// Array marker — true when the selector points at a JSON array context. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub a: Option, + /// Exactly one equality / ordering term, flattened onto the element. + #[serde(flatten)] + pub term: SteVecTerm, +} + +/// SteVec element term. FROZEN as **untagged** — this is the v2.3 wire shape. +/// +/// A consumer must narrow with `'hm' in term`; there is no literal +/// discriminant. A *new* type would tag this — see [`crate::int4`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export)] +#[serde(untagged)] +pub enum SteVecTerm { + /// HMAC term — boolean leaves, and array / object root placeholders. + Hmac { hm: String }, + /// CLLW ORE term — string / number leaves. + OreCllw { oc: String }, +} diff --git a/crates/eql-types/tests/conformance.rs b/crates/eql-types/tests/conformance.rs new file mode 100644 index 00000000..8d13fbb4 --- /dev/null +++ b/crates/eql-types/tests/conformance.rs @@ -0,0 +1,96 @@ +//! Conformance fixtures — the real guarantee that Rust / TS / JSON Schema and +//! the wire format agree. Codegen guarantees *shape*; these round-trips +//! guarantee *behaviour*. + +use eql_types::int4::{Int4Eq, Int4Tagged}; +use eql_types::v2_3::EqlEncrypted; +use serde_json::json; + +#[test] +fn v2_3_scalar_round_trips() { + let wire = json!({ + "k": "ct", "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); +} + +#[test] +fn int4_eq_round_trips() { + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: Int4Eq = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); +} + +#[test] +fn int4_eq_rejects_missing_hmac() { + // The capability is type-enforced: an `int4_eq` payload with no `hm` is + // not representable. This is the bug class — a search term missing its + // index term — closed at the type boundary, before any consumer runs. + let no_hm = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext" + }); + let result: Result = serde_json::from_value(no_hm); + assert!(result.is_err(), "Int4Eq must reject a payload with no hm"); +} + +#[test] +fn legacy_payload_silently_accepts_missing_terms() { + // Contrast: the frozen v2.3 scalar type accepts a payload carrying no + // index terms at all — `hm`/`bf`/`ob` are optional. Nothing is wrong with + // the payload *as v2.3*; the point is the type tells a consumer nothing + // about which operators it can support. Hence the runtime guard. + let bare = json!({ + "k": "ct", "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext" + }); + let parsed: EqlEncrypted = serde_json::from_value(bare).unwrap(); + match parsed { + EqlEncrypted::Ct(p) => { + assert!(p.hm.is_none() && p.bf.is_none() && p.ob.is_none()); + } + EqlEncrypted::Sv(_) => panic!("expected Ct"), + } +} + +#[test] +fn int4_tagged_proposal_round_trips_and_discriminates() { + let wire = json!({ + "x": "int4_eq", "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: Int4Tagged = serde_json::from_value(wire.clone()).unwrap(); + assert!(matches!(parsed, Int4Tagged::Eq { .. })); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); +} + +#[test] +fn dump_json_schemas() { + use schemars::schema_for; + std::fs::create_dir_all("schema").unwrap(); + let schemas = [ + ("EqlEncrypted", schema_for!(EqlEncrypted)), + ("Int4Eq", schema_for!(Int4Eq)), + ("Int4Tagged", schema_for!(Int4Tagged)), + ]; + for (name, schema) in schemas { + std::fs::write( + format!("schema/{name}.json"), + serde_json::to_string_pretty(&schema).unwrap(), + ) + .unwrap(); + } +} From 01dd8fe947cffb05539383d31051fecc8692194d Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 9 Jun 2026 21:59:31 +1000 Subject: [PATCH 02/12] fix(eql-types): correct bf signedness and accept k-less scalar payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two wire-fidelity bugs in the FROZEN v2.3 types — both diverged from the canonical eql-payload-v2.3.schema.json they claim to mirror: - `bf` is stored by EQL as `smallint[]` (signed i16). A `match` filter sized above 32768 (configurable up to 65536) emits upper-half bit positions as negative signed values, which `Option>` cannot deserialize. Changed to `Option>`. - `EqlEncrypted` required `k` via `#[serde(tag = "k")]`, but the wire contract and `eql_v2.check_encrypted` make `k` optional on the scalar form (required: only v, c, i) and discriminate on c-vs-sv. Kept the tag for serialization and codegen (TS discriminated union + JSON Schema oneOf) and hand-wrote a tolerant Deserialize that mirrors check_encrypted: key off `k` when present, else fall back to c/sv. Also corrects the `bf` range in the canonical reference schema (minimum 0 -> signed smallint -32768..32767) to match `smallint[]`. Regenerated bindings/ and schema/ from the updated types; added conformance tests for k-less scalars, negative bf, and the sv path (the previously-untested flatten + untagged SteVecTerm route). --- crates/eql-types/bindings/EncryptedPayload.ts | 5 ++ crates/eql-types/bindings/EqlEncrypted.ts | 11 +++- crates/eql-types/schema/EqlEncrypted.json | 7 +-- crates/eql-types/src/v2_3.rs | 53 ++++++++++++++++- crates/eql-types/tests/conformance.rs | 59 +++++++++++++++++++ .../schema/eql-payload-v2.3.schema.json | 4 +- 6 files changed, 129 insertions(+), 10 deletions(-) diff --git a/crates/eql-types/bindings/EncryptedPayload.ts b/crates/eql-types/bindings/EncryptedPayload.ts index 3c037f71..838d2c8c 100644 --- a/crates/eql-types/bindings/EncryptedPayload.ts +++ b/crates/eql-types/bindings/EncryptedPayload.ts @@ -28,6 +28,11 @@ c: string, hm?: string, /** * Bloom filter term — present iff a `match` index is configured. + * + * Array of set bit positions. EQL stores these as `smallint[]` (signed + * `i16`); a `match` filter sized above 32768 (configurable up to 65536) + * emits upper-half positions as negative signed values, so this is `i16`, + * not `u16` — a `u16` cannot deserialize a real large-filter payload. */ bf?: Array, /** diff --git a/crates/eql-types/bindings/EqlEncrypted.ts b/crates/eql-types/bindings/EqlEncrypted.ts index 3e7bd8ed..3e4b4299 100644 --- a/crates/eql-types/bindings/EqlEncrypted.ts +++ b/crates/eql-types/bindings/EqlEncrypted.ts @@ -3,6 +3,15 @@ import type { EncryptedPayload } from "./EncryptedPayload"; import type { SteVecPayload } from "./SteVecPayload"; /** - * `eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`. + * `eql_v2_encrypted` — the EQL v2.3 storage payload. + * + * **Serialization** always emits the `k` discriminator (`"ct"` / `"sv"`) — + * this is what drives the internally-tagged TypeScript union and the JSON + * Schema `oneOf`. **Deserialization** is hand-written (below) because the + * v2.3 wire contract makes `k` *optional* on the scalar form: + * `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on + * the presence of `c` vs `sv`, not on `k` (the scalar form requires only + * `v`, `c`, `i`). A `#[serde(tag = "k")]`-derived `Deserialize` would reject a + * schema-valid scalar payload that omits `k`. */ export type EqlEncrypted = { "k": "ct" } & EncryptedPayload | { "k": "sv" } & SteVecPayload; diff --git a/crates/eql-types/schema/EqlEncrypted.json b/crates/eql-types/schema/EqlEncrypted.json index 8df7b1ad..fe28cc9e 100644 --- a/crates/eql-types/schema/EqlEncrypted.json +++ b/crates/eql-types/schema/EqlEncrypted.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "EqlEncrypted", - "description": "`eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`.", + "description": "`eql_v2_encrypted` — the EQL v2.3 storage payload.\n\n**Serialization** always emits the `k` discriminator (`\"ct\"` / `\"sv\"`) — this is what drives the internally-tagged TypeScript union and the JSON Schema `oneOf`. **Deserialization** is hand-written (below) because the v2.3 wire contract makes `k` *optional* on the scalar form: `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on the presence of `c` vs `sv`, not on `k` (the scalar form requires only `v`, `c`, `i`). A `#[serde(tag = \"k\")]`-derived `Deserialize` would reject a schema-valid scalar payload that omits `k`.", "oneOf": [ { "description": "Scalar ciphertext payload.", @@ -14,15 +14,14 @@ ], "properties": { "bf": { - "description": "Bloom filter term — present iff a `match` index is configured.", + "description": "Bloom filter term — present iff a `match` index is configured.\n\nArray of set bit positions. EQL stores these as `smallint[]` (signed `i16`); a `match` filter sized above 32768 (configurable up to 65536) emits upper-half positions as negative signed values, so this is `i16`, not `u16` — a `u16` cannot deserialize a real large-filter payload.", "type": [ "array", "null" ], "items": { "type": "integer", - "format": "uint16", - "minimum": 0.0 + "format": "int16" } }, "c": { diff --git a/crates/eql-types/src/v2_3.rs b/crates/eql-types/src/v2_3.rs index 6c7957f7..2cc578ad 100644 --- a/crates/eql-types/src/v2_3.rs +++ b/crates/eql-types/src/v2_3.rs @@ -16,8 +16,17 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; -/// `eql_v2_encrypted` — the EQL v2.3 storage payload. Discriminated on `k`. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +/// `eql_v2_encrypted` — the EQL v2.3 storage payload. +/// +/// **Serialization** always emits the `k` discriminator (`"ct"` / `"sv"`) — +/// this is what drives the internally-tagged TypeScript union and the JSON +/// Schema `oneOf`. **Deserialization** is hand-written (below) because the +/// v2.3 wire contract makes `k` *optional* on the scalar form: +/// `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on +/// the presence of `c` vs `sv`, not on `k` (the scalar form requires only +/// `v`, `c`, `i`). A `#[serde(tag = "k")]`-derived `Deserialize` would reject a +/// schema-valid scalar payload that omits `k`. +#[derive(Clone, Debug, PartialEq, Serialize, TS, JsonSchema)] #[ts(export)] #[serde(tag = "k")] pub enum EqlEncrypted { @@ -29,6 +38,39 @@ pub enum EqlEncrypted { Sv(SteVecPayload), } +impl<'de> Deserialize<'de> for EqlEncrypted { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + // Mirror eql_v2.check_encrypted: key off `k` when present, otherwise + // fall back to which body field is present (`sv` => STE vector, + // otherwise scalar `ct`). `k` is optional on the scalar form per + // eql-payload-v2.3.schema.json (required there: only `v`, `c`, `i`). + let value = serde_json::Value::deserialize(deserializer)?; + let is_sv = match value.get("k").and_then(serde_json::Value::as_str) { + Some("sv") => true, + Some("ct") => false, + Some(other) => { + return Err(D::Error::custom(format!( + "unknown EQL payload kind: k = {other:?}" + ))) + } + None => value.get("sv").is_some(), + }; + if is_sv { + serde_json::from_value(value) + .map(EqlEncrypted::Sv) + .map_err(D::Error::custom) + } else { + serde_json::from_value(value) + .map(EqlEncrypted::Ct) + .map_err(D::Error::custom) + } + } +} + /// Scalar storage payload (`k = "ct"`). /// /// FROZEN imperfection: `hm`/`bf`/`ob` are independently optional. A consumer @@ -49,9 +91,14 @@ pub struct EncryptedPayload { #[ts(optional)] pub hm: Option, /// Bloom filter term — present iff a `match` index is configured. + /// + /// Array of set bit positions. EQL stores these as `smallint[]` (signed + /// `i16`); a `match` filter sized above 32768 (configurable up to 65536) + /// emits upper-half positions as negative signed values, so this is `i16`, + /// not `u16` — a `u16` cannot deserialize a real large-filter payload. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub bf: Option>, + pub bf: Option>, /// Block ORE term — present iff an `ore` index is configured. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] diff --git a/crates/eql-types/tests/conformance.rs b/crates/eql-types/tests/conformance.rs index 8d13fbb4..9d13cd70 100644 --- a/crates/eql-types/tests/conformance.rs +++ b/crates/eql-types/tests/conformance.rs @@ -64,6 +64,65 @@ fn legacy_payload_silently_accepts_missing_terms() { } } +#[test] +fn v2_3_scalar_without_k_is_accepted() { + // The canonical v2.3 schema makes `k` optional on the scalar form + // (required: v, c, i) and check_encrypted discriminates on c-vs-sv, not k. + // A scalar payload that omits `k` must still deserialize as `Ct`. + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: EqlEncrypted = serde_json::from_value(wire).unwrap(); + assert!(matches!(parsed, EqlEncrypted::Ct(_))); + // Serialization always re-emits the discriminator. + assert_eq!( + serde_json::to_value(&parsed).unwrap(), + json!({ + "k": "ct", "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }) + ); +} + +#[test] +fn v2_3_bf_accepts_negative_smallint() { + // `bf` is stored as smallint[] (signed i16). A `match` filter sized above + // 32768 (allowed up to 65536) emits upper-half bit positions as negative + // signed smallints; the type must round-trip them. + let wire = json!({ + "k": "ct", "v": 2, + "i": { "t": "users", "c": "email" }, + "c": "mp_base85_ciphertext", + "bf": [-1, -32768, 32767, 0] + }); + let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); + assert!(matches!(parsed, EqlEncrypted::Ct(_))); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); +} + +#[test] +fn v2_3_ste_vec_round_trips() { + // Exercises the `sv` path: SteVecPayload plus the flatten + untagged + // SteVecTerm (both `hm` and `oc` elements) — the crate's most fragile serde + // construct, and the route the hand-written EqlEncrypted::deserialize takes. + let wire = json!({ + "k": "sv", "v": 2, + "i": { "t": "users", "c": "profile" }, + "sv": [ + { "s": "selector_root", "c": "ct_root", "hm": "deadbeef" }, + { "s": "selector_name", "c": "ct_name", "oc": "00cafe", "a": true } + ] + }); + let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); + assert!(matches!(parsed, EqlEncrypted::Sv(_))); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); +} + #[test] fn int4_tagged_proposal_round_trips_and_discriminates() { let wire = json!({ diff --git a/docs/reference/schema/eql-payload-v2.3.schema.json b/docs/reference/schema/eql-payload-v2.3.schema.json index 0b1e5741..02df122a 100644 --- a/docs/reference/schema/eql-payload-v2.3.schema.json +++ b/docs/reference/schema/eql-payload-v2.3.schema.json @@ -184,9 +184,9 @@ "bf": { "title": "Bloom filter (bf)", - "description": "Bloom filter representation as an array of set bit positions. Used by `LIKE` / `ILIKE` (`~~`, `~~*`) via `eql_v2.bloom_filter` and the corresponding GIN index.", + "description": "Bloom filter representation as an array of set bit positions. Used by `LIKE` / `ILIKE` (`~~`, `~~*`) via `eql_v2.bloom_filter` and the corresponding GIN index. Stored as `smallint[]` (signed `int2`): the filter size is a power of two up to 65536, so positions in the upper half of a filter larger than 32768 are emitted as negative signed values (two's-complement of the unsigned position). Consumers must use a signed 16-bit integer type.", "type": "array", - "items": { "type": "integer", "minimum": 0 } + "items": { "type": "integer", "minimum": -32768, "maximum": 32767 } }, "ob": { From af715c03c3acf86279561dd65160d8c767a0fe97 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 10 Jun 2026 20:21:33 +1000 Subject: [PATCH 03/12] feat(eql-types): v3 domain payload types, parity-gated against the catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the eql_v2_int4 prototype tier with a v3 tier covering every eql_v3 encrypted domain: one capability-encoded struct per SQL domain (23 across int4/int2/int8/date/timestamptz/text), stamped by the eql_v3_domain! macro from reusable term newtypes (Ciphertext, Hmac256, OreBlockU64_8_256, BloomFilter). Index terms are required fields — Option does not appear in the tier — mirroring the generated domain CHECKs (envelope v/i/c + term keys, envelope version still v: 2). tests/catalog_parity.rs dev-depends on eql-scalars and asserts the v3 registry exactly covers CATALOG (every domain, catalog order) and that each type's schemars required keys equal envelope + catalog term keys, so the crate cannot drift from the generated SQL surface. ts-rs bindings land in bindings/v3/, JSON Schemas (with injected $id) in schema/v3/, both checked in; mise types:generate / types:check plus a CI step in the rust-crates job keep them fresh. eql-types joins the workspace and the lean test:crates set. The Int4Tagged self-describing proposal is removed from code (the x tag is not on the v3 wire) and preserved as README prose. --- .github/workflows/test-eql.yml | 7 + Cargo.lock | 85 ++++++++++ Cargo.toml | 4 + crates/eql-types/Cargo.toml | 6 + crates/eql-types/README.md | 68 ++++++-- crates/eql-types/bindings/Int4.ts | 19 --- crates/eql-types/bindings/Int4Eq.ts | 27 --- crates/eql-types/bindings/Int4Ord.ts | 28 ---- crates/eql-types/bindings/Int4Tagged.ts | 10 -- crates/eql-types/bindings/v3/BloomFilter.ts | 11 ++ crates/eql-types/bindings/v3/Ciphertext.ts | 8 + crates/eql-types/bindings/v3/Date.ts | 20 +++ crates/eql-types/bindings/v3/DateEq.ts | 25 +++ crates/eql-types/bindings/v3/DateOrd.ts | 25 +++ crates/eql-types/bindings/v3/DateOrdOre.ts | 25 +++ crates/eql-types/bindings/v3/Hmac256.ts | 7 + crates/eql-types/bindings/v3/Int2.ts | 20 +++ crates/eql-types/bindings/v3/Int2Eq.ts | 25 +++ crates/eql-types/bindings/v3/Int2Ord.ts | 25 +++ crates/eql-types/bindings/v3/Int2OrdOre.ts | 25 +++ crates/eql-types/bindings/v3/Int4.ts | 20 +++ crates/eql-types/bindings/v3/Int4Eq.ts | 25 +++ crates/eql-types/bindings/v3/Int4Ord.ts | 25 +++ crates/eql-types/bindings/v3/Int4OrdOre.ts | 27 +++ crates/eql-types/bindings/v3/Int8.ts | 20 +++ crates/eql-types/bindings/v3/Int8Eq.ts | 25 +++ crates/eql-types/bindings/v3/Int8Ord.ts | 25 +++ crates/eql-types/bindings/v3/Int8OrdOre.ts | 25 +++ .../bindings/v3/OreBlockU64_8_256.ts | 9 + crates/eql-types/bindings/v3/Text.ts | 20 +++ crates/eql-types/bindings/v3/TextEq.ts | 25 +++ crates/eql-types/bindings/v3/TextMatch.ts | 25 +++ crates/eql-types/bindings/v3/TextOrd.ts | 26 +++ crates/eql-types/bindings/v3/TextOrdOre.ts | 26 +++ crates/eql-types/bindings/v3/Timestamptz.ts | 20 +++ crates/eql-types/bindings/v3/TimestamptzEq.ts | 25 +++ crates/eql-types/schema/Int4Tagged.json | 125 -------------- .../schema/{Int4Eq.json => v3/date.json} | 82 ++++----- crates/eql-types/schema/v3/date_eq.json | 73 ++++++++ crates/eql-types/schema/v3/date_ord.json | 76 +++++++++ crates/eql-types/schema/v3/date_ord_ore.json | 76 +++++++++ crates/eql-types/schema/v3/int2.json | 60 +++++++ crates/eql-types/schema/v3/int2_eq.json | 73 ++++++++ crates/eql-types/schema/v3/int2_ord.json | 76 +++++++++ crates/eql-types/schema/v3/int2_ord_ore.json | 76 +++++++++ crates/eql-types/schema/v3/int4.json | 60 +++++++ crates/eql-types/schema/v3/int4_eq.json | 73 ++++++++ crates/eql-types/schema/v3/int4_ord.json | 76 +++++++++ crates/eql-types/schema/v3/int4_ord_ore.json | 76 +++++++++ crates/eql-types/schema/v3/int8.json | 60 +++++++ crates/eql-types/schema/v3/int8_eq.json | 73 ++++++++ crates/eql-types/schema/v3/int8_ord.json | 76 +++++++++ crates/eql-types/schema/v3/int8_ord_ore.json | 76 +++++++++ crates/eql-types/schema/v3/text.json | 60 +++++++ crates/eql-types/schema/v3/text_eq.json | 73 ++++++++ crates/eql-types/schema/v3/text_match.json | 77 +++++++++ crates/eql-types/schema/v3/text_ord.json | 76 +++++++++ crates/eql-types/schema/v3/text_ord_ore.json | 76 +++++++++ crates/eql-types/schema/v3/timestamptz.json | 60 +++++++ .../eql-types/schema/v3/timestamptz_eq.json | 73 ++++++++ crates/eql-types/src/int4.rs | 125 -------------- crates/eql-types/src/lib.rs | 10 +- crates/eql-types/src/v3/date.rs | 35 ++++ crates/eql-types/src/v3/int2.rs | 33 ++++ crates/eql-types/src/v3/int4.rs | 41 +++++ crates/eql-types/src/v3/int8.rs | 33 ++++ crates/eql-types/src/v3/mod.rs | 80 +++++++++ crates/eql-types/src/v3/registry.rs | 72 ++++++++ crates/eql-types/src/v3/terms.rs | 71 ++++++++ crates/eql-types/src/v3/text.rs | 44 +++++ crates/eql-types/src/v3/timestamptz.rs | 20 +++ crates/eql-types/tests/catalog_parity.rs | 63 +++++++ crates/eql-types/tests/conformance.rs | 66 +------- crates/eql-types/tests/export.rs | 40 +++++ crates/eql-types/tests/v3_conformance.rs | 158 ++++++++++++++++++ mise.toml | 50 +++++- 76 files changed, 3102 insertions(+), 459 deletions(-) delete mode 100644 crates/eql-types/bindings/Int4.ts delete mode 100644 crates/eql-types/bindings/Int4Eq.ts delete mode 100644 crates/eql-types/bindings/Int4Ord.ts delete mode 100644 crates/eql-types/bindings/Int4Tagged.ts create mode 100644 crates/eql-types/bindings/v3/BloomFilter.ts create mode 100644 crates/eql-types/bindings/v3/Ciphertext.ts create mode 100644 crates/eql-types/bindings/v3/Date.ts create mode 100644 crates/eql-types/bindings/v3/DateEq.ts create mode 100644 crates/eql-types/bindings/v3/DateOrd.ts create mode 100644 crates/eql-types/bindings/v3/DateOrdOre.ts create mode 100644 crates/eql-types/bindings/v3/Hmac256.ts create mode 100644 crates/eql-types/bindings/v3/Int2.ts create mode 100644 crates/eql-types/bindings/v3/Int2Eq.ts create mode 100644 crates/eql-types/bindings/v3/Int2Ord.ts create mode 100644 crates/eql-types/bindings/v3/Int2OrdOre.ts create mode 100644 crates/eql-types/bindings/v3/Int4.ts create mode 100644 crates/eql-types/bindings/v3/Int4Eq.ts create mode 100644 crates/eql-types/bindings/v3/Int4Ord.ts create mode 100644 crates/eql-types/bindings/v3/Int4OrdOre.ts create mode 100644 crates/eql-types/bindings/v3/Int8.ts create mode 100644 crates/eql-types/bindings/v3/Int8Eq.ts create mode 100644 crates/eql-types/bindings/v3/Int8Ord.ts create mode 100644 crates/eql-types/bindings/v3/Int8OrdOre.ts create mode 100644 crates/eql-types/bindings/v3/OreBlockU64_8_256.ts create mode 100644 crates/eql-types/bindings/v3/Text.ts create mode 100644 crates/eql-types/bindings/v3/TextEq.ts create mode 100644 crates/eql-types/bindings/v3/TextMatch.ts create mode 100644 crates/eql-types/bindings/v3/TextOrd.ts create mode 100644 crates/eql-types/bindings/v3/TextOrdOre.ts create mode 100644 crates/eql-types/bindings/v3/Timestamptz.ts create mode 100644 crates/eql-types/bindings/v3/TimestamptzEq.ts delete mode 100644 crates/eql-types/schema/Int4Tagged.json rename crates/eql-types/schema/{Int4Eq.json => v3/date.json} (50%) create mode 100644 crates/eql-types/schema/v3/date_eq.json create mode 100644 crates/eql-types/schema/v3/date_ord.json create mode 100644 crates/eql-types/schema/v3/date_ord_ore.json create mode 100644 crates/eql-types/schema/v3/int2.json create mode 100644 crates/eql-types/schema/v3/int2_eq.json create mode 100644 crates/eql-types/schema/v3/int2_ord.json create mode 100644 crates/eql-types/schema/v3/int2_ord_ore.json create mode 100644 crates/eql-types/schema/v3/int4.json create mode 100644 crates/eql-types/schema/v3/int4_eq.json create mode 100644 crates/eql-types/schema/v3/int4_ord.json create mode 100644 crates/eql-types/schema/v3/int4_ord_ore.json create mode 100644 crates/eql-types/schema/v3/int8.json create mode 100644 crates/eql-types/schema/v3/int8_eq.json create mode 100644 crates/eql-types/schema/v3/int8_ord.json create mode 100644 crates/eql-types/schema/v3/int8_ord_ore.json create mode 100644 crates/eql-types/schema/v3/text.json create mode 100644 crates/eql-types/schema/v3/text_eq.json create mode 100644 crates/eql-types/schema/v3/text_match.json create mode 100644 crates/eql-types/schema/v3/text_ord.json create mode 100644 crates/eql-types/schema/v3/text_ord_ore.json create mode 100644 crates/eql-types/schema/v3/timestamptz.json create mode 100644 crates/eql-types/schema/v3/timestamptz_eq.json delete mode 100644 crates/eql-types/src/int4.rs create mode 100644 crates/eql-types/src/v3/date.rs create mode 100644 crates/eql-types/src/v3/int2.rs create mode 100644 crates/eql-types/src/v3/int4.rs create mode 100644 crates/eql-types/src/v3/int8.rs create mode 100644 crates/eql-types/src/v3/mod.rs create mode 100644 crates/eql-types/src/v3/registry.rs create mode 100644 crates/eql-types/src/v3/terms.rs create mode 100644 crates/eql-types/src/v3/text.rs create mode 100644 crates/eql-types/src/v3/timestamptz.rs create mode 100644 crates/eql-types/tests/catalog_parity.rs create mode 100644 crates/eql-types/tests/export.rs create mode 100644 crates/eql-types/tests/v3_conformance.rs diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 776d701a..4e0a0d63 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -91,6 +91,13 @@ jobs: rustup component add --toolchain ${active_rust_toolchain} rustfmt clippy mise run test:crates + # Freshness gate for the eql-types codegen output: regenerate the + # TypeScript bindings and JSON Schemas and fail if the checked-in + # copies differ. Reuses the build artifacts from the step above. + - name: Verify eql-types bindings and schemas are fresh + run: | + mise run types:check + codegen: name: "Encrypted-domain codegen" runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 20adc322..9d4a77df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,6 +1125,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -1178,6 +1184,17 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "eql-types" +version = "0.1.0" +dependencies = [ + "eql-scalars", + "schemars", + "serde", + "serde_json", + "ts-rs", +] + [[package]] name = "eql_tests" version = "0.1.0" @@ -3365,6 +3382,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.108", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3456,6 +3497,17 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "serde_json" version = "1.0.145" @@ -3982,6 +4034,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -4319,6 +4380,30 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" +dependencies = [ + "lazy_static", + "serde_json", + "thiserror 2.0.18", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "termcolor", +] + [[package]] name = "typenum" version = "1.20.0" diff --git a/Cargo.toml b/Cargo.toml index 6e0b6760..0ce781ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ # crates/eql-codegen — the SQL generator binary (stub here; Plan 2 fills it in). # crates/eql-tests-macros — proc-macros expanding the single scalar-harness # list into the per-type SQLx-matrix wiring. +# crates/eql-types — canonical wire types for EQL payloads (Rust → ts-rs +# TypeScript bindings + schemars JSON Schema); parity- +# tested against the eql-scalars catalog. # tests/sqlx — the existing `eql_tests` SQLx integration crate. # # resolver = "2" keeps the heavy test-crate feature set (sqlx/tokio/cipherstash- @@ -20,6 +23,7 @@ members = [ "crates/eql-scalars", "crates/eql-codegen", "crates/eql-tests-macros", + "crates/eql-types", "tests/sqlx", ] default-members = ["tests/sqlx"] diff --git a/crates/eql-types/Cargo.toml b/crates/eql-types/Cargo.toml index deb841da..d2021998 100644 --- a/crates/eql-types/Cargo.toml +++ b/crates/eql-types/Cargo.toml @@ -9,3 +9,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" ts-rs = { version = "10", features = ["serde-json-impl"] } schemars = "0.8" + +[dev-dependencies] +# Parity oracle: tests/catalog_parity.rs asserts the v3 registry exactly +# covers eql_scalars::CATALOG, so the types here cannot drift from the +# generated SQL surface. +eql-scalars = { path = "../eql-scalars" } diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index 1c18cefe..20e705f7 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -1,4 +1,4 @@ -# eql-types (prototype) +# eql-types Canonical wire types for EQL payloads — **one Rust definition per payload shape**, intended as the single source of truth for: @@ -7,9 +7,6 @@ shape**, intended as the single source of truth for: - **TypeScript** — generated via [`ts-rs`] into [`bindings/`](bindings/) - **JSON Schema** — generated via [`schemars`] into [`schema/`](schema/) -> **Status: prototype / draft for discussion.** Not wired into the EQL build -> or CI. See the pull request description for full context. - ## Why Type information is lost at every hop of `EQL → cipherstash-client → @@ -24,25 +21,68 @@ hand-copying. | Module | Tier | Rule | |--------|------|------| | [`src/v2_3.rs`](src/v2_3.rs) | `eql_v2_encrypted` v2.3 wire contract | **FROZEN** — in production; mirrors `eql-payload-v2.3.schema.json`; must not change | -| [`src/int4.rs`](src/int4.rs) | `eql_v2_int4` variant family (#225) | **Design freedom** — capability-encoded types | +| [`src/v3/`](src/v3/) | `eql_v3` encrypted-domain families | One struct per SQL domain, parity-tested against `eql-scalars::CATALOG` | -## Capability-encoded types +## Capability-encoded types (the v3 tier) `eql_v2_encrypted` is one type with every index term optional, so consumers -must guess at runtime which terms are present. The `int4` family instead has -one type per capability — `Int4` / `Int4Eq` / `Int4Ord` — each carrying its -index terms as **required** fields. The capability is the type identity; -`Option` never appears. +must guess at runtime which terms are present. The v3 tier instead has one +type per **SQL domain** — `Int4` / `Int4Eq` / `Int4Ord` / `Int4OrdOre`, and +likewise for `int2`, `int8`, `date`, `timestamptz` (eq-only), and `text` +(which adds `TextMatch`) — each carrying its index terms as **required** +fields. The capability is the type identity; `Option` never appears. + +Shared wire fields are reusable newtypes in +[`src/v3/terms.rs`](src/v3/terms.rs): + +| Newtype | Wire key | Inner | Backs | +|---------|----------|-------|-------| +| `Ciphertext` | `c` | `String` | every domain (envelope) | +| `Hmac256` | `hm` | `String` | `_eq` domains | +| `OreBlockU64_8_256` | `ob` | `Vec` | `_ord` / `_ord_ore` domains | +| `BloomFilter` | `bf` | `Vec` (signed!) | `_match` domains | + +Note "v3" names the SQL schema generation (`eql_v3.*`); the JSON envelope +version is still `v: 2` — the generated domain CHECKs assert it, and the wire +field names are unchanged from v2 (the purpose-named rename in +`docs/plans/eql-payload-scheme-discipline-rfc.md` is deferred). + +### Drift protection + +`tests/catalog_parity.rs` asserts the [`v3::registry`](src/v3/registry.rs) +exactly covers `eql-scalars::CATALOG` (every domain, in order) and that each +type's required JSON keys equal the envelope keys plus the catalog's term +keys. Adding a scalar to the catalog without adding its types here fails the +build; so does accidentally making a term field `Option`. ## Develop ```sh -cargo test +mise run types:generate # clean-regenerate bindings/ and schema/ +mise run types:check # regenerate + fail if checked-in outputs are stale ``` -Runs the conformance round-trip tests and regenerates `bindings/` (TypeScript) -and `schema/` (JSON Schema). Both directories are checked in so reviewers can -see the codegen output without running anything. +Both wrap `cargo test -p eql-types`, which runs the conformance round-trip +tests and regenerates `bindings/` (TypeScript, via ts-rs) and `schema/` +(JSON Schema, via `tests/export.rs`). Both directories are checked in so +reviewers can see the codegen output without running anything; CI runs +`types:check` to keep them fresh. + +## Future direction: self-describing payloads + +On the wire, a v3 payload is discriminated only by *which key is present* +(`hm` vs `ob` vs `bf`) — the SQL domain name carries the rest. Once the JSON +leaves SQL (into protect-ffi, into TypeScript, into a log line) that +information is gone, and a consumer is back to sniffing keys: the untagged +failure mode that produced the original protect-dynamodb bug. An earlier +prototype here carried an `Int4Tagged` enum with a one-field capability tag +(`"x": "int4_eq"`), which generates a clean TypeScript discriminated union +and a JSON Schema `oneOf` with per-branch `const`s. It was removed because +the tag is not part of the v3 wire contract (the generated domain CHECKs +know no `x` key) — but it remains the recommended shape if a future payload +revision adds a discriminator. See +`docs/plans/eql-payload-scheme-discipline-rfc.md` for the wider payload +evolution plan. [`ts-rs`]: https://github.com/Aleph-Alpha/ts-rs [`schemars`]: https://graham.cool/schemars/ diff --git a/crates/eql-types/bindings/Int4.ts b/crates/eql-types/bindings/Int4.ts deleted file mode 100644 index ffddccf3..00000000 --- a/crates/eql-types/bindings/Int4.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; - -/** - * `eql_v2_int4` — storage only. Carries `c`; every operator is blocked. - */ -export type Int4 = { -/** - * Schema version. - */ -v: number, -/** - * Table/column identifier. - */ -i: Identifier, -/** - * mp_base85 ciphertext. Required by the domain's CHECK constraint. - */ -c: string, }; diff --git a/crates/eql-types/bindings/Int4Eq.ts b/crates/eql-types/bindings/Int4Eq.ts deleted file mode 100644 index de28bece..00000000 --- a/crates/eql-types/bindings/Int4Eq.ts +++ /dev/null @@ -1,27 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; - -/** - * `eql_v2_int4_eq` — HMAC equality (`=`, `<>`). - * - * `hm` is a required field. There is no `Option`: the type *is* the - * equality capability. A payload without `hm` cannot be deserialized into - * this type — the Rust analogue of the SQL domain's CHECK constraint. - */ -export type Int4Eq = { -/** - * Schema version. - */ -v: number, -/** - * Table/column identifier. - */ -i: Identifier, -/** - * mp_base85 ciphertext. Required. - */ -c: string, -/** - * HMAC-SHA256 equality term. Required. - */ -hm: string, }; diff --git a/crates/eql-types/bindings/Int4Ord.ts b/crates/eql-types/bindings/Int4Ord.ts deleted file mode 100644 index c0e15301..00000000 --- a/crates/eql-types/bindings/Int4Ord.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; - -/** - * `eql_v2_int4_ord` — equality + ORE-block range (`=` `<>` `<` `<=` `>` `>=`). - * - * Deliberately carries no `hm`: ORE over a full-domain `int4` is lossless, so - * the order term `ob` doubles as an exact equality term. - * (`eql_v2_int4_ord_ore` in #225 is the same shape under a scheme-explicit - * name — structurally identical, so it is not a separate Rust type.) - */ -export type Int4Ord = { -/** - * Schema version. - */ -v: number, -/** - * Table/column identifier. - */ -i: Identifier, -/** - * mp_base85 ciphertext. Required. - */ -c: string, -/** - * Block ORE term. Required — serves both range and equality. - */ -ob: Array, }; diff --git a/crates/eql-types/bindings/Int4Tagged.ts b/crates/eql-types/bindings/Int4Tagged.ts deleted file mode 100644 index 09a90e43..00000000 --- a/crates/eql-types/bindings/Int4Tagged.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; - -/** - * **Proposed.** Self-describing int4 payload — `x` is the capability tag. - * - * Generates a clean TypeScript discriminated union (`switch (p.x)` with - * exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`. - */ -export type Int4Tagged = { "x": "int4", v: number, i: Identifier, c: string, } | { "x": "int4_eq", v: number, i: Identifier, c: string, hm: string, } | { "x": "int4_ord", v: number, i: Identifier, c: string, ob: Array, }; diff --git a/crates/eql-types/bindings/v3/BloomFilter.ts b/crates/eql-types/bindings/v3/BloomFilter.ts new file mode 100644 index 00000000..a1ac0d7c --- /dev/null +++ b/crates/eql-types/bindings/v3/BloomFilter.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Bloom-filter match term — the `bf` wire key. Backs the `_match` domains + * (`~~` containment via `@>`/`<@`). + * + * **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, + * and filters sized above 32768 emit upper-half bit positions as negative + * signed values (same rationale as `v2_3::EncryptedPayload::bf`). + */ +export type BloomFilter = Array; diff --git a/crates/eql-types/bindings/v3/Ciphertext.ts b/crates/eql-types/bindings/v3/Ciphertext.ts new file mode 100644 index 00000000..7beff648 --- /dev/null +++ b/crates/eql-types/bindings/v3/Ciphertext.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * mp_base85 source ciphertext — the `c` envelope key. + * + * Required by every v3 domain CHECK; present on every payload. + */ +export type Ciphertext = string; diff --git a/crates/eql-types/bindings/v3/Date.ts b/crates/eql-types/bindings/v3/Date.ts new file mode 100644 index 00000000..12f801e5 --- /dev/null +++ b/crates/eql-types/bindings/v3/Date.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.date` — storage only; every operator is blocked. + */ +export type Date = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/DateEq.ts b/crates/eql-types/bindings/v3/DateEq.ts new file mode 100644 index 00000000..db4b23c3 --- /dev/null +++ b/crates/eql-types/bindings/v3/DateEq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.date_eq` — HMAC equality (`=`, `<>`). + */ +export type DateEq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/DateOrd.ts b/crates/eql-types/bindings/v3/DateOrd.ts new file mode 100644 index 00000000..8eb61922 --- /dev/null +++ b/crates/eql-types/bindings/v3/DateOrd.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). + */ +export type DateOrd = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/DateOrdOre.ts b/crates/eql-types/bindings/v3/DateOrdOre.ts new file mode 100644 index 00000000..8e6496fc --- /dev/null +++ b/crates/eql-types/bindings/v3/DateOrdOre.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. + */ +export type DateOrdOre = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Hmac256.ts b/crates/eql-types/bindings/v3/Hmac256.ts new file mode 100644 index 00000000..22cefc00 --- /dev/null +++ b/crates/eql-types/bindings/v3/Hmac256.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains + * (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. + */ +export type Hmac256 = string; diff --git a/crates/eql-types/bindings/v3/Int2.ts b/crates/eql-types/bindings/v3/Int2.ts new file mode 100644 index 00000000..5457a00f --- /dev/null +++ b/crates/eql-types/bindings/v3/Int2.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int2` — storage only; every operator is blocked. + */ +export type Int2 = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int2Eq.ts b/crates/eql-types/bindings/v3/Int2Eq.ts new file mode 100644 index 00000000..1563906d --- /dev/null +++ b/crates/eql-types/bindings/v3/Int2Eq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). + */ +export type Int2Eq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int2Ord.ts b/crates/eql-types/bindings/v3/Int2Ord.ts new file mode 100644 index 00000000..b0720d69 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int2Ord.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). + */ +export type Int2Ord = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int2OrdOre.ts b/crates/eql-types/bindings/v3/Int2OrdOre.ts new file mode 100644 index 00000000..7b2c3416 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int2OrdOre.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. + */ +export type Int2OrdOre = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int4.ts b/crates/eql-types/bindings/v3/Int4.ts new file mode 100644 index 00000000..0918e410 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int4.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int4` — storage only; every operator is blocked. + */ +export type Int4 = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int4Eq.ts b/crates/eql-types/bindings/v3/Int4Eq.ts new file mode 100644 index 00000000..98c7ccc4 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int4Eq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). + */ +export type Int4Eq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int4Ord.ts b/crates/eql-types/bindings/v3/Int4Ord.ts new file mode 100644 index 00000000..0e36e362 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int4Ord.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). + */ +export type Int4Ord = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int4OrdOre.ts b/crates/eql-types/bindings/v3/Int4OrdOre.ts new file mode 100644 index 00000000..a77c4a95 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int4OrdOre.ts @@ -0,0 +1,27 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), + * scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. + */ +export type Int4OrdOre = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too — ORE over a + * full-domain `int4` is lossless, so no separate `hm` is carried. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int8.ts b/crates/eql-types/bindings/v3/Int8.ts new file mode 100644 index 00000000..c2ef0fe2 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int8.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int8` — storage only; every operator is blocked. + */ +export type Int8 = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int8Eq.ts b/crates/eql-types/bindings/v3/Int8Eq.ts new file mode 100644 index 00000000..435e66dd --- /dev/null +++ b/crates/eql-types/bindings/v3/Int8Eq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). + */ +export type Int8Eq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int8Ord.ts b/crates/eql-types/bindings/v3/Int8Ord.ts new file mode 100644 index 00000000..ae4b8182 --- /dev/null +++ b/crates/eql-types/bindings/v3/Int8Ord.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). + */ +export type Int8Ord = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int8OrdOre.ts b/crates/eql-types/bindings/v3/Int8OrdOre.ts new file mode 100644 index 00000000..ec33282b --- /dev/null +++ b/crates/eql-types/bindings/v3/Int8OrdOre.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. + */ +export type Int8OrdOre = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts b/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts new file mode 100644 index 00000000..5701b17f --- /dev/null +++ b/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the + * `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless + * over the scalar's domain, so it serves equality too. SQL-side constructor: + * `eql_v3.ore_block_u64_8_256`. + */ +export type OreBlockU64_8_256 = Array; diff --git a/crates/eql-types/bindings/v3/Text.ts b/crates/eql-types/bindings/v3/Text.ts new file mode 100644 index 00000000..fa65aeb2 --- /dev/null +++ b/crates/eql-types/bindings/v3/Text.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.text` — storage only; every operator is blocked. + */ +export type Text = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/TextEq.ts b/crates/eql-types/bindings/v3/TextEq.ts new file mode 100644 index 00000000..5a5f10f4 --- /dev/null +++ b/crates/eql-types/bindings/v3/TextEq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.text_eq` — HMAC equality (`=`, `<>`). + */ +export type TextEq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/TextMatch.ts b/crates/eql-types/bindings/v3/TextMatch.ts new file mode 100644 index 00000000..c6cacd05 --- /dev/null +++ b/crates/eql-types/bindings/v3/TextMatch.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BloomFilter } from "./BloomFilter"; +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.text_match` — Bloom-filter containment match. + */ +export type TextMatch = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Bloom-filter match term (signed smallint bit positions). + */ +bf: BloomFilter, }; diff --git a/crates/eql-types/bindings/v3/TextOrd.ts b/crates/eql-types/bindings/v3/TextOrd.ts new file mode 100644 index 00000000..fbf73e9c --- /dev/null +++ b/crates/eql-types/bindings/v3/TextOrd.ts @@ -0,0 +1,26 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.text_ord` — full lexicographic comparison + * (`=` `<>` `<` `<=` `>` `>=`). + */ +export type TextOrd = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/TextOrdOre.ts b/crates/eql-types/bindings/v3/TextOrdOre.ts new file mode 100644 index 00000000..218423f2 --- /dev/null +++ b/crates/eql-types/bindings/v3/TextOrdOre.ts @@ -0,0 +1,26 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; +import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; + +/** + * `eql_v3.text_ord_ore` — full lexicographic comparison, + * scheme-explicit name. + */ +export type TextOrdOre = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * Block-ORE order term. Serves equality too. + */ +ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Timestamptz.ts b/crates/eql-types/bindings/v3/Timestamptz.ts new file mode 100644 index 00000000..860055a1 --- /dev/null +++ b/crates/eql-types/bindings/v3/Timestamptz.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.timestamptz` — storage only; every operator is blocked. + */ +export type Timestamptz = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/TimestamptzEq.ts b/crates/eql-types/bindings/v3/TimestamptzEq.ts new file mode 100644 index 00000000..3db0d720 --- /dev/null +++ b/crates/eql-types/bindings/v3/TimestamptzEq.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Ciphertext } from "./Ciphertext"; +import type { Hmac256 } from "./Hmac256"; +import type { Identifier } from "../Identifier"; + +/** + * `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). + */ +export type TimestamptzEq = { +/** + * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + */ +v: number, +/** + * Table/column identifier. Required by the domain CHECK. + */ +i: Identifier, +/** + * mp_base85 source ciphertext. Required by the domain CHECK. + */ +c: Ciphertext, +/** + * HMAC-SHA-256 equality term. + */ +hm: Hmac256, }; diff --git a/crates/eql-types/schema/Int4Tagged.json b/crates/eql-types/schema/Int4Tagged.json deleted file mode 100644 index 86c54cd7..00000000 --- a/crates/eql-types/schema/Int4Tagged.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Int4Tagged", - "description": "**Proposed.** Self-describing int4 payload — `x` is the capability tag.\n\nGenerates a clean TypeScript discriminated union (`switch (p.x)` with exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`.", - "oneOf": [ - { - "description": "`x: \"int4\"` — storage only.", - "type": "object", - "required": [ - "c", - "i", - "v", - "x" - ], - "properties": { - "c": { - "type": "string" - }, - "i": { - "$ref": "#/definitions/Identifier" - }, - "v": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "x": { - "type": "string", - "enum": [ - "int4" - ] - } - } - }, - { - "description": "`x: \"int4_eq\"` — HMAC equality.", - "type": "object", - "required": [ - "c", - "hm", - "i", - "v", - "x" - ], - "properties": { - "c": { - "type": "string" - }, - "hm": { - "type": "string" - }, - "i": { - "$ref": "#/definitions/Identifier" - }, - "v": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "x": { - "type": "string", - "enum": [ - "int4_eq" - ] - } - } - }, - { - "description": "`x: \"int4_ord\"` — equality + ORE-block range.", - "type": "object", - "required": [ - "c", - "i", - "ob", - "v", - "x" - ], - "properties": { - "c": { - "type": "string" - }, - "i": { - "$ref": "#/definitions/Identifier" - }, - "ob": { - "type": "array", - "items": { - "type": "string" - } - }, - "v": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "x": { - "type": "string", - "enum": [ - "int4_ord" - ] - } - } - } - ], - "definitions": { - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "type": "object", - "required": [ - "c", - "t" - ], - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/crates/eql-types/schema/Int4Eq.json b/crates/eql-types/schema/v3/date.json similarity index 50% rename from crates/eql-types/schema/Int4Eq.json rename to crates/eql-types/schema/v3/date.json index 9bde81c0..fcbe8e71 100644 --- a/crates/eql-types/schema/Int4Eq.json +++ b/crates/eql-types/schema/v3/date.json @@ -1,46 +1,13 @@ { + "$id": "https://schemas.cipherstash.com/eql/v3/date.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Int4Eq", - "description": "`eql_v2_int4_eq` — HMAC equality (`=`, `<>`).\n\n`hm` is a required field. There is no `Option`: the type *is* the equality capability. A payload without `hm` cannot be deserialized into this type — the Rust analogue of the SQL domain's CHECK constraint.", - "type": "object", - "required": [ - "c", - "hm", - "i", - "v" - ], - "properties": { - "c": { - "description": "mp_base85 ciphertext. Required.", - "type": "string" - }, - "hm": { - "description": "HMAC-SHA256 equality term. Required.", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", "type": "string" }, - "i": { - "description": "Table/column identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "v": { - "description": "Schema version.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - }, - "definitions": { "Identifier": { "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "type": "object", - "required": [ - "c", - "t" - ], "properties": { "c": { "description": "Column name.", @@ -50,7 +17,44 @@ "description": "Table name.", "type": "string" } - } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.date` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" } - } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Date", + "type": "object" } \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_eq.json b/crates/eql-types/schema/v3/date_eq.json new file mode 100644 index 00000000..8aae8ef3 --- /dev/null +++ b/crates/eql-types/schema/v3/date_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.date_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "DateEq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_ord.json b/crates/eql-types/schema/v3/date_ord.json new file mode 100644 index 00000000..d3253e3e --- /dev/null +++ b/crates/eql-types/schema/v3/date_ord.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "DateOrd", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_ord_ore.json b/crates/eql-types/schema/v3/date_ord_ore.json new file mode 100644 index 00000000..d2471cef --- /dev/null +++ b/crates/eql-types/schema/v3/date_ord_ore.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.date_ord_ore` — full comparison, scheme-explicit name.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "DateOrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2.json b/crates/eql-types/schema/v3/int2.json new file mode 100644 index 00000000..36c48d29 --- /dev/null +++ b/crates/eql-types/schema/v3/int2.json @@ -0,0 +1,60 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int2` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Int2", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_eq.json b/crates/eql-types/schema/v3/int2_eq.json new file mode 100644 index 00000000..84e122b8 --- /dev/null +++ b/crates/eql-types/schema/v3/int2_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int2_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "Int2Eq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_ord.json b/crates/eql-types/schema/v3/int2_ord.json new file mode 100644 index 00000000..9eeee77f --- /dev/null +++ b/crates/eql-types/schema/v3/int2_ord.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int2Ord", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_ord_ore.json b/crates/eql-types/schema/v3/int2_ord_ore.json new file mode 100644 index 00000000..632d62a1 --- /dev/null +++ b/crates/eql-types/schema/v3/int2_ord_ore.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int2_ord_ore` — full comparison, scheme-explicit name.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int2OrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4.json b/crates/eql-types/schema/v3/int4.json new file mode 100644 index 00000000..25d61648 --- /dev/null +++ b/crates/eql-types/schema/v3/int4.json @@ -0,0 +1,60 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int4` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Int4", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_eq.json b/crates/eql-types/schema/v3/int4_eq.json new file mode 100644 index 00000000..0f9204ba --- /dev/null +++ b/crates/eql-types/schema/v3/int4_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int4_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "Int4Eq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_ord.json b/crates/eql-types/schema/v3/int4_ord.json new file mode 100644 index 00000000..a45f5298 --- /dev/null +++ b/crates/eql-types/schema/v3/int4_ord.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int4Ord", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_ord_ore.json b/crates/eql-types/schema/v3/int4_ord_ore.json new file mode 100644 index 00000000..191843c4 --- /dev/null +++ b/crates/eql-types/schema/v3/int4_ord_ore.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too — ORE over a full-domain `int4` is lossless, so no separate `hm` is carried." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int4OrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8.json b/crates/eql-types/schema/v3/int8.json new file mode 100644 index 00000000..3892f8ba --- /dev/null +++ b/crates/eql-types/schema/v3/int8.json @@ -0,0 +1,60 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int8` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Int8", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_eq.json b/crates/eql-types/schema/v3/int8_eq.json new file mode 100644 index 00000000..8f970b64 --- /dev/null +++ b/crates/eql-types/schema/v3/int8_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.int8_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "Int8Eq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_ord.json b/crates/eql-types/schema/v3/int8_ord.json new file mode 100644 index 00000000..a7ca295d --- /dev/null +++ b/crates/eql-types/schema/v3/int8_ord.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int8Ord", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_ord_ore.json b/crates/eql-types/schema/v3/int8_ord_ore.json new file mode 100644 index 00000000..a86d1b8c --- /dev/null +++ b/crates/eql-types/schema/v3/int8_ord_ore.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.int8_ord_ore` — full comparison, scheme-explicit name.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "Int8OrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text.json b/crates/eql-types/schema/v3/text.json new file mode 100644 index 00000000..1d2605c0 --- /dev/null +++ b/crates/eql-types/schema/v3/text.json @@ -0,0 +1,60 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.text` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Text", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_eq.json b/crates/eql-types/schema/v3/text_eq.json new file mode 100644 index 00000000..abbf3a6f --- /dev/null +++ b/crates/eql-types/schema/v3/text_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.text_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "TextEq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_match.json b/crates/eql-types/schema/v3/text_match.json new file mode 100644 index 00000000..4cf7c4f9 --- /dev/null +++ b/crates/eql-types/schema/v3/text_match.json @@ -0,0 +1,77 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_match.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BloomFilter": { + "description": "Bloom-filter match term — the `bf` wire key. Backs the `_match` domains (`~~` containment via `@>`/`<@`).\n\n**Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, and filters sized above 32768 emit upper-half bit positions as negative signed values (same rationale as `v2_3::EncryptedPayload::bf`).", + "items": { + "format": "int16", + "type": "integer" + }, + "type": "array" + }, + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.text_match` — Bloom-filter containment match.", + "properties": { + "bf": { + "allOf": [ + { + "$ref": "#/definitions/BloomFilter" + } + ], + "description": "Bloom-filter match term (signed smallint bit positions)." + }, + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "bf", + "c", + "i", + "v" + ], + "title": "TextMatch", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_ord.json b/crates/eql-types/schema/v3/text_ord.json new file mode 100644 index 00000000..1758e763 --- /dev/null +++ b/crates/eql-types/schema/v3/text_ord.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.text_ord` — full lexicographic comparison (`=` `<>` `<` `<=` `>` `>=`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "TextOrd", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_ord_ore.json b/crates/eql-types/schema/v3/text_ord_ore.json new file mode 100644 index 00000000..3b467f14 --- /dev/null +++ b/crates/eql-types/schema/v3/text_ord_ore.json @@ -0,0 +1,76 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "OreBlockU64_8_256": { + "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "description": "`eql_v3.text_ord_ore` — full lexicographic comparison, scheme-explicit name.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term. Serves equality too." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "ob", + "v" + ], + "title": "TextOrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/timestamptz.json b/crates/eql-types/schema/v3/timestamptz.json new file mode 100644 index 00000000..e1cd070a --- /dev/null +++ b/crates/eql-types/schema/v3/timestamptz.json @@ -0,0 +1,60 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.timestamptz` — storage only; every operator is blocked.", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "i", + "v" + ], + "title": "Timestamptz", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/timestamptz_eq.json b/crates/eql-types/schema/v3/timestamptz_eq.json new file mode 100644 index 00000000..6d1759f8 --- /dev/null +++ b/crates/eql-types/schema/v3/timestamptz_eq.json @@ -0,0 +1,73 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Ciphertext": { + "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", + "type": "string" + }, + "Hmac256": { + "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", + "type": "string" + }, + "Identifier": { + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + } + }, + "description": "`eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`).", + "properties": { + "c": { + "allOf": [ + { + "$ref": "#/definitions/Ciphertext" + } + ], + "description": "mp_base85 source ciphertext. Required by the domain CHECK." + }, + "hm": { + "allOf": [ + { + "$ref": "#/definitions/Hmac256" + } + ], + "description": "HMAC-SHA-256 equality term." + }, + "i": { + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ], + "description": "Table/column identifier. Required by the domain CHECK." + }, + "v": { + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "c", + "hm", + "i", + "v" + ], + "title": "TimestamptzEq", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/src/int4.rs b/crates/eql-types/src/int4.rs deleted file mode 100644 index ab1f0fd5..00000000 --- a/crates/eql-types/src/int4.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! # `eql_v2_int4` variant family — NEW (targets EQL 2.4) -//! -//! Where [`crate::v2_3`] is frozen, this module has design freedom. It mirrors -//! the SQL domain family from `encrypt-query-language#225`. -//! -//! ## The idea: capability-encoded types -//! -//! `eql_v2_encrypted` is one mega-type with every index term optional — so a -//! consumer must runtime-check "do I have an `hm`?". The int4 family instead -//! splits storage into one type per **capability**: -//! -//! | Rust type | SQL domain | Required keys | Operators | -//! |-------------|---------------------|---------------|----------------------------| -//! | [`Int4`] | `eql_v2_int4` | `c` | none (storage only) | -//! | [`Int4Eq`] | `eql_v2_int4_eq` | `c`, `hm` | `=` `<>` | -//! | [`Int4Ord`] | `eql_v2_int4_ord` | `c`, `ob` | `=` `<>` `<` `<=` `>` `>=` | -//! -//! The capability is the **type identity**. There are no optional index-term -//! fields: hold an [`Int4Eq`] and `hm` is present — guaranteed by the Rust -//! type, and (on the SQL side) by the domain's `CHECK` constraint. The runtime -//! guard the `protect-dynamodb` bug reached for becomes impossible to need. -//! -//! `Option` does not appear in this module. - -use crate::Identifier; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -/// `eql_v2_int4` — storage only. Carries `c`; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct Int4 { - /// Schema version. - pub v: u16, - /// Table/column identifier. - pub i: Identifier, - /// mp_base85 ciphertext. Required by the domain's CHECK constraint. - pub c: String, -} - -/// `eql_v2_int4_eq` — HMAC equality (`=`, `<>`). -/// -/// `hm` is a required field. There is no `Option`: the type *is* the -/// equality capability. A payload without `hm` cannot be deserialized into -/// this type — the Rust analogue of the SQL domain's CHECK constraint. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct Int4Eq { - /// Schema version. - pub v: u16, - /// Table/column identifier. - pub i: Identifier, - /// mp_base85 ciphertext. Required. - pub c: String, - /// HMAC-SHA256 equality term. Required. - pub hm: String, -} - -/// `eql_v2_int4_ord` — equality + ORE-block range (`=` `<>` `<` `<=` `>` `>=`). -/// -/// Deliberately carries no `hm`: ORE over a full-domain `int4` is lossless, so -/// the order term `ob` doubles as an exact equality term. -/// (`eql_v2_int4_ord_ore` in #225 is the same shape under a scheme-explicit -/// name — structurally identical, so it is not a separate Rust type.) -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct Int4Ord { - /// Schema version. - pub v: u16, - /// Table/column identifier. - pub i: Identifier, - /// mp_base85 ciphertext. Required. - pub c: String, - /// Block ORE term. Required — serves both range and equality. - pub ob: Vec, -} - -// =========================================================================== -// PROPOSAL (beyond #225) — a self-describing wire discriminator -// =========================================================================== -// -// On the wire, an int4 payload is discriminated only by *which key is present* -// (`hm` vs `ob`). The SQL domain name carries the rest — but once the JSON -// leaves SQL (into protect-ffi, into TypeScript, into a log line) that -// information is gone and a consumer is back to sniffing keys: the same -// untagged failure mode that produced the original protect-dynamodb bug. -// -// While the int4 family is still pre-release, a one-field capability tag `x` -// makes every payload self-describing and gives Rust / TS / SQL a single -// literal discriminant. This is the tagged-union lesson applied to a type we -// are still free to change. - -/// **Proposed.** Self-describing int4 payload — `x` is the capability tag. -/// -/// Generates a clean TypeScript discriminated union (`switch (p.x)` with -/// exhaustiveness) and a JSON Schema `oneOf` with a per-branch `const`. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -#[serde(tag = "x")] -pub enum Int4Tagged { - /// `x: "int4"` — storage only. - #[serde(rename = "int4")] - Storage { - v: u16, - i: Identifier, - c: String, - }, - /// `x: "int4_eq"` — HMAC equality. - #[serde(rename = "int4_eq")] - Eq { - v: u16, - i: Identifier, - c: String, - hm: String, - }, - /// `x: "int4_ord"` — equality + ORE-block range. - #[serde(rename = "int4_ord")] - Ord { - v: u16, - i: Identifier, - c: String, - ob: Vec, - }, -} diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index 5382ebfb..5a5a2af1 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -11,9 +11,11 @@ //! - [`v2_3`] — **FROZEN.** The `eql_v2_encrypted` wire contract, in production //! use by customers. Mirrors `eql-payload-v2.3.schema.json`, imperfections //! included. Nothing here may change. -//! - [`int4`] — **NEW** (targets EQL 2.4). Design freedom. Demonstrates -//! *capability-encoded types* — the pattern that removes the runtime -//! index-term guessing `eql_v2_encrypted` forces onto every consumer. +//! - [`v3`] — the `eql_v3` schema's encrypted-domain types: one struct per +//! SQL domain (`eql_v3.int4_eq`, `eql_v3.text_match`, …), *capability-encoded* +//! — index terms are required fields, never `Option`. Mirrors +//! `eql-scalars::CATALOG` 1:1, enforced by `tests/catalog_parity.rs`. +//! The wire envelope version stays `v: 2` — see the [`v3`] module docs. //! //! ## Codegen rules (learned from the ts-rs spike) //! @@ -28,8 +30,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; -pub mod int4; pub mod v2_3; +pub mod v3; /// EQL wire-format version. Hard-coded to `2` for every v2.x payload. pub const EQL_SCHEMA_VERSION: u16 = 2; diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs new file mode 100644 index 00000000..be7dc068 --- /dev/null +++ b/crates/eql-types/src/v3/date.rs @@ -0,0 +1,35 @@ +//! The `date` encrypted-domain family — an ordered, non-integer scalar. +//! Same four-domain ordered shape as [`crate::v3::int4`] (ORE compares +//! ciphertext, so dates order like integers); see that module for the +//! capability table. + +use crate::v3::eql_v3_domain; +use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; + +eql_v3_domain!( + /// `eql_v3.date` — storage only; every operator is blocked. + Date, domain = "date"); + +eql_v3_domain!( +/// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). +DateEq, domain = "date_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); + +eql_v3_domain!( +/// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. +DateOrdOre, domain = "date_ord_ore", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); + +eql_v3_domain!( +/// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +DateOrd, domain = "date_ord", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs new file mode 100644 index 00000000..a573033c --- /dev/null +++ b/crates/eql-types/src/v3/int2.rs @@ -0,0 +1,33 @@ +//! The `int2` encrypted-domain family. Same four-domain ordered shape as +//! [`crate::v3::int4`] — see that module for the capability table. + +use crate::v3::eql_v3_domain; +use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; + +eql_v3_domain!( + /// `eql_v3.int2` — storage only; every operator is blocked. + Int2, domain = "int2"); + +eql_v3_domain!( +/// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). +Int2Eq, domain = "int2_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); + +eql_v3_domain!( +/// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. +Int2OrdOre, domain = "int2_ord_ore", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); + +eql_v3_domain!( +/// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +Int2Ord, domain = "int2_ord", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs new file mode 100644 index 00000000..7a2350a3 --- /dev/null +++ b/crates/eql-types/src/v3/int4.rs @@ -0,0 +1,41 @@ +//! The `int4` encrypted-domain family — the reference scalar. +//! +//! | Rust type | SQL domain | Required keys | Operators | +//! |----------------|------------------------|---------------|----------------------------| +//! | [`Int4`] | `eql_v3.int4` | `v` `i` `c` | none (storage only) | +//! | [`Int4Eq`] | `eql_v3.int4_eq` | `v` `i` `c` `hm` | `=` `<>` | +//! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | +//! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | + +use crate::v3::eql_v3_domain; +use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; + +eql_v3_domain!( + /// `eql_v3.int4` — storage only; every operator is blocked. + Int4, domain = "int4"); + +eql_v3_domain!( +/// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). +Int4Eq, domain = "int4_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); + +eql_v3_domain!( +/// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), +/// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. +Int4OrdOre, domain = "int4_ord_ore", +terms { + /// Block-ORE order term. Serves equality too — ORE over a + /// full-domain `int4` is lossless, so no separate `hm` is carried. + ob: OreBlockU64_8_256, +}); + +eql_v3_domain!( +/// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +Int4Ord, domain = "int4_ord", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs new file mode 100644 index 00000000..e550c8c1 --- /dev/null +++ b/crates/eql-types/src/v3/int8.rs @@ -0,0 +1,33 @@ +//! The `int8` encrypted-domain family. Same four-domain ordered shape as +//! [`crate::v3::int4`] — see that module for the capability table. + +use crate::v3::eql_v3_domain; +use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; + +eql_v3_domain!( + /// `eql_v3.int8` — storage only; every operator is blocked. + Int8, domain = "int8"); + +eql_v3_domain!( +/// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). +Int8Eq, domain = "int8_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); + +eql_v3_domain!( +/// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. +Int8OrdOre, domain = "int8_ord_ore", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); + +eql_v3_domain!( +/// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +Int8Ord, domain = "int8_ord", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs new file mode 100644 index 00000000..c8ffdd8b --- /dev/null +++ b/crates/eql-types/src/v3/mod.rs @@ -0,0 +1,80 @@ +//! # `eql_v3` domain payload types +//! +//! One Rust struct per **SQL domain** in the `eql_v3` schema — the +//! capability-encoded design from the [`crate::int4`] prototype, formalized: +//! the SQL surface is generated from `eql-scalars::CATALOG`, and these types +//! mirror it 1:1 (enforced by `tests/catalog_parity.rs`, which fails if the +//! catalog and this module ever disagree on domains or required wire keys). +//! +//! **Versioning.** "v3" is the SQL schema generation (`eql_v3.*` domains). +//! The JSON envelope version is still `v: 2` ([`crate::EQL_SCHEMA_VERSION`]) — +//! every generated domain CHECK asserts `VALUE->>'v' = '2'`, and the wire +//! field names are unchanged from v2 (`hm`/`ob`/`bf`; the purpose-named +//! rename in the payload-scheme-discipline RFC is deferred). +//! +//! ## Shape of every payload +//! +//! Envelope (required by every domain CHECK): `v`, `i`, `c`. Then the +//! domain's required term keys — `hm` for `_eq`, `ob` for `_ord`/`_ord_ore`, +//! `bf` for `_match`, none for storage-only. `Option` does not appear in +//! this module: the capability **is** the type identity. Hold a +//! [`int4::Int4Eq`] and `hm` is present, guaranteed by the Rust type and +//! (SQL-side) by the domain CHECK. +//! +//! ## Why there is no discriminated enum +//! +//! Cross-token: impossible — an `int4_eq` and an `int8_eq` payload are +//! byte-identical on the wire (`v`/`i`/`c`/`hm`); nothing discriminates them. +//! Per-token: deliberately omitted — an untagged enum over a token's domains +//! would discriminate by key-sniffing, the exact `v2_3::SteVecTerm` failure +//! mode this tier exists to retire, and `_ord` vs `_ord_ore` are identical +//! shapes that no sniffing can separate. Consumers read from a typed column +//! and already know the domain. + +pub mod date; +pub mod int2; +pub mod int4; +pub mod int8; +pub mod registry; +pub mod terms; +pub mod text; +pub mod timestamptz; + +/// The PostgreSQL schema every domain in this module inhabits. +pub const SQL_SCHEMA: &str = "eql_v3"; + +/// Defines one `eql_v3` domain payload type: the required envelope +/// (`v`, `i`, `c` — mirrors `ENVELOPE_KEYS` in `eql-codegen/src/consts.rs`) +/// plus the domain's required term fields. No `Option`, ever — a missing +/// term key is a deserialization error, the Rust analogue of the SQL +/// domain's CHECK constraint. +macro_rules! eql_v3_domain { + ( + $(#[$meta:meta])* + $name:ident, domain = $domain:literal + $(, terms { $( $(#[$tmeta:meta])* $tkey:ident : $tty:ty ),+ $(,)? })? + ) => { + $(#[$meta])* + #[derive(Clone, Debug, PartialEq, ::serde::Serialize, ::serde::Deserialize, + ::ts_rs::TS, ::schemars::JsonSchema)] + #[ts(export, export_to = "v3/")] + pub struct $name { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: $crate::Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: $crate::v3::terms::Ciphertext, + $($( + $(#[$tmeta])* + pub $tkey: $tty, + )+)? + } + + impl $name { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = concat!("eql_v3.", $domain); + } + }; +} +pub(crate) use eql_v3_domain; diff --git a/crates/eql-types/src/v3/registry.rs b/crates/eql-types/src/v3/registry.rs new file mode 100644 index 00000000..928440a3 --- /dev/null +++ b/crates/eql-types/src/v3/registry.rs @@ -0,0 +1,72 @@ +//! Runtime registry of every v3 domain type — the one hand-maintained +//! mapping from SQL domain name to Rust type. +//! +//! Three consumers: `tests/catalog_parity.rs` (asserts this list exactly +//! covers `eql-scalars::CATALOG`, so it cannot silently go stale), the +//! generic round-trip loop in `tests/v3_conformance.rs`, and the JSON Schema +//! exporter in `tests/export.rs`. Public so FFI consumers can enumerate the +//! protocol surface too. + +use schemars::{schema::RootSchema, schema_for, JsonSchema}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::v3::{date, int2, int4, int8, text, timestamptz}; + +/// One registered v3 domain type. +pub struct DomainType { + /// Unqualified SQL domain name (e.g. `"int4_eq"`) — matches + /// `eql-scalars` `ScalarSpec::domain_name`. + pub domain: &'static str, + /// The Rust type's full path (via `std::any::type_name`). + pub type_name: &'static str, + /// The type's JSON Schema. + pub schema: fn() -> RootSchema, + /// serde round-trip through the concrete type + /// (`Value` → `T` → `Value`). + pub roundtrip: fn(serde_json::Value) -> Result, +} + +fn entry(domain: &'static str) -> DomainType +where + T: DeserializeOwned + Serialize + JsonSchema, +{ + DomainType { + domain, + type_name: std::any::type_name::(), + schema: || schema_for!(T), + roundtrip: |value| { + let parsed: T = serde_json::from_value(value)?; + serde_json::to_value(&parsed) + }, + } +} + +/// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then +/// each token's domains in manifest order). +pub fn all() -> Vec { + vec![ + entry::("int4"), + entry::("int4_eq"), + entry::("int4_ord_ore"), + entry::("int4_ord"), + entry::("int2"), + entry::("int2_eq"), + entry::("int2_ord_ore"), + entry::("int2_ord"), + entry::("int8"), + entry::("int8_eq"), + entry::("int8_ord_ore"), + entry::("int8_ord"), + entry::("date"), + entry::("date_eq"), + entry::("date_ord_ore"), + entry::("date_ord"), + entry::("timestamptz"), + entry::("timestamptz_eq"), + entry::("text"), + entry::("text_eq"), + entry::("text_match"), + entry::("text_ord_ore"), + entry::("text_ord"), + ] +} diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs new file mode 100644 index 00000000..26067df5 --- /dev/null +++ b/crates/eql-types/src/v3/terms.rs @@ -0,0 +1,71 @@ +//! Reusable wire-field newtypes shared by every v3 domain payload. +//! +//! Each newtype serializes as its inner value (serde's newtype-struct +//! default), so the wire shape is unchanged — but the *name* survives +//! codegen: ts-rs exports a named TS alias (`export type Hmac256 = string`) +//! that every domain binding imports, and schemars registers a named +//! definition that every domain schema `$ref`s. A plain Rust `type` alias +//! would vanish in both outputs. +//! +//! Names follow the SEM constructor names in `eql-scalars` (`Term::ctor()`): +//! a future scheme change (e.g. a 12-block wide ORE term for timestamptz +//! ordering) is a new newtype, not a hunt through `Vec` fields. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +/// mp_base85 source ciphertext — the `c` envelope key. +/// +/// Required by every v3 domain CHECK; present on every payload. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Ciphertext(pub String); + +/// HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains +/// (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Hmac256(pub String); + +/// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the +/// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless +/// over the scalar's domain, so it serves equality too. SQL-side constructor: +/// `eql_v3.ore_block_u64_8_256`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct OreBlockU64_8_256(pub Vec); + +/// Bloom-filter match term — the `bf` wire key. Backs the `_match` domains +/// (`~~` containment via `@>`/`<@`). +/// +/// **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, +/// and filters sized above 32768 emit upper-half bit positions as negative +/// signed values (same rationale as `v2_3::EncryptedPayload::bf`). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct BloomFilter(pub Vec); + +impl From for Ciphertext { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From for Hmac256 { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From> for OreBlockU64_8_256 { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From> for BloomFilter { + fn from(value: Vec) -> Self { + Self(value) + } +} diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs new file mode 100644 index 00000000..6fb52c6a --- /dev/null +++ b/crates/eql-types/src/v3/text.rs @@ -0,0 +1,44 @@ +//! The `text` encrypted-domain family — the ordered shape of +//! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter +//! term (`@>`/`<@` containment for `LIKE`-style matching). + +use crate::v3::eql_v3_domain; +use crate::v3::terms::{BloomFilter, Hmac256, OreBlockU64_8_256}; + +eql_v3_domain!( + /// `eql_v3.text` — storage only; every operator is blocked. + Text, domain = "text"); + +eql_v3_domain!( +/// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). +TextEq, domain = "text_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); + +eql_v3_domain!( +/// `eql_v3.text_match` — Bloom-filter containment match. +TextMatch, domain = "text_match", +terms { + /// Bloom-filter match term (signed smallint bit positions). + bf: BloomFilter, +}); + +eql_v3_domain!( +/// `eql_v3.text_ord_ore` — full lexicographic comparison, +/// scheme-explicit name. +TextOrdOre, domain = "text_ord_ore", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); + +eql_v3_domain!( +/// `eql_v3.text_ord` — full lexicographic comparison +/// (`=` `<>` `<` `<=` `>` `>=`). +TextOrd, domain = "text_ord", +terms { + /// Block-ORE order term. Serves equality too. + ob: OreBlockU64_8_256, +}); diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs new file mode 100644 index 00000000..ba93835f --- /dev/null +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -0,0 +1,20 @@ +//! The `timestamptz` encrypted-domain family — **equality-only** (storage + +//! `_eq`). There is no ordered domain: cipherstash encrypts timestamps at +//! native 12-block ORE width, but EQL's only ORE comparator is hardcoded to +//! 8 blocks, so an ordered timestamptz domain would silently mis-order. +//! Ordering arrives with a future wide-ORE term (see `eql-scalars`). + +use crate::v3::eql_v3_domain; +use crate::v3::terms::Hmac256; + +eql_v3_domain!( + /// `eql_v3.timestamptz` — storage only; every operator is blocked. + Timestamptz, domain = "timestamptz"); + +eql_v3_domain!( +/// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). +TimestamptzEq, domain = "timestamptz_eq", +terms { + /// HMAC-SHA-256 equality term. + hm: Hmac256, +}); diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs new file mode 100644 index 00000000..64a977dd --- /dev/null +++ b/crates/eql-types/tests/catalog_parity.rs @@ -0,0 +1,63 @@ +//! The drift gate: the v3 registry must mirror `eql-scalars::CATALOG` — the +//! same catalog that generates the `eql_v3` SQL surface — exactly. Append a +//! scalar to the catalog without adding its types here and the first test +//! fails; let a term field become `Option` (or carry the wrong wire key) and +//! the second fails, because schemars `required` reflects the real serde +//! contract. + +use std::collections::BTreeSet; + +use eql_scalars::{Term, CATALOG}; +use eql_types::v3::registry; + +/// Mirrors `ENVELOPE_KEYS` in `eql-codegen/src/consts.rs` (`pub(crate)` +/// there, so restated here): the keys every generated domain CHECK requires +/// before its term keys. +const ENVELOPE_KEYS: &[&str] = &["v", "i", "c"]; + +#[test] +fn registry_exactly_covers_catalog() { + let expected: Vec = CATALOG + .iter() + .flat_map(|spec| spec.domains.iter().map(|d| spec.domain_name(d))) + .collect(); + let actual: Vec<&str> = registry::all().iter().map(|e| e.domain).collect(); + assert_eq!( + actual, expected, + "v3 registry must list every CATALOG domain, in catalog order" + ); +} + +#[test] +fn required_keys_match_catalog_terms() { + let entries = registry::all(); + for spec in CATALOG { + for domain in spec.domains { + let name = spec.domain_name(domain); + let entry = entries + .iter() + .find(|e| e.domain == name) + .unwrap_or_else(|| panic!("no registry entry for {name}")); + + let schema = (entry.schema)(); + let object = schema + .schema + .object + .as_ref() + .unwrap_or_else(|| panic!("{name}: schema is not an object")); + let required: BTreeSet<&str> = object.required.iter().map(String::as_str).collect(); + + let expected: BTreeSet<&str> = ENVELOPE_KEYS + .iter() + .copied() + .chain(Term::term_json_keys(domain.terms)) + .collect(); + + assert_eq!( + required, expected, + "{name} ({}): required wire keys must be envelope + catalog terms", + entry.type_name + ); + } + } +} diff --git a/crates/eql-types/tests/conformance.rs b/crates/eql-types/tests/conformance.rs index 9d13cd70..bc68086a 100644 --- a/crates/eql-types/tests/conformance.rs +++ b/crates/eql-types/tests/conformance.rs @@ -1,8 +1,9 @@ -//! Conformance fixtures — the real guarantee that Rust / TS / JSON Schema and -//! the wire format agree. Codegen guarantees *shape*; these round-trips -//! guarantee *behaviour*. +//! Conformance fixtures for the FROZEN v2.3 tier — the real guarantee that +//! Rust / TS / JSON Schema and the wire format agree. Codegen guarantees +//! *shape*; these round-trips guarantee *behaviour*. +//! +//! v3 conformance lives in `v3_conformance.rs`; schema export in `export.rs`. -use eql_types::int4::{Int4Eq, Int4Tagged}; use eql_types::v2_3::EqlEncrypted; use serde_json::json; @@ -18,32 +19,6 @@ fn v2_3_scalar_round_trips() { assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); } -#[test] -fn int4_eq_round_trips() { - let wire = json!({ - "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext", - "hm": "deadbeef" - }); - let parsed: Int4Eq = serde_json::from_value(wire.clone()).unwrap(); - assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); -} - -#[test] -fn int4_eq_rejects_missing_hmac() { - // The capability is type-enforced: an `int4_eq` payload with no `hm` is - // not representable. This is the bug class — a search term missing its - // index term — closed at the type boundary, before any consumer runs. - let no_hm = json!({ - "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext" - }); - let result: Result = serde_json::from_value(no_hm); - assert!(result.is_err(), "Int4Eq must reject a payload with no hm"); -} - #[test] fn legacy_payload_silently_accepts_missing_terms() { // Contrast: the frozen v2.3 scalar type accepts a payload carrying no @@ -122,34 +97,3 @@ fn v2_3_ste_vec_round_trips() { assert!(matches!(parsed, EqlEncrypted::Sv(_))); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); } - -#[test] -fn int4_tagged_proposal_round_trips_and_discriminates() { - let wire = json!({ - "x": "int4_eq", "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext", - "hm": "deadbeef" - }); - let parsed: Int4Tagged = serde_json::from_value(wire.clone()).unwrap(); - assert!(matches!(parsed, Int4Tagged::Eq { .. })); - assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); -} - -#[test] -fn dump_json_schemas() { - use schemars::schema_for; - std::fs::create_dir_all("schema").unwrap(); - let schemas = [ - ("EqlEncrypted", schema_for!(EqlEncrypted)), - ("Int4Eq", schema_for!(Int4Eq)), - ("Int4Tagged", schema_for!(Int4Tagged)), - ]; - for (name, schema) in schemas { - std::fs::write( - format!("schema/{name}.json"), - serde_json::to_string_pretty(&schema).unwrap(), - ) - .unwrap(); - } -} diff --git a/crates/eql-types/tests/export.rs b/crates/eql-types/tests/export.rs new file mode 100644 index 00000000..08f78ade --- /dev/null +++ b/crates/eql-types/tests/export.rs @@ -0,0 +1,40 @@ +//! JSON Schema export — runs during `cargo test` (alongside ts-rs's own +//! export tests, which write `bindings/`). Output is checked in; freshness is +//! enforced by `mise run types:check`. v3 schema files are named after the +//! SQL domain — the protocol identity — not the Rust type. + +use eql_types::v2_3::EqlEncrypted; +use eql_types::v3::registry; +use schemars::schema_for; + +#[test] +fn dump_v2_3_json_schemas() { + std::fs::create_dir_all("schema").unwrap(); + std::fs::write( + "schema/EqlEncrypted.json", + serde_json::to_string_pretty(&schema_for!(EqlEncrypted)).unwrap(), + ) + .unwrap(); +} + +#[test] +fn dump_v3_json_schemas() { + std::fs::create_dir_all("schema/v3").unwrap(); + for entry in registry::all() { + let mut schema = serde_json::to_value((entry.schema)()).unwrap(); + // schemars 0.8 emits no $id; inject the canonical one. + schema.as_object_mut().unwrap().insert( + "$id".into(), + format!( + "https://schemas.cipherstash.com/eql/v3/{}.json", + entry.domain + ) + .into(), + ); + std::fs::write( + format!("schema/v3/{}.json", entry.domain), + serde_json::to_string_pretty(&schema).unwrap(), + ) + .unwrap(); + } +} diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs new file mode 100644 index 00000000..51aa8cb5 --- /dev/null +++ b/crates/eql-types/tests/v3_conformance.rs @@ -0,0 +1,158 @@ +//! Conformance for the v3 tier: explicit per-domain tests for the reference +//! token (`int4`, plus the term shapes it doesn't carry), then a generic +//! sweep over the whole registry — every domain type round-trips its wire +//! shape and rejects a payload missing any required key. + +use eql_types::v3::int4::{Int4, Int4Eq, Int4Ord, Int4OrdOre}; +use eql_types::v3::registry; +use eql_types::v3::text::TextMatch; +use serde_json::{json, Value}; + +#[test] +fn int4_storage_round_trips() { + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext" + }); + let parsed: Int4 = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + assert_eq!(Int4::SQL_DOMAIN, "eql_v3.int4"); +} + +#[test] +fn int4_eq_round_trips() { + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: Int4Eq = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + assert_eq!(Int4Eq::SQL_DOMAIN, "eql_v3.int4_eq"); +} + +#[test] +fn int4_ord_round_trips() { + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "ob": ["ore_block_0", "ore_block_1"] + }); + let parsed: Int4Ord = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + // `_ord_ore` is the same shape under the scheme-explicit domain name. + let parsed: Int4OrdOre = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + assert_eq!(Int4OrdOre::SQL_DOMAIN, "eql_v3.int4_ord_ore"); +} + +#[test] +fn int4_eq_rejects_missing_hmac() { + // The capability is type-enforced: an `int4_eq` payload with no `hm` is + // not representable. This is the bug class — a search term missing its + // index term — closed at the type boundary, before any consumer runs. + let no_hm = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext" + }); + let result: Result = serde_json::from_value(no_hm); + assert!(result.is_err(), "Int4Eq must reject a payload with no hm"); +} + +#[test] +fn int4_ord_rejects_missing_ore_term() { + let no_ob = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let result: Result = serde_json::from_value(no_ob); + assert!(result.is_err(), "Int4Ord must reject a payload with no ob"); +} + +#[test] +fn text_match_round_trips_signed_bloom_filter() { + // `bf` is signed i16 (smallint[]): filters sized above 32768 emit + // upper-half bit positions as negative values. + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "email" }, + "c": "mp_base85_ciphertext", + "bf": [-1, -32768, 32767, 0] + }); + let parsed: TextMatch = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + + let no_bf = json!({ + "v": 2, + "i": { "t": "users", "c": "email" }, + "c": "mp_base85_ciphertext" + }); + let result: Result = serde_json::from_value(no_bf); + assert!( + result.is_err(), + "TextMatch must reject a payload with no bf" + ); +} + +/// A synthetic wire value for a required key, by key name. +fn synthesize(key: &str) -> Value { + match key { + "v" => json!(2), + "i" => json!({ "t": "users", "c": "field" }), + "c" => json!("mp_base85_ciphertext"), + "hm" => json!("deadbeef"), + "ob" => json!(["ore_block_0", "ore_block_1"]), + "bf" => json!([-1, 0, 32767]), + other => panic!("no synthetic value for unexpected required key {other:?}"), + } +} + +/// The registry sweep: every domain type round-trips a payload synthesized +/// from its schema's required keys, and rejects the payload with any one +/// required key removed. (That the required keys are the *right* ones is +/// `catalog_parity.rs`'s job.) +#[test] +fn every_registered_domain_round_trips_and_rejects_missing_keys() { + for entry in registry::all() { + let schema = (entry.schema)(); + let required: Vec = schema + .schema + .object + .as_ref() + .expect("object schema") + .required + .iter() + .cloned() + .collect(); + assert!(!required.is_empty(), "{}: no required keys", entry.domain); + + let full: Value = required + .iter() + .map(|k| (k.clone(), synthesize(k))) + .collect::>() + .into(); + let round_tripped = (entry.roundtrip)(full.clone()) + .unwrap_or_else(|e| panic!("{}: round-trip failed: {e}", entry.domain)); + assert_eq!( + round_tripped, full, + "{}: round-trip not identity", + entry.domain + ); + + for key in &required { + let mut partial = full.clone(); + partial.as_object_mut().unwrap().remove(key); + assert!( + (entry.roundtrip)(partial).is_err(), + "{}: must reject payload missing required key {key:?}", + entry.domain + ); + } + } +} diff --git a/mise.toml b/mise.toml index 7e34b78e..b1a5f066 100644 --- a/mise.toml +++ b/mise.toml @@ -119,11 +119,12 @@ description = "Compile, lint and test the std-only Rust workspace crates (no dat dir = "{{config_root}}" run = """ #!/usr/bin/env bash -# eql-scalars / eql-codegen / eql-tests-macros are the lean workspace members. -# Scope explicitly to them (NOT --workspace): a workspace-wide test would drag -# in tests/sqlx, whose suite needs Postgres + CS_* secrets and is already -# covered by the `test` job. eql-tests-macros only pulls syn/quote/proc-macro2, -# so it stays in the lean set. clippy is likewise scoped — a workspace clippy +# eql-scalars / eql-codegen / eql-tests-macros / eql-types are the lean +# workspace members. Scope explicitly to them (NOT --workspace): a +# workspace-wide test would drag in tests/sqlx, whose suite needs Postgres + +# CS_* secrets and is already covered by the `test` job. eql-tests-macros only +# pulls syn/quote/proc-macro2 and eql-types only serde/ts-rs/schemars, so they +# stay in the lean set. clippy is likewise scoped — a workspace clippy # recompiles the heavy sqlx/tokio/cipherstash-client tree for no added coverage # of these crates. # bash is pinned via the `#!/usr/bin/env bash` shebang above (mise honors a @@ -131,8 +132,43 @@ run = """ # /bin/sh (dash on the CI images). set -euo pipefail cargo fmt --check -cargo clippy -p eql-scalars -p eql-codegen -p eql-tests-macros --all-targets -- -D warnings -cargo test -p eql-scalars -p eql-codegen -p eql-tests-macros +cargo clippy -p eql-scalars -p eql-codegen -p eql-tests-macros -p eql-types --all-targets -- -D warnings +cargo test -p eql-scalars -p eql-codegen -p eql-tests-macros -p eql-types +""" + +[tasks."types:generate"] +description = "Regenerate eql-types TypeScript bindings and JSON Schemas from the Rust types (no database required)" +dir = "{{config_root}}" +run = """ +#!/usr/bin/env bash +# Clean-then-regenerate: ts-rs and tests/export.rs only ever ADD files, so a +# renamed or removed type would otherwise leave an orphaned binding/schema +# behind. The rm lives here (not in the tests — they run in parallel). +# v2.3 outputs at the top level of bindings/ and schema/ are frozen alongside +# their types and are regenerated in place, not cleaned. +set -euo pipefail +rm -rf crates/eql-types/bindings/v3 crates/eql-types/schema/v3 +cargo test -p eql-types +""" + +[tasks."types:check"] +description = "Verify the checked-in eql-types bindings/ and schema/ are fresh (regenerate + git diff)" +dir = "{{config_root}}" +depends = ["types:generate"] +run = """ +#!/usr/bin/env bash +set -euo pipefail +git diff --exit-code -- crates/eql-types/bindings crates/eql-types/schema || { + echo "eql-types bindings/ or schema/ are stale — run 'mise run types:generate' and commit the result" >&2 + exit 1 +} +# git diff is blind to brand-new files; untracked output is stale too. +untracked=$(git ls-files --others --exclude-standard -- crates/eql-types/bindings crates/eql-types/schema) +if [ -n "$untracked" ]; then + echo "eql-types has uncommitted generated files:" >&2 + echo "$untracked" >&2 + exit 1 +fi """ [tasks."test:matrix:inventory"] From 57c0b54b55de1605ba31ed2a62c341e52ffafbd7 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 10 Jun 2026 20:43:11 +1000 Subject: [PATCH 04/12] refactor(eql-types): unroll eql_v3_domain! macro into explicit structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macro hid exactly what a protocol crate exists to show — the struct definitions. Its only real guarantee (envelope uniformity) is already covered by the catalog parity tests, rustfmt could not format the invocations, and 'pub struct Int4Eq' was un-greppable. Each domain is now a plain hand-written struct with the same fields, derives, doc comments, and SQL_DOMAIN const the macro emitted; bindings/ and schema/ regenerate byte-identical (verified: empty git diff after types:generate). --- crates/eql-types/src/v3/date.rs | 89 +++++++++++++++----- crates/eql-types/src/v3/int2.rs | 89 +++++++++++++++----- crates/eql-types/src/v3/int4.rs | 89 +++++++++++++++----- crates/eql-types/src/v3/int8.rs | 89 +++++++++++++++----- crates/eql-types/src/v3/mod.rs | 51 ++---------- crates/eql-types/src/v3/text.rs | 110 +++++++++++++++++++------ crates/eql-types/src/v3/timestamptz.rs | 47 ++++++++--- 7 files changed, 407 insertions(+), 157 deletions(-) diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index be7dc068..be820930 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -3,33 +3,82 @@ //! ciphertext, so dates order like integers); see that module for the //! capability table. -use crate::v3::eql_v3_domain; -use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.date` — storage only; every operator is blocked. - Date, domain = "date"); +/// `eql_v3.date` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Date { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Date { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.date"; +} -eql_v3_domain!( /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). -DateEq, domain = "date_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct DateEq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl DateEq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.date_eq"; +} -eql_v3_domain!( /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. -DateOrdOre, domain = "date_ord_ore", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct DateOrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl DateOrdOre { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.date_ord_ore"; +} -eql_v3_domain!( /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -DateOrd, domain = "date_ord", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct DateOrd { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl DateOrd { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.date_ord"; +} diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index a573033c..a587d2f2 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -1,33 +1,82 @@ //! The `int2` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use crate::v3::eql_v3_domain; -use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.int2` — storage only; every operator is blocked. - Int2, domain = "int2"); +/// `eql_v3.int2` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int2 { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Int2 { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int2"; +} -eql_v3_domain!( /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). -Int2Eq, domain = "int2_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int2Eq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl Int2Eq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int2_eq"; +} -eql_v3_domain!( /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. -Int2OrdOre, domain = "int2_ord_ore", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int2OrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int2OrdOre { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int2_ord_ore"; +} -eql_v3_domain!( /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -Int2Ord, domain = "int2_ord", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int2Ord { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int2Ord { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int2_ord"; +} diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index 7a2350a3..e1768156 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -7,35 +7,84 @@ //! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | -use crate::v3::eql_v3_domain; -use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.int4` — storage only; every operator is blocked. - Int4, domain = "int4"); +/// `eql_v3.int4` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int4 { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Int4 { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int4"; +} -eql_v3_domain!( /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). -Int4Eq, domain = "int4_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int4Eq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl Int4Eq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int4_eq"; +} -eql_v3_domain!( /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), /// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. -Int4OrdOre, domain = "int4_ord_ore", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int4OrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too — ORE over a /// full-domain `int4` is lossless, so no separate `hm` is carried. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int4OrdOre { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int4_ord_ore"; +} -eql_v3_domain!( /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -Int4Ord, domain = "int4_ord", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int4Ord { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int4Ord { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int4_ord"; +} diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index e550c8c1..7b906a6a 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -1,33 +1,82 @@ //! The `int8` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use crate::v3::eql_v3_domain; -use crate::v3::terms::{Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.int8` — storage only; every operator is blocked. - Int8, domain = "int8"); +/// `eql_v3.int8` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int8 { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Int8 { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int8"; +} -eql_v3_domain!( /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). -Int8Eq, domain = "int8_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int8Eq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl Int8Eq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int8_eq"; +} -eql_v3_domain!( /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. -Int8OrdOre, domain = "int8_ord_ore", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int8OrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int8OrdOre { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int8_ord_ore"; +} -eql_v3_domain!( /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -Int8Ord, domain = "int8_ord", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Int8Ord { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl Int8Ord { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.int8_ord"; +} diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index c8ffdd8b..2d0f7847 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -1,7 +1,8 @@ //! # `eql_v3` domain payload types //! //! One Rust struct per **SQL domain** in the `eql_v3` schema — the -//! capability-encoded design from the [`crate::int4`] prototype, formalized: +//! capability-encoded design from the original `eql_v2_int4` prototype +//! (PR #236's first cut), formalized: //! the SQL surface is generated from `eql-scalars::CATALOG`, and these types //! mirror it 1:1 (enforced by `tests/catalog_parity.rs`, which fails if the //! catalog and this module ever disagree on domains or required wire keys). @@ -14,12 +15,14 @@ //! //! ## Shape of every payload //! -//! Envelope (required by every domain CHECK): `v`, `i`, `c`. Then the -//! domain's required term keys — `hm` for `_eq`, `ob` for `_ord`/`_ord_ore`, -//! `bf` for `_match`, none for storage-only. `Option` does not appear in -//! this module: the capability **is** the type identity. Hold a +//! Envelope (required by every domain CHECK, mirroring `ENVELOPE_KEYS` in +//! `eql-codegen/src/consts.rs`): `v`, `i`, `c`. Then the domain's required +//! term keys — `hm` for `_eq`, `ob` for `_ord`/`_ord_ore`, `bf` for +//! `_match`, none for storage-only. `Option` does not appear in this +//! module: the capability **is** the type identity. Hold a //! [`int4::Int4Eq`] and `hm` is present, guaranteed by the Rust type and -//! (SQL-side) by the domain CHECK. +//! (SQL-side) by the domain CHECK. A missing term key is a deserialization +//! error — the Rust analogue of the CHECK constraint. //! //! ## Why there is no discriminated enum //! @@ -42,39 +45,3 @@ pub mod timestamptz; /// The PostgreSQL schema every domain in this module inhabits. pub const SQL_SCHEMA: &str = "eql_v3"; - -/// Defines one `eql_v3` domain payload type: the required envelope -/// (`v`, `i`, `c` — mirrors `ENVELOPE_KEYS` in `eql-codegen/src/consts.rs`) -/// plus the domain's required term fields. No `Option`, ever — a missing -/// term key is a deserialization error, the Rust analogue of the SQL -/// domain's CHECK constraint. -macro_rules! eql_v3_domain { - ( - $(#[$meta:meta])* - $name:ident, domain = $domain:literal - $(, terms { $( $(#[$tmeta:meta])* $tkey:ident : $tty:ty ),+ $(,)? })? - ) => { - $(#[$meta])* - #[derive(Clone, Debug, PartialEq, ::serde::Serialize, ::serde::Deserialize, - ::ts_rs::TS, ::schemars::JsonSchema)] - #[ts(export, export_to = "v3/")] - pub struct $name { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, - /// Table/column identifier. Required by the domain CHECK. - pub i: $crate::Identifier, - /// mp_base85 source ciphertext. Required by the domain CHECK. - pub c: $crate::v3::terms::Ciphertext, - $($( - $(#[$tmeta])* - pub $tkey: $tty, - )+)? - } - - impl $name { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = concat!("eql_v3.", $domain); - } - }; -} -pub(crate) use eql_v3_domain; diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index 6fb52c6a..a6065584 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -2,43 +2,103 @@ //! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter //! term (`@>`/`<@` containment for `LIKE`-style matching). -use crate::v3::eql_v3_domain; -use crate::v3::terms::{BloomFilter, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.text` — storage only; every operator is blocked. - Text, domain = "text"); +/// `eql_v3.text` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Text { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Text { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.text"; +} -eql_v3_domain!( /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). -TextEq, domain = "text_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct TextEq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl TextEq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.text_eq"; +} -eql_v3_domain!( /// `eql_v3.text_match` — Bloom-filter containment match. -TextMatch, domain = "text_match", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct TextMatch { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Bloom-filter match term (signed smallint bit positions). - bf: BloomFilter, -}); + pub bf: BloomFilter, +} + +impl TextMatch { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.text_match"; +} -eql_v3_domain!( /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. -TextOrdOre, domain = "text_ord_ore", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct TextOrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl TextOrdOre { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.text_ord_ore"; +} -eql_v3_domain!( /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). -TextOrd, domain = "text_ord", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct TextOrd { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - ob: OreBlockU64_8_256, -}); + pub ob: OreBlockU64_8_256, +} + +impl TextOrd { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.text_ord"; +} diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index ba93835f..89cae954 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -4,17 +4,44 @@ //! 8 blocks, so an ordered timestamptz domain would silently mis-order. //! Ordering arrives with a future wide-ORE term (see `eql-scalars`). -use crate::v3::eql_v3_domain; -use crate::v3::terms::Hmac256; +use crate::v3::terms::{Ciphertext, Hmac256}; +use crate::Identifier; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; -eql_v3_domain!( - /// `eql_v3.timestamptz` — storage only; every operator is blocked. - Timestamptz, domain = "timestamptz"); +/// `eql_v3.timestamptz` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct Timestamptz { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl Timestamptz { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.timestamptz"; +} -eql_v3_domain!( /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). -TimestamptzEq, domain = "timestamptz_eq", -terms { +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] +#[ts(export, export_to = "v3/")] +pub struct TimestamptzEq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). + pub v: u16, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, /// HMAC-SHA-256 equality term. - hm: Hmac256, -}); + pub hm: Hmac256, +} + +impl TimestamptzEq { + /// Fully-qualified SQL domain this payload inhabits. + pub const SQL_DOMAIN: &'static str = "eql_v3.timestamptz_eq"; +} From 21bd88730cdb3c30fe9d70b0338aee83d55274f2 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 10 Jun 2026 21:01:11 +1000 Subject: [PATCH 05/12] refactor(eql-types)!: drop the v2.3 tier and split codegen into stacked changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scope the base crate to the Rust contract only: - Remove the frozen eql_v2_encrypted v2.3 tier (v2_3.rs, its tests, bindings, and schema) — EQL 2.3 won't consume these types; the crate targets the eql_v3 surface. - Remove the ts-rs and schemars dependencies, derives, generated bindings/ and schema/ output, and the types:generate / types:check mise tasks + CI step. TypeScript bindings and JSON Schemas return as two stacked changes so each lands as a small, reviewable diff. - Rework the required-keys parity test to be behavioural instead of schemars-based: a payload carrying exactly the catalog's keys (per eql-scalars ENVELOPE_KEYS + Term::term_json_keys) must round-trip identically, and removing any one key must fail deserialization — the same drift guarantee, proven through serde alone. The crate now depends on serde + serde_json only. --- .github/workflows/test-eql.yml | 7 - Cargo.lock | 76 -------- Cargo.toml | 7 +- crates/eql-types/Cargo.toml | 4 +- crates/eql-types/README.md | 54 +++--- crates/eql-types/bindings/EncryptedPayload.ts | 41 ---- crates/eql-types/bindings/EqlEncrypted.ts | 17 -- crates/eql-types/bindings/Identifier.ts | 16 -- crates/eql-types/bindings/SteVecElement.ts | 18 -- crates/eql-types/bindings/SteVecPayload.ts | 20 -- crates/eql-types/bindings/SteVecTerm.ts | 9 - crates/eql-types/bindings/v3/BloomFilter.ts | 11 -- crates/eql-types/bindings/v3/Ciphertext.ts | 8 - crates/eql-types/bindings/v3/Date.ts | 20 -- crates/eql-types/bindings/v3/DateEq.ts | 25 --- crates/eql-types/bindings/v3/DateOrd.ts | 25 --- crates/eql-types/bindings/v3/DateOrdOre.ts | 25 --- crates/eql-types/bindings/v3/Hmac256.ts | 7 - crates/eql-types/bindings/v3/Int2.ts | 20 -- crates/eql-types/bindings/v3/Int2Eq.ts | 25 --- crates/eql-types/bindings/v3/Int2Ord.ts | 25 --- crates/eql-types/bindings/v3/Int2OrdOre.ts | 25 --- crates/eql-types/bindings/v3/Int4.ts | 20 -- crates/eql-types/bindings/v3/Int4Eq.ts | 25 --- crates/eql-types/bindings/v3/Int4Ord.ts | 25 --- crates/eql-types/bindings/v3/Int4OrdOre.ts | 27 --- crates/eql-types/bindings/v3/Int8.ts | 20 -- crates/eql-types/bindings/v3/Int8Eq.ts | 25 --- crates/eql-types/bindings/v3/Int8Ord.ts | 25 --- crates/eql-types/bindings/v3/Int8OrdOre.ts | 25 --- .../bindings/v3/OreBlockU64_8_256.ts | 9 - crates/eql-types/bindings/v3/Text.ts | 20 -- crates/eql-types/bindings/v3/TextEq.ts | 25 --- crates/eql-types/bindings/v3/TextMatch.ts | 25 --- crates/eql-types/bindings/v3/TextOrd.ts | 26 --- crates/eql-types/bindings/v3/TextOrdOre.ts | 26 --- crates/eql-types/bindings/v3/Timestamptz.ts | 20 -- crates/eql-types/bindings/v3/TimestamptzEq.ts | 25 --- crates/eql-types/schema/EqlEncrypted.json | 181 ------------------ crates/eql-types/schema/v3/date.json | 60 ------ crates/eql-types/schema/v3/date_eq.json | 73 ------- crates/eql-types/schema/v3/date_ord.json | 76 -------- crates/eql-types/schema/v3/date_ord_ore.json | 76 -------- crates/eql-types/schema/v3/int2.json | 60 ------ crates/eql-types/schema/v3/int2_eq.json | 73 ------- crates/eql-types/schema/v3/int2_ord.json | 76 -------- crates/eql-types/schema/v3/int2_ord_ore.json | 76 -------- crates/eql-types/schema/v3/int4.json | 60 ------ crates/eql-types/schema/v3/int4_eq.json | 73 ------- crates/eql-types/schema/v3/int4_ord.json | 76 -------- crates/eql-types/schema/v3/int4_ord_ore.json | 76 -------- crates/eql-types/schema/v3/int8.json | 60 ------ crates/eql-types/schema/v3/int8_eq.json | 73 ------- crates/eql-types/schema/v3/int8_ord.json | 76 -------- crates/eql-types/schema/v3/int8_ord_ore.json | 76 -------- crates/eql-types/schema/v3/text.json | 60 ------ crates/eql-types/schema/v3/text_eq.json | 73 ------- crates/eql-types/schema/v3/text_match.json | 77 -------- crates/eql-types/schema/v3/text_ord.json | 76 -------- crates/eql-types/schema/v3/text_ord_ore.json | 76 -------- crates/eql-types/schema/v3/timestamptz.json | 60 ------ .../eql-types/schema/v3/timestamptz_eq.json | 73 ------- crates/eql-types/src/lib.rs | 48 ++--- crates/eql-types/src/v2_3.rs | 149 -------------- crates/eql-types/src/v3/date.rs | 14 +- crates/eql-types/src/v3/int2.rs | 14 +- crates/eql-types/src/v3/int4.rs | 14 +- crates/eql-types/src/v3/int8.rs | 14 +- crates/eql-types/src/v3/registry.rs | 15 +- crates/eql-types/src/v3/terms.rs | 25 +-- crates/eql-types/src/v3/text.rs | 17 +- crates/eql-types/src/v3/timestamptz.rs | 8 +- crates/eql-types/tests/catalog_parity.rs | 63 ++++-- crates/eql-types/tests/conformance.rs | 99 ---------- crates/eql-types/tests/export.rs | 40 ---- crates/eql-types/tests/v3_conformance.rs | 68 +------ mise.toml | 37 +--- 77 files changed, 136 insertions(+), 3158 deletions(-) delete mode 100644 crates/eql-types/bindings/EncryptedPayload.ts delete mode 100644 crates/eql-types/bindings/EqlEncrypted.ts delete mode 100644 crates/eql-types/bindings/Identifier.ts delete mode 100644 crates/eql-types/bindings/SteVecElement.ts delete mode 100644 crates/eql-types/bindings/SteVecPayload.ts delete mode 100644 crates/eql-types/bindings/SteVecTerm.ts delete mode 100644 crates/eql-types/bindings/v3/BloomFilter.ts delete mode 100644 crates/eql-types/bindings/v3/Ciphertext.ts delete mode 100644 crates/eql-types/bindings/v3/Date.ts delete mode 100644 crates/eql-types/bindings/v3/DateEq.ts delete mode 100644 crates/eql-types/bindings/v3/DateOrd.ts delete mode 100644 crates/eql-types/bindings/v3/DateOrdOre.ts delete mode 100644 crates/eql-types/bindings/v3/Hmac256.ts delete mode 100644 crates/eql-types/bindings/v3/Int2.ts delete mode 100644 crates/eql-types/bindings/v3/Int2Eq.ts delete mode 100644 crates/eql-types/bindings/v3/Int2Ord.ts delete mode 100644 crates/eql-types/bindings/v3/Int2OrdOre.ts delete mode 100644 crates/eql-types/bindings/v3/Int4.ts delete mode 100644 crates/eql-types/bindings/v3/Int4Eq.ts delete mode 100644 crates/eql-types/bindings/v3/Int4Ord.ts delete mode 100644 crates/eql-types/bindings/v3/Int4OrdOre.ts delete mode 100644 crates/eql-types/bindings/v3/Int8.ts delete mode 100644 crates/eql-types/bindings/v3/Int8Eq.ts delete mode 100644 crates/eql-types/bindings/v3/Int8Ord.ts delete mode 100644 crates/eql-types/bindings/v3/Int8OrdOre.ts delete mode 100644 crates/eql-types/bindings/v3/OreBlockU64_8_256.ts delete mode 100644 crates/eql-types/bindings/v3/Text.ts delete mode 100644 crates/eql-types/bindings/v3/TextEq.ts delete mode 100644 crates/eql-types/bindings/v3/TextMatch.ts delete mode 100644 crates/eql-types/bindings/v3/TextOrd.ts delete mode 100644 crates/eql-types/bindings/v3/TextOrdOre.ts delete mode 100644 crates/eql-types/bindings/v3/Timestamptz.ts delete mode 100644 crates/eql-types/bindings/v3/TimestamptzEq.ts delete mode 100644 crates/eql-types/schema/EqlEncrypted.json delete mode 100644 crates/eql-types/schema/v3/date.json delete mode 100644 crates/eql-types/schema/v3/date_eq.json delete mode 100644 crates/eql-types/schema/v3/date_ord.json delete mode 100644 crates/eql-types/schema/v3/date_ord_ore.json delete mode 100644 crates/eql-types/schema/v3/int2.json delete mode 100644 crates/eql-types/schema/v3/int2_eq.json delete mode 100644 crates/eql-types/schema/v3/int2_ord.json delete mode 100644 crates/eql-types/schema/v3/int2_ord_ore.json delete mode 100644 crates/eql-types/schema/v3/int4.json delete mode 100644 crates/eql-types/schema/v3/int4_eq.json delete mode 100644 crates/eql-types/schema/v3/int4_ord.json delete mode 100644 crates/eql-types/schema/v3/int4_ord_ore.json delete mode 100644 crates/eql-types/schema/v3/int8.json delete mode 100644 crates/eql-types/schema/v3/int8_eq.json delete mode 100644 crates/eql-types/schema/v3/int8_ord.json delete mode 100644 crates/eql-types/schema/v3/int8_ord_ore.json delete mode 100644 crates/eql-types/schema/v3/text.json delete mode 100644 crates/eql-types/schema/v3/text_eq.json delete mode 100644 crates/eql-types/schema/v3/text_match.json delete mode 100644 crates/eql-types/schema/v3/text_ord.json delete mode 100644 crates/eql-types/schema/v3/text_ord_ore.json delete mode 100644 crates/eql-types/schema/v3/timestamptz.json delete mode 100644 crates/eql-types/schema/v3/timestamptz_eq.json delete mode 100644 crates/eql-types/src/v2_3.rs delete mode 100644 crates/eql-types/tests/conformance.rs delete mode 100644 crates/eql-types/tests/export.rs diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 4e0a0d63..776d701a 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -91,13 +91,6 @@ jobs: rustup component add --toolchain ${active_rust_toolchain} rustfmt clippy mise run test:crates - # Freshness gate for the eql-types codegen output: regenerate the - # TypeScript bindings and JSON Schemas and fail if the checked-in - # copies differ. Reuses the build artifacts from the step above. - - name: Verify eql-types bindings and schemas are fresh - run: | - mise run types:check - codegen: name: "Encrypted-domain codegen" runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 9d4a77df..760a55c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,12 +1125,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "either" version = "1.15.0" @@ -1189,10 +1183,8 @@ name = "eql-types" version = "0.1.0" dependencies = [ "eql-scalars", - "schemars", "serde", "serde_json", - "ts-rs", ] [[package]] @@ -3382,30 +3374,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.108", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3497,17 +3465,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "serde_json" version = "1.0.145" @@ -4034,15 +3991,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.4.4" @@ -4380,30 +4328,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ts-rs" -version = "10.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" -dependencies = [ - "lazy_static", - "serde_json", - "thiserror 2.0.18", - "ts-rs-macros", -] - -[[package]] -name = "ts-rs-macros" -version = "10.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", - "termcolor", -] - [[package]] name = "typenum" version = "1.20.0" diff --git a/Cargo.toml b/Cargo.toml index 0ce781ff..2baf1e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,10 @@ # crates/eql-codegen — the SQL generator binary (stub here; Plan 2 fills it in). # crates/eql-tests-macros — proc-macros expanding the single scalar-harness # list into the per-type SQLx-matrix wiring. -# crates/eql-types — canonical wire types for EQL payloads (Rust → ts-rs -# TypeScript bindings + schemars JSON Schema); parity- -# tested against the eql-scalars catalog. +# crates/eql-types — canonical Rust wire types for EQL payloads, parity- +# tested against the eql-scalars catalog. (TypeScript +# bindings and JSON Schemas are generated from these +# types in stacked changes.) # tests/sqlx — the existing `eql_tests` SQLx integration crate. # # resolver = "2" keeps the heavy test-crate feature set (sqlx/tokio/cipherstash- diff --git a/crates/eql-types/Cargo.toml b/crates/eql-types/Cargo.toml index d2021998..d37a417a 100644 --- a/crates/eql-types/Cargo.toml +++ b/crates/eql-types/Cargo.toml @@ -2,13 +2,11 @@ name = "eql-types" version = "0.1.0" edition = "2021" -description = "Canonical wire types for EQL payloads — single source of truth for Rust, TypeScript (ts-rs), and JSON Schema (schemars)." +description = "Canonical wire types for EQL payloads — the single Rust source of truth (TypeScript bindings and JSON Schemas are generated from these types in stacked changes)." [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -ts-rs = { version = "10", features = ["serde-json-impl"] } -schemars = "0.8" [dev-dependencies] # Parity oracle: tests/catalog_parity.rs asserts the v3 registry exactly diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index 20e705f7..07ce7331 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -1,11 +1,12 @@ # eql-types Canonical wire types for EQL payloads — **one Rust definition per payload -shape**, intended as the single source of truth for: +shape**, the single source of truth for every tool that produces or consumes +EQL payloads (`cipherstash-client`, `protect-ffi`, CipherStash Proxy). -- **Rust** — consumed directly by `cipherstash-client` / `protect-ffi` -- **TypeScript** — generated via [`ts-rs`] into [`bindings/`](bindings/) -- **JSON Schema** — generated via [`schemars`] into [`schema/`](schema/) +TypeScript bindings (via [`ts-rs`]) and JSON Schemas (via [`schemars`]) are +generated from these definitions in stacked changes; this crate is the +Rust contract only. ## Why @@ -13,24 +14,18 @@ Type information is lost at every hop of `EQL → cipherstash-client → protect-ffi → stack`. protect-ffi hand-writes its TypeScript types; they drift from the Rust they describe; stack widens them further. The result is bugs like the `protect-dynamodb` search-term check that validates a payload shape -EQL v2.3 never actually defined. A generated, single-source crate removes the +EQL never actually defined. A generated, single-source crate removes the hand-copying. -## Two tiers +## Capability-encoded types -| Module | Tier | Rule | -|--------|------|------| -| [`src/v2_3.rs`](src/v2_3.rs) | `eql_v2_encrypted` v2.3 wire contract | **FROZEN** — in production; mirrors `eql-payload-v2.3.schema.json`; must not change | -| [`src/v3/`](src/v3/) | `eql_v3` encrypted-domain families | One struct per SQL domain, parity-tested against `eql-scalars::CATALOG` | - -## Capability-encoded types (the v3 tier) - -`eql_v2_encrypted` is one type with every index term optional, so consumers -must guess at runtime which terms are present. The v3 tier instead has one -type per **SQL domain** — `Int4` / `Int4Eq` / `Int4Ord` / `Int4OrdOre`, and -likewise for `int2`, `int8`, `date`, `timestamptz` (eq-only), and `text` -(which adds `TextMatch`) — each carrying its index terms as **required** -fields. The capability is the type identity; `Option` never appears. +The [`src/v3/`](src/v3/) module has one type per **SQL domain** in the +`eql_v3` schema — `Int4` / `Int4Eq` / `Int4Ord` / `Int4OrdOre`, and likewise +for `int2`, `int8`, `date`, `timestamptz` (eq-only), and `text` (which adds +`TextMatch`) — each carrying its index terms as **required** fields. The +capability is the type identity; `Option` never appears. A payload missing +its term key fails to deserialize: the Rust analogue of the SQL domain's +CHECK constraint. Shared wire fields are reusable newtypes in [`src/v3/terms.rs`](src/v3/terms.rs): @@ -47,26 +42,23 @@ version is still `v: 2` — the generated domain CHECKs assert it, and the wire field names are unchanged from v2 (the purpose-named rename in `docs/plans/eql-payload-scheme-discipline-rfc.md` is deferred). -### Drift protection +## Drift protection `tests/catalog_parity.rs` asserts the [`v3::registry`](src/v3/registry.rs) -exactly covers `eql-scalars::CATALOG` (every domain, in order) and that each -type's required JSON keys equal the envelope keys plus the catalog's term -keys. Adding a scalar to the catalog without adding its types here fails the -build; so does accidentally making a term field `Option`. +exactly covers `eql-scalars::CATALOG` — the same catalog that generates the +`eql_v3` SQL surface — every domain, in order, and proves behaviourally that +each type's wire keys are exactly the envelope (`v`, `i`, `c`) plus the +catalog's term keys. Adding a scalar to the catalog without adding its types +here fails the build; so does accidentally making a term field `Option`. ## Develop ```sh -mise run types:generate # clean-regenerate bindings/ and schema/ -mise run types:check # regenerate + fail if checked-in outputs are stale +cargo test -p eql-types ``` -Both wrap `cargo test -p eql-types`, which runs the conformance round-trip -tests and regenerates `bindings/` (TypeScript, via ts-rs) and `schema/` -(JSON Schema, via `tests/export.rs`). Both directories are checked in so -reviewers can see the codegen output without running anything; CI runs -`types:check` to keep them fresh. +The crate is also part of the lean `mise run test:crates` set (fmt, clippy, +test — no database). ## Future direction: self-describing payloads diff --git a/crates/eql-types/bindings/EncryptedPayload.ts b/crates/eql-types/bindings/EncryptedPayload.ts deleted file mode 100644 index 838d2c8c..00000000 --- a/crates/eql-types/bindings/EncryptedPayload.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; - -/** - * Scalar storage payload (`k = "ct"`). - * - * FROZEN imperfection: `hm`/`bf`/`ob` are independently optional. A consumer - * cannot tell from the type which terms are present — it must inspect at - * runtime. This is precisely the gap the `protect-dynamodb` bug fell into. - * The fix, for *new* types, is [`crate::int4`]. - */ -export type EncryptedPayload = { -/** - * Schema version — always [`crate::EQL_SCHEMA_VERSION`]. - */ -v: number, -/** - * Table/column identifier. - */ -i: Identifier, -/** - * mp_base85 ciphertext. Required. - */ -c: string, -/** - * HMAC-SHA256 equality term — present iff a `unique` index is configured. - */ -hm?: string, -/** - * Bloom filter term — present iff a `match` index is configured. - * - * Array of set bit positions. EQL stores these as `smallint[]` (signed - * `i16`); a `match` filter sized above 32768 (configurable up to 65536) - * emits upper-half positions as negative signed values, so this is `i16`, - * not `u16` — a `u16` cannot deserialize a real large-filter payload. - */ -bf?: Array, -/** - * Block ORE term — present iff an `ore` index is configured. - */ -ob?: Array, }; diff --git a/crates/eql-types/bindings/EqlEncrypted.ts b/crates/eql-types/bindings/EqlEncrypted.ts deleted file mode 100644 index 3e4b4299..00000000 --- a/crates/eql-types/bindings/EqlEncrypted.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { EncryptedPayload } from "./EncryptedPayload"; -import type { SteVecPayload } from "./SteVecPayload"; - -/** - * `eql_v2_encrypted` — the EQL v2.3 storage payload. - * - * **Serialization** always emits the `k` discriminator (`"ct"` / `"sv"`) — - * this is what drives the internally-tagged TypeScript union and the JSON - * Schema `oneOf`. **Deserialization** is hand-written (below) because the - * v2.3 wire contract makes `k` *optional* on the scalar form: - * `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on - * the presence of `c` vs `sv`, not on `k` (the scalar form requires only - * `v`, `c`, `i`). A `#[serde(tag = "k")]`-derived `Deserialize` would reject a - * schema-valid scalar payload that omits `k`. - */ -export type EqlEncrypted = { "k": "ct" } & EncryptedPayload | { "k": "sv" } & SteVecPayload; diff --git a/crates/eql-types/bindings/Identifier.ts b/crates/eql-types/bindings/Identifier.ts deleted file mode 100644 index 5e976dbe..00000000 --- a/crates/eql-types/bindings/Identifier.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Table + column identifier — wire shape `{"t": "...", "c": "..."}`. - * - * Shared by every payload in both tiers. - */ -export type Identifier = { -/** - * Table name. - */ -t: string, -/** - * Column name. - */ -c: string, }; diff --git a/crates/eql-types/bindings/SteVecElement.ts b/crates/eql-types/bindings/SteVecElement.ts deleted file mode 100644 index 3446c7a7..00000000 --- a/crates/eql-types/bindings/SteVecElement.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * One STE-vector element. - */ -export type SteVecElement = { -/** - * Tokenized selector — deterministic per (path, key). - */ -s: string, -/** - * Per-entry mp_base85 ciphertext. Required. - */ -c: string, -/** - * Array marker — true when the selector points at a JSON array context. - */ -a?: boolean, } & ({ hm: string, } | { oc: string, }); diff --git a/crates/eql-types/bindings/SteVecPayload.ts b/crates/eql-types/bindings/SteVecPayload.ts deleted file mode 100644 index eeae7992..00000000 --- a/crates/eql-types/bindings/SteVecPayload.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Identifier } from "./Identifier"; -import type { SteVecElement } from "./SteVecElement"; - -/** - * STE-vector storage payload (`k = "sv"`). - */ -export type SteVecPayload = { -/** - * Schema version. - */ -v: number, -/** - * Table/column identifier. - */ -i: Identifier, -/** - * Per-selector encrypted entries; root document ciphertext at `sv[0].c`. - */ -sv: Array, }; diff --git a/crates/eql-types/bindings/SteVecTerm.ts b/crates/eql-types/bindings/SteVecTerm.ts deleted file mode 100644 index fe6778f4..00000000 --- a/crates/eql-types/bindings/SteVecTerm.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * SteVec element term. FROZEN as **untagged** — this is the v2.3 wire shape. - * - * A consumer must narrow with `'hm' in term`; there is no literal - * discriminant. A *new* type would tag this — see [`crate::int4`]. - */ -export type SteVecTerm = { hm: string, } | { oc: string, }; diff --git a/crates/eql-types/bindings/v3/BloomFilter.ts b/crates/eql-types/bindings/v3/BloomFilter.ts deleted file mode 100644 index a1ac0d7c..00000000 --- a/crates/eql-types/bindings/v3/BloomFilter.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Bloom-filter match term — the `bf` wire key. Backs the `_match` domains - * (`~~` containment via `@>`/`<@`). - * - * **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, - * and filters sized above 32768 emit upper-half bit positions as negative - * signed values (same rationale as `v2_3::EncryptedPayload::bf`). - */ -export type BloomFilter = Array; diff --git a/crates/eql-types/bindings/v3/Ciphertext.ts b/crates/eql-types/bindings/v3/Ciphertext.ts deleted file mode 100644 index 7beff648..00000000 --- a/crates/eql-types/bindings/v3/Ciphertext.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * mp_base85 source ciphertext — the `c` envelope key. - * - * Required by every v3 domain CHECK; present on every payload. - */ -export type Ciphertext = string; diff --git a/crates/eql-types/bindings/v3/Date.ts b/crates/eql-types/bindings/v3/Date.ts deleted file mode 100644 index 12f801e5..00000000 --- a/crates/eql-types/bindings/v3/Date.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.date` — storage only; every operator is blocked. - */ -export type Date = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/DateEq.ts b/crates/eql-types/bindings/v3/DateEq.ts deleted file mode 100644 index db4b23c3..00000000 --- a/crates/eql-types/bindings/v3/DateEq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.date_eq` — HMAC equality (`=`, `<>`). - */ -export type DateEq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/DateOrd.ts b/crates/eql-types/bindings/v3/DateOrd.ts deleted file mode 100644 index 8eb61922..00000000 --- a/crates/eql-types/bindings/v3/DateOrd.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). - */ -export type DateOrd = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/DateOrdOre.ts b/crates/eql-types/bindings/v3/DateOrdOre.ts deleted file mode 100644 index 8e6496fc..00000000 --- a/crates/eql-types/bindings/v3/DateOrdOre.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. - */ -export type DateOrdOre = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Hmac256.ts b/crates/eql-types/bindings/v3/Hmac256.ts deleted file mode 100644 index 22cefc00..00000000 --- a/crates/eql-types/bindings/v3/Hmac256.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains - * (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. - */ -export type Hmac256 = string; diff --git a/crates/eql-types/bindings/v3/Int2.ts b/crates/eql-types/bindings/v3/Int2.ts deleted file mode 100644 index 5457a00f..00000000 --- a/crates/eql-types/bindings/v3/Int2.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int2` — storage only; every operator is blocked. - */ -export type Int2 = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int2Eq.ts b/crates/eql-types/bindings/v3/Int2Eq.ts deleted file mode 100644 index 1563906d..00000000 --- a/crates/eql-types/bindings/v3/Int2Eq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). - */ -export type Int2Eq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int2Ord.ts b/crates/eql-types/bindings/v3/Int2Ord.ts deleted file mode 100644 index b0720d69..00000000 --- a/crates/eql-types/bindings/v3/Int2Ord.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). - */ -export type Int2Ord = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int2OrdOre.ts b/crates/eql-types/bindings/v3/Int2OrdOre.ts deleted file mode 100644 index 7b2c3416..00000000 --- a/crates/eql-types/bindings/v3/Int2OrdOre.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. - */ -export type Int2OrdOre = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int4.ts b/crates/eql-types/bindings/v3/Int4.ts deleted file mode 100644 index 0918e410..00000000 --- a/crates/eql-types/bindings/v3/Int4.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int4` — storage only; every operator is blocked. - */ -export type Int4 = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int4Eq.ts b/crates/eql-types/bindings/v3/Int4Eq.ts deleted file mode 100644 index 98c7ccc4..00000000 --- a/crates/eql-types/bindings/v3/Int4Eq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). - */ -export type Int4Eq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int4Ord.ts b/crates/eql-types/bindings/v3/Int4Ord.ts deleted file mode 100644 index 0e36e362..00000000 --- a/crates/eql-types/bindings/v3/Int4Ord.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). - */ -export type Int4Ord = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int4OrdOre.ts b/crates/eql-types/bindings/v3/Int4OrdOre.ts deleted file mode 100644 index a77c4a95..00000000 --- a/crates/eql-types/bindings/v3/Int4OrdOre.ts +++ /dev/null @@ -1,27 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), - * scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. - */ -export type Int4OrdOre = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too — ORE over a - * full-domain `int4` is lossless, so no separate `hm` is carried. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int8.ts b/crates/eql-types/bindings/v3/Int8.ts deleted file mode 100644 index c2ef0fe2..00000000 --- a/crates/eql-types/bindings/v3/Int8.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int8` — storage only; every operator is blocked. - */ -export type Int8 = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/Int8Eq.ts b/crates/eql-types/bindings/v3/Int8Eq.ts deleted file mode 100644 index 435e66dd..00000000 --- a/crates/eql-types/bindings/v3/Int8Eq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). - */ -export type Int8Eq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/Int8Ord.ts b/crates/eql-types/bindings/v3/Int8Ord.ts deleted file mode 100644 index ae4b8182..00000000 --- a/crates/eql-types/bindings/v3/Int8Ord.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). - */ -export type Int8Ord = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Int8OrdOre.ts b/crates/eql-types/bindings/v3/Int8OrdOre.ts deleted file mode 100644 index ec33282b..00000000 --- a/crates/eql-types/bindings/v3/Int8OrdOre.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. - */ -export type Int8OrdOre = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts b/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts deleted file mode 100644 index 5701b17f..00000000 --- a/crates/eql-types/bindings/v3/OreBlockU64_8_256.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the - * `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless - * over the scalar's domain, so it serves equality too. SQL-side constructor: - * `eql_v3.ore_block_u64_8_256`. - */ -export type OreBlockU64_8_256 = Array; diff --git a/crates/eql-types/bindings/v3/Text.ts b/crates/eql-types/bindings/v3/Text.ts deleted file mode 100644 index fa65aeb2..00000000 --- a/crates/eql-types/bindings/v3/Text.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.text` — storage only; every operator is blocked. - */ -export type Text = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/TextEq.ts b/crates/eql-types/bindings/v3/TextEq.ts deleted file mode 100644 index 5a5f10f4..00000000 --- a/crates/eql-types/bindings/v3/TextEq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.text_eq` — HMAC equality (`=`, `<>`). - */ -export type TextEq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/bindings/v3/TextMatch.ts b/crates/eql-types/bindings/v3/TextMatch.ts deleted file mode 100644 index c6cacd05..00000000 --- a/crates/eql-types/bindings/v3/TextMatch.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BloomFilter } from "./BloomFilter"; -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.text_match` — Bloom-filter containment match. - */ -export type TextMatch = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Bloom-filter match term (signed smallint bit positions). - */ -bf: BloomFilter, }; diff --git a/crates/eql-types/bindings/v3/TextOrd.ts b/crates/eql-types/bindings/v3/TextOrd.ts deleted file mode 100644 index fbf73e9c..00000000 --- a/crates/eql-types/bindings/v3/TextOrd.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.text_ord` — full lexicographic comparison - * (`=` `<>` `<` `<=` `>` `>=`). - */ -export type TextOrd = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/TextOrdOre.ts b/crates/eql-types/bindings/v3/TextOrdOre.ts deleted file mode 100644 index 218423f2..00000000 --- a/crates/eql-types/bindings/v3/TextOrdOre.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; -import type { OreBlockU64_8_256 } from "./OreBlockU64_8_256"; - -/** - * `eql_v3.text_ord_ore` — full lexicographic comparison, - * scheme-explicit name. - */ -export type TextOrdOre = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * Block-ORE order term. Serves equality too. - */ -ob: OreBlockU64_8_256, }; diff --git a/crates/eql-types/bindings/v3/Timestamptz.ts b/crates/eql-types/bindings/v3/Timestamptz.ts deleted file mode 100644 index 860055a1..00000000 --- a/crates/eql-types/bindings/v3/Timestamptz.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.timestamptz` — storage only; every operator is blocked. - */ -export type Timestamptz = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, }; diff --git a/crates/eql-types/bindings/v3/TimestamptzEq.ts b/crates/eql-types/bindings/v3/TimestamptzEq.ts deleted file mode 100644 index 3db0d720..00000000 --- a/crates/eql-types/bindings/v3/TimestamptzEq.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Ciphertext } from "./Ciphertext"; -import type { Hmac256 } from "./Hmac256"; -import type { Identifier } from "../Identifier"; - -/** - * `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). - */ -export type TimestamptzEq = { -/** - * Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - */ -v: number, -/** - * Table/column identifier. Required by the domain CHECK. - */ -i: Identifier, -/** - * mp_base85 source ciphertext. Required by the domain CHECK. - */ -c: Ciphertext, -/** - * HMAC-SHA-256 equality term. - */ -hm: Hmac256, }; diff --git a/crates/eql-types/schema/EqlEncrypted.json b/crates/eql-types/schema/EqlEncrypted.json deleted file mode 100644 index fe28cc9e..00000000 --- a/crates/eql-types/schema/EqlEncrypted.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "EqlEncrypted", - "description": "`eql_v2_encrypted` — the EQL v2.3 storage payload.\n\n**Serialization** always emits the `k` discriminator (`\"ct\"` / `\"sv\"`) — this is what drives the internally-tagged TypeScript union and the JSON Schema `oneOf`. **Deserialization** is hand-written (below) because the v2.3 wire contract makes `k` *optional* on the scalar form: `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on the presence of `c` vs `sv`, not on `k` (the scalar form requires only `v`, `c`, `i`). A `#[serde(tag = \"k\")]`-derived `Deserialize` would reject a schema-valid scalar payload that omits `k`.", - "oneOf": [ - { - "description": "Scalar ciphertext payload.", - "type": "object", - "required": [ - "c", - "i", - "k", - "v" - ], - "properties": { - "bf": { - "description": "Bloom filter term — present iff a `match` index is configured.\n\nArray of set bit positions. EQL stores these as `smallint[]` (signed `i16`); a `match` filter sized above 32768 (configurable up to 65536) emits upper-half positions as negative signed values, so this is `i16`, not `u16` — a `u16` cannot deserialize a real large-filter payload.", - "type": [ - "array", - "null" - ], - "items": { - "type": "integer", - "format": "int16" - } - }, - "c": { - "description": "mp_base85 ciphertext. Required.", - "type": "string" - }, - "hm": { - "description": "HMAC-SHA256 equality term — present iff a `unique` index is configured.", - "type": [ - "string", - "null" - ] - }, - "i": { - "description": "Table/column identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "k": { - "type": "string", - "enum": [ - "ct" - ] - }, - "ob": { - "description": "Block ORE term — present iff an `ore` index is configured.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "v": { - "description": "Schema version — always [`crate::EQL_SCHEMA_VERSION`].", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - { - "description": "STE-vector payload (jsonb / structured values).", - "type": "object", - "required": [ - "i", - "k", - "sv", - "v" - ], - "properties": { - "i": { - "description": "Table/column identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "k": { - "type": "string", - "enum": [ - "sv" - ] - }, - "sv": { - "description": "Per-selector encrypted entries; root document ciphertext at `sv[0].c`.", - "type": "array", - "items": { - "$ref": "#/definitions/SteVecElement" - } - }, - "v": { - "description": "Schema version.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - } - ], - "definitions": { - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "type": "object", - "required": [ - "c", - "t" - ], - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - } - }, - "SteVecElement": { - "description": "One STE-vector element.", - "type": "object", - "anyOf": [ - { - "description": "HMAC term — boolean leaves, and array / object root placeholders.", - "type": "object", - "required": [ - "hm" - ], - "properties": { - "hm": { - "type": "string" - } - } - }, - { - "description": "CLLW ORE term — string / number leaves.", - "type": "object", - "required": [ - "oc" - ], - "properties": { - "oc": { - "type": "string" - } - } - } - ], - "required": [ - "c", - "s" - ], - "properties": { - "a": { - "description": "Array marker — true when the selector points at a JSON array context.", - "type": [ - "boolean", - "null" - ] - }, - "c": { - "description": "Per-entry mp_base85 ciphertext. Required.", - "type": "string" - }, - "s": { - "description": "Tokenized selector — deterministic per (path, key).", - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date.json b/crates/eql-types/schema/v3/date.json deleted file mode 100644 index fcbe8e71..00000000 --- a/crates/eql-types/schema/v3/date.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/date.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.date` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Date", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_eq.json b/crates/eql-types/schema/v3/date_eq.json deleted file mode 100644 index 8aae8ef3..00000000 --- a/crates/eql-types/schema/v3/date_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/date_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.date_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "DateEq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_ord.json b/crates/eql-types/schema/v3/date_ord.json deleted file mode 100644 index d3253e3e..00000000 --- a/crates/eql-types/schema/v3/date_ord.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/date_ord.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "DateOrd", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/date_ord_ore.json b/crates/eql-types/schema/v3/date_ord_ore.json deleted file mode 100644 index d2471cef..00000000 --- a/crates/eql-types/schema/v3/date_ord_ore.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/date_ord_ore.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.date_ord_ore` — full comparison, scheme-explicit name.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "DateOrdOre", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2.json b/crates/eql-types/schema/v3/int2.json deleted file mode 100644 index 36c48d29..00000000 --- a/crates/eql-types/schema/v3/int2.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int2.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int2` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Int2", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_eq.json b/crates/eql-types/schema/v3/int2_eq.json deleted file mode 100644 index 84e122b8..00000000 --- a/crates/eql-types/schema/v3/int2_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int2_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int2_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "Int2Eq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_ord.json b/crates/eql-types/schema/v3/int2_ord.json deleted file mode 100644 index 9eeee77f..00000000 --- a/crates/eql-types/schema/v3/int2_ord.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int2Ord", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int2_ord_ore.json b/crates/eql-types/schema/v3/int2_ord_ore.json deleted file mode 100644 index 632d62a1..00000000 --- a/crates/eql-types/schema/v3/int2_ord_ore.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord_ore.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int2_ord_ore` — full comparison, scheme-explicit name.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int2OrdOre", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4.json b/crates/eql-types/schema/v3/int4.json deleted file mode 100644 index 25d61648..00000000 --- a/crates/eql-types/schema/v3/int4.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int4.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int4` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Int4", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_eq.json b/crates/eql-types/schema/v3/int4_eq.json deleted file mode 100644 index 0f9204ba..00000000 --- a/crates/eql-types/schema/v3/int4_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int4_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int4_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "Int4Eq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_ord.json b/crates/eql-types/schema/v3/int4_ord.json deleted file mode 100644 index a45f5298..00000000 --- a/crates/eql-types/schema/v3/int4_ord.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int4Ord", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int4_ord_ore.json b/crates/eql-types/schema/v3/int4_ord_ore.json deleted file mode 100644 index 191843c4..00000000 --- a/crates/eql-types/schema/v3/int4_ord_ore.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord_ore.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too — ORE over a full-domain `int4` is lossless, so no separate `hm` is carried." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int4OrdOre", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8.json b/crates/eql-types/schema/v3/int8.json deleted file mode 100644 index 3892f8ba..00000000 --- a/crates/eql-types/schema/v3/int8.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int8.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int8` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Int8", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_eq.json b/crates/eql-types/schema/v3/int8_eq.json deleted file mode 100644 index 8f970b64..00000000 --- a/crates/eql-types/schema/v3/int8_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int8_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.int8_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "Int8Eq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_ord.json b/crates/eql-types/schema/v3/int8_ord.json deleted file mode 100644 index a7ca295d..00000000 --- a/crates/eql-types/schema/v3/int8_ord.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int8Ord", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/int8_ord_ore.json b/crates/eql-types/schema/v3/int8_ord_ore.json deleted file mode 100644 index a86d1b8c..00000000 --- a/crates/eql-types/schema/v3/int8_ord_ore.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord_ore.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.int8_ord_ore` — full comparison, scheme-explicit name.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "Int8OrdOre", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text.json b/crates/eql-types/schema/v3/text.json deleted file mode 100644 index 1d2605c0..00000000 --- a/crates/eql-types/schema/v3/text.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/text.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.text` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Text", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_eq.json b/crates/eql-types/schema/v3/text_eq.json deleted file mode 100644 index abbf3a6f..00000000 --- a/crates/eql-types/schema/v3/text_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/text_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.text_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "TextEq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_match.json b/crates/eql-types/schema/v3/text_match.json deleted file mode 100644 index 4cf7c4f9..00000000 --- a/crates/eql-types/schema/v3/text_match.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/text_match.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "BloomFilter": { - "description": "Bloom-filter match term — the `bf` wire key. Backs the `_match` domains (`~~` containment via `@>`/`<@`).\n\n**Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, and filters sized above 32768 emit upper-half bit positions as negative signed values (same rationale as `v2_3::EncryptedPayload::bf`).", - "items": { - "format": "int16", - "type": "integer" - }, - "type": "array" - }, - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.text_match` — Bloom-filter containment match.", - "properties": { - "bf": { - "allOf": [ - { - "$ref": "#/definitions/BloomFilter" - } - ], - "description": "Bloom-filter match term (signed smallint bit positions)." - }, - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "bf", - "c", - "i", - "v" - ], - "title": "TextMatch", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_ord.json b/crates/eql-types/schema/v3/text_ord.json deleted file mode 100644 index 1758e763..00000000 --- a/crates/eql-types/schema/v3/text_ord.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/text_ord.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.text_ord` — full lexicographic comparison (`=` `<>` `<` `<=` `>` `>=`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "TextOrd", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_ord_ore.json b/crates/eql-types/schema/v3/text_ord_ore.json deleted file mode 100644 index 3b467f14..00000000 --- a/crates/eql-types/schema/v3/text_ord_ore.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/text_ord_ore.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - }, - "OreBlockU64_8_256": { - "description": "Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless over the scalar's domain, so it serves equality too. SQL-side constructor: `eql_v3.ore_block_u64_8_256`.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "description": "`eql_v3.text_ord_ore` — full lexicographic comparison, scheme-explicit name.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "ob": { - "allOf": [ - { - "$ref": "#/definitions/OreBlockU64_8_256" - } - ], - "description": "Block-ORE order term. Serves equality too." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "ob", - "v" - ], - "title": "TextOrdOre", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/timestamptz.json b/crates/eql-types/schema/v3/timestamptz.json deleted file mode 100644 index e1cd070a..00000000 --- a/crates/eql-types/schema/v3/timestamptz.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.timestamptz` — storage only; every operator is blocked.", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "i", - "v" - ], - "title": "Timestamptz", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/timestamptz_eq.json b/crates/eql-types/schema/v3/timestamptz_eq.json deleted file mode 100644 index 6d1759f8..00000000 --- a/crates/eql-types/schema/v3/timestamptz_eq.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz_eq.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "Ciphertext": { - "description": "mp_base85 source ciphertext — the `c` envelope key.\n\nRequired by every v3 domain CHECK; present on every payload.", - "type": "string" - }, - "Hmac256": { - "description": "HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`.", - "type": "string" - }, - "Identifier": { - "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload in both tiers.", - "properties": { - "c": { - "description": "Column name.", - "type": "string" - }, - "t": { - "description": "Table name.", - "type": "string" - } - }, - "required": [ - "c", - "t" - ], - "type": "object" - } - }, - "description": "`eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`).", - "properties": { - "c": { - "allOf": [ - { - "$ref": "#/definitions/Ciphertext" - } - ], - "description": "mp_base85 source ciphertext. Required by the domain CHECK." - }, - "hm": { - "allOf": [ - { - "$ref": "#/definitions/Hmac256" - } - ], - "description": "HMAC-SHA-256 equality term." - }, - "i": { - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ], - "description": "Table/column identifier. Required by the domain CHECK." - }, - "v": { - "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`).", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "c", - "hm", - "i", - "v" - ], - "title": "TimestamptzEq", - "type": "object" -} \ No newline at end of file diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index 5a5a2af1..ff654f61 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -1,46 +1,32 @@ -//! # eql-types — canonical EQL payload types (prototype) +//! # eql-types — canonical EQL payload types //! -//! One Rust definition per EQL payload shape — the single source of truth for: +//! One Rust definition per EQL payload shape — the single source of truth +//! for every tool that produces or consumes EQL payloads +//! (`cipherstash-client`, `protect-ffi`, CipherStash Proxy). TypeScript +//! bindings and JSON Schemas are generated from these definitions in +//! stacked changes; the Rust types are the contract. //! -//! - **Rust** — consumed directly by `cipherstash-client` / `protect-ffi` -//! - **TypeScript** — generated via `ts-rs` (run `cargo test`, see `bindings/`) -//! - **JSON Schema** — generated via `schemars` (run `cargo test`, see `schema/`) +//! The [`v3`] module holds the `eql_v3` encrypted-domain types: one struct +//! per SQL domain (`eql_v3.int4_eq`, `eql_v3.text_match`, …), +//! *capability-encoded* — index terms are required fields, never `Option`. +//! It mirrors `eql-scalars::CATALOG` 1:1, enforced by +//! `tests/catalog_parity.rs`. //! -//! ## Two tiers -//! -//! - [`v2_3`] — **FROZEN.** The `eql_v2_encrypted` wire contract, in production -//! use by customers. Mirrors `eql-payload-v2.3.schema.json`, imperfections -//! included. Nothing here may change. -//! - [`v3`] — the `eql_v3` schema's encrypted-domain types: one struct per -//! SQL domain (`eql_v3.int4_eq`, `eql_v3.text_match`, …), *capability-encoded* -//! — index terms are required fields, never `Option`. Mirrors -//! `eql-scalars::CATALOG` 1:1, enforced by `tests/catalog_parity.rs`. -//! The wire envelope version stays `v: 2` — see the [`v3`] module docs. -//! -//! ## Codegen rules (learned from the ts-rs spike) -//! -//! 1. **Field names ARE wire names** — no `#[serde(rename)]` on fields. ts-rs -//! silently drops a `rename` that is bundled into an attribute it can't -//! parse (`skip_serializing_if`); having no rename removes the footgun. -//! 2. Every `Option` field carries `#[ts(optional)]`, so it generates -//! `field?: T` rather than a required `field: T | null`. -//! 3. `serde`, `ts-rs`, and `schemars` derives travel together on every type. +//! Wire rule: **field names ARE wire names** — no `#[serde(rename)]` +//! anywhere. The struct definition reads exactly like the JSON payload. -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; -pub mod v2_3; pub mod v3; -/// EQL wire-format version. Hard-coded to `2` for every v2.x payload. +/// EQL wire-format version. Hard-coded to `2` for every payload — including +/// the [`v3`] tier, whose generated domain CHECKs assert `VALUE->>'v' = '2'`. pub const EQL_SCHEMA_VERSION: u16 = 2; /// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. /// -/// Shared by every payload in both tiers. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] +/// Shared by every payload. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Identifier { /// Table name. pub t: String, diff --git a/crates/eql-types/src/v2_3.rs b/crates/eql-types/src/v2_3.rs deleted file mode 100644 index 2cc578ad..00000000 --- a/crates/eql-types/src/v2_3.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! # EQL v2.3 wire types — FROZEN -//! -//! `eql_v2_encrypted` is in production use by customers. The shapes here are -//! the v2.3 wire contract and MUST NOT change — not field names, not -//! optionality, not enum tagging. They mirror `eql-payload-v2.3.schema.json` -//! exactly, including its imperfections: -//! -//! - [`EncryptedPayload`] carries `hm`/`bf`/`ob` as independent optionals -//! ("any subset" — a column with several indexes carries several terms). -//! - [`SteVecTerm`] is an **untagged** enum — a consumer must sniff keys. -//! -//! New design work goes in sibling modules (see [`crate::int4`]), never here. - -use crate::Identifier; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -/// `eql_v2_encrypted` — the EQL v2.3 storage payload. -/// -/// **Serialization** always emits the `k` discriminator (`"ct"` / `"sv"`) — -/// this is what drives the internally-tagged TypeScript union and the JSON -/// Schema `oneOf`. **Deserialization** is hand-written (below) because the -/// v2.3 wire contract makes `k` *optional* on the scalar form: -/// `eql_v2.check_encrypted` and `eql-payload-v2.3.schema.json` discriminate on -/// the presence of `c` vs `sv`, not on `k` (the scalar form requires only -/// `v`, `c`, `i`). A `#[serde(tag = "k")]`-derived `Deserialize` would reject a -/// schema-valid scalar payload that omits `k`. -#[derive(Clone, Debug, PartialEq, Serialize, TS, JsonSchema)] -#[ts(export)] -#[serde(tag = "k")] -pub enum EqlEncrypted { - /// Scalar ciphertext payload. - #[serde(rename = "ct")] - Ct(EncryptedPayload), - /// STE-vector payload (jsonb / structured values). - #[serde(rename = "sv")] - Sv(SteVecPayload), -} - -impl<'de> Deserialize<'de> for EqlEncrypted { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - // Mirror eql_v2.check_encrypted: key off `k` when present, otherwise - // fall back to which body field is present (`sv` => STE vector, - // otherwise scalar `ct`). `k` is optional on the scalar form per - // eql-payload-v2.3.schema.json (required there: only `v`, `c`, `i`). - let value = serde_json::Value::deserialize(deserializer)?; - let is_sv = match value.get("k").and_then(serde_json::Value::as_str) { - Some("sv") => true, - Some("ct") => false, - Some(other) => { - return Err(D::Error::custom(format!( - "unknown EQL payload kind: k = {other:?}" - ))) - } - None => value.get("sv").is_some(), - }; - if is_sv { - serde_json::from_value(value) - .map(EqlEncrypted::Sv) - .map_err(D::Error::custom) - } else { - serde_json::from_value(value) - .map(EqlEncrypted::Ct) - .map_err(D::Error::custom) - } - } -} - -/// Scalar storage payload (`k = "ct"`). -/// -/// FROZEN imperfection: `hm`/`bf`/`ob` are independently optional. A consumer -/// cannot tell from the type which terms are present — it must inspect at -/// runtime. This is precisely the gap the `protect-dynamodb` bug fell into. -/// The fix, for *new* types, is [`crate::int4`]. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct EncryptedPayload { - /// Schema version — always [`crate::EQL_SCHEMA_VERSION`]. - pub v: u16, - /// Table/column identifier. - pub i: Identifier, - /// mp_base85 ciphertext. Required. - pub c: String, - /// HMAC-SHA256 equality term — present iff a `unique` index is configured. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub hm: Option, - /// Bloom filter term — present iff a `match` index is configured. - /// - /// Array of set bit positions. EQL stores these as `smallint[]` (signed - /// `i16`); a `match` filter sized above 32768 (configurable up to 65536) - /// emits upper-half positions as negative signed values, so this is `i16`, - /// not `u16` — a `u16` cannot deserialize a real large-filter payload. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub bf: Option>, - /// Block ORE term — present iff an `ore` index is configured. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub ob: Option>, -} - -/// STE-vector storage payload (`k = "sv"`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct SteVecPayload { - /// Schema version. - pub v: u16, - /// Table/column identifier. - pub i: Identifier, - /// Per-selector encrypted entries; root document ciphertext at `sv[0].c`. - pub sv: Vec, -} - -/// One STE-vector element. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -pub struct SteVecElement { - /// Tokenized selector — deterministic per (path, key). - pub s: String, - /// Per-entry mp_base85 ciphertext. Required. - pub c: String, - /// Array marker — true when the selector points at a JSON array context. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub a: Option, - /// Exactly one equality / ordering term, flattened onto the element. - #[serde(flatten)] - pub term: SteVecTerm, -} - -/// SteVec element term. FROZEN as **untagged** — this is the v2.3 wire shape. -/// -/// A consumer must narrow with `'hm' in term`; there is no literal -/// discriminant. A *new* type would tag this — see [`crate::int4`]. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export)] -#[serde(untagged)] -pub enum SteVecTerm { - /// HMAC term — boolean leaves, and array / object root placeholders. - Hmac { hm: String }, - /// CLLW ORE term — string / number leaves. - OreCllw { oc: String }, -} diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index be820930..607dffc0 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -5,13 +5,10 @@ use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.date` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Date { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -27,8 +24,7 @@ impl Date { } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DateEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -46,8 +42,7 @@ impl DateEq { } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DateOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -65,8 +60,7 @@ impl DateOrdOre { } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DateOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index a587d2f2..3c894fb8 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -3,13 +3,10 @@ use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.int2` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int2 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -25,8 +22,7 @@ impl Int2 { } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int2Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -44,8 +40,7 @@ impl Int2Eq { } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int2OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -63,8 +58,7 @@ impl Int2OrdOre { } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int2Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index e1768156..672067fb 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -9,13 +9,10 @@ use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.int4` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int4 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -31,8 +28,7 @@ impl Int4 { } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int4Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -51,8 +47,7 @@ impl Int4Eq { /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), /// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int4OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -71,8 +66,7 @@ impl Int4OrdOre { } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int4Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 7b906a6a..a92dd8c6 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -3,13 +3,10 @@ use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.int8` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int8 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -25,8 +22,7 @@ impl Int8 { } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int8Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -44,8 +40,7 @@ impl Int8Eq { } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int8OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -63,8 +58,7 @@ impl Int8OrdOre { } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Int8Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/src/v3/registry.rs b/crates/eql-types/src/v3/registry.rs index 928440a3..d9472765 100644 --- a/crates/eql-types/src/v3/registry.rs +++ b/crates/eql-types/src/v3/registry.rs @@ -1,13 +1,11 @@ //! Runtime registry of every v3 domain type — the one hand-maintained //! mapping from SQL domain name to Rust type. //! -//! Three consumers: `tests/catalog_parity.rs` (asserts this list exactly -//! covers `eql-scalars::CATALOG`, so it cannot silently go stale), the -//! generic round-trip loop in `tests/v3_conformance.rs`, and the JSON Schema -//! exporter in `tests/export.rs`. Public so FFI consumers can enumerate the -//! protocol surface too. +//! Consumed by `tests/catalog_parity.rs` (which asserts this list exactly +//! covers `eql-scalars::CATALOG`, so it cannot silently go stale) and by +//! the binding/schema exporters added in stacked changes. Public so FFI +//! consumers can enumerate the protocol surface too. -use schemars::{schema::RootSchema, schema_for, JsonSchema}; use serde::{de::DeserializeOwned, Serialize}; use crate::v3::{date, int2, int4, int8, text, timestamptz}; @@ -19,8 +17,6 @@ pub struct DomainType { pub domain: &'static str, /// The Rust type's full path (via `std::any::type_name`). pub type_name: &'static str, - /// The type's JSON Schema. - pub schema: fn() -> RootSchema, /// serde round-trip through the concrete type /// (`Value` → `T` → `Value`). pub roundtrip: fn(serde_json::Value) -> Result, @@ -28,12 +24,11 @@ pub struct DomainType { fn entry(domain: &'static str) -> DomainType where - T: DeserializeOwned + Serialize + JsonSchema, + T: DeserializeOwned + Serialize, { DomainType { domain, type_name: std::any::type_name::(), - schema: || schema_for!(T), roundtrip: |value| { let parsed: T = serde_json::from_value(value)?; serde_json::to_value(&parsed) diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index 26067df5..ddad74bf 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -1,39 +1,33 @@ //! Reusable wire-field newtypes shared by every v3 domain payload. //! //! Each newtype serializes as its inner value (serde's newtype-struct -//! default), so the wire shape is unchanged — but the *name* survives -//! codegen: ts-rs exports a named TS alias (`export type Hmac256 = string`) -//! that every domain binding imports, and schemars registers a named -//! definition that every domain schema `$ref`s. A plain Rust `type` alias -//! would vanish in both outputs. +//! default), so the wire shape is unchanged — but the *name* survives into +//! generated artifacts: the TypeScript bindings and JSON Schemas (added in +//! stacked changes) emit these as named aliases/definitions that every +//! domain type references. A plain Rust `type` alias would vanish there. //! //! Names follow the SEM constructor names in `eql-scalars` (`Term::ctor()`): //! a future scheme change (e.g. a 12-block wide ORE term for timestamptz //! ordering) is a new newtype, not a hunt through `Vec` fields. -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// mp_base85 source ciphertext — the `c` envelope key. /// /// Required by every v3 domain CHECK; present on every payload. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Ciphertext(pub String); /// HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains /// (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Hmac256(pub String); /// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the /// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless /// over the scalar's domain, so it serves equality too. SQL-side constructor: /// `eql_v3.ore_block_u64_8_256`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct OreBlockU64_8_256(pub Vec); /// Bloom-filter match term — the `bf` wire key. Backs the `_match` domains @@ -41,9 +35,8 @@ pub struct OreBlockU64_8_256(pub Vec); /// /// **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, /// and filters sized above 32768 emit upper-half bit positions as negative -/// signed values (same rationale as `v2_3::EncryptedPayload::bf`). -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +/// signed values. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct BloomFilter(pub Vec); impl From for Ciphertext { diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index a6065584..728dc244 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -4,13 +4,10 @@ use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.text` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Text { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -26,8 +23,7 @@ impl Text { } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TextEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -45,8 +41,7 @@ impl TextEq { } /// `eql_v3.text_match` — Bloom-filter containment match. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TextMatch { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -65,8 +60,7 @@ impl TextMatch { /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TextOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -85,8 +79,7 @@ impl TextOrdOre { /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TextOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 89cae954..74b5b1be 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -6,13 +6,10 @@ use crate::v3::terms::{Ciphertext, Hmac256}; use crate::Identifier; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use ts_rs::TS; /// `eql_v3.timestamptz` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Timestamptz { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, @@ -28,8 +25,7 @@ impl Timestamptz { } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] -#[ts(export, export_to = "v3/")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TimestamptzEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). pub v: u16, diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index 64a977dd..129d3cd3 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -2,19 +2,32 @@ //! same catalog that generates the `eql_v3` SQL surface — exactly. Append a //! scalar to the catalog without adding its types here and the first test //! fails; let a term field become `Option` (or carry the wrong wire key) and -//! the second fails, because schemars `required` reflects the real serde -//! contract. - -use std::collections::BTreeSet; +//! the second fails, because it exercises the real serde contract: a payload +//! carrying exactly the catalog's keys must round-trip identically, and +//! removing any one of them must be a deserialization error. use eql_scalars::{Term, CATALOG}; use eql_types::v3::registry; +use serde_json::{json, Value}; /// Mirrors `ENVELOPE_KEYS` in `eql-codegen/src/consts.rs` (`pub(crate)` /// there, so restated here): the keys every generated domain CHECK requires /// before its term keys. const ENVELOPE_KEYS: &[&str] = &["v", "i", "c"]; +/// A synthetic wire value for a required key, by key name. +fn synthesize(key: &str) -> Value { + match key { + "v" => json!(2), + "i" => json!({ "t": "users", "c": "field" }), + "c" => json!("mp_base85_ciphertext"), + "hm" => json!("deadbeef"), + "ob" => json!(["ore_block_0", "ore_block_1"]), + "bf" => json!([-1, 0, 32767]), + other => panic!("no synthetic value for unexpected catalog key {other:?}"), + } +} + #[test] fn registry_exactly_covers_catalog() { let expected: Vec = CATALOG @@ -28,6 +41,13 @@ fn registry_exactly_covers_catalog() { ); } +/// Every domain's wire keys are exactly envelope + catalog terms, proven +/// behaviourally through serde: +/// +/// - a payload carrying exactly those keys round-trips **identically**, so +/// the type requires nothing more and emits nothing less; +/// - removing any one key fails deserialization, so every key is required +/// (no `Option` has crept in). #[test] fn required_keys_match_catalog_terms() { let entries = registry::all(); @@ -39,25 +59,38 @@ fn required_keys_match_catalog_terms() { .find(|e| e.domain == name) .unwrap_or_else(|| panic!("no registry entry for {name}")); - let schema = (entry.schema)(); - let object = schema - .schema - .object - .as_ref() - .unwrap_or_else(|| panic!("{name}: schema is not an object")); - let required: BTreeSet<&str> = object.required.iter().map(String::as_str).collect(); - - let expected: BTreeSet<&str> = ENVELOPE_KEYS + let keys: Vec<&str> = ENVELOPE_KEYS .iter() .copied() .chain(Term::term_json_keys(domain.terms)) .collect(); + let full: Value = keys + .iter() + .map(|k| (k.to_string(), synthesize(k))) + .collect::>() + .into(); + let round_tripped = (entry.roundtrip)(full.clone()).unwrap_or_else(|e| { + panic!( + "{name} ({}): catalog payload rejected: {e}", + entry.type_name + ) + }); assert_eq!( - required, expected, - "{name} ({}): required wire keys must be envelope + catalog terms", + round_tripped, full, + "{name} ({}): round-trip must be identity over the catalog keys", entry.type_name ); + + for key in &keys { + let mut partial = full.clone(); + partial.as_object_mut().unwrap().remove(*key); + assert!( + (entry.roundtrip)(partial).is_err(), + "{name} ({}): must reject payload missing required key {key:?}", + entry.type_name + ); + } } } } diff --git a/crates/eql-types/tests/conformance.rs b/crates/eql-types/tests/conformance.rs deleted file mode 100644 index bc68086a..00000000 --- a/crates/eql-types/tests/conformance.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Conformance fixtures for the FROZEN v2.3 tier — the real guarantee that -//! Rust / TS / JSON Schema and the wire format agree. Codegen guarantees -//! *shape*; these round-trips guarantee *behaviour*. -//! -//! v3 conformance lives in `v3_conformance.rs`; schema export in `export.rs`. - -use eql_types::v2_3::EqlEncrypted; -use serde_json::json; - -#[test] -fn v2_3_scalar_round_trips() { - let wire = json!({ - "k": "ct", "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext", - "hm": "deadbeef" - }); - let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); - assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); -} - -#[test] -fn legacy_payload_silently_accepts_missing_terms() { - // Contrast: the frozen v2.3 scalar type accepts a payload carrying no - // index terms at all — `hm`/`bf`/`ob` are optional. Nothing is wrong with - // the payload *as v2.3*; the point is the type tells a consumer nothing - // about which operators it can support. Hence the runtime guard. - let bare = json!({ - "k": "ct", "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext" - }); - let parsed: EqlEncrypted = serde_json::from_value(bare).unwrap(); - match parsed { - EqlEncrypted::Ct(p) => { - assert!(p.hm.is_none() && p.bf.is_none() && p.ob.is_none()); - } - EqlEncrypted::Sv(_) => panic!("expected Ct"), - } -} - -#[test] -fn v2_3_scalar_without_k_is_accepted() { - // The canonical v2.3 schema makes `k` optional on the scalar form - // (required: v, c, i) and check_encrypted discriminates on c-vs-sv, not k. - // A scalar payload that omits `k` must still deserialize as `Ct`. - let wire = json!({ - "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext", - "hm": "deadbeef" - }); - let parsed: EqlEncrypted = serde_json::from_value(wire).unwrap(); - assert!(matches!(parsed, EqlEncrypted::Ct(_))); - // Serialization always re-emits the discriminator. - assert_eq!( - serde_json::to_value(&parsed).unwrap(), - json!({ - "k": "ct", "v": 2, - "i": { "t": "users", "c": "age" }, - "c": "mp_base85_ciphertext", - "hm": "deadbeef" - }) - ); -} - -#[test] -fn v2_3_bf_accepts_negative_smallint() { - // `bf` is stored as smallint[] (signed i16). A `match` filter sized above - // 32768 (allowed up to 65536) emits upper-half bit positions as negative - // signed smallints; the type must round-trip them. - let wire = json!({ - "k": "ct", "v": 2, - "i": { "t": "users", "c": "email" }, - "c": "mp_base85_ciphertext", - "bf": [-1, -32768, 32767, 0] - }); - let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); - assert!(matches!(parsed, EqlEncrypted::Ct(_))); - assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); -} - -#[test] -fn v2_3_ste_vec_round_trips() { - // Exercises the `sv` path: SteVecPayload plus the flatten + untagged - // SteVecTerm (both `hm` and `oc` elements) — the crate's most fragile serde - // construct, and the route the hand-written EqlEncrypted::deserialize takes. - let wire = json!({ - "k": "sv", "v": 2, - "i": { "t": "users", "c": "profile" }, - "sv": [ - { "s": "selector_root", "c": "ct_root", "hm": "deadbeef" }, - { "s": "selector_name", "c": "ct_name", "oc": "00cafe", "a": true } - ] - }); - let parsed: EqlEncrypted = serde_json::from_value(wire.clone()).unwrap(); - assert!(matches!(parsed, EqlEncrypted::Sv(_))); - assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); -} diff --git a/crates/eql-types/tests/export.rs b/crates/eql-types/tests/export.rs deleted file mode 100644 index 08f78ade..00000000 --- a/crates/eql-types/tests/export.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! JSON Schema export — runs during `cargo test` (alongside ts-rs's own -//! export tests, which write `bindings/`). Output is checked in; freshness is -//! enforced by `mise run types:check`. v3 schema files are named after the -//! SQL domain — the protocol identity — not the Rust type. - -use eql_types::v2_3::EqlEncrypted; -use eql_types::v3::registry; -use schemars::schema_for; - -#[test] -fn dump_v2_3_json_schemas() { - std::fs::create_dir_all("schema").unwrap(); - std::fs::write( - "schema/EqlEncrypted.json", - serde_json::to_string_pretty(&schema_for!(EqlEncrypted)).unwrap(), - ) - .unwrap(); -} - -#[test] -fn dump_v3_json_schemas() { - std::fs::create_dir_all("schema/v3").unwrap(); - for entry in registry::all() { - let mut schema = serde_json::to_value((entry.schema)()).unwrap(); - // schemars 0.8 emits no $id; inject the canonical one. - schema.as_object_mut().unwrap().insert( - "$id".into(), - format!( - "https://schemas.cipherstash.com/eql/v3/{}.json", - entry.domain - ) - .into(), - ); - std::fs::write( - format!("schema/v3/{}.json", entry.domain), - serde_json::to_string_pretty(&schema).unwrap(), - ) - .unwrap(); - } -} diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index 51aa8cb5..2610fa76 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -1,12 +1,11 @@ -//! Conformance for the v3 tier: explicit per-domain tests for the reference -//! token (`int4`, plus the term shapes it doesn't carry), then a generic -//! sweep over the whole registry — every domain type round-trips its wire -//! shape and rejects a payload missing any required key. +//! Conformance for the v3 tier: explicit, readable tests for the reference +//! token (`int4`) plus the term shapes it doesn't carry. The exhaustive +//! catalog-driven sweep (every domain, every required key) lives in +//! `catalog_parity.rs`. use eql_types::v3::int4::{Int4, Int4Eq, Int4Ord, Int4OrdOre}; -use eql_types::v3::registry; use eql_types::v3::text::TextMatch; -use serde_json::{json, Value}; +use serde_json::json; #[test] fn int4_storage_round_trips() { @@ -99,60 +98,3 @@ fn text_match_round_trips_signed_bloom_filter() { "TextMatch must reject a payload with no bf" ); } - -/// A synthetic wire value for a required key, by key name. -fn synthesize(key: &str) -> Value { - match key { - "v" => json!(2), - "i" => json!({ "t": "users", "c": "field" }), - "c" => json!("mp_base85_ciphertext"), - "hm" => json!("deadbeef"), - "ob" => json!(["ore_block_0", "ore_block_1"]), - "bf" => json!([-1, 0, 32767]), - other => panic!("no synthetic value for unexpected required key {other:?}"), - } -} - -/// The registry sweep: every domain type round-trips a payload synthesized -/// from its schema's required keys, and rejects the payload with any one -/// required key removed. (That the required keys are the *right* ones is -/// `catalog_parity.rs`'s job.) -#[test] -fn every_registered_domain_round_trips_and_rejects_missing_keys() { - for entry in registry::all() { - let schema = (entry.schema)(); - let required: Vec = schema - .schema - .object - .as_ref() - .expect("object schema") - .required - .iter() - .cloned() - .collect(); - assert!(!required.is_empty(), "{}: no required keys", entry.domain); - - let full: Value = required - .iter() - .map(|k| (k.clone(), synthesize(k))) - .collect::>() - .into(); - let round_tripped = (entry.roundtrip)(full.clone()) - .unwrap_or_else(|e| panic!("{}: round-trip failed: {e}", entry.domain)); - assert_eq!( - round_tripped, full, - "{}: round-trip not identity", - entry.domain - ); - - for key in &required { - let mut partial = full.clone(); - partial.as_object_mut().unwrap().remove(key); - assert!( - (entry.roundtrip)(partial).is_err(), - "{}: must reject payload missing required key {key:?}", - entry.domain - ); - } - } -} diff --git a/mise.toml b/mise.toml index b1a5f066..0b210278 100644 --- a/mise.toml +++ b/mise.toml @@ -123,7 +123,7 @@ run = """ # workspace members. Scope explicitly to them (NOT --workspace): a # workspace-wide test would drag in tests/sqlx, whose suite needs Postgres + # CS_* secrets and is already covered by the `test` job. eql-tests-macros only -# pulls syn/quote/proc-macro2 and eql-types only serde/ts-rs/schemars, so they +# pulls syn/quote/proc-macro2 and eql-types only serde/serde_json, so they # stay in the lean set. clippy is likewise scoped — a workspace clippy # recompiles the heavy sqlx/tokio/cipherstash-client tree for no added coverage # of these crates. @@ -136,41 +136,6 @@ cargo clippy -p eql-scalars -p eql-codegen -p eql-tests-macros -p eql-types --al cargo test -p eql-scalars -p eql-codegen -p eql-tests-macros -p eql-types """ -[tasks."types:generate"] -description = "Regenerate eql-types TypeScript bindings and JSON Schemas from the Rust types (no database required)" -dir = "{{config_root}}" -run = """ -#!/usr/bin/env bash -# Clean-then-regenerate: ts-rs and tests/export.rs only ever ADD files, so a -# renamed or removed type would otherwise leave an orphaned binding/schema -# behind. The rm lives here (not in the tests — they run in parallel). -# v2.3 outputs at the top level of bindings/ and schema/ are frozen alongside -# their types and are regenerated in place, not cleaned. -set -euo pipefail -rm -rf crates/eql-types/bindings/v3 crates/eql-types/schema/v3 -cargo test -p eql-types -""" - -[tasks."types:check"] -description = "Verify the checked-in eql-types bindings/ and schema/ are fresh (regenerate + git diff)" -dir = "{{config_root}}" -depends = ["types:generate"] -run = """ -#!/usr/bin/env bash -set -euo pipefail -git diff --exit-code -- crates/eql-types/bindings crates/eql-types/schema || { - echo "eql-types bindings/ or schema/ are stale — run 'mise run types:generate' and commit the result" >&2 - exit 1 -} -# git diff is blind to brand-new files; untracked output is stale too. -untracked=$(git ls-files --others --exclude-standard -- crates/eql-types/bindings crates/eql-types/schema) -if [ -n "$untracked" ]; then - echo "eql-types has uncommitted generated files:" >&2 - echo "$untracked" >&2 - exit 1 -fi -""" - [tasks."test:matrix:inventory"] description = "Verify the matrix test-name set against the single canonical snapshot (or its derived eq-only subset), catalog-cross-checked (no database required)" dir = "{{config_root}}/tests/sqlx" From aa46959ebdc097a068d2ac5b0be42c3ff459177b Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 10 Jun 2026 21:44:08 +1000 Subject: [PATCH 06/12] fix(eql-types): pin the envelope version, reject unknown keys, derive registry domains, share ENVELOPE_KEYS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four review findings closed at the serde layer: - v is now SchemaVersion, a validated newtype whose deserializer rejects any value other than 2 (including the string "2" that the CHECK's ->> coercion would admit) — the Rust analogue of VALUE->>'v' = '2', failing at the type boundary instead of at INSERT. - Every domain struct (and Identifier) is #[serde(deny_unknown_fields)]: a payload carrying keys outside the domain's set fails to deserialize rather than being silently stripped on the next serialize, so a pass-through consumer cannot lose terms it didn't know about. - The registry derives each domain name from the type's own SQL_DOMAIN (new V3Domain trait) instead of re-typing 23 string literals — two same-shaped types (_ord vs _ord_ore) can no longer be registered under each other's domain. - ENVELOPE_KEYS is hoisted into eql-scalars (the catalog) and consumed by eql-codegen's CHECK generation, eql-types' parity tests, and the sqlx harness's payload_required_keys — collapsing three hand-synced copies into one definition. codegen golden parity stays byte-identical. The catalog parity sweep gains unknown-key and wrong-version rejection legs across all 23 domains. --- crates/eql-codegen/src/consts.rs | 9 +-- crates/eql-scalars/src/lib.rs | 11 ++++ crates/eql-types/src/lib.rs | 43 +++++++++++++++ crates/eql-types/src/v3/date.rs | 47 +++++++++------- crates/eql-types/src/v3/int2.rs | 47 +++++++++------- crates/eql-types/src/v3/int4.rs | 47 +++++++++------- crates/eql-types/src/v3/int8.rs | 47 +++++++++------- crates/eql-types/src/v3/mod.rs | 19 +++++++ crates/eql-types/src/v3/registry.rs | 70 +++++++++++++----------- crates/eql-types/src/v3/text.rs | 58 +++++++++++--------- crates/eql-types/src/v3/timestamptz.rs | 25 +++++---- crates/eql-types/tests/catalog_parity.rs | 35 +++++++++--- crates/eql-types/tests/v3_conformance.rs | 38 +++++++++++++ tests/sqlx/src/scalar_domains.rs | 5 +- 14 files changed, 336 insertions(+), 165 deletions(-) diff --git a/crates/eql-codegen/src/consts.rs b/crates/eql-codegen/src/consts.rs index 9266882f..a3bccf3c 100644 --- a/crates/eql-codegen/src/consts.rs +++ b/crates/eql-codegen/src/consts.rs @@ -11,10 +11,11 @@ pub(crate) const AUTO_GENERATED_HEADER: &str = "-- AUTOMATICALLY GENERATED FILE. /// the core types at. pub(crate) const SCHEMA: &str = "eql_v3"; -/// Always-present payload keys checked for presence in every domain CHECK, in -/// order: envelope version (`v`), ident (`i`), ciphertext (`c`). Term-specific -/// keys are appended after these by `context::domain_block`. -pub(crate) const ENVELOPE_KEYS: &[&str] = &["v", "i", "c"]; +/// Always-present payload keys checked for presence in every domain CHECK. +/// Term-specific keys are appended after these by `context::domain_block`. +/// Defined in the catalog (`eql_scalars::ENVELOPE_KEYS`) so the CHECKs and +/// the `eql-types` payload structs share one envelope definition. +pub(crate) const ENVELOPE_KEYS: &[&str] = eql_scalars::ENVELOPE_KEYS; /// Escape a string for use inside a single-quoted SQL literal by doubling /// embedded single quotes. diff --git a/crates/eql-scalars/src/lib.rs b/crates/eql-scalars/src/lib.rs index d3d055e1..8cbabcd8 100644 --- a/crates/eql-scalars/src/lib.rs +++ b/crates/eql-scalars/src/lib.rs @@ -73,6 +73,17 @@ pub enum ScalarKind { Timestamptz, } +/// Always-present payload keys required by every generated domain CHECK, +/// before the domain's term keys, in order: envelope version (`v`), ident +/// (`i`), ciphertext (`c`). +/// +/// Lives here — in the catalog — because it is cross-schema contract data +/// consumed on both sides of the generated surface: `eql-codegen` builds +/// every domain CHECK from it, and `eql-types` builds its payload structs +/// and parity tests against it. One definition, so the envelope cannot +/// drift between the SQL and the canonical types. +pub const ENVELOPE_KEYS: &[&str] = &["v", "i", "c"]; + /// A fixed index term known to the scalar materializer. /// /// `Hm` provides equality; `Ore` provides equality plus ordering. The diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index ff654f61..9232bdd9 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -23,10 +23,53 @@ pub mod v3; /// the [`v3`] tier, whose generated domain CHECKs assert `VALUE->>'v' = '2'`. pub const EQL_SCHEMA_VERSION: u16 = 2; +/// The envelope version field (`v`) — always exactly [`EQL_SCHEMA_VERSION`] +/// on the wire. +/// +/// Deserialization rejects any other value: the Rust analogue of the domain +/// CHECK's `VALUE->>'v' = '2'`, so a wrong-version payload fails at the type +/// boundary instead of at INSERT. The inner value is private; the only +/// constructible instance is the current version. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct SchemaVersion(u16); + +impl SchemaVersion { + /// The current (only) wire version, `2`. + pub const CURRENT: Self = Self(EQL_SCHEMA_VERSION); + + /// The wire value. + pub const fn get(self) -> u16 { + self.0 + } +} + +impl Default for SchemaVersion { + fn default() -> Self { + Self::CURRENT + } +} + +impl<'de> Deserialize<'de> for SchemaVersion { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let v = u16::deserialize(deserializer)?; + if v == EQL_SCHEMA_VERSION { + Ok(Self(v)) + } else { + Err(serde::de::Error::custom(format!( + "unsupported EQL schema version {v} (expected {EQL_SCHEMA_VERSION})" + ))) + } + } +} + /// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. /// /// Shared by every payload. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Identifier { /// Table name. pub t: String, diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index 607dffc0..721e099d 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -4,30 +4,34 @@ //! capability table. use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.date` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Date { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Date { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.date"; +impl V3Domain for Date { + const SQL_DOMAIN: &'static str = "eql_v3.date"; } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DateEq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -36,16 +40,17 @@ pub struct DateEq { pub hm: Hmac256, } -impl DateEq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.date_eq"; +impl V3Domain for DateEq { + const SQL_DOMAIN: &'static str = "eql_v3.date_eq"; } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DateOrdOre { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -54,16 +59,17 @@ pub struct DateOrdOre { pub ob: OreBlockU64_8_256, } -impl DateOrdOre { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.date_ord_ore"; +impl V3Domain for DateOrdOre { + const SQL_DOMAIN: &'static str = "eql_v3.date_ord_ore"; } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DateOrd { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -72,7 +78,6 @@ pub struct DateOrd { pub ob: OreBlockU64_8_256, } -impl DateOrd { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.date_ord"; +impl V3Domain for DateOrd { + const SQL_DOMAIN: &'static str = "eql_v3.date_ord"; } diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index 3c894fb8..7e0b1029 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -2,30 +2,34 @@ //! [`crate::v3::int4`] — see that module for the capability table. use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int2` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int2 { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Int2 { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int2"; +impl V3Domain for Int2 { + const SQL_DOMAIN: &'static str = "eql_v3.int2"; } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int2Eq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -34,16 +38,17 @@ pub struct Int2Eq { pub hm: Hmac256, } -impl Int2Eq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int2_eq"; +impl V3Domain for Int2Eq { + const SQL_DOMAIN: &'static str = "eql_v3.int2_eq"; } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int2OrdOre { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -52,16 +57,17 @@ pub struct Int2OrdOre { pub ob: OreBlockU64_8_256, } -impl Int2OrdOre { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int2_ord_ore"; +impl V3Domain for Int2OrdOre { + const SQL_DOMAIN: &'static str = "eql_v3.int2_ord_ore"; } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int2Ord { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -70,7 +76,6 @@ pub struct Int2Ord { pub ob: OreBlockU64_8_256, } -impl Int2Ord { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int2_ord"; +impl V3Domain for Int2Ord { + const SQL_DOMAIN: &'static str = "eql_v3.int2_ord"; } diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index 672067fb..7f2cb57c 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -8,30 +8,34 @@ //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int4` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int4 { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Int4 { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int4"; +impl V3Domain for Int4 { + const SQL_DOMAIN: &'static str = "eql_v3.int4"; } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int4Eq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -40,17 +44,18 @@ pub struct Int4Eq { pub hm: Hmac256, } -impl Int4Eq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int4_eq"; +impl V3Domain for Int4Eq { + const SQL_DOMAIN: &'static str = "eql_v3.int4_eq"; } /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), /// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int4OrdOre { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -60,16 +65,17 @@ pub struct Int4OrdOre { pub ob: OreBlockU64_8_256, } -impl Int4OrdOre { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int4_ord_ore"; +impl V3Domain for Int4OrdOre { + const SQL_DOMAIN: &'static str = "eql_v3.int4_ord_ore"; } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int4Ord { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -78,7 +84,6 @@ pub struct Int4Ord { pub ob: OreBlockU64_8_256, } -impl Int4Ord { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int4_ord"; +impl V3Domain for Int4Ord { + const SQL_DOMAIN: &'static str = "eql_v3.int4_ord"; } diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index a92dd8c6..6d3f4516 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -2,30 +2,34 @@ //! [`crate::v3::int4`] — see that module for the capability table. use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int8` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int8 { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Int8 { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int8"; +impl V3Domain for Int8 { + const SQL_DOMAIN: &'static str = "eql_v3.int8"; } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int8Eq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -34,16 +38,17 @@ pub struct Int8Eq { pub hm: Hmac256, } -impl Int8Eq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int8_eq"; +impl V3Domain for Int8Eq { + const SQL_DOMAIN: &'static str = "eql_v3.int8_eq"; } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int8OrdOre { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -52,16 +57,17 @@ pub struct Int8OrdOre { pub ob: OreBlockU64_8_256, } -impl Int8OrdOre { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int8_ord_ore"; +impl V3Domain for Int8OrdOre { + const SQL_DOMAIN: &'static str = "eql_v3.int8_ord_ore"; } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Int8Ord { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -70,7 +76,6 @@ pub struct Int8Ord { pub ob: OreBlockU64_8_256, } -impl Int8Ord { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.int8_ord"; +impl V3Domain for Int8Ord { + const SQL_DOMAIN: &'static str = "eql_v3.int8_ord"; } diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 2d0f7847..46f1a426 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -24,6 +24,13 @@ //! (SQL-side) by the domain CHECK. A missing term key is a deserialization //! error — the Rust analogue of the CHECK constraint. //! +//! The types are also **strict**: every struct is +//! `#[serde(deny_unknown_fields)]`, so a payload carrying keys outside the +//! domain's set fails to deserialize rather than being silently stripped on +//! the next serialize (a pass-through consumer must not lose data it didn't +//! know about), and the `v` field is [`crate::SchemaVersion`], which rejects +//! any version other than `2`. +//! //! ## Why there is no discriminated enum //! //! Cross-token: impossible — an `int4_eq` and an `int8_eq` payload are @@ -45,3 +52,15 @@ pub mod timestamptz; /// The PostgreSQL schema every domain in this module inhabits. pub const SQL_SCHEMA: &str = "eql_v3"; + +/// Implemented by every v3 domain payload type: the fully-qualified SQL +/// domain the payload inhabits (e.g. `"eql_v3.int4_eq"`). +/// +/// The [`registry`] derives its domain names from this constant, so the +/// type ↔ domain binding has exactly one definition per type — there is no +/// second string to keep in sync, and two same-shaped types (`_ord` vs +/// `_ord_ore`) cannot be registered under each other's domain. +pub trait V3Domain { + /// Fully-qualified SQL domain, e.g. `"eql_v3.int4_eq"`. + const SQL_DOMAIN: &'static str; +} diff --git a/crates/eql-types/src/v3/registry.rs b/crates/eql-types/src/v3/registry.rs index d9472765..8ebcf56c 100644 --- a/crates/eql-types/src/v3/registry.rs +++ b/crates/eql-types/src/v3/registry.rs @@ -1,19 +1,22 @@ //! Runtime registry of every v3 domain type — the one hand-maintained -//! mapping from SQL domain name to Rust type. +//! list of types in catalog order. //! -//! Consumed by `tests/catalog_parity.rs` (which asserts this list exactly -//! covers `eql-scalars::CATALOG`, so it cannot silently go stale) and by -//! the binding/schema exporters added in stacked changes. Public so FFI +//! Each entry's domain name is derived from the type's own +//! [`V3Domain::SQL_DOMAIN`], so the type ↔ domain binding cannot be +//! mis-registered (there is no second string to typo or swap). Consumed by +//! `tests/catalog_parity.rs` (which asserts this list exactly covers +//! `eql-scalars::CATALOG`, so it cannot silently go stale) and by the +//! binding/schema exporters added in stacked changes. Public so FFI //! consumers can enumerate the protocol surface too. use serde::{de::DeserializeOwned, Serialize}; -use crate::v3::{date, int2, int4, int8, text, timestamptz}; +use crate::v3::{date, int2, int4, int8, text, timestamptz, V3Domain}; /// One registered v3 domain type. pub struct DomainType { - /// Unqualified SQL domain name (e.g. `"int4_eq"`) — matches - /// `eql-scalars` `ScalarSpec::domain_name`. + /// Unqualified SQL domain name (e.g. `"int4_eq"`) — `SQL_DOMAIN` minus + /// the schema qualifier; matches `eql-scalars` `ScalarSpec::domain_name`. pub domain: &'static str, /// The Rust type's full path (via `std::any::type_name`). pub type_name: &'static str, @@ -22,10 +25,13 @@ pub struct DomainType { pub roundtrip: fn(serde_json::Value) -> Result, } -fn entry(domain: &'static str) -> DomainType +fn entry() -> DomainType where - T: DeserializeOwned + Serialize, + T: V3Domain + DeserializeOwned + Serialize, { + let domain = T::SQL_DOMAIN + .strip_prefix("eql_v3.") + .expect("SQL_DOMAIN must be qualified with the eql_v3 schema"); DomainType { domain, type_name: std::any::type_name::(), @@ -40,28 +46,28 @@ where /// each token's domains in manifest order). pub fn all() -> Vec { vec![ - entry::("int4"), - entry::("int4_eq"), - entry::("int4_ord_ore"), - entry::("int4_ord"), - entry::("int2"), - entry::("int2_eq"), - entry::("int2_ord_ore"), - entry::("int2_ord"), - entry::("int8"), - entry::("int8_eq"), - entry::("int8_ord_ore"), - entry::("int8_ord"), - entry::("date"), - entry::("date_eq"), - entry::("date_ord_ore"), - entry::("date_ord"), - entry::("timestamptz"), - entry::("timestamptz_eq"), - entry::("text"), - entry::("text_eq"), - entry::("text_match"), - entry::("text_ord_ore"), - entry::("text_ord"), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), + entry::(), ] } diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index 728dc244..ef4d9b14 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -3,30 +3,34 @@ //! term (`@>`/`<@` containment for `LIKE`-style matching). use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.text` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Text { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Text { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.text"; +impl V3Domain for Text { + const SQL_DOMAIN: &'static str = "eql_v3.text"; } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TextEq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -35,16 +39,17 @@ pub struct TextEq { pub hm: Hmac256, } -impl TextEq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.text_eq"; +impl V3Domain for TextEq { + const SQL_DOMAIN: &'static str = "eql_v3.text_eq"; } /// `eql_v3.text_match` — Bloom-filter containment match. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TextMatch { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -53,17 +58,18 @@ pub struct TextMatch { pub bf: BloomFilter, } -impl TextMatch { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.text_match"; +impl V3Domain for TextMatch { + const SQL_DOMAIN: &'static str = "eql_v3.text_match"; } /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TextOrdOre { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -72,17 +78,18 @@ pub struct TextOrdOre { pub ob: OreBlockU64_8_256, } -impl TextOrdOre { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.text_ord_ore"; +impl V3Domain for TextOrdOre { + const SQL_DOMAIN: &'static str = "eql_v3.text_ord_ore"; } /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TextOrd { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -91,7 +98,6 @@ pub struct TextOrd { pub ob: OreBlockU64_8_256, } -impl TextOrd { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.text_ord"; +impl V3Domain for TextOrd { + const SQL_DOMAIN: &'static str = "eql_v3.text_ord"; } diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 74b5b1be..79f2c8c0 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -5,30 +5,34 @@ //! Ordering arrives with a future wide-ORE term (see `eql-scalars`). use crate::v3::terms::{Ciphertext, Hmac256}; -use crate::Identifier; +use crate::v3::V3Domain; +use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.timestamptz` — storage only; every operator is blocked. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Timestamptz { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, } -impl Timestamptz { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.timestamptz"; +impl V3Domain for Timestamptz { + const SQL_DOMAIN: &'static str = "eql_v3.timestamptz"; } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TimestamptzEq { - /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`). - pub v: u16, + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, /// Table/column identifier. Required by the domain CHECK. pub i: Identifier, /// mp_base85 source ciphertext. Required by the domain CHECK. @@ -37,7 +41,6 @@ pub struct TimestamptzEq { pub hm: Hmac256, } -impl TimestamptzEq { - /// Fully-qualified SQL domain this payload inhabits. - pub const SQL_DOMAIN: &'static str = "eql_v3.timestamptz_eq"; +impl V3Domain for TimestamptzEq { + const SQL_DOMAIN: &'static str = "eql_v3.timestamptz_eq"; } diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index 129d3cd3..b96d55d8 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -6,15 +6,10 @@ //! carrying exactly the catalog's keys must round-trip identically, and //! removing any one of them must be a deserialization error. -use eql_scalars::{Term, CATALOG}; +use eql_scalars::{Term, CATALOG, ENVELOPE_KEYS}; use eql_types::v3::registry; use serde_json::{json, Value}; -/// Mirrors `ENVELOPE_KEYS` in `eql-codegen/src/consts.rs` (`pub(crate)` -/// there, so restated here): the keys every generated domain CHECK requires -/// before its term keys. -const ENVELOPE_KEYS: &[&str] = &["v", "i", "c"]; - /// A synthetic wire value for a required key, by key name. fn synthesize(key: &str) -> Value { match key { @@ -47,7 +42,11 @@ fn registry_exactly_covers_catalog() { /// - a payload carrying exactly those keys round-trips **identically**, so /// the type requires nothing more and emits nothing less; /// - removing any one key fails deserialization, so every key is required -/// (no `Option` has crept in). +/// (no `Option` has crept in); +/// - adding a key outside the set fails deserialization +/// (`deny_unknown_fields` — no silent stripping on re-serialize); +/// - a wrong envelope version fails deserialization (`SchemaVersion` +/// mirrors the domain CHECK's `VALUE->>'v' = '2'`). #[test] fn required_keys_match_catalog_terms() { let entries = registry::all(); @@ -91,6 +90,28 @@ fn required_keys_match_catalog_terms() { entry.type_name ); } + + let mut extra = full.clone(); + extra + .as_object_mut() + .unwrap() + .insert("zz".into(), json!(true)); + assert!( + (entry.roundtrip)(extra).is_err(), + "{name} ({}): must reject payload carrying an unknown key", + entry.type_name + ); + + let mut wrong_version = full.clone(); + wrong_version + .as_object_mut() + .unwrap() + .insert("v".into(), json!(3)); + assert!( + (entry.roundtrip)(wrong_version).is_err(), + "{name} ({}): must reject envelope version other than 2", + entry.type_name + ); } } } diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index 2610fa76..7896231f 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -5,6 +5,7 @@ use eql_types::v3::int4::{Int4, Int4Eq, Int4Ord, Int4OrdOre}; use eql_types::v3::text::TextMatch; +use eql_types::v3::V3Domain; use serde_json::json; #[test] @@ -62,6 +63,43 @@ fn int4_eq_rejects_missing_hmac() { assert!(result.is_err(), "Int4Eq must reject a payload with no hm"); } +#[test] +fn rejects_wrong_envelope_version() { + // The SchemaVersion field is the Rust analogue of the domain CHECK's + // `VALUE->>'v' = '2'`: any other version — including a string "2", + // which the CHECK's `->>` coercion would accept — fails at the type + // boundary instead of at INSERT. + for v in [json!(1), json!(3), json!("2")] { + let wire = json!({ + "v": v, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let result: Result = serde_json::from_value(wire); + assert!(result.is_err(), "Int4Eq must reject v = {v}"); + } +} + +#[test] +fn rejects_unknown_keys() { + // deny_unknown_fields: a payload carrying keys outside the domain's set + // is not silently accepted-and-stripped — a pass-through consumer must + // not lose data it didn't know about. + let wire = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef", + "ob": ["ore_block_0"] + }); + let result: Result = serde_json::from_value(wire); + assert!( + result.is_err(), + "Int4Eq must reject a payload carrying keys beyond its domain (here: ob)" + ); +} + #[test] fn int4_ord_rejects_missing_ore_term() { let no_ob = json!({ diff --git a/tests/sqlx/src/scalar_domains.rs b/tests/sqlx/src/scalar_domains.rs index 25ad3605..55bf26c8 100644 --- a/tests/sqlx/src/scalar_domains.rs +++ b/tests/sqlx/src/scalar_domains.rs @@ -502,7 +502,10 @@ impl Variant { /// matrix `payload_check` arm iterates this to assert each key's /// absence is rejected at the cast. pub fn payload_required_keys(self) -> impl Iterator { - ["v", "i", "c"].into_iter().chain(self.required_term()) + eql_scalars::ENVELOPE_KEYS + .iter() + .copied() + .chain(self.required_term()) } pub const fn supports_eq(self) -> bool { From b3338ff1256a3488a25cd34602cfc6a4698633b8 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 14:08:29 +1000 Subject: [PATCH 07/12] refactor(eql-types): reshape DomainType as an object-safe trait The fn-pointer registry struct becomes a trait with one blanket impl on PhantomData, so Vec> entries are zero-sized type-level handles and V3Domain::SQL_DOMAIN stays the single per-type anchor the impl reads from. The separate registry module is gone; the inventory (all()) lives in v3/mod.rs. --- crates/eql-types/README.md | 5 +- crates/eql-types/src/v3/mod.rs | 96 ++++++++++++++++++++++-- crates/eql-types/src/v3/registry.rs | 73 ------------------ crates/eql-types/tests/catalog_parity.rs | 37 ++++----- 4 files changed, 113 insertions(+), 98 deletions(-) delete mode 100644 crates/eql-types/src/v3/registry.rs diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index 07ce7331..447c2613 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -44,8 +44,9 @@ field names are unchanged from v2 (the purpose-named rename in ## Drift protection -`tests/catalog_parity.rs` asserts the [`v3::registry`](src/v3/registry.rs) -exactly covers `eql-scalars::CATALOG` — the same catalog that generates the +`tests/catalog_parity.rs` asserts the domain inventory — +[`v3::all()`](src/v3/mod.rs), a `Vec>` of zero-sized +type-level handles — exactly covers `eql-scalars::CATALOG` — the same catalog that generates the `eql_v3` SQL surface — every domain, in order, and proves behaviourally that each type's wire keys are exactly the envelope (`v`, `i`, `c`) plus the catalog's term keys. Adding a scalar to the catalog without adding its types diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 46f1a426..3625831d 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -41,11 +41,14 @@ //! shapes that no sniffing can separate. Consumers read from a typed column //! and already know the domain. +use std::marker::PhantomData; + +use serde::{de::DeserializeOwned, Serialize}; + pub mod date; pub mod int2; pub mod int4; pub mod int8; -pub mod registry; pub mod terms; pub mod text; pub mod timestamptz; @@ -56,11 +59,94 @@ pub const SQL_SCHEMA: &str = "eql_v3"; /// Implemented by every v3 domain payload type: the fully-qualified SQL /// domain the payload inhabits (e.g. `"eql_v3.int4_eq"`). /// -/// The [`registry`] derives its domain names from this constant, so the -/// type ↔ domain binding has exactly one definition per type — there is no -/// second string to keep in sync, and two same-shaped types (`_ord` vs -/// `_ord_ore`) cannot be registered under each other's domain. +/// The [`DomainType`] blanket impl derives everything else from this +/// constant, so the type ↔ domain binding has exactly one definition per +/// type — there is no second string to keep in sync, and two same-shaped +/// types (`_ord` vs `_ord_ore`) cannot be enumerated under each other's +/// domain. pub trait V3Domain { /// Fully-qualified SQL domain, e.g. `"eql_v3.int4_eq"`. const SQL_DOMAIN: &'static str; } + +/// Object-safe view of one v3 domain type — what [`all`] enumerates. +/// +/// Implemented once, by the blanket impl below, for `PhantomData` over +/// every payload type: a `Box` is a zero-sized type-level +/// handle, not a payload instance. (The trait cannot be [`V3Domain`] itself: +/// an associated const is not object-safe, and [`Self::roundtrip`] needs +/// `Deserialize`, which is `Sized`-only — so the dyn surface lives on the +/// handle, and `V3Domain` stays the compile-time anchor it reads from.) +/// +/// Consumed by `tests/catalog_parity.rs` (which asserts [`all`] exactly +/// covers `eql-scalars::CATALOG`, so the list cannot silently go stale) and +/// by the binding/schema exporters added in stacked changes. Public so FFI +/// consumers can enumerate the protocol surface too. +pub trait DomainType { + /// Fully-qualified SQL domain name, e.g. `"eql_v3.int4_eq"`. + fn sql_domain(&self) -> &'static str; + + /// Unqualified SQL domain name (e.g. `"int4_eq"`) — [`Self::sql_domain`] + /// minus the schema qualifier; matches `eql-scalars` + /// `ScalarSpec::domain_name`. + fn domain(&self) -> &'static str { + self.sql_domain() + .strip_prefix("eql_v3.") + .expect("SQL_DOMAIN must be qualified with the eql_v3 schema") + } + + /// The Rust type's full path (via `std::any::type_name`). + fn type_name(&self) -> &'static str; + + /// serde round-trip through the concrete type (`Value` → `T` → `Value`). + fn roundtrip(&self, value: serde_json::Value) -> Result; +} + +impl DomainType for PhantomData +where + T: V3Domain + DeserializeOwned + Serialize, +{ + fn sql_domain(&self) -> &'static str { + T::SQL_DOMAIN + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } + + fn roundtrip(&self, value: serde_json::Value) -> Result { + let parsed: T = serde_json::from_value(value)?; + serde_json::to_value(&parsed) + } +} + +/// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then +/// each token's domains in manifest order) — the one hand-maintained list of +/// types in the crate. +pub fn all() -> Vec> { + vec![ + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + ] +} diff --git a/crates/eql-types/src/v3/registry.rs b/crates/eql-types/src/v3/registry.rs deleted file mode 100644 index 8ebcf56c..00000000 --- a/crates/eql-types/src/v3/registry.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Runtime registry of every v3 domain type — the one hand-maintained -//! list of types in catalog order. -//! -//! Each entry's domain name is derived from the type's own -//! [`V3Domain::SQL_DOMAIN`], so the type ↔ domain binding cannot be -//! mis-registered (there is no second string to typo or swap). Consumed by -//! `tests/catalog_parity.rs` (which asserts this list exactly covers -//! `eql-scalars::CATALOG`, so it cannot silently go stale) and by the -//! binding/schema exporters added in stacked changes. Public so FFI -//! consumers can enumerate the protocol surface too. - -use serde::{de::DeserializeOwned, Serialize}; - -use crate::v3::{date, int2, int4, int8, text, timestamptz, V3Domain}; - -/// One registered v3 domain type. -pub struct DomainType { - /// Unqualified SQL domain name (e.g. `"int4_eq"`) — `SQL_DOMAIN` minus - /// the schema qualifier; matches `eql-scalars` `ScalarSpec::domain_name`. - pub domain: &'static str, - /// The Rust type's full path (via `std::any::type_name`). - pub type_name: &'static str, - /// serde round-trip through the concrete type - /// (`Value` → `T` → `Value`). - pub roundtrip: fn(serde_json::Value) -> Result, -} - -fn entry() -> DomainType -where - T: V3Domain + DeserializeOwned + Serialize, -{ - let domain = T::SQL_DOMAIN - .strip_prefix("eql_v3.") - .expect("SQL_DOMAIN must be qualified with the eql_v3 schema"); - DomainType { - domain, - type_name: std::any::type_name::(), - roundtrip: |value| { - let parsed: T = serde_json::from_value(value)?; - serde_json::to_value(&parsed) - }, - } -} - -/// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then -/// each token's domains in manifest order). -pub fn all() -> Vec { - vec![ - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - entry::(), - ] -} diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index b96d55d8..376822bf 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -1,4 +1,4 @@ -//! The drift gate: the v3 registry must mirror `eql-scalars::CATALOG` — the +//! The drift gate: the v3 domain inventory must mirror `eql-scalars::CATALOG` — the //! same catalog that generates the `eql_v3` SQL surface — exactly. Append a //! scalar to the catalog without adding its types here and the first test //! fails; let a term field become `Option` (or carry the wrong wire key) and @@ -7,7 +7,7 @@ //! removing any one of them must be a deserialization error. use eql_scalars::{Term, CATALOG, ENVELOPE_KEYS}; -use eql_types::v3::registry; +use eql_types::v3; use serde_json::{json, Value}; /// A synthetic wire value for a required key, by key name. @@ -24,15 +24,15 @@ fn synthesize(key: &str) -> Value { } #[test] -fn registry_exactly_covers_catalog() { +fn inventory_exactly_covers_catalog() { let expected: Vec = CATALOG .iter() .flat_map(|spec| spec.domains.iter().map(|d| spec.domain_name(d))) .collect(); - let actual: Vec<&str> = registry::all().iter().map(|e| e.domain).collect(); + let actual: Vec<&str> = v3::all().iter().map(|e| e.domain()).collect(); assert_eq!( actual, expected, - "v3 registry must list every CATALOG domain, in catalog order" + "v3::all() must list every CATALOG domain, in catalog order" ); } @@ -49,14 +49,14 @@ fn registry_exactly_covers_catalog() { /// mirrors the domain CHECK's `VALUE->>'v' = '2'`). #[test] fn required_keys_match_catalog_terms() { - let entries = registry::all(); + let entries = v3::all(); for spec in CATALOG { for domain in spec.domains { let name = spec.domain_name(domain); let entry = entries .iter() - .find(|e| e.domain == name) - .unwrap_or_else(|| panic!("no registry entry for {name}")); + .find(|e| e.domain() == name) + .unwrap_or_else(|| panic!("no domain inventory entry for {name}")); let keys: Vec<&str> = ENVELOPE_KEYS .iter() @@ -69,25 +69,26 @@ fn required_keys_match_catalog_terms() { .map(|k| (k.to_string(), synthesize(k))) .collect::>() .into(); - let round_tripped = (entry.roundtrip)(full.clone()).unwrap_or_else(|e| { + let round_tripped = entry.roundtrip(full.clone()).unwrap_or_else(|e| { panic!( "{name} ({}): catalog payload rejected: {e}", - entry.type_name + entry.type_name() ) }); assert_eq!( - round_tripped, full, + round_tripped, + full, "{name} ({}): round-trip must be identity over the catalog keys", - entry.type_name + entry.type_name() ); for key in &keys { let mut partial = full.clone(); partial.as_object_mut().unwrap().remove(*key); assert!( - (entry.roundtrip)(partial).is_err(), + entry.roundtrip(partial).is_err(), "{name} ({}): must reject payload missing required key {key:?}", - entry.type_name + entry.type_name() ); } @@ -97,9 +98,9 @@ fn required_keys_match_catalog_terms() { .unwrap() .insert("zz".into(), json!(true)); assert!( - (entry.roundtrip)(extra).is_err(), + entry.roundtrip(extra).is_err(), "{name} ({}): must reject payload carrying an unknown key", - entry.type_name + entry.type_name() ); let mut wrong_version = full.clone(); @@ -108,9 +109,9 @@ fn required_keys_match_catalog_terms() { .unwrap() .insert("v".into(), json!(3)); assert!( - (entry.roundtrip)(wrong_version).is_err(), + entry.roundtrip(wrong_version).is_err(), "{name} ({}): must reject envelope version other than 2", - entry.type_name + entry.type_name() ); } } From 7777853e2dfea8d3ed7c077710aa8cc544b997aa Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 14:18:07 +1000 Subject: [PATCH 08/12] refactor(eql-types): drop roundtrip from DomainType; slim the parity gate to inventory order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The behavioural required-keys test was the only roundtrip consumer; that gate is schema-based in the stacked schemars change (schemars required reflects the serde contract), with per-type strictness spot checks in v3_conformance. serde_json moves to dev-dependencies — the lib no longer touches Value. --- crates/eql-types/Cargo.toml | 8 +- crates/eql-types/README.md | 11 ++- crates/eql-types/src/v3/mod.rs | 25 ++--- crates/eql-types/tests/catalog_parity.rs | 115 +++-------------------- 4 files changed, 30 insertions(+), 129 deletions(-) diff --git a/crates/eql-types/Cargo.toml b/crates/eql-types/Cargo.toml index d37a417a..d01b6a0a 100644 --- a/crates/eql-types/Cargo.toml +++ b/crates/eql-types/Cargo.toml @@ -6,10 +6,10 @@ description = "Canonical wire types for EQL payloads — the single Rust source [dependencies] serde = { version = "1", features = ["derive"] } -serde_json = "1" [dev-dependencies] -# Parity oracle: tests/catalog_parity.rs asserts the v3 registry exactly -# covers eql_scalars::CATALOG, so the types here cannot drift from the -# generated SQL surface. +# Parity oracle: tests/catalog_parity.rs asserts the v3 domain inventory +# exactly covers eql_scalars::CATALOG, so the types here cannot drift from +# the generated SQL surface. eql-scalars = { path = "../eql-scalars" } +serde_json = "1" diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index 447c2613..f3256e16 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -46,11 +46,12 @@ field names are unchanged from v2 (the purpose-named rename in `tests/catalog_parity.rs` asserts the domain inventory — [`v3::all()`](src/v3/mod.rs), a `Vec>` of zero-sized -type-level handles — exactly covers `eql-scalars::CATALOG` — the same catalog that generates the -`eql_v3` SQL surface — every domain, in order, and proves behaviourally that -each type's wire keys are exactly the envelope (`v`, `i`, `c`) plus the -catalog's term keys. Adding a scalar to the catalog without adding its types -here fails the build; so does accidentally making a term field `Option`. +type-level handles — exactly covers `eql-scalars::CATALOG` (the same catalog +that generates the `eql_v3` SQL surface): every domain, in order. Adding a +scalar to the catalog without adding its types here fails the build. +Wire-key strictness (required term keys, unknown-key rejection, envelope +version) is covered per-type in `tests/v3_conformance.rs` and pinned against +the catalog by the JSON Schema parity test in the stacked schemars change. ## Develop diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 3625831d..1ba085e5 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -5,7 +5,10 @@ //! (PR #236's first cut), formalized: //! the SQL surface is generated from `eql-scalars::CATALOG`, and these types //! mirror it 1:1 (enforced by `tests/catalog_parity.rs`, which fails if the -//! catalog and this module ever disagree on domains or required wire keys). +//! catalog and [`all`] ever disagree on the set or order of domains; the +//! catalog-derived wire-key gate is schema-based and lands with the stacked +//! schemars change, with per-type strictness spot checks in +//! `tests/v3_conformance.rs`). //! //! **Versioning.** "v3" is the SQL schema generation (`eql_v3.*` domains). //! The JSON envelope version is still `v: 2` ([`crate::EQL_SCHEMA_VERSION`]) — @@ -43,8 +46,6 @@ use std::marker::PhantomData; -use serde::{de::DeserializeOwned, Serialize}; - pub mod date; pub mod int2; pub mod int4; @@ -73,10 +74,10 @@ pub trait V3Domain { /// /// Implemented once, by the blanket impl below, for `PhantomData` over /// every payload type: a `Box` is a zero-sized type-level -/// handle, not a payload instance. (The trait cannot be [`V3Domain`] itself: -/// an associated const is not object-safe, and [`Self::roundtrip`] needs -/// `Deserialize`, which is `Sized`-only — so the dyn surface lives on the -/// handle, and `V3Domain` stays the compile-time anchor it reads from.) +/// handle, not a payload instance (there are no payload instances to box; +/// the trait cannot be [`V3Domain`] itself because an associated const is +/// not object-safe — so the dyn surface lives on the handle, and `V3Domain` +/// stays the one-line-per-type anchor it derives from). /// /// Consumed by `tests/catalog_parity.rs` (which asserts [`all`] exactly /// covers `eql-scalars::CATALOG`, so the list cannot silently go stale) and @@ -97,14 +98,11 @@ pub trait DomainType { /// The Rust type's full path (via `std::any::type_name`). fn type_name(&self) -> &'static str; - - /// serde round-trip through the concrete type (`Value` → `T` → `Value`). - fn roundtrip(&self, value: serde_json::Value) -> Result; } impl DomainType for PhantomData where - T: V3Domain + DeserializeOwned + Serialize, + T: V3Domain, { fn sql_domain(&self) -> &'static str { T::SQL_DOMAIN @@ -113,11 +111,6 @@ where fn type_name(&self) -> &'static str { std::any::type_name::() } - - fn roundtrip(&self, value: serde_json::Value) -> Result { - let parsed: T = serde_json::from_value(value)?; - serde_json::to_value(&parsed) - } } /// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index 376822bf..94ff8eb7 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -1,27 +1,15 @@ -//! The drift gate: the v3 domain inventory must mirror `eql-scalars::CATALOG` — the -//! same catalog that generates the `eql_v3` SQL surface — exactly. Append a -//! scalar to the catalog without adding its types here and the first test -//! fails; let a term field become `Option` (or carry the wrong wire key) and -//! the second fails, because it exercises the real serde contract: a payload -//! carrying exactly the catalog's keys must round-trip identically, and -//! removing any one of them must be a deserialization error. - -use eql_scalars::{Term, CATALOG, ENVELOPE_KEYS}; +//! The drift gate: the v3 domain inventory must mirror `eql-scalars::CATALOG` +//! — the same catalog that generates the `eql_v3` SQL surface — exactly: +//! every domain, in catalog order. Append a scalar to the catalog without +//! adding its types (and their `all()` entries) and this fails. +//! +//! Wire-key strictness (required term keys, unknown-key rejection, envelope +//! version) is covered behaviourally per-type in `tests/v3_conformance.rs`, +//! and pinned against the catalog by the JSON Schema parity test in the +//! stacked schemars change. + +use eql_scalars::CATALOG; use eql_types::v3; -use serde_json::{json, Value}; - -/// A synthetic wire value for a required key, by key name. -fn synthesize(key: &str) -> Value { - match key { - "v" => json!(2), - "i" => json!({ "t": "users", "c": "field" }), - "c" => json!("mp_base85_ciphertext"), - "hm" => json!("deadbeef"), - "ob" => json!(["ore_block_0", "ore_block_1"]), - "bf" => json!([-1, 0, 32767]), - other => panic!("no synthetic value for unexpected catalog key {other:?}"), - } -} #[test] fn inventory_exactly_covers_catalog() { @@ -35,84 +23,3 @@ fn inventory_exactly_covers_catalog() { "v3::all() must list every CATALOG domain, in catalog order" ); } - -/// Every domain's wire keys are exactly envelope + catalog terms, proven -/// behaviourally through serde: -/// -/// - a payload carrying exactly those keys round-trips **identically**, so -/// the type requires nothing more and emits nothing less; -/// - removing any one key fails deserialization, so every key is required -/// (no `Option` has crept in); -/// - adding a key outside the set fails deserialization -/// (`deny_unknown_fields` — no silent stripping on re-serialize); -/// - a wrong envelope version fails deserialization (`SchemaVersion` -/// mirrors the domain CHECK's `VALUE->>'v' = '2'`). -#[test] -fn required_keys_match_catalog_terms() { - let entries = v3::all(); - for spec in CATALOG { - for domain in spec.domains { - let name = spec.domain_name(domain); - let entry = entries - .iter() - .find(|e| e.domain() == name) - .unwrap_or_else(|| panic!("no domain inventory entry for {name}")); - - let keys: Vec<&str> = ENVELOPE_KEYS - .iter() - .copied() - .chain(Term::term_json_keys(domain.terms)) - .collect(); - - let full: Value = keys - .iter() - .map(|k| (k.to_string(), synthesize(k))) - .collect::>() - .into(); - let round_tripped = entry.roundtrip(full.clone()).unwrap_or_else(|e| { - panic!( - "{name} ({}): catalog payload rejected: {e}", - entry.type_name() - ) - }); - assert_eq!( - round_tripped, - full, - "{name} ({}): round-trip must be identity over the catalog keys", - entry.type_name() - ); - - for key in &keys { - let mut partial = full.clone(); - partial.as_object_mut().unwrap().remove(*key); - assert!( - entry.roundtrip(partial).is_err(), - "{name} ({}): must reject payload missing required key {key:?}", - entry.type_name() - ); - } - - let mut extra = full.clone(); - extra - .as_object_mut() - .unwrap() - .insert("zz".into(), json!(true)); - assert!( - entry.roundtrip(extra).is_err(), - "{name} ({}): must reject payload carrying an unknown key", - entry.type_name() - ); - - let mut wrong_version = full.clone(); - wrong_version - .as_object_mut() - .unwrap() - .insert("v".into(), json!(3)); - assert!( - entry.roundtrip(wrong_version).is_err(), - "{name} ({}): must reject envelope version other than 2", - entry.type_name() - ); - } - } -} From b810fadbbad01e34ab7edb9594108deae5f8a69d Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 14:27:11 +1000 Subject: [PATCH 09/12] refactor(eql-types): implement DomainType per type; drop the V3Domain const trait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sql_domain() is implemented directly on each type's PhantomData handle in its token file — the domain string still has exactly one definition per type, and the catalog parity test still catches a typo'd or mis-ordered domain. The const-trait + blanket-impl indirection (and type_name, which only test messages used) is gone. --- crates/eql-types/src/v3/date.rs | 28 +++++++++---- crates/eql-types/src/v3/int2.rs | 28 +++++++++---- crates/eql-types/src/v3/int4.rs | 28 +++++++++---- crates/eql-types/src/v3/int8.rs | 28 +++++++++---- crates/eql-types/src/v3/mod.rs | 52 +++++------------------- crates/eql-types/src/v3/text.rs | 34 +++++++++++----- crates/eql-types/src/v3/timestamptz.rs | 16 +++++--- crates/eql-types/tests/v3_conformance.rs | 12 ++++-- 8 files changed, 128 insertions(+), 98 deletions(-) diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index 721e099d..4fcfd878 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -3,8 +3,10 @@ //! ciphertext, so dates order like integers); see that module for the //! capability table. +use std::marker::PhantomData; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -21,8 +23,10 @@ pub struct Date { pub c: Ciphertext, } -impl V3Domain for Date { - const SQL_DOMAIN: &'static str = "eql_v3.date"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.date" + } } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). @@ -40,8 +44,10 @@ pub struct DateEq { pub hm: Hmac256, } -impl V3Domain for DateEq { - const SQL_DOMAIN: &'static str = "eql_v3.date_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.date_eq" + } } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. @@ -59,8 +65,10 @@ pub struct DateOrdOre { pub ob: OreBlockU64_8_256, } -impl V3Domain for DateOrdOre { - const SQL_DOMAIN: &'static str = "eql_v3.date_ord_ore"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.date_ord_ore" + } } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). @@ -78,6 +86,8 @@ pub struct DateOrd { pub ob: OreBlockU64_8_256, } -impl V3Domain for DateOrd { - const SQL_DOMAIN: &'static str = "eql_v3.date_ord"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.date_ord" + } } diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index 7e0b1029..9a365d21 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -1,8 +1,10 @@ //! The `int2` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. +use std::marker::PhantomData; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -19,8 +21,10 @@ pub struct Int2 { pub c: Ciphertext, } -impl V3Domain for Int2 { - const SQL_DOMAIN: &'static str = "eql_v3.int2"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int2" + } } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). @@ -38,8 +42,10 @@ pub struct Int2Eq { pub hm: Hmac256, } -impl V3Domain for Int2Eq { - const SQL_DOMAIN: &'static str = "eql_v3.int2_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int2_eq" + } } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. @@ -57,8 +63,10 @@ pub struct Int2OrdOre { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int2OrdOre { - const SQL_DOMAIN: &'static str = "eql_v3.int2_ord_ore"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int2_ord_ore" + } } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). @@ -76,6 +84,8 @@ pub struct Int2Ord { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int2Ord { - const SQL_DOMAIN: &'static str = "eql_v3.int2_ord"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int2_ord" + } } diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index 7f2cb57c..f2737706 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -7,8 +7,10 @@ //! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | +use std::marker::PhantomData; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -25,8 +27,10 @@ pub struct Int4 { pub c: Ciphertext, } -impl V3Domain for Int4 { - const SQL_DOMAIN: &'static str = "eql_v3.int4"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int4" + } } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). @@ -44,8 +48,10 @@ pub struct Int4Eq { pub hm: Hmac256, } -impl V3Domain for Int4Eq { - const SQL_DOMAIN: &'static str = "eql_v3.int4_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int4_eq" + } } /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), @@ -65,8 +71,10 @@ pub struct Int4OrdOre { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int4OrdOre { - const SQL_DOMAIN: &'static str = "eql_v3.int4_ord_ore"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int4_ord_ore" + } } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). @@ -84,6 +92,8 @@ pub struct Int4Ord { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int4Ord { - const SQL_DOMAIN: &'static str = "eql_v3.int4_ord"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int4_ord" + } } diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 6d3f4516..721c50ce 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -1,8 +1,10 @@ //! The `int8` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. +use std::marker::PhantomData; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -19,8 +21,10 @@ pub struct Int8 { pub c: Ciphertext, } -impl V3Domain for Int8 { - const SQL_DOMAIN: &'static str = "eql_v3.int8"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int8" + } } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). @@ -38,8 +42,10 @@ pub struct Int8Eq { pub hm: Hmac256, } -impl V3Domain for Int8Eq { - const SQL_DOMAIN: &'static str = "eql_v3.int8_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int8_eq" + } } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. @@ -57,8 +63,10 @@ pub struct Int8OrdOre { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int8OrdOre { - const SQL_DOMAIN: &'static str = "eql_v3.int8_ord_ore"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int8_ord_ore" + } } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). @@ -76,6 +84,8 @@ pub struct Int8Ord { pub ob: OreBlockU64_8_256, } -impl V3Domain for Int8Ord { - const SQL_DOMAIN: &'static str = "eql_v3.int8_ord"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.int8_ord" + } } diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 1ba085e5..fe4220f8 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -57,32 +57,16 @@ pub mod timestamptz; /// The PostgreSQL schema every domain in this module inhabits. pub const SQL_SCHEMA: &str = "eql_v3"; -/// Implemented by every v3 domain payload type: the fully-qualified SQL -/// domain the payload inhabits (e.g. `"eql_v3.int4_eq"`). +/// One v3 domain type — what [`all`] enumerates. /// -/// The [`DomainType`] blanket impl derives everything else from this -/// constant, so the type ↔ domain binding has exactly one definition per -/// type — there is no second string to keep in sync, and two same-shaped -/// types (`_ord` vs `_ord_ore`) cannot be enumerated under each other's -/// domain. -pub trait V3Domain { - /// Fully-qualified SQL domain, e.g. `"eql_v3.int4_eq"`. - const SQL_DOMAIN: &'static str; -} - -/// Object-safe view of one v3 domain type — what [`all`] enumerates. -/// -/// Implemented once, by the blanket impl below, for `PhantomData` over -/// every payload type: a `Box` is a zero-sized type-level -/// handle, not a payload instance (there are no payload instances to box; -/// the trait cannot be [`V3Domain`] itself because an associated const is -/// not object-safe — so the dyn surface lives on the handle, and `V3Domain` -/// stays the one-line-per-type anchor it derives from). -/// -/// Consumed by `tests/catalog_parity.rs` (which asserts [`all`] exactly -/// covers `eql-scalars::CATALOG`, so the list cannot silently go stale) and -/// by the binding/schema exporters added in stacked changes. Public so FFI -/// consumers can enumerate the protocol surface too. +/// Each token file implements this for `PhantomData` next to the payload +/// type `T` it describes, e.g. `impl DomainType for PhantomData`: +/// a `Box` is a zero-sized type-level handle, not a payload +/// instance (payload types have no instances to box, so the dyn surface +/// lives on the handle). The SQL domain string is defined exactly once, in +/// that impl, and `tests/catalog_parity.rs` cross-checks every handle +/// against `eql-scalars::CATALOG` — a typo'd or mis-ordered domain fails +/// there. Public so FFI consumers can enumerate the protocol surface too. pub trait DomainType { /// Fully-qualified SQL domain name, e.g. `"eql_v3.int4_eq"`. fn sql_domain(&self) -> &'static str; @@ -93,23 +77,7 @@ pub trait DomainType { fn domain(&self) -> &'static str { self.sql_domain() .strip_prefix("eql_v3.") - .expect("SQL_DOMAIN must be qualified with the eql_v3 schema") - } - - /// The Rust type's full path (via `std::any::type_name`). - fn type_name(&self) -> &'static str; -} - -impl DomainType for PhantomData -where - T: V3Domain, -{ - fn sql_domain(&self) -> &'static str { - T::SQL_DOMAIN - } - - fn type_name(&self) -> &'static str { - std::any::type_name::() + .expect("sql_domain must be qualified with the eql_v3 schema") } } diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index ef4d9b14..00f61f65 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -2,8 +2,10 @@ //! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter //! term (`@>`/`<@` containment for `LIKE`-style matching). +use std::marker::PhantomData; + use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -20,8 +22,10 @@ pub struct Text { pub c: Ciphertext, } -impl V3Domain for Text { - const SQL_DOMAIN: &'static str = "eql_v3.text"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.text" + } } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). @@ -39,8 +43,10 @@ pub struct TextEq { pub hm: Hmac256, } -impl V3Domain for TextEq { - const SQL_DOMAIN: &'static str = "eql_v3.text_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.text_eq" + } } /// `eql_v3.text_match` — Bloom-filter containment match. @@ -58,8 +64,10 @@ pub struct TextMatch { pub bf: BloomFilter, } -impl V3Domain for TextMatch { - const SQL_DOMAIN: &'static str = "eql_v3.text_match"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.text_match" + } } /// `eql_v3.text_ord_ore` — full lexicographic comparison, @@ -78,8 +86,10 @@ pub struct TextOrdOre { pub ob: OreBlockU64_8_256, } -impl V3Domain for TextOrdOre { - const SQL_DOMAIN: &'static str = "eql_v3.text_ord_ore"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.text_ord_ore" + } } /// `eql_v3.text_ord` — full lexicographic comparison @@ -98,6 +108,8 @@ pub struct TextOrd { pub ob: OreBlockU64_8_256, } -impl V3Domain for TextOrd { - const SQL_DOMAIN: &'static str = "eql_v3.text_ord"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.text_ord" + } } diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 79f2c8c0..9cd7544c 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -4,8 +4,10 @@ //! 8 blocks, so an ordered timestamptz domain would silently mis-order. //! Ordering arrives with a future wide-ORE term (see `eql-scalars`). +use std::marker::PhantomData; + use crate::v3::terms::{Ciphertext, Hmac256}; -use crate::v3::V3Domain; +use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -22,8 +24,10 @@ pub struct Timestamptz { pub c: Ciphertext, } -impl V3Domain for Timestamptz { - const SQL_DOMAIN: &'static str = "eql_v3.timestamptz"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.timestamptz" + } } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). @@ -41,6 +45,8 @@ pub struct TimestamptzEq { pub hm: Hmac256, } -impl V3Domain for TimestamptzEq { - const SQL_DOMAIN: &'static str = "eql_v3.timestamptz_eq"; +impl DomainType for PhantomData { + fn sql_domain(&self) -> &'static str { + "eql_v3.timestamptz_eq" + } } diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index 7896231f..48346f35 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -5,8 +5,9 @@ use eql_types::v3::int4::{Int4, Int4Eq, Int4Ord, Int4OrdOre}; use eql_types::v3::text::TextMatch; -use eql_types::v3::V3Domain; +use eql_types::v3::DomainType; use serde_json::json; +use std::marker::PhantomData; #[test] fn int4_storage_round_trips() { @@ -17,7 +18,7 @@ fn int4_storage_round_trips() { }); let parsed: Int4 = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!(Int4::SQL_DOMAIN, "eql_v3.int4"); + assert_eq!(PhantomData::.sql_domain(), "eql_v3.int4"); } #[test] @@ -30,7 +31,7 @@ fn int4_eq_round_trips() { }); let parsed: Int4Eq = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!(Int4Eq::SQL_DOMAIN, "eql_v3.int4_eq"); + assert_eq!(PhantomData::.sql_domain(), "eql_v3.int4_eq"); } #[test] @@ -46,7 +47,10 @@ fn int4_ord_round_trips() { // `_ord_ore` is the same shape under the scheme-explicit domain name. let parsed: Int4OrdOre = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!(Int4OrdOre::SQL_DOMAIN, "eql_v3.int4_ord_ore"); + assert_eq!( + PhantomData::.sql_domain(), + "eql_v3.int4_ord_ore" + ); } #[test] From fd5e626316461c8505c4c6674849dec0fe62fa20 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 14:56:55 +1000 Subject: [PATCH 10/12] refactor(eql-types): implement DomainType on the payload types themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumers holding a payload value can now ask it for its SQL domain directly (payload.sql_domain()). The Box inventory keeps its zero-sized PhantomData handles via one blanket impl that delegates through a transient T::default() — allocation-free, since every field defaults to an empty string/vec. Default exists on the payload types only to power that; a default payload is structurally complete but semantically empty. --- crates/eql-types/src/lib.rs | 2 +- crates/eql-types/src/v3/date.rs | 18 +++++++-------- crates/eql-types/src/v3/int2.rs | 18 +++++++-------- crates/eql-types/src/v3/int4.rs | 18 +++++++-------- crates/eql-types/src/v3/int8.rs | 18 +++++++-------- crates/eql-types/src/v3/mod.rs | 32 ++++++++++++++++++-------- crates/eql-types/src/v3/terms.rs | 8 +++---- crates/eql-types/src/v3/text.rs | 22 ++++++++---------- crates/eql-types/src/v3/timestamptz.rs | 10 ++++---- 9 files changed, 74 insertions(+), 72 deletions(-) diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index 9232bdd9..4a2c9b85 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -68,7 +68,7 @@ impl<'de> Deserialize<'de> for SchemaVersion { /// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. /// /// Shared by every payload. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Identifier { /// Table name. diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index 4fcfd878..5c07010e 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -3,15 +3,13 @@ //! ciphertext, so dates order like integers); see that module for the //! capability table. -use std::marker::PhantomData; - use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.date` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Date { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -23,14 +21,14 @@ pub struct Date { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Date { fn sql_domain(&self) -> &'static str { "eql_v3.date" } } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -44,14 +42,14 @@ pub struct DateEq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for DateEq { fn sql_domain(&self) -> &'static str { "eql_v3.date_eq" } } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -65,14 +63,14 @@ pub struct DateOrdOre { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for DateOrdOre { fn sql_domain(&self) -> &'static str { "eql_v3.date_ord_ore" } } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -86,7 +84,7 @@ pub struct DateOrd { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for DateOrd { fn sql_domain(&self) -> &'static str { "eql_v3.date_ord" } diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index 9a365d21..b31dee01 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -1,15 +1,13 @@ //! The `int2` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use std::marker::PhantomData; - use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int2` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -21,14 +19,14 @@ pub struct Int2 { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Int2 { fn sql_domain(&self) -> &'static str { "eql_v3.int2" } } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -42,14 +40,14 @@ pub struct Int2Eq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for Int2Eq { fn sql_domain(&self) -> &'static str { "eql_v3.int2_eq" } } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -63,14 +61,14 @@ pub struct Int2OrdOre { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int2OrdOre { fn sql_domain(&self) -> &'static str { "eql_v3.int2_ord_ore" } } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -84,7 +82,7 @@ pub struct Int2Ord { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int2Ord { fn sql_domain(&self) -> &'static str { "eql_v3.int2_ord" } diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index f2737706..9533d17c 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -7,15 +7,13 @@ //! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | -use std::marker::PhantomData; - use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int4` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -27,14 +25,14 @@ pub struct Int4 { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Int4 { fn sql_domain(&self) -> &'static str { "eql_v3.int4" } } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -48,7 +46,7 @@ pub struct Int4Eq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for Int4Eq { fn sql_domain(&self) -> &'static str { "eql_v3.int4_eq" } @@ -56,7 +54,7 @@ impl DomainType for PhantomData { /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), /// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -71,14 +69,14 @@ pub struct Int4OrdOre { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int4OrdOre { fn sql_domain(&self) -> &'static str { "eql_v3.int4_ord_ore" } } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -92,7 +90,7 @@ pub struct Int4Ord { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int4Ord { fn sql_domain(&self) -> &'static str { "eql_v3.int4_ord" } diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 721c50ce..97d44590 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -1,15 +1,13 @@ //! The `int8` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use std::marker::PhantomData; - use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int8` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -21,14 +19,14 @@ pub struct Int8 { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Int8 { fn sql_domain(&self) -> &'static str { "eql_v3.int8" } } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -42,14 +40,14 @@ pub struct Int8Eq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for Int8Eq { fn sql_domain(&self) -> &'static str { "eql_v3.int8_eq" } } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -63,14 +61,14 @@ pub struct Int8OrdOre { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int8OrdOre { fn sql_domain(&self) -> &'static str { "eql_v3.int8_ord_ore" } } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -84,7 +82,7 @@ pub struct Int8Ord { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for Int8Ord { fn sql_domain(&self) -> &'static str { "eql_v3.int8_ord" } diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index fe4220f8..62afaabe 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -57,16 +57,14 @@ pub mod timestamptz; /// The PostgreSQL schema every domain in this module inhabits. pub const SQL_SCHEMA: &str = "eql_v3"; -/// One v3 domain type — what [`all`] enumerates. +/// One v3 domain type — implemented by every payload type, so any payload +/// value can report the SQL domain it inhabits (`payload.sql_domain()`). /// -/// Each token file implements this for `PhantomData` next to the payload -/// type `T` it describes, e.g. `impl DomainType for PhantomData`: -/// a `Box` is a zero-sized type-level handle, not a payload -/// instance (payload types have no instances to box, so the dyn surface -/// lives on the handle). The SQL domain string is defined exactly once, in -/// that impl, and `tests/catalog_parity.rs` cross-checks every handle -/// against `eql-scalars::CATALOG` — a typo'd or mis-ordered domain fails -/// there. Public so FFI consumers can enumerate the protocol surface too. +/// Each token file implements this next to the type it describes; the SQL +/// domain string is defined exactly once, in that impl, and +/// `tests/catalog_parity.rs` cross-checks every entry of [`all`] against +/// `eql-scalars::CATALOG` — a typo'd or mis-ordered domain fails there. +/// Public so FFI consumers can enumerate the protocol surface too. pub trait DomainType { /// Fully-qualified SQL domain name, e.g. `"eql_v3.int4_eq"`. fn sql_domain(&self) -> &'static str; @@ -81,6 +79,22 @@ pub trait DomainType { } } +/// Type-level handle: lets [`all`] enumerate the domain types without +/// payload values to box — `Box::new(PhantomData::)` is zero-sized. +/// +/// Delegates through a transient `T::default()`, which is allocation-free +/// (every field defaults to an empty string/vec). `Default` exists on the +/// payload types only to power this: a default payload is structurally +/// complete but semantically empty — never serialize one. +impl DomainType for PhantomData +where + T: DomainType + Default, +{ + fn sql_domain(&self) -> &'static str { + T::default().sql_domain() + } +} + /// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then /// each token's domains in manifest order) — the one hand-maintained list of /// types in the crate. diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index ddad74bf..6ee36b22 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -15,19 +15,19 @@ use serde::{Deserialize, Serialize}; /// mp_base85 source ciphertext — the `c` envelope key. /// /// Required by every v3 domain CHECK; present on every payload. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Ciphertext(pub String); /// HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains /// (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Hmac256(pub String); /// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the /// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless /// over the scalar's domain, so it serves equality too. SQL-side constructor: /// `eql_v3.ore_block_u64_8_256`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct OreBlockU64_8_256(pub Vec); /// Bloom-filter match term — the `bf` wire key. Backs the `_match` domains @@ -36,7 +36,7 @@ pub struct OreBlockU64_8_256(pub Vec); /// **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, /// and filters sized above 32768 emit upper-half bit positions as negative /// signed values. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct BloomFilter(pub Vec); impl From for Ciphertext { diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index 00f61f65..16ecd60c 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -2,15 +2,13 @@ //! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter //! term (`@>`/`<@` containment for `LIKE`-style matching). -use std::marker::PhantomData; - use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.text` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Text { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -22,14 +20,14 @@ pub struct Text { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Text { fn sql_domain(&self) -> &'static str { "eql_v3.text" } } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -43,14 +41,14 @@ pub struct TextEq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for TextEq { fn sql_domain(&self) -> &'static str { "eql_v3.text_eq" } } /// `eql_v3.text_match` — Bloom-filter containment match. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextMatch { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -64,7 +62,7 @@ pub struct TextMatch { pub bf: BloomFilter, } -impl DomainType for PhantomData { +impl DomainType for TextMatch { fn sql_domain(&self) -> &'static str { "eql_v3.text_match" } @@ -72,7 +70,7 @@ impl DomainType for PhantomData { /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -86,7 +84,7 @@ pub struct TextOrdOre { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for TextOrdOre { fn sql_domain(&self) -> &'static str { "eql_v3.text_ord_ore" } @@ -94,7 +92,7 @@ impl DomainType for PhantomData { /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -108,7 +106,7 @@ pub struct TextOrd { pub ob: OreBlockU64_8_256, } -impl DomainType for PhantomData { +impl DomainType for TextOrd { fn sql_domain(&self) -> &'static str { "eql_v3.text_ord" } diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 9cd7544c..628c35f6 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -4,15 +4,13 @@ //! 8 blocks, so an ordered timestamptz domain would silently mis-order. //! Ordering arrives with a future wide-ORE term (see `eql-scalars`). -use std::marker::PhantomData; - use crate::v3::terms::{Ciphertext, Hmac256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.timestamptz` — storage only; every operator is blocked. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Timestamptz { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -24,14 +22,14 @@ pub struct Timestamptz { pub c: Ciphertext, } -impl DomainType for PhantomData { +impl DomainType for Timestamptz { fn sql_domain(&self) -> &'static str { "eql_v3.timestamptz" } } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TimestamptzEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -45,7 +43,7 @@ pub struct TimestamptzEq { pub hm: Hmac256, } -impl DomainType for PhantomData { +impl DomainType for TimestamptzEq { fn sql_domain(&self) -> &'static str { "eql_v3.timestamptz_eq" } From 035952e13fafc87c8a3c89fc7a7ff5447597bdd4 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 16:36:35 +1000 Subject: [PATCH 11/12] fix(eql-types): drop Default from payload types; power handles via a Sized-bounded static Review finding: Int4Eq::default() was a constructible, serializable payload (v:2, empty i/c/hm) that passes the generated domain CHECK - and empty hm terms all match each other under encrypted equality. The PhantomData blanket impl now delegates through sql_domain_static() (excluded from the vtable by `where Self: Sized`, so the trait stays object-safe) and no payload instance is ever constructed. Default comes off the 23 payload types, the term newtypes, and Identifier; SchemaVersion keeps its CURRENT-valued Default. --- crates/eql-types/src/lib.rs | 2 +- crates/eql-types/src/v3/date.rs | 32 ++++++++++++++----- crates/eql-types/src/v3/int2.rs | 32 ++++++++++++++----- crates/eql-types/src/v3/int4.rs | 32 ++++++++++++++----- crates/eql-types/src/v3/int8.rs | 32 ++++++++++++++----- crates/eql-types/src/v3/mod.rs | 30 ++++++++++++------ crates/eql-types/src/v3/terms.rs | 8 ++--- crates/eql-types/src/v3/text.rs | 40 ++++++++++++++++++------ crates/eql-types/src/v3/timestamptz.rs | 16 +++++++--- crates/eql-types/tests/v3_conformance.rs | 10 ++---- 10 files changed, 167 insertions(+), 67 deletions(-) diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index 4a2c9b85..9232bdd9 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -68,7 +68,7 @@ impl<'de> Deserialize<'de> for SchemaVersion { /// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. /// /// Shared by every payload. -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Identifier { /// Table name. diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index 5c07010e..00cb706a 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -9,7 +9,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.date` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Date { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -22,13 +22,17 @@ pub struct Date { } impl DomainType for Date { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.date" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -43,13 +47,17 @@ pub struct DateEq { } impl DomainType for DateEq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.date_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -64,13 +72,17 @@ pub struct DateOrdOre { } impl DomainType for DateOrdOre { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.date_ord_ore" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DateOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -85,7 +97,11 @@ pub struct DateOrd { } impl DomainType for DateOrd { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.date_ord" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index b31dee01..b641408d 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -7,7 +7,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int2` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -20,13 +20,17 @@ pub struct Int2 { } impl DomainType for Int2 { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int2" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -41,13 +45,17 @@ pub struct Int2Eq { } impl DomainType for Int2Eq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int2_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -62,13 +70,17 @@ pub struct Int2OrdOre { } impl DomainType for Int2OrdOre { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int2_ord_ore" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int2Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -83,7 +95,11 @@ pub struct Int2Ord { } impl DomainType for Int2Ord { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int2_ord" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index 9533d17c..44dbcf34 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -13,7 +13,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int4` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -26,13 +26,17 @@ pub struct Int4 { } impl DomainType for Int4 { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int4" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -47,14 +51,18 @@ pub struct Int4Eq { } impl DomainType for Int4Eq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int4_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int4_ord_ore` — full comparison (`=` `<>` `<` `<=` `>` `>=`), /// scheme-explicit name. Same shape as [`Int4Ord`], distinct SQL domain. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -70,13 +78,17 @@ pub struct Int4OrdOre { } impl DomainType for Int4OrdOre { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int4_ord_ore" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int4Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -91,7 +103,11 @@ pub struct Int4Ord { } impl DomainType for Int4Ord { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int4_ord" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 97d44590..4ab0a232 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -7,7 +7,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.int8` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8 { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -20,13 +20,17 @@ pub struct Int8 { } impl DomainType for Int8 { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int8" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8Eq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -41,13 +45,17 @@ pub struct Int8Eq { } impl DomainType for Int8Eq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int8_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8OrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -62,13 +70,17 @@ pub struct Int8OrdOre { } impl DomainType for Int8OrdOre { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int8_ord_ore" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Int8Ord { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -83,7 +95,11 @@ pub struct Int8Ord { } impl DomainType for Int8Ord { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.int8_ord" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 62afaabe..97262778 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -66,7 +66,18 @@ pub const SQL_SCHEMA: &str = "eql_v3"; /// `eql-scalars::CATALOG` — a typo'd or mis-ordered domain fails there. /// Public so FFI consumers can enumerate the protocol surface too. pub trait DomainType { - /// Fully-qualified SQL domain name, e.g. `"eql_v3.int4_eq"`. + /// Fully-qualified SQL domain name, e.g. `"eql_v3.int4_eq"` — the + /// per-type fact everything else derives from, defined once in each + /// type's impl. + /// + /// `where Self: Sized` keeps the trait object-safe (the method is + /// excluded from the vtable); through `dyn DomainType`, use + /// [`Self::sql_domain`]. + fn sql_domain_static() -> &'static str + where + Self: Sized; + + /// Fully-qualified SQL domain name of this payload value. fn sql_domain(&self) -> &'static str; /// Unqualified SQL domain name (e.g. `"int4_eq"`) — [`Self::sql_domain`] @@ -80,18 +91,19 @@ pub trait DomainType { } /// Type-level handle: lets [`all`] enumerate the domain types without -/// payload values to box — `Box::new(PhantomData::)` is zero-sized. -/// -/// Delegates through a transient `T::default()`, which is allocation-free -/// (every field defaults to an empty string/vec). `Default` exists on the -/// payload types only to power this: a default payload is structurally -/// complete but semantically empty — never serialize one. +/// payload values to box — `Box::new(PhantomData::)` is zero-sized, +/// and the delegation goes through [`DomainType::sql_domain_static`], so no +/// payload instance is ever constructed. impl DomainType for PhantomData where - T: DomainType + Default, + T: DomainType, { + fn sql_domain_static() -> &'static str { + T::sql_domain_static() + } + fn sql_domain(&self) -> &'static str { - T::default().sql_domain() + T::sql_domain_static() } } diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index 6ee36b22..ddad74bf 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -15,19 +15,19 @@ use serde::{Deserialize, Serialize}; /// mp_base85 source ciphertext — the `c` envelope key. /// /// Required by every v3 domain CHECK; present on every payload. -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Ciphertext(pub String); /// HMAC-SHA-256 equality term — the `hm` wire key. Backs the `_eq` domains /// (`=`, `<>`). SQL-side constructor: `eql_v3.hmac_256`. -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Hmac256(pub String); /// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the /// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless /// over the scalar's domain, so it serves equality too. SQL-side constructor: /// `eql_v3.ore_block_u64_8_256`. -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct OreBlockU64_8_256(pub Vec); /// Bloom-filter match term — the `bf` wire key. Backs the `_match` domains @@ -36,7 +36,7 @@ pub struct OreBlockU64_8_256(pub Vec); /// **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, /// and filters sized above 32768 emit upper-half bit positions as negative /// signed values. -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct BloomFilter(pub Vec); impl From for Ciphertext { diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index 16ecd60c..9e11fc4d 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -8,7 +8,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.text` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Text { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -21,13 +21,17 @@ pub struct Text { } impl DomainType for Text { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.text" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -42,13 +46,17 @@ pub struct TextEq { } impl DomainType for TextEq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.text_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.text_match` — Bloom-filter containment match. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextMatch { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -63,14 +71,18 @@ pub struct TextMatch { } impl DomainType for TextMatch { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.text_match" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextOrdOre { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -85,14 +97,18 @@ pub struct TextOrdOre { } impl DomainType for TextOrdOre { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.text_ord_ore" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextOrd { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -107,7 +123,11 @@ pub struct TextOrd { } impl DomainType for TextOrd { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.text_ord" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 628c35f6..6c4621b7 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -10,7 +10,7 @@ use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; /// `eql_v3.timestamptz` — storage only; every operator is blocked. -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Timestamptz { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -23,13 +23,17 @@ pub struct Timestamptz { } impl DomainType for Timestamptz { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.timestamptz" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TimestamptzEq { /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other @@ -44,7 +48,11 @@ pub struct TimestamptzEq { } impl DomainType for TimestamptzEq { - fn sql_domain(&self) -> &'static str { + fn sql_domain_static() -> &'static str { "eql_v3.timestamptz_eq" } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } } diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index 48346f35..9b899ed8 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -7,7 +7,6 @@ use eql_types::v3::int4::{Int4, Int4Eq, Int4Ord, Int4OrdOre}; use eql_types::v3::text::TextMatch; use eql_types::v3::DomainType; use serde_json::json; -use std::marker::PhantomData; #[test] fn int4_storage_round_trips() { @@ -18,7 +17,7 @@ fn int4_storage_round_trips() { }); let parsed: Int4 = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!(PhantomData::.sql_domain(), "eql_v3.int4"); + assert_eq!(Int4::sql_domain_static(), "eql_v3.int4"); } #[test] @@ -31,7 +30,7 @@ fn int4_eq_round_trips() { }); let parsed: Int4Eq = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!(PhantomData::.sql_domain(), "eql_v3.int4_eq"); + assert_eq!(Int4Eq::sql_domain_static(), "eql_v3.int4_eq"); } #[test] @@ -47,10 +46,7 @@ fn int4_ord_round_trips() { // `_ord_ore` is the same shape under the scheme-explicit domain name. let parsed: Int4OrdOre = serde_json::from_value(wire.clone()).unwrap(); assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); - assert_eq!( - PhantomData::.sql_domain(), - "eql_v3.int4_ord_ore" - ); + assert_eq!(Int4OrdOre::sql_domain_static(), "eql_v3.int4_ord_ore"); } #[test] From b3c0cefe8886b1cd01ae0bf5e9923fb21299c163 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 15 Jun 2026 16:22:28 +1000 Subject: [PATCH 12/12] test(eql-types): cover wire shape of every non-int4 v3 domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit catalog_parity.rs checks domain names only, so the 18 non-int4 payload structs — hand-written copies of the int4 template — had their wire field names exercised by nothing; a typo like `hm` -> `hmm` in int8.rs would ship green and only surface in a downstream consumer. Add serde-conformance tests in v3_conformance.rs: - non_int4_tokens_round_trip_every_domain: roundtrips storage/_eq/_ord/ _ord_ore for int2/int8/date/text and pins each catalog domain name. - timestamptz_round_trips_and_enforces_equality_term: roundtrips the equality-only token (storage + _eq) and keeps its hm term required. - rejects_missing_envelope_keys: a payload missing any of the v/i/c envelope keys fails to deserialize, mirroring the missing-term negatives. Addresses review comments from @auxesis on #236. --- crates/eql-types/tests/v3_conformance.rs | 113 +++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index 9b899ed8..d8eecb54 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -63,6 +63,29 @@ fn int4_eq_rejects_missing_hmac() { assert!(result.is_err(), "Int4Eq must reject a payload with no hm"); } +#[test] +fn rejects_missing_envelope_keys() { + // v/i/c are the shared envelope contract every domain CHECK asserts. The + // missing-term negatives cover hm/ob/bf; these cover the envelope itself — + // dropping the version, identifier, or ciphertext fails at the type + // boundary, the Rust analogue of the CHECK's NOT NULL envelope columns. + let base = json!({ + "v": 2, + "i": { "t": "users", "c": "age" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + for key in ["v", "i", "c"] { + let mut wire = base.clone(); + wire.as_object_mut().unwrap().remove(key); + let result: Result = serde_json::from_value(wire); + assert!( + result.is_err(), + "Int4Eq must reject a payload with no {key}" + ); + } +} + #[test] fn rejects_wrong_envelope_version() { // The SchemaVersion field is the Rust analogue of the domain CHECK's @@ -136,3 +159,93 @@ fn text_match_round_trips_signed_bloom_filter() { "TextMatch must reject a payload with no bf" ); } + +#[test] +fn non_int4_tokens_round_trip_every_domain() { + // int4 is exercised exhaustively above; the other ordered tokens carry the + // *same* wire field names but were serialized by no test, so a copy-paste + // field typo (e.g. `hm` -> `hmm` in `int8.rs`) would ship green — + // `catalog_parity.rs` checks domain *names* only, never the wire shape. + // This sweep roundtrips every non-int4 domain and pins its catalog name, + // failing the instant a token drifts from the shared envelope/term contract. + use eql_types::v3::{date::*, int2::*, int8::*, text::*}; + + // Wire builders for the three shapes the ordered tokens share. + let storage = |t: &str| json!({ "v": 2, "i": { "t": t, "c": "x" }, "c": "ct" }); + let eq = |t: &str| json!({ "v": 2, "i": { "t": t, "c": "x" }, "c": "ct", "hm": "deadbeef" }); + let ord = |t: &str| json!({ "v": 2, "i": { "t": t, "c": "x" }, "c": "ct", "ob": ["b0", "b1"] }); + + // Roundtrip a payload byte-for-byte, then confirm the catalog domain name. + macro_rules! round_trip { + ($ty:ty, $wire:expr, $domain:expr) => {{ + let wire = $wire; + let parsed: $ty = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), wire); + assert_eq!(<$ty>::sql_domain_static(), $domain); + }}; + } + + round_trip!(Int2, storage("a"), "eql_v3.int2"); + round_trip!(Int2Eq, eq("a"), "eql_v3.int2_eq"); + round_trip!(Int2Ord, ord("a"), "eql_v3.int2_ord"); + round_trip!(Int2OrdOre, ord("a"), "eql_v3.int2_ord_ore"); + + round_trip!(Int8, storage("a"), "eql_v3.int8"); + round_trip!(Int8Eq, eq("a"), "eql_v3.int8_eq"); + round_trip!(Int8Ord, ord("a"), "eql_v3.int8_ord"); + round_trip!(Int8OrdOre, ord("a"), "eql_v3.int8_ord_ore"); + + round_trip!(Date, storage("a"), "eql_v3.date"); + round_trip!(DateEq, eq("a"), "eql_v3.date_eq"); + round_trip!(DateOrd, ord("a"), "eql_v3.date_ord"); + round_trip!(DateOrdOre, ord("a"), "eql_v3.date_ord_ore"); + + // text_match is covered by `text_match_round_trips_signed_bloom_filter`. + round_trip!(Text, storage("a"), "eql_v3.text"); + round_trip!(TextEq, eq("a"), "eql_v3.text_eq"); + round_trip!(TextOrd, ord("a"), "eql_v3.text_ord"); + round_trip!(TextOrdOre, ord("a"), "eql_v3.text_ord_ore"); +} + +#[test] +fn timestamptz_round_trips_and_enforces_equality_term() { + // The one structurally-distinct token: equality-only, no `_ord`/`_ord_ore` + // (the 8-block-ORE limitation). The int4 template was copy-pasted to + // produce it, so an accidental extra `ob` field or a dropped `hm` would + // pass `catalog_parity` (domain names only) but is caught here. + use eql_types::v3::timestamptz::{Timestamptz, TimestamptzEq}; + + // Storage-only: envelope, no term. + let storage = json!({ + "v": 2, + "i": { "t": "events", "c": "occurred_at" }, + "c": "mp_base85_ciphertext" + }); + let parsed: Timestamptz = serde_json::from_value(storage.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), storage); + assert_eq!(Timestamptz::sql_domain_static(), "eql_v3.timestamptz"); + + // Equality: envelope + hm. + let with_hm = json!({ + "v": 2, + "i": { "t": "events", "c": "occurred_at" }, + "c": "mp_base85_ciphertext", + "hm": "deadbeef" + }); + let parsed: TimestamptzEq = serde_json::from_value(with_hm.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), with_hm); + assert_eq!(TimestamptzEq::sql_domain_static(), "eql_v3.timestamptz_eq"); + + // `_eq` is the only searchable shape this token has, so its equality term + // cannot silently become optional. + let no_hm = json!({ + "v": 2, + "i": { "t": "events", "c": "occurred_at" }, + "c": "mp_base85_ciphertext" + }); + let result: Result = serde_json::from_value(no_hm); + assert!( + result.is_err(), + "TimestamptzEq must reject a payload with no hm" + ); +}