From 536e56cf435d9bae3ea041a0aaa7b0989b34a602 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 13:26:06 +1000 Subject: [PATCH 01/12] refactor(v3): rename ore_block_u64_8_256 -> ore_block_256 (SEM type, repo-wide) Behaviour-preserving rename of the self-contained eql_v3 SEM index-term type to its width-agnostic name, ahead of the N-block ORE comparator work. The eql_v2 public API (eql_v2.ore_block_u64_8_256, src/ore_block_u64_8_256/, v2 operators/docs/tests) is deliberately UNCHANGED. Scope (eql_v3 only): - src/v3/sem/ore_block_u64_8_256/ -> src/v3/sem/ore_block_256/ (dir + symbols) - eql-scalars Term::Ore ctor + REQUIRE paths; eql-codegen doc/assertion - committed codegen goldens (int2/int4/int8/date/text incl text_search) - eql-types: OreBlockU64_8_256 newtype -> OreBlock256, SQL constructor doc - tasks/pin_search_path.sql (v3 block only), splinter.sh (v3 rows only), clean_install_v3.sh, drop_operator_classes.sql (v3 opclass only) - v3 SQLx family tests (sem/mutations/inlinability), preserving the eql_v2.compare_ore_block_u64_8_256_terms parity reference in sem.rs - v3 docs (CLAUDE, adding-a-scalar, eql-functions, sql-support, analysis), CHANGELOG eql_v3-qualified mentions Covers the new old-name references introduced by #280 (text-search). Verified: codegen:parity (byte-for-byte), test:crates (fmt+clippy+tests), clean build + v3 release artifact carries no eql_v2 symbol. The DB-backed clean_install_v3 gate runs in CI (Docker unavailable locally). Deliberately NOT renamed: tests/sqlx/src/index_types.rs ORE_BLOCK_U64_8_256 (crypto wire index-term identifier, not the SQL type name). --- CHANGELOG.md | 4 +- CLAUDE.md | 6 +- crates/eql-codegen/src/context.rs | 2 +- crates/eql-codegen/src/generate.rs | 2 +- crates/eql-scalars/src/term.rs | 6 +- crates/eql-scalars/src/tests.rs | 10 +- crates/eql-types/README.md | 2 +- crates/eql-types/src/v3/date.rs | 6 +- crates/eql-types/src/v3/int2.rs | 6 +- crates/eql-types/src/v3/int4.rs | 6 +- crates/eql-types/src/v3/int8.rs | 6 +- crates/eql-types/src/v3/terms.rs | 6 +- crates/eql-types/src/v3/text.rs | 8 +- ...-06-11-v3-scalar-vs-jsonb-test-coverage.md | 2 +- .../adding-a-scalar-encrypted-domain-type.md | 12 +- docs/reference/eql-functions.md | 4 +- docs/reference/sql-support.md | 2 +- src/v3/schema.sql | 2 +- src/v3/sem/bloom_filter/functions.sql | 2 +- .../functions.sql | 56 +++--- .../operator_class.sql | 16 +- src/v3/sem/ore_block_256/operators.sql | 180 ++++++++++++++++++ .../types.sql | 8 +- src/v3/sem/ore_block_u64_8_256/operators.sql | 180 ------------------ tasks/pin_search_path.sql | 8 +- tasks/test/clean_install_v3.sh | 2 +- tasks/test/splinter.sh | 18 +- .../reference/date/date_ord_functions.sql | 10 +- .../reference/date/date_ord_ore_functions.sql | 10 +- .../reference/int2/int2_ord_functions.sql | 10 +- .../reference/int2/int2_ord_ore_functions.sql | 10 +- .../reference/int4/int4_ord_functions.sql | 10 +- .../reference/int4/int4_ord_ore_functions.sql | 10 +- .../reference/int8/int8_ord_functions.sql | 10 +- .../reference/int8/int8_ord_ore_functions.sql | 10 +- .../reference/text/text_ord_functions.sql | 10 +- .../reference/text/text_ord_ore_functions.sql | 10 +- .../reference/text/text_search_functions.sql | 10 +- tests/sqlx/fixtures/drop_operator_classes.sql | 4 +- .../encrypted_domain/family/inlinability.rs | 18 +- .../encrypted_domain/family/mutations.rs | 16 +- .../sqlx/tests/encrypted_domain/family/sem.rs | 105 +++++----- 42 files changed, 405 insertions(+), 410 deletions(-) rename src/v3/sem/{ore_block_u64_8_256 => ore_block_256}/functions.sql (76%) rename src/v3/sem/{ore_block_u64_8_256 => ore_block_256}/operator_class.sql (53%) create mode 100644 src/v3/sem/ore_block_256/operators.sql rename src/v3/sem/{ore_block_u64_8_256 => ore_block_256}/types.sql (79%) delete mode 100644 src/v3/sem/ore_block_u64_8_256/operators.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 88814a49..f60f4903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,14 +23,14 @@ Each entry that ships in a published release links to the PR that introduced it. ### Added - **`eql_v3` encrypted-JSONB (SteVec) document type.** A self-contained encrypted-JSONB surface in the `eql_v3` schema: the storage domain `eql_v3.json` plus `eql_v3.ste_vec_entry` (a single sv element) and `eql_v3.ste_vec_query` (a containment needle). Encrypted JSON documents are searchable without decryption via document containment (`@>`, `<@`), field/array access (`->`, `->>`, `jsonb_path_query` / `_exists` / `_query_first`, `jsonb_array_length` / `_elements` / `_elements_text`), entry equality (`=`, `<>`) on extracted leaves, and entry-level ordered range (`<`, `<=`, `>`, `>=`) on ordered leaves via CLLW ORE (`eql_v3.ore_cllw`). Comparisons are leaf-level only: root-document `=`, `<>`, `<`, `<=`, `>`, `>=` are blocked (they raise rather than falling through to native whole-`jsonb` comparison). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ore_cllw` extractors (entry equality / ordering) and `eql_v3.to_ste_vec_query(col)::jsonb jsonb_path_ops` GIN (containment); ordered leaves get a default btree opclass on `eql_v3.ore_cllw`. Every other native `jsonb` operator reachable through domain fallback (`?`, `?|`, `?&`, `@?`, `@@`, `#>`, `#>>`, `-`, `#-`, `||`, and the root comparisons above) is blocked — it raises rather than silently routing to plaintext-jsonb semantics. Note: because `eql_v3.json` is a `jsonb` domain, selector/operand literals must be typed (`col -> 'sel'::text` — the CipherStash Proxy interface passes typed parameters); a *bare untyped* literal (`col -> 'sel'`) resolves to the native `jsonb` operator instead of the encrypted one (PostgreSQL reduces the domain to its base type for unknown-typed literals), and the same caveat applies to the native-jsonb blockers — see `docs/decisions/2026-06-10-eql-v3-json-type-kind.md`. The whole surface owns its SEM index-term types (`eql_v3.hmac_256`, `eql_v3.ore_cllw`) and has no `eql_v2` dependency (CI-gated by `test:self_contained_v3` and the standalone `release/cipherstash-encrypt-v3.sql` installer). Why: a type-safe, searchable encrypted JSONB document column namespaced under `eql_v3`, complementing the scalar encrypted-domain families. The existing `eql_v2` SteVec surface on `eql_v2_encrypted` is unchanged. ([#267](https://github.com/cipherstash/encrypt-query-language/pull/267)) -- **`eql_v3` encrypted-domain schema, with the `int4` family as its first member.** Encrypted-domain type families now live in a new, additional `eql_v3` schema (the existing `eql_v2` schema is unchanged — it keeps the core types/operators and stays the documented public API). Four jsonb-backed domains for encrypted `int4` columns: `eql_v3.int4` (storage-only), `eql_v3.int4_eq` (`=` / `<>` via HMAC), and `eql_v3.int4_ord` / `eql_v3.int4_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms). Supported comparisons resolve to inlinable wrappers; the native `jsonb` operator surface reachable through domain fallback is blocked (raises rather than silently mis-resolving). Each domain's `CHECK` requires the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s), and pins the payload version (`VALUE->>'v' = '2'`, matching `eql_v2._encrypted_check_v`) — so a missing key or wrong-version payload is rejected on insert or cast rather than surfacing later at query time. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. The extractors return the searchable-encrypted-metadata index-term types `eql_v3.hmac_256` / `eql_v3.ore_block_u64_8_256`, which `eql_v3` owns directly (see the self-contained `eql_v3` schema entry below). Why: a type-safe, per-capability encrypted integer column instead of the untyped `eql_v2_encrypted`, namespaced under its own schema. This is the reference scalar implementation for the generated domain family. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239), supersedes [#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) +- **`eql_v3` encrypted-domain schema, with the `int4` family as its first member.** Encrypted-domain type families now live in a new, additional `eql_v3` schema (the existing `eql_v2` schema is unchanged — it keeps the core types/operators and stays the documented public API). Four jsonb-backed domains for encrypted `int4` columns: `eql_v3.int4` (storage-only), `eql_v3.int4_eq` (`=` / `<>` via HMAC), and `eql_v3.int4_ord` / `eql_v3.int4_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms). Supported comparisons resolve to inlinable wrappers; the native `jsonb` operator surface reachable through domain fallback is blocked (raises rather than silently mis-resolving). Each domain's `CHECK` requires the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s), and pins the payload version (`VALUE->>'v' = '2'`, matching `eql_v2._encrypted_check_v`) — so a missing key or wrong-version payload is rejected on insert or cast rather than surfacing later at query time. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. The extractors return the searchable-encrypted-metadata index-term types `eql_v3.hmac_256` / `eql_v3.ore_block_256`, which `eql_v3` owns directly (see the self-contained `eql_v3` schema entry below). Why: a type-safe, per-capability encrypted integer column instead of the untyped `eql_v2_encrypted`, namespaced under its own schema. This is the reference scalar implementation for the generated domain family. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239), supersedes [#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) - **`eql_v3.int2` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int2` columns — `eql_v3.int2` (storage-only), `eql_v3.int2_eq` (`=` / `<>` via HMAC), and `eql_v3.int2_ord` / `eql_v3.int2_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int2` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `smallint` column, proving the scalar generator generalizes beyond the `int4` reference. ([#243](https://github.com/cipherstash/encrypt-query-language/pull/243)) - **`eql_v3.int8` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int8` columns — `eql_v3.int8` (storage-only), `eql_v3.int8_eq` (`=` / `<>` via HMAC), and `eql_v3.int8_ord` / `eql_v3.int8_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int8` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `bigint` column, extending the scalar generator across the full 64-bit integer width. ([#253](https://github.com/cipherstash/encrypt-query-language/pull/253)) - **`eql_v3.date` encrypted-domain type family.** Four jsonb-backed domains for encrypted `date` columns — `eql_v3.date` (storage-only), `eql_v3.date_eq` (`=` / `<>` via HMAC), and `eql_v3.date_ord` / `eql_v3.date_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `date` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Plaintexts encrypt under the `date` cast and compare via the same ORE block terms as the integer scalars (ORE is plaintext-agnostic — dates order like integers). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: the first **non-integer ordered** scalar encrypted-domain type — a type-safe, per-capability encrypted `date` column — proving the generator and SQLx test matrix generalize beyond fixed-width integers. ([#256](https://github.com/cipherstash/encrypt-query-language/pull/256)) - **`eql_v3.timestamptz` encrypted-domain type family (equality-only).** Two jsonb-backed domains for encrypted `timestamptz` columns — `eql_v3.timestamptz` (storage-only) and `eql_v3.timestamptz_eq` (`=` / `<>` via HMAC) — generated from the `timestamptz` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.date` family. Values are **UTC-normalized** (cipherstash has no timezone-preserving type): plaintexts encrypt under the `timestamp` cast. Index via a functional index on the `eql_v3.eq_term` extractor, not an operator class on the domain. **Ordering (`<` `<=` `>` `>=`, `MIN` / `MAX`) is deferred:** cipherstash encrypts `Plaintext::Timestamp` at native 12-block ORE width, but EQL's only ORE comparator (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so ordered timestamptz domains would silently mis-order. There are no `eql_v3.timestamptz_ord` / `_ord_ore` domains and no timestamptz `MIN` / `MAX` aggregates until a wide-ORE (12-block) term lands — tracked in [#241](https://github.com/cipherstash/encrypt-query-language/issues/241). Why: a type-safe, equality-searchable encrypted UTC-timestamp column, stacking on the `date` temporal-scalar foundation; ordering follows once the comparator supports the native ciphertext width. ([#257](https://github.com/cipherstash/encrypt-query-language/pull/257)) - **Per-domain `MIN` / `MAX` aggregates for the encrypted-domain family.** `eql_v3.min(eql_v3._ord)` / `eql_v3.max(eql_v3._ord)` (and the `_ord_ore` twin) are generated for every ord-capable scalar variant, giving type-safe extrema on domain-typed columns — comparison routes through the variant's `<` / `>` operator (ORE block term, no decryption). The aggregates are declared `PARALLEL = SAFE` with a combine function (the state function itself — min/max are associative), so PostgreSQL can use partial/parallel aggregation on large `GROUP BY` workloads. Why: the new domain types previously had no equivalent of the composite-type aggregates. The existing `eql_v2.min(eql_v2_encrypted)` / `eql_v2.max(eql_v2_encrypted)` aggregates are **retained** and continue to work on `eql_v2_encrypted` columns; the per-domain aggregates are additive and coexist with them. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239)) - **`eql_v3.text` encrypted-domain family (`text`, `text_eq`, `text_match`, `text_ord`, `text_ord_ore`, `text_search`).** Adds equality (`=` / `<>` via HMAC), match (`@>` / `<@` via a new self-contained `eql_v3.bloom_filter` SEM index term), and ORE ordering (`<` `<=` `>` `>=`, `min` / `max`) for encrypted text, at parity with EQL v2 text — generated from the `text` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. `text` is the first scalar to add a new index `Term` (`Bloom`) and the first non-integer, unbounded ordered kind (lexicographic pivots, hand-written `impl ScalarType`). The combined **`text_search`** domain carries all three capabilities in one type — `=` / `<>` via HMAC, `<` `<=` `>` `>=` / `min` / `max` via ORE, and `@>` / `<@` via bloom filter. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` / `eql_v3.match_term` extractors, not an operator class on the domain. Why: brings searchable encrypted text to the namespaced, `eql_v2`-free `eql_v3` surface. Match is exposed as bloom-filter containment on the `text_match` / `text_search` domains — deliberately *not* SQL `LIKE` (no wildcard/anchoring; probabilistic ngram containment) — and never backs equality. **Equality on the ordered text domains (`text_ord`, `text_ord_ore`) and on `text_search` always routes `=` / `<>` through `hm` (exact HMAC), never the ORE term — ORE is not exact-equality for text** (integer ordered domains keep exact ORE equality, which is lossless for them). ([#260](https://github.com/cipherstash/encrypt-query-language/pull/260)) -- **Self-contained `eql_v3` schema + standalone `release/cipherstash-encrypt-v3.sql` installer.** The `eql_v3` encrypted-domain surface no longer depends on `eql_v2` at runtime: it now owns its own copies of the searchable-encrypted-metadata (SEM) index-term types — `eql_v3.hmac_256` and `eql_v3.ore_block_u64_8_256` (with its btree operator class) — so the `eql_v3.eq_term` / `eql_v3.ord_term` extractors return `eql_v3` types and no `eql_v2.` appears anywhere in the v3 SQL. The whole v3 surface relocated under a single `src/v3/` tree (`src/v3/sem/` for the hand-written SEM types, `src/v3/scalars/` for the generated domain families). A new build variant ships the `eql_v3` schema on its own as `release/cipherstash-encrypt-v3.sql`, installable into a database with no `eql_v2` present; a CI gate greps that artifact and its dependency closure to keep it `eql_v2`-free. Why: a clean foundation for the per-scalar encrypted-domain model to stand alone, ahead of it replacing the `eql_v2_encrypted` composite column type. This is additive — a new schema and a new artifact — and leaves `eql_v2` byte-for-byte unchanged. ([#255](https://github.com/cipherstash/encrypt-query-language/pull/255)) +- **Self-contained `eql_v3` schema + standalone `release/cipherstash-encrypt-v3.sql` installer.** The `eql_v3` encrypted-domain surface no longer depends on `eql_v2` at runtime: it now owns its own copies of the searchable-encrypted-metadata (SEM) index-term types — `eql_v3.hmac_256` and `eql_v3.ore_block_256` (with its btree operator class) — so the `eql_v3.eq_term` / `eql_v3.ord_term` extractors return `eql_v3` types and no `eql_v2.` appears anywhere in the v3 SQL. The whole v3 surface relocated under a single `src/v3/` tree (`src/v3/sem/` for the hand-written SEM types, `src/v3/scalars/` for the generated domain families). A new build variant ships the `eql_v3` schema on its own as `release/cipherstash-encrypt-v3.sql`, installable into a database with no `eql_v2` present; a CI gate greps that artifact and its dependency closure to keep it `eql_v2`-free. Why: a clean foundation for the per-scalar encrypted-domain model to stand alone, ahead of it replacing the `eql_v2_encrypted` composite column type. This is additive — a new schema and a new artifact — and leaves `eql_v2` byte-for-byte unchanged. ([#255](https://github.com/cipherstash/encrypt-query-language/pull/255)) - **`eql_v3.min` / `eql_v3.max` aggregates over `eql_v3.ste_vec_entry`.** SteVec document entries extracted at a selector (`doc -> 'sel'`) can now be aggregated like ordered scalars: `eql_v3.min(doc -> 'sel')` / `eql_v3.max(...)` return the entry with the smallest / largest ordered leaf. Ordering routes through the entry's `oc` (CLLW ORE) term via `eql_v3.ore_cllw` — the same comparator the entry `<` / `<=` / `>` / `>=` operators use, not the scalar Block-ORE `ord_term`. Only `oc`-carrying entries are orderable: an entry without an `oc` term (`eql_v3.ore_cllw` returns NULL) is non-orderable and is ignored by the aggregate — the same way the `eql_v3.ore_cllw` btree NULL-filters such rows — so a mix of `oc`-carrying and `oc`-less entries yields the extremum of the orderable subset rather than a corrupted result. Declared `PARALLEL = SAFE` with a combine function (the state function itself), so partial / parallel aggregation is available on large `GROUP BY` workloads. Why: brings encrypted-JSONB entry ordering to parity with the scalar encrypted-domain families' `MIN` / `MAX`, and lets the shared scalar behaviour matrix cover entry aggregation. Additive — the document and entry comparison surface is otherwise unchanged. ([#267](https://github.com/cipherstash/encrypt-query-language/pull/267)) ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 6005f72d..34149104 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ This project uses `mise` for task management. Common commands: This is the **Encrypt Query Language (EQL)** - a PostgreSQL extension for searchable encryption. Key architectural components: ### Core Structure -- **Schema**: Core EQL functions/types are in the `eql_v2` PostgreSQL schema. The encrypted-domain type families (`int4` and future scalar domains) live in a separate `eql_v3` schema (see below). The `eql_v3` surface is **self-contained**: it owns its own copies of the searchable-encrypted-metadata (SEM) index-term types (`eql_v3.hmac_256`, `eql_v3.ore_block_u64_8_256`, hand-written under `src/v3/sem/`) and has no runtime dependency on `eql_v2`. `eql_v2` is unchanged and remains the documented public API. +- **Schema**: Core EQL functions/types are in the `eql_v2` PostgreSQL schema. The encrypted-domain type families (`int4` and future scalar domains) live in a separate `eql_v3` schema (see below). The `eql_v3` surface is **self-contained**: it owns its own copies of the searchable-encrypted-metadata (SEM) index-term types (`eql_v3.hmac_256`, `eql_v3.ore_block_256`, hand-written under `src/v3/sem/`) and has no runtime dependency on `eql_v2`. `eql_v2` is unchanged and remains the documented public API. - **Main Type**: `eql_v2_encrypted` - composite type for encrypted columns (stored as JSONB) - **Configuration**: `eql_v2_configuration` table tracks encryption configs - **Index Types**: Various encrypted index types (blake3, hmac_256, bloom_filter, ore variants) @@ -64,7 +64,7 @@ This is the **Encrypt Query Language (EQL)** - a PostgreSQL extension for search - `src/operators/` - SQL operators for encrypted data comparisons - `src/config/` - Configuration management functions - `src/blake3/`, `src/hmac_256/`, `src/bloom_filter/`, `src/ore_*` - Index implementations -- `src/v3/` - Self-contained `eql_v3` surface: `src/v3/schema.sql`, forked `src/v3/crypto.sql` / `src/v3/common.sql`, hand-written SEM index-term types under `src/v3/sem/` (`hmac_256`, `ore_block_u64_8_256`), and the generated scalar encrypted-domain families under `src/v3/scalars//` (plus the shared blocker `src/v3/scalars/functions.sql`) +- `src/v3/` - Self-contained `eql_v3` surface: `src/v3/schema.sql`, forked `src/v3/crypto.sql` / `src/v3/common.sql`, hand-written SEM index-term types under `src/v3/sem/` (`hmac_256`, `ore_block_256`), and the generated scalar encrypted-domain families under `src/v3/scalars//` (plus the shared blocker `src/v3/scalars/functions.sql`) - `tasks/` - mise task scripts - `tests/sqlx/` - Rust/SQLx test framework (PostgreSQL 14-17 support) - `release/` - Generated SQL installation files @@ -78,7 +78,7 @@ This is the **Encrypt Query Language (EQL)** - a PostgreSQL extension for search ### Encrypted-Domain Types -`src/v3/scalars/` holds the generated **encrypted-domain type families** — jsonb-backed PostgreSQL domains in the **`eql_v3` schema**, one domain per operator/index capability (`eql_v3.` storage-only, `eql_v3._eq`, `eql_v3._ord`). The schema qualifier replaces the old version-prefixed name, so the domains are `eql_v3.int4`, `eql_v3.int4_eq`, `eql_v3.int4_ord`, `eql_v3.int4_ord_ore` — created in `eql_v3`, not `public`. Their extractors/wrappers/aggregates (`eql_v3.eq_term`, `eql_v3.ord_term`, `eql_v3.eq`/`lt`/…, `eql_v3.min`/`max`) also live in `eql_v3`, and the SEM index-term types they return and construct (`eql_v3.hmac_256`, `eql_v3.ore_block_u64_8_256`) are **also `eql_v3`** — hand-written under `src/v3/sem/` so the whole v3 surface is self-contained (no `eql_v2.` appears anywhere in v3 SQL; CI gates this via `mise run test:self_contained_v3` and the standalone `release/cipherstash-encrypt-v3.sql` installer). `eql_v3.int4` (PR #239, supersedes #225) is the reference scalar implementation; future scalar types such as `int8`, `bool`, `date`, `float`, `numeric`, `timestamp`, `text`, and `jsonb` follow this materializer pattern. `text`, `numeric`, and `jsonb` are planned but have no generated SQL surface yet — `jsonb` in particular needs a separate SQL design beyond the ordered-scalar materializer. The `eql-scalars` fixture catalog (`crates/eql-scalars`) already models their fixture values ahead of the SQL surface. +`src/v3/scalars/` holds the generated **encrypted-domain type families** — jsonb-backed PostgreSQL domains in the **`eql_v3` schema**, one domain per operator/index capability (`eql_v3.` storage-only, `eql_v3._eq`, `eql_v3._ord`). The schema qualifier replaces the old version-prefixed name, so the domains are `eql_v3.int4`, `eql_v3.int4_eq`, `eql_v3.int4_ord`, `eql_v3.int4_ord_ore` — created in `eql_v3`, not `public`. Their extractors/wrappers/aggregates (`eql_v3.eq_term`, `eql_v3.ord_term`, `eql_v3.eq`/`lt`/…, `eql_v3.min`/`max`) also live in `eql_v3`, and the SEM index-term types they return and construct (`eql_v3.hmac_256`, `eql_v3.ore_block_256`) are **also `eql_v3`** — hand-written under `src/v3/sem/` so the whole v3 surface is self-contained (no `eql_v2.` appears anywhere in v3 SQL; CI gates this via `mise run test:self_contained_v3` and the standalone `release/cipherstash-encrypt-v3.sql` installer). `eql_v3.int4` (PR #239, supersedes #225) is the reference scalar implementation; future scalar types such as `int8`, `bool`, `date`, `float`, `numeric`, `timestamp`, `text`, and `jsonb` follow this materializer pattern. `text`, `numeric`, and `jsonb` are planned but have no generated SQL surface yet — `jsonb` in particular needs a separate SQL design beyond the ordered-scalar materializer. The `eql-scalars` fixture catalog (`crates/eql-scalars`) already models their fixture values ahead of the SQL surface. Adding a scalar encrypted-domain type is one row in the Rust catalog `eql-scalars::CATALOG` (`crates/eql-scalars/src/lib.rs`): a `ScalarSpec` giving the type `token` (e.g. `int8`), its `ScalarKind` (the `kind` field), the `DomainSpec`s mapping each generated domain suffix to its fixed index `Term`s (`_eq => [Hm]`, `_ord`/`_ord_ore => [Ore]`), and the `Fixture` value list. Term capabilities are fixed in the `Term` enum's `impl` methods (with unit tests): `Hm` provides equality, and `Ore` provides equality plus ordering. There is no TOML manifest and no Python — the catalog is the source of truth, validated by the compiler (an undefined term or unknown scalar is a compile error) plus catalog `#[test]`s. `mise run build` runs `cargo run -p eql-codegen`, which regenerates the scalar SQL surface into `src/v3/scalars//` from `CATALOG` at the start of every build; that surface includes supported comparison wrappers plus blockers for native `jsonb` operators that would otherwise be reachable through domain fallback. `cargo run -p eql-codegen` regenerates every type at once (the same call `mise run build` uses; there is no per-type codegen task). The generated `*_types.sql` / `*_functions.sql` / `*_operators.sql` / `*_aggregates.sql` files are gitignored and never committed. The per-type plaintext fixture lists the SQLx matrix consumes are **not** a generated file — they are materialised from each `CATALOG` row at compile time as `eql_scalars::INT4_VALUES` / `INT2_VALUES` (the `int_values!` macro) and read directly by `ScalarType::FIXTURE_VALUES`; a Rust source of truth no longer round-trips through a committed generated `.rs`. Generated SQL carries a `-- AUTOMATICALLY GENERATED FILE` header (the project-wide marker `docs:validate` greps on); change the catalog and rebuild, never hand-edit. Hand-written SQL beyond the fixed surface goes in `src/v3/scalars//_extensions.sql` with no auto-generated header and explicit `-- REQUIRE:` edges — that file IS committed. `text` and `jsonb` are out of scope for this scalar materializer. diff --git a/crates/eql-codegen/src/context.rs b/crates/eql-codegen/src/context.rs index 15aeacc3..d5b928f3 100644 --- a/crates/eql-codegen/src/context.rs +++ b/crates/eql-codegen/src/context.rs @@ -133,7 +133,7 @@ pub struct FunctionsContext { /// Build the inlinable index-extractor entry for a domain term. /// /// The `RETURNS` type name equals the constructor name (`hmac_256`, -/// `ore_block_u64_8_256`); qualify it with `SCHEMA` — the same schema as the +/// `ore_block_256`); qualify it with `SCHEMA` — the same schema as the /// body's constructor call — so the declared return type and the call stay in /// lockstep. `Term::returns()` is intentionally not used. pub fn extractor_entry(term: Term) -> FnEntry { diff --git a/crates/eql-codegen/src/generate.rs b/crates/eql-codegen/src/generate.rs index af6c9cbc..49af8967 100644 --- a/crates/eql-codegen/src/generate.rs +++ b/crates/eql-codegen/src/generate.rs @@ -468,7 +468,7 @@ mod tests { let sql = render_functions_file(s.token, domain(s, "_ord")); assert_eq!(sql.matches("CREATE FUNCTION").count(), 45); assert!(sql.contains("CREATE FUNCTION eql_v3.ord_term(a eql_v3.int4_ord)")); - assert!(sql.contains("RETURNS eql_v3.ore_block_u64_8_256")); + assert!(sql.contains("RETURNS eql_v3.ore_block_256")); assert_eq!( sql.matches("LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE") .count(), diff --git a/crates/eql-scalars/src/term.rs b/crates/eql-scalars/src/term.rs index 461a1714..5db834b8 100644 --- a/crates/eql-scalars/src/term.rs +++ b/crates/eql-scalars/src/term.rs @@ -28,7 +28,7 @@ impl Term { pub const fn ctor(self) -> &'static str { match self { Term::Hm => "hmac_256", - Term::Ore => "ore_block_u64_8_256", + Term::Ore => "ore_block_256", Term::Bloom => "bloom_filter", } } @@ -57,8 +57,8 @@ impl Term { match self { Term::Hm => &["src/v3/sem/hmac_256/functions.sql"], Term::Ore => &[ - "src/v3/sem/ore_block_u64_8_256/functions.sql", - "src/v3/sem/ore_block_u64_8_256/operators.sql", + "src/v3/sem/ore_block_256/functions.sql", + "src/v3/sem/ore_block_256/operators.sql", ], Term::Bloom => &["src/v3/sem/bloom_filter/functions.sql"], } diff --git a/crates/eql-scalars/src/tests.rs b/crates/eql-scalars/src/tests.rs index 017de217..85341337 100644 --- a/crates/eql-scalars/src/tests.rs +++ b/crates/eql-scalars/src/tests.rs @@ -193,14 +193,14 @@ mod term_tests { let ore = Term::Ore; assert_eq!(ore.json_key(), "ob"); assert_eq!(ore.extractor(), "ord_term"); - assert_eq!(ore.ctor(), "ore_block_u64_8_256"); + assert_eq!(ore.ctor(), "ore_block_256"); assert_eq!(ore.role(), Role::Ord); assert_eq!(ore.operators(), &["=", "<>", "<", "<=", ">", ">="]); assert_eq!( ore.requires(), &[ - "src/v3/sem/ore_block_u64_8_256/functions.sql", - "src/v3/sem/ore_block_u64_8_256/operators.sql", + "src/v3/sem/ore_block_256/functions.sql", + "src/v3/sem/ore_block_256/operators.sql", ] ); } @@ -277,8 +277,8 @@ mod term_helper_tests { assert_eq!( Term::term_requires(&[Term::Ore, Term::Ore, Term::Hm]), vec![ - "src/v3/sem/ore_block_u64_8_256/functions.sql", - "src/v3/sem/ore_block_u64_8_256/operators.sql", + "src/v3/sem/ore_block_256/functions.sql", + "src/v3/sem/ore_block_256/operators.sql", "src/v3/sem/hmac_256/functions.sql", ] ); diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index f3256e16..df19baff 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -34,7 +34,7 @@ Shared wire fields are reusable newtypes in |---------|----------|-------|-------| | `Ciphertext` | `c` | `String` | every domain (envelope) | | `Hmac256` | `hm` | `String` | `_eq` domains | -| `OreBlockU64_8_256` | `ob` | `Vec` | `_ord` / `_ord_ore` domains | +| `OreBlock256` | `ob` | `Vec` | `_ord` / `_ord_ore` domains | | `BloomFilter` | `bf` | `Vec` (signed!) | `_match` domains | Note "v3" names the SQL schema generation (`eql_v3.*`); the JSON envelope diff --git a/crates/eql-types/src/v3/date.rs b/crates/eql-types/src/v3/date.rs index 00cb706a..8fda75a0 100644 --- a/crates/eql-types/src/v3/date.rs +++ b/crates/eql-types/src/v3/date.rs @@ -3,7 +3,7 @@ //! ciphertext, so dates order like integers); see that module for the //! capability table. -use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -68,7 +68,7 @@ pub struct DateOrdOre { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for DateOrdOre { @@ -93,7 +93,7 @@ pub struct DateOrd { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for DateOrd { diff --git a/crates/eql-types/src/v3/int2.rs b/crates/eql-types/src/v3/int2.rs index b641408d..d0ae5834 100644 --- a/crates/eql-types/src/v3/int2.rs +++ b/crates/eql-types/src/v3/int2.rs @@ -1,7 +1,7 @@ //! The `int2` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -66,7 +66,7 @@ pub struct Int2OrdOre { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int2OrdOre { @@ -91,7 +91,7 @@ pub struct Int2Ord { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int2Ord { diff --git a/crates/eql-types/src/v3/int4.rs b/crates/eql-types/src/v3/int4.rs index 44dbcf34..e107b482 100644 --- a/crates/eql-types/src/v3/int4.rs +++ b/crates/eql-types/src/v3/int4.rs @@ -7,7 +7,7 @@ //! | [`Int4OrdOre`] | `eql_v3.int4_ord_ore` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | //! | [`Int4Ord`] | `eql_v3.int4_ord` | `v` `i` `c` `ob` | `=` `<>` `<` `<=` `>` `>=` | -use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -74,7 +74,7 @@ pub struct Int4OrdOre { pub c: Ciphertext, /// Block-ORE order term. Serves equality too — ORE over a /// full-domain `int4` is lossless, so no separate `hm` is carried. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int4OrdOre { @@ -99,7 +99,7 @@ pub struct Int4Ord { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int4Ord { diff --git a/crates/eql-types/src/v3/int8.rs b/crates/eql-types/src/v3/int8.rs index 4ab0a232..29526409 100644 --- a/crates/eql-types/src/v3/int8.rs +++ b/crates/eql-types/src/v3/int8.rs @@ -1,7 +1,7 @@ //! The `int8` encrypted-domain family. Same four-domain ordered shape as //! [`crate::v3::int4`] — see that module for the capability table. -use crate::v3::terms::{Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -66,7 +66,7 @@ pub struct Int8OrdOre { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int8OrdOre { @@ -91,7 +91,7 @@ pub struct Int8Ord { /// mp_base85 source ciphertext. Required by the domain CHECK. pub c: Ciphertext, /// Block-ORE order term. Serves equality too. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for Int8Ord { diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index ddad74bf..f5319ed7 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -26,9 +26,9 @@ pub struct Hmac256(pub String); /// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the /// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless /// over the scalar's domain, so it serves equality too. SQL-side constructor: -/// `eql_v3.ore_block_u64_8_256`. +/// `eql_v3.ore_block_256`. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct OreBlockU64_8_256(pub Vec); +pub struct OreBlock256(pub Vec); /// Bloom-filter match term — the `bf` wire key. Backs the `_match` domains /// (`~~` containment via `@>`/`<@`). @@ -51,7 +51,7 @@ impl From for Hmac256 { } } -impl From> for OreBlockU64_8_256 { +impl From> for OreBlock256 { fn from(value: Vec) -> Self { Self(value) } diff --git a/crates/eql-types/src/v3/text.rs b/crates/eql-types/src/v3/text.rs index ed905ca2..c0270fad 100644 --- a/crates/eql-types/src/v3/text.rs +++ b/crates/eql-types/src/v3/text.rs @@ -2,7 +2,7 @@ //! [`crate::v3::int4`] plus a `_match` domain backed by the Bloom-filter //! term (`@>`/`<@` containment for `LIKE`-style matching). -use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlockU64_8_256}; +use crate::v3::terms::{BloomFilter, Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -97,7 +97,7 @@ pub struct TextOrdOre { /// HMAC-SHA-256 equality term. Text routes `=`/`<>` through `hm`. pub hm: Hmac256, /// Block-ORE order term. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for TextOrdOre { @@ -126,7 +126,7 @@ pub struct TextOrd { /// HMAC-SHA-256 equality term. Text routes `=`/`<>` through `hm`. pub hm: Hmac256, /// Block-ORE order term. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, } impl DomainType for TextOrd { @@ -155,7 +155,7 @@ pub struct TextSearch { /// HMAC-SHA-256 equality term. pub hm: Hmac256, /// Block-ORE order term. - pub ob: OreBlockU64_8_256, + pub ob: OreBlock256, /// Bloom-filter match term (signed smallint bit positions). pub bf: BloomFilter, } diff --git a/docs/analysis/2026-06-11-v3-scalar-vs-jsonb-test-coverage.md b/docs/analysis/2026-06-11-v3-scalar-vs-jsonb-test-coverage.md index 474fb78e..d4cac1ee 100644 --- a/docs/analysis/2026-06-11-v3-scalar-vs-jsonb-test-coverage.md +++ b/docs/analysis/2026-06-11-v3-scalar-vs-jsonb-test-coverage.md @@ -125,7 +125,7 @@ Source of truth: `crates/eql-scalars/src` (`CATALOG`). Adding a type is one `Sca | Term | Key | Extractor → type | Provides | Operators | |---|---|---|---|---| | `Hm` | `hm` | `eq_term` → `eql_v3.hmac_256` | equality | `=` `<>` | -| `Ore` | `ob` | `ord_term` → `eql_v3.ore_block_u64_8_256` | equality + ordering | `=` `<>` `<` `<=` `>` `>=` | +| `Ore` | `ob` | `ord_term` → `eql_v3.ore_block_256` | equality + ordering | `=` `<>` `<` `<=` `>` `>=` | | `Bloom` | `bf` | `match_term` → `eql_v3.bloom_filter` | containment | `@>` `<@` | Domain → role: empty ⇒ Storage, first term `Hm` ⇒ Eq, `Ore` ⇒ Ord, `Bloom` ⇒ Match. diff --git a/docs/reference/adding-a-scalar-encrypted-domain-type.md b/docs/reference/adding-a-scalar-encrypted-domain-type.md index 6c760f4f..76d987dc 100644 --- a/docs/reference/adding-a-scalar-encrypted-domain-type.md +++ b/docs/reference/adding-a-scalar-encrypted-domain-type.md @@ -13,7 +13,7 @@ A scalar encrypted-domain type is a family of concrete `jsonb` domains in the an `eql_v2` uninstall. Their extractors, comparison wrappers, and MIN/MAX aggregates also live in `eql_v3`; the searchable-encrypted-metadata (SEM) index-term types they return (`eql_v3.hmac_256`, -`eql_v3.ore_block_u64_8_256`) are **also `eql_v3`** — hand-written under +`eql_v3.ore_block_256`) are **also `eql_v3`** — hand-written under `src/v3/sem/`. The whole v3 surface is self-contained: it owns every type it needs and has no runtime dependency on `eql_v2` (CI gates this — see §6). @@ -124,7 +124,7 @@ behaviour change, not a refactor: | Term | JSON key | Extractor | Returns | Operators | | ------- | -------- | ------------ | -------------------------------- | -------------------------- | | `Hm` | `hm` | `eq_term` | `eql_v3.hmac_256` | `=` `<>` | -| `Ore` | `ob` | `ord_term` | `eql_v3.ore_block_u64_8_256` | `=` `<>` `<` `<=` `>` `>=` | +| `Ore` | `ob` | `ord_term` | `eql_v3.ore_block_256` | `=` `<>` `<` `<=` `>` `>=` | | `Bloom` | `bf` | `match_term` | `eql_v3.bloom_filter` | `@>` `<@` | A type that needs a non-ORE equality term on an ordered domain needs a **new @@ -558,8 +558,8 @@ CREATE INDEX ... ON table_name USING btree (eql_v3.ord_term(col)); CREATE INDEX ... ON table_name USING hash (eql_v3.eq_term(col)); ``` -`ore` depends on `src/v3/sem/ore_block_u64_8_256/functions.sql` and -`src/v3/sem/ore_block_u64_8_256/operators.sql`; `hm` depends on +`ore` depends on `src/v3/sem/ore_block_256/functions.sql` and +`src/v3/sem/ore_block_256/operators.sql`; `hm` depends on `src/v3/sem/hmac_256/functions.sql`. ### Extension files @@ -623,7 +623,7 @@ edits: extractor names (`eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `eq_term`, `ord_term`, the `Bloom` term's `match_term` extractor and its `contains` / `contained_by` containment wrappers) plus the generated `min` / `max` aggregates and the SEM - `hmac_256` / `ore_block_u64_8_256` / `bloom_filter` constructors are already + `hmac_256` / `ore_block_256` / `bloom_filter` constructors are already covered by `eql_v3`-schema entries. A new scalar type inherits coverage; **a new term needs splinter entries for each new name it introduces — both its extractor and its comparison wrappers** (adding `Bloom` required `match_term`, @@ -665,7 +665,7 @@ recognises exactly these two forms; any other argument is a usage error. The generator targets the `eql_v3` schema throughout: `SCHEMA = "eql_v3"` (`crates/eql-codegen/src/consts.rs`) qualifies both the domain families and the SEM index-term types the extractors return (`eql_v3.hmac_256`, -`eql_v3.ore_block_u64_8_256`), so no generated SQL references `eql_v2`. +`eql_v3.ore_block_256`), so no generated SQL references `eql_v2`. `tasks/build.sh` runs `cargo run -p eql-codegen` at the start of every `mise run build`, so the generated SQL is never checked in. (The build first sweeps every diff --git a/docs/reference/eql-functions.md b/docs/reference/eql-functions.md index b610349c..91c4013f 100644 --- a/docs/reference/eql-functions.md +++ b/docs/reference/eql-functions.md @@ -435,8 +435,8 @@ SEM index-term types. ```sql -- int4 — generated for every scalar type's eq / ord variants. eql_v3.eq_term(a eql_v3.int4_eq) RETURNS eql_v3.hmac_256 -eql_v3.ord_term(a eql_v3.int4_ord) RETURNS eql_v3.ore_block_u64_8_256 -eql_v3.ord_term(a eql_v3.int4_ord_ore) RETURNS eql_v3.ore_block_u64_8_256 +eql_v3.ord_term(a eql_v3.int4_ord) RETURNS eql_v3.ore_block_256 +eql_v3.ord_term(a eql_v3.int4_ord_ore) RETURNS eql_v3.ore_block_256 ``` **Example:** diff --git a/docs/reference/sql-support.md b/docs/reference/sql-support.md index 82770ad9..9cd7e544 100644 --- a/docs/reference/sql-support.md +++ b/docs/reference/sql-support.md @@ -61,7 +61,7 @@ Use the equivalent [`jsonb_path_query`](#jsonb-functions-and-selectors-enabled-b ## Encrypted-domain scalar types (`eql_v3.`) -Scalar encrypted-domain types (e.g. `eql_v3.int4`; see [Adding a Scalar Encrypted-Domain Type](./adding-a-scalar-encrypted-domain-type.md)) are a different access model from the matrix above. Instead of configuring a search index on an `eql_v2_encrypted` column, you type the column as a specific domain *variant* whose operator surface is fixed at generation time. The index terms travel in the payload; there is no `add_search_config` step. The domains and their operator surface live in the `eql_v3` schema (dropped by `DROP SCHEMA eql_v3 CASCADE`, and they survive an `eql_v2` uninstall); their extracted index-term types are the self-contained `eql_v3` SEM types (`eql_v3.hmac_256`, `eql_v3.ore_block_u64_8_256`). +Scalar encrypted-domain types (e.g. `eql_v3.int4`; see [Adding a Scalar Encrypted-Domain Type](./adding-a-scalar-encrypted-domain-type.md)) are a different access model from the matrix above. Instead of configuring a search index on an `eql_v2_encrypted` column, you type the column as a specific domain *variant* whose operator surface is fixed at generation time. The index terms travel in the payload; there is no `add_search_config` step. The domains and their operator surface live in the `eql_v3` schema (dropped by `DROP SCHEMA eql_v3 CASCADE`, and they survive an `eql_v2` uninstall); their extracted index-term types are the self-contained `eql_v3` SEM types (`eql_v3.hmac_256`, `eql_v3.ore_block_256`). Each scalar type `` generates one storage-only variant plus eq/ord query variants: diff --git a/src/v3/schema.sql b/src/v3/schema.sql index 41be4d40..7ec2f51f 100644 --- a/src/v3/schema.sql +++ b/src/v3/schema.sql @@ -4,7 +4,7 @@ --! Creates the eql_v3 schema, which houses the self-contained encrypted-domain --! type families (eql_v3.int4, eql_v3.int8, and future scalar domains): their --! jsonb-backed domains, the searchable-encrypted-metadata (SEM) index-term ---! types they use (eql_v3.hmac_256, eql_v3.ore_block_u64_8_256), the index-term +--! types they use (eql_v3.hmac_256, eql_v3.ore_block_256), the index-term --! extractors, comparison wrappers, blockers, and aggregates. The v3 surface is --! self-contained — it owns every type it needs and has no runtime dependency --! on another EQL schema. diff --git a/src/v3/sem/bloom_filter/functions.sql b/src/v3/sem/bloom_filter/functions.sql index 291ba736..d4c782c5 100644 --- a/src/v3/sem/bloom_filter/functions.sql +++ b/src/v3/sem/bloom_filter/functions.sql @@ -16,7 +16,7 @@ --! @return boolean True when the `bf` key is present and non-null. --! --! @internal Defined for parity with the eql_v3 SEM index-term predicates ---! (`has_hmac_256` / `has_ore_block_u64_8_256`); it is not currently called by +--! (`has_hmac_256` / `has_ore_block_256`); it is not currently called by --! the extractor below, which gates on value-shape inline, nor by the generated --! domain CHECK, which tests `bf` presence via the envelope-key skeleton. Kept --! as the canonical presence test for callers that need one. diff --git a/src/v3/sem/ore_block_u64_8_256/functions.sql b/src/v3/sem/ore_block_256/functions.sql similarity index 76% rename from src/v3/sem/ore_block_u64_8_256/functions.sql rename to src/v3/sem/ore_block_256/functions.sql index 2a13ef31..7d5bce88 100644 --- a/src/v3/sem/ore_block_u64_8_256/functions.sql +++ b/src/v3/sem/ore_block_256/functions.sql @@ -1,12 +1,12 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/crypto.sql -- REQUIRE: src/v3/common.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/types.sql +-- REQUIRE: src/v3/sem/ore_block_256/types.sql ---! @file v3/sem/ore_block_u64_8_256/functions.sql +--! @file v3/sem/ore_block_256/functions.sql --! @brief ORE block construction, extraction, and comparison (eql_v3 SEM). --! ---! jsonb-only subset of src/ore_block_u64_8_256/functions.sql. The +--! jsonb-only subset of src/ore_block_256/functions.sql. The --! encrypted-column overloads are omitted; the helper jsonb_array_to_bytea_array --! and pgcrypto encrypt() are reached via the forked src/v3/common.sql and --! src/v3/crypto.sql so the whole closure stays under src/v3. (Doc comments @@ -16,7 +16,7 @@ --! @brief Convert JSONB array to ORE block composite type --! @internal --! @param val jsonb Array of hex-encoded ORE block terms ---! @return eql_v3.ore_block_u64_8_256 ORE block composite, or NULL if input is null +--! @return eql_v3.ore_block_256 ORE block composite, or NULL if input is null --! @note Inlinable `LANGUAGE sql` IMMUTABLE form (no `SET search_path`) so the --! planner can fold this per-encrypted-value helper into the calling query. --! This deliberately diverges from the v2 plpgsql equivalent (intentionally @@ -25,15 +25,15 @@ --! NULL here instead of raising. The sole caller passes `val->'ob'`, always an --! array or JSON null, so the divergence is unreachable in practice; JSON null --! and empty array still return NULL exactly as before. -CREATE FUNCTION eql_v3.jsonb_array_to_ore_block_u64_8_256(val jsonb) -RETURNS eql_v3.ore_block_u64_8_256 +CREATE FUNCTION eql_v3.jsonb_array_to_ore_block_256(val jsonb) +RETURNS eql_v3.ore_block_256 IMMUTABLE AS $$ SELECT CASE WHEN jsonb_typeof(val) = 'array' THEN ROW(( - SELECT array_agg(ROW(b)::eql_v3.ore_block_u64_8_256_term) + SELECT array_agg(ROW(b)::eql_v3.ore_block_256_term) FROM unnest(eql_v3.jsonb_array_to_bytea_array(val)) AS b - ))::eql_v3.ore_block_u64_8_256 + ))::eql_v3.ore_block_256 ELSE NULL END; $$ LANGUAGE sql; @@ -43,24 +43,24 @@ $$ LANGUAGE sql; --! SQL-function inlining. It takes a bare `jsonb` arg (not a jsonb-backed --! encrypted DOMAIN), so the structural skip in tasks/pin_search_path.sql does --! not recognise it; this marker is the documented manual opt-in. -COMMENT ON FUNCTION eql_v3.jsonb_array_to_ore_block_u64_8_256(jsonb) IS +COMMENT ON FUNCTION eql_v3.jsonb_array_to_ore_block_256(jsonb) IS 'eql-inline-critical: per-encrypted-value ORE helper; must stay inlinable (unpinned search_path)'; --! @brief Extract ORE block index term from JSONB payload --! @param val jsonb containing encrypted EQL payload ---! @return eql_v3.ore_block_u64_8_256 ORE block index term +--! @return eql_v3.ore_block_256 ORE block index term --! @throws Exception if 'ob' field is missing -CREATE FUNCTION eql_v3.ore_block_u64_8_256(val jsonb) - RETURNS eql_v3.ore_block_u64_8_256 +CREATE FUNCTION eql_v3.ore_block_256(val jsonb) + RETURNS eql_v3.ore_block_256 IMMUTABLE STRICT PARALLEL SAFE SET search_path = pg_catalog, extensions, public AS $$ BEGIN -- Declared STRICT: PostgreSQL returns NULL for a NULL argument without -- entering the body, so no explicit `val IS NULL` guard is needed. - IF eql_v3.has_ore_block_u64_8_256(val) THEN - RETURN eql_v3.jsonb_array_to_ore_block_u64_8_256(val->'ob'); + IF eql_v3.has_ore_block_256(val) THEN + RETURN eql_v3.jsonb_array_to_ore_block_256(val->'ob'); END IF; RAISE 'Expected an ore index (ob) value in json: %', val; END; @@ -70,7 +70,7 @@ $$ LANGUAGE plpgsql; --! @brief Check if JSONB payload contains ORE block index term --! @param val jsonb containing encrypted EQL payload --! @return boolean True if 'ob' field is present and non-null -CREATE FUNCTION eql_v3.has_ore_block_u64_8_256(val jsonb) +CREATE FUNCTION eql_v3.has_ore_block_256(val jsonb) RETURNS boolean IMMUTABLE STRICT PARALLEL SAFE SET search_path = pg_catalog, extensions, public @@ -83,18 +83,18 @@ $$ LANGUAGE plpgsql; --! @brief Compare two ORE block terms using cryptographic comparison --! @internal ---! @param a eql_v3.ore_block_u64_8_256_term First ORE term ---! @param b eql_v3.ore_block_u64_8_256_term Second ORE term +--! @param a eql_v3.ore_block_256_term First ORE term +--! @param b eql_v3.ore_block_256_term Second ORE term --! @return integer -1 if a < b, 0 if a = b, 1 if a > b --! @throws Exception if ciphertexts are different lengths ---! @note Marked `IMMUTABLE` (the three `compare_ore_block_u64_8_256_term(s)` +--! @note Marked `IMMUTABLE` (the three `compare_ore_block_256_term(s)` --! overloads all are). This deliberately diverges from the v2 originals, --! which carry no volatility marker and so default to `VOLATILE`. The --! comparison is deterministic — its only crypto call, pgcrypto `encrypt()`, --! is itself `IMMUTABLE STRICT PARALLEL SAFE` — so `IMMUTABLE` lets the --! planner fold/cache these in ordering and index contexts. NOT `STRICT`: --! the NULL-handling branches below are load-bearing for the array overload. -CREATE FUNCTION eql_v3.compare_ore_block_u64_8_256_term(a eql_v3.ore_block_u64_8_256_term, b eql_v3.ore_block_u64_8_256_term) +CREATE FUNCTION eql_v3.compare_ore_block_256_term(a eql_v3.ore_block_256_term, b eql_v3.ore_block_256_term) RETURNS integer IMMUTABLE SET search_path = pg_catalog, extensions, public @@ -170,10 +170,10 @@ $$ LANGUAGE plpgsql; --! @brief Compare arrays of ORE block terms recursively --! @internal ---! @param a eql_v3.ore_block_u64_8_256_term[] First array ---! @param b eql_v3.ore_block_u64_8_256_term[] Second array +--! @param a eql_v3.ore_block_256_term[] First array +--! @param b eql_v3.ore_block_256_term[] Second array --! @return integer -1/0/1, or NULL if either array is NULL -CREATE FUNCTION eql_v3.compare_ore_block_u64_8_256_terms(a eql_v3.ore_block_u64_8_256_term[], b eql_v3.ore_block_u64_8_256_term[]) +CREATE FUNCTION eql_v3.compare_ore_block_256_terms(a eql_v3.ore_block_256_term[], b eql_v3.ore_block_256_term[]) RETURNS integer IMMUTABLE SET search_path = pg_catalog, extensions, public @@ -197,10 +197,10 @@ AS $$ RETURN 1; END IF; - cmp_result := eql_v3.compare_ore_block_u64_8_256_term(a[1], b[1]); + cmp_result := eql_v3.compare_ore_block_256_term(a[1], b[1]); IF cmp_result = 0 THEN - RETURN eql_v3.compare_ore_block_u64_8_256_terms(a[2:array_length(a,1)], b[2:array_length(b,1)]); + RETURN eql_v3.compare_ore_block_256_terms(a[2:array_length(a,1)], b[2:array_length(b,1)]); END IF; RETURN cmp_result; @@ -210,15 +210,15 @@ $$ LANGUAGE plpgsql; --! @brief Compare ORE block composite types --! @internal ---! @param a eql_v3.ore_block_u64_8_256 First ORE block ---! @param b eql_v3.ore_block_u64_8_256 Second ORE block +--! @param a eql_v3.ore_block_256 First ORE block +--! @param b eql_v3.ore_block_256 Second ORE block --! @return integer -1/0/1 -CREATE FUNCTION eql_v3.compare_ore_block_u64_8_256_terms(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) +CREATE FUNCTION eql_v3.compare_ore_block_256_terms(a eql_v3.ore_block_256, b eql_v3.ore_block_256) RETURNS integer IMMUTABLE SET search_path = pg_catalog, extensions, public AS $$ BEGIN - RETURN eql_v3.compare_ore_block_u64_8_256_terms(a.terms, b.terms); + RETURN eql_v3.compare_ore_block_256_terms(a.terms, b.terms); END $$ LANGUAGE plpgsql; diff --git a/src/v3/sem/ore_block_u64_8_256/operator_class.sql b/src/v3/sem/ore_block_256/operator_class.sql similarity index 53% rename from src/v3/sem/ore_block_u64_8_256/operator_class.sql rename to src/v3/sem/ore_block_256/operator_class.sql index b367c8f6..04018d31 100644 --- a/src/v3/sem/ore_block_u64_8_256/operator_class.sql +++ b/src/v3/sem/ore_block_256/operator_class.sql @@ -1,9 +1,9 @@ -- REQUIRE: src/v3/schema.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/types.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/types.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql ---! @file v3/sem/ore_block_u64_8_256/operator_class.sql ---! @brief B-tree operator family + default class on eql_v3.ore_block_u64_8_256. +--! @file v3/sem/ore_block_256/operator_class.sql +--! @brief B-tree operator family + default class on eql_v3.ore_block_256. --! --! Gives the composite type its DEFAULT btree opclass so the recommended --! functional index `CREATE INDEX ON t (eql_v3.ord_term(col))` engages without @@ -11,16 +11,16 @@ --! variant by the `**/*operator_class.sql` glob. --! @brief B-tree operator family for ORE block types -CREATE OPERATOR FAMILY eql_v3.ore_block_u64_8_256_operator_family USING btree; +CREATE OPERATOR FAMILY eql_v3.ore_block_256_operator_family USING btree; --! @brief B-tree operator class for ORE block encrypted values --! --! Supports operators: <, <=, =, >=, >. Uses comparison function ---! compare_ore_block_u64_8_256_terms. -CREATE OPERATOR CLASS eql_v3.ore_block_u64_8_256_operator_class DEFAULT FOR TYPE eql_v3.ore_block_u64_8_256 USING btree FAMILY eql_v3.ore_block_u64_8_256_operator_family AS +--! compare_ore_block_256_terms. +CREATE OPERATOR CLASS eql_v3.ore_block_256_operator_class DEFAULT FOR TYPE eql_v3.ore_block_256 USING btree FAMILY eql_v3.ore_block_256_operator_family AS OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 =, OPERATOR 4 >=, OPERATOR 5 >, - FUNCTION 1 eql_v3.compare_ore_block_u64_8_256_terms(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256); + FUNCTION 1 eql_v3.compare_ore_block_256_terms(a eql_v3.ore_block_256, b eql_v3.ore_block_256); diff --git a/src/v3/sem/ore_block_256/operators.sql b/src/v3/sem/ore_block_256/operators.sql new file mode 100644 index 00000000..525ecc94 --- /dev/null +++ b/src/v3/sem/ore_block_256/operators.sql @@ -0,0 +1,180 @@ +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/sem/ore_block_256/types.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql + +--! @file v3/sem/ore_block_256/operators.sql +--! @brief Comparison operators on eql_v3.ore_block_256. +--! +--! The six backing functions are inlinable single-statement SQL so the planner +--! can fold the eql_v3 comparison wrappers through to functional-index matching. + +--! @brief Equality backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the ORE blocks are equal +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_eq(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) = 0 +$$; + +--! @brief Not-equal backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the ORE blocks are not equal +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_neq(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) <> 0 +$$; + +--! @brief Less-than backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the left operand is less than the right operand +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_lt(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) = -1 +$$; + +--! @brief Less-than-or-equal backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the left operand is less than or equal to the right operand +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_lte(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) != 1 +$$; + +--! @brief Greater-than backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the left operand is greater than the right operand +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_gt(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) = 1 +$$; + +--! @brief Greater-than-or-equal backing function for ORE block types +--! @internal +--! +--! @param a eql_v3.ore_block_256 Left operand +--! @param b eql_v3.ore_block_256 Right operand +--! @return boolean True if the left operand is greater than or equal to the right operand +--! +--! @see eql_v3.compare_ore_block_256_terms +CREATE FUNCTION eql_v3.ore_block_256_gte(a eql_v3.ore_block_256, b eql_v3.ore_block_256) +RETURNS boolean + LANGUAGE sql + IMMUTABLE STRICT PARALLEL SAFE +AS $$ + SELECT eql_v3.compare_ore_block_256_terms(a, b) != -1 +$$; + + +--! @brief = operator for ORE block types +--! +--! COMMUTATOR is the operator itself: equality is symmetric. Required for the +--! MERGES flag — without it the planner raises "could not find commutator" the +--! first time an ore_block equality is used as a join qual (e.g. via the inlined +--! eql_v3._ord_ore equality wrappers). +CREATE OPERATOR = ( + FUNCTION=eql_v3.ore_block_256_eq, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = =, + NEGATOR = <>, + RESTRICT = eqsel, + JOIN = eqjoinsel, + HASHES, + MERGES +); + +--! @brief <> operator for ORE block types +CREATE OPERATOR <> ( + FUNCTION=eql_v3.ore_block_256_neq, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = <>, + NEGATOR = =, + RESTRICT = neqsel, + JOIN = neqjoinsel, + MERGES +); + +--! @brief > operator for ORE block types +CREATE OPERATOR > ( + FUNCTION=eql_v3.ore_block_256_gt, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = <, + NEGATOR = <=, + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel +); + +--! @brief < operator for ORE block types +CREATE OPERATOR < ( + FUNCTION=eql_v3.ore_block_256_lt, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = >, + NEGATOR = >=, + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel +); + +--! @brief <= operator for ORE block types +CREATE OPERATOR <= ( + FUNCTION=eql_v3.ore_block_256_lte, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = >=, + NEGATOR = >, + RESTRICT = scalarlesel, + JOIN = scalarlejoinsel +); + +--! @brief >= operator for ORE block types +CREATE OPERATOR >= ( + FUNCTION=eql_v3.ore_block_256_gte, + LEFTARG=eql_v3.ore_block_256, + RIGHTARG=eql_v3.ore_block_256, + COMMUTATOR = <=, + NEGATOR = <, + RESTRICT = scalargesel, + JOIN = scalargejoinsel +); diff --git a/src/v3/sem/ore_block_u64_8_256/types.sql b/src/v3/sem/ore_block_256/types.sql similarity index 79% rename from src/v3/sem/ore_block_u64_8_256/types.sql rename to src/v3/sem/ore_block_256/types.sql index f7e44dd0..b77c5733 100644 --- a/src/v3/sem/ore_block_u64_8_256/types.sql +++ b/src/v3/sem/ore_block_256/types.sql @@ -1,6 +1,6 @@ -- REQUIRE: src/v3/schema.sql ---! @file v3/sem/ore_block_u64_8_256/types.sql +--! @file v3/sem/ore_block_256/types.sql --! @brief ORE block index-term types (eql_v3 SEM). --! --! Self-contained eql_v3 copies of the Order-Revealing Encryption block types @@ -10,7 +10,7 @@ --! --! Composite type representing a single ORE block term. Stores encrypted data --! as bytea that enables range comparisons without decryption. -CREATE TYPE eql_v3.ore_block_u64_8_256_term AS ( +CREATE TYPE eql_v3.ore_block_256_term AS ( bytes bytea ); @@ -21,6 +21,6 @@ CREATE TYPE eql_v3.ore_block_u64_8_256_term AS ( --! in the 'ob' field of encrypted data payloads. --! --! @note Transient type used only during query execution. -CREATE TYPE eql_v3.ore_block_u64_8_256 AS ( - terms eql_v3.ore_block_u64_8_256_term[] +CREATE TYPE eql_v3.ore_block_256 AS ( + terms eql_v3.ore_block_256_term[] ); diff --git a/src/v3/sem/ore_block_u64_8_256/operators.sql b/src/v3/sem/ore_block_u64_8_256/operators.sql deleted file mode 100644 index d2e670b7..00000000 --- a/src/v3/sem/ore_block_u64_8_256/operators.sql +++ /dev/null @@ -1,180 +0,0 @@ --- REQUIRE: src/v3/schema.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/types.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql - ---! @file v3/sem/ore_block_u64_8_256/operators.sql ---! @brief Comparison operators on eql_v3.ore_block_u64_8_256. ---! ---! The six backing functions are inlinable single-statement SQL so the planner ---! can fold the eql_v3 comparison wrappers through to functional-index matching. - ---! @brief Equality backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the ORE blocks are equal ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_eq(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) = 0 -$$; - ---! @brief Not-equal backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the ORE blocks are not equal ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_neq(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) <> 0 -$$; - ---! @brief Less-than backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the left operand is less than the right operand ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_lt(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) = -1 -$$; - ---! @brief Less-than-or-equal backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the left operand is less than or equal to the right operand ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_lte(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) != 1 -$$; - ---! @brief Greater-than backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the left operand is greater than the right operand ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_gt(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) = 1 -$$; - ---! @brief Greater-than-or-equal backing function for ORE block types ---! @internal ---! ---! @param a eql_v3.ore_block_u64_8_256 Left operand ---! @param b eql_v3.ore_block_u64_8_256 Right operand ---! @return boolean True if the left operand is greater than or equal to the right operand ---! ---! @see eql_v3.compare_ore_block_u64_8_256_terms -CREATE FUNCTION eql_v3.ore_block_u64_8_256_gte(a eql_v3.ore_block_u64_8_256, b eql_v3.ore_block_u64_8_256) -RETURNS boolean - LANGUAGE sql - IMMUTABLE STRICT PARALLEL SAFE -AS $$ - SELECT eql_v3.compare_ore_block_u64_8_256_terms(a, b) != -1 -$$; - - ---! @brief = operator for ORE block types ---! ---! COMMUTATOR is the operator itself: equality is symmetric. Required for the ---! MERGES flag — without it the planner raises "could not find commutator" the ---! first time an ore_block equality is used as a join qual (e.g. via the inlined ---! eql_v3._ord_ore equality wrappers). -CREATE OPERATOR = ( - FUNCTION=eql_v3.ore_block_u64_8_256_eq, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = =, - NEGATOR = <>, - RESTRICT = eqsel, - JOIN = eqjoinsel, - HASHES, - MERGES -); - ---! @brief <> operator for ORE block types -CREATE OPERATOR <> ( - FUNCTION=eql_v3.ore_block_u64_8_256_neq, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = <>, - NEGATOR = =, - RESTRICT = neqsel, - JOIN = neqjoinsel, - MERGES -); - ---! @brief > operator for ORE block types -CREATE OPERATOR > ( - FUNCTION=eql_v3.ore_block_u64_8_256_gt, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = <, - NEGATOR = <=, - RESTRICT = scalargtsel, - JOIN = scalargtjoinsel -); - ---! @brief < operator for ORE block types -CREATE OPERATOR < ( - FUNCTION=eql_v3.ore_block_u64_8_256_lt, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = >, - NEGATOR = >=, - RESTRICT = scalarltsel, - JOIN = scalarltjoinsel -); - ---! @brief <= operator for ORE block types -CREATE OPERATOR <= ( - FUNCTION=eql_v3.ore_block_u64_8_256_lte, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = >=, - NEGATOR = >, - RESTRICT = scalarlesel, - JOIN = scalarlejoinsel -); - ---! @brief >= operator for ORE block types -CREATE OPERATOR >= ( - FUNCTION=eql_v3.ore_block_u64_8_256_gte, - LEFTARG=eql_v3.ore_block_u64_8_256, - RIGHTARG=eql_v3.ore_block_u64_8_256, - COMMUTATOR = <=, - NEGATOR = <, - RESTRICT = scalargesel, - JOIN = scalargejoinsel -); diff --git a/tasks/pin_search_path.sql b/tasks/pin_search_path.sql index d00e3426..bfbd702c 100644 --- a/tasks/pin_search_path.sql +++ b/tasks/pin_search_path.sql @@ -259,13 +259,13 @@ BEGIN n.nspname = 'eql_v3' AND ( (p.pronargs = 2 - AND p.proname IN ('ore_block_u64_8_256_eq', 'ore_block_u64_8_256_neq', - 'ore_block_u64_8_256_lt', 'ore_block_u64_8_256_lte', - 'ore_block_u64_8_256_gt', 'ore_block_u64_8_256_gte')) + AND p.proname IN ('ore_block_256_eq', 'ore_block_256_neq', + 'ore_block_256_lt', 'ore_block_256_lte', + 'ore_block_256_gt', 'ore_block_256_gte')) -- Inner ORE-CLLW comparison helpers backing the `<`, `<=`, `=`, `>=`, -- `>`, `<>` operators on the eql_v3.ore_cllw composite type (registered -- via the DEFAULT eql_v3.ore_cllw_ops btree opclass). Same precedent as - -- the ore_block_u64_8_256_* helpers above and the eql_v2.ore_cllw_* + -- the ore_block_256_* helpers above and the eql_v2.ore_cllw_* -- helpers: PG only carries the inlined operator wrapper through to -- functional-index match if the inner backing function is also -- inlinable. They take the composite arg (not a jsonb-backed domain), diff --git a/tasks/test/clean_install_v3.sh b/tasks/test/clean_install_v3.sh index cb9e9d02..07553f67 100755 --- a/tasks/test/clean_install_v3.sh +++ b/tasks/test/clean_install_v3.sh @@ -36,7 +36,7 @@ echo "==> smoke: domains, SEM types, extractors, opclass functional index (D4)" -- Domains and SEM types exist in eql_v3. SELECT 'eql_v3.int4_ord'::regtype; SELECT 'eql_v3.hmac_256'::regtype; -SELECT 'eql_v3.ore_block_u64_8_256'::regtype; +SELECT 'eql_v3.ore_block_256'::regtype; -- A real ordered-domain column + the documented functional index. This is the -- D4 proof: it fails outright if the ported operator_class is absent. diff --git a/tasks/test/splinter.sh b/tasks/test/splinter.sh index fcf53e8c..97383be0 100755 --- a/tasks/test/splinter.sh +++ b/tasks/test/splinter.sh @@ -108,7 +108,7 @@ function_search_path_mutable eql_v2 grouped_value function Aggregate: same as mi # they need their own rows. The plpgsql blockers are pinned by # tasks/pin_search_path.sql and do not surface here. function_search_path_mutable eql_v3 eq_term function HMAC equality term extractor for the eql_v3 *_eq domains: returns eql_v3.hmac_256. Must inline so `eql_v3.eq_term(col)` folds into the calling query and matches the functional hash/btree index built on the same expression. SET search_path would disable SQL function inlining (see PostgreSQL inline_function). -function_search_path_mutable eql_v3 ord_term function ORE-block order term extractor for the eql_v3 ordered domains: returns eql_v3.ore_block_u64_8_256 (carrying the main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v3.ord_term(col)); must inline. Covers both ord_term overloads (eql_v3.int4_ord, eql_v3.int4_ord_ore). +function_search_path_mutable eql_v3 ord_term function ORE-block order term extractor for the eql_v3 ordered domains: returns eql_v3.ore_block_256 (carrying the main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v3.ord_term(col)); must inline. Covers both ord_term overloads (eql_v3.int4_ord, eql_v3.int4_ord_ore). function_search_path_mutable eql_v3 match_term function Bloom-filter match term extractor for the eql_v3 *_match domains: returns eql_v3.bloom_filter. Used inside the inlinable @>/<@ containment wrappers and as the functional-index expression USING gin (eql_v3.match_term(col)); must inline so the GIN index engages. SET search_path would disable SQL function inlining. function_search_path_mutable eql_v3 contains function Containment (@>) comparison wrapper on the eql_v3 *_match domains. Inlines to `match_term(a) @> match_term(b)`; must reach the functional GIN index on eql_v3.match_term(col) for bloom-filter match to engage Bitmap Index Scan. function_search_path_mutable eql_v3 contained_by function Contained-by (<@) comparison wrapper on the eql_v3 *_match domains. Same rationale as eql_v3.contains. @@ -120,16 +120,16 @@ function_search_path_mutable eql_v3 gt function Greater-than comparison wrapper function_search_path_mutable eql_v3 gte function Greater-than-or-equal comparison wrapper on the eql_v3 ordered domains. Same rationale as eql_v3.lt. function_search_path_mutable eql_v3 min function Per-domain MIN aggregate on the eql_v3 ordered domains (splinter labels aggregates type=function): ALTER AGGREGATE has no SET configuration_parameter syntax, and ALTER ROUTINE/FUNCTION reject aggregates. The aggregate's SFUNC carries a pinned search_path. function_search_path_mutable eql_v3 max function Per-domain MAX aggregate on the eql_v3 ordered domains. Same as eql_v3.min. -function_search_path_mutable eql_v3 ore_block_u64_8_256_eq function Inner comparator for the eql_v3 ore_block_u64_8_256 type's `=` operator (self-contained SEM fork). The eql_v3 *_ord comparison wrappers inline to `ord_term(a) op ord_term(b)`; the planner only carries that through to the functional ORE index if this inner function is also inlinable (no SET, IMMUTABLE). Mirrors eql_v2.ore_block_u64_8_256_eq. -function_search_path_mutable eql_v3 ore_block_u64_8_256_neq function Inner comparator for the eql_v3 ore_block_u64_8_256 `<>` operator. Same rationale as eql_v3.ore_block_u64_8_256_eq. -function_search_path_mutable eql_v3 ore_block_u64_8_256_lt function Inner comparator for the eql_v3 ore_block_u64_8_256 `<` operator. Same rationale as eql_v3.ore_block_u64_8_256_eq. -function_search_path_mutable eql_v3 ore_block_u64_8_256_lte function Inner comparator for the eql_v3 ore_block_u64_8_256 `<=` operator. Same rationale as eql_v3.ore_block_u64_8_256_eq. -function_search_path_mutable eql_v3 ore_block_u64_8_256_gt function Inner comparator for the eql_v3 ore_block_u64_8_256 `>` operator. Same rationale as eql_v3.ore_block_u64_8_256_eq. -function_search_path_mutable eql_v3 ore_block_u64_8_256_gte function Inner comparator for the eql_v3 ore_block_u64_8_256 `>=` operator. Same rationale as eql_v3.ore_block_u64_8_256_eq. +function_search_path_mutable eql_v3 ore_block_256_eq function Inner comparator for the eql_v3 ore_block_256 type's `=` operator (self-contained SEM fork). The eql_v3 *_ord comparison wrappers inline to `ord_term(a) op ord_term(b)`; the planner only carries that through to the functional ORE index if this inner function is also inlinable (no SET, IMMUTABLE). Mirrors eql_v2.ore_block_u64_8_256_eq. +function_search_path_mutable eql_v3 ore_block_256_neq function Inner comparator for the eql_v3 ore_block_256 `<>` operator. Same rationale as eql_v3.ore_block_256_eq. +function_search_path_mutable eql_v3 ore_block_256_lt function Inner comparator for the eql_v3 ore_block_256 `<` operator. Same rationale as eql_v3.ore_block_256_eq. +function_search_path_mutable eql_v3 ore_block_256_lte function Inner comparator for the eql_v3 ore_block_256 `<=` operator. Same rationale as eql_v3.ore_block_256_eq. +function_search_path_mutable eql_v3 ore_block_256_gt function Inner comparator for the eql_v3 ore_block_256 `>` operator. Same rationale as eql_v3.ore_block_256_eq. +function_search_path_mutable eql_v3 ore_block_256_gte function Inner comparator for the eql_v3 ore_block_256 `>=` operator. Same rationale as eql_v3.ore_block_256_eq. function_search_path_mutable eql_v3 hmac_256 function HMAC equality extractor for the eql_v3 SEM fork: inlinable SQL (jsonb) constructor used inside eql_v3.eq_term. Must inline so the functional hash/btree index on eql_v3.eq_term(col) engages. Mirrors eql_v2.hmac_256. function_search_path_mutable eql_v3 bloom_filter function Bloom-filter match extractor for the eql_v3 SEM fork: inlinable SQL (jsonb) constructor used inside eql_v3.match_term. Must inline so the functional GIN index on eql_v3.match_term(col) engages. Mirrors eql_v3.hmac_256. -function_search_path_mutable eql_v3 jsonb_array_to_bytea_array function Hand-written jsonb→bytea[] helper for the eql_v3 SEM fork: inlinable SQL (no SET, IMMUTABLE). Reached per-encrypted-value through eql_v3.ore_block_u64_8_256; must inline so the planner can fold it into the calling query. Pinned by neither the structural skip (it takes bare jsonb, not a jsonb-backed domain) nor an inline-critical OID clause — it carries the documented `eql-inline-critical` COMMENT marker that tasks/pin_search_path.sql honours. The eql_v2 copy stays plpgsql (pinned) by design. -function_search_path_mutable eql_v3 jsonb_array_to_ore_block_u64_8_256 function Hand-written jsonb→ore_block composite helper for the eql_v3 SEM fork: inlinable SQL (no SET, IMMUTABLE). Same rationale as eql_v3.jsonb_array_to_bytea_array — reached per-encrypted-value through eql_v3.ore_block_u64_8_256, carries the `eql-inline-critical` COMMENT marker. The eql_v2 copy stays plpgsql (pinned) by design. +function_search_path_mutable eql_v3 jsonb_array_to_bytea_array function Hand-written jsonb→bytea[] helper for the eql_v3 SEM fork: inlinable SQL (no SET, IMMUTABLE). Reached per-encrypted-value through eql_v3.ore_block_256; must inline so the planner can fold it into the calling query. Pinned by neither the structural skip (it takes bare jsonb, not a jsonb-backed domain) nor an inline-critical OID clause — it carries the documented `eql-inline-critical` COMMENT marker that tasks/pin_search_path.sql honours. The eql_v2 copy stays plpgsql (pinned) by design. +function_search_path_mutable eql_v3 jsonb_array_to_ore_block_256 function Hand-written jsonb→ore_block composite helper for the eql_v3 SEM fork: inlinable SQL (no SET, IMMUTABLE). Same rationale as eql_v3.jsonb_array_to_bytea_array — reached per-encrypted-value through eql_v3.ore_block_256, carries the `eql-inline-critical` COMMENT marker. The eql_v2 copy stays plpgsql (pinned) by design. function_search_path_mutable eql_v3 ore_cllw_eq function Inner comparator for the eql_v3.ore_cllw composite type's `=` operator (self-contained SEM fork, DEFAULT FOR TYPE btree opclass eql_v3.ore_cllw_ops). The outer same-type operators back the opclass; the planner only carries the inlined form through to functional-index match if this inner function is also inlinable (no SET, IMMUTABLE). The plpgsql FUNCTION 1 comparator (compare_ore_cllw_term) stays pinned by design. Mirrors eql_v2.ore_cllw_eq. function_search_path_mutable eql_v3 ore_cllw_neq function Inner comparator for the eql_v3.ore_cllw `<>` operator. Same rationale as eql_v3.ore_cllw_eq. function_search_path_mutable eql_v3 ore_cllw_lt function Inner comparator for the eql_v3.ore_cllw `<` operator. Same rationale as eql_v3.ore_cllw_eq. diff --git a/tests/codegen/reference/date/date_ord_functions.sql b/tests/codegen/reference/date/date_ord_functions.sql index 8c2adf48..99fa57eb 100644 --- a/tests/codegen/reference/date/date_ord_functions.sql +++ b/tests/codegen/reference/date/date_ord_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/date/date_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/date/date_ord_functions.sql --! @brief Functions for eql_v3.date_ord. --! @brief Index extractor for eql_v3.date_ord. --! @param a eql_v3.date_ord ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.date_ord) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.date_ord. --! @param a eql_v3.date_ord diff --git a/tests/codegen/reference/date/date_ord_ore_functions.sql b/tests/codegen/reference/date/date_ord_ore_functions.sql index 1fe59073..d65e3d9d 100644 --- a/tests/codegen/reference/date/date_ord_ore_functions.sql +++ b/tests/codegen/reference/date/date_ord_ore_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/date/date_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/date/date_ord_ore_functions.sql --! @brief Functions for eql_v3.date_ord_ore. --! @brief Index extractor for eql_v3.date_ord_ore. --! @param a eql_v3.date_ord_ore ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.date_ord_ore) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.date_ord_ore. --! @param a eql_v3.date_ord_ore diff --git a/tests/codegen/reference/int2/int2_ord_functions.sql b/tests/codegen/reference/int2/int2_ord_functions.sql index 58c977d2..a9a375a8 100644 --- a/tests/codegen/reference/int2/int2_ord_functions.sql +++ b/tests/codegen/reference/int2/int2_ord_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int2/int2_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int2/int2_ord_functions.sql --! @brief Functions for eql_v3.int2_ord. --! @brief Index extractor for eql_v3.int2_ord. --! @param a eql_v3.int2_ord ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int2_ord) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int2_ord. --! @param a eql_v3.int2_ord diff --git a/tests/codegen/reference/int2/int2_ord_ore_functions.sql b/tests/codegen/reference/int2/int2_ord_ore_functions.sql index ab200402..f28400bd 100644 --- a/tests/codegen/reference/int2/int2_ord_ore_functions.sql +++ b/tests/codegen/reference/int2/int2_ord_ore_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int2/int2_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int2/int2_ord_ore_functions.sql --! @brief Functions for eql_v3.int2_ord_ore. --! @brief Index extractor for eql_v3.int2_ord_ore. --! @param a eql_v3.int2_ord_ore ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int2_ord_ore) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int2_ord_ore. --! @param a eql_v3.int2_ord_ore diff --git a/tests/codegen/reference/int4/int4_ord_functions.sql b/tests/codegen/reference/int4/int4_ord_functions.sql index 4b170fcb..b4c67732 100644 --- a/tests/codegen/reference/int4/int4_ord_functions.sql +++ b/tests/codegen/reference/int4/int4_ord_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int4/int4_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int4/int4_ord_functions.sql --! @brief Functions for eql_v3.int4_ord. --! @brief Index extractor for eql_v3.int4_ord. --! @param a eql_v3.int4_ord ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int4_ord) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int4_ord. --! @param a eql_v3.int4_ord diff --git a/tests/codegen/reference/int4/int4_ord_ore_functions.sql b/tests/codegen/reference/int4/int4_ord_ore_functions.sql index e93c8491..964fc480 100644 --- a/tests/codegen/reference/int4/int4_ord_ore_functions.sql +++ b/tests/codegen/reference/int4/int4_ord_ore_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int4/int4_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int4/int4_ord_ore_functions.sql --! @brief Functions for eql_v3.int4_ord_ore. --! @brief Index extractor for eql_v3.int4_ord_ore. --! @param a eql_v3.int4_ord_ore ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int4_ord_ore) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int4_ord_ore. --! @param a eql_v3.int4_ord_ore diff --git a/tests/codegen/reference/int8/int8_ord_functions.sql b/tests/codegen/reference/int8/int8_ord_functions.sql index 109dcd2b..c86fa6a0 100644 --- a/tests/codegen/reference/int8/int8_ord_functions.sql +++ b/tests/codegen/reference/int8/int8_ord_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int8/int8_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int8/int8_ord_functions.sql --! @brief Functions for eql_v3.int8_ord. --! @brief Index extractor for eql_v3.int8_ord. --! @param a eql_v3.int8_ord ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int8_ord) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int8_ord. --! @param a eql_v3.int8_ord diff --git a/tests/codegen/reference/int8/int8_ord_ore_functions.sql b/tests/codegen/reference/int8/int8_ord_ore_functions.sql index dd413fce..6d9ac335 100644 --- a/tests/codegen/reference/int8/int8_ord_ore_functions.sql +++ b/tests/codegen/reference/int8/int8_ord_ore_functions.sql @@ -3,19 +3,19 @@ -- REQUIRE: src/v3/schema.sql -- REQUIRE: src/v3/scalars/int8/int8_types.sql -- REQUIRE: src/v3/scalars/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/int8/int8_ord_ore_functions.sql --! @brief Functions for eql_v3.int8_ord_ore. --! @brief Index extractor for eql_v3.int8_ord_ore. --! @param a eql_v3.int8_ord_ore ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.int8_ord_ore) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.int8_ord_ore. --! @param a eql_v3.int8_ord_ore diff --git a/tests/codegen/reference/text/text_ord_functions.sql b/tests/codegen/reference/text/text_ord_functions.sql index f07e8b64..5b67ab5e 100644 --- a/tests/codegen/reference/text/text_ord_functions.sql +++ b/tests/codegen/reference/text/text_ord_functions.sql @@ -4,8 +4,8 @@ -- REQUIRE: src/v3/scalars/text/text_types.sql -- REQUIRE: src/v3/scalars/functions.sql -- REQUIRE: src/v3/sem/hmac_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/text/text_ord_functions.sql --! @brief Functions for eql_v3.text_ord. @@ -20,11 +20,11 @@ AS $$ SELECT eql_v3.hmac_256(a::jsonb) $$; --! @brief Index extractor for eql_v3.text_ord. --! @param a eql_v3.text_ord ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.text_ord) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.text_ord. --! @param a eql_v3.text_ord diff --git a/tests/codegen/reference/text/text_ord_ore_functions.sql b/tests/codegen/reference/text/text_ord_ore_functions.sql index 58e1abac..e541eee9 100644 --- a/tests/codegen/reference/text/text_ord_ore_functions.sql +++ b/tests/codegen/reference/text/text_ord_ore_functions.sql @@ -4,8 +4,8 @@ -- REQUIRE: src/v3/scalars/text/text_types.sql -- REQUIRE: src/v3/scalars/functions.sql -- REQUIRE: src/v3/sem/hmac_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql --! @file encrypted_domain/text/text_ord_ore_functions.sql --! @brief Functions for eql_v3.text_ord_ore. @@ -20,11 +20,11 @@ AS $$ SELECT eql_v3.hmac_256(a::jsonb) $$; --! @brief Index extractor for eql_v3.text_ord_ore. --! @param a eql_v3.text_ord_ore ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.text_ord_ore) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Operator wrapper for eql_v3.text_ord_ore. --! @param a eql_v3.text_ord_ore diff --git a/tests/codegen/reference/text/text_search_functions.sql b/tests/codegen/reference/text/text_search_functions.sql index 21dd5793..3146f152 100644 --- a/tests/codegen/reference/text/text_search_functions.sql +++ b/tests/codegen/reference/text/text_search_functions.sql @@ -4,8 +4,8 @@ -- REQUIRE: src/v3/scalars/text/text_types.sql -- REQUIRE: src/v3/scalars/functions.sql -- REQUIRE: src/v3/sem/hmac_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/functions.sql --- REQUIRE: src/v3/sem/ore_block_u64_8_256/operators.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql -- REQUIRE: src/v3/sem/bloom_filter/functions.sql --! @file encrypted_domain/text/text_search_functions.sql @@ -21,11 +21,11 @@ AS $$ SELECT eql_v3.hmac_256(a::jsonb) $$; --! @brief Index extractor for eql_v3.text_search. --! @param a eql_v3.text_search ---! @return eql_v3.ore_block_u64_8_256 +--! @return eql_v3.ore_block_256 CREATE FUNCTION eql_v3.ord_term(a eql_v3.text_search) -RETURNS eql_v3.ore_block_u64_8_256 +RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) $$; +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; --! @brief Index extractor for eql_v3.text_search. --! @param a eql_v3.text_search diff --git a/tests/sqlx/fixtures/drop_operator_classes.sql b/tests/sqlx/fixtures/drop_operator_classes.sql index 094073aa..13d1d350 100644 --- a/tests/sqlx/fixtures/drop_operator_classes.sql +++ b/tests/sqlx/fixtures/drop_operator_classes.sql @@ -20,8 +20,8 @@ DROP OPERATOR FAMILY IF EXISTS eql_v2.ore_block_u64_8_256_operator_family USING -- the `*operator_class.sql` suffix, so the Supabase build's `**/*operator_class.sql` -- glob excludes it as well. Without this the unqualified-name opclass check below -- still finds the eql_v3 copy. -DROP OPERATOR CLASS IF EXISTS eql_v3.ore_block_u64_8_256_operator_class USING btree CASCADE; -DROP OPERATOR FAMILY IF EXISTS eql_v3.ore_block_u64_8_256_operator_family USING btree CASCADE; +DROP OPERATOR CLASS IF EXISTS eql_v3.ore_block_256_operator_class USING btree CASCADE; +DROP OPERATOR FAMILY IF EXISTS eql_v3.ore_block_256_operator_family USING btree CASCADE; -- Drop ore_block_u64_8_256 operators (also excluded from Supabase build) DROP OPERATOR IF EXISTS = (eql_v2.ore_block_u64_8_256, eql_v2.ore_block_u64_8_256) CASCADE; diff --git a/tests/sqlx/tests/encrypted_domain/family/inlinability.rs b/tests/sqlx/tests/encrypted_domain/family/inlinability.rs index 922d9ee4..9c62404d 100644 --- a/tests/sqlx/tests/encrypted_domain/family/inlinability.rs +++ b/tests/sqlx/tests/encrypted_domain/family/inlinability.rs @@ -89,7 +89,7 @@ async fn no_encrypted_domain_inline_critical_function_is_pinned(pool: PgPool) -> /// Direct guard for the self-contained eql_v3 SEM index-term functions. Unlike /// the structural guard above (which covers jsonb-domain-arg functions), these -/// take a composite (the ore_block_u64_8_256 and ore_cllw comparators) or raw +/// take a composite (the ore_block_256 and ore_cllw comparators) or raw /// jsonb (hmac_256, bloom_filter, the ore_cllw/has_ore_cllw extractors, the two /// per-encrypted-value `jsonb_array_to_*` helpers) arg, so they are NOT caught /// by the structural pin-skip and need explicit inline_critical allowlisting. If @@ -97,7 +97,7 @@ async fn no_encrypted_domain_inline_critical_function_is_pinned(pool: PgPool) -> /// regresses to Seq Scan — this test fails instead. /// /// `jsonb_array_to_bytea_array(jsonb)` and -/// `jsonb_array_to_ore_block_u64_8_256(jsonb)` are included here: both take a +/// `jsonb_array_to_ore_block_256(jsonb)` are included here: both take a /// bare `jsonb` arg (not a jsonb-backed encrypted DOMAIN), so the structural /// skip in tasks/pin_search_path.sql does not recognise them — they are kept /// unpinned by the `eql-inline-critical` COMMENT marker instead. This test @@ -113,7 +113,7 @@ async fn eql_v3_sem_inline_critical_functions_are_unpinned(pool: PgPool) -> Resu WITH expected(proname, pronargs, arg0, arg1) AS ( VALUES ('jsonb_array_to_bytea_array', 1, 'jsonb'::regtype, 0::oid), - ('jsonb_array_to_ore_block_u64_8_256', 1, 'jsonb'::regtype, 0::oid), + ('jsonb_array_to_ore_block_256', 1, 'jsonb'::regtype, 0::oid), ('ore_cllw', 1, 'jsonb'::regtype, 0::oid), ('has_ore_cllw', 1, 'jsonb'::regtype, 0::oid), ('meta_data', 1, 'jsonb'::regtype, 0::oid), @@ -135,9 +135,9 @@ async fn eql_v3_sem_inline_critical_functions_are_unpinned(pool: PgPool) -> Resu WHERE n.nspname = 'eql_v3' AND ( (p.pronargs = 2 AND p.proname IN ( - 'ore_block_u64_8_256_eq','ore_block_u64_8_256_neq', - 'ore_block_u64_8_256_lt','ore_block_u64_8_256_lte', - 'ore_block_u64_8_256_gt','ore_block_u64_8_256_gte')) + 'ore_block_256_eq','ore_block_256_neq', + 'ore_block_256_lt','ore_block_256_lte', + 'ore_block_256_gt','ore_block_256_gte')) OR (p.pronargs = 2 AND p.proname IN ( 'ore_cllw_eq','ore_cllw_neq', 'ore_cllw_lt','ore_cllw_lte', @@ -148,7 +148,7 @@ async fn eql_v3_sem_inline_critical_functions_are_unpinned(pool: PgPool) -> Resu 'ore_cllw', 'has_ore_cllw', 'jsonb_array_to_bytea_array', - 'jsonb_array_to_ore_block_u64_8_256') + 'jsonb_array_to_ore_block_256') AND p.proargtypes[0] = 'jsonb'::regtype) OR e.proname IS NOT NULL ) @@ -173,7 +173,7 @@ async fn eql_v3_sem_inline_critical_functions_are_unpinned(pool: PgPool) -> Resu } /// Companion guard for the two bare-`jsonb` per-encrypted-value helpers -/// (`jsonb_array_to_bytea_array`, `jsonb_array_to_ore_block_u64_8_256`). The +/// (`jsonb_array_to_bytea_array`, `jsonb_array_to_ore_block_256`). The /// unpinned state asserted above is only DURABLE because each helper carries an /// `eql-inline-critical` COMMENT marker that `tasks/pin_search_path.sql` honours /// (it skips pinning functions whose `pg_description` matches @@ -193,7 +193,7 @@ async fn eql_v3_sem_inline_critical_helpers_carry_marker(pool: PgPool) -> Result WITH expected(proname, pronargs, arg0, arg1) AS ( VALUES ('jsonb_array_to_bytea_array', 1, 'jsonb'::regtype, 0::oid), - ('jsonb_array_to_ore_block_u64_8_256', 1, 'jsonb'::regtype, 0::oid), + ('jsonb_array_to_ore_block_256', 1, 'jsonb'::regtype, 0::oid), ('ore_cllw', 1, 'jsonb'::regtype, 0::oid), ('has_ore_cllw', 1, 'jsonb'::regtype, 0::oid), ('meta_data', 1, 'jsonb'::regtype, 0::oid), diff --git a/tests/sqlx/tests/encrypted_domain/family/mutations.rs b/tests/sqlx/tests/encrypted_domain/family/mutations.rs index d78d5520..f2399a2b 100644 --- a/tests/sqlx/tests/encrypted_domain/family/mutations.rs +++ b/tests/sqlx/tests/encrypted_domain/family/mutations.rs @@ -259,7 +259,7 @@ async fn blocking_lt_flips_lt_arm_but_not_order_by(pool: PgPool) -> Result<()> { // 6. `_eq` equality must route through `eq_term` (`hm`), never ORE — the // mirror of #3 for the eq path. Rerouting it through -// `ore_block_u64_8_256` (`ob`) over ob-stripped rows breaks equality. +// `ore_block_256` (`ob`) over ob-stripped rows breaks equality. // // Two notes on why this is shaped differently from the plan's literal // "returns 0 where forward expects 1": @@ -267,7 +267,7 @@ async fn blocking_lt_flips_lt_arm_but_not_order_by(pool: PgPool) -> Result<()> { // `=` through ORE on the RAW fixture would still match (both terms are // injective per plaintext) — vacuous. Stripping `ob` forces the // rerouted operator onto an absent term, exactly as #3 strips `hm`. -// - `ore_block_u64_8_256(jsonb)` RAISES on an absent `ob` ("Expected an +// - `ore_block_256(jsonb)` RAISES on an absent `ob` ("Expected an // ore index (ob)"), whereas `hmac_256(jsonb)` returns NULL on an absent // `hm`. So the eq path breaks via a raise, not a 0-count. Either way the // correct hm-routed equality matches and the rerouted one does not. @@ -297,12 +297,12 @@ async fn rerouting_eq_eq_through_ob_flips_eq_arm(pool: PgPool) -> Result<()> { ); // Mutation: reroute `_eq` `=` through ORE. The `ob` key is absent, so - // `eql_v3.ore_block_u64_8_256(jsonb)` raises rather than matching. + // `eql_v3.ore_block_256(jsonb)` raises rather than matching. mutate( &pool, "CREATE OR REPLACE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b eql_v3.int4_eq) \ RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE \ - AS $$ SELECT eql_v3.ore_block_u64_8_256(a::jsonb) = eql_v3.ore_block_u64_8_256(b::jsonb) $$", + AS $$ SELECT eql_v3.ore_block_256(a::jsonb) = eql_v3.ore_block_256(b::jsonb) $$", ) .await?; @@ -354,8 +354,8 @@ async fn collapsing_ord_term_flips_order_by_arm(pool: PgPool) -> Result<()> { let const_payload = fetch_fixture_payload::(&pool, 0).await?; let ddl = format!( "CREATE OR REPLACE FUNCTION eql_v3.ord_term(a eql_v3.int4_ord) \ - RETURNS eql_v3.ore_block_u64_8_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE \ - AS $mutbody$ SELECT eql_v3.ore_block_u64_8_256('{esc}'::jsonb) $mutbody$", + RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE \ + AS $mutbody$ SELECT eql_v3.ore_block_256('{esc}'::jsonb) $mutbody$", esc = const_payload.replace('\'', "''"), ); mutate(&pool, &ddl).await?; @@ -409,8 +409,8 @@ async fn making_ord_term_non_strict_flips_order_by_nulls_arm(pool: PgPool) -> Re let const_payload = fetch_fixture_payload::(&pool, 0).await?; let ddl = format!( "CREATE OR REPLACE FUNCTION eql_v3.ord_term(a eql_v3.int4_ord) \ - RETURNS eql_v3.ore_block_u64_8_256 LANGUAGE sql IMMUTABLE PARALLEL SAFE \ - AS $mutbody$ SELECT eql_v3.ore_block_u64_8_256(\ + RETURNS eql_v3.ore_block_256 LANGUAGE sql IMMUTABLE PARALLEL SAFE \ + AS $mutbody$ SELECT eql_v3.ore_block_256(\ coalesce(a, '{esc}'::jsonb::eql_v3.int4_ord)::jsonb) $mutbody$", esc = const_payload.replace('\'', "''"), ); diff --git a/tests/sqlx/tests/encrypted_domain/family/sem.rs b/tests/sqlx/tests/encrypted_domain/family/sem.rs index 7518f6d2..fe2593b9 100644 --- a/tests/sqlx/tests/encrypted_domain/family/sem.rs +++ b/tests/sqlx/tests/encrypted_domain/family/sem.rs @@ -1,6 +1,6 @@ //! Direct behavioural tests for the self-contained `eql_v3` searchable- //! encrypted-metadata (SEM) index-term functions (`eql_v3.hmac_256`, -//! `eql_v3.ore_block_u64_8_256` and their comparators). +//! `eql_v3.ore_block_256` and their comparators). //! //! These functions are a HAND-PORT of the `eql_v2` originals (`src/v3/sem/`). //! The scalar matrix already exercises the happy path of the *array* comparator @@ -13,7 +13,7 @@ //! against a faithful-port slip — see below). //! - T2: the `'Ciphertexts are different lengths'` RAISE (all real fixtures are //! equal length, so the matrix never hits it). -//! - T3: NULL-term ordering inside `compare_ore_block_u64_8_256_term` — the +//! - T3: NULL-term ordering inside `compare_ore_block_256_term` — the //! `STRICT` comparison wrappers short-circuit before these branches run. //! - T4: array-level NULL + empty/cardinality base cases of the recursion. //! - T5: presence checks (`has_*`) and the missing-`ob` RAISE. @@ -30,13 +30,13 @@ use sqlx::PgPool; /// A single term built directly from hex — no encryption needed for the /// structural/edge-case tests. fn term(hex: &str) -> String { - format!("ROW(decode('{hex}', 'hex'))::eql_v3.ore_block_u64_8_256_term") + format!("ROW(decode('{hex}', 'hex'))::eql_v3.ore_block_256_term") } /// T1 — Differential parity: the same real `ob` payload must compare identically /// through the `eql_v2` and `eql_v3` array comparators. `eql_v2` is the trusted /// oracle; `eql_v3` is the byte-port. Both sides route through the SAME path -/// (jsonb extractor → composite → `compare_ore_block_u64_8_256_terms`) so the +/// (jsonb extractor → composite → `compare_ore_block_256_terms`) so the /// schema prefix is the only variable — any divergence is a genuine port bug. /// v3 has no encrypted-arg `compare` overload, hence the extractor routing. #[sqlx::test] @@ -60,8 +60,8 @@ async fn ore_v2_v3_comparator_parity_on_real_fixtures(pool: PgPool) -> Result<() SELECT eql_v2.compare_ore_block_u64_8_256_terms( eql_v2.ore_block_u64_8_256(a.j), eql_v2.ore_block_u64_8_256(b.j)) AS v2, - eql_v3.compare_ore_block_u64_8_256_terms( - eql_v3.ore_block_u64_8_256(a.j), eql_v3.ore_block_u64_8_256(b.j)) AS v3 + eql_v3.compare_ore_block_256_terms( + eql_v3.ore_block_256(a.j), eql_v3.ore_block_256(b.j)) AS v3 FROM a, b "#; @@ -97,7 +97,7 @@ async fn ore_v2_v3_comparator_parity_on_real_fixtures(pool: PgPool) -> Result<() #[sqlx::test] async fn ore_term_comparator_rejects_different_length_ciphertexts(pool: PgPool) -> Result<()> { let sql = format!( - "SELECT eql_v3.compare_ore_block_u64_8_256_term({}, {})", + "SELECT eql_v3.compare_ore_block_256_term({}, {})", term("aabbccdd"), // 4 bytes term("aabbccddee"), // 5 bytes ); @@ -105,26 +105,26 @@ async fn ore_term_comparator_rejects_different_length_ciphertexts(pool: PgPool) Ok(()) } -/// T3 — NULL-term ordering inside `compare_ore_block_u64_8_256_term`. The +/// T3 — NULL-term ordering inside `compare_ore_block_256_term`. The /// function is intentionally NOT `STRICT`, so these defensive branches are /// reachable by a direct call (the `STRICT` comparison wrappers never reach /// them). Pins: `(NULL, t) = -1`, `(t, NULL) = 1`, `(NULL, NULL) = 0`. #[sqlx::test] async fn ore_term_comparator_null_ordering(pool: PgPool) -> Result<()> { let t = term("aabb"); - let n = "NULL::eql_v3.ore_block_u64_8_256_term"; + let n = "NULL::eql_v3.ore_block_256_term"; let cases = [ ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_term({n}, {t})"), + format!("SELECT eql_v3.compare_ore_block_256_term({n}, {t})"), -1, ), ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_term({t}, {n})"), + format!("SELECT eql_v3.compare_ore_block_256_term({t}, {n})"), 1, ), ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_term({n}, {n})"), + format!("SELECT eql_v3.compare_ore_block_256_term({n}, {n})"), 0, ), ]; @@ -137,20 +137,20 @@ async fn ore_term_comparator_null_ordering(pool: PgPool) -> Result<()> { } /// T4 — Array-level NULL and empty/cardinality base cases of the recursive -/// `compare_ore_block_u64_8_256_terms(term[], term[])`. NULL array → NULL; +/// `compare_ore_block_256_terms(term[], term[])`. NULL array → NULL; /// both empty → 0; empty vs non-empty → -1; non-empty vs empty → 1. #[sqlx::test] async fn ore_terms_array_null_and_empty_base_cases(pool: PgPool) -> Result<()> { let t = format!("ARRAY[{}]", term("aabb")); - let empty = "ARRAY[]::eql_v3.ore_block_u64_8_256_term[]"; - let null_arr = "NULL::eql_v3.ore_block_u64_8_256_term[]"; + let empty = "ARRAY[]::eql_v3.ore_block_256_term[]"; + let null_arr = "NULL::eql_v3.ore_block_256_term[]"; // NULL array operand → NULL result (the array overload returns NULL; it is // not STRICT). Typed as Option; the shared `assert_null` helper only // types Option, so query directly here. for sql in [ - format!("SELECT eql_v3.compare_ore_block_u64_8_256_terms({null_arr}, {t})"), - format!("SELECT eql_v3.compare_ore_block_u64_8_256_terms({t}, {null_arr})"), + format!("SELECT eql_v3.compare_ore_block_256_terms({null_arr}, {t})"), + format!("SELECT eql_v3.compare_ore_block_256_terms({t}, {null_arr})"), ] { let got: Option = sqlx::query_scalar(&sql).fetch_one(&pool).await?; assert!(got.is_none(), "NULL array operand must yield NULL: {sql}"); @@ -158,15 +158,15 @@ async fn ore_terms_array_null_and_empty_base_cases(pool: PgPool) -> Result<()> { let cases = [ ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_terms({empty}, {empty})"), + format!("SELECT eql_v3.compare_ore_block_256_terms({empty}, {empty})"), 0, ), ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_terms({empty}, {t})"), + format!("SELECT eql_v3.compare_ore_block_256_terms({empty}, {t})"), -1, ), ( - format!("SELECT eql_v3.compare_ore_block_u64_8_256_terms({t}, {empty})"), + format!("SELECT eql_v3.compare_ore_block_256_terms({t}, {empty})"), 1, ), ]; @@ -177,22 +177,19 @@ async fn ore_terms_array_null_and_empty_base_cases(pool: PgPool) -> Result<()> { Ok(()) } -/// T5 — SEM presence checks (`has_ore_block_u64_8_256`, `has_hmac_256`), the +/// T5 — SEM presence checks (`has_ore_block_256`, `has_hmac_256`), the /// extractor's missing-`ob` RAISE, and its NULL-jsonb short-circuit. #[sqlx::test] async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<()> { let bool_cases = [ ( - r#"SELECT eql_v3.has_ore_block_u64_8_256('{"ob":["aa"]}'::jsonb)"#, + r#"SELECT eql_v3.has_ore_block_256('{"ob":["aa"]}'::jsonb)"#, true, ), - ( - r#"SELECT eql_v3.has_ore_block_u64_8_256('{}'::jsonb)"#, - false, - ), + (r#"SELECT eql_v3.has_ore_block_256('{}'::jsonb)"#, false), // json-null `ob` → `->>` yields NULL → absent. ( - r#"SELECT eql_v3.has_ore_block_u64_8_256('{"ob":null}'::jsonb)"#, + r#"SELECT eql_v3.has_ore_block_256('{"ob":null}'::jsonb)"#, false, ), (r#"SELECT eql_v3.has_hmac_256('{"hm":"abc"}'::jsonb)"#, true), @@ -206,17 +203,16 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<() // Missing `ob` → RAISE. assert_raises( &pool, - r#"SELECT eql_v3.ore_block_u64_8_256('{"foo":1}'::jsonb)"#, + r#"SELECT eql_v3.ore_block_256('{"foo":1}'::jsonb)"#, &[], "Expected an ore index (ob) value", ) .await?; // NULL jsonb → NULL composite (STRICT short-circuit), NOT a raise. - let is_null: bool = - sqlx::query_scalar("SELECT eql_v3.ore_block_u64_8_256(NULL::jsonb) IS NULL") - .fetch_one(&pool) - .await?; + let is_null: bool = sqlx::query_scalar("SELECT eql_v3.ore_block_256(NULL::jsonb) IS NULL") + .fetch_one(&pool) + .await?; assert!( is_null, "NULL jsonb must extract to a NULL composite, not raise" @@ -312,7 +308,7 @@ async fn jsonb_array_to_bytea_array_input_shapes(pool: PgPool) -> Result<()> { Ok(()) } -/// T7 — Characterization of `eql_v3.jsonb_array_to_ore_block_u64_8_256(jsonb)` +/// T7 — Characterization of `eql_v3.jsonb_array_to_ore_block_256(jsonb)` /// across the same three input shapes. Safety net for the same plpgsql→sql /// inlining refactor. Behaviour pinned: /// - JSON null (`'null'`) → NULL composite @@ -325,7 +321,7 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { // SQL NULL (distinct from JSON null `'null'`). Not STRICT, so the body // runs: `jsonb_typeof(NULL)` is NULL → CASE guard not-true → ELSE NULL. let is_null: bool = - sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_u64_8_256(NULL::jsonb) IS NULL") + sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_256(NULL::jsonb) IS NULL") .fetch_one(&pool) .await?; assert!( @@ -334,23 +330,22 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { ); // JSON null → NULL composite. - let is_null: bool = sqlx::query_scalar( - "SELECT eql_v3.jsonb_array_to_ore_block_u64_8_256('null'::jsonb) IS NULL", - ) - .fetch_one(&pool) - .await?; + let is_null: bool = + sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_256('null'::jsonb) IS NULL") + .fetch_one(&pool) + .await?; assert!(is_null, "JSON null must yield NULL composite"); // Empty array → NULL composite. let is_null: bool = - sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_u64_8_256('[]'::jsonb) IS NULL") + sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_256('[]'::jsonb) IS NULL") .fetch_one(&pool) .await?; assert!(is_null, "empty JSON array must yield NULL composite"); // Single-element array → non-NULL composite with exactly 1 term. let term_count: i32 = sqlx::query_scalar( - "SELECT cardinality((eql_v3.jsonb_array_to_ore_block_u64_8_256('[\"aabb\"]'::jsonb)).terms)", + "SELECT cardinality((eql_v3.jsonb_array_to_ore_block_256('[\"aabb\"]'::jsonb)).terms)", ) .fetch_one(&pool) .await?; @@ -361,7 +356,7 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { // Populated array → non-NULL composite with one term per element. let term_count: i32 = sqlx::query_scalar( - "SELECT cardinality((eql_v3.jsonb_array_to_ore_block_u64_8_256('[\"aabb\",\"ccdd\",\"eeff\"]'::jsonb)).terms)", + "SELECT cardinality((eql_v3.jsonb_array_to_ore_block_256('[\"aabb\",\"ccdd\",\"eeff\"]'::jsonb)).terms)", ) .fetch_one(&pool) .await?; @@ -372,7 +367,7 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { // Deliberate delta: a non-array JSON scalar returns NULL (not a raise). let is_null: bool = - sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_u64_8_256('5'::jsonb) IS NULL") + sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_256('5'::jsonb) IS NULL") .fetch_one(&pool) .await?; assert!( @@ -383,7 +378,7 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { // Same delta for a non-array JSON object — `jsonb_typeof` is 'object', so // the CASE guard is not-true → ELSE NULL (not a raise). let is_null: bool = - sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_u64_8_256('{}'::jsonb) IS NULL") + sqlx::query_scalar("SELECT eql_v3.jsonb_array_to_ore_block_256('{}'::jsonb) IS NULL") .fetch_one(&pool) .await?; assert!( @@ -394,7 +389,7 @@ async fn jsonb_array_to_ore_block_input_shapes(pool: PgPool) -> Result<()> { Ok(()) } -/// T8 — Catalog pin for all three `compare_ore_block_u64_8_256_term(s)` overloads +/// T8 — Catalog pin for all three `compare_ore_block_256_term(s)` overloads /// (term×term, term[]×term[], composite×composite). Two load-bearing catalog /// properties are pinned at the same layer: /// @@ -420,8 +415,8 @@ async fn ore_comparators_are_immutable(pool: PgPool) -> Result<()> { FROM pg_catalog.pg_proc p WHERE p.pronamespace = 'eql_v3'::regnamespace AND p.proname IN ( - 'compare_ore_block_u64_8_256_term', - 'compare_ore_block_u64_8_256_terms' + 'compare_ore_block_256_term', + 'compare_ore_block_256_terms' ) ORDER BY args "#, @@ -440,11 +435,11 @@ async fn ore_comparators_are_immutable(pool: PgPool) -> Result<()> { for (args, provolatile, isstrict) in &rows { assert_eq!( provolatile, "i", - "compare_ore_block_u64_8_256_term(s)({args}) must be IMMUTABLE, got provolatile={provolatile}" + "compare_ore_block_256_term(s)({args}) must be IMMUTABLE, got provolatile={provolatile}" ); assert!( !isstrict, - "compare_ore_block_u64_8_256_term(s)({args}) must NOT be STRICT (NULL branches are load-bearing)" + "compare_ore_block_256_term(s)({args}) must NOT be STRICT (NULL branches are load-bearing)" ); } Ok(()) @@ -516,7 +511,7 @@ async fn bloom_filter_extractor_empty_array_is_empty_not_null(pool: PgPool) -> R } /// T10 — `eql_v3.has_bloom_filter(jsonb)` presence predicate. Mirrors the -/// `has_hmac_256` / `has_ore_block_u64_8_256` coverage in T5: its two-part guard +/// `has_hmac_256` / `has_ore_block_256` coverage in T5: its two-part guard /// (`val ? 'bf'` AND `val ->> 'bf' IS NOT NULL`) is exercised across present, /// absent, and json-null cases. The `{"bf":null}` → false case pins the /// `IS NOT NULL` half — the predicate is not reached transitively by the @@ -547,7 +542,7 @@ async fn has_bloom_filter_detects_bf_presence(pool: PgPool) -> Result<()> { Ok(()) } -/// T11 — Planner-selectivity metadata for the `eql_v3.ore_block_u64_8_256` +/// T11 — Planner-selectivity metadata for the `eql_v3.ore_block_256` /// `=` / `<>` operators. `<>` must use the inequality estimators /// (`neqsel` / `neqjoinsel`) and must NOT declare `HASHES` — an earlier revision /// copied `=`'s `eqsel` / `eqjoinsel` + `HASHES` onto `<>`, which is meaningless @@ -561,8 +556,8 @@ async fn ore_block_comparison_operators_declare_correct_selectivity(pool: PgPool SELECT o.oprrest::text, o.oprjoin::text, o.oprcanhash, o.oprcanmerge FROM pg_operator o WHERE o.oprname = '=' - AND o.oprleft = 'eql_v3.ore_block_u64_8_256'::regtype - AND o.oprright = 'eql_v3.ore_block_u64_8_256'::regtype + AND o.oprleft = 'eql_v3.ore_block_256'::regtype + AND o.oprright = 'eql_v3.ore_block_256'::regtype "#, ) .fetch_one(&pool) @@ -577,8 +572,8 @@ async fn ore_block_comparison_operators_declare_correct_selectivity(pool: PgPool SELECT o.oprrest::text, o.oprjoin::text, o.oprcanhash FROM pg_operator o WHERE o.oprname = '<>' - AND o.oprleft = 'eql_v3.ore_block_u64_8_256'::regtype - AND o.oprright = 'eql_v3.ore_block_u64_8_256'::regtype + AND o.oprleft = 'eql_v3.ore_block_256'::regtype + AND o.oprright = 'eql_v3.ore_block_256'::regtype "#, ) .fetch_one(&pool) From aaec244657392ee073fa41bb0a95a4f5b06142ed Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 11 Jun 2026 18:31:29 +1000 Subject: [PATCH 02/12] feat(v3): N-block ORE comparator + ordered numeric & timestamptz Derive the ORE block count N from term length instead of hardcoding 8, and wire the ordered numeric scalar while promoting timestamptz to ordered. --- Cargo.lock | 4 + crates/eql-scalars/src/lib.rs | 70 +++++++++++----- crates/eql-scalars/src/tests.rs | 46 ++++++++--- crates/eql-tests-macros/src/lib.rs | 14 +++- src/v3/sem/ore_block_256/functions.sql | 32 +++++++- tests/sqlx/Cargo.toml | 7 +- tests/sqlx/src/fixtures/eql_plaintext.rs | 16 +++- tests/sqlx/src/fixtures/scalar_fixture.rs | 31 +++++++ tests/sqlx/src/scalar_domains.rs | 82 +++++++++++++++++++ tests/sqlx/src/scalar_types.rs | 1 + .../encrypted_domain/family/mutations.rs | 24 ++++-- .../sqlx/tests/ore_block_comparator_tests.rs | 52 ++++++++++++ 12 files changed, 329 insertions(+), 50 deletions(-) create mode 100644 tests/sqlx/tests/ore_block_comparator_tests.rs diff --git a/Cargo.lock b/Cargo.lock index dd9a5f47..2fb317e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,7 @@ dependencies = [ "hex", "jsonschema", "paste", + "rust_decimal", "serde", "serde_json", "sqlx", @@ -3679,6 +3680,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", + "rust_decimal", "serde", "serde_json", "sha2", @@ -3760,6 +3762,7 @@ dependencies = [ "percent-encoding", "rand 0.8.6", "rsa", + "rust_decimal", "serde", "sha1", "sha2", @@ -3798,6 +3801,7 @@ dependencies = [ "memchr", "once_cell", "rand 0.8.6", + "rust_decimal", "serde", "serde_json", "sha2", diff --git a/crates/eql-scalars/src/lib.rs b/crates/eql-scalars/src/lib.rs index b9f87c70..7607609c 100644 --- a/crates/eql-scalars/src/lib.rs +++ b/crates/eql-scalars/src/lib.rs @@ -230,13 +230,15 @@ const ORDERED_INT_DOMAINS: &[DomainSpec] = &[ }, ]; -/// Equality-only domains: storage (no terms) + `_eq` (hm). Used by scalar types -/// that can hash for equality but cannot (yet) be ordered. `timestamptz` is the -/// first such type: cipherstash encrypts `Plaintext::Timestamp` at native -/// 12-block ORE width, but EQL's only ORE comparator -/// (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so an -/// ordered domain would silently mis-order. Ordering is deferred until a -/// wide-ORE (12-block) term exists. +/// Equality-only domains: storage (no terms) + `_eq` (hm). The canonical shape +/// for a scalar type that can hash for equality but is not ORE-orderable. +/// **Currently unused:** `timestamptz` (the previous sole user) was promoted to +/// the ordered shape once `eql_v3.compare_ore_block_256_term` generalized to N +/// blocks and could order its native 12-block ORE width. Retained — and still +/// validated as a known-valid shape by `every_type_uses_a_known_domain_shape` — +/// so a future non-orderable scalar (e.g. a hash-only type) can reuse it without +/// reconstructing the shape. +#[allow(dead_code)] const EQ_ONLY_DOMAINS: &[DomainSpec] = &[ DomainSpec { suffix: "", @@ -296,6 +298,17 @@ const TIMESTAMPTZ_FIXTURES: &[Fixture] = fixtures!(timestamptz; "2012-06-30T11:59:59Z", "2016-03-15T08:15:30Z", "2020-10-21T14:45:00Z", "2024-02-29T17:30:45Z", "2038-01-19T03:14:07Z", "2099-12-31T23:59:59Z"); +/// `numeric` fixture plaintexts — distinct by `Decimal` value, spanning sign, +/// magnitude, and scale, and including `0` plus the min/max pivots +/// (`-1000000000000` / `1000000000000`). They mirror `ore-rs`'s own +/// order-pinning vectors so the 14-block ORE edges (sign + high/low blocks) are +/// exercised. Each literal is distinct by parsed value (no `"1"`/`"1.0"` +/// aliasing) — the harness `numeric_fixtures_distinct_by_value` guard enforces +/// this, since the zero-dep catalog only dedupes by literal string. +const NUMERIC_FIXTURES: &[Fixture] = fixtures!(numeric; + "-1000000000000", "-1000000", "-1.001", "-1", "-0.5", "-0.001", + "0", "0.001", "0.5", "0.999999999", "1", "1.001", "1000000", "1000000000000"); + const INT4: ScalarSpec = ScalarSpec { token: "int4", kind: ScalarKind::I32, @@ -332,27 +345,42 @@ pub const DATE: ScalarSpec = ScalarSpec { fixtures: DATE_FIXTURES, }; -/// `timestamptz` — an **equality-only** (UTC-normalized) non-integer scalar. -/// Uses `EQ_ONLY_DOMAINS` (storage + `_eq`) rather than the four-domain ordered -/// shape: cipherstash encrypts `Plaintext::Timestamp` at native 12-block ORE -/// width, but EQL's only ORE comparator -/// (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so an -/// ordered timestamptz domain would silently mis-order. Ordering is deferred to -/// a future PR that adds a wide-ORE (12-block) term. The three "pivot" fixture -/// values are retained as equality pivots; the kind stays ordered-shaped -/// (carries a rust type, no i128 range) so the harness can parse them. +/// `timestamptz` — an **ordered**, UTC-normalized non-integer scalar. Uses the +/// four-domain ordered shape (storage, `_eq`, `_ord`, `_ord_ore`): cipherstash +/// encrypts `Plaintext::Timestamp` at native 12-block ORE width, which the +/// generalized `eql_v3.compare_ore_block_256_term` comparator orders correctly. +/// Values are UTC-normalized (cipherstash has no tz-preserving type) and encrypt +/// under the `timestamp` cast. /// /// Public (like `DATE`) because the SQLx harness reads `TIMESTAMPTZ.fixtures` -/// directly to parse the RFC3339 strings into `chrono::DateTime` at -/// runtime — there is no `TIMESTAMPTZ_VALUES` const (chrono is not -/// `const`-friendly and `eql-scalars` stays zero-dep). +/// directly to parse the RFC3339 strings into `chrono::DateTime` at runtime +/// (no `TIMESTAMPTZ_VALUES` const; `eql-scalars` stays zero-dep). pub const TIMESTAMPTZ: ScalarSpec = ScalarSpec { token: "timestamptz", kind: ScalarKind::Timestamptz, - domains: EQ_ONLY_DOMAINS, + domains: ORDERED_INT_DOMAINS, fixtures: TIMESTAMPTZ_FIXTURES, }; +/// `numeric` — an **ordered** non-integer scalar backed by +/// `rust_decimal::Decimal`. Uses the four-domain ordered shape: cipherstash +/// encrypts `Plaintext::Decimal` at native 14-block ORE width, which the +/// generalized `eql_v3.compare_ore_block_256_term` comparator orders correctly. +/// `numeric_value` returns `None` (no i128 range); ordering is supplied by the +/// harness `Decimal: Ord`, which `ore-rs` guarantees agrees with the ciphertext +/// order (equivalent scales collide, like `Decimal`'s own `Ord`). +/// +/// Public (like `DATE` / `TIMESTAMPTZ`) so the SQLx harness reads +/// `NUMERIC.fixtures` directly to parse the decimal strings into +/// `rust_decimal::Decimal` at runtime (the catalog stays zero-dep: no +/// `rust_decimal`). +pub const NUMERIC: ScalarSpec = ScalarSpec { + token: "numeric", + kind: ScalarKind::Numeric, + domains: ORDERED_INT_DOMAINS, + fixtures: NUMERIC_FIXTURES, +}; + /// Domains for `text`: the ordered shape (with exact `hm` equality on the /// ordered domains), a `_match` domain (`Bloom` containment), and a combined /// `_search` domain carrying equality + ordering + match in one type. @@ -417,7 +445,7 @@ pub const TEXT: ScalarSpec = ScalarSpec { /// The scalar catalog — the single source of truth. Order is significant (it /// drives generation order). New types are appended as their SQL surface lands. -pub const CATALOG: &[ScalarSpec] = &[INT4, INT2, INT8, DATE, TIMESTAMPTZ, TEXT]; +pub const CATALOG: &[ScalarSpec] = &[INT4, INT2, INT8, DATE, TIMESTAMPTZ, NUMERIC, TEXT]; /// Materialise an integer scalar's fixtures into a typed `&'static` slice at /// compile time. This is the **single-sourced** plaintext list the SQLx test diff --git a/crates/eql-scalars/src/tests.rs b/crates/eql-scalars/src/tests.rs index 85341337..1be3a65f 100644 --- a/crates/eql-scalars/src/tests.rs +++ b/crates/eql-scalars/src/tests.rs @@ -170,7 +170,24 @@ mod rust_tests { let date = CATALOG.iter().find(|s| s.token == "date").unwrap(); assert!(!date.is_eq_only(), "date is ordered"); let ts = CATALOG.iter().find(|s| s.token == "timestamptz").unwrap(); - assert!(ts.is_eq_only(), "timestamptz is equality-only"); + assert!( + !ts.is_eq_only(), + "timestamptz is now ordered (native 12-block ORE, comparator generalized to N blocks)" + ); + + // No catalog type is currently eq-only, so exercise `is_eq_only()`'s + // positive path with a synthetic spec built on the retained + // `EQ_ONLY_DOMAINS` shape (storage + `_eq`, no `_ord`). + let eq_only = ScalarSpec { + token: "synthetic_eq_only", + kind: ScalarKind::Timestamptz, + domains: EQ_ONLY_DOMAINS, + fixtures: &[], + }; + assert!( + eq_only.is_eq_only(), + "a storage+_eq spec (no _ord) must be detected as eq-only" + ); } } @@ -496,11 +513,19 @@ mod catalog_tests { } #[test] - fn catalog_has_int4_int2_int8_date_timestamptz_text_in_order() { + fn catalog_has_int4_int2_int8_date_timestamptz_numeric_text_in_order() { let tokens: Vec<&str> = CATALOG.iter().map(|s| s.token).collect(); assert_eq!( tokens, - vec!["int4", "int2", "int8", "date", "timestamptz", "text"] + vec![ + "int4", + "int2", + "int8", + "date", + "timestamptz", + "numeric", + "text" + ] ); } @@ -710,17 +735,14 @@ mod catalog_tests { #[test] fn ordered_and_eq_only_shapes_are_used_as_declared() { - // Pin which catalog tokens carry which shape, so a row silently flipping - // ORDERED_INT_DOMAINS <-> EQ_ONLY_DOMAINS is caught. timestamptz is - // equality-only (12-block ORE vs 8-block comparator); the rest ordered - // (text adds `_match` and `_search` domains on top, so it is not - // eq_only either). + // All current catalog types use the four-domain ordered shape; none is + // equality-only. (timestamptz was promoted to ordered once the ORE + // comparator generalized to N blocks — see the numeric/ORE work.) for s in CATALOG { let is_eq_only = s.domains.len() == 2; - let expect_eq_only = s.token == "timestamptz"; - assert_eq!( - is_eq_only, expect_eq_only, - "{} domain shape (eq_only={is_eq_only}) does not match expectation", + assert!( + !is_eq_only, + "{} is unexpectedly eq-only; no catalog type is eq-only currently", s.token ); } diff --git a/crates/eql-tests-macros/src/lib.rs b/crates/eql-tests-macros/src/lib.rs index aba3ebaa..8ad49520 100644 --- a/crates/eql-tests-macros/src/lib.rs +++ b/crates/eql-tests-macros/src/lib.rs @@ -93,6 +93,14 @@ fn is_text_token(token: &str) -> bool { spec_for_token(token).kind.is_text() } +/// True when `token`'s catalog row is the `numeric` kind (owned +/// `rust_decimal::Decimal`). Like `text` it is ordered but non-integer and +/// non-chrono, so it stamps the `numeric` fixture discriminator and draws its +/// values from the harness accessor (`numeric_values()`). +fn is_numeric_token(token: &str) -> bool { + matches!(spec_for_token(token).kind, eql_scalars::ScalarKind::Numeric) +} + /// True when `token`'s catalog row declares no ordered domain — equality-only. /// Replaces the `[eq_only]` marker. Consumed by [`matrix_suite_for_entry`] to /// keep an eq-only type out of the ordered matrix (which exercises ordering @@ -224,10 +232,12 @@ fn scalar_fixture_modules_tokens(list: &ScalarList) -> TokenStream2 { format_ident!("temporal") } else if is_text_token(&token_str) { format_ident!("text") + } else if is_numeric_token(&token_str) { + format_ident!("numeric") } else { panic!( - "scalar token `{token_str}` is neither integer, temporal, nor \ - text — no fixture discriminator is wired for its kind" + "scalar token `{token_str}` is neither integer, temporal, text, \ + nor numeric — no fixture discriminator is wired for its kind" ) }; quote! { diff --git a/src/v3/sem/ore_block_256/functions.sql b/src/v3/sem/ore_block_256/functions.sql index 7d5bce88..83bd42e1 100644 --- a/src/v3/sem/ore_block_256/functions.sql +++ b/src/v3/sem/ore_block_256/functions.sql @@ -109,7 +109,16 @@ AS $$ left_block_size CONSTANT smallint := 16; right_block_size CONSTANT smallint := 32; - right_offset CONSTANT smallint := 136; -- 8 * 17 + + -- Block count N is DERIVED from the ciphertext length, not hardcoded to 8. + -- Wire format per term: + -- [ N PRP bytes ][ N*16B left blocks ][ 16B hash key ][ N*32B right blocks ] + -- octet_length = 17*N + 16 + 32*N = 49*N + 16 => N = (octet_length - 16) / 49 + -- This serves int4 (N=8, 408B), timestamp (N=12, 604B), and numeric + -- (N=14, 702B) with one comparator. + n integer; + left_offset integer; -- ordinal offset of the first left block (1 + N PRP bytes) + right_offset integer; -- ordinal start of the right CT (= total left CT length = 17*N) indicator smallint := 0; BEGIN @@ -129,10 +138,23 @@ AS $$ RAISE EXCEPTION 'Ciphertexts are different lengths'; END IF; - FOR block IN 0..7 LOOP + -- Well-formedness: length must be exactly 49*N + 16 for some N >= 1. The + -- modulo alone is insufficient -- a 16-byte term passes (16 - 16) % 49 = 0 + -- and derives N = 0, which would fall through to the all-blocks-equal path + -- and return 0 instead of raising. The `<= 16` clause is load-bearing. + IF octet_length(a.bytes) <= 16 OR (octet_length(a.bytes) - 16) % 49 != 0 THEN + RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(a.bytes); + END IF; + + n := (octet_length(a.bytes) - 16) / 49; + left_offset := 1 + n; -- left blocks begin right after the N PRP bytes + right_offset := 17 * n; -- right CT begins right after the 17*N-byte left CT + + FOR block IN 0..n-1 LOOP + -- Compare each PRP byte (the first N bytes) and its 16-byte left block. IF substr(a.bytes, 1 + block, 1) != substr(b.bytes, 1 + block, 1) - OR substr(a.bytes, 9 + left_block_size * block, left_block_size) != substr(b.bytes, 9 + left_block_size * block, left_block_size) + OR substr(a.bytes, left_offset + left_block_size * block, left_block_size) != substr(b.bytes, left_offset + left_block_size * block, left_block_size) THEN IF eq THEN unequal_block := block; @@ -145,11 +167,13 @@ AS $$ RETURN 0::integer; END IF; + -- Hash key is the IV from the right CT of b. hash_key := substr(b.bytes, right_offset + 1, 16); + -- First right block is at right_offset + nonce_size (ordinally indexed). target_block := substr(b.bytes, right_offset + 17 + (unequal_block * right_block_size), right_block_size); - data_block := substr(a.bytes, 9 + (left_block_size * unequal_block), left_block_size); + data_block := substr(a.bytes, left_offset + (left_block_size * unequal_block), left_block_size); encrypt_block := encrypt(data_block::bytea, hash_key::bytea, 'aes-ecb'); diff --git a/tests/sqlx/Cargo.toml b/tests/sqlx/Cargo.toml index fbdd9506..a142233c 100644 --- a/tests/sqlx/Cargo.toml +++ b/tests/sqlx/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "macros", "chrono"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "macros", "chrono", "rust_decimal"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -16,6 +16,11 @@ cipherstash-client = { version = "0.35", features = ["tokio"] } # it as a direct dependency so the harness can name `chrono::NaiveDate` for the # `date` scalar (Encode/Decode/Type come from the sqlx `chrono` feature above). chrono = { version = "0.4", default-features = false } +# rust_decimal backs the `numeric` scalar. The sqlx `rust_decimal` feature above +# provides Encode/Decode/Type; this names the type directly for the +# harness impls (scalar_domains.rs, eql_plaintext.rs). Already in the tree +# transitively (cipherstash-client / ore-rs). +rust_decimal = "1" paste = "1" eql-scalars = { path = "../../crates/eql-scalars" } eql-tests-macros = { path = "../../crates/eql-tests-macros" } diff --git a/tests/sqlx/src/fixtures/eql_plaintext.rs b/tests/sqlx/src/fixtures/eql_plaintext.rs index 9e85aac5..f447429b 100644 --- a/tests/sqlx/src/fixtures/eql_plaintext.rs +++ b/tests/sqlx/src/fixtures/eql_plaintext.rs @@ -60,6 +60,7 @@ impl PlaintextSqlType { pub const TIMESTAMPTZ: PlaintextSqlType = PlaintextSqlType("timestamp with time zone"); pub const TEXT: PlaintextSqlType = PlaintextSqlType("text"); pub const JSONB: PlaintextSqlType = PlaintextSqlType("jsonb"); + pub const NUMERIC: PlaintextSqlType = PlaintextSqlType("numeric"); pub fn as_str(&self) -> &'static str { self.0 @@ -86,7 +87,8 @@ const fn cast_for_kind(kind: ScalarKind) -> Cast { ScalarKind::Date => Cast::DATE, ScalarKind::Timestamptz => Cast::TIMESTAMP, ScalarKind::Text => Cast::TEXT, - ScalarKind::Numeric | ScalarKind::Jsonb => { + ScalarKind::Numeric => Cast::DECIMAL, + ScalarKind::Jsonb => { panic!("EqlPlaintext is only implemented for the wired scalar kinds") } } @@ -103,7 +105,8 @@ const fn plaintext_sql_type_for_kind(kind: ScalarKind) -> PlaintextSqlType { ScalarKind::Date => PlaintextSqlType::DATE, ScalarKind::Timestamptz => PlaintextSqlType::TIMESTAMPTZ, ScalarKind::Text => PlaintextSqlType::TEXT, - ScalarKind::Numeric | ScalarKind::Jsonb => { + ScalarKind::Numeric => PlaintextSqlType::NUMERIC, + ScalarKind::Jsonb => { panic!("EqlPlaintext is only implemented for the wired scalar kinds") } } @@ -118,6 +121,7 @@ mod sealed { impl Sealed for chrono::DateTime {} impl Sealed for String {} impl Sealed for serde_json::Value {} + impl Sealed for rust_decimal::Decimal {} } /// A Rust type usable as a fixture `plaintext` value, carrying its EQL cast @@ -213,6 +217,14 @@ impl EqlPlaintext for serde_json::Value { } } +impl EqlPlaintext for rust_decimal::Decimal { + const KIND: ScalarKind = ScalarKind::Numeric; + + fn to_plaintext(&self) -> Plaintext { + Plaintext::Decimal(Some(*self)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/sqlx/src/fixtures/scalar_fixture.rs b/tests/sqlx/src/fixtures/scalar_fixture.rs index 6efcfa3d..b84f0a0d 100644 --- a/tests/sqlx/src/fixtures/scalar_fixture.rs +++ b/tests/sqlx/src/fixtures/scalar_fixture.rs @@ -138,6 +138,37 @@ macro_rules! scalar_fixture { } }; + // Numeric scalars (`rust_decimal::Decimal`): ordered, non-chrono. Same + // shape as `temporal` — `[Unique, Ore]` indexes, pivot-presence asserts via + // `OrderedScalar` — but materialised from owned `Decimal` values (no `Match` + // index, no chrono). + (numeric, $name:literal, $ty:ty, $values:expr $(,)?) => { + $crate::scalar_fixture!(@common $name, $ty, $values, [Unique, Ore]); + + #[cfg(test)] + mod tests { + use super::*; + use $crate::scalar_domains::OrderedScalar; + + #[test] + fn spec_is_complete() { + assert!(spec().check_complete().is_ok()); + } + + #[test] + fn spec_includes_pivots() { + let spec = spec(); + let values = spec.values(); + let min = <$ty as OrderedScalar>::min_pivot(); + let mid = <$ty as OrderedScalar>::mid_pivot(); + let max = <$ty as OrderedScalar>::max_pivot(); + assert!(values.contains(&min), "spec must include min_pivot {min:?}"); + assert!(values.contains(&mid), "spec must include mid_pivot {mid:?}"); + assert!(values.contains(&max), "spec must include max_pivot {max:?}"); + } + } + }; + // Shared expansion: the `spec()` builder + the gated generator test. The // trailing `[Unique, Ore, ...]` token list parametrizes the index set. (@common $name:literal, $ty:ty, $values:expr, [$($ix:ident),+ $(,)?]) => { diff --git a/tests/sqlx/src/scalar_domains.rs b/tests/sqlx/src/scalar_domains.rs index 16e5a088..84bf9da2 100644 --- a/tests/sqlx/src/scalar_domains.rs +++ b/tests/sqlx/src/scalar_domains.rs @@ -485,6 +485,88 @@ impl MatchScalar for String { // numeric origin / sign boundary. The signed-only sign-boundary test bounds on // `SignedScalar`, so a `String` instantiation of it would not compile. +// `numeric` is hand-written (like `text`): an owned `rust_decimal::Decimal`, +// not chrono-backed, so it parses the catalog's `Fixture::Numeric` strings into +// a `LazyLock>` rather than going through `temporal_values!`. The +// catalog stays zero-dep, so the parse happens here, not in `eql-scalars`. + +/// Typed `Decimal` fixture values, parsed once from `numeric`'s catalog row. +static NUMERIC_VALUES_CELL: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + use std::str::FromStr; + eql_scalars::NUMERIC + .fixtures + .iter() + .map(|f| match f { + eql_scalars::Fixture::Numeric(s) => rust_decimal::Decimal::from_str(s) + .unwrap_or_else(|e| panic!("invalid numeric catalog fixture {s:?}: {e}")), + other => panic!("non-numeric fixture in numeric catalog row: {other:?}"), + }) + .collect() + }); + +/// The `Decimal` fixture values, in catalog order. Public so the `eql_v2_numeric` +/// fixture module (emitted by `scalar_types!(fixture_modules)`) can hand the +/// slice to `scalar_fixture!`. +pub fn numeric_values() -> &'static [rust_decimal::Decimal] { + &NUMERIC_VALUES_CELL +} + +impl ScalarType for rust_decimal::Decimal { + const PG_TYPE: &'static str = "numeric"; + + fn fixture_values() -> &'static [Self] { + numeric_values() + } + // `to_sql_literal` inherits the default (`value.to_string()`): a `Decimal`'s + // `Display` form (e.g. `-1000000000000`, `0.001`) is a valid SQL numeric + // literal, so no quoting/override is needed (unlike `text` / `date`). +} + +impl OrderedScalar for rust_decimal::Decimal { + /// The smallest fixture decimal. Present verbatim in `fixture_values()`. + fn min_pivot() -> Self { + use std::str::FromStr; + rust_decimal::Decimal::from_str("-1000000000000").unwrap() + } + + /// The largest fixture decimal. Present verbatim in `fixture_values()`. + fn max_pivot() -> Self { + use std::str::FromStr; + rust_decimal::Decimal::from_str("1000000000000").unwrap() + } + // `mid_pivot` inherits the default `Self::default()` = `Decimal::ZERO` = 0, + // which is a real fixture and the numeric origin. +} + +// `Decimal` is deliberately NOT `SignedScalar`: like `text`, it is an +// ordered non-integer kind. The signed-only sign-boundary test bounds on +// `SignedScalar`, so it is not instantiated for numeric. + +/// `eql-scalars`' distinctness invariant keys `Fixture::Numeric` by its literal +/// string, so `"1"` and `"1.0"` would pass there as "distinct". But they denote +/// the same `Decimal` value (and collide in the ORE ciphertext, per ore-rs's +/// `equivalent_forms_collide_in_ciphertext`), so an aliasing pair would insert +/// duplicate `plaintext` rows and break `fetch_fixture_payload`'s `fetch_one`. +/// This guards distinctness by parsed value, which is the property the fixture +/// table relies on. +#[cfg(test)] +mod numeric_value_guards { + use super::*; + + #[test] + fn fixtures_are_distinct_by_value() { + use std::collections::HashSet; + let vals = numeric_values(); // &[Decimal], parsed from the catalog + let unique: HashSet<_> = vals.iter().collect(); + assert_eq!( + unique.len(), + vals.len(), + "two numeric fixtures alias to the same Decimal value", + ); + } +} + #[cfg(test)] mod text_value_tests { use super::*; diff --git a/tests/sqlx/src/scalar_types.rs b/tests/sqlx/src/scalar_types.rs index a51b288d..926d363d 100644 --- a/tests/sqlx/src/scalar_types.rs +++ b/tests/sqlx/src/scalar_types.rs @@ -56,6 +56,7 @@ macro_rules! scalar_types { int8 => i64, date => chrono::NaiveDate, timestamptz => chrono::DateTime, + numeric => rust_decimal::Decimal, text => String, } }; diff --git a/tests/sqlx/tests/encrypted_domain/family/mutations.rs b/tests/sqlx/tests/encrypted_domain/family/mutations.rs index f2399a2b..889293a5 100644 --- a/tests/sqlx/tests/encrypted_domain/family/mutations.rs +++ b/tests/sqlx/tests/encrypted_domain/family/mutations.rs @@ -212,15 +212,23 @@ async fn blocking_lt_flips_lt_arm_but_not_order_by(pool: PgPool) -> Result<()> { let mut ascending: Vec = ::fixture_values().to_vec(); ascending.sort(); - // Baseline: `<` works (no raise) and ORDER BY is plaintext-sorted. - let lt_baseline: Option = sqlx::query_scalar(lt_sql) - .bind(PLACEHOLDER_PAYLOAD) - .bind(PLACEHOLDER_PAYLOAD) - .fetch_one(&pool) - .await?; + // Baseline: `<` works (no raise) and ORDER BY is plaintext-sorted. Uses two + // REAL fixture ORE payloads (smallest two plaintexts), not PLACEHOLDER_PAYLOAD + // — the latter carries a 1-byte `ob` stub that the N-block comparator's + // well-formedness guard now (correctly) rejects. PLACEHOLDER stays in the + // post-mutation `assert_raises` below, where the `lt` blocker raises before + // the comparator ever inspects the term. + let lt_baseline: Option = sqlx::query_scalar( + "SELECT (SELECT payload FROM fixtures.eql_v2_int4 WHERE plaintext = $1)::eql_v3.int4_ord \ + < (SELECT payload FROM fixtures.eql_v2_int4 WHERE plaintext = $2)::eql_v3.int4_ord", + ) + .bind(ascending[0]) + .bind(ascending[1]) + .fetch_one(&pool) + .await?; ensure!( - lt_baseline.is_some(), - "baseline: `_ord` `<` must return a boolean (got {lt_baseline:?})" + lt_baseline == Some(true), + "baseline: smaller `_ord` `<` larger must be true (got {lt_baseline:?})" ); let order_baseline: Vec = sqlx::query_scalar(order_by_sql).fetch_all(&pool).await?; ensure!( diff --git a/tests/sqlx/tests/ore_block_comparator_tests.rs b/tests/sqlx/tests/ore_block_comparator_tests.rs new file mode 100644 index 00000000..cec07bf4 --- /dev/null +++ b/tests/sqlx/tests/ore_block_comparator_tests.rs @@ -0,0 +1,52 @@ +//! Direct unit tests for the generalized N-block ORE comparator +//! `eql_v3.compare_ore_block_256_term`. +//! +//! The malformed-length guards here are creds-free: they construct ORE terms +//! by hand from short byte strings, so they exercise the length validation +//! without needing real (ZeroKMS-generated) ciphertexts. The wide-term ordering +//! test (added in Phase 4) uses generated numeric fixtures. + +use anyhow::Result; +use sqlx::PgPool; + +/// A `bytea` whose length is NOT a valid `49*N + 16` must raise, not silently +/// return 0. Uses a 4-byte term (equal lengths so the equal-length guard does +/// not fire first). +#[sqlx::test] +async fn comparator_rejects_non_conforming_length(pool: PgPool) -> Result<()> { + let sql = "SELECT eql_v3.compare_ore_block_256_term( \ + ROW('\\x00010203'::bytea)::eql_v3.ore_block_256_term, \ + ROW('\\x04050607'::bytea)::eql_v3.ore_block_256_term)"; + let err = sqlx::query_scalar::<_, i32>(sql) + .fetch_one(&pool) + .await + .expect_err("a 4-byte ORE term must raise, not return a comparison"); + assert!( + err.to_string() + .to_lowercase() + .contains("malformed ore term"), + "expected malformed-term error, got: {err}" + ); + Ok(()) +} + +/// A 16-byte term satisfies `(16 - 16) % 49 == 0` and derives N = 0; the +/// `<= 16` clause must still reject it (otherwise it falls through to the +/// all-blocks-equal path and wrongly returns 0). +#[sqlx::test] +async fn comparator_rejects_sixteen_byte_term(pool: PgPool) -> Result<()> { + let sql = "SELECT eql_v3.compare_ore_block_256_term( \ + ROW(repeat('a', 16)::bytea)::eql_v3.ore_block_256_term, \ + ROW(repeat('b', 16)::bytea)::eql_v3.ore_block_256_term)"; + let err = sqlx::query_scalar::<_, i32>(sql) + .fetch_one(&pool) + .await + .expect_err("a 16-byte ORE term (N=0) must raise"); + assert!( + err.to_string() + .to_lowercase() + .contains("malformed ore term"), + "expected malformed-term error, got: {err}" + ); + Ok(()) +} From 9d861c6cd25b34ea4b6691ee7a8e0dd3522bd0d9 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 11 Jun 2026 19:15:00 +1000 Subject: [PATCH 03/12] test(v3): cover 14-block numeric + 12-block timestamptz ORE ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct comparator tests over generated fixtures: numeric terms are 702 bytes (N=14) and order across a full 14-value ascending chain spanning sign, magnitude, and fractional scale (so the left blocks decide ordering — the regression the missed 9 -> 1+n offset would fail); timestamptz terms are 604 bytes (N=12) and order 1900 < 2099. Verified end-to-end: numeric + timestamptz ordered matrix suites (211 each, incl. < <= > >= / ORDER BY / MIN / MAX) green; full SQLx suite 2026 passed; test:matrix:inventory reconciles both new ordered types; self-contained v3 install green. --- .../sqlx/tests/ore_block_comparator_tests.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/sqlx/tests/ore_block_comparator_tests.rs b/tests/sqlx/tests/ore_block_comparator_tests.rs index cec07bf4..6003d6d5 100644 --- a/tests/sqlx/tests/ore_block_comparator_tests.rs +++ b/tests/sqlx/tests/ore_block_comparator_tests.rs @@ -50,3 +50,81 @@ async fn comparator_rejects_sixteen_byte_term(pool: PgPool) -> Result<()> { ); Ok(()) } + +/// Width: a numeric ORE term must be 14 blocks => 49*14 + 16 = 702 bytes. +#[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric")))] +async fn numeric_term_is_14_blocks(pool: PgPool) -> Result<()> { + let width: i32 = sqlx::query_scalar( + "SELECT octet_length((((eql_v3.ord_term( \ + (SELECT payload FROM fixtures.eql_v2_numeric WHERE plaintext = (-1000000)::numeric) \ + ::eql_v3.numeric_ord)).terms)[1]).bytes)", + ) + .fetch_one(&pool) + .await?; + assert_eq!(width, 702, "numeric ORE term must be 14 blocks (702 bytes)"); + Ok(()) +} + +/// Full ascending chain of 14-block numeric terms: every adjacent pair must +/// order `-1`. Spans sign, magnitude, and fractional (low-block) scale, so the +/// left blocks — not just the right blocks — decide ordering. This is the +/// regression the missed `9 -> 1+n` left-offset would fail; a single pair could +/// pass against that bug. +#[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric")))] +async fn numeric_terms_order_in_ascending_chain(pool: PgPool) -> Result<()> { + let ascending = [ + "-1000000000000", + "-1000000", + "-1.001", + "-1", + "-0.5", + "-0.001", + "0", + "0.001", + "0.5", + "0.999999999", + "1", + "1.001", + "1000000", + "1000000000000", + ]; + for pair in ascending.windows(2) { + let (lo, hi) = (pair[0], pair[1]); + let cmp: i32 = sqlx::query_scalar(&format!( + "SELECT eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_numeric WHERE plaintext = ({lo})::numeric)::eql_v3.numeric_ord), \ + eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_numeric WHERE plaintext = ({hi})::numeric)::eql_v3.numeric_ord))" + )) + .fetch_one(&pool) + .await?; + assert_eq!(cmp, -1, "{lo} must order before {hi}"); + } + Ok(()) +} + +/// Symmetric 12-block (timestamptz, N=12 => 604 bytes) width + ordering check. +/// 12 is the only N strictly between the working 8 and the headline 14. +#[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_timestamptz")))] +async fn timestamptz_term_is_12_blocks_and_orders(pool: PgPool) -> Result<()> { + let width: i32 = sqlx::query_scalar( + "SELECT octet_length((((eql_v3.ord_term( \ + (SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '1970-01-01T00:00:00Z'::timestamptz) \ + ::eql_v3.timestamptz_ord)).terms)[1]).bytes)", + ) + .fetch_one(&pool) + .await?; + assert_eq!( + width, 604, + "timestamptz ORE term must be 12 blocks (604 bytes)" + ); + + let cmp: i32 = sqlx::query_scalar( + "SELECT eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '1900-01-01T00:00:00Z'::timestamptz)::eql_v3.timestamptz_ord), \ + eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '2099-12-31T23:59:59Z'::timestamptz)::eql_v3.timestamptz_ord))", + ) + .fetch_one(&pool) + .await?; + assert_eq!(cmp, -1, "1900 must order before 2099"); + Ok(()) +} From 3d1b87cac037398a18ba9488b32b85fa3c97ee42 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 11 Jun 2026 19:16:40 +1000 Subject: [PATCH 04/12] docs(v3): changelog + reference for N-block ORE, ordered numeric/timestamptz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG: rewrite the timestamptz entry (ordering now ships), add the numeric ordered-domain entry, and add a Fixed entry for the N-block ORE comparator generalization + the eql_v3 ore_block_u64_8_256 -> ore_block_256 rename. Reference guide: document the fourth (numeric/Decimal) fixture discriminator — proc-macro routing, scalar_fixture arm, numeric_values accessor + distinctness guard, rust_decimal dep. --- CHANGELOG.md | 7 +++- .../adding-a-scalar-encrypted-domain-type.md | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60f4903..c5e6997a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,8 @@ Each entry that ships in a published release links to the PR that introduced it. - **`eql_v3.int2` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int2` columns — `eql_v3.int2` (storage-only), `eql_v3.int2_eq` (`=` / `<>` via HMAC), and `eql_v3.int2_ord` / `eql_v3.int2_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int2` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `smallint` column, proving the scalar generator generalizes beyond the `int4` reference. ([#243](https://github.com/cipherstash/encrypt-query-language/pull/243)) - **`eql_v3.int8` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int8` columns — `eql_v3.int8` (storage-only), `eql_v3.int8_eq` (`=` / `<>` via HMAC), and `eql_v3.int8_ord` / `eql_v3.int8_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int8` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `bigint` column, extending the scalar generator across the full 64-bit integer width. ([#253](https://github.com/cipherstash/encrypt-query-language/pull/253)) - **`eql_v3.date` encrypted-domain type family.** Four jsonb-backed domains for encrypted `date` columns — `eql_v3.date` (storage-only), `eql_v3.date_eq` (`=` / `<>` via HMAC), and `eql_v3.date_ord` / `eql_v3.date_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `date` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Plaintexts encrypt under the `date` cast and compare via the same ORE block terms as the integer scalars (ORE is plaintext-agnostic — dates order like integers). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: the first **non-integer ordered** scalar encrypted-domain type — a type-safe, per-capability encrypted `date` column — proving the generator and SQLx test matrix generalize beyond fixed-width integers. ([#256](https://github.com/cipherstash/encrypt-query-language/pull/256)) -- **`eql_v3.timestamptz` encrypted-domain type family (equality-only).** Two jsonb-backed domains for encrypted `timestamptz` columns — `eql_v3.timestamptz` (storage-only) and `eql_v3.timestamptz_eq` (`=` / `<>` via HMAC) — generated from the `timestamptz` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.date` family. Values are **UTC-normalized** (cipherstash has no timezone-preserving type): plaintexts encrypt under the `timestamp` cast. Index via a functional index on the `eql_v3.eq_term` extractor, not an operator class on the domain. **Ordering (`<` `<=` `>` `>=`, `MIN` / `MAX`) is deferred:** cipherstash encrypts `Plaintext::Timestamp` at native 12-block ORE width, but EQL's only ORE comparator (`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so ordered timestamptz domains would silently mis-order. There are no `eql_v3.timestamptz_ord` / `_ord_ore` domains and no timestamptz `MIN` / `MAX` aggregates until a wide-ORE (12-block) term lands — tracked in [#241](https://github.com/cipherstash/encrypt-query-language/issues/241). Why: a type-safe, equality-searchable encrypted UTC-timestamp column, stacking on the `date` temporal-scalar foundation; ordering follows once the comparator supports the native ciphertext width. ([#257](https://github.com/cipherstash/encrypt-query-language/pull/257)) +- **`eql_v3.timestamptz` encrypted-domain type family (ordered).** Four jsonb-backed domains for encrypted `timestamptz` columns — `eql_v3.timestamptz` (storage-only), `eql_v3.timestamptz_eq` (`=` / `<>` via HMAC), and `eql_v3.timestamptz_ord` / `eql_v3.timestamptz_ord_ore` (also `<` `<=` `>` `>=`, `MIN` / `MAX` via 12-block ORE) — generated from the `timestamptz` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.date` family. Values are **UTC-normalized** (cipherstash has no timezone-preserving type): plaintexts encrypt under the `timestamp` cast. Ordering works because the `eql_v3` ORE block comparator now derives its block count from the ciphertext width (see the comparator entry below) instead of assuming 8. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. ([#257](https://github.com/cipherstash/encrypt-query-language/pull/257)) +- **`eql_v3.numeric` encrypted-domain type family (ordered).** Four jsonb-backed domains for encrypted `numeric` / `decimal` columns — `eql_v3.numeric` (storage-only), `eql_v3.numeric_eq` (`=` / `<>` via HMAC), and `eql_v3.numeric_ord` / `eql_v3.numeric_ord_ore` (also `<` `<=` `>` `>=`, `MIN` / `MAX` via 14-block ORE) — generated from the `numeric` row in `eql-scalars::CATALOG`. cipherstash encrypts `Plaintext::Decimal` at native 14-block ORE width; ordering matches `rust_decimal::Decimal` ordering exactly (equivalent scales such as `1` and `1.0` collide, like Postgres `numeric`). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors. Why: a type-safe, ordered encrypted decimal column, the first scalar to exercise an ORE term wider than 8 blocks. ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241)) - **Per-domain `MIN` / `MAX` aggregates for the encrypted-domain family.** `eql_v3.min(eql_v3._ord)` / `eql_v3.max(eql_v3._ord)` (and the `_ord_ore` twin) are generated for every ord-capable scalar variant, giving type-safe extrema on domain-typed columns — comparison routes through the variant's `<` / `>` operator (ORE block term, no decryption). The aggregates are declared `PARALLEL = SAFE` with a combine function (the state function itself — min/max are associative), so PostgreSQL can use partial/parallel aggregation on large `GROUP BY` workloads. Why: the new domain types previously had no equivalent of the composite-type aggregates. The existing `eql_v2.min(eql_v2_encrypted)` / `eql_v2.max(eql_v2_encrypted)` aggregates are **retained** and continue to work on `eql_v2_encrypted` columns; the per-domain aggregates are additive and coexist with them. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239)) - **`eql_v3.text` encrypted-domain family (`text`, `text_eq`, `text_match`, `text_ord`, `text_ord_ore`, `text_search`).** Adds equality (`=` / `<>` via HMAC), match (`@>` / `<@` via a new self-contained `eql_v3.bloom_filter` SEM index term), and ORE ordering (`<` `<=` `>` `>=`, `min` / `max`) for encrypted text, at parity with EQL v2 text — generated from the `text` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. `text` is the first scalar to add a new index `Term` (`Bloom`) and the first non-integer, unbounded ordered kind (lexicographic pivots, hand-written `impl ScalarType`). The combined **`text_search`** domain carries all three capabilities in one type — `=` / `<>` via HMAC, `<` `<=` `>` `>=` / `min` / `max` via ORE, and `@>` / `<@` via bloom filter. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` / `eql_v3.match_term` extractors, not an operator class on the domain. Why: brings searchable encrypted text to the namespaced, `eql_v2`-free `eql_v3` surface. Match is exposed as bloom-filter containment on the `text_match` / `text_search` domains — deliberately *not* SQL `LIKE` (no wildcard/anchoring; probabilistic ngram containment) — and never backs equality. **Equality on the ordered text domains (`text_ord`, `text_ord_ore`) and on `text_search` always routes `=` / `<>` through `hm` (exact HMAC), never the ORE term — ORE is not exact-equality for text** (integer ordered domains keep exact ORE equality, which is lossless for them). ([#260](https://github.com/cipherstash/encrypt-query-language/pull/260)) - **Self-contained `eql_v3` schema + standalone `release/cipherstash-encrypt-v3.sql` installer.** The `eql_v3` encrypted-domain surface no longer depends on `eql_v2` at runtime: it now owns its own copies of the searchable-encrypted-metadata (SEM) index-term types — `eql_v3.hmac_256` and `eql_v3.ore_block_256` (with its btree operator class) — so the `eql_v3.eq_term` / `eql_v3.ord_term` extractors return `eql_v3` types and no `eql_v2.` appears anywhere in the v3 SQL. The whole v3 surface relocated under a single `src/v3/` tree (`src/v3/sem/` for the hand-written SEM types, `src/v3/scalars/` for the generated domain families). A new build variant ships the `eql_v3` schema on its own as `release/cipherstash-encrypt-v3.sql`, installable into a database with no `eql_v2` present; a CI gate greps that artifact and its dependency closure to keep it `eql_v2`-free. Why: a clean foundation for the per-scalar encrypted-domain model to stand alone, ahead of it replacing the `eql_v2_encrypted` composite column type. This is additive — a new schema and a new artifact — and leaves `eql_v2` byte-for-byte unchanged. ([#255](https://github.com/cipherstash/encrypt-query-language/pull/255)) @@ -37,6 +38,10 @@ Each entry that ships in a published release links to the PR that introduced it. - **Scalar encrypted-domain types are now defined in a Rust catalog, not TOML manifests; the Python codegen toolchain is removed.** Adding a scalar encrypted-domain type (`int4`, `int8`, …) is now one row in `eql-scalars::CATALOG` (`crates/eql-scalars/src/lib.rs`) instead of authoring `tasks/codegen/types/.toml`. `mise run build` regenerates the gitignored SQL surface via `cargo run -p eql-codegen` (Rust, std-only) rather than the Python generator. The catalog row's `Fixture` list is the single source of truth for that type's plaintext fixtures: the SQLx test matrix reads it directly as a compile-time-materialised const (`eql_scalars::INT4_VALUES` / `INT2_VALUES`, `ScalarType::FIXTURE_VALUES`), so there is no longer a generated, committed `tests/sqlx/src/fixtures/_values.rs` — a Rust source of truth no longer round-trips through generated Rust. The shipped SQL is unchanged — `release/*.sql` is byte-identical across the cutover — so there is no change for callers installing EQL; this only affects contributors who extend the scalar domain families. The `python` mise tool, the `pytest`-based `test:codegen` (now `cargo test -p eql-scalars -p eql-codegen`), the per-type `mise run codegen:domain` tasks, and the per-type `tests/sqlx/snapshots/_matrix_tests.txt` baselines (collapsed into one catalog-reconciled `tests/sqlx/snapshots/matrix_tests.txt`) are gone. Why: a single compiler-validated source of truth shared by the generator and the SQLx test harness, and one fewer toolchain in the build/test path — building and testing EQL no longer needs Python (Python remains only for the separate docs-markdown tooling). ([#252](https://github.com/cipherstash/encrypt-query-language/pull/252)) +### Fixed + +- **The `eql_v3` ORE block comparator now orders ciphertexts of any block count, not just 8.** `eql_v3.compare_ore_block_256_term` derives the block count `N` from the term length (`octet_length = 49·N + 16`) instead of hardcoding 8, so encrypted types whose native ORE width exceeds 8 blocks — `numeric` (14) and `timestamptz` (12) — order, range-query, `ORDER BY`, and `MIN`/`MAX` correctly instead of silently mis-ordering. Malformed terms (length not `49·N + 16` for `N ≥ 1`) now raise instead of returning a bogus comparison. The self-contained `eql_v3` SEM type was renamed `eql_v3.ore_block_u64_8_256 → eql_v3.ore_block_256` to reflect that it is width-agnostic (the `eql_v2` type is unchanged). No effect on existing 8-block types (a no-op for `N = 8`). ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241)) + ## [2.3.1] — 2026-05-21 ### Fixed diff --git a/docs/reference/adding-a-scalar-encrypted-domain-type.md b/docs/reference/adding-a-scalar-encrypted-domain-type.md index 76d987dc..9c6c107a 100644 --- a/docs/reference/adding-a-scalar-encrypted-domain-type.md +++ b/docs/reference/adding-a-scalar-encrypted-domain-type.md @@ -326,6 +326,42 @@ first added) also needs, in `tests/sqlx/src/fixtures/eql_plaintext.rs`: `Plaintext::*` variant (`Plaintext::NaiveDate` / `Plaintext::Timestamp` / `Plaintext::Text`), plus the three mirrored `#[test]`s. +#### A fourth fixture shape: non-integer, non-chrono, non-text (`numeric` / `Decimal`) + +`numeric` (backed by `rust_decimal::Decimal`, 14-block ORE — the first scalar +whose ORE term is wider than 8 blocks) is ordered but is **neither** the integer +materialiser, **nor** chrono (`temporal`), **nor** `text` (it owns a `Decimal`, +not a `String`, and has no `Match` index). It therefore introduces a **fourth +fixture discriminator**, which means touching the proc-macro routing, not just +the type list. Beyond the §3.1 `eql_plaintext.rs` wiring above (`Cast::DECIMAL`, +`PlaintextSqlType::NUMERIC`, the `cast_for_kind` / `plaintext_sql_type_for_kind` +arms, `Sealed for Decimal`, `EqlPlaintext for Decimal` → `Plaintext::Decimal`), +it also needs: + +- an **`is_numeric_token`** arm in `crates/eql-tests-macros/src/lib.rs`'s + fixture-module router — without it `scalar_types!(fixture_modules)` panics at + compile time on the unrecognised kind (the router handled only `temporal` / + `text` before); +- a **`numeric` arm** in the `scalar_fixture!` macro + (`tests/sqlx/src/fixtures/scalar_fixture.rs`) — the temporal arm's twin + (`[Unique, Ore]`, pivot-presence asserts via `OrderedScalar`), but no `Match` + and no chrono; +- a hand-written **`numeric_values()`** accessor plus `impl ScalarType` / + `OrderedScalar for Decimal` in `tests/sqlx/src/scalar_domains.rs` — parsing the + catalog's `Fixture::Numeric` strings into a `LazyLock>` (the + catalog stays zero-dep; the parse lives in the harness). `Decimal: Ord` supplies + the expected sort order — `ore-rs` guarantees the ciphertext order agrees, and + equivalent scales (`1` ≡ `1.0`) collide like `Decimal`'s own `Ord`. Add a + **`fixtures_are_distinct_by_value`** guard (parse → `HashSet`): the zero-dep + catalog only dedupes by literal string, so `"1"` / `"1.0"` would slip past it + but collide in both the ORE ciphertext and the fixture table; +- the **`rust_decimal` dependency** + the sqlx **`rust_decimal` feature** in + `tests/sqlx/Cargo.toml` (in `[dependencies]`, not `[dev-dependencies]` — the + `Decimal` impls live in the crate's library code). + +See `docs/plans/2026-06-11-ore-block-comparator-n-blocks-design.md` for the full +worked example (and the N-block ORE comparator change the wide term relies on). + ### New-capability domains (e.g. `_match` / `Bloom`) A domain carrying a capability the matrix does not model — `text`'s `_match` From 2111c2afa6d02a2497e4a801f2db2a37955d1a14 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 15 Jun 2026 11:06:19 +1000 Subject: [PATCH 05/12] refactor(v3): finish ore_block_256 rename + sync codegen goldens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the eql_v3.ore_block_u64_8_256 -> ore_block_256 rename across the docs (CLAUDE.md, the scalar guide, eql-functions, sql-support; eql_v2 names left unchanged) and the v3 SEM file header — the @file/subset comment had over-applied the rename to the v2-origin path (src/ore_block_u64_8_256). Regenerate the codegen reference goldens for every catalog type after rebasing onto eql_v3: add the numeric reference dir and expand timestamptz to its now-ordered shape so the parity gate passes. Also address review feedback: - ScalarKind::rust_type returns the now-real numeric type (rust_decimal::Decimal); only jsonb remains surfaceless. De-stale its doc and the 'timestamptz is equality-only' test comment; add numeric_maps_to_decimal. - Correct the guide's stale 'timestamptz is equality-only' prose and a dangling link to the removed design plan doc. - Add comparator_rejects_mismatched_block_widths (8-block vs 14-block terms must raise via the different-lengths guard). - Add the PR link (#276) to the numeric and N-block changelog entries. --- CHANGELOG.md | 4 +- crates/eql-scalars/src/kind.rs | 15 +- crates/eql-scalars/src/tests.rs | 14 +- .../adding-a-scalar-encrypted-domain-type.md | 22 +- src/v3/sem/ore_block_256/functions.sql | 2 +- .../numeric/numeric_eq_functions.sql | 407 ++++++++++++++++++ .../numeric/numeric_eq_operators.sql | 234 ++++++++++ .../reference/numeric/numeric_functions.sql | 404 +++++++++++++++++ .../reference/numeric/numeric_operators.sql | 228 ++++++++++ .../numeric/numeric_ord_aggregates.sql | 63 +++ .../numeric/numeric_ord_functions.sql | 396 +++++++++++++++++ .../numeric/numeric_ord_operators.sql | 246 +++++++++++ .../numeric/numeric_ord_ore_aggregates.sql | 63 +++ .../numeric/numeric_ord_ore_functions.sql | 396 +++++++++++++++++ .../numeric/numeric_ord_ore_operators.sql | 246 +++++++++++ .../reference/numeric/numeric_types.sql | 73 ++++ .../timestamptz_ord_aggregates.sql | 63 +++ .../timestamptz/timestamptz_ord_functions.sql | 396 +++++++++++++++++ .../timestamptz/timestamptz_ord_operators.sql | 246 +++++++++++ .../timestamptz_ord_ore_aggregates.sql | 63 +++ .../timestamptz_ord_ore_functions.sql | 396 +++++++++++++++++ .../timestamptz_ord_ore_operators.sql | 246 +++++++++++ .../timestamptz/timestamptz_types.sql | 32 ++ .../sqlx/tests/ore_block_comparator_tests.rs | 22 + 24 files changed, 4255 insertions(+), 22 deletions(-) create mode 100644 tests/codegen/reference/numeric/numeric_eq_functions.sql create mode 100644 tests/codegen/reference/numeric/numeric_eq_operators.sql create mode 100644 tests/codegen/reference/numeric/numeric_functions.sql create mode 100644 tests/codegen/reference/numeric/numeric_operators.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_aggregates.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_functions.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_operators.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_ore_aggregates.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_ore_functions.sql create mode 100644 tests/codegen/reference/numeric/numeric_ord_ore_operators.sql create mode 100644 tests/codegen/reference/numeric/numeric_types.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_aggregates.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_functions.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_operators.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_ore_aggregates.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_ore_functions.sql create mode 100644 tests/codegen/reference/timestamptz/timestamptz_ord_ore_operators.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e6997a..017b05d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Each entry that ships in a published release links to the PR that introduced it. - **`eql_v3.int8` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int8` columns — `eql_v3.int8` (storage-only), `eql_v3.int8_eq` (`=` / `<>` via HMAC), and `eql_v3.int8_ord` / `eql_v3.int8_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int8` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `bigint` column, extending the scalar generator across the full 64-bit integer width. ([#253](https://github.com/cipherstash/encrypt-query-language/pull/253)) - **`eql_v3.date` encrypted-domain type family.** Four jsonb-backed domains for encrypted `date` columns — `eql_v3.date` (storage-only), `eql_v3.date_eq` (`=` / `<>` via HMAC), and `eql_v3.date_ord` / `eql_v3.date_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `date` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Plaintexts encrypt under the `date` cast and compare via the same ORE block terms as the integer scalars (ORE is plaintext-agnostic — dates order like integers). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: the first **non-integer ordered** scalar encrypted-domain type — a type-safe, per-capability encrypted `date` column — proving the generator and SQLx test matrix generalize beyond fixed-width integers. ([#256](https://github.com/cipherstash/encrypt-query-language/pull/256)) - **`eql_v3.timestamptz` encrypted-domain type family (ordered).** Four jsonb-backed domains for encrypted `timestamptz` columns — `eql_v3.timestamptz` (storage-only), `eql_v3.timestamptz_eq` (`=` / `<>` via HMAC), and `eql_v3.timestamptz_ord` / `eql_v3.timestamptz_ord_ore` (also `<` `<=` `>` `>=`, `MIN` / `MAX` via 12-block ORE) — generated from the `timestamptz` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.date` family. Values are **UTC-normalized** (cipherstash has no timezone-preserving type): plaintexts encrypt under the `timestamp` cast. Ordering works because the `eql_v3` ORE block comparator now derives its block count from the ciphertext width (see the comparator entry below) instead of assuming 8. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. ([#257](https://github.com/cipherstash/encrypt-query-language/pull/257)) -- **`eql_v3.numeric` encrypted-domain type family (ordered).** Four jsonb-backed domains for encrypted `numeric` / `decimal` columns — `eql_v3.numeric` (storage-only), `eql_v3.numeric_eq` (`=` / `<>` via HMAC), and `eql_v3.numeric_ord` / `eql_v3.numeric_ord_ore` (also `<` `<=` `>` `>=`, `MIN` / `MAX` via 14-block ORE) — generated from the `numeric` row in `eql-scalars::CATALOG`. cipherstash encrypts `Plaintext::Decimal` at native 14-block ORE width; ordering matches `rust_decimal::Decimal` ordering exactly (equivalent scales such as `1` and `1.0` collide, like Postgres `numeric`). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors. Why: a type-safe, ordered encrypted decimal column, the first scalar to exercise an ORE term wider than 8 blocks. ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241)) +- **`eql_v3.numeric` encrypted-domain type family (ordered).** Four jsonb-backed domains for encrypted `numeric` / `decimal` columns — `eql_v3.numeric` (storage-only), `eql_v3.numeric_eq` (`=` / `<>` via HMAC), and `eql_v3.numeric_ord` / `eql_v3.numeric_ord_ore` (also `<` `<=` `>` `>=`, `MIN` / `MAX` via 14-block ORE) — generated from the `numeric` row in `eql-scalars::CATALOG`. cipherstash encrypts `Plaintext::Decimal` at native 14-block ORE width; ordering matches `rust_decimal::Decimal` ordering exactly (equivalent scales such as `1` and `1.0` collide, like Postgres `numeric`). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors. Why: a type-safe, ordered encrypted decimal column, the first scalar to exercise an ORE term wider than 8 blocks. ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241), [#276](https://github.com/cipherstash/encrypt-query-language/pull/276)) - **Per-domain `MIN` / `MAX` aggregates for the encrypted-domain family.** `eql_v3.min(eql_v3._ord)` / `eql_v3.max(eql_v3._ord)` (and the `_ord_ore` twin) are generated for every ord-capable scalar variant, giving type-safe extrema on domain-typed columns — comparison routes through the variant's `<` / `>` operator (ORE block term, no decryption). The aggregates are declared `PARALLEL = SAFE` with a combine function (the state function itself — min/max are associative), so PostgreSQL can use partial/parallel aggregation on large `GROUP BY` workloads. Why: the new domain types previously had no equivalent of the composite-type aggregates. The existing `eql_v2.min(eql_v2_encrypted)` / `eql_v2.max(eql_v2_encrypted)` aggregates are **retained** and continue to work on `eql_v2_encrypted` columns; the per-domain aggregates are additive and coexist with them. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239)) - **`eql_v3.text` encrypted-domain family (`text`, `text_eq`, `text_match`, `text_ord`, `text_ord_ore`, `text_search`).** Adds equality (`=` / `<>` via HMAC), match (`@>` / `<@` via a new self-contained `eql_v3.bloom_filter` SEM index term), and ORE ordering (`<` `<=` `>` `>=`, `min` / `max`) for encrypted text, at parity with EQL v2 text — generated from the `text` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. `text` is the first scalar to add a new index `Term` (`Bloom`) and the first non-integer, unbounded ordered kind (lexicographic pivots, hand-written `impl ScalarType`). The combined **`text_search`** domain carries all three capabilities in one type — `=` / `<>` via HMAC, `<` `<=` `>` `>=` / `min` / `max` via ORE, and `@>` / `<@` via bloom filter. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` / `eql_v3.match_term` extractors, not an operator class on the domain. Why: brings searchable encrypted text to the namespaced, `eql_v2`-free `eql_v3` surface. Match is exposed as bloom-filter containment on the `text_match` / `text_search` domains — deliberately *not* SQL `LIKE` (no wildcard/anchoring; probabilistic ngram containment) — and never backs equality. **Equality on the ordered text domains (`text_ord`, `text_ord_ore`) and on `text_search` always routes `=` / `<>` through `hm` (exact HMAC), never the ORE term — ORE is not exact-equality for text** (integer ordered domains keep exact ORE equality, which is lossless for them). ([#260](https://github.com/cipherstash/encrypt-query-language/pull/260)) - **Self-contained `eql_v3` schema + standalone `release/cipherstash-encrypt-v3.sql` installer.** The `eql_v3` encrypted-domain surface no longer depends on `eql_v2` at runtime: it now owns its own copies of the searchable-encrypted-metadata (SEM) index-term types — `eql_v3.hmac_256` and `eql_v3.ore_block_256` (with its btree operator class) — so the `eql_v3.eq_term` / `eql_v3.ord_term` extractors return `eql_v3` types and no `eql_v2.` appears anywhere in the v3 SQL. The whole v3 surface relocated under a single `src/v3/` tree (`src/v3/sem/` for the hand-written SEM types, `src/v3/scalars/` for the generated domain families). A new build variant ships the `eql_v3` schema on its own as `release/cipherstash-encrypt-v3.sql`, installable into a database with no `eql_v2` present; a CI gate greps that artifact and its dependency closure to keep it `eql_v2`-free. Why: a clean foundation for the per-scalar encrypted-domain model to stand alone, ahead of it replacing the `eql_v2_encrypted` composite column type. This is additive — a new schema and a new artifact — and leaves `eql_v2` byte-for-byte unchanged. ([#255](https://github.com/cipherstash/encrypt-query-language/pull/255)) @@ -40,7 +40,7 @@ Each entry that ships in a published release links to the PR that introduced it. ### Fixed -- **The `eql_v3` ORE block comparator now orders ciphertexts of any block count, not just 8.** `eql_v3.compare_ore_block_256_term` derives the block count `N` from the term length (`octet_length = 49·N + 16`) instead of hardcoding 8, so encrypted types whose native ORE width exceeds 8 blocks — `numeric` (14) and `timestamptz` (12) — order, range-query, `ORDER BY`, and `MIN`/`MAX` correctly instead of silently mis-ordering. Malformed terms (length not `49·N + 16` for `N ≥ 1`) now raise instead of returning a bogus comparison. The self-contained `eql_v3` SEM type was renamed `eql_v3.ore_block_u64_8_256 → eql_v3.ore_block_256` to reflect that it is width-agnostic (the `eql_v2` type is unchanged). No effect on existing 8-block types (a no-op for `N = 8`). ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241)) +- **The `eql_v3` ORE block comparator now orders ciphertexts of any block count, not just 8.** `eql_v3.compare_ore_block_256_term` derives the block count `N` from the term length (`octet_length = 49·N + 16`) instead of hardcoding 8, so encrypted types whose native ORE width exceeds 8 blocks — `numeric` (14) and `timestamptz` (12) — order, range-query, `ORDER BY`, and `MIN`/`MAX` correctly instead of silently mis-ordering. Malformed terms (length not `49·N + 16` for `N ≥ 1`) now raise instead of returning a bogus comparison. The self-contained `eql_v3` SEM type was renamed `eql_v3.ore_block_u64_8_256 → eql_v3.ore_block_256` to reflect that it is width-agnostic (the `eql_v2` type is unchanged). No effect on existing 8-block types (a no-op for `N = 8`). ([#241](https://github.com/cipherstash/encrypt-query-language/issues/241), [#276](https://github.com/cipherstash/encrypt-query-language/pull/276)) ## [2.3.1] — 2026-05-21 diff --git a/crates/eql-scalars/src/kind.rs b/crates/eql-scalars/src/kind.rs index 69ab70f5..e91813da 100644 --- a/crates/eql-scalars/src/kind.rs +++ b/crates/eql-scalars/src/kind.rs @@ -97,11 +97,11 @@ impl ScalarKind { } /// A debug/identifier string for the kind: the canonical Rust plaintext type - /// name (`"i32"`, `"chrono::NaiveDate"`). `Numeric`/`Jsonb` have **no - /// generated SQL surface** and no catalog row, so calling this on them is a - /// programming error and panics loudly rather than returning a plausible SQL - /// token a premature caller might feed into codegen. Only call site today is - /// `crates/eql-scalars/src/tests.rs`. + /// name (`"i32"`, `"chrono::NaiveDate"`, `"rust_decimal::Decimal"`). `Jsonb` + /// has **no generated SQL surface** and no catalog row, so calling this on it + /// is a programming error and panics loudly rather than returning a plausible + /// SQL token a premature caller might feed into codegen. Only call site today + /// is `crates/eql-scalars/src/tests.rs`. pub const fn rust_type(self) -> &'static str { match self { ScalarKind::I16 => "i16", @@ -110,8 +110,9 @@ impl ScalarKind { ScalarKind::Text => "text", ScalarKind::Date => "chrono::NaiveDate", ScalarKind::Timestamptz => "chrono::DateTime", - ScalarKind::Numeric | ScalarKind::Jsonb => { - panic!("ScalarKind::rust_type: numeric/jsonb have no generated surface yet") + ScalarKind::Numeric => "rust_decimal::Decimal", + ScalarKind::Jsonb => { + panic!("ScalarKind::rust_type: jsonb has no generated surface yet") } } } diff --git a/crates/eql-scalars/src/tests.rs b/crates/eql-scalars/src/tests.rs index 1be3a65f..be5e9880 100644 --- a/crates/eql-scalars/src/tests.rs +++ b/crates/eql-scalars/src/tests.rs @@ -125,14 +125,24 @@ mod rust_tests { #[test] fn timestamptz_maps_to_datetime() { - // Temporal, non-integer, equality-only kind: it carries a rust type but - // no i128 range, so it is not `is_int()` and `as_bounded_int()` returns + // Temporal, non-integer, ordered kind: it carries a rust type but no + // i128 range, so it is not `is_int()` and `as_bounded_int()` returns // `None` — the bounded accessors are not reachable for it. assert_eq!(ScalarKind::Timestamptz.rust_type(), "chrono::DateTime"); assert!(!ScalarKind::Timestamptz.is_int()); assert_eq!(ScalarKind::Timestamptz.as_bounded_int(), None); } + #[test] + fn numeric_maps_to_decimal() { + // Ordered, non-integer, non-chrono kind (14-block ORE): carries a rust + // type but no i128 range, so it is not `is_int()` and `as_bounded_int()` + // returns `None`. Pins the now-real `rust_type` arm (it no longer panics). + assert_eq!(ScalarKind::Numeric.rust_type(), "rust_decimal::Decimal"); + assert!(!ScalarKind::Numeric.is_int()); + assert_eq!(ScalarKind::Numeric.as_bounded_int(), None); + } + /// The structural guarantee that replaces the old runtime panics: a /// `Min`/`Max`/`Zero` pivot sentinel may only appear in a `CATALOG` row whose /// kind is an integer kind. `numeric_value` would resolve to `None` for a diff --git a/docs/reference/adding-a-scalar-encrypted-domain-type.md b/docs/reference/adding-a-scalar-encrypted-domain-type.md index 9c6c107a..8c0bdf6e 100644 --- a/docs/reference/adding-a-scalar-encrypted-domain-type.md +++ b/docs/reference/adding-a-scalar-encrypted-domain-type.md @@ -204,14 +204,16 @@ there is no `_VALUES` const; the SQLx harness parses the catalog strings into A **temporal** scalar (`date` is the *ordered* temporal reference) is *ordered but non-integer*, so it diverges from the integer path in three places — all in the catalog/harness, never the SQL codegen (domains stay jsonb-backed and -token-driven). **`timestamptz` is the exception: it is equality-only, not -ordered** — its catalog row uses `EQ_ONLY_DOMAINS` (storage + `_eq`, no -`_ord`/`_ord_ore`), the eq-only shape of §3, because cipherstash encrypts -`Plaintext::Timestamp` at native 12-block ORE width while EQL's only comparator -(`eql_v2.compare_ore_block_u64_8_256_term`) is hardcoded to 8 blocks, so an -ordered `timestamptz` domain would silently mis-order (see the catalog comment on -the `TIMESTAMPTZ` spec). Its value-wiring is still the temporal path below; only -its domain set differs. The three divergences (for the ordered `date`): +token-driven). **`timestamptz` follows the same *ordered* temporal path as +`date`** — its catalog row carries the full ordered domain set (storage + `_eq` + +`_ord`/`_ord_ore`). cipherstash encrypts `Plaintext::Timestamp` at native +12-block ORE width, and the `eql_v3` comparator +(`eql_v3.compare_ore_block_256_term`) now derives its block count `N` from the +term length instead of assuming 8, so the 12-block ciphertexts order correctly +(see the N-block ORE comparator entry in the `CHANGELOG.md` and the catalog +comment on the `TIMESTAMPTZ` spec). Its value-wiring is the temporal path below; +the only practical difference from `date` is that values are UTC-normalized. The +three divergences (for the ordered `date`): - **String-backed fixtures.** `eql-scalars` stays zero-dependency, so the catalog stores ISO strings (`Fixture::Date("1970-01-01")`), not `chrono` @@ -359,8 +361,8 @@ it also needs: `tests/sqlx/Cargo.toml` (in `[dependencies]`, not `[dev-dependencies]` — the `Decimal` impls live in the crate's library code). -See `docs/plans/2026-06-11-ore-block-comparator-n-blocks-design.md` for the full -worked example (and the N-block ORE comparator change the wide term relies on). +See the N-block ORE comparator entry in the `CHANGELOG.md` for the comparator +change the wide `numeric` / `timestamptz` terms rely on. ### New-capability domains (e.g. `_match` / `Bloom`) diff --git a/src/v3/sem/ore_block_256/functions.sql b/src/v3/sem/ore_block_256/functions.sql index 83bd42e1..3eca4f1d 100644 --- a/src/v3/sem/ore_block_256/functions.sql +++ b/src/v3/sem/ore_block_256/functions.sql @@ -6,7 +6,7 @@ --! @file v3/sem/ore_block_256/functions.sql --! @brief ORE block construction, extraction, and comparison (eql_v3 SEM). --! ---! jsonb-only subset of src/ore_block_256/functions.sql. The +--! jsonb-only subset of src/ore_block_u64_8_256/functions.sql. The --! encrypted-column overloads are omitted; the helper jsonb_array_to_bytea_array --! and pgcrypto encrypt() are reached via the forked src/v3/common.sql and --! src/v3/crypto.sql so the whole closure stays under src/v3. (Doc comments diff --git a/tests/codegen/reference/numeric/numeric_eq_functions.sql b/tests/codegen/reference/numeric/numeric_eq_functions.sql new file mode 100644 index 00000000..2b20d70b --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_eq_functions.sql @@ -0,0 +1,407 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/functions.sql +-- REQUIRE: src/v3/sem/hmac_256/functions.sql + +--! @file encrypted_domain/numeric/numeric_eq_functions.sql +--! @brief Functions for eql_v3.numeric_eq. + +--! @brief Index extractor for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @return eql_v3.hmac_256 +CREATE FUNCTION eql_v3.eq_term(a eql_v3.numeric_eq) +RETURNS eql_v3.hmac_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.hmac_256(a::jsonb) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a) = eql_v3.eq_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a) = eql_v3.eq_term(b::eql_v3.numeric_eq) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a::eql_v3.numeric_eq) = eql_v3.eq_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a) <> eql_v3.eq_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a) <> eql_v3.eq_term(b::eql_v3.numeric_eq) $$; + +--! @brief Operator wrapper for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.eq_term(a::eql_v3.numeric_eq) <> eql_v3.eq_term(b) $$; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.numeric_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param selector text +--! @return eql_v3.numeric_eq +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_eq, selector text) +RETURNS eql_v3.numeric_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param selector integer +--! @return eql_v3.numeric_eq +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_eq, selector integer) +RETURNS eql_v3.numeric_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param selector eql_v3.numeric_eq +--! @return eql_v3.numeric_eq +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.numeric_eq) +RETURNS eql_v3.numeric_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_eq, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_eq, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param selector eql_v3.numeric_eq +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.numeric_eq) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.numeric_eq, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.numeric_eq, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.numeric_eq, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.numeric_eq, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.numeric_eq, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.numeric_eq, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.numeric_eq, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_eq, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_eq, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_eq, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.numeric_eq, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b eql_v3.numeric_eq +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_eq, b eql_v3.numeric_eq) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a eql_v3.numeric_eq +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_eq, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_eq. +--! @param a jsonb +--! @param b eql_v3.numeric_eq +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.numeric_eq) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_eq'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/numeric/numeric_eq_operators.sql b/tests/codegen/reference/numeric/numeric_eq_operators.sql new file mode 100644 index 00000000..6ec23570 --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_eq_operators.sql @@ -0,0 +1,234 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_eq_functions.sql + +--! @file encrypted_domain/numeric/numeric_eq_operators.sql +--! @brief Operators for eql_v3.numeric_eq. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = eql_v3.numeric_eq +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_eq, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_eq +); diff --git a/tests/codegen/reference/numeric/numeric_functions.sql b/tests/codegen/reference/numeric/numeric_functions.sql new file mode 100644 index 00000000..9c6146ab --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_functions.sql @@ -0,0 +1,404 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/functions.sql + +--! @file encrypted_domain/numeric/numeric_functions.sql +--! @brief Functions for eql_v3.numeric. + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '>=', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.numeric) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param selector text +--! @return eql_v3.numeric +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric, selector text) +RETURNS eql_v3.numeric IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param selector integer +--! @return eql_v3.numeric +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric, selector integer) +RETURNS eql_v3.numeric IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param selector eql_v3.numeric +--! @return eql_v3.numeric +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.numeric) +RETURNS eql_v3.numeric IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param selector eql_v3.numeric +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.numeric) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.numeric, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.numeric, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.numeric, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.numeric, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.numeric, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.numeric, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.numeric, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.numeric, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b eql_v3.numeric +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric, b eql_v3.numeric) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a eql_v3.numeric +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric. +--! @param a jsonb +--! @param b eql_v3.numeric +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.numeric) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/numeric/numeric_operators.sql b/tests/codegen/reference/numeric/numeric_operators.sql new file mode 100644 index 00000000..b62f419e --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_operators.sql @@ -0,0 +1,228 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_functions.sql + +--! @file encrypted_domain/numeric/numeric_operators.sql +--! @brief Operators for eql_v3.numeric. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.numeric, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.numeric, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.numeric, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.numeric, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric, RIGHTARG = eql_v3.numeric +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric +); diff --git a/tests/codegen/reference/numeric/numeric_ord_aggregates.sql b/tests/codegen/reference/numeric/numeric_ord_aggregates.sql new file mode 100644 index 00000000..03bf02c1 --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_aggregates.sql @@ -0,0 +1,63 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_functions.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_operators.sql + +--! @file encrypted_domain/numeric/numeric_ord_aggregates.sql +--! @brief Aggregates for eql_v3.numeric_ord. + +--! @brief State function for min on eql_v3.numeric_ord. +--! @param state eql_v3.numeric_ord +--! @param value eql_v3.numeric_ord +--! @return eql_v3.numeric_ord +CREATE FUNCTION eql_v3.min_sfunc(state eql_v3.numeric_ord, value eql_v3.numeric_ord) +RETURNS eql_v3.numeric_ord +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value < state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief min aggregate for eql_v3.numeric_ord. +--! @param input eql_v3.numeric_ord +--! @return eql_v3.numeric_ord +CREATE AGGREGATE eql_v3.min(eql_v3.numeric_ord) ( + sfunc = eql_v3.min_sfunc, + stype = eql_v3.numeric_ord, + combinefunc = eql_v3.min_sfunc, + parallel = safe +); + +--! @brief State function for max on eql_v3.numeric_ord. +--! @param state eql_v3.numeric_ord +--! @param value eql_v3.numeric_ord +--! @return eql_v3.numeric_ord +CREATE FUNCTION eql_v3.max_sfunc(state eql_v3.numeric_ord, value eql_v3.numeric_ord) +RETURNS eql_v3.numeric_ord +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value > state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief max aggregate for eql_v3.numeric_ord. +--! @param input eql_v3.numeric_ord +--! @return eql_v3.numeric_ord +CREATE AGGREGATE eql_v3.max(eql_v3.numeric_ord) ( + sfunc = eql_v3.max_sfunc, + stype = eql_v3.numeric_ord, + combinefunc = eql_v3.max_sfunc, + parallel = safe +); diff --git a/tests/codegen/reference/numeric/numeric_ord_functions.sql b/tests/codegen/reference/numeric/numeric_ord_functions.sql new file mode 100644 index 00000000..5467f834 --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_functions.sql @@ -0,0 +1,396 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql + +--! @file encrypted_domain/numeric/numeric_ord_functions.sql +--! @brief Functions for eql_v3.numeric_ord. + +--! @brief Index extractor for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @return eql_v3.ore_block_256 +CREATE FUNCTION eql_v3.ord_term(a eql_v3.numeric_ord) +RETURNS eql_v3.ore_block_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b::eql_v3.numeric_ord) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord) >= eql_v3.ord_term(b) $$; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.numeric_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param selector text +--! @return eql_v3.numeric_ord +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_ord, selector text) +RETURNS eql_v3.numeric_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param selector integer +--! @return eql_v3.numeric_ord +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_ord, selector integer) +RETURNS eql_v3.numeric_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a jsonb +--! @param selector eql_v3.numeric_ord +--! @return eql_v3.numeric_ord +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.numeric_ord) +RETURNS eql_v3.numeric_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_ord, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_ord, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a jsonb +--! @param selector eql_v3.numeric_ord +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.numeric_ord) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.numeric_ord, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.numeric_ord, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.numeric_ord, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.numeric_ord, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.numeric_ord, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.numeric_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.numeric_ord, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.numeric_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b eql_v3.numeric_ord +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_ord, b eql_v3.numeric_ord) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a eql_v3.numeric_ord +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_ord, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord. +--! @param a jsonb +--! @param b eql_v3.numeric_ord +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.numeric_ord) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/numeric/numeric_ord_operators.sql b/tests/codegen/reference/numeric/numeric_ord_operators.sql new file mode 100644 index 00000000..847bf6a1 --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_operators.sql @@ -0,0 +1,246 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_functions.sql + +--! @file encrypted_domain/numeric/numeric_ord_operators.sql +--! @brief Operators for eql_v3.numeric_ord. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = eql_v3.numeric_ord +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord +); diff --git a/tests/codegen/reference/numeric/numeric_ord_ore_aggregates.sql b/tests/codegen/reference/numeric/numeric_ord_ore_aggregates.sql new file mode 100644 index 00000000..e78a6ffb --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_ore_aggregates.sql @@ -0,0 +1,63 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_ore_functions.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_ore_operators.sql + +--! @file encrypted_domain/numeric/numeric_ord_ore_aggregates.sql +--! @brief Aggregates for eql_v3.numeric_ord_ore. + +--! @brief State function for min on eql_v3.numeric_ord_ore. +--! @param state eql_v3.numeric_ord_ore +--! @param value eql_v3.numeric_ord_ore +--! @return eql_v3.numeric_ord_ore +CREATE FUNCTION eql_v3.min_sfunc(state eql_v3.numeric_ord_ore, value eql_v3.numeric_ord_ore) +RETURNS eql_v3.numeric_ord_ore +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value < state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief min aggregate for eql_v3.numeric_ord_ore. +--! @param input eql_v3.numeric_ord_ore +--! @return eql_v3.numeric_ord_ore +CREATE AGGREGATE eql_v3.min(eql_v3.numeric_ord_ore) ( + sfunc = eql_v3.min_sfunc, + stype = eql_v3.numeric_ord_ore, + combinefunc = eql_v3.min_sfunc, + parallel = safe +); + +--! @brief State function for max on eql_v3.numeric_ord_ore. +--! @param state eql_v3.numeric_ord_ore +--! @param value eql_v3.numeric_ord_ore +--! @return eql_v3.numeric_ord_ore +CREATE FUNCTION eql_v3.max_sfunc(state eql_v3.numeric_ord_ore, value eql_v3.numeric_ord_ore) +RETURNS eql_v3.numeric_ord_ore +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value > state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief max aggregate for eql_v3.numeric_ord_ore. +--! @param input eql_v3.numeric_ord_ore +--! @return eql_v3.numeric_ord_ore +CREATE AGGREGATE eql_v3.max(eql_v3.numeric_ord_ore) ( + sfunc = eql_v3.max_sfunc, + stype = eql_v3.numeric_ord_ore, + combinefunc = eql_v3.max_sfunc, + parallel = safe +); diff --git a/tests/codegen/reference/numeric/numeric_ord_ore_functions.sql b/tests/codegen/reference/numeric/numeric_ord_ore_functions.sql new file mode 100644 index 00000000..a62ab25a --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_ore_functions.sql @@ -0,0 +1,396 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql + +--! @file encrypted_domain/numeric/numeric_ord_ore_functions.sql +--! @brief Functions for eql_v3.numeric_ord_ore. + +--! @brief Index extractor for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @return eql_v3.ore_block_256 +CREATE FUNCTION eql_v3.ord_term(a eql_v3.numeric_ord_ore) +RETURNS eql_v3.ore_block_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b::eql_v3.numeric_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.numeric_ord_ore) >= eql_v3.ord_term(b) $$; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param selector text +--! @return eql_v3.numeric_ord_ore +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_ord_ore, selector text) +RETURNS eql_v3.numeric_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param selector integer +--! @return eql_v3.numeric_ord_ore +CREATE FUNCTION eql_v3."->"(a eql_v3.numeric_ord_ore, selector integer) +RETURNS eql_v3.numeric_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param selector eql_v3.numeric_ord_ore +--! @return eql_v3.numeric_ord_ore +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.numeric_ord_ore) +RETURNS eql_v3.numeric_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_ord_ore, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.numeric_ord_ore, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param selector eql_v3.numeric_ord_ore +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.numeric_ord_ore) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.numeric_ord_ore, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.numeric_ord_ore, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.numeric_ord_ore, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord_ore, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord_ore, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.numeric_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b eql_v3.numeric_ord_ore +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_ord_ore, b eql_v3.numeric_ord_ore) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a eql_v3.numeric_ord_ore +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.numeric_ord_ore, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.numeric_ord_ore. +--! @param a jsonb +--! @param b eql_v3.numeric_ord_ore +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.numeric_ord_ore) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.numeric_ord_ore'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/numeric/numeric_ord_ore_operators.sql b/tests/codegen/reference/numeric/numeric_ord_ore_operators.sql new file mode 100644 index 00000000..e449a61a --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_ord_ore_operators.sql @@ -0,0 +1,246 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_types.sql +-- REQUIRE: src/v3/scalars/numeric/numeric_ord_ore_functions.sql + +--! @file encrypted_domain/numeric/numeric_ord_ore_operators.sql +--! @brief Operators for eql_v3.numeric_ord_ore. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = eql_v3.numeric_ord_ore +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.numeric_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.numeric_ord_ore +); diff --git a/tests/codegen/reference/numeric/numeric_types.sql b/tests/codegen/reference/numeric/numeric_types.sql new file mode 100644 index 00000000..6c9b85d3 --- /dev/null +++ b/tests/codegen/reference/numeric/numeric_types.sql @@ -0,0 +1,73 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql + +--! @file v3/scalars/numeric/numeric_types.sql +--! @brief Encrypted-domain types for numeric. + +DO $$ +BEGIN + --! @brief Encrypted domain eql_v3.numeric. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'numeric' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.numeric AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE->>'v' = '2' + ); + END IF; + + --! @brief Encrypted domain eql_v3.numeric_eq. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'numeric_eq' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.numeric_eq AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'hm' + AND VALUE->>'v' = '2' + ); + END IF; + + --! @brief Encrypted domain eql_v3.numeric_ord_ore. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'numeric_ord_ore' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.numeric_ord_ore AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'ob' + AND VALUE->>'v' = '2' + ); + END IF; + + --! @brief Encrypted domain eql_v3.numeric_ord. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'numeric_ord' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.numeric_ord AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'ob' + AND VALUE->>'v' = '2' + ); + END IF; +END +$$; diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_aggregates.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_aggregates.sql new file mode 100644 index 00000000..637fb338 --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_aggregates.sql @@ -0,0 +1,63 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_functions.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_operators.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_aggregates.sql +--! @brief Aggregates for eql_v3.timestamptz_ord. + +--! @brief State function for min on eql_v3.timestamptz_ord. +--! @param state eql_v3.timestamptz_ord +--! @param value eql_v3.timestamptz_ord +--! @return eql_v3.timestamptz_ord +CREATE FUNCTION eql_v3.min_sfunc(state eql_v3.timestamptz_ord, value eql_v3.timestamptz_ord) +RETURNS eql_v3.timestamptz_ord +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value < state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief min aggregate for eql_v3.timestamptz_ord. +--! @param input eql_v3.timestamptz_ord +--! @return eql_v3.timestamptz_ord +CREATE AGGREGATE eql_v3.min(eql_v3.timestamptz_ord) ( + sfunc = eql_v3.min_sfunc, + stype = eql_v3.timestamptz_ord, + combinefunc = eql_v3.min_sfunc, + parallel = safe +); + +--! @brief State function for max on eql_v3.timestamptz_ord. +--! @param state eql_v3.timestamptz_ord +--! @param value eql_v3.timestamptz_ord +--! @return eql_v3.timestamptz_ord +CREATE FUNCTION eql_v3.max_sfunc(state eql_v3.timestamptz_ord, value eql_v3.timestamptz_ord) +RETURNS eql_v3.timestamptz_ord +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value > state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief max aggregate for eql_v3.timestamptz_ord. +--! @param input eql_v3.timestamptz_ord +--! @return eql_v3.timestamptz_ord +CREATE AGGREGATE eql_v3.max(eql_v3.timestamptz_ord) ( + sfunc = eql_v3.max_sfunc, + stype = eql_v3.timestamptz_ord, + combinefunc = eql_v3.max_sfunc, + parallel = safe +); diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_functions.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_functions.sql new file mode 100644 index 00000000..518e8e8d --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_functions.sql @@ -0,0 +1,396 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_functions.sql +--! @brief Functions for eql_v3.timestamptz_ord. + +--! @brief Index extractor for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @return eql_v3.ore_block_256 +CREATE FUNCTION eql_v3.ord_term(a eql_v3.timestamptz_ord) +RETURNS eql_v3.ore_block_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b::eql_v3.timestamptz_ord) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord) >= eql_v3.ord_term(b) $$; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.timestamptz_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.timestamptz_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param selector text +--! @return eql_v3.timestamptz_ord +CREATE FUNCTION eql_v3."->"(a eql_v3.timestamptz_ord, selector text) +RETURNS eql_v3.timestamptz_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param selector integer +--! @return eql_v3.timestamptz_ord +CREATE FUNCTION eql_v3."->"(a eql_v3.timestamptz_ord, selector integer) +RETURNS eql_v3.timestamptz_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param selector eql_v3.timestamptz_ord +--! @return eql_v3.timestamptz_ord +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.timestamptz_ord) +RETURNS eql_v3.timestamptz_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.timestamptz_ord, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.timestamptz_ord, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param selector eql_v3.timestamptz_ord +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.timestamptz_ord) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.timestamptz_ord, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.timestamptz_ord, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.timestamptz_ord, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.timestamptz_ord, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.timestamptz_ord, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.timestamptz_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.timestamptz_ord, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.timestamptz_ord, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b eql_v3.timestamptz_ord +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.timestamptz_ord, b eql_v3.timestamptz_ord) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a eql_v3.timestamptz_ord +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.timestamptz_ord, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.timestamptz_ord) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_operators.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_operators.sql new file mode 100644 index 00000000..3a05729b --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_operators.sql @@ -0,0 +1,246 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_functions.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_operators.sql +--! @brief Operators for eql_v3.timestamptz_ord. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = eql_v3.timestamptz_ord +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.timestamptz_ord, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord +); diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_ore_aggregates.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_aggregates.sql new file mode 100644 index 00000000..73bae985 --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_aggregates.sql @@ -0,0 +1,63 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_ore_functions.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_ore_operators.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_ore_aggregates.sql +--! @brief Aggregates for eql_v3.timestamptz_ord_ore. + +--! @brief State function for min on eql_v3.timestamptz_ord_ore. +--! @param state eql_v3.timestamptz_ord_ore +--! @param value eql_v3.timestamptz_ord_ore +--! @return eql_v3.timestamptz_ord_ore +CREATE FUNCTION eql_v3.min_sfunc(state eql_v3.timestamptz_ord_ore, value eql_v3.timestamptz_ord_ore) +RETURNS eql_v3.timestamptz_ord_ore +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value < state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief min aggregate for eql_v3.timestamptz_ord_ore. +--! @param input eql_v3.timestamptz_ord_ore +--! @return eql_v3.timestamptz_ord_ore +CREATE AGGREGATE eql_v3.min(eql_v3.timestamptz_ord_ore) ( + sfunc = eql_v3.min_sfunc, + stype = eql_v3.timestamptz_ord_ore, + combinefunc = eql_v3.min_sfunc, + parallel = safe +); + +--! @brief State function for max on eql_v3.timestamptz_ord_ore. +--! @param state eql_v3.timestamptz_ord_ore +--! @param value eql_v3.timestamptz_ord_ore +--! @return eql_v3.timestamptz_ord_ore +CREATE FUNCTION eql_v3.max_sfunc(state eql_v3.timestamptz_ord_ore, value eql_v3.timestamptz_ord_ore) +RETURNS eql_v3.timestamptz_ord_ore +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + IF value > state THEN + RETURN value; + END IF; + RETURN state; +END; +$$; + +--! @brief max aggregate for eql_v3.timestamptz_ord_ore. +--! @param input eql_v3.timestamptz_ord_ore +--! @return eql_v3.timestamptz_ord_ore +CREATE AGGREGATE eql_v3.max(eql_v3.timestamptz_ord_ore) ( + sfunc = eql_v3.max_sfunc, + stype = eql_v3.timestamptz_ord_ore, + combinefunc = eql_v3.max_sfunc, + parallel = safe +); diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_ore_functions.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_functions.sql new file mode 100644 index 00000000..c93ddef7 --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_functions.sql @@ -0,0 +1,396 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/functions.sql +-- REQUIRE: src/v3/sem/ore_block_256/operators.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_ore_functions.sql +--! @brief Functions for eql_v3.timestamptz_ord_ore. + +--! @brief Index extractor for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @return eql_v3.ore_block_256 +CREATE FUNCTION eql_v3.ord_term(a eql_v3.timestamptz_ord_ore) +RETURNS eql_v3.ore_block_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ore_block_256(a::jsonb) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.eq(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) = eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) = eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.neq(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <> eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.neq(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) <> eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lt(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lt(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) < eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.lte(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) <= eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.lte(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) <= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gt(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) > eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gt(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) > eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.gte(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) >= eql_v3.ord_term(b::eql_v3.timestamptz_ord_ore) $$; + +--! @brief Operator wrapper for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.gte(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a::eql_v3.timestamptz_ord_ore) >= eql_v3.ord_term(b) $$; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contains(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contains(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return boolean +CREATE FUNCTION eql_v3.contained_by(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<@', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param selector text +--! @return eql_v3.timestamptz_ord_ore +CREATE FUNCTION eql_v3."->"(a eql_v3.timestamptz_ord_ore, selector text) +RETURNS eql_v3.timestamptz_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param selector integer +--! @return eql_v3.timestamptz_ord_ore +CREATE FUNCTION eql_v3."->"(a eql_v3.timestamptz_ord_ore, selector integer) +RETURNS eql_v3.timestamptz_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param selector eql_v3.timestamptz_ord_ore +--! @return eql_v3.timestamptz_ord_ore +CREATE FUNCTION eql_v3."->"(a jsonb, selector eql_v3.timestamptz_ord_ore) +RETURNS eql_v3.timestamptz_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param selector text +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.timestamptz_ord_ore, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param selector integer +--! @return text +CREATE FUNCTION eql_v3."->>"(a eql_v3.timestamptz_ord_ore, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param selector eql_v3.timestamptz_ord_ore +--! @return text +CREATE FUNCTION eql_v3."->>"(a jsonb, selector eql_v3.timestamptz_ord_ore) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text +--! @return boolean +CREATE FUNCTION eql_v3."?"(a eql_v3.timestamptz_ord_ore, b text) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?|"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?|', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return boolean +CREATE FUNCTION eql_v3."?&"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '?&', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@?"(a eql_v3.timestamptz_ord_ore, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@?', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonpath +--! @return boolean +CREATE FUNCTION eql_v3."@@"(a eql_v3.timestamptz_ord_ore, b jsonpath) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '@@', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#>"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return text +CREATE FUNCTION eql_v3."#>>"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#>>', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord_ore, b text) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b integer +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord_ore, b integer) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."-"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '-', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b text[] +--! @return jsonb +CREATE FUNCTION eql_v3."#-"(a eql_v3.timestamptz_ord_ore, b text[]) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '#-', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b eql_v3.timestamptz_ord_ore +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.timestamptz_ord_ore, b eql_v3.timestamptz_ord_ore) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a eql_v3.timestamptz_ord_ore +--! @param b jsonb +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a eql_v3.timestamptz_ord_ore, b jsonb) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Unsupported operator blocker for eql_v3.timestamptz_ord_ore. +--! @param a jsonb +--! @param b eql_v3.timestamptz_ord_ore +--! @return jsonb +CREATE FUNCTION eql_v3."||"(a jsonb, b eql_v3.timestamptz_ord_ore) +RETURNS jsonb IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '||', 'eql_v3.timestamptz_ord_ore'; END; $$ +LANGUAGE plpgsql; diff --git a/tests/codegen/reference/timestamptz/timestamptz_ord_ore_operators.sql b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_operators.sql new file mode 100644 index 00000000..d5b7980c --- /dev/null +++ b/tests/codegen/reference/timestamptz/timestamptz_ord_ore_operators.sql @@ -0,0 +1,246 @@ +-- REFERENCE: hand-maintained parity baseline for crates/eql-codegen - see ../README.md +-- AUTOMATICALLY GENERATED FILE. +-- REQUIRE: src/v3/schema.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_types.sql +-- REQUIRE: src/v3/scalars/timestamptz/timestamptz_ord_ore_functions.sql + +--! @file encrypted_domain/timestamptz/timestamptz_ord_ore_operators.sql +--! @brief Operators for eql_v3.timestamptz_ord_ore. + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v3.neq, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v3.lt, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR <= ( + FUNCTION = eql_v3.lte, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR > ( + FUNCTION = eql_v3.gt, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR >= ( + FUNCTION = eql_v3.gte, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore, + COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR @> ( + FUNCTION = eql_v3.contains, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR <@ ( + FUNCTION = eql_v3.contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR -> ( + FUNCTION = eql_v3."->", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR ->> ( + FUNCTION = eql_v3."->>", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR ? ( + FUNCTION = eql_v3."?", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR ?| ( + FUNCTION = eql_v3."?|", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR ?& ( + FUNCTION = eql_v3."?&", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR @? ( + FUNCTION = eql_v3."@?", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonpath +); + +CREATE OPERATOR @@ ( + FUNCTION = eql_v3."@@", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonpath +); + +CREATE OPERATOR #> ( + FUNCTION = eql_v3."#>", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR #>> ( + FUNCTION = eql_v3."#>>", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = integer +); + +CREATE OPERATOR - ( + FUNCTION = eql_v3."-", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR #- ( + FUNCTION = eql_v3."#-", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = text[] +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = eql_v3.timestamptz_ord_ore +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = eql_v3.timestamptz_ord_ore, RIGHTARG = jsonb +); + +CREATE OPERATOR || ( + FUNCTION = eql_v3."||", + LEFTARG = jsonb, RIGHTARG = eql_v3.timestamptz_ord_ore +); diff --git a/tests/codegen/reference/timestamptz/timestamptz_types.sql b/tests/codegen/reference/timestamptz/timestamptz_types.sql index 61445e87..38930d16 100644 --- a/tests/codegen/reference/timestamptz/timestamptz_types.sql +++ b/tests/codegen/reference/timestamptz/timestamptz_types.sql @@ -37,5 +37,37 @@ BEGIN AND VALUE->>'v' = '2' ); END IF; + + --! @brief Encrypted domain eql_v3.timestamptz_ord_ore. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'timestamptz_ord_ore' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.timestamptz_ord_ore AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'ob' + AND VALUE->>'v' = '2' + ); + END IF; + + --! @brief Encrypted domain eql_v3.timestamptz_ord. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'timestamptz_ord' AND typnamespace = 'eql_v3'::regnamespace + ) THEN + CREATE DOMAIN eql_v3.timestamptz_ord AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'ob' + AND VALUE->>'v' = '2' + ); + END IF; END $$; diff --git a/tests/sqlx/tests/ore_block_comparator_tests.rs b/tests/sqlx/tests/ore_block_comparator_tests.rs index 6003d6d5..7fcc5eb3 100644 --- a/tests/sqlx/tests/ore_block_comparator_tests.rs +++ b/tests/sqlx/tests/ore_block_comparator_tests.rs @@ -51,6 +51,28 @@ async fn comparator_rejects_sixteen_byte_term(pool: PgPool) -> Result<()> { Ok(()) } +/// Cross-width footgun: now that N is derived per-term, comparing terms of two +/// different (individually valid) widths must raise via the equal-length guard, +/// not silently compare the shared prefix. Both lengths here are well-formed — +/// 408 = 49*8 + 16 (the int4 width, N=8) and 702 = 49*14 + 16 (the numeric +/// width, N=14) — so the only thing that fires is the different-lengths check, +/// ahead of the malformed-length guard. Creds-free (hand-built bytea). +#[sqlx::test] +async fn comparator_rejects_mismatched_block_widths(pool: PgPool) -> Result<()> { + let sql = "SELECT eql_v3.compare_ore_block_256_term( \ + ROW(repeat('a', 408)::bytea)::eql_v3.ore_block_256_term, \ + ROW(repeat('b', 702)::bytea)::eql_v3.ore_block_256_term)"; + let err = sqlx::query_scalar::<_, i32>(sql) + .fetch_one(&pool) + .await + .expect_err("an 8-block vs 14-block ORE term comparison must raise"); + assert!( + err.to_string().to_lowercase().contains("different lengths"), + "expected different-lengths error, got: {err}" + ); + Ok(()) +} + /// Width: a numeric ORE term must be 14 blocks => 49*14 + 16 = 702 bytes. #[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric")))] async fn numeric_term_is_14_blocks(pool: PgPool) -> Result<()> { From cf93a1a2ed1a23b7d8c7f59d5d18ad54c83e5167 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 15 Jun 2026 20:56:39 +1000 Subject: [PATCH 06/12] fix(v3): reject non-array ore 'ob' payloads at the extractor boundary has_ore_block_256 used "val ->> 'ob' IS NOT NULL", which stringifies a scalar/object 'ob' and reports it present. ore_block_256 then fed the malformed payload into jsonb_array_to_ore_block_256, which returns NULL instead of raising, silently degrading a structurally invalid ORE term into a NULL comparison/index term. Tighten the guard to require a JSON array (jsonb_typeof(val->'ob') = 'array'); a present-but-non-array 'ob' now RAISEs at the extractor boundary. '{}' (absent ob) and '{"ob": null}' (JSON null) remain absent (false). Adds T5 characterization cases for the scalar/object 'ob' presence checks and the extractor RAISE. Addresses CodeRabbit review thread on PR #276. --- src/v3/sem/ore_block_256/functions.sql | 21 +++++++++++----- .../sqlx/tests/encrypted_domain/family/sem.rs | 24 +++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/v3/sem/ore_block_256/functions.sql b/src/v3/sem/ore_block_256/functions.sql index 3eca4f1d..0cd323d8 100644 --- a/src/v3/sem/ore_block_256/functions.sql +++ b/src/v3/sem/ore_block_256/functions.sql @@ -22,9 +22,10 @@ --! This deliberately diverges from the v2 plpgsql equivalent (intentionally --! left unchanged): the `CASE WHEN jsonb_typeof(val) = 'array'` guard only --! evaluates the array path for an array, so a non-array JSON scalar returns ---! NULL here instead of raising. The sole caller passes `val->'ob'`, always an ---! array or JSON null, so the divergence is unreachable in practice; JSON null ---! and empty array still return NULL exactly as before. +--! NULL here instead of raising. The sole caller (`ore_block_256`) only reaches +--! this when `has_ore_block_256(val)` is true, which now requires `val->'ob'` +--! to be a JSON array, so the non-array branch is unreachable in practice; +--! empty array still returns NULL exactly as before (pinned by T7). CREATE FUNCTION eql_v3.jsonb_array_to_ore_block_256(val jsonb) RETURNS eql_v3.ore_block_256 IMMUTABLE @@ -67,16 +68,24 @@ AS $$ $$ LANGUAGE plpgsql; ---! @brief Check if JSONB payload contains ORE block index term +--! @brief Check if JSONB payload contains an ORE block index term --! @param val jsonb containing encrypted EQL payload ---! @return boolean True if 'ob' field is present and non-null +--! @return boolean True only if the 'ob' field is present and is a JSON array +--! @note A well-formed ORE index term is always a JSON array of block terms, so +--! this guard treats a present-but-non-array `ob` (a scalar or object) as +--! absent. That makes the extractor `ore_block_256(val)` RAISE on a +--! structurally invalid `ob` payload at the boundary instead of silently +--! degrading it to a NULL index term in `jsonb_array_to_ore_block_256`. The +--! previous `val ->> 'ob' IS NOT NULL` form stringified scalars/objects and so +--! reported them as present. `{}` (absent `ob`) and `{"ob": null}` (JSON null) +--! both remain `false`. CREATE FUNCTION eql_v3.has_ore_block_256(val jsonb) RETURNS boolean IMMUTABLE STRICT PARALLEL SAFE SET search_path = pg_catalog, extensions, public AS $$ BEGIN - RETURN val ->> 'ob' IS NOT NULL; + RETURN COALESCE(jsonb_typeof(val -> 'ob') = 'array', false); END; $$ LANGUAGE plpgsql; diff --git a/tests/sqlx/tests/encrypted_domain/family/sem.rs b/tests/sqlx/tests/encrypted_domain/family/sem.rs index fe2593b9..fd675673 100644 --- a/tests/sqlx/tests/encrypted_domain/family/sem.rs +++ b/tests/sqlx/tests/encrypted_domain/family/sem.rs @@ -178,7 +178,8 @@ async fn ore_terms_array_null_and_empty_base_cases(pool: PgPool) -> Result<()> { } /// T5 — SEM presence checks (`has_ore_block_256`, `has_hmac_256`), the -/// extractor's missing-`ob` RAISE, and its NULL-jsonb short-circuit. +/// extractor's missing-`ob` and non-array-`ob` RAISEs, and its NULL-jsonb +/// short-circuit. #[sqlx::test] async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<()> { let bool_cases = [ @@ -187,11 +188,20 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<() true, ), (r#"SELECT eql_v3.has_ore_block_256('{}'::jsonb)"#, false), - // json-null `ob` → `->>` yields NULL → absent. + // json-null `ob` is typed `'null'`, not `'array'` → absent. ( r#"SELECT eql_v3.has_ore_block_256('{"ob":null}'::jsonb)"#, false, ), + // Present-but-non-array `ob` is rejected as absent: a well-formed ORE + // term is always a JSON array of block terms, so a scalar and an object + // both → false. This is the boundary that makes `ore_block_256` RAISE on + // a malformed `ob` instead of degrading it to a NULL index term. + (r#"SELECT eql_v3.has_ore_block_256('{"ob":5}'::jsonb)"#, false), + ( + r#"SELECT eql_v3.has_ore_block_256('{"ob":{}}'::jsonb)"#, + false, + ), (r#"SELECT eql_v3.has_hmac_256('{"hm":"abc"}'::jsonb)"#, true), (r#"SELECT eql_v3.has_hmac_256('{}'::jsonb)"#, false), ]; @@ -209,6 +219,16 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<() ) .await?; + // Present-but-non-array `ob` → RAISE at the extractor boundary, NOT a silent + // NULL index term (`has_ore_block_256` reports it absent). + assert_raises( + &pool, + r#"SELECT eql_v3.ore_block_256('{"ob":5}'::jsonb)"#, + &[], + "Expected an ore index (ob) value", + ) + .await?; + // NULL jsonb → NULL composite (STRICT short-circuit), NOT a raise. let is_null: bool = sqlx::query_scalar("SELECT eql_v3.ore_block_256(NULL::jsonb) IS NULL") .fetch_one(&pool) From 807b7939e0208278aca40bab0b80d4ec7da661ca Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 09:26:54 +1000 Subject: [PATCH 07/12] style(v3): cargo fmt sem.rs has_ore_block_256 test case --- tests/sqlx/tests/encrypted_domain/family/sem.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/sqlx/tests/encrypted_domain/family/sem.rs b/tests/sqlx/tests/encrypted_domain/family/sem.rs index fd675673..d13847e5 100644 --- a/tests/sqlx/tests/encrypted_domain/family/sem.rs +++ b/tests/sqlx/tests/encrypted_domain/family/sem.rs @@ -197,7 +197,10 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<() // term is always a JSON array of block terms, so a scalar and an object // both → false. This is the boundary that makes `ore_block_256` RAISE on // a malformed `ob` instead of degrading it to a NULL index term. - (r#"SELECT eql_v3.has_ore_block_256('{"ob":5}'::jsonb)"#, false), + ( + r#"SELECT eql_v3.has_ore_block_256('{"ob":5}'::jsonb)"#, + false, + ), ( r#"SELECT eql_v3.has_ore_block_256('{"ob":{}}'::jsonb)"#, false, From 5e37e6045437f0fb38d920930622fe28fdae84bc Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 09:36:22 +1000 Subject: [PATCH 08/12] test(v3): always-on ORE comparator coverage + numeric scale-collision fixture Strengthen the N-block ORE comparator tests so they run creds-free on no-creds CI shards, sourcing real ORE terms from committed fixtures: - assert_orders_like_oracle: all-pairs oracle agreement + antisymmetry (replaces the adjacent-pair-only ascending chain), with a row-count drift guard against the catalog fixture order - comparator_length_guard_sweep: boundary/off-by lengths for the 49*N+16 guard across N=1..14 - wide_block_term_compares_equal_to_itself: reflexive path at N=14/12 - numeric_scale_equivalents_collide: 1 == 1.0 ORE collision via the new hand-written v3_numeric_collision fixture (the value-equal pair the catalog distinctness guard forbids in eql_v2_numeric) Also fix the stale timestamptz comment in scalar_domains.rs (now ordered, not eq-only). --- .gitignore | 1 + tests/sqlx/src/fixtures/mod.rs | 7 + .../sqlx/src/fixtures/v3_numeric_collision.rs | 77 ++++ tests/sqlx/src/scalar_domains.rs | 6 +- tests/sqlx/tests/generate_all_fixtures.rs | 9 + .../sqlx/tests/ore_block_comparator_tests.rs | 342 ++++++++++++++++-- 6 files changed, 404 insertions(+), 38 deletions(-) create mode 100644 tests/sqlx/src/fixtures/v3_numeric_collision.rs diff --git a/.gitignore b/.gitignore index 37af0cf2..98b64012 100644 --- a/.gitignore +++ b/.gitignore @@ -227,6 +227,7 @@ tests/sqlx/migrations/001_install_eql.sql tests/sqlx/fixtures/eql_v2* tests/sqlx/fixtures/v3_ste_vec.sql tests/sqlx/fixtures/v3_doc_int4.sql +tests/sqlx/fixtures/v3_numeric_collision.sql # Generated encrypted-domain SQL — regenerated by `tasks/build.sh` from the # eql-scalars::CATALOG via `cargo run -p eql-codegen` on every build. The diff --git a/tests/sqlx/src/fixtures/mod.rs b/tests/sqlx/src/fixtures/mod.rs index 098db4f6..62e86713 100644 --- a/tests/sqlx/src/fixtures/mod.rs +++ b/tests/sqlx/src/fixtures/mod.rs @@ -39,6 +39,13 @@ pub mod v3_ste_vec; // jsonb-entry behaviour matrix (`JsonbEntryInt4`). pub mod v3_doc_int4; +// The numeric scale-equivalence collision fixture (`1`, `1.0`, `2`). Not a +// CATALOG scalar — the catalog distinctness guard forbids the value-equal pair +// `1`/`1.0` — so it is hand-written and registered here directly (like the +// other `v3_` fixtures). Gives the `1 == 1.0` ORE collision an always-on +// (committed-fixture) home instead of a creds-gated runtime encryption. +pub mod v3_numeric_collision; + // The per-type scalar fixture modules (`eql_v2_int4`, `eql_v2_int2`, …) are // generated from the harness list in `scalar_types.rs`. Each expands to // `pub mod eql_v2_ { … scalar_fixture! … }`, reading its plaintext values diff --git a/tests/sqlx/src/fixtures/v3_numeric_collision.rs b/tests/sqlx/src/fixtures/v3_numeric_collision.rs new file mode 100644 index 00000000..fe58c63d --- /dev/null +++ b/tests/sqlx/src/fixtures/v3_numeric_collision.rs @@ -0,0 +1,77 @@ +//! The `v3_numeric_collision` fixture — the scale-equivalence collision pair +//! (`1`, `1.0`) plus a `2` discriminator, encrypted at numeric ORE width. +//! +//! Hand-written, non-catalog (like `v3_ste_vec` / `v3_doc_int4`, hence the +//! `v3_` prefix), because the catalog-driven `eql_v2_numeric` fixture CANNOT +//! carry it: `numeric_value_guards::fixtures_are_distinct_by_value` forbids two +//! fixtures that alias to the same `Decimal`, and `1` / `1.0` are value-equal. +//! So the `1 == 1.0` ORE collision — that scale-equivalent decimals encrypt to +//! comparison-equal ORE terms — has no catalog home. This tiny bespoke fixture +//! is the only place those two representations can coexist. +//! +//! Rows are addressed by `id` (NOT `plaintext`): `WHERE plaintext = 1` matches +//! both `1` and `1.0` (numeric equality ignores scale), so the collision test +//! fetches `id = 1` (`1`), `id = 2` (`1.0`), `id = 3` (`2`). The `id` is the +//! 1-based insertion ordinal the driver assigns over `VALUES`. +//! +//! Gitignored output: tests/sqlx/fixtures/v3_numeric_collision.sql +//! (regenerated by `mise run fixture:generate:all`). + +use anyhow::Result; +use rust_decimal::Decimal; +use std::str::FromStr; + +use super::index_kind::IndexKind; +use super::spec::FixtureSpec; + +/// The committed fixture name → table `fixtures.v3_numeric_collision`, script +/// `v3_numeric_collision.sql`, SQLx ref `scripts("v3_numeric_collision")`. +const NAME: &str = "v3_numeric_collision"; + +/// The fixture plaintexts, in insertion order. `id` is the 1-based ordinal, so +/// `1` → id 1, `1.0` → id 2, `2` → id 3. `1` and `1.0` are deliberately +/// value-equal (the collision pair); `2` is a distinct discriminator so the +/// test can prove the comparator is not a degenerate everything-collides. +fn values() -> Vec { + ["1", "1.0", "2"] + .iter() + .map(|s| Decimal::from_str(s).expect("valid decimal literal")) + .collect() +} + +/// Generate `tests/sqlx/fixtures/v3_numeric_collision.sql`. Encrypts the +/// three decimals at numeric ORE width via the standard `.run()` driver — the +/// encryption input and the `plaintext` oracle column are the same value stream, +/// so no split (`run_with_payloads`) is needed. +pub async fn generate() -> Result<()> { + let values = values(); + FixtureSpec::new(NAME) + .with_index(IndexKind::Unique) + .with_index(IndexKind::Ore) + .with_column_type("jsonb") + .with_values(&values) + .run() + .await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_collision_pair_is_value_equal_but_distinct_in_scale() { + let v = values(); + assert_eq!(v.len(), 3, "fixture is [1, 1.0, 2]"); + // The collision pair compares equal by value... + assert_eq!(v[0], v[1], "1 and 1.0 must be value-equal (the collision)"); + // ...yet is two distinct textual representations (different scale), so + // the catalog distinctness guard would reject them together. + assert_ne!( + v[0].scale(), + v[1].scale(), + "1 (scale 0) and 1.0 (scale 1) must differ in scale" + ); + // The discriminator is genuinely larger. + assert!(v[2] > v[0], "2 must order after 1"); + } +} diff --git a/tests/sqlx/src/scalar_domains.rs b/tests/sqlx/src/scalar_domains.rs index 84bf9da2..11d0ff15 100644 --- a/tests/sqlx/src/scalar_domains.rs +++ b/tests/sqlx/src/scalar_domains.rs @@ -319,11 +319,11 @@ temporal_values! { } // `timestamptz`'s `ScalarType` wiring, generated from its catalog row by the -// same `temporal_values!` path as `date`. timestamptz is equality-only (its -// catalog row uses the eq-only domain shape), but the *value* wiring is +// same `temporal_values!` path as `date`. timestamptz is ordered (its catalog +// row uses the ordered domain shape, 12-block ORE), and the *value* wiring is // identical to any temporal scalar: RFC3339 strings parsed once into // `DateTime` behind `timestamptz_values()`. The pivots are retained as the -// three equality anchors the matrix sweeps. +// three min/mid/max anchors the matrix sweeps. temporal_values! { cell = TIMESTAMPTZ_VALUES_CELL, accessor = timestamptz_values, diff --git a/tests/sqlx/tests/generate_all_fixtures.rs b/tests/sqlx/tests/generate_all_fixtures.rs index 40db2216..dcb50c27 100644 --- a/tests/sqlx/tests/generate_all_fixtures.rs +++ b/tests/sqlx/tests/generate_all_fixtures.rs @@ -49,5 +49,14 @@ async fn generate_all() -> anyhow::Result<()> { eprintln!("Generating fixture v3_doc_int4 (scalar-shaped SteVec document)..."); eql_tests::fixtures::v3_doc_int4::generate().await?; eprintln!("Regenerated v3_doc_int4."); + + // The numeric scale-equivalence collision fixture (`1`, `1.0`, `2`). Not a + // CATALOG scalar — the distinctness guard forbids `1`/`1.0` coexisting in + // `eql_v2_numeric` — so it rides the same pipeline as a hand-written + // `FixtureSpec`. Gives the always-on `1 == 1.0` ORE collision test + // its committed fixture. + eprintln!("Generating fixture v3_numeric_collision (1 == 1.0 ORE collision)..."); + eql_tests::fixtures::v3_numeric_collision::generate().await?; + eprintln!("Regenerated v3_numeric_collision."); Ok(()) } diff --git a/tests/sqlx/tests/ore_block_comparator_tests.rs b/tests/sqlx/tests/ore_block_comparator_tests.rs index 7fcc5eb3..5a65cd85 100644 --- a/tests/sqlx/tests/ore_block_comparator_tests.rs +++ b/tests/sqlx/tests/ore_block_comparator_tests.rs @@ -1,14 +1,128 @@ -//! Direct unit tests for the generalized N-block ORE comparator -//! `eql_v3.compare_ore_block_256_term`. +//! Direct tests for the generalized N-block ORE comparator +//! `eql_v3.compare_ore_block_256_term(s)`. //! -//! The malformed-length guards here are creds-free: they construct ORE terms -//! by hand from short byte strings, so they exercise the length validation -//! without needing real (ZeroKMS-generated) ciphertexts. The wide-term ordering -//! test (added in Phase 4) uses generated numeric fixtures. +//! Every test here is **always-on** — it runs in normal (no-creds) CI, because +//! it sources real ORE terms from committed fixtures rather than encrypting at +//! runtime: +//! * The malformed-length guards build ORE terms by hand from short byte +//! strings, exercising length validation without real ciphertexts. +//! * The ordering properties (all-pairs oracle agreement + antisymmetry) read +//! the committed `eql_v2_numeric` / `eql_v2_timestamptz` fixtures, whose +//! catalog order is the strict ascending oracle. +//! * The `1 == 1.0` ORE collision reads the committed `v3_numeric_collision` +//! fixture — the one place the value-equal pair can live, since the catalog +//! distinctness guard forbids it in `eql_v2_numeric`. +//! +//! Fixtures are generated once (with creds) in the `build-archive` CI job and +//! baked into the test binaries via `include_str!`, so the no-creds shards +//! consume them directly. See `tasks/test/sqlx-archive.sh`. use anyhow::Result; use sqlx::PgPool; +/// Fetch two fixture payloads by plaintext literal, wrap each in the ordered +/// extractor, and return the ORE comparison. The single fetch + `ord_term` + +/// `compare_ore_block_256_terms` shape every chain/pair test shares; `lo`/`hi` +/// are the plaintext SQL literals (with cast), e.g. `"(-1)::numeric"`. +async fn compare_fixture_pair( + pool: &PgPool, + table: &str, + ord_domain: &str, + lo: &str, + hi: &str, +) -> Result { + let sql = format!( + "SELECT eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term((SELECT payload FROM fixtures.{table} WHERE plaintext = {lo})::eql_v3.{ord_domain}), \ + eql_v3.ord_term((SELECT payload FROM fixtures.{table} WHERE plaintext = {hi})::eql_v3.{ord_domain}))" + ); + Ok(sqlx::query_scalar::<_, i32>(&sql).fetch_one(pool).await?) +} + +/// A hand-built ORE term of `len` bytes filled with `fill`. Creds-free — the +/// bytes are cryptographically meaningless, so this only drives length/structure +/// validation, never ordering semantics. +fn term_sql(fill: char, len: usize) -> String { + format!("ROW(repeat('{fill}', {len})::bytea)::eql_v3.ore_block_256_term") +} + +/// Assert the ORE comparator agrees with an explicit oracle order over a +/// committed fixture. `ascending[i]` is the SQL literal (with cast) for the +/// value whose oracle rank is `i`; its real ciphertext is fetched from +/// `fixtures.{table}` by `plaintext` and loaded into a connection-local +/// `ore_sample(rank, payload)`. +/// +/// Two properties, both over EVERY pair (not just adjacent): +/// * **Oracle agreement** — `rank a < rank b` ⇒ `compare(a, b) = -1`. +/// All-pairs subsumes totality and transitivity; combined with antisymmetry +/// it also pins the `>` direction, so a one-sided bug cannot hide. +/// * **Antisymmetry** — `compare(a, b) = -compare(b, a)` for all distinct +/// pairs. +/// +/// Creds-free: every term is a real committed ciphertext. The per-row +/// `rows_affected == 1` check fails loudly if the `ascending` list drifts from +/// the fixture (a removed/renamed value resolves to zero rows). +async fn assert_orders_like_oracle( + pool: &PgPool, + table: &str, + ord_domain: &str, + ascending: &[String], +) -> Result<()> { + // TEMP is connection-scoped and a pool may hand out different connections + // per query — pin everything to one acquired connection. + let mut conn = pool.acquire().await?; + sqlx::query("CREATE TEMP TABLE ore_sample (rank int, payload jsonb)") + .execute(&mut *conn) + .await?; + for (rank, literal) in ascending.iter().enumerate() { + let inserted = sqlx::query(&format!( + "INSERT INTO ore_sample (rank, payload) \ + SELECT {rank}, payload FROM fixtures.{table} WHERE plaintext = {literal}" + )) + .execute(&mut *conn) + .await? + .rows_affected(); + anyhow::ensure!( + inserted == 1, + "expected exactly 1 fixture row for {literal} (rank {rank}), got {inserted} \ + — the ascending list has drifted from fixtures.{table}" + ); + } + + // Oracle agreement: lower rank (smaller value) MUST compare -1. + let order_violations: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM ore_sample a JOIN ore_sample b ON a.rank < b.rank \ + WHERE eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term(a.payload::eql_v3.{ord_domain}), \ + eql_v3.ord_term(b.payload::eql_v3.{ord_domain})) <> -1" + )) + .fetch_one(&mut *conn) + .await?; + assert_eq!( + order_violations, 0, + "ORE comparator disagreed with the oracle order on some pair" + ); + + // Antisymmetry: compare(a, b) = -compare(b, a) for every distinct pair. + let antisymmetry_violations: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM ore_sample a JOIN ore_sample b ON a.rank <> b.rank \ + WHERE eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term(a.payload::eql_v3.{ord_domain}), \ + eql_v3.ord_term(b.payload::eql_v3.{ord_domain})) \ + <> - eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term(b.payload::eql_v3.{ord_domain}), \ + eql_v3.ord_term(a.payload::eql_v3.{ord_domain}))" + )) + .fetch_one(&mut *conn) + .await?; + assert_eq!( + antisymmetry_violations, 0, + "ORE comparator violated antisymmetry on some pair" + ); + + Ok(()) +} + /// A `bytea` whose length is NOT a valid `49*N + 16` must raise, not silently /// return 0. Uses a 4-byte term (equal lengths so the equal-length guard does /// not fire first). @@ -73,6 +187,66 @@ async fn comparator_rejects_mismatched_block_widths(pool: PgPool) -> Result<()> Ok(()) } +/// Sweep the `49*N + 16` length guard across boundary/off-by lengths the +/// point-example tests above don't reach. Both operands are kept the SAME length +/// so only the malformed-length guard can fire (the different-lengths guard at +/// `bit_length` precedes it). Creds-free. +/// +/// Valid lengths are pinned to exact return values (verified against +/// `src/v3/sem/ore_block_256/functions.sql`): +/// * equal operands take the all-blocks-equal path → return **0** +/// (`functions.sql:166-168`; the `encrypt()` branch is unreachable); +/// * differing operands fall through to the `encrypt()` path → return **±1** +/// (`functions.sql:170-190`), which is the branch the length guard protects. +#[sqlx::test] +async fn comparator_length_guard_sweep(pool: PgPool) -> Result<()> { + // Invalid: not 49*N + 16 (16 and 4 are covered by the dedicated tests above). + for len in [15usize, 17, 50, 64, 66, 407, 409, 701, 703] { + let sql = format!( + "SELECT eql_v3.compare_ore_block_256_term({}, {})", + term_sql('a', len), + term_sql('b', len) + ); + let err = sqlx::query_scalar::<_, i32>(&sql) + .fetch_one(&pool) + .await + .expect_err(&format!("length {len} (not 49*N+16) must raise")); + assert!( + err.to_string() + .to_lowercase() + .contains("malformed ore term"), + "len {len}: expected malformed-term error, got: {err}" + ); + } + + // Valid: 49*N + 16 for N = 1..=14 (spans the int4/timestamp/numeric widths). + for n in 1..=14usize { + let len = 49 * n + 16; + + let eq: i32 = sqlx::query_scalar(&format!( + "SELECT eql_v3.compare_ore_block_256_term({}, {})", + term_sql('a', len), + term_sql('a', len) + )) + .fetch_one(&pool) + .await?; + assert_eq!(eq, 0, "len {len} (N={n}): identical terms must compare 0"); + + let ne: i32 = sqlx::query_scalar(&format!( + "SELECT eql_v3.compare_ore_block_256_term({}, {})", + term_sql('a', len), + term_sql('b', len) + )) + .fetch_one(&pool) + .await?; + assert!( + ne == -1 || ne == 1, + "len {len} (N={n}): differing terms must compare ±1 (encrypt path), got {ne}" + ); + } + Ok(()) +} + /// Width: a numeric ORE term must be 14 blocks => 49*14 + 16 = 702 bytes. #[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric")))] async fn numeric_term_is_14_blocks(pool: PgPool) -> Result<()> { @@ -87,14 +261,17 @@ async fn numeric_term_is_14_blocks(pool: PgPool) -> Result<()> { Ok(()) } -/// Full ascending chain of 14-block numeric terms: every adjacent pair must -/// order `-1`. Spans sign, magnitude, and fractional (low-block) scale, so the -/// left blocks — not just the right blocks — decide ordering. This is the -/// regression the missed `9 -> 1+n` left-offset would fail; a single pair could -/// pass against that bug. +/// 14-block numeric terms must order like `Decimal`'s `Ord` over ALL pairs (not +/// just adjacent), plus antisymmetry. Spans sign, magnitude, and fractional +/// (low-block) scale, so the left blocks — not just the right blocks — decide +/// ordering. This is the regression the missed `9 -> 1+n` left-offset would +/// fail; the all-pairs sweep makes a single lucky pair unable to mask it. The +/// list is the strict ascending oracle (matching `NUMERIC_FIXTURES`' catalog +/// order); `assert_orders_like_oracle` fails loudly if it drifts from the +/// committed fixture. #[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric")))] -async fn numeric_terms_order_in_ascending_chain(pool: PgPool) -> Result<()> { - let ascending = [ +async fn numeric_terms_order_like_decimal_ord(pool: PgPool) -> Result<()> { + let ascending: Vec = [ "-1000000000000", "-1000000", "-1.001", @@ -109,25 +286,17 @@ async fn numeric_terms_order_in_ascending_chain(pool: PgPool) -> Result<()> { "1.001", "1000000", "1000000000000", - ]; - for pair in ascending.windows(2) { - let (lo, hi) = (pair[0], pair[1]); - let cmp: i32 = sqlx::query_scalar(&format!( - "SELECT eql_v3.compare_ore_block_256_terms( \ - eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_numeric WHERE plaintext = ({lo})::numeric)::eql_v3.numeric_ord), \ - eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_numeric WHERE plaintext = ({hi})::numeric)::eql_v3.numeric_ord))" - )) - .fetch_one(&pool) - .await?; - assert_eq!(cmp, -1, "{lo} must order before {hi}"); - } - Ok(()) + ] + .iter() + .map(|v| format!("({v})::numeric")) + .collect(); + assert_orders_like_oracle(&pool, "eql_v2_numeric", "numeric_ord", &ascending).await } -/// Symmetric 12-block (timestamptz, N=12 => 604 bytes) width + ordering check. -/// 12 is the only N strictly between the working 8 and the headline 14. +/// Width + single-pair sanity for the 12-block (timestamptz, N=12 => 604 bytes) +/// term. The full ordering property is `timestamptz_terms_order_like_datetime_ord`. #[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_timestamptz")))] -async fn timestamptz_term_is_12_blocks_and_orders(pool: PgPool) -> Result<()> { +async fn timestamptz_term_is_12_blocks(pool: PgPool) -> Result<()> { let width: i32 = sqlx::query_scalar( "SELECT octet_length((((eql_v3.ord_term( \ (SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '1970-01-01T00:00:00Z'::timestamptz) \ @@ -139,14 +308,117 @@ async fn timestamptz_term_is_12_blocks_and_orders(pool: PgPool) -> Result<()> { width, 604, "timestamptz ORE term must be 12 blocks (604 bytes)" ); + Ok(()) +} - let cmp: i32 = sqlx::query_scalar( - "SELECT eql_v3.compare_ore_block_256_terms( \ - eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '1900-01-01T00:00:00Z'::timestamptz)::eql_v3.timestamptz_ord), \ - eql_v3.ord_term((SELECT payload FROM fixtures.eql_v2_timestamptz WHERE plaintext = '2099-12-31T23:59:59Z'::timestamptz)::eql_v3.timestamptz_ord))", +/// 12-block (timestamptz) terms must order like `DateTime`'s `Ord` over +/// ALL pairs, plus antisymmetry. N=12 is the only width strictly between the +/// working 8 and the headline 14, so it needs the same left-block-deciding +/// coverage as numeric. The 15 values are the strict ascending oracle (matching +/// `TIMESTAMPTZ_FIXTURES`' catalog order); `assert_orders_like_oracle` fails +/// loudly if the list drifts from the committed fixture. +#[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_timestamptz")))] +async fn timestamptz_terms_order_like_datetime_ord(pool: PgPool) -> Result<()> { + let ascending: Vec = [ + "1900-01-01T00:00:00Z", + "1950-07-15T06:30:00Z", + "1969-12-31T23:59:59Z", + "1970-01-01T00:00:00Z", + "1970-01-01T00:00:01Z", + "1985-04-12T23:20:50Z", + "1999-12-31T23:59:59Z", + "2000-01-01T00:00:00Z", + "2004-02-29T12:00:00Z", + "2012-06-30T11:59:59Z", + "2016-03-15T08:15:30Z", + "2020-10-21T14:45:00Z", + "2024-02-29T17:30:45Z", + "2038-01-19T03:14:07Z", + "2099-12-31T23:59:59Z", + ] + .iter() + .map(|v| format!("'{v}'::timestamptz")) + .collect(); + assert_orders_like_oracle(&pool, "eql_v2_timestamptz", "timestamptz_ord", &ascending).await +} + +/// A real wide-block term must compare equal to itself — the reflexive +/// `eq`-true path (`functions.sql:166`) at N=14 and N=12, creds-free (reuses the +/// generated fixtures). Distinct from the `1 == 1.0` collision (Gap 1), which is +/// equality across *different* ciphertexts. +#[sqlx::test(fixtures(path = "../fixtures", scripts("eql_v2_numeric", "eql_v2_timestamptz")))] +async fn wide_block_term_compares_equal_to_itself(pool: PgPool) -> Result<()> { + let numeric = compare_fixture_pair( + &pool, + "eql_v2_numeric", + "numeric_ord", + "(1)::numeric", + "(1)::numeric", + ) + .await?; + assert_eq!(numeric, 0, "a 14-block numeric term must equal itself"); + + let timestamptz = compare_fixture_pair( + &pool, + "eql_v2_timestamptz", + "timestamptz_ord", + "'2000-01-01T00:00:00Z'::timestamptz", + "'2000-01-01T00:00:00Z'::timestamptz", ) - .fetch_one(&pool) .await?; - assert_eq!(cmp, -1, "1900 must order before 2099"); + assert_eq!( + timestamptz, 0, + "a 12-block timestamptz term must equal itself" + ); + Ok(()) +} + +/// Compare the collision-fixture rows addressed by `id`. The +/// `v3_numeric_collision` fixture stores `1` (id 1), `1.0` (id 2), `2` (id 3); +/// rows are fetched by `id` because `WHERE plaintext = 1` is ambiguous (numeric +/// equality matches both `1` and `1.0`). +async fn compare_collision_ids(pool: &PgPool, a: i64, b: i64) -> Result { + let sql = format!( + "SELECT eql_v3.compare_ore_block_256_terms( \ + eql_v3.ord_term((SELECT payload FROM fixtures.v3_numeric_collision WHERE id = {a})::eql_v3.numeric_ord), \ + eql_v3.ord_term((SELECT payload FROM fixtures.v3_numeric_collision WHERE id = {b})::eql_v3.numeric_ord))" + ); + Ok(sqlx::query_scalar::<_, i32>(&sql).fetch_one(pool).await?) +} + +/// Scale-equivalent decimals (`1` and `1.0`) must collide in the ORE +/// ciphertext: they are value-equal numerics, so their ORE terms must compare +/// `0`. Always-on via the committed `v3_numeric_collision` fixture — the only +/// place the value-equal pair can live, since the catalog distinctness guard +/// (`scalar_domains.rs` `numeric_value_guards`) forbids it in `eql_v2_numeric`. +/// This is the positive counterpart to that negative guard. +/// +/// Asserted in BOTH directions (a scale-biased comparator could pass a +/// one-directional check); the `1`-vs-`2` guards are load-bearing — they defeat +/// a degenerate everything-returns-0 comparator that would otherwise pass the +/// collision assertions. +#[sqlx::test(fixtures(path = "../fixtures", scripts("v3_numeric_collision")))] +async fn numeric_scale_equivalents_collide(pool: PgPool) -> Result<()> { + // ids: 1 => `1`, 2 => `1.0`, 3 => `2`. + assert_eq!( + compare_collision_ids(&pool, 1, 2).await?, + 0, + "1 and 1.0 must collide" + ); + assert_eq!( + compare_collision_ids(&pool, 2, 1).await?, + 0, + "the collision must be order-independent" + ); + assert_eq!( + compare_collision_ids(&pool, 1, 3).await?, + -1, + "1 must order before 2" + ); + assert_eq!( + compare_collision_ids(&pool, 3, 1).await?, + 1, + "2 must order after 1" + ); Ok(()) } From 61c5f503c135a9063ec2fe852f85a58f961d73aa Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 10:22:23 +1000 Subject: [PATCH 09/12] feat(eql-types): add numeric + promote timestamptz to ordered domains The eql-types crate landed on eql_v3 (PR #236) after this branch forked, so merging the base in surfaced a catalog_parity failure: CATALOG now has the numeric family and ordered timestamptz, but v3::all() didn't. - numeric.rs: four ordered domains (storage/_eq/_ord/_ord_ore), mirroring date.rs; numeric is the first scalar with a >8-block ORE term (14) - timestamptz.rs: add the two ordered domains; the eq-only/8-block-limit rationale is gone now that eql_v3.ore_block_256 derives N from term length - mod.rs: register the six new domains in all(), in CATALOG order - terms.rs: the ob term's SQL constructor is eql_v3.ore_block_256 (renamed this branch) and is width-agnostic (8/12/14 blocks) - v3_conformance.rs: cover numeric + timestamptz ord wire shapes; drop the stale equality-only claim - README: timestamptz no longer eq-only; add numeric --- crates/eql-types/README.md | 2 +- crates/eql-types/src/v3/mod.rs | 7 ++ crates/eql-types/src/v3/numeric.rs | 112 +++++++++++++++++++++++ crates/eql-types/src/v3/terms.rs | 8 +- crates/eql-types/src/v3/timestamptz.rs | 67 ++++++++++++-- crates/eql-types/tests/v3_conformance.rs | 55 +++++++++-- 6 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 crates/eql-types/src/v3/numeric.rs diff --git a/crates/eql-types/README.md b/crates/eql-types/README.md index df19baff..2940a01f 100644 --- a/crates/eql-types/README.md +++ b/crates/eql-types/README.md @@ -21,7 +21,7 @@ hand-copying. The [`src/v3/`](src/v3/) module has one type per **SQL domain** in the `eql_v3` schema — `Int4` / `Int4Eq` / `Int4Ord` / `Int4OrdOre`, and likewise -for `int2`, `int8`, `date`, `timestamptz` (eq-only), and `text` (which adds +for `int2`, `int8`, `date`, `timestamptz`, `numeric`, and `text` (which adds `TextMatch`) — each carrying its index terms as **required** fields. The capability is the type identity; `Option` never appears. A payload missing its term key fails to deserialize: the Rust analogue of the SQL domain's diff --git a/crates/eql-types/src/v3/mod.rs b/crates/eql-types/src/v3/mod.rs index 316f53e7..0b29017e 100644 --- a/crates/eql-types/src/v3/mod.rs +++ b/crates/eql-types/src/v3/mod.rs @@ -50,6 +50,7 @@ pub mod date; pub mod int2; pub mod int4; pub mod int8; +pub mod numeric; pub mod terms; pub mod text; pub mod timestamptz; @@ -130,6 +131,12 @@ pub fn all() -> Vec> { Box::new(PhantomData::), Box::new(PhantomData::), Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), + Box::new(PhantomData::), Box::new(PhantomData::), Box::new(PhantomData::), Box::new(PhantomData::), diff --git a/crates/eql-types/src/v3/numeric.rs b/crates/eql-types/src/v3/numeric.rs new file mode 100644 index 00000000..660833e0 --- /dev/null +++ b/crates/eql-types/src/v3/numeric.rs @@ -0,0 +1,112 @@ +//! The `numeric` encrypted-domain family — an ordered, non-integer scalar +//! backed by `rust_decimal::Decimal`. Same four-domain ordered shape as +//! [`crate::v3::int4`] (ORE compares ciphertext, so decimals order like +//! integers); see that module for the capability table. +//! +//! `numeric` is the first scalar whose native ORE term is wider than 8 blocks +//! (14 blocks): the wire shape is unchanged — the `ob` array simply carries +//! more block strings — and the generalized `eql_v3.ore_block_256` comparator +//! orders any block count, so no new type is needed here. + +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; +use crate::v3::DomainType; +use crate::{Identifier, SchemaVersion}; +use serde::{Deserialize, Serialize}; + +/// `eql_v3.numeric` — storage only; every operator is blocked. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Numeric { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, +} + +impl DomainType for Numeric { + fn sql_domain_static() -> &'static str { + "eql_v3.numeric" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} + +/// `eql_v3.numeric_eq` — HMAC equality (`=`, `<>`). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NumericEq { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, + /// HMAC-SHA-256 equality term. + pub hm: Hmac256, +} + +impl DomainType for NumericEq { + fn sql_domain_static() -> &'static str { + "eql_v3.numeric_eq" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} + +/// `eql_v3.numeric_ord_ore` — full comparison, scheme-explicit name. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NumericOrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, + /// Block-ORE order term (14 blocks for numeric). Serves equality too. + pub ob: OreBlock256, +} + +impl DomainType for NumericOrdOre { + fn sql_domain_static() -> &'static str { + "eql_v3.numeric_ord_ore" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} + +/// `eql_v3.numeric_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NumericOrd { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, + /// Block-ORE order term (14 blocks for numeric). Serves equality too. + pub ob: OreBlock256, +} + +impl DomainType for NumericOrd { + fn sql_domain_static() -> &'static str { + "eql_v3.numeric_ord" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} diff --git a/crates/eql-types/src/v3/terms.rs b/crates/eql-types/src/v3/terms.rs index f5319ed7..1036ee62 100644 --- a/crates/eql-types/src/v3/terms.rs +++ b/crates/eql-types/src/v3/terms.rs @@ -23,9 +23,11 @@ pub struct Ciphertext(pub String); #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Hmac256(pub String); -/// Block-ORE (u64, 8 blocks, 256) order term — the `ob` wire key. Backs the -/// `_ord` / `_ord_ore` domains (`=` `<>` `<` `<=` `>` `>=`); ORE is lossless -/// over the scalar's domain, so it serves equality too. SQL-side constructor: +/// Block-ORE 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. The block count is width-agnostic on the +/// wire (8 for the int scalars, 12 for timestamptz, 14 for numeric) — the +/// array just carries more block strings. SQL-side constructor: /// `eql_v3.ore_block_256`. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct OreBlock256(pub Vec); diff --git a/crates/eql-types/src/v3/timestamptz.rs b/crates/eql-types/src/v3/timestamptz.rs index 6c4621b7..5cb959b7 100644 --- a/crates/eql-types/src/v3/timestamptz.rs +++ b/crates/eql-types/src/v3/timestamptz.rs @@ -1,10 +1,15 @@ -//! The `timestamptz` encrypted-domain family — **equality-only** (storage + -//! `_eq`). There is no ordered domain: cipherstash encrypts timestamps at -//! native 12-block ORE width, but EQL's only ORE comparator is hardcoded to -//! 8 blocks, so an ordered timestamptz domain would silently mis-order. -//! Ordering arrives with a future wide-ORE term (see `eql-scalars`). +//! The `timestamptz` encrypted-domain family — an ordered, non-integer scalar. +//! Same four-domain ordered shape as [`crate::v3::int4`] (ORE compares +//! ciphertext, so timestamps order like integers); see that module for the +//! capability table. +//! +//! cipherstash encrypts timestamps at native 12-block ORE width. The family +//! was equality-only while EQL's ORE comparator was hardcoded to 8 blocks; +//! now that `eql_v3.ore_block_256` derives the block count from the term +//! length, the 12-block `ob` term orders correctly and the ordered domains +//! ship. The wire shape is unchanged — the `ob` array just carries 12 blocks. -use crate::v3::terms::{Ciphertext, Hmac256}; +use crate::v3::terms::{Ciphertext, Hmac256, OreBlock256}; use crate::v3::DomainType; use crate::{Identifier, SchemaVersion}; use serde::{Deserialize, Serialize}; @@ -56,3 +61,53 @@ impl DomainType for TimestamptzEq { Self::sql_domain_static() } } + +/// `eql_v3.timestamptz_ord_ore` — full comparison, scheme-explicit name. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TimestamptzOrdOre { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, + /// Block-ORE order term (12 blocks for timestamptz). Serves equality too. + pub ob: OreBlock256, +} + +impl DomainType for TimestamptzOrdOre { + fn sql_domain_static() -> &'static str { + "eql_v3.timestamptz_ord_ore" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} + +/// `eql_v3.timestamptz_ord` — full comparison (`=` `<>` `<` `<=` `>` `>=`). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TimestamptzOrd { + /// Envelope version — always `2` (`EQL_SCHEMA_VERSION`); any other + /// value fails deserialization. + pub v: SchemaVersion, + /// Table/column identifier. Required by the domain CHECK. + pub i: Identifier, + /// mp_base85 source ciphertext. Required by the domain CHECK. + pub c: Ciphertext, + /// Block-ORE order term (12 blocks for timestamptz). Serves equality too. + pub ob: OreBlock256, +} + +impl DomainType for TimestamptzOrd { + fn sql_domain_static() -> &'static str { + "eql_v3.timestamptz_ord" + } + + fn sql_domain(&self) -> &'static str { + Self::sql_domain_static() + } +} diff --git a/crates/eql-types/tests/v3_conformance.rs b/crates/eql-types/tests/v3_conformance.rs index a20e6e99..df345674 100644 --- a/crates/eql-types/tests/v3_conformance.rs +++ b/crates/eql-types/tests/v3_conformance.rs @@ -168,7 +168,7 @@ fn non_int4_tokens_round_trip_every_domain() { // `catalog_parity.rs` checks domain *names* only, never the wire shape. // This sweep roundtrips every non-int4 domain and pins its catalog name, // failing the instant a token drifts from the shared envelope/term contract. - use eql_types::v3::{date::*, int2::*, int8::*, text::*}; + use eql_types::v3::{date::*, int2::*, int8::*, numeric::*, text::*}; // Wire builders for the three shapes the ordered tokens share. let storage = |t: &str| json!({ "v": 2, "i": { "t": t, "c": "x" }, "c": "ct" }); @@ -204,6 +204,13 @@ fn non_int4_tokens_round_trip_every_domain() { round_trip!(DateOrd, ord("a"), "eql_v3.date_ord"); round_trip!(DateOrdOre, ord("a"), "eql_v3.date_ord_ore"); + // numeric is the first scalar whose native ORE term exceeds 8 blocks (14); + // the wire shape is identical, so the same `ord` builder applies. + round_trip!(Numeric, storage("a"), "eql_v3.numeric"); + round_trip!(NumericEq, eq("a"), "eql_v3.numeric_eq"); + round_trip!(NumericOrd, ord("a"), "eql_v3.numeric_ord"); + round_trip!(NumericOrdOre, ord("a"), "eql_v3.numeric_ord_ore"); + // text_match is covered by `text_match_round_trips_signed_bloom_filter`. round_trip!(Text, storage("a"), "eql_v3.text"); round_trip!(TextEq, eq("a"), "eql_v3.text_eq"); @@ -213,12 +220,16 @@ fn non_int4_tokens_round_trip_every_domain() { } #[test] -fn timestamptz_round_trips_and_enforces_equality_term() { - // The one structurally-distinct token: equality-only, no `_ord`/`_ord_ore` - // (the 8-block-ORE limitation). The int4 template was copy-pasted to - // produce it, so an accidental extra `ob` field or a dropped `hm` would - // pass `catalog_parity` (domain names only) but is caught here. - use eql_types::v3::timestamptz::{Timestamptz, TimestamptzEq}; +fn timestamptz_round_trips_and_enforces_term_capabilities() { + // timestamptz is an ordered token (12-block ORE) — it carries the full + // storage/`_eq`/`_ord`/`_ord_ore` shape, the same as the int scalars. The + // int4 template was copy-pasted to produce it, so a dropped `hm`/`ob` or a + // field typo would pass `catalog_parity` (domain names only) but is caught + // here. (Was equality-only while the ORE comparator was hardcoded to 8 + // blocks; promoted once `eql_v3.ore_block_256` generalized to any width.) + use eql_types::v3::timestamptz::{ + Timestamptz, TimestamptzEq, TimestamptzOrd, TimestamptzOrdOre, + }; // Storage-only: envelope, no term. let storage = json!({ @@ -241,16 +252,40 @@ fn timestamptz_round_trips_and_enforces_equality_term() { assert_eq!(serde_json::to_value(&parsed).unwrap(), with_hm); assert_eq!(TimestamptzEq::sql_domain_static(), "eql_v3.timestamptz_eq"); - // `_eq` is the only searchable shape this token has, so its equality term - // cannot silently become optional. + // Ordered: envelope + ob (a 12-block array on the wire; shape is the same). + let with_ob = json!({ + "v": 2, + "i": { "t": "events", "c": "occurred_at" }, + "c": "mp_base85_ciphertext", + "ob": ["b0", "b1"] + }); + let parsed: TimestamptzOrd = serde_json::from_value(with_ob.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), with_ob); + assert_eq!( + TimestamptzOrd::sql_domain_static(), + "eql_v3.timestamptz_ord" + ); + let parsed: TimestamptzOrdOre = serde_json::from_value(with_ob.clone()).unwrap(); + assert_eq!(serde_json::to_value(&parsed).unwrap(), with_ob); + assert_eq!( + TimestamptzOrdOre::sql_domain_static(), + "eql_v3.timestamptz_ord_ore" + ); + + // The searchable domains cannot let their term silently become optional. let no_hm = json!({ "v": 2, "i": { "t": "events", "c": "occurred_at" }, "c": "mp_base85_ciphertext" }); - let result: Result = serde_json::from_value(no_hm); + let result: Result = serde_json::from_value(no_hm.clone()); assert!( result.is_err(), "TimestamptzEq must reject a payload with no hm" ); + let result: Result = serde_json::from_value(no_hm); + assert!( + result.is_err(), + "TimestamptzOrd must reject a payload with no ob" + ); } From b16d6ad5bae0db7f5d6609acd500eaa83893e0a3 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 09:25:18 +1000 Subject: [PATCH 10/12] docs: add walkthroughs for SQL code generator and fixture generator Add two self-contained walkthroughs under docs/walkthroughs/ covering the catalog-driven Rust generation systems: - sql-code-generator.md: how eql-codegen renders the eql_v3 encrypted-domain SQL surface (types/functions/operators/aggregates) from eql-scalars::CATALOG, the ScalarSpec/DomainSpec/Term data model, a worked int4 trace, the enforced invariants/footguns (blocker plpgsql/non-STRICT, inlinable SQL fns, no domain-over-domain, no opclass on a domain), and determinism/parity gating. - fixture-generator.md: how the generate_all_fixtures test materializes catalog fixture values (int_values!/temporal_values!) and encrypts them through cipherstash-client/ZeroKMS into the gitignored eql_v2_*.sql fixtures, the real-crypto hard requirement and credential gating, and a worked int4 trace. Both include Mermaid pipeline and data-model diagrams with path:line references. --- docs/walkthroughs/fixture-generator.md | 351 ++++++++++++++++++++ docs/walkthroughs/sql-code-generator.md | 410 ++++++++++++++++++++++++ 2 files changed, 761 insertions(+) create mode 100644 docs/walkthroughs/fixture-generator.md create mode 100644 docs/walkthroughs/sql-code-generator.md diff --git a/docs/walkthroughs/fixture-generator.md b/docs/walkthroughs/fixture-generator.md new file mode 100644 index 00000000..dd7fd960 --- /dev/null +++ b/docs/walkthroughs/fixture-generator.md @@ -0,0 +1,351 @@ +# Fixture Generator Walkthrough + +How EQL's SQLx test suite gets its encrypted test data: plaintext values declared +once in a Rust catalog, encrypted through **real** CipherStash crypto, and emitted +as gitignored SQL `INSERT` scripts the test matrix loads. + +> Scope: the **fixture / value** side of the system — where plaintext values come +> from, how they are encrypted, and what SQL is produced. The sibling SQL code +> generator (`eql-codegen`, which renders the `eql_v3.*` domain functions and +> operators) consumes the same `eql-scalars::CATALOG` but is documented elsewhere. + +--- + +## 1. Why this exists: real ciphertexts, never synthetic blobs + +EQL is searchable encryption. Its correctness claims (`WHERE col = $1` engages an +HMAC index; `WHERE col < $1` engages an ORE comparator) are only meaningful if the +tests run against ciphertexts produced by the *actual* crypto. Hand-curated or +synthetic JSONB blobs would silently diverge from what `cipherstash-client` +actually emits, hiding real bugs. + +The project `CLAUDE.md` states this as a hard requirement: + +> EQL is searchable encryption; tests MUST use real ciphertexts/index terms from +> the actual crypto, never hand-curated or synthetic blobs. Fixtures are +> **generated** by encrypting plaintext through cipherstash-client. + +Consequences that shape the whole pipeline: + +- The generator needs live CipherStash credentials (two pairs, not alternatives): + - `CS_CLIENT_ACCESS_KEY` + `CS_WORKSPACE_CRN` — ZeroKMS auth, via `AutoStrategy`. + - `CS_CLIENT_ID` + `CS_CLIENT_KEY` — client key material, via `EnvKeyProvider`. + (`tasks/fixtures.toml:12-17`; built in `tests/sqlx/src/fixtures/cipherstash.rs:44-53`.) +- Generated fixture SQL is **gitignored** and regenerated on every test run + (`.gitignore:225-230`), so a stale fixture cannot rot in the tree. +- Static/committed fixtures are forbidden as a way to dodge the creds dependency. + There is exactly **one** committed exception, `tests/sqlx/fixtures/v3_ste_vec.sql`, + pending a SteVec-document generator — a gap, not a pattern to copy. + +The plaintext **values** are single-sourced in the Rust catalog +`crates/eql-scalars` so the test oracle (the expected-result computation) and the +encrypted fixture can never drift apart — they are derived from the same `CATALOG` +row. + +--- + +## 2. End-to-end pipeline + +```mermaid +flowchart TD + subgraph catalog["crates/eql-scalars (zero-dep catalog)"] + SPEC["ScalarSpec rows in CATALOG
token + kind + domains + fixtures
lib.rs:301-427"] + FIX["Fixture value lists
INT4_FIXTURES, NUMERIC_FIXTURES, …
lib.rs:242-299"] + MAC["int_values! / text_values! macros
materialise typed const slices
lib.rs:439-510"] + VALS["INT4_VALUES: &[i32]
INT2_VALUES, INT8_VALUES, TEXT_VALUES
lib.rs:476-510"] + SPEC --> FIX --> MAC --> VALS + end + + subgraph harness["tests/sqlx (harness)"] + ST["ScalarType::fixture_values()
scalar_domains.rs:53"] + TEMP["temporal_values! / hand impls
date/timestamptz/numeric → LazyLock>
scalar_domains.rs:192-468"] + MOD["scalar_fixture! → spec()
FixtureSpec builder per type
scalar_fixture.rs:174-194"] + VALS --> ST + SPEC --> TEMP --> ST + ST --> MOD + end + + subgraph gen["fixture-gen entry point (needs creds + DB)"] + ALL["generate_all test
iterates CATALOG
generate_all_fixtures.rs:25-62"] + DISP["generate_for_token(token)
dispatch per token"] + RUN["FixtureSpec::run()
driver.rs:122-181"] + ENC["cipherstash::encrypt_store()
batched eql::encrypt_eql
cipherstash.rs:156-218"] + ZK["ZeroKMS / ScopedCipher
real crypto round trip"] + ALL --> DISP --> RUN --> ENC --> ZK + MOD --> RUN + end + + subgraph out["output + consumption"] + SQL["tests/sqlx/fixtures/eql_v2_<T>.sql
(gitignored INSERT scripts)"] + SUITE["SQLx matrix suites
#[sqlx::test(fixtures(scripts(...)))]"] + ZK --> SQL + SQL --> SUITE + ST -. "oracle: expected_forward()" .-> SUITE + end +``` + +Step by step: + +1. **`mise run test:sqlx:prep`** (`mise.toml:49-80`) builds EQL, copies it into the + SQLx migrations, runs `sqlx migrate run`, then calls `mise run fixture:generate:all`. +2. **`fixture:generate:all`** (`tasks/fixtures.toml:1-29`) runs the gated test: + `cargo test --features fixture-gen --test generate_all_fixtures generate_all -- --ignored --exact --nocapture`. +3. **`generate_all`** (`generate_all_fixtures.rs:25-62`) iterates `eql_scalars::CATALOG`, + calling `generate_for_token(spec.token)` for each scalar, then generates the three + hand-written non-catalog `v3_*` fixtures. +4. Each token resolves to `fixtures::eql_v2_::spec().run()` via the generated + dispatch (`scalar_types!(fixture_dispatch)`). +5. **`FixtureSpec::run()`** (`driver.rs:122-181`) opens a direct Postgres connection, + creates a transient working table, batch-encrypts every plaintext via + `cipherstash-client`, inserts the ciphertexts as `jsonb`, renders them as committed + `INSERT` statements with server-side `format('%L', ...)` escaping, drops the working + table, and writes `tests/sqlx/fixtures/.sql`. +6. The SQLx suites load those scripts with `#[sqlx::test(fixtures(scripts("eql_v2_int4")))]` + and compare query results against the Rust oracle (`expected_forward`, which iterates + the **same** `fixture_values()`). + +--- + +## 3. CATALOG values → macro → consumers + +```mermaid +flowchart LR + ROW["INT4 ScalarSpec
token=int4, kind=I32
fixtures=INT4_FIXTURES
lib.rs:301-306"] + LIST["INT4_FIXTURES: &[Fixture]
Min, N(-100), …, Max
lib.rs:244-246"] + IV["int_values!(INT4_VALUES, i32, INT4)
const-eval resolve + range check
lib.rs:476"] + CONST["INT4_VALUES: &[i32]
(committed source of truth,
NOT a generated .rs)"] + + ROW --> LIST + LIST --> IV --> CONST + + CONST --> ST["i32: ScalarType
fixture_values() = INT4_VALUES
scalar_domains.rs (via macro)"] + + ST --> ORACLE["expected_forward(op, pivot)
test ground truth
scalar_domains.rs:109-128"] + ST --> SPECB["scalar_fixture! spec()
.with_values(INT4_VALUES)
scalar_fixture.rs:178-183"] + SPECB --> ENCIN["encrypt_store(values)
encryption input"] + + ORACLE -. "must agree" .-> ENCIN +``` + +The key invariant: the **oracle** (`expected_forward` filtering `fixture_values()`) +and the **encryption input** (`spec().values()` fed to `encrypt_store`) are the same +`&[i32]` const. The catalog comment makes this explicit (`lib.rs:429-435`): + +> This is the **single-sourced** plaintext list the SQLx test matrix reads via +> `ScalarType::fixture_values()` and the fixture generator encrypts — derived from +> the same `CATALOG` row that drives SQL generation, so the oracle cannot drift +> from the fixture. + +### Value materialization is per-kind + +| Kind | Catalog literal | Materialized via | Result type | +|------|-----------------|------------------|-------------| +| integers (`int4`/`int2`/`int8`) | `Fixture::Int` / `Min`/`Max`/`Zero` | `int_values!` macro, **`const`** (`lib.rs:439-478`) | `&'static [iN]` | +| `text` | `Fixture::Text(&str)` | `text_values!` macro, **`const`** (`lib.rs:486-510`) | `&'static [&str]` | +| `date` / `timestamptz` | `Fixture::Date`/`Timestamptz(&str)` | `temporal_values!` → `LazyLock>` (`scalar_domains.rs:192-320`) | `&'static [chrono::…]` | +| `numeric` | `Fixture::Numeric(&str)` | hand-written `LazyLock>` (`scalar_domains.rs:449-468`) | `&'static [Decimal]` | + +Integers and text can be `const` slices; chrono and `rust_decimal` constructors +are not `const`, so those parse the catalog strings once into a `LazyLock>`. +The catalog itself stays **zero-dep** (no chrono, no rust_decimal) — parsing happens +in the harness, not in `eql-scalars`. The `int_values!` macro does a compile-time +range check so an out-of-range literal is a build error, not a silent `as` truncation +(`lib.rs:456-462`). + +--- + +## 4. Worked example: `int4` from catalog row to SQL INSERT + +### 4a. The catalog row and value list + +`crates/eql-scalars/src/lib.rs:244-246`: + +```rust +const INT4_FIXTURES: &[Fixture] = fixtures!(int i32; + Min, N(-100), N(-1), Zero, N(1), N(2), N(5), N(10), N(17), N(25), + N(42), N(50), N(100), N(250), N(1000), N(9999), Max); +``` + +`lib.rs:301-306`: + +```rust +const INT4: ScalarSpec = ScalarSpec { + token: "int4", + kind: ScalarKind::I32, + domains: ORDERED_INT_DOMAINS, + fixtures: INT4_FIXTURES, +}; +``` + +The `fixtures!` macro range-checks every `N(..)` literal against `i32` at its +definition site (`lib.rs:191-193`), and `Min`/`Max`/`Zero` are resolved to the +kind's bounds. + +### 4b. Materialized into a typed const + +`lib.rs:476`: + +```rust +int_values!(INT4_VALUES, i32, INT4); +``` + +The macro (`lib.rs:439-474`) walks `INT4.fixtures`, calls `numeric_value(kind)` to +resolve each `Fixture` (`Min` → `i32::MIN`, `N(-100)` → `-100`, `Zero` → `0`, …), and +const-evaluates the whole thing into `pub const INT4_VALUES: &[i32]`. So +`INT4_VALUES == [-2147483648, -100, -1, 0, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999, 2147483647]`. + +### 4c. Wired into the harness and the spec + +`i32` gets its `impl ScalarType` from the `scalar_types!(scalar_type_impls)` list +(`tests/sqlx/src/scalar_types.rs:54`, `int4 => i32`), with +`fixture_values() = eql_scalars::INT4_VALUES`. The fixture module `eql_v2_int4` is +emitted by `scalar_types!(fixture_modules)` (`fixtures/mod.rs:53`), expanding the +`scalar_fixture!(int, "eql_v2_int4", i32, eql_scalars::INT4_VALUES)` builder: + +`tests/sqlx/src/fixtures/scalar_fixture.rs:178-194`: + +```rust +pub fn spec() -> $crate::fixtures::FixtureSpec<'static, $ty> { + $crate::fixtures::FixtureSpec::new($name) + $(.with_index($crate::fixtures::IndexKind::$ix))+ // Unique, Ore for int + .with_column_type("jsonb") + .with_values($values) // INT4_VALUES +} +``` + +`IndexKind::Unique` drives the `hm` (HMAC) equality term; `IndexKind::Ore` drives +the `ob` (ORE block) ordering term. + +### 4d. Plaintext → cipherstash-client → ciphertext + +`FixtureSpec::run()` calls `insert_direct` (`driver.rs:194-223`), which builds a +`ColumnConfig` and batch-encrypts: + +`tests/sqlx/src/fixtures/cipherstash.rs:156-218` — `encrypt_store`: + +```rust +let prepared: Vec = values + .iter() + .map(|value| PreparedPlaintext::new( + Cow::Borrowed(config), + Identifier::new(table, column), // table = "_fixture_eql_v2_int4" + value.to_plaintext(), // i32 → Plaintext::Int(Some(n)) + EqlOperation::Store, + )) + .collect(); +let outputs = encrypt_eql(cipher, prepared, &opts).await?; // ONE ZeroKMS round trip +``` + +`value.to_plaintext()` is the `EqlPlaintext` lift — for `i32` it is +`Plaintext::Int(Some(*self))` (`eql_plaintext.rs:152-158`). The `Cast::INT` maps to +`ColumnType::Int` (`cipherstash.rs:92-110`), and the `Unique`+`Ore` indexes map to +the unique + ORE `IndexType`s (`cipherstash.rs:118-135`). `EqlOperation::Store` +yields a full storage payload `{"k":"ct","v":2,"i":…,"c":…,"hm":…,"ob":…}`. + +The cipher is built once per process (`build_cipher`, `cipherstash.rs:44-53`): +`ZeroKMSBuilder::auto()` + `EnvKeyProvider` + `ScopedCipher::init_default` — this is +where the four `CS_*` env vars are consumed. + +### 4e. The rendered INSERT + +The driver inserts each ciphertext into the transient `public._fixture_eql_v2_int4` +working table as plain `jsonb`, then renders committed rows with server-side literal +escaping (`spec.rs:180-190`, `render_rows_sql` using `format('%L', ...)`). The +preamble (`spec.rs:149-172`) is prepended, and the file is written to +`tests/sqlx/fixtures/eql_v2_int4.sql` (`driver.rs:176-179`). + +A real generated row (from the current gitignored `eql_v2_int4.sql`, abridged) — note +`id=1` is `Min` = `-2147483648`, carrying the real `hm` HMAC term and `ob` ORE block +array: + +```sql +INSERT INTO fixtures.eql_v2_int4 (id, plaintext, payload) VALUES ('1', '-2147483648', + '{"c": "mBbK?@VVW…", "i": {"c": "payload", "t": "_fixture_eql_v2_int4"}, + "k": "ct", "v": 2, + "hm": "f87e361e4ac3502898add25074bec82a66aa4ad563c9bb21aa89fd7b23e2a649", + "ob": ["a1a1a1a1a1a1a1a165cecfeb3421313b…"]}'::jsonb); +``` + +The `plaintext` column (`-2147483648`) is the committed oracle; the `payload` is the +real encrypted document. At test time `fetch_fixture_payload` (`scalar_domains.rs:726-743`) +looks up a payload by plaintext, and `assert_scalar_plaintexts` (`scalar_domains.rs:763-778`) +compares the DB query result against `expected_forward` over `INT4_VALUES`. + +--- + +## 5. Non-catalog `v3_*` fixtures (same pipeline, different shape) + +`generate_all` also runs three hand-written fixtures that aren't `CATALOG` scalars +(`generate_all_fixtures.rs:37-61`): + +- **`v3_ste_vec`** — a SteVec JSONB document fixture. **The one committed + exception** (`.gitignore` re-lists it but `CLAUDE.md` documents it as a gap pending + a SteVec-document generator). A hand-written `FixtureSpec` riding + the same `run()` pipeline. +- **`v3_doc_int4`** — a scalar-shaped SteVec document, one `{"field": }` per + `INT4_VALUES`. A **split** fixture: the encryption input is the jsonb document but + the plaintext oracle column is the bare `int4`, so it uses the + `run_with_payloads` seam (`driver.rs:242-303`) instead of `run()`. +- **`v3_numeric_collision`** — the `[1, 1.0, 2]` scale-equivalence collision + (`tests/sqlx/src/fixtures/v3_numeric_collision.rs`). It cannot live in the + catalog `eql_v2_numeric` fixture because the catalog distinctness guard + (`numeric_value_guards::fixtures_are_distinct_by_value`, `scalar_domains.rs:508-523`) + forbids the value-equal pair `1`/`1.0`. Encrypted at numeric ORE width via the + standard `.run()` driver, addressed by `id` rather than `plaintext` (since + `WHERE plaintext = 1` would match both). + +These ride the **same** `FixtureSpec` → `encrypt_store` → SQL pipeline; they are +registered directly in `fixtures/mod.rs:29-47` rather than via the `scalar_types!` +list, which only enumerates catalog scalars. + +--- + +## 6. Credentials & the real-crypto requirement (why static is forbidden) + +The generator is gated behind the `fixture-gen` cargo feature and `#[ignore]` +(`generate_all_fixtures.rs:14,26` and `scalar_fixture.rs:188-193`), so a plain +`cargo test` never compiles or runs it — and never needs credentials. It runs only +through the prep flow, which requires: + +- A live Postgres with EQL installed (`mise run postgres:up`). +- **Both** CipherStash credential pairs in the environment (`tasks/fixtures.toml:12-17`): + - `CS_CLIENT_ACCESS_KEY` + `CS_WORKSPACE_CRN` → ZeroKMS auth (`AutoStrategy`). + - `CS_CLIENT_ID` + `CS_CLIENT_KEY` → client key (`EnvKeyProvider`). + These are roles, not alternatives — auth and key material are separate. + +Why not just commit the SQL and skip the creds? Because the ciphertexts must be the +*actual* output of the crypto. `CLAUDE.md`: + +> Do NOT add static/committed fixtures to dodge the creds dependency. The one +> committed exception, `tests/sqlx/fixtures/v3_ste_vec.sql`, is a gap pending a +> SteVec-document generator … not a pattern to copy. + +The gitignore enforces it mechanically (`.gitignore:225-230`): every +`eql_v2*` fixture plus `v3_doc_int4.sql` and `v3_numeric_collision.sql` are ignored +and regenerated on every `mise run test:sqlx`. A stale or hand-edited fixture can't +survive a run. The live round-trip is additionally smoke-tested by the +`#[ignore]` `live_tests` in `cipherstash.rs:309-412`, which assert the real Store +payload shape (`v=2`, non-null `hm`/`ob`/`c`/`i`) and that distinct plaintexts yield +distinct `hm` terms — so an SDK API drift surfaces there before the whole pipeline +breaks. + +--- + +## File reference index + +| Concern | Path | +|---------|------| +| Catalog rows, fixture lists, `int_values!`/`text_values!` | `crates/eql-scalars/src/lib.rs:184-510` | +| `generate_all` entry point | `tests/sqlx/tests/generate_all_fixtures.rs:25-62` | +| `fixture:generate:all` mise task | `tasks/fixtures.toml:1-29` | +| `test:sqlx:prep` flow | `mise.toml:49-80` | +| `ScalarType` trait + `fixture_values()` + oracle | `tests/sqlx/src/scalar_domains.rs:17-128` | +| `temporal_values!` (date/timestamptz), numeric/text impls | `tests/sqlx/src/scalar_domains.rs:192-468` | +| `scalar_types!` harness token list | `tests/sqlx/src/scalar_types.rs:39-63` | +| `scalar_fixture!` (spec builder + generator test) | `tests/sqlx/src/fixtures/scalar_fixture.rs` | +| `FixtureSpec` builder + SQL renderers | `tests/sqlx/src/fixtures/spec.rs` | +| `FixtureSpec::run()` / `run_with_payloads()` driver | `tests/sqlx/src/fixtures/driver.rs` | +| `encrypt_store` + `build_cipher` (cipherstash-client) | `tests/sqlx/src/fixtures/cipherstash.rs` | +| `EqlPlaintext` (`to_plaintext`, `Cast`) | `tests/sqlx/src/fixtures/eql_plaintext.rs` | +| Non-catalog fixtures | `tests/sqlx/src/fixtures/{v3_ste_vec,v3_doc_int4,v3_numeric_collision}.rs` | +| Gitignore enforcement | `.gitignore:225-230` | diff --git a/docs/walkthroughs/sql-code-generator.md b/docs/walkthroughs/sql-code-generator.md new file mode 100644 index 00000000..3a25489d --- /dev/null +++ b/docs/walkthroughs/sql-code-generator.md @@ -0,0 +1,410 @@ +# The EQL Scalar SQL Code Generator + +A walkthrough of the machinery that turns the Rust catalog (`crates/eql-scalars`) into the +generated `eql_v3` encrypted-domain SQL surface under `src/v3/scalars//`, driven by +`crates/eql-codegen`. + +> Scope: this covers the **SQL generation path** only — `cargo run -p eql-codegen` (which +> `mise run build` invokes). The fixture-generation path (`generate_all_fixtures`, the +> `fixture-gen` feature) is out of scope here. + +--- + +## 1. What it does and why it exists + +EQL v3 exposes **encrypted-domain types** — jsonb-backed PostgreSQL `DOMAIN`s in the `eql_v3` +schema, one per operator/index capability (e.g. `eql_v3.int4`, `eql_v3.int4_eq`, +`eql_v3.int4_ord`, `eql_v3.int4_ord_ore`). Each domain carries: + +- a `CREATE DOMAIN ... CHECK (...)` validating the encrypted JSONB envelope, +- inlinable **extractor** functions (`eq_term`, `ord_term`) projecting the index term out of + the payload, +- inlinable **comparison wrappers** (`eq`/`neq`/`lt`/…) for the operators the domain supports, +- **blocker** functions for every native jsonb operator the domain must *not* answer + (`->`, `@>`, `||`, …) so a domain-fallback can never silently return a wrong answer, +- `CREATE OPERATOR` bindings and (for ordered domains) `min`/`max` aggregates. + +Hand-writing this surface per scalar type is repetitive and error-prone — the operator +matrix is ~20 operators × multiple signatures × multiple domains. The generator makes the +**Rust `CATALOG` the single source of truth**: one `ScalarSpec` row per type declares the +token, the native kind, the domain suffixes, and their index terms; everything else is +rendered deterministically. The generated `*_types.sql` / `*_functions.sql` / `*_operators.sql` +/ `*_aggregates.sql` files are **gitignored and never committed** — they are reproduced from +the catalog at the start of every build (`tasks/build.sh:33`). + +A committed *reference* copy (with a `-- REFERENCE:` provenance line) lives under +`tests/codegen/reference//` and is byte-compared against generator output by the parity +gate (`crates/eql-codegen/tests/parity.rs`), so a renderer change that drifts from the +reviewed baseline fails CI. + +--- + +## 2. End-to-end pipeline + +```mermaid +flowchart TD + subgraph catalog["crates/eql-scalars (catalog = source of truth)"] + SPEC["CATALOG: &[ScalarSpec]
lib.rs:427
(INT4, INT2, INT8, DATE,
TIMESTAMPTZ, NUMERIC, TEXT)"] + TERM["Term enum + impls
term.rs
(json_key, extractor, ctor,
role, operators, requires)"] + end + + subgraph codegen["crates/eql-codegen"] + MAIN["main.rs
no args -> generate_all
'list-types' -> print tokens"] + GEN["generate.rs
generate_all / generate_type
render_*_file"] + OPS["operator_surface.rs
OPERATORS: 20 ops
+ signatures + metadata"] + CTX["context.rs
minijinja env + serde
context structs + helpers"] + TPL["templates/*.sql.j2
(include_str!, compiled-in)"] + WRITER["writer.rs
ownership-guarded write
(AUTO-GENERATED marker)"] + end + + SPEC --> MAIN + TERM --> GEN + MAIN --> GEN + GEN --> OPS + GEN --> CTX + CTX --> TPL + GEN --> WRITER + + WRITER -->|"writes gitignored"| OUT["src/v3/scalars/<T>/
*_types.sql
*_functions.sql
*_operators.sql
*_aggregates.sql"] + + OUT --> BUILD["mise run build (tasks/build.sh)
tsort -- REQUIRE: edges
concatenate -> release/*.sql"] + + OUT -.->|"byte-compare
(modulo -- REFERENCE: line)"| REF["tests/codegen/reference/<T>/
committed baseline"] + REF -.->|"parity gate"| PARITY["tests/parity.rs"] +``` + +The build calls `cargo run -p eql-codegen` with **no args**, which runs `generate_all(repo_root())` +(`main.rs:18-28`, `generate.rs:249`). `generate_all` iterates `eql_scalars::CATALOG` and calls +`generate_type` per row, writing into `src/v3/scalars//`. The `list-types` subcommand +(`main.rs:11-16`) prints the catalog tokens one per line — consumed by the fixtures/matrix +enumeration, not by SQL generation. + +--- + +## 3. Core data model + +```mermaid +classDiagram + class ScalarSpec { + +token: &str // "int4" + +kind: ScalarKind + +domains: &[DomainSpec] + +fixtures: &[Fixture] + +domain_name(domain) String + +is_eq_only() bool + } + class DomainSpec { + +suffix: &str // "" / "_eq" / "_ord" / "_ord_ore" / "_match" + +terms: &[Term] + +name_with_token(token) String + } + class ScalarKind { + <> + I16 / I32 / I64 + Numeric / Text / Jsonb + Date / Timestamptz + +as_bounded_int() Option~BoundedIntKind~ + +is_int() / is_temporal() / is_text() + } + class Term { + <> + Hm / Ore / Bloom + +json_key() &str // hm / ob / bf + +extractor() &str // eq_term / ord_term / match_term + +ctor() &str // hmac_256 / ore_block_256 / bloom_filter + +role() Role + +operators() &[&str] + +requires() &[&str] + } + class Role { + <> + Storage / Eq / Ord / Match + +rank() u8 // Ord>Eq>Match>Storage + } + ScalarSpec --> "1" ScalarKind : kind + ScalarSpec --> "*" DomainSpec : domains + ScalarSpec --> "*" Fixture : fixtures + DomainSpec --> "*" Term : terms + Term --> Role : role() +``` + +Key facts, with the impl locations: + +- **`ScalarSpec`** (`eql-scalars/src/lib.rs:170`) is "one row" = one scalar type. `domain_name` + / `is_eq_only` are in `spec.rs:20,28`. +- **`DomainSpec`** (`lib.rs:161`) is one generated public domain: a `suffix` appended to the + token plus the fixed index `terms` it carries. `name_with_token` (`spec.rs:12`) is the *single* + place token+suffix concatenation happens, making "domain name starts with token" structural. +- **`Term`** (`lib.rs:82`) is the index-term vocabulary. Its capability methods + (`term.rs:8-66`) are the **cross-schema SQL contract**: `Hm` → key `hm`, extractor `eq_term`, + ctor `hmac_256`, operators `= <>`; `Ore` → key `ob`, extractor `ord_term`, ctor + `ore_block_256`, operators `= <> < <= > >=`; `Bloom` → key `bf`, extractor `match_term`, + operators `@> <@`. Term capabilities are fixed in code (with unit tests), not free-form data — + an undefined term is a compile error. +- **`Role`** (`lib.rs:96`) is resolved from a domain's terms by `Term::role_for_terms` + (`term.rs:128`) using `Role::rank` precedence. `Ore` ⇒ `Ord`, which is what gates aggregate + generation. + +The four ordered-integer domains are shared via one const (`lib.rs:203` `ORDERED_INT_DOMAINS`): +`""` (storage, no terms), `_eq` (`[Hm]`), `_ord_ore` (`[Ore]`), `_ord` (`[Ore]`). `int4`/`int2`/ +`int8`/`date`/`timestamptz`/`numeric` all reuse it; `text` (`lib.rs:377` `TEXT_DOMAINS`) adds a +`_match` domain carrying `[Bloom]`. The catalog itself is `lib.rs:427`. + +--- + +## 4. Worked example: `int4` + +### 4.1 The catalog row + +```rust +// crates/eql-scalars/src/lib.rs:301 +const INT4: ScalarSpec = ScalarSpec { + token: "int4", + kind: ScalarKind::I32, + domains: ORDERED_INT_DOMAINS, // "", "_eq", "_ord_ore", "_ord" + fixtures: INT4_FIXTURES, +}; +``` + +`generate_type` (`generate.rs:206`) computes the target file set: one `int4_types.sql`, plus +`_functions.sql` + `_operators.sql` for **each** domain, plus `_aggregates.sql` only for +ord-capable domains (`is_ord_capable`, `context.rs:276`). For `int4` that's **11 files** — the +two ordered domains (`_ord`, `_ord_ore`) get aggregates; storage and `_eq` do not +(`generate.rs:213`, and the test at `generate.rs:415` asserts `written.len() == 11`). + +### 4.2 `int4_types.sql` — one DO block, all four domains + +`render_types_file` (`generate.rs:42`) maps each domain through `domain_block` (`context.rs:72`), +which prepends the always-present `ENVELOPE_KEYS` (`v`, `i`, `c` — `consts.rs:19`) and then the +domain's term keys (`Term::term_json_keys`, e.g. `hm` for `_eq`, `ob` for `_ord`). The +`types.sql.j2` template emits an idempotent `IF NOT EXISTS` guard per domain. Rendered +(`tests/codegen/reference/int4/int4_types.sql:30`): + +```sql +CREATE DOMAIN eql_v3.int4_eq AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' + AND VALUE ? 'i' + AND VALUE ? 'c' + AND VALUE ? 'hm' -- the Hm term's json_key + AND VALUE->>'v' = '2' + ); +``` + +The `_ord`/`_ord_ore` domains are identical but with `AND VALUE ? 'ob'` (the `Ore` json_key). +Note `CREATE DOMAIN ... AS jsonb` — the base type is always `jsonb`, never another domain +(see invariant §5). + +### 4.3 `int4_eq_functions.sql` — extractor + wrappers + blockers + +`render_functions_file` (`generate.rs:75`) builds three kinds of `FnEntry` (`context.rs:102`): + +1. **Extractor** — one per distinct extractor among the domain's terms + (`Term::extractor_terms`). For `_eq`, the `Hm` term yields `eq_term` + (`extractor_entry`, `context.rs:139`). Rendered via `functions/extractor.sql.j2` + (`int4_eq_functions.sql:14`): + + ```sql + CREATE FUNCTION eql_v3.eq_term(a eql_v3.int4_eq) + RETURNS eql_v3.hmac_256 + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE + AS $$ SELECT eql_v3.hmac_256(a::jsonb) $$; + ``` + + The `RETURNS` type and the body's constructor are the *same* `SCHEMA.ctor()` name kept in + lockstep (`context.rs:139-145`). + +2. **Wrapper** — for each operator in `OPERATORS` (`operator_surface.rs:230`) that the domain's + terms support (`Term::operators_for_terms`), and only when an extractor backs it + (`Term::extractor_for_operator`). `_eq` supports `=` (`eq`) and `<>` (`neq`). The body casts a + bare `jsonb` operand to the domain before extracting (`extract_arg`, `context.rs:242`). + Rendered via `functions/wrapper.sql.j2` (`int4_eq_functions.sql:23,31`): + + ```sql + CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b eql_v3.int4_eq) + RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE + AS $$ SELECT eql_v3.eq_term(a) = eql_v3.eq_term(b) $$; + + CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b jsonb) + RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE + AS $$ SELECT eql_v3.eq_term(a) = eql_v3.eq_term(b::eql_v3.int4_eq) $$; + ``` + +3. **Unsupported (blocker)** — every operator/signature the domain does *not* support + (`unsupported_entry`, `context.rs:178`) gets a function whose body always `RAISE`s. + Rendered via `functions/unsupported.sql.j2`: + + ```sql + CREATE FUNCTION eql_v3.lt(a eql_v3.int4_eq, b eql_v3.int4_eq) + RETURNS boolean IMMUTABLE PARALLEL SAFE + AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '<', 'eql_v3.int4_eq'; END; $$ + LANGUAGE plpgsql; + ``` + + The `_eq` file has 45 `CREATE FUNCTION` (1 extractor + 4 supported wrappers + 40 blockers); + the storage `""` file is **all 44 blockers** (no extractor, no supported operator). These + counts are pinned by tests at `generate.rs:439,453`. + +For an **ordered** domain (`int4_ord`), the same machinery produces `ord_term` +(`RETURNS eql_v3.ore_block_256`, `int4_ord_functions.sql:15`) and the six comparison wrappers +(`eq`/`neq`/`lt`/`lte`/`gt`/`gte`), e.g. `int4_ord_functions.sql:72`: + +```sql +CREATE FUNCTION eql_v3.lt(a eql_v3.int4_ord, b eql_v3.int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v3.ord_term(a) < eql_v3.ord_term(b) $$; +``` + +### 4.4 `int4_eq_operators.sql` — CREATE OPERATOR bindings + +`render_operators_file` (`generate.rs:130`) emits a `CREATE OPERATOR` for every +operator/signature (`operator_entry`, `context.rs:210`). Planner metadata +(`COMMUTATOR`/`NEGATOR`/`RESTRICT`/`JOIN`) is emitted **only when the domain supports that +operator** — a blocker-bound operator gets the binding but no selectivity hints +(`context.rs:211-215`). The emission order is fixed and load-bearing for the byte-match +(`operator_surface.rs:42-57`). Each domain file has 44 `CREATE OPERATOR` (`generate.rs:484`). +Example (`int4_ord_operators.sql:10`): + +```sql +CREATE OPERATOR = ( + FUNCTION = eql_v3.eq, + LEFTARG = eql_v3.int4_ord, RIGHTARG = eql_v3.int4_ord, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +``` + +### 4.5 `int4_ord_aggregates.sql` — min/max (ord-only) + +`render_aggregates_file` (`generate.rs:172`) returns `None` unless the domain `is_ord_capable` +(carries a term whose `role()` is `Role::Ord`, i.e. `Ore`). So storage and `_eq` get *no* +aggregates file; `_ord`/`_ord_ore` do (`generate.rs:490-494`). It iterates `AGGREGATE_OPS` +(`context.rs:260`: `min`/`<`, `max`/`>`) and emits a state function + `CREATE AGGREGATE` each: + +```sql +CREATE FUNCTION eql_v3.min_sfunc(state eql_v3.int4_ord, value eql_v3.int4_ord) +RETURNS eql_v3.int4_ord +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ BEGIN IF value < state THEN RETURN value; END IF; RETURN state; END; $$; +``` + +### 4.6 REQUIRE edges + +Every generated file leads with the `-- AUTOMATICALLY GENERATED FILE.` marker followed by +`-- REQUIRE:` edges that the build's `tsort` (`tasks/build.sh`) uses to order concatenation. +`functions_requires` (`generate.rs:60`) always pulls `src/v3/schema.sql`, the type file, and the +shared blocker helper `src/v3/scalars/functions.sql`, then appends each term's `requires()` — +for `_eq` that's `src/v3/sem/hmac_256/functions.sql`, for `_ord` the two +`ore_block_256` files (`term.rs:56-65`). Visible at `int4_eq_functions.sql:3-6`. + +--- + +## 5. Enforced invariants and footguns + +The generator's renderers *structurally* enforce a set of correctness rules. These are not +style preferences — each one prevents a specific way an encrypted domain could silently leak or +return a wrong answer. Guard tests live in `generate.rs:531-603`. + +### Blockers must be `LANGUAGE plpgsql`, never `LANGUAGE sql` +A blocker exists only to `RAISE`. A `LANGUAGE sql` body is **inlinable**, and the planner can +*elide* an inlined call whose result is provably unused (a dead `CASE` branch, a folded +predicate) — the `RAISE` would never run and the "operator not supported" guarantee would +silently evaporate. `LANGUAGE plpgsql` is opaque to the planner, so the body always executes. +The `unsupported.sql.j2` template hard-codes `LANGUAGE plpgsql` (template line 8); the test +`blockers_are_never_strict_and_always_plpgsql` (`generate.rs:531`) asserts +`CREATE FUNCTION count == LANGUAGE plpgsql count` in the all-blocker storage file. + +### Blockers must never be `STRICT` +A `STRICT` function returns `NULL` on any `NULL` argument **without running its body** — which +would bypass the `RAISE` entirely on a `NULL` input. The blocker template emits +`IMMUTABLE PARALLEL SAFE` with **no `STRICT`** (template line 6); the same guard test asserts the +rendered blocker SQL contains no `STRICT`. + +### Inlinable functions need `LANGUAGE sql` + single `SELECT` + `IMMUTABLE` + **no `SET`** +The extractors and comparison wrappers must inline so the planner can structurally match a bare +`WHERE col = $1` against the functional index on the extractor. Inlining requires a single-statement +SQL `SELECT`, `IMMUTABLE`, and crucially **no `SET` clause** — a pinned `search_path` disables +inlining. The `extractor.sql.j2`/`wrapper.sql.j2` templates emit exactly +`LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE` with no `SET`; the test +`inlinable_functions_have_no_set_search_path` (`generate.rs:546`) verifies no `LANGUAGE sql` +block pins `search_path`. + +### Aggregate state functions are `LANGUAGE plpgsql` (the inverse) +`min_sfunc`/`max_sfunc` carry a `SET search_path` and procedural `IF` — they are deliberately +**not** inlinable (`aggregates.sql.j2:14-15`). The test +`aggregate_state_functions_are_plpgsql_not_inlinable` (`generate.rs:563`) pins this. + +### No domain-over-domain +Every domain is `CREATE DOMAIN eql_v3. AS jsonb` (`types.sql.j2:15`), never +`AS `. Operators resolve against the *ultimate base type* (`jsonb`), so a +derived domain would not inherit the base domain's operator surface and the blockers would stop +engaging. The single base type is the fixed `jsonb` slot in the template. + +### No operator class on a domain +The generator emits `CREATE OPERATOR` bindings and (for indexing) relies on a functional index +over the extractor (`eq_term`/`ord_term`), whose **return type** (`eql_v3.hmac_256` / +`eql_v3.ore_block_256`) already carries a default opclass. No `CREATE OPERATOR CLASS` is +generated for the domain itself. + +### Operators are bound even when unsupported +`render_operators_file` emits a `CREATE OPERATOR` for *every* operator on *every* domain, but +only attaches planner metadata when supported (`context.rs:210`). The binding ensures the +operator resolves to the blocker (which raises) rather than falling through to a native jsonb +operator that would silently compute on ciphertext. + +### Doxygen tags survive +Each generated function/aggregate keeps `@brief`/`@param`/`@return` tags (templates) — the test +`generated_function_like_docs_keep_required_tags` (`generate.rs:580`) asserts one `@return` per +`CREATE FUNCTION`/`CREATE AGGREGATE`, so `mise run docs:validate` stays green. + +--- + +## 6. Determinism and the AUTO-GENERATED marker + +**Determinism.** An identical `CATALOG` produces **byte-identical** SQL. The generator iterates +ordered `&[...]` slices everywhere (`CATALOG`, `domains`, `OPERATORS`, `AGGREGATE_OPS`) — no +`HashMap`/`HashSet` iteration leaks into a renderer. `Term` dedupe is order-preserving +(`term.rs:69` `dedupe_preserving_order`, first occurrence wins). The +`generate_all_is_deterministic_across_runs` test (`parity.rs:136`) runs the generator twice into +separate temp dirs and byte-compares every file. minijinja keeps each template's trailing +newline (`context.rs:14` `set_keep_trailing_newline(true)`) so the output ends in exactly one. + +**The AUTO-GENERATED marker.** Every generated file's first line is +`-- AUTOMATICALLY GENERATED FILE.` (`consts.rs:7` `AUTO_GENERATED_MARKER`, emitted by each +template as line 1). It serves three roles: + +1. **Ownership / clobber safety.** `writer.rs` only overwrites or cleans files whose first line + is the marker (`is_generated`, `writer.rs:36`; `clean_generated_files`, `writer.rs:42`; + `ensure_generated_paths_writable`, `writer.rs:63`). A hand-written file (e.g. + `_extensions.sql`, which has no marker) is never clobbered — the run errors out instead. + `write_generated_file` (`writer.rs:80`) *re-validates* the marker on the rendered body before + writing, so a renderer bug that dropped the marker can't produce an unowned file. +2. **Doc-validation skip.** `tasks/docs/validate/*.sh` grep this marker to skip generated SQL + (`consts.rs:32` test pins the marker to the grep). +3. **Parity baseline.** The committed reference files under `tests/codegen/reference//` + carry one extra `-- REFERENCE:` provenance line *above* the marker; the parity gate strips it + and byte-compares the rest (`parity.rs:71` `reference_body`). `reference_dirs_match_catalog_tokens` + (`parity.rs:78`) ensures the reference dir set equals the catalog token set — a new catalog + type with no reference, or a stale reference with no catalog row, fails CI. + +--- + +## 7. Where to look + +| Concern | File | +| --- | --- | +| Catalog rows / `CATALOG` | `crates/eql-scalars/src/lib.rs:301-427` | +| `ScalarSpec` / `DomainSpec` defs + impls | `crates/eql-scalars/src/lib.rs:160-176`, `spec.rs` | +| `Term` capability contract | `crates/eql-scalars/src/term.rs` | +| `ScalarKind` / `Role` | `crates/eql-scalars/src/kind.rs`, `lib.rs:96-130` | +| Entry point / `list-types` | `crates/eql-codegen/src/main.rs` | +| Orchestrator + render functions | `crates/eql-codegen/src/generate.rs` | +| minijinja env + serde context | `crates/eql-codegen/src/context.rs` | +| Operator surface (20 ops) | `crates/eql-codegen/src/operator_surface.rs` | +| Marker + schema + escaping | `crates/eql-codegen/src/consts.rs` | +| Ownership-guarded writer | `crates/eql-codegen/src/writer.rs` | +| Templates | `crates/eql-codegen/templates/*.sql.j2` | +| Parity gate | `crates/eql-codegen/tests/parity.rs` | +| Committed reference baseline | `tests/codegen/reference//` | +| Build wiring | `tasks/build.sh:33` | From 2c0f90b373b773f0762922481d1c21c21a28f895 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 11:57:59 +1000 Subject: [PATCH 11/12] docs(walkthroughs): fix review findings (counts, ste_vec framing, line refs) --- docs/walkthroughs/fixture-generator.md | 81 ++++++++++++++----------- docs/walkthroughs/sql-code-generator.md | 51 +++++++++------- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/docs/walkthroughs/fixture-generator.md b/docs/walkthroughs/fixture-generator.md index dd7fd960..81427810 100644 --- a/docs/walkthroughs/fixture-generator.md +++ b/docs/walkthroughs/fixture-generator.md @@ -32,10 +32,14 @@ Consequences that shape the whole pipeline: - `CS_CLIENT_ID` + `CS_CLIENT_KEY` — client key material, via `EnvKeyProvider`. (`tasks/fixtures.toml:12-17`; built in `tests/sqlx/src/fixtures/cipherstash.rs:44-53`.) - Generated fixture SQL is **gitignored** and regenerated on every test run - (`.gitignore:225-230`), so a stale fixture cannot rot in the tree. + (`.gitignore:227-230`), so a stale fixture cannot rot in the tree. - Static/committed fixtures are forbidden as a way to dodge the creds dependency. - There is exactly **one** committed exception, `tests/sqlx/fixtures/v3_ste_vec.sql`, - pending a SteVec-document generator — a gap, not a pattern to copy. + Every fixture is generated — including the SteVec document + `tests/sqlx/fixtures/v3_ste_vec.sql`, which is gitignored (`.gitignore:228`) and + rebuilt each run by its own generator (`fixtures::v3_ste_vec::generate()`). + (`CLAUDE.md` still calls `v3_ste_vec.sql` "the one committed exception … pending a + SteVec-document generator", but that note is stale: the generator exists and the file + is gitignored like the rest.) The plaintext **values** are single-sourced in the Rust catalog `crates/eql-scalars` so the test oracle (the expected-result computation) and the @@ -49,16 +53,16 @@ row. ```mermaid flowchart TD subgraph catalog["crates/eql-scalars (zero-dep catalog)"] - SPEC["ScalarSpec rows in CATALOG
token + kind + domains + fixtures
lib.rs:301-427"] - FIX["Fixture value lists
INT4_FIXTURES, NUMERIC_FIXTURES, …
lib.rs:242-299"] - MAC["int_values! / text_values! macros
materialise typed const slices
lib.rs:439-510"] - VALS["INT4_VALUES: &[i32]
INT2_VALUES, INT8_VALUES, TEXT_VALUES
lib.rs:476-510"] + SPEC["ScalarSpec rows in CATALOG
token + kind + domains + fixtures
lib.rs:312-438"] + FIX["Fixture value lists
INT4_FIXTURES, NUMERIC_FIXTURES, …
lib.rs:253-310"] + MAC["int_values! / text_values! macros
materialise typed const slices
lib.rs:450-521"] + VALS["INT4_VALUES: &[i32]
INT2_VALUES, INT8_VALUES, TEXT_VALUES
lib.rs:487-521"] SPEC --> FIX --> MAC --> VALS end subgraph harness["tests/sqlx (harness)"] ST["ScalarType::fixture_values()
scalar_domains.rs:53"] - TEMP["temporal_values! / hand impls
date/timestamptz/numeric → LazyLock>
scalar_domains.rs:192-468"] + TEMP["temporal_values! / hand impls
date/timestamptz/numeric → LazyLock>
scalar_domains.rs:192-495"] MOD["scalar_fixture! → spec()
FixtureSpec builder per type
scalar_fixture.rs:174-194"] VALS --> ST SPEC --> TEMP --> ST @@ -110,9 +114,9 @@ Step by step: ```mermaid flowchart LR - ROW["INT4 ScalarSpec
token=int4, kind=I32
fixtures=INT4_FIXTURES
lib.rs:301-306"] - LIST["INT4_FIXTURES: &[Fixture]
Min, N(-100), …, Max
lib.rs:244-246"] - IV["int_values!(INT4_VALUES, i32, INT4)
const-eval resolve + range check
lib.rs:476"] + ROW["INT4 ScalarSpec
token=int4, kind=I32
fixtures=INT4_FIXTURES
lib.rs:312-317"] + LIST["INT4_FIXTURES: &[Fixture]
Min, N(-100), …, Max
lib.rs:255-257"] + IV["int_values!(INT4_VALUES, i32, INT4)
const-eval resolve + range check
lib.rs:487"] CONST["INT4_VALUES: &[i32]
(committed source of truth,
NOT a generated .rs)"] ROW --> LIST @@ -129,7 +133,7 @@ flowchart LR The key invariant: the **oracle** (`expected_forward` filtering `fixture_values()`) and the **encryption input** (`spec().values()` fed to `encrypt_store`) are the same -`&[i32]` const. The catalog comment makes this explicit (`lib.rs:429-435`): +`&[i32]` const. The catalog comment makes this explicit (`lib.rs:440-446`): > This is the **single-sourced** plaintext list the SQLx test matrix reads via > `ScalarType::fixture_values()` and the fixture generator encrypts — derived from @@ -140,8 +144,8 @@ and the **encryption input** (`spec().values()` fed to `encrypt_store`) are the | Kind | Catalog literal | Materialized via | Result type | |------|-----------------|------------------|-------------| -| integers (`int4`/`int2`/`int8`) | `Fixture::Int` / `Min`/`Max`/`Zero` | `int_values!` macro, **`const`** (`lib.rs:439-478`) | `&'static [iN]` | -| `text` | `Fixture::Text(&str)` | `text_values!` macro, **`const`** (`lib.rs:486-510`) | `&'static [&str]` | +| integers (`int4`/`int2`/`int8`) | `Fixture::Int` / `Min`/`Max`/`Zero` | `int_values!` macro, **`const`** (`lib.rs:450-485`) | `&'static [iN]` | +| `text` | `Fixture::Text(&str)` | `text_values!` macro, **`const`** (`lib.rs:497-519`) | `&'static [&str]` | | `date` / `timestamptz` | `Fixture::Date`/`Timestamptz(&str)` | `temporal_values!` → `LazyLock>` (`scalar_domains.rs:192-320`) | `&'static [chrono::…]` | | `numeric` | `Fixture::Numeric(&str)` | hand-written `LazyLock>` (`scalar_domains.rs:449-468`) | `&'static [Decimal]` | @@ -150,7 +154,7 @@ are not `const`, so those parse the catalog strings once into a `LazyLock The catalog itself stays **zero-dep** (no chrono, no rust_decimal) — parsing happens in the harness, not in `eql-scalars`. The `int_values!` macro does a compile-time range check so an out-of-range literal is a build error, not a silent `as` truncation -(`lib.rs:456-462`). +(`lib.rs:467-473`). --- @@ -158,7 +162,7 @@ range check so an out-of-range literal is a build error, not a silent `as` trunc ### 4a. The catalog row and value list -`crates/eql-scalars/src/lib.rs:244-246`: +`crates/eql-scalars/src/lib.rs:255-257`: ```rust const INT4_FIXTURES: &[Fixture] = fixtures!(int i32; @@ -166,7 +170,7 @@ const INT4_FIXTURES: &[Fixture] = fixtures!(int i32; N(42), N(50), N(100), N(250), N(1000), N(9999), Max); ``` -`lib.rs:301-306`: +`lib.rs:312-317`: ```rust const INT4: ScalarSpec = ScalarSpec { @@ -178,18 +182,18 @@ const INT4: ScalarSpec = ScalarSpec { ``` The `fixtures!` macro range-checks every `N(..)` literal against `i32` at its -definition site (`lib.rs:191-193`), and `Min`/`Max`/`Zero` are resolved to the +definition site (`lib.rs:202-204`), and `Min`/`Max`/`Zero` are resolved to the kind's bounds. ### 4b. Materialized into a typed const -`lib.rs:476`: +`lib.rs:487`: ```rust int_values!(INT4_VALUES, i32, INT4); ``` -The macro (`lib.rs:439-474`) walks `INT4.fixtures`, calls `numeric_value(kind)` to +The macro (`lib.rs:450-485`) walks `INT4.fixtures`, calls `numeric_value(kind)` to resolve each `Fixture` (`Min` → `i32::MIN`, `N(-100)` → `-100`, `Zero` → `0`, …), and const-evaluates the whole thing into `pub const INT4_VALUES: &[i32]`. So `INT4_VALUES == [-2147483648, -100, -1, 0, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999, 2147483647]`. @@ -237,7 +241,7 @@ let outputs = encrypt_eql(cipher, prepared, &opts).await?; // ONE ZeroKMS roun ``` `value.to_plaintext()` is the `EqlPlaintext` lift — for `i32` it is -`Plaintext::Int(Some(*self))` (`eql_plaintext.rs:152-158`). The `Cast::INT` maps to +`Plaintext::Int(Some(*self))` (`eql_plaintext.rs:153-159`). The `Cast::INT` maps to `ColumnType::Int` (`cipherstash.rs:92-110`), and the `Unique`+`Ore` indexes map to the unique + ORE `IndexType`s (`cipherstash.rs:118-135`). `EqlOperation::Store` yields a full storage payload `{"k":"ct","v":2,"i":…,"c":…,"hm":…,"ob":…}`. @@ -267,8 +271,8 @@ INSERT INTO fixtures.eql_v2_int4 (id, plaintext, payload) VALUES ('1', '-2147483 ``` The `plaintext` column (`-2147483648`) is the committed oracle; the `payload` is the -real encrypted document. At test time `fetch_fixture_payload` (`scalar_domains.rs:726-743`) -looks up a payload by plaintext, and `assert_scalar_plaintexts` (`scalar_domains.rs:763-778`) +real encrypted document. At test time `fetch_fixture_payload` (`scalar_domains.rs:729-746`) +looks up a payload by plaintext, and `assert_scalar_plaintexts` (`scalar_domains.rs:766-781`) compares the DB query result against `expected_forward` over `INT4_VALUES`. --- @@ -278,10 +282,11 @@ compares the DB query result against `expected_forward` over `INT4_VALUES`. `generate_all` also runs three hand-written fixtures that aren't `CATALOG` scalars (`generate_all_fixtures.rs:37-61`): -- **`v3_ste_vec`** — a SteVec JSONB document fixture. **The one committed - exception** (`.gitignore` re-lists it but `CLAUDE.md` documents it as a gap pending - a SteVec-document generator). A hand-written `FixtureSpec` riding - the same `run()` pipeline. +- **`v3_ste_vec`** — a SteVec JSONB document fixture. A hand-written + `FixtureSpec` riding the same `run()` pipeline + (`fixtures::v3_ste_vec::generate()`), gitignored and regenerated like every other + fixture. (`CLAUDE.md` still calls it "the one committed exception"; that wording is + stale — see §6.) - **`v3_doc_int4`** — a scalar-shaped SteVec document, one `{"field": }` per `INT4_VALUES`. A **split** fixture: the encryption input is the jsonb document but the plaintext oracle column is the bare `int4`, so it uses the @@ -289,7 +294,7 @@ compares the DB query result against `expected_forward` over `INT4_VALUES`. - **`v3_numeric_collision`** — the `[1, 1.0, 2]` scale-equivalence collision (`tests/sqlx/src/fixtures/v3_numeric_collision.rs`). It cannot live in the catalog `eql_v2_numeric` fixture because the catalog distinctness guard - (`numeric_value_guards::fixtures_are_distinct_by_value`, `scalar_domains.rs:508-523`) + (`numeric_value_guards::fixtures_are_distinct_by_value`, `scalar_domains.rs:509-523`) forbids the value-equal pair `1`/`1.0`. Encrypted at numeric ORE width via the standard `.run()` driver, addressed by `id` rather than `plaintext` (since `WHERE plaintext = 1` would match both). @@ -316,14 +321,16 @@ through the prep flow, which requires: Why not just commit the SQL and skip the creds? Because the ciphertexts must be the *actual* output of the crypto. `CLAUDE.md`: -> Do NOT add static/committed fixtures to dodge the creds dependency. The one -> committed exception, `tests/sqlx/fixtures/v3_ste_vec.sql`, is a gap pending a -> SteVec-document generator … not a pattern to copy. +> Do NOT add static/committed fixtures to dodge the creds dependency. -The gitignore enforces it mechanically (`.gitignore:225-230`): every -`eql_v2*` fixture plus `v3_doc_int4.sql` and `v3_numeric_collision.sql` are ignored -and regenerated on every `mise run test:sqlx`. A stale or hand-edited fixture can't -survive a run. The live round-trip is additionally smoke-tested by the +(`CLAUDE.md` goes on to name `tests/sqlx/fixtures/v3_ste_vec.sql` as "the one committed +exception … pending a SteVec-document generator", but that clause is stale: the SteVec +generator now exists and the file is gitignored and regenerated like the rest.) + +The gitignore enforces it mechanically (`.gitignore:227-230`): every +`eql_v2*` fixture plus `v3_ste_vec.sql`, `v3_doc_int4.sql`, and `v3_numeric_collision.sql` +are ignored and regenerated on every `mise run test:sqlx`. A stale or hand-edited fixture +can't survive a run. The live round-trip is additionally smoke-tested by the `#[ignore]` `live_tests` in `cipherstash.rs:309-412`, which assert the real Store payload shape (`v=2`, non-null `hm`/`ob`/`c`/`i`) and that distinct plaintexts yield distinct `hm` terms — so an SDK API drift surfaces there before the whole pipeline @@ -335,12 +342,12 @@ breaks. | Concern | Path | |---------|------| -| Catalog rows, fixture lists, `int_values!`/`text_values!` | `crates/eql-scalars/src/lib.rs:184-510` | +| Catalog rows, fixture lists, `int_values!`/`text_values!` | `crates/eql-scalars/src/lib.rs:195-521` | | `generate_all` entry point | `tests/sqlx/tests/generate_all_fixtures.rs:25-62` | | `fixture:generate:all` mise task | `tasks/fixtures.toml:1-29` | | `test:sqlx:prep` flow | `mise.toml:49-80` | | `ScalarType` trait + `fixture_values()` + oracle | `tests/sqlx/src/scalar_domains.rs:17-128` | -| `temporal_values!` (date/timestamptz), numeric/text impls | `tests/sqlx/src/scalar_domains.rs:192-468` | +| `temporal_values!` (date/timestamptz), numeric/text impls | `tests/sqlx/src/scalar_domains.rs:192-495` | | `scalar_types!` harness token list | `tests/sqlx/src/scalar_types.rs:39-63` | | `scalar_fixture!` (spec builder + generator test) | `tests/sqlx/src/fixtures/scalar_fixture.rs` | | `FixtureSpec` builder + SQL renderers | `tests/sqlx/src/fixtures/spec.rs` | diff --git a/docs/walkthroughs/sql-code-generator.md b/docs/walkthroughs/sql-code-generator.md index 3a25489d..e94b6ed7 100644 --- a/docs/walkthroughs/sql-code-generator.md +++ b/docs/walkthroughs/sql-code-generator.md @@ -44,7 +44,7 @@ reviewed baseline fails CI. ```mermaid flowchart TD subgraph catalog["crates/eql-scalars (catalog = source of truth)"] - SPEC["CATALOG: &[ScalarSpec]
lib.rs:427
(INT4, INT2, INT8, DATE,
TIMESTAMPTZ, NUMERIC, TEXT)"] + SPEC["CATALOG: &[ScalarSpec]
lib.rs:438
(INT4, INT2, INT8, DATE,
TIMESTAMPTZ, NUMERIC, TEXT)"] TERM["Term enum + impls
term.rs
(json_key, extractor, ctor,
role, operators, requires)"] end @@ -73,7 +73,7 @@ flowchart TD REF -.->|"parity gate"| PARITY["tests/parity.rs"] ``` -The build calls `cargo run -p eql-codegen` with **no args**, which runs `generate_all(repo_root())` +The build calls `cargo run -p eql-codegen` with **no args**, which runs `generate_all(&repo_root())` (`main.rs:18-28`, `generate.rs:249`). `generate_all` iterates `eql_scalars::CATALOG` and calls `generate_type` per row, writing into `src/v3/scalars//`. The `list-types` subcommand (`main.rs:11-16`) prints the catalog tokens one per line — consumed by the fixtures/matrix @@ -130,25 +130,25 @@ classDiagram Key facts, with the impl locations: -- **`ScalarSpec`** (`eql-scalars/src/lib.rs:170`) is "one row" = one scalar type. `domain_name` +- **`ScalarSpec`** (`eql-scalars/src/lib.rs:181`) is "one row" = one scalar type. `domain_name` / `is_eq_only` are in `spec.rs:20,28`. -- **`DomainSpec`** (`lib.rs:161`) is one generated public domain: a `suffix` appended to the +- **`DomainSpec`** (`lib.rs:172`) is one generated public domain: a `suffix` appended to the token plus the fixed index `terms` it carries. `name_with_token` (`spec.rs:12`) is the *single* place token+suffix concatenation happens, making "domain name starts with token" structural. -- **`Term`** (`lib.rs:82`) is the index-term vocabulary. Its capability methods +- **`Term`** (`lib.rs:93`) is the index-term vocabulary. Its capability methods (`term.rs:8-66`) are the **cross-schema SQL contract**: `Hm` → key `hm`, extractor `eq_term`, ctor `hmac_256`, operators `= <>`; `Ore` → key `ob`, extractor `ord_term`, ctor `ore_block_256`, operators `= <> < <= > >=`; `Bloom` → key `bf`, extractor `match_term`, operators `@> <@`. Term capabilities are fixed in code (with unit tests), not free-form data — an undefined term is a compile error. -- **`Role`** (`lib.rs:96`) is resolved from a domain's terms by `Term::role_for_terms` +- **`Role`** (`lib.rs:107`) is resolved from a domain's terms by `Term::role_for_terms` (`term.rs:128`) using `Role::rank` precedence. `Ore` ⇒ `Ord`, which is what gates aggregate generation. -The four ordered-integer domains are shared via one const (`lib.rs:203` `ORDERED_INT_DOMAINS`): +The four ordered-integer domains are shared via one const (`lib.rs:214` `ORDERED_INT_DOMAINS`): `""` (storage, no terms), `_eq` (`[Hm]`), `_ord_ore` (`[Ore]`), `_ord` (`[Ore]`). `int4`/`int2`/ -`int8`/`date`/`timestamptz`/`numeric` all reuse it; `text` (`lib.rs:377` `TEXT_DOMAINS`) adds a -`_match` domain carrying `[Bloom]`. The catalog itself is `lib.rs:427`. +`int8`/`date`/`timestamptz`/`numeric` all reuse it; `text` (`lib.rs:388` `TEXT_DOMAINS`) adds a +`_match` domain carrying `[Bloom]`. The catalog itself is `lib.rs:438`. --- @@ -157,7 +157,7 @@ The four ordered-integer domains are shared via one const (`lib.rs:203` `ORDERED ### 4.1 The catalog row ```rust -// crates/eql-scalars/src/lib.rs:301 +// crates/eql-scalars/src/lib.rs:312 const INT4: ScalarSpec = ScalarSpec { token: "int4", kind: ScalarKind::I32, @@ -175,7 +175,7 @@ two ordered domains (`_ord`, `_ord_ore`) get aggregates; storage and `_eq` do no ### 4.2 `int4_types.sql` — one DO block, all four domains `render_types_file` (`generate.rs:42`) maps each domain through `domain_block` (`context.rs:72`), -which prepends the always-present `ENVELOPE_KEYS` (`v`, `i`, `c` — `consts.rs:19`) and then the +which prepends the always-present `ENVELOPE_KEYS` (`v`, `i`, `c` — `consts.rs:20`) and then the domain's term keys (`Term::term_json_keys`, e.g. `hm` for `_eq`, `ob` for `_ord`). The `types.sql.j2` template emits an idempotent `IF NOT EXISTS` guard per domain. Rendered (`tests/codegen/reference/int4/int4_types.sql:30`): @@ -217,9 +217,11 @@ Note `CREATE DOMAIN ... AS jsonb` — the base type is always `jsonb`, never ano 2. **Wrapper** — for each operator in `OPERATORS` (`operator_surface.rs:230`) that the domain's terms support (`Term::operators_for_terms`), and only when an extractor backs it - (`Term::extractor_for_operator`). `_eq` supports `=` (`eq`) and `<>` (`neq`). The body casts a - bare `jsonb` operand to the domain before extracting (`extract_arg`, `context.rs:242`). - Rendered via `functions/wrapper.sql.j2` (`int4_eq_functions.sql:23,31`): + (`Term::extractor_for_operator`). `_eq` supports `=` (`eq`) and `<>` (`neq`). Each supported + operator is emitted in **three signatures** — `(domain, domain)`, `(domain, jsonb)`, and + `(jsonb, domain)` — so a bare `jsonb` operand on either side resolves to the wrapper; the body + casts that `jsonb` operand to the domain before extracting (`extract_arg`, `context.rs:242`). + Rendered via `functions/wrapper.sql.j2` (`int4_eq_functions.sql:23,31,39`): ```sql CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b eql_v3.int4_eq) @@ -229,6 +231,10 @@ Note `CREATE DOMAIN ... AS jsonb` — the base type is always `jsonb`, never ano CREATE FUNCTION eql_v3.eq(a eql_v3.int4_eq, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v3.eq_term(a) = eql_v3.eq_term(b::eql_v3.int4_eq) $$; + + CREATE FUNCTION eql_v3.eq(a jsonb, b eql_v3.int4_eq) + RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE + AS $$ SELECT eql_v3.eq_term(a::eql_v3.int4_eq) = eql_v3.eq_term(b) $$; ``` 3. **Unsupported (blocker)** — every operator/signature the domain does *not* support @@ -242,9 +248,10 @@ Note `CREATE DOMAIN ... AS jsonb` — the base type is always `jsonb`, never ano LANGUAGE plpgsql; ``` - The `_eq` file has 45 `CREATE FUNCTION` (1 extractor + 4 supported wrappers + 40 blockers); - the storage `""` file is **all 44 blockers** (no extractor, no supported operator). These - counts are pinned by tests at `generate.rs:439,453`. + The `_eq` file has 45 `CREATE FUNCTION` (1 extractor + 6 supported wrappers — `eq`/`neq` in + three signatures each — + 38 blockers); the storage `""` file is **all 44 blockers** (no + extractor, no supported operator). These counts are pinned by tests at `generate.rs:439,453` + (and the `LANGUAGE sql` == 7 / `LANGUAGE plpgsql` == 38 split at `generate.rs:457-461`). For an **ordered** domain (`int4_ord`), the same machinery produces `ord_term` (`RETURNS eql_v3.ore_block_256`, `int4_ord_functions.sql:15`) and the six comparison wrappers @@ -368,7 +375,7 @@ ordered `&[...]` slices everywhere (`CATALOG`, `domains`, `OPERATORS`, `AGGREGAT (`term.rs:69` `dedupe_preserving_order`, first occurrence wins). The `generate_all_is_deterministic_across_runs` test (`parity.rs:136`) runs the generator twice into separate temp dirs and byte-compares every file. minijinja keeps each template's trailing -newline (`context.rs:14` `set_keep_trailing_newline(true)`) so the output ends in exactly one. +newline (`context.rs:16` `set_keep_trailing_newline(true)`) so the output ends in exactly one. **The AUTO-GENERATED marker.** Every generated file's first line is `-- AUTOMATICALLY GENERATED FILE.` (`consts.rs:7` `AUTO_GENERATED_MARKER`, emitted by each @@ -381,7 +388,7 @@ template as line 1). It serves three roles: `write_generated_file` (`writer.rs:80`) *re-validates* the marker on the rendered body before writing, so a renderer bug that dropped the marker can't produce an unowned file. 2. **Doc-validation skip.** `tasks/docs/validate/*.sh` grep this marker to skip generated SQL - (`consts.rs:32` test pins the marker to the grep). + (`consts.rs:33` test pins the marker to the grep). 3. **Parity baseline.** The committed reference files under `tests/codegen/reference//` carry one extra `-- REFERENCE:` provenance line *above* the marker; the parity gate strips it and byte-compares the rest (`parity.rs:71` `reference_body`). `reference_dirs_match_catalog_tokens` @@ -394,10 +401,10 @@ template as line 1). It serves three roles: | Concern | File | | --- | --- | -| Catalog rows / `CATALOG` | `crates/eql-scalars/src/lib.rs:301-427` | -| `ScalarSpec` / `DomainSpec` defs + impls | `crates/eql-scalars/src/lib.rs:160-176`, `spec.rs` | +| Catalog rows / `CATALOG` | `crates/eql-scalars/src/lib.rs:312-438` | +| `ScalarSpec` / `DomainSpec` defs + impls | `crates/eql-scalars/src/lib.rs:171-187`, `spec.rs` | | `Term` capability contract | `crates/eql-scalars/src/term.rs` | -| `ScalarKind` / `Role` | `crates/eql-scalars/src/kind.rs`, `lib.rs:96-130` | +| `ScalarKind` (def `lib.rs:53`, impls `kind.rs`) / `Role` (`lib.rs:107-141`) | `crates/eql-scalars/src/lib.rs`, `kind.rs` | | Entry point / `list-types` | `crates/eql-codegen/src/main.rs` | | Orchestrator + render functions | `crates/eql-codegen/src/generate.rs` | | minijinja env + serde context | `crates/eql-codegen/src/context.rs` | From 07fa023fb34a065cd9b6252c14cc9e242d390424 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 16 Jun 2026 12:11:15 +1000 Subject: [PATCH 12/12] docs: correct stale v3_ste_vec 'committed exception' claim in CLAUDE.md + walkthroughs --- CLAUDE.md | 7 ++++--- docs/walkthroughs/fixture-generator.md | 13 +++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 34149104..e62913a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,9 +115,10 @@ cipherstash-client: `mise run test:sqlx:prep` runs `fixture:generate:all` (the `CS_WORKSPACE_CRN`) AND a client key (`CS_CLIENT_ID` + `CS_CLIENT_KEY`); see the `test:sqlx:prep` comment in `mise.toml`. CI has them. This is expected, not a reason to avoid generated fixtures. -- Do NOT add static/committed fixtures to dodge the creds dependency. The one committed - exception, `tests/sqlx/fixtures/v3_ste_vec.sql`, is a gap pending a SteVec-document generator - (`docs/handoff/2026-06-10-v3-jsonb-fixture-alignment.md`), not a pattern to copy. +- Do NOT add static/committed fixtures to dodge the creds dependency. Every fixture is + generated and gitignored — including the SteVec document `tests/sqlx/fixtures/v3_ste_vec.sql`, + which has its own generator (`fixtures::v3_ste_vec::generate()`, run by `generate_all`) and is + gitignored (`.gitignore`) like the rest. There is no committed-fixture exception. ## Project Learning & Retrospectives diff --git a/docs/walkthroughs/fixture-generator.md b/docs/walkthroughs/fixture-generator.md index 81427810..d8ba64f7 100644 --- a/docs/walkthroughs/fixture-generator.md +++ b/docs/walkthroughs/fixture-generator.md @@ -37,9 +37,6 @@ Consequences that shape the whole pipeline: Every fixture is generated — including the SteVec document `tests/sqlx/fixtures/v3_ste_vec.sql`, which is gitignored (`.gitignore:228`) and rebuilt each run by its own generator (`fixtures::v3_ste_vec::generate()`). - (`CLAUDE.md` still calls `v3_ste_vec.sql` "the one committed exception … pending a - SteVec-document generator", but that note is stale: the generator exists and the file - is gitignored like the rest.) The plaintext **values** are single-sourced in the Rust catalog `crates/eql-scalars` so the test oracle (the expected-result computation) and the @@ -285,8 +282,7 @@ compares the DB query result against `expected_forward` over `INT4_VALUES`. - **`v3_ste_vec`** — a SteVec JSONB document fixture. A hand-written `FixtureSpec` riding the same `run()` pipeline (`fixtures::v3_ste_vec::generate()`), gitignored and regenerated like every other - fixture. (`CLAUDE.md` still calls it "the one committed exception"; that wording is - stale — see §6.) + fixture. - **`v3_doc_int4`** — a scalar-shaped SteVec document, one `{"field": }` per `INT4_VALUES`. A **split** fixture: the encryption input is the jsonb document but the plaintext oracle column is the bare `int4`, so it uses the @@ -323,11 +319,8 @@ Why not just commit the SQL and skip the creds? Because the ciphertexts must be > Do NOT add static/committed fixtures to dodge the creds dependency. -(`CLAUDE.md` goes on to name `tests/sqlx/fixtures/v3_ste_vec.sql` as "the one committed -exception … pending a SteVec-document generator", but that clause is stale: the SteVec -generator now exists and the file is gitignored and regenerated like the rest.) - -The gitignore enforces it mechanically (`.gitignore:227-230`): every +Every fixture — including `v3_ste_vec.sql` — is generated and gitignored; there is no +committed-fixture exception. The gitignore enforces it mechanically (`.gitignore:227-230`): every `eql_v2*` fixture plus `v3_ste_vec.sql`, `v3_doc_int4.sql`, and `v3_numeric_collision.sql` are ignored and regenerated on every `mise run test:sqlx`. A stale or hand-edited fixture can't survive a run. The live round-trip is additionally smoke-tested by the