From 2f375e9bf469532d956a6af3b0f523660f450e10 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 10 Jun 2026 21:06:06 +1000 Subject: [PATCH 1/3] feat(eql-types): generate JSON Schemas via schemars Stacks on the TypeScript bindings change. Every v3 domain type, the term newtypes, and Identifier gain a JsonSchema derive; the registry regains its schema fn; tests/export.rs writes one schema per SQL domain to crates/eql-types/schema/v3/ (23 files, checked in) with a canonical $id (https://schemas.cipherstash.com/eql/v3/.json) injected at write time (schemars 0.8 emits none). Term newtypes appear as named definitions that every domain schema $refs. catalog_parity.rs gains a third gate: each domain's published schema 'required' list must equal envelope + catalog term keys, pinning the artifact schema consumers validate against (the behavioural serde test already pins the wire contract). types:generate / types:check and the CI freshness step now cover schema/ as well as bindings/. --- .github/workflows/test-eql.yml | 6 +- Cargo.lock | 42 +++++++ crates/eql-types/Cargo.toml | 7 +- crates/eql-types/README.md | 25 ++-- crates/eql-types/bindings/v3/BloomFilter.ts | 2 +- crates/eql-types/schema/v3/date.json | 69 +++++++++++ crates/eql-types/schema/v3/date_eq.json | 82 ++++++++++++ crates/eql-types/schema/v3/date_ord.json | 85 +++++++++++++ crates/eql-types/schema/v3/date_ord_ore.json | 85 +++++++++++++ crates/eql-types/schema/v3/int2.json | 69 +++++++++++ crates/eql-types/schema/v3/int2_eq.json | 82 ++++++++++++ crates/eql-types/schema/v3/int2_ord.json | 85 +++++++++++++ crates/eql-types/schema/v3/int2_ord_ore.json | 85 +++++++++++++ crates/eql-types/schema/v3/int4.json | 69 +++++++++++ crates/eql-types/schema/v3/int4_eq.json | 82 ++++++++++++ crates/eql-types/schema/v3/int4_ord.json | 85 +++++++++++++ crates/eql-types/schema/v3/int4_ord_ore.json | 85 +++++++++++++ crates/eql-types/schema/v3/int8.json | 69 +++++++++++ crates/eql-types/schema/v3/int8_eq.json | 82 ++++++++++++ crates/eql-types/schema/v3/int8_ord.json | 85 +++++++++++++ crates/eql-types/schema/v3/int8_ord_ore.json | 85 +++++++++++++ crates/eql-types/schema/v3/text.json | 69 +++++++++++ crates/eql-types/schema/v3/text_eq.json | 82 ++++++++++++ crates/eql-types/schema/v3/text_match.json | 88 +++++++++++++ crates/eql-types/schema/v3/text_ord.json | 98 +++++++++++++++ crates/eql-types/schema/v3/text_ord_ore.json | 98 +++++++++++++++ crates/eql-types/schema/v3/text_search.json | 117 ++++++++++++++++++ crates/eql-types/schema/v3/timestamptz.json | 69 +++++++++++ .../eql-types/schema/v3/timestamptz_eq.json | 82 ++++++++++++ crates/eql-types/src/lib.rs | 32 ++++- crates/eql-types/src/v3/date.rs | 27 +++- crates/eql-types/src/v3/int2.rs | 27 +++- crates/eql-types/src/v3/int4.rs | 27 +++- crates/eql-types/src/v3/int8.rs | 27 +++- crates/eql-types/src/v3/mod.rs | 11 +- crates/eql-types/src/v3/terms.rs | 58 ++++++++- crates/eql-types/src/v3/text.rs | 39 +++++- crates/eql-types/src/v3/timestamptz.rs | 15 ++- crates/eql-types/tests/catalog_parity.rs | 53 ++++++-- crates/eql-types/tests/export.rs | 36 ++++++ mise.toml | 33 ++--- 41 files changed, 2378 insertions(+), 76 deletions(-) create mode 100644 crates/eql-types/schema/v3/date.json 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/text_search.json create mode 100644 crates/eql-types/schema/v3/timestamptz.json create mode 100644 crates/eql-types/schema/v3/timestamptz_eq.json create mode 100644 crates/eql-types/tests/export.rs diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 9cd135e1..1e5a7d3d 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -328,9 +328,9 @@ jobs: mise run test:crates # Freshness gate for the eql-types codegen output: regenerate the - # TypeScript bindings and fail if the checked-in copies differ. - # Reuses the toolchain from the step above. - - name: Verify eql-types bindings are fresh + # TypeScript bindings and JSON Schemas and fail if the checked-in + # copies differ. Reuses the toolchain from the step above. + - name: Verify eql-types bindings and schemas are fresh run: | mise run types:check diff --git a/Cargo.lock b/Cargo.lock index bd29da0c..92723c93 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" @@ -1184,6 +1190,7 @@ name = "eql-types" version = "0.1.0" dependencies = [ "eql-scalars", + "schemars", "serde", "serde_json", "ts-rs", @@ -3376,6 +3383,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" @@ -3467,6 +3498,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" diff --git a/crates/eql-types/Cargo.toml b/crates/eql-types/Cargo.toml index 8e644068..82c70efa 100644 --- a/crates/eql-types/Cargo.toml +++ b/crates/eql-types/Cargo.toml @@ -2,15 +2,18 @@ name = "eql-types" version = "0.1.0" edition = "2021" -description = "Canonical wire types for EQL payloads — the single Rust source of truth, with generated TypeScript bindings (JSON Schemas are generated from these types in a stacked change)." +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"] } +# Direct dependency again at this layer: SchemaVersion's manual JsonSchema +# impl pins `const: 2` via serde_json::json!. +serde_json = "1" ts-rs = "10" +schemars = "0.8" [dev-dependencies] # 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 01bfd2a9..aff5f5f9 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -5,8 +5,8 @@ shape**, the single source of truth for every tool that produces or consumes EQL payloads (`cipherstash-client`, `protect-ffi`, CipherStash Proxy). TypeScript bindings are generated from these definitions via [`ts-rs`] into -[`bindings/`](bindings/); JSON Schemas (via [`schemars`]) follow in a -stacked change. +[`bindings/`](bindings/), and JSON Schemas via [`schemars`] into +[`schema/`](schema/). ## Why @@ -56,21 +56,24 @@ the catalog by the JSON Schema parity test in the stacked schemars change. ## Develop ```sh -mise run types:generate # clean-regenerate bindings/ -mise run types:check # regenerate + fail if checked-in bindings are stale +mise run types:generate # clean-regenerate bindings/ and schema/ +mise run types:check # regenerate + fail if checked-in outputs are stale ``` Both wrap `cargo test -p eql-types`, which runs the conformance tests and -regenerates `bindings/` (TypeScript, via ts-rs). The directory is checked in -so reviewers can see the codegen output without running anything; CI runs -`types:check` to keep it fresh. The crate is also part of the lean +regenerates `bindings/` (TypeScript, via ts-rs) and `schema/` (JSON Schema, +via `tests/export.rs`, with canonical `$id`s injected). 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). -Note that ts-rs writes to `./bindings` by default, so a plain +Note that both exporters default to writing under the crate dir (ts-rs to +`./bindings`, `tests/export.rs` to `./schema`), so a plain `cargo test -p eql-types` (and therefore `mise run test:crates`) regenerates -`bindings/` **in place** as a side effect — it can leave your working tree -dirty if the checked-in copies were stale. Only `types:generate` isolates the -write (it exports into a temp dir and swaps it in after the build succeeds). +`bindings/` and `schema/` **in place** as a side effect — it can leave your +working tree dirty if the checked-in copies were stale. Only `types:generate` +isolates the writes (it exports into a temp dir and swaps them in after the +build succeeds). ## Future direction: self-describing payloads diff --git a/crates/eql-types/bindings/v3/BloomFilter.ts b/crates/eql-types/bindings/v3/BloomFilter.ts index 280f4c7b..6861ce1c 100644 --- a/crates/eql-types/bindings/v3/BloomFilter.ts +++ b/crates/eql-types/bindings/v3/BloomFilter.ts @@ -2,7 +2,7 @@ /** * Bloom-filter match term — the `bf` wire key. Backs the `_match` domains - * (`~~` containment via `@>`/`<@`). + * (`@>`/`<@` containment). * * **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, * and filters sized above 32768 emit upper-half bit positions as negative diff --git a/crates/eql-types/schema/v3/date.json b/crates/eql-types/schema/v3/date.json new file mode 100644 index 00000000..706e4b56 --- /dev/null +++ b/crates/eql-types/schema/v3/date.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..d7cf20d1 --- /dev/null +++ b/crates/eql-types/schema/v3/date_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..03315ea1 --- /dev/null +++ b/crates/eql-types/schema/v3/date_ord.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..dfff7403 --- /dev/null +++ b/crates/eql-types/schema/v3/date_ord_ore.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/date_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..118cfbf2 --- /dev/null +++ b/crates/eql-types/schema/v3/int2.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..2b3616d7 --- /dev/null +++ b/crates/eql-types/schema/v3/int2_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..5073b3a4 --- /dev/null +++ b/crates/eql-types/schema/v3/int2_ord.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..83b37587 --- /dev/null +++ b/crates/eql-types/schema/v3/int2_ord_ore.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int2_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..4e850627 --- /dev/null +++ b/crates/eql-types/schema/v3/int4.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..cf88e7f7 --- /dev/null +++ b/crates/eql-types/schema/v3/int4_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..cbacaa32 --- /dev/null +++ b/crates/eql-types/schema/v3/int4_ord.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..b8cdb95f --- /dev/null +++ b/crates/eql-types/schema/v3/int4_ord_ore.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int4_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..be50c73b --- /dev/null +++ b/crates/eql-types/schema/v3/int8.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..3a7d3042 --- /dev/null +++ b/crates/eql-types/schema/v3/int8_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..9adc8520 --- /dev/null +++ b/crates/eql-types/schema/v3/int8_ord.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..174e68af --- /dev/null +++ b/crates/eql-types/schema/v3/int8_ord_ore.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/int8_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..4b4e34d9 --- /dev/null +++ b/crates/eql-types/schema/v3/text.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..71a0f15e --- /dev/null +++ b/crates/eql-types/schema/v3/text_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..cedacf78 --- /dev/null +++ b/crates/eql-types/schema/v3/text_match.json @@ -0,0 +1,88 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_match.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BloomFilter": { + "description": "Bloom-filter match term — the `bf` wire key. Backs the `_match` domains (`@>`/`<@` containment). Signed i16: EQL stores the filter as PostgreSQL `smallint[]`, and filters sized above 32768 emit upper-half bit positions as negative signed values.", + "items": { + "format": "int16", + "maximum": 32767.0, + "minimum": -32768.0, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..6b059e2a --- /dev/null +++ b/crates/eql-types/schema/v3/text_ord.json @@ -0,0 +1,98 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_ord.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "description": "`eql_v3.text_ord` — full lexicographic comparison (`=` `<>` `<` `<=` `>` `>=`). Carries both `hm` (equality) and `ob` (ordering) — text routes equality through `hm` (`[Hm, Ore]`).", + "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. Text routes `=`/`<>` through `hm`." + }, + "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." + }, + "v": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "required": [ + "c", + "hm", + "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..d4899142 --- /dev/null +++ b/crates/eql-types/schema/v3/text_ord_ore.json @@ -0,0 +1,98 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_ord_ore.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "description": "`eql_v3.text_ord_ore` — full lexicographic comparison, scheme-explicit name. Unlike the integer ordered domains (`[Ore]` only), text routes equality through `hm` rather than the ORE term, so the domain carries both `hm` and `ob` (`[Hm, Ore]`).", + "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. Text routes `=`/`<>` through `hm`." + }, + "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." + }, + "v": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "required": [ + "c", + "hm", + "i", + "ob", + "v" + ], + "title": "TextOrdOre", + "type": "object" +} \ No newline at end of file diff --git a/crates/eql-types/schema/v3/text_search.json b/crates/eql-types/schema/v3/text_search.json new file mode 100644 index 00000000..87188f4b --- /dev/null +++ b/crates/eql-types/schema/v3/text_search.json @@ -0,0 +1,117 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/text_search.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BloomFilter": { + "description": "Bloom-filter match term — the `bf` wire key. Backs the `_match` domains (`@>`/`<@` containment). Signed i16: EQL stores the filter as PostgreSQL `smallint[]`, and filters sized above 32768 emit upper-half bit positions as negative signed values.", + "items": { + "format": "int16", + "maximum": 32767.0, + "minimum": -32768.0, + "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" + }, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "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" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "description": "`eql_v3.text_search` — the full text search surface: HMAC equality, ORE ordering, and Bloom-filter containment match (`[Hm, Ore, Bloom]`). The superset domain combining `_eq`, `_ord`, and `_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." + }, + "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." + }, + "ob": { + "allOf": [ + { + "$ref": "#/definitions/OreBlockU64_8_256" + } + ], + "description": "Block-ORE order term." + }, + "v": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "required": [ + "bf", + "c", + "hm", + "i", + "ob", + "v" + ], + "title": "TextSearch", + "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..72a154d8 --- /dev/null +++ b/crates/eql-types/schema/v3/timestamptz.json @@ -0,0 +1,69 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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..90fc71d4 --- /dev/null +++ b/crates/eql-types/schema/v3/timestamptz_eq.json @@ -0,0 +1,82 @@ +{ + "$id": "https://schemas.cipherstash.com/eql/v3/timestamptz_eq.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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": { + "additionalProperties": false, + "description": "Table + column identifier — wire shape `{\"t\": \"...\", \"c\": \"...\"}`.\n\nShared by every payload.", + "properties": { + "c": { + "description": "Column name.", + "type": "string" + }, + "t": { + "description": "Table name.", + "type": "string" + } + }, + "required": [ + "c", + "t" + ], + "type": "object" + }, + "SchemaVersion": { + "const": 2, + "description": "The envelope version field (`v`) — always exactly `2` on the wire.", + "type": "integer" + } + }, + "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": { + "allOf": [ + { + "$ref": "#/definitions/SchemaVersion" + } + ], + "description": "Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other value fails deserialization." + } + }, + "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 c11e398a..82d1b503 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -3,9 +3,9 @@ //! 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 are generated from these definitions via `ts-rs` (run -//! `cargo test`, see `bindings/v3/`); JSON Schemas follow in a stacked -//! change. The Rust types are the contract. +//! bindings (`ts-rs`) and JSON Schemas (`schemars`) are generated from +//! these definitions — run `cargo test`, see `bindings/v3/` and +//! `schema/v3/`. The Rust types are the contract. //! //! ts-rs rule (learned from the original spike): ts-rs silently drops a //! serde attribute it cannot parse, so keep field-level serde attributes @@ -20,6 +20,7 @@ //! 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; @@ -72,10 +73,33 @@ impl<'de> Deserialize<'de> for SchemaVersion { } } +/// Manual schema: pins `v` to the literal `2` (`const`), mirroring the +/// domain CHECK — the derive would emit an unconstrained integer. +impl schemars::JsonSchema for SchemaVersion { + fn schema_name() -> String { + "SchemaVersion".to_owned() + } + + fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + const_value: Some(serde_json::json!(EQL_SCHEMA_VERSION)), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some( + "The envelope version field (`v`) — always exactly `2` on the wire.".to_owned(), + ), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + /// Table + column identifier — wire shape `{"t": "...", "c": "..."}`. /// /// Shared by every payload. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Identifier { diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index d9b1776f..ffd0bbbb 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -3,14 +3,17 @@ //! ciphertext, so dates order like integers); see that module for the //! capability table. +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Date { @@ -31,10 +34,14 @@ impl DomainType for Date { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Date) + } } /// `eql_v3.date_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct DateEq { @@ -57,10 +64,14 @@ impl DomainType for DateEq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(DateEq) + } } /// `eql_v3.date_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct DateOrdOre { @@ -83,10 +94,14 @@ impl DomainType for DateOrdOre { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(DateOrdOre) + } } /// `eql_v3.date_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct DateOrd { @@ -109,4 +124,8 @@ impl DomainType for DateOrd { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(DateOrd) + } } diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index 968784e7..8e2fc939 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -1,14 +1,17 @@ //! The `int2` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int2 { @@ -29,10 +32,14 @@ impl DomainType for Int2 { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int2) + } } /// `eql_v3.int2_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int2Eq { @@ -55,10 +62,14 @@ impl DomainType for Int2Eq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int2Eq) + } } /// `eql_v3.int2_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int2OrdOre { @@ -81,10 +92,14 @@ impl DomainType for Int2OrdOre { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int2OrdOre) + } } /// `eql_v3.int2_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int2Ord { @@ -107,4 +122,8 @@ impl DomainType for Int2Ord { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int2Ord) + } } diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index e0a804e6..752ddbf1 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -7,14 +7,17 @@ //! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int4 { @@ -35,10 +38,14 @@ impl DomainType for Int4 { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int4) + } } /// `eql_v3.int4_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int4Eq { @@ -61,11 +68,15 @@ impl DomainType for Int4Eq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int4OrdOre { @@ -89,10 +100,14 @@ impl DomainType for Int4OrdOre { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int4OrdOre) + } } /// `eql_v3.int4_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int4Ord { @@ -115,4 +130,8 @@ impl DomainType for Int4Ord { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int4Ord) + } } diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 5d50fe70..ea0f5714 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -1,14 +1,17 @@ //! The `int8` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int8 { @@ -29,10 +32,14 @@ impl DomainType for Int8 { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int8) + } } /// `eql_v3.int8_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int8Eq { @@ -55,10 +62,14 @@ impl DomainType for Int8Eq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int8Eq) + } } /// `eql_v3.int8_ord_ore` — full comparison, scheme-explicit name. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int8OrdOre { @@ -81,10 +92,14 @@ impl DomainType for Int8OrdOre { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int8OrdOre) + } } /// `eql_v3.int8_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Int8Ord { @@ -107,4 +122,8 @@ impl DomainType for Int8Ord { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Int8Ord) + } } diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 316f53e7..e2681bc6 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -46,6 +46,8 @@ use std::marker::PhantomData; +use schemars::{schema::RootSchema, schema_for, JsonSchema}; + pub mod date; pub mod int2; pub mod int4; @@ -88,6 +90,9 @@ pub trait DomainType { .strip_prefix("eql_v3.") .expect("sql_domain must be qualified with the eql_v3 schema") } + + /// The type's JSON Schema. + fn schema(&self) -> RootSchema; } /// Type-level handle: lets [`all`] enumerate the domain types without @@ -96,7 +101,7 @@ pub trait DomainType { /// payload instance is ever constructed. impl DomainType for PhantomData where - T: DomainType, + T: DomainType + JsonSchema, { fn sql_domain_static() -> &'static str { T::sql_domain_static() @@ -105,6 +110,10 @@ where fn sql_domain(&self) -> &'static str { T::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(T) + } } /// Every v3 domain type, in `eql-scalars::CATALOG` order (token order, then diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index b66b3ea7..0c9e006b 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -4,26 +4,27 @@ //! default), so the wire shape is unchanged — but the *name* survives into //! generated artifacts: ts-rs exports a named TS alias //! (`export type Hmac256 = string`) that every domain binding imports, and -//! the JSON Schemas (added in a stacked change) emit named definitions -//! likewise. A plain Rust `type` alias would vanish there. +//! 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)] +#[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)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] pub struct Hmac256(pub String); @@ -31,12 +32,12 @@ pub struct Hmac256(pub String); /// `_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)] +#[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 `@>`/`<@`). +/// (`@>`/`<@` containment). /// /// **Signed** i16, not u16: EQL stores the filter as PostgreSQL `smallint[]`, /// and filters sized above 32768 emit upper-half bit positions as negative @@ -45,6 +46,51 @@ pub struct OreBlockU64_8_256(pub Vec); #[ts(export, export_to = "v3/")] pub struct BloomFilter(pub Vec); +/// Manual schema: bounds the items to the `smallint` range — the derive +/// emits only `format: "int16"`, a non-validating annotation in draft-07, +/// so an out-of-range bit position would pass schema validation and fail +/// at the database. +impl schemars::JsonSchema for BloomFilter { + fn schema_name() -> String { + "BloomFilter".to_owned() + } + + fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::schema::{ + ArrayValidation, InstanceType, Metadata, NumberValidation, Schema, SchemaObject, + }; + let items = SchemaObject { + instance_type: Some(InstanceType::Integer.into()), + format: Some("int16".to_owned()), + number: Some(Box::new(NumberValidation { + minimum: Some(f64::from(i16::MIN)), + maximum: Some(f64::from(i16::MAX)), + ..Default::default() + })), + ..Default::default() + }; + SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(Schema::Object(items).into()), + ..Default::default() + })), + metadata: Some(Box::new(Metadata { + description: Some( + "Bloom-filter match term — the `bf` wire key. Backs the `_match` \ + domains (`@>`/`<@` containment). Signed i16: EQL stores the filter \ + as PostgreSQL `smallint[]`, and filters sized above 32768 emit \ + upper-half bit positions as negative signed values." + .to_owned(), + ), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + impl From for Ciphertext { fn from(value: String) -> Self { Self(value) diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index 77d51d57..a0008947 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -2,14 +2,17 @@ //! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter //! term (`@>`/`<@` containment for `LIKE`-style matching). +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Text { @@ -30,10 +33,14 @@ impl DomainType for Text { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Text) + } } /// `eql_v3.text_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TextEq { @@ -56,10 +63,14 @@ impl DomainType for TextEq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TextEq) + } } /// `eql_v3.text_match` — Bloom-filter containment match. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TextMatch { @@ -82,13 +93,17 @@ impl DomainType for TextMatch { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TextMatch) + } } /// `eql_v3.text_ord_ore` — full lexicographic comparison, /// scheme-explicit name. Unlike the integer ordered domains (`[Ore]` only), /// text routes equality through `hm` rather than the ORE term, so the domain /// carries both `hm` and `ob` (`[Hm, Ore]`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TextOrdOre { @@ -113,12 +128,16 @@ impl DomainType for TextOrdOre { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TextOrdOre) + } } /// `eql_v3.text_ord` — full lexicographic comparison /// (`=` `<>` `<` `<=` `>` `>=`). Carries both `hm` (equality) and `ob` /// (ordering) — text routes equality through `hm` (`[Hm, Ore]`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TextOrd { @@ -143,12 +162,16 @@ impl DomainType for TextOrd { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TextOrd) + } } /// `eql_v3.text_search` — the full text search surface: HMAC equality, ORE /// ordering, and Bloom-filter containment match (`[Hm, Ore, Bloom]`). The /// superset domain combining `_eq`, `_ord`, and `_match`. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TextSearch { @@ -175,4 +198,8 @@ impl DomainType for TextSearch { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TextSearch) + } } diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index e3775ede..3deb51a2 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -4,14 +4,17 @@ //! 8 blocks, so an ordered timestamptz domain would silently mis-order. //! Ordering arrives with a future wide-ORE term (see `eql-scalars`). +use schemars::{schema::RootSchema, schema_for}; + use crate::v3::terms::{Ciphertext, Hmac256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; +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)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct Timestamptz { @@ -32,10 +35,14 @@ impl DomainType for Timestamptz { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(Timestamptz) + } } /// `eql_v3.timestamptz_eq` — HMAC equality (`=`, `<>`). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "v3/")] #[serde(deny_unknown_fields)] pub struct TimestamptzEq { @@ -58,4 +65,8 @@ impl DomainType for TimestamptzEq { fn sql_domain(&self) -> &'static str { Self::sql_domain_static() } + + fn schema(&self) -> RootSchema { + schema_for!(TimestamptzEq) + } } diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index 94ff8eb7..b068e5cf 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -1,14 +1,14 @@ //! 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. +//! every domain, in catalog order, and every domain's wire keys, pinned +//! through the published JSON Schema (schemars `required` reflects the real +//! serde contract, so an `Option` term field or a wrong wire key fails +//! here). Per-type strictness (unknown-key rejection, envelope version) is +//! covered behaviourally in `tests/v3_conformance.rs`. -use eql_scalars::CATALOG; +use std::collections::BTreeSet; + +use eql_scalars::{Term, CATALOG, ENVELOPE_KEYS}; use eql_types::v3; #[test] @@ -23,3 +23,40 @@ fn inventory_exactly_covers_catalog() { "v3::all() must list every CATALOG domain, in catalog order" ); } + +/// The *published* JSON Schemas must agree with the catalog: each domain's +/// schema `required` list is exactly envelope + catalog term keys — the +/// artifact schema consumers validate against cannot drift from the SQL +/// surface's CHECK constraints. +#[test] +fn schema_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 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}: schema required keys must be envelope + catalog terms" + ); + } + } +} diff --git a/crates/eql-types/tests/export.rs b/crates/eql-types/tests/export.rs new file mode 100644 index 00000000..3fb4e2d3 --- /dev/null +++ b/crates/eql-types/tests/export.rs @@ -0,0 +1,36 @@ +//! 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`. Schema files are named after the +//! SQL domain — the protocol identity — not the Rust type. +//! +//! Output base defaults to `schema/` (relative to the crate dir, where +//! `cargo test` runs), so a plain `cargo test` regenerates the checked-in +//! tree. `EQL_TYPES_SCHEMA_DIR` overrides it — mirroring ts-rs's +//! `TS_RS_EXPORT_DIR` — so `mise run types:generate` can redirect output to a +//! throwaway temp dir and only swap it into place after a successful build. + +use eql_types::v3; + +#[test] +fn dump_v3_json_schemas() { + let base = std::env::var("EQL_TYPES_SCHEMA_DIR").unwrap_or_else(|_| "schema".into()); + let dir = format!("{base}/v3"); + std::fs::create_dir_all(&dir).unwrap(); + for entry in v3::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!("{dir}/{}.json", entry.domain()), + serde_json::to_string_pretty(&schema).unwrap(), + ) + .unwrap(); + } +} diff --git a/mise.toml b/mise.toml index 6fe2235d..bfece3d4 100644 --- a/mise.toml +++ b/mise.toml @@ -161,41 +161,42 @@ cargo test -p eql-scalars -p eql-codegen -p eql-tests-macros -p eql-types """ [tasks."types:generate"] -description = "Regenerate eql-types TypeScript bindings from the Rust types (no database required)" +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 only ever ADDS files, so a renamed or removed -# type would otherwise leave an orphaned binding behind — we need a clean dir, -# not an overlay. But we must NOT rm the checked-in bindings before the build: -# a failing or interrupted `cargo test` would then leave the working tree -# missing them. Instead, export into a throwaway temp dir (ts-rs honors -# TS_RS_EXPORT_DIR as the base, and every v3 type uses export_to = "v3/", so -# the temp dir mirrors crates/eql-types/bindings exactly) and only swap it into +# 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 — we need clean dirs, not an overlay. But we must NOT rm the +# checked-in output before the build: a failing or interrupted `cargo test` +# would then leave the working tree missing it. Instead, export into a +# throwaway temp dir (ts-rs honors TS_RS_EXPORT_DIR; tests/export.rs honors +# EQL_TYPES_SCHEMA_DIR; every type uses the v3/ subdir, so the temp tree +# mirrors crates/eql-types/{bindings,schema} exactly) and only swap it into # place after the tests succeed. The swap stays out of the tests themselves — # they run in parallel and can't safely rm. set -euo pipefail tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT -TS_RS_EXPORT_DIR="$tmp" cargo test -p eql-types -rm -rf crates/eql-types/bindings -mv "$tmp" crates/eql-types/bindings -trap - EXIT +TS_RS_EXPORT_DIR="$tmp/bindings" EQL_TYPES_SCHEMA_DIR="$tmp/schema" cargo test -p eql-types +rm -rf crates/eql-types/bindings crates/eql-types/schema +mv "$tmp/bindings" crates/eql-types/bindings +mv "$tmp/schema" crates/eql-types/schema """ [tasks."types:check"] -description = "Verify the checked-in eql-types bindings/ are fresh (regenerate + git diff)" +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 || { - echo "eql-types bindings/ are stale — run 'mise run types:generate' and commit the result" >&2 +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) +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 From f49e65cd1854114916972f8a8b9d6dc316a559e5 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 11 Jun 2026 16:41:14 +1000 Subject: [PATCH 2/3] test(eql-types): pin schema strictness per domain, not just required keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding: the schema parity gate checked only `required`, so a future token file losing #[serde(deny_unknown_fields)] or declaring v as a bare integer would regenerate a permissive schema that types:check commits as the new baseline — with behavioural spot checks existing for only 5 of 23 domains. schemas_are_strict now asserts, for every domain: additionalProperties: false at the root and on the nested Identifier, v $ref'ing the SchemaVersion definition, and that definition pinning const: EQL_SCHEMA_VERSION. Drilled: stripping deny_unknown_fields from DateEq fails the gate with the targeted message. --- crates/eql-types/tests/catalog_parity.rs | 55 +++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index b068e5cf..10b199d6 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -1,15 +1,19 @@ //! 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, and every domain's wire keys, pinned -//! through the published JSON Schema (schemars `required` reflects the real -//! serde contract, so an `Option` term field or a wrong wire key fails -//! here). Per-type strictness (unknown-key rejection, envelope version) is -//! covered behaviourally in `tests/v3_conformance.rs`. +//! every domain, in catalog order, and every domain's wire contract, pinned +//! through the published JSON Schema. schemars output reflects the real +//! serde contract, so per domain this catches an `Option` term field or a +//! wrong wire key (`required`), a struct that lost +//! `#[serde(deny_unknown_fields)]` (`additionalProperties: false`), and a +//! `v` field that is not [`eql_types::SchemaVersion`] (the `$ref` and its +//! `const: 2`). Behavioural spot checks of the same properties live in +//! `tests/v3_conformance.rs`. use std::collections::BTreeSet; use eql_scalars::{Term, CATALOG, ENVELOPE_KEYS}; -use eql_types::v3; +use eql_types::{v3, EQL_SCHEMA_VERSION}; +use serde_json::{json, Value}; #[test] fn inventory_exactly_covers_catalog() { @@ -60,3 +64,42 @@ fn schema_required_keys_match_catalog_terms() { } } } + +/// Every published schema must be *strict*, not just complete: unknown keys +/// rejected at the root and inside the nested `Identifier`, and the `v` +/// property pinned to the `SchemaVersion` definition whose `const` is the +/// wire version. `required` alone (the test above) would stay green if a +/// struct lost `#[serde(deny_unknown_fields)]` or swapped `SchemaVersion` +/// for a bare integer — both regenerate a permissive schema that +/// `types:check` would happily commit as the new baseline. +#[test] +fn schemas_are_strict() { + for entry in v3::all() { + let name = entry.domain(); + let schema: Value = serde_json::to_value(entry.schema()) + .unwrap_or_else(|e| panic!("{name}: schema does not serialize: {e}")); + + assert_eq!( + schema.pointer("/additionalProperties"), + Some(&json!(false)), + "{name}: schema must set additionalProperties: false \ + (struct lost #[serde(deny_unknown_fields)]?)" + ); + assert_eq!( + schema.pointer("/definitions/Identifier/additionalProperties"), + Some(&json!(false)), + "{name}: Identifier definition must set additionalProperties: false" + ); + assert_eq!( + schema.pointer("/properties/v/allOf/0/$ref"), + Some(&json!("#/definitions/SchemaVersion")), + "{name}: the v property must $ref the SchemaVersion definition \ + (field declared as a bare integer instead of SchemaVersion?)" + ); + assert_eq!( + schema.pointer("/definitions/SchemaVersion/const"), + Some(&json!(EQL_SCHEMA_VERSION)), + "{name}: SchemaVersion must pin const: {EQL_SCHEMA_VERSION}" + ); + } +} From 68a977e6a2eceac3ea692a756d5c3e627ccf0cc3 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 16 Jun 2026 18:13:35 +1000 Subject: [PATCH 3/3] =?UTF-8?q?chore(eql-types):=20address=20#269=20review?= =?UTF-8?q?=20=E2=80=94=20pin=20$id,=20flag=20manual-description=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Factor the published schema `$id` into `DomainType::schema_id` (`{SCHEMA_ID_BASE}{domain}.json`); `tests/export.rs` now injects that single source of truth, and a new `schema_id_is_canonical` parity test pins the URL shape with independent literals (wrong host / dropped `v3/` / wrong domain now fails a test, not just the freshness diff). - Mark the `BloomFilter` and `SchemaVersion` doc comments as the canonical source for the descriptions their manual `JsonSchema` impls hand-copy, so the two can't silently drift (the derive copies doc comments automatically; these manual impls can't). Schema artifacts regenerate byte-identically (schema_id matches the old inline format) — verified via `mise run types:check`. --- crates/eql-types/src/lib.rs | 4 +++ crates/eql-types/src/v3/mod.rs | 12 ++++++++ crates/eql-types/src/v3/terms.rs | 4 +++ crates/eql-types/tests/catalog_parity.rs | 39 ++++++++++++++++++++++++ crates/eql-types/tests/export.rs | 15 ++++----- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/eql-types/src/lib.rs b/crates/eql-types/src/lib.rs index 82d1b503..1f87f95a 100644 --- a/crates/eql-types/src/lib.rs +++ b/crates/eql-types/src/lib.rs @@ -85,6 +85,10 @@ impl schemars::JsonSchema for SchemaVersion { instance_type: Some(schemars::schema::InstanceType::Integer.into()), const_value: Some(serde_json::json!(EQL_SCHEMA_VERSION)), metadata: Some(Box::new(schemars::schema::Metadata { + // KEEP IN SYNC with the `SchemaVersion` doc comment above — it + // is the canonical text. A derived `JsonSchema` would copy the + // doc comment automatically; this manual impl can't, so this + // hand-written copy must be updated alongside it. description: Some( "The envelope version field (`v`) — always exactly `2` on the wire.".to_owned(), ), diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index e2681bc6..deb3b297 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -59,6 +59,11 @@ pub mod timestamptz; /// The PostgreSQL schema every domain in this module inhabits. pub const SQL_SCHEMA: &str = "eql_v3"; +/// Base URL for the canonical `$id` of every published v3 JSON Schema. +/// The per-domain `$id` is `{SCHEMA_ID_BASE}{domain}.json` (see +/// [`DomainType::schema_id`]); `tests/export.rs` injects it at write time. +pub const SCHEMA_ID_BASE: &str = "https://schemas.cipherstash.com/eql/v3/"; + /// One v3 domain type — implemented by every payload type, so any payload /// value can report the SQL domain it inhabits (`payload.sql_domain()`). /// @@ -91,6 +96,13 @@ pub trait DomainType { .expect("sql_domain must be qualified with the eql_v3 schema") } + /// Canonical `$id` for this domain's published JSON Schema — + /// `{SCHEMA_ID_BASE}{domain}.json`. The single source of truth for the + /// identity `tests/export.rs` injects; pinned by `tests/catalog_parity.rs`. + fn schema_id(&self) -> String { + format!("{SCHEMA_ID_BASE}{}.json", self.domain()) + } + /// The type's JSON Schema. fn schema(&self) -> RootSchema; } diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index 0c9e006b..d12cfad4 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -76,6 +76,10 @@ impl schemars::JsonSchema for BloomFilter { ..Default::default() })), metadata: Some(Box::new(Metadata { + // KEEP IN SYNC with the doc comment on `BloomFilter` above — it + // is the canonical text. A derived `JsonSchema` would copy the + // doc comment automatically; this manual impl can't, so this + // hand-written paraphrase must be updated alongside it. description: Some( "Bloom-filter match term — the `bf` wire key. Backs the `_match` \ domains (`@>`/`<@` containment). Signed i16: EQL stores the filter \ diff --git a/crates/eql-types/tests/catalog_parity.rs b/crates/eql-types/tests/catalog_parity.rs index 10b199d6..465a5139 100644 --- a/crates/eql-types/tests/catalog_parity.rs +++ b/crates/eql-types/tests/catalog_parity.rs @@ -65,6 +65,45 @@ fn schema_required_keys_match_catalog_terms() { } } +/// The published `$id` is the schema's identity URL — `tests/export.rs` +/// injects [`v3::DomainType::schema_id`] into every written file. Pin its +/// shape with independent literals (NOT the helper, which would only test +/// itself): a regressed host, a dropped `v3/`, or a wrong domain segment must +/// turn a test red, not merely shift the freshness diff. +#[test] +fn schema_id_is_canonical() { + let entries = v3::all(); + let id_of = |domain: &str| { + entries + .iter() + .find(|e| e.domain() == domain) + .unwrap_or_else(|| panic!("no domain inventory entry for {domain}")) + .schema_id() + }; + + // Fully-literal anchors — no interpolation, so a typo in the helper's base + // URL or path cannot match. + assert_eq!( + id_of("int4_eq"), + "https://schemas.cipherstash.com/eql/v3/int4_eq.json" + ); + assert_eq!( + id_of("text_search"), + "https://schemas.cipherstash.com/eql/v3/text_search.json" + ); + + // Every domain follows the same canonical pattern. + for entry in &entries { + let id = entry.schema_id(); + let name = entry.domain(); + assert_eq!( + id, + format!("https://schemas.cipherstash.com/eql/v3/{name}.json"), + "{name}: $id must be the canonical eql/v3 URL" + ); + } +} + /// Every published schema must be *strict*, not just complete: unknown keys /// rejected at the root and inside the nested `Identifier`, and the `v` /// property pinned to the `SchemaVersion` definition whose `const` is the diff --git a/crates/eql-types/tests/export.rs b/crates/eql-types/tests/export.rs index 3fb4e2d3..bb95244a 100644 --- a/crates/eql-types/tests/export.rs +++ b/crates/eql-types/tests/export.rs @@ -18,15 +18,12 @@ fn dump_v3_json_schemas() { std::fs::create_dir_all(&dir).unwrap(); for entry in v3::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(), - ); + // schemars 0.8 emits no $id; inject the canonical one (the URL format + // lives on DomainType::schema_id, pinned by tests/catalog_parity.rs). + schema + .as_object_mut() + .unwrap() + .insert("$id".into(), entry.schema_id().into()); std::fs::write( format!("{dir}/{}.json", entry.domain()), serde_json::to_string_pretty(&schema).unwrap(),