diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 436df886..ff82bb42 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -121,6 +121,7 @@ jobs: run: | mise run test:matrix:inventory git diff --exit-code -- tests/sqlx/snapshots/int4_matrix_tests.txt \ + tests/sqlx/snapshots/int2_matrix_tests.txt \ || { echo "Coverage inventory stale — run 'mise run test:matrix:inventory' and commit."; exit 1; } test: diff --git a/CHANGELOG.md b/CHANGELOG.md index 00215fd2..c75b70db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Each entry that ships in a published release links to the PR that introduced it. ### Added - **`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 still return the core `eql_v2.hmac_256` / `eql_v2.ore_block_u64_8_256` index-term types, which remain in `eql_v2` and are referenced cross-schema. 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 `tasks/codegen/types/int2.toml` 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)) - **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)) ## [2.3.1] — 2026-05-21 diff --git a/CLAUDE.md b/CLAUDE.md index 1328a8b0..4b489123 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,7 @@ This project uses `mise` for task management. Common commands: - Run SQLx tests directly: `mise run test:sqlx` - Run SQLx tests in watch mode: `mise run test:sqlx:watch` - Tests are located in `tests/sqlx/` using Rust and SQLx framework +- Regenerate the scalar matrix coverage snapshots: `mise run test:matrix:inventory` (no database required). These committed `tests/sqlx/snapshots/_matrix_tests.txt` baselines pin the set of `scalars::::*` test names so a silently dropped/renamed/`#[cfg]`-gated test fails CI's `matrix-coverage` job. When you add or remove matrix tests (or add a scalar type), regenerate and commit the affected snapshot in the same change. See `tests/sqlx/snapshots/README.md`. ### Build System - Dependencies are resolved using `-- REQUIRE:` comments in SQL files diff --git a/docs/reference/encrypted-domain-generator.md b/docs/reference/encrypted-domain-generator.md index bb6252be..9b2d6a07 100644 --- a/docs/reference/encrypted-domain-generator.md +++ b/docs/reference/encrypted-domain-generator.md @@ -377,12 +377,23 @@ The end-to-end shape from a generator perspective: 4. **Build picks it up automatically** — `tasks/build.sh` regenerates before computing the `tsort` graph, so the new files appear in the dependency walk via the `-- REQUIRE:` edges the generator emits. -5. **Baseline & test.** Create a hand-reviewed byte-parity baseline under - `tests/codegen/reference//` (each file marked `-- REFERENCE:` / - `// REFERENCE:`) so `test_against_reference.py` guards the new type — it - only covers types that have a baseline directory. Then run - `mise run test:codegen`, the relevant SQLx suites, and the PostgreSQL - matrix. +5. **Test.** Do **not** add a `tests/codegen/reference//` baseline. + `int4` is the sole golden master for the type-generic generator: the SQL + templates are pure token substitution and the only type-specific rendering + is `_values.rs`, so a per-type baseline can only fail where `int4`'s + already would. Drift protection for the new type comes from the `int4` + reference (shared templates + `terms.py`), the committed `_values.rs` + const guarded by the codegen staleness check, the `` cases in + `test_scalars.py`, and the `ordered_numeric_matrix!` SQLx suite (behaviour, + not bytes). Run `mise run test:codegen`, the relevant SQLx suites, and the + PostgreSQL matrix. +6. **Snapshot the matrix inventory.** Run `mise run test:matrix:inventory` + and commit the new `tests/sqlx/snapshots/_matrix_tests.txt` — the + sorted list of the type's `scalars::::*` test names. CI's + `matrix-coverage` job `git diff --exit-code`s it (like `_values.rs`) + to catch a silently dropped or renamed matrix test. The snapshot is a + committed test baseline, not gitignored generated SQL. See + `tests/sqlx/snapshots/README.md`. Adding a new **term** is a bigger move — edit `terms.py`, add tests, audit `splinter.sh` for a name collision, and update the reference diff --git a/docs/reference/encrypted-domain-implementation-spec.md b/docs/reference/encrypted-domain-implementation-spec.md index c6bec96a..e21c8d6a 100644 --- a/docs/reference/encrypted-domain-implementation-spec.md +++ b/docs/reference/encrypted-domain-implementation-spec.md @@ -83,13 +83,21 @@ future migration. - [ ] Put optional hand-written SQL in `src/encrypted_domain//_extensions.sql` with explicit `-- REQUIRE:` edges. This file IS committed. -- [ ] Create a hand-reviewed byte-parity baseline under - `tests/codegen/reference//` — one file per generated SQL output plus - `_values.rs`, each headed with the `-- REFERENCE:` / `// REFERENCE:` - marker. `tasks/codegen/test_against_reference.py` only guards types that - have a baseline directory, so without it the new type gets no - drift protection. The committed-fixture parity assertion is currently - `int4`-only; extend it to cover ``. +- [ ] Do **not** add a `tests/codegen/reference//` baseline. `int4` is the + single golden master for the type-generic generator: the SQL templates are + pure token substitution and the only type-specific rendering is + `_values.rs`, so a per-type baseline can only fail when `int4`'s already + would. Drift protection for the new type comes from the `int4` reference + (shared templates + `terms.py`), the committed `_values.rs` const guarded + by the CI staleness check (`mise run codegen:domain ` + `git diff + --exit-code`) and the `` cases in `tasks/codegen/test_scalars.py`, and + the `ordered_numeric_matrix!` SQLx suite (behaviour, not bytes). +- [ ] Run `mise run test:matrix:inventory` and commit the regenerated + `tests/sqlx/snapshots/_matrix_tests.txt` — the sorted inventory of every + `scalars::::*` test name in the `encrypted_domain` binary. CI diffs it + (same as `_values.rs`); a stale snapshot fails the `matrix-coverage` + job with "Coverage inventory stale". This baseline is what catches a + silently dropped, renamed, or `#[cfg]`-gated matrix test. See §8. - [ ] Run `mise run test:codegen`, the relevant SQLx suites, and the PostgreSQL matrix before merging. @@ -252,8 +260,9 @@ Cover each generated domain with SQLx tests appropriate to its terms: - domain `CHECK` rejects non-object and under-populated payloads; - real typed columns are tested, not only cast literals; - generated ordered-domain twins remain byte-identical modulo type name - (verified by `tasks/codegen/test_against_reference.py` against the - hand-reviewed baseline in `tests/codegen/reference//`). + (the shared generator is anchored by the `int4` golden master in + `tests/codegen/reference/int4/` via `tasks/codegen/test_against_reference.py`; + new types add no baseline of their own — see §2). For ordered numeric scalars this coverage is generated by the `ordered_numeric_matrix!` convention wrapper in `tests/sqlx/src/matrix.rs`: @@ -272,6 +281,24 @@ For ordered `int4`, keep the assertion that distinct plaintext values produce distinct ORE blocks. Do not add assertions for term behavior that the catalog does not promise. +### Matrix coverage inventory snapshot + +The *set of test names* the matrix emits is itself guarded. `mise run +test:matrix:inventory` lists every test in the `encrypted_domain` binary +under a pinned feature set (`--no-default-features`, which deliberately +excludes the `scale` arm — see the task comment in `mise.toml`), greps it to +each `scalars::::*` matrix, `LC_ALL=C sort`s for byte-stable ordering, and +writes one committed snapshot per scalar at +`tests/sqlx/snapshots/_matrix_tests.txt`. The CI `matrix-coverage` job +regenerates with the same feature set and `git diff --exit-code`s every +snapshot; a divergence fails with "Coverage inventory stale". This is the +guard that catches a silently dropped, renamed, or `#[cfg]`-gated matrix +test — a behaviour the SQLx assertions above cannot see, because a deleted +test simply stops running. When you add a scalar you add a new snapshot; +when you add or remove matrix tests you regenerate and commit the affected +snapshot in the same change. The files are a committed test baseline, **not** +gitignored generated SQL. See `tests/sqlx/snapshots/README.md`. + ## 9. Fixtures Fixture generation should use real encrypted payloads produced through diff --git a/mise.toml b/mise.toml index 33c96519..8eecd6b6 100644 --- a/mise.toml +++ b/mise.toml @@ -51,12 +51,17 @@ cd tests/sqlx sqlx migrate run # Regenerate fixtures every run — they are not committed (see .gitignore). -# Generator encrypts via cipherstash-client directly; CS_* credentials must -# be present in the shell environment (CS_CLIENT_ACCESS_KEY + -# CS_WORKSPACE_CRN, or the legacy CS_CLIENT_ID/CS_CLIENT_KEY pair). +# fixture:generate:all enumerates every scalar manifest in +# tasks/codegen/types/ that declares a [fixture] table, so new scalar types +# are picked up automatically without editing this task. +# Generator encrypts via cipherstash-client directly, which needs BOTH a +# ZeroKMS auth credential (CS_CLIENT_ACCESS_KEY + CS_WORKSPACE_CRN, via +# AutoStrategy) AND a client key (CS_CLIENT_ID + CS_CLIENT_KEY, via +# EnvKeyProvider) in the shell environment. Auth and key material are +# separate roles — the two pairs are not alternatives. echo "Regenerating SQLx fixtures..." cd "{{config_root}}" -mise run fixture:generate eql_v2_int4 +mise run fixture:generate:all echo "Running Rust tests..." cd tests/sqlx @@ -94,7 +99,7 @@ mise exec python -- python -m pytest tasks/codegen -q """ [tasks."test:matrix:inventory"] -description = "Regenerate the int4 matrix test-name inventory snapshot (no database required)" +description = "Regenerate the int4/int2 matrix test-name inventory snapshots (no database required)" dir = "{{config_root}}/tests/sqlx" run = """ # Pin an explicit feature set so the inventory is deterministic regardless of @@ -103,14 +108,18 @@ run = """ # of this default-feature inventory, covered instead by the scale gate + the # family::mutations negative controls. `--list` enumerates the whole # encrypted_domain binary (family::support, family::inlinability, -# family::mutations, scalars::int4); `grep '^scalars::int4'` scopes the -# snapshot to the matrix only, so landing other family tests never dirties it. -# `LC_ALL=C sort` makes ordering byte-stable across locales (a bare `sort` is -# locale-dependent and yields spurious CI diffs). +# family::mutations, scalars::int4, scalars::int2); the per-scalar `grep` +# scopes each snapshot to that matrix only, so landing other family tests +# never dirties it. `LC_ALL=C sort` makes ordering byte-stable across locales +# (a bare `sort` is locale-dependent and yields spurious CI diffs). set -euo pipefail mkdir -p snapshots cargo test --no-default-features --test encrypted_domain -- --list | sed -n 's/: test$//p' | grep '^scalars::int4' | LC_ALL=C sort > snapshots/int4_matrix_tests.txt +cargo test --no-default-features --test encrypted_domain -- --list | + sed -n 's/: test$//p' | + grep '^scalars::int2' | + LC_ALL=C sort > snapshots/int2_matrix_tests.txt """ diff --git a/tasks/codegen/scalars.py b/tasks/codegen/scalars.py index a93df905..eee01dc4 100644 --- a/tasks/codegen/scalars.py +++ b/tasks/codegen/scalars.py @@ -79,6 +79,15 @@ def render_literal(self, value: str) -> str: min_value=-2147483648, max_value=2147483647, ), + "int2": ScalarKind( + token="int2", + rust_type="i16", + min_symbol="i16::MIN", + max_symbol="i16::MAX", + zero_symbol="0", + min_value=-32768, + max_value=32767, + ), } diff --git a/tasks/codegen/test_scalars.py b/tasks/codegen/test_scalars.py index 3ef7d0f0..1f15f1c3 100644 --- a/tasks/codegen/test_scalars.py +++ b/tasks/codegen/test_scalars.py @@ -62,3 +62,21 @@ def test_require_scalar_unknown_raises(): def test_int4_registered_in_catalog(): assert "int4" in SCALAR_KINDS + + +def test_int2_kind_resolves_and_renders(): + kind = require_scalar("int2") + assert kind.rust_type == "i16" + assert kind.numeric_value("MIN") == -32768 + assert kind.numeric_value("MAX") == 32767 + assert kind.numeric_value("ZERO") == 0 + assert kind.render_literal("MIN") == "i16::MIN" + assert kind.render_literal("MAX") == "i16::MAX" + assert kind.render_literal("ZERO") == "0" + assert kind.render_literal("30000") == "30000" + + +def test_int2_kind_rejects_out_of_range(): + kind = require_scalar("int2") + with pytest.raises(ScalarError, match="out of range"): + kind.numeric_value("40000") diff --git a/tasks/codegen/types/int2.toml b/tasks/codegen/types/int2.toml new file mode 100644 index 00000000..314bc698 --- /dev/null +++ b/tasks/codegen/types/int2.toml @@ -0,0 +1,19 @@ +# Encrypted-domain scalar manifest for int2. +# The filename supplies the type token. Each domain lists the index terms +# it carries; term capabilities are fixed in tasks/codegen/terms.py. + +[domain] +int2 = [] +int2_eq = ["hm"] +int2_ord_ore = ["ore"] +int2_ord = ["ore"] + +# Single source of truth for the int2 fixture plaintext list. Drives the +# generated tests/sqlx/src/fixtures/int2_values.rs const, shared by the fixture +# generator and the matrix oracle. Sentinels MIN/MAX/ZERO map to i16 named +# consts; the set MUST include MIN, MAX, and zero (matrix comparison pivots). +[fixture] +values = [ + "MIN", "-30000", "-100", "-1", "ZERO", "1", "2", "5", "10", "17", "25", + "42", "50", "100", "250", "1000", "9999", "30000", "MAX", +] diff --git a/tasks/fixtures.toml b/tasks/fixtures.toml index a9804f8b..68dafb73 100644 --- a/tasks/fixtures.toml +++ b/tasks/fixtures.toml @@ -27,3 +27,32 @@ cargo test --features fixture-gen --lib \ "fixtures::${fixture}::generate" \ -- --ignored --exact --nocapture """ + +["fixture:generate:all"] +description = "Regenerate every scalar SQLx fixture declared by a type manifest" +# Enumerates tasks/codegen/types/*.toml — the SAME manifests that +# `codegen:domain:all` drives — and regenerates the SQLx fixture for each type +# whose manifest declares a [fixture] table. This keeps the test fixtures in +# lockstep with the declared scalar types: adding a new scalar type (a new +# .toml with a [fixture] table) is picked up automatically, so the test +# task never has to hand-list each fixture. Same prerequisites as +# `fixture:generate` (Postgres up + CS_* credentials). +dir = "{{config_root}}" +run = """ +generated=0 +for manifest in tasks/codegen/types/*.toml; do + # Guard the no-match case (glob stays literal under POSIX sh). + [ -e "$manifest" ] || continue + # Only types that declare a [fixture] table have a SQLx fixture generator. + grep -qE '^\\[fixture\\]' "$manifest" || continue + token=$(basename "$manifest" .toml) + echo "Generating fixture eql_v2_${token}..." + mise run fixture:generate "eql_v2_${token}" + generated=$((generated + 1)) +done +if [ "$generated" -eq 0 ]; then + echo "No scalar manifests with a [fixture] table found in tasks/codegen/types/" >&2 + exit 1 +fi +echo "Regenerated ${generated} scalar fixture(s)." +""" diff --git a/tests/codegen/reference/README.md b/tests/codegen/reference/README.md index c1fa5118..58f01cc1 100644 --- a/tests/codegen/reference/README.md +++ b/tests/codegen/reference/README.md @@ -1,5 +1,13 @@ # Codegen reference -The SQL files under `/` are the original, hand-written reference implementation for each encrypted-domain scalar type. +The SQL files under `int4/` are the original, hand-written reference implementation for the encrypted-domain scalar generator. `int4` is the **single golden master**: the generator in `tasks/codegen/` is type-generic — its SQL templates are pure token substitution, and the only type-specific rendering is the `_values.rs` const — so one anchored type detects all template/term drift for every current and future scalar. -They are the parity baseline for the generator in `tasks/codegen/`. `tasks/codegen/test_against_reference.py` renders the generator's output and asserts it matches these files byte-for-byte. If the generator diverges, either it regressed (fix `tasks/codegen/`) or the reference is being updated deliberately (commit the new reference in the same PR). +`tasks/codegen/test_against_reference.py` renders the generator's output for `int4` and asserts it matches these files byte-for-byte. If the generator diverges, either it regressed (fix `tasks/codegen/`) or the reference is being updated deliberately (commit the new `int4` reference in the same PR). + +## New scalar types do not add a reference + +Adding a scalar type (`int2`, `int8`, …) does **not** add a `tests/codegen/reference//` directory. A per-type baseline would be redundant: the SQL is byte-identical to `int4` modulo the type token, so it can only fail when `int4`'s baseline already would. New types are guaranteed three other ways: + +- the `int4` reference here anchors the shared generator (templates + `terms.py`); +- the committed `tests/sqlx/src/fixtures/_values.rs` const is pinned by the CI staleness guard (`git diff --exit-code` after `mise run codegen:domain `) and by the `` cases in `tasks/codegen/test_scalars.py` (the only type-specific rendering, `i16::MIN` vs `i32::MIN`); +- the SQLx `ordered_numeric_matrix!` suite exercises the generated SQL's *behaviour* against a real database — a far stronger guarantee than a byte comparison. diff --git a/tests/sqlx/snapshots/README.md b/tests/sqlx/snapshots/README.md new file mode 100644 index 00000000..a4ce5ae9 --- /dev/null +++ b/tests/sqlx/snapshots/README.md @@ -0,0 +1,53 @@ +# Matrix coverage inventory snapshots + +This directory holds one committed snapshot per scalar encrypted-domain type: + +- `int4_matrix_tests.txt` +- `int2_matrix_tests.txt` + +Each file is a sorted, byte-stable list of every `scalars::::*` test name in +the `encrypted_domain` SQLx binary. They are a **committed test baseline**, not +gitignored generated SQL — keep them in version control. + +## What they guard + +The SQLx assertions verify that the tests which run produce the right results. +They cannot see a test that *stops running* — a matrix test that is deleted, +renamed, or hidden behind a `#[cfg]` gate simply vanishes silently, quietly +shrinking coverage. These snapshots close that gap: they pin the *set of test +names* so any such change shows up as an added/removed line in the PR diff. + +## How they are generated + +Run: + +```bash +mise run test:matrix:inventory +``` + +The task (`mise.toml`, `[tasks."test:matrix:inventory"]`) enumerates the binary +with `cargo test --test encrypted_domain -- --list`, greps each +`scalars::` matrix into its own file, and `LC_ALL=C sort`s for ordering +that is byte-stable across locales. No database is required — `--list` only +enumerates; the suite uses runtime queries. + +It pins `--no-default-features` so the inventory is deterministic regardless of +the caller's local flags. That deliberately excludes the `scale` feature arm +(`#[cfg(feature = "scale")]`) — a known blind spot of this inventory, covered +instead by the scale gate plus the `family::mutations` negative controls. + +## CI enforcement + +The `matrix-coverage` job in `.github/workflows/test-eql.yml` regenerates with +the same pinned feature set and runs `git diff --exit-code` against every +snapshot in this directory. A divergence fails the job with: + +> Coverage inventory stale — run 'mise run test:matrix:inventory' and commit. + +## When you must update these + +- **Adding a new scalar type** → a new `_matrix_tests.txt` appears; commit it. +- **Adding / removing / renaming matrix tests** → regenerate and commit the + affected snapshot in the same change. + +See `docs/reference/encrypted-domain-implementation-spec.md` §2 and §8. diff --git a/tests/sqlx/snapshots/int2_matrix_tests.txt b/tests/sqlx/snapshots/int2_matrix_tests.txt new file mode 100644 index 00000000..3b6ed674 --- /dev/null +++ b/tests/sqlx/snapshots/int2_matrix_tests.txt @@ -0,0 +1,211 @@ +scalars::int2::matrix_int2_eq_aggregate_typecheck_max +scalars::int2::matrix_int2_eq_aggregate_typecheck_min +scalars::int2::matrix_int2_eq_contained_by_blocker +scalars::int2::matrix_int2_eq_contains_blocker +scalars::int2::matrix_int2_eq_count_distinct_extractor +scalars::int2::matrix_int2_eq_count_path_cast +scalars::int2::matrix_int2_eq_count_typed_column +scalars::int2::matrix_int2_eq_eq_pivot_max_correctness +scalars::int2::matrix_int2_eq_eq_pivot_max_cross_shape +scalars::int2::matrix_int2_eq_eq_pivot_min_correctness +scalars::int2::matrix_int2_eq_eq_pivot_min_cross_shape +scalars::int2::matrix_int2_eq_eq_pivot_zero_correctness +scalars::int2::matrix_int2_eq_eq_pivot_zero_cross_shape +scalars::int2::matrix_int2_eq_eq_supported_null +scalars::int2::matrix_int2_eq_gt_blocker +scalars::int2::matrix_int2_eq_gte_blocker +scalars::int2::matrix_int2_eq_index_engages_btree +scalars::int2::matrix_int2_eq_index_engages_hash +scalars::int2::matrix_int2_eq_lt_blocker +scalars::int2::matrix_int2_eq_lte_blocker +scalars::int2::matrix_int2_eq_native_absent_ops +scalars::int2::matrix_int2_eq_neq_pivot_max_correctness +scalars::int2::matrix_int2_eq_neq_pivot_max_cross_shape +scalars::int2::matrix_int2_eq_neq_pivot_min_correctness +scalars::int2::matrix_int2_eq_neq_pivot_min_cross_shape +scalars::int2::matrix_int2_eq_neq_pivot_zero_correctness +scalars::int2::matrix_int2_eq_neq_pivot_zero_cross_shape +scalars::int2::matrix_int2_eq_neq_supported_null +scalars::int2::matrix_int2_eq_path_op_blockers +scalars::int2::matrix_int2_eq_payload_check +scalars::int2::matrix_int2_eq_planner_metadata_eq +scalars::int2::matrix_int2_eq_sanity +scalars::int2::matrix_int2_eq_typed_column_blocker +scalars::int2::matrix_int2_fixture_shape +scalars::int2::matrix_int2_ord_aggregate_group_by_max +scalars::int2::matrix_int2_ord_aggregate_group_by_min +scalars::int2::matrix_int2_ord_aggregate_max +scalars::int2::matrix_int2_ord_aggregate_max_all_null +scalars::int2::matrix_int2_ord_aggregate_max_empty +scalars::int2::matrix_int2_ord_aggregate_max_mixed_null +scalars::int2::matrix_int2_ord_aggregate_min +scalars::int2::matrix_int2_ord_aggregate_min_all_null +scalars::int2::matrix_int2_ord_aggregate_min_empty +scalars::int2::matrix_int2_ord_aggregate_min_mixed_null +scalars::int2::matrix_int2_ord_aggregate_parallel_safe +scalars::int2::matrix_int2_ord_contained_by_blocker +scalars::int2::matrix_int2_ord_contains_blocker +scalars::int2::matrix_int2_ord_count_distinct_extractor +scalars::int2::matrix_int2_ord_count_path_cast +scalars::int2::matrix_int2_ord_count_typed_column +scalars::int2::matrix_int2_ord_eq_pivot_max_correctness +scalars::int2::matrix_int2_ord_eq_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_eq_pivot_min_correctness +scalars::int2::matrix_int2_ord_eq_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_eq_pivot_zero_correctness +scalars::int2::matrix_int2_ord_eq_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_eq_supported_null +scalars::int2::matrix_int2_ord_gt_pivot_max_correctness +scalars::int2::matrix_int2_ord_gt_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_gt_pivot_min_correctness +scalars::int2::matrix_int2_ord_gt_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_gt_pivot_zero_correctness +scalars::int2::matrix_int2_ord_gt_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_gt_supported_null +scalars::int2::matrix_int2_ord_gte_pivot_max_correctness +scalars::int2::matrix_int2_ord_gte_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_gte_pivot_min_correctness +scalars::int2::matrix_int2_ord_gte_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_gte_pivot_zero_correctness +scalars::int2::matrix_int2_ord_gte_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_gte_supported_null +scalars::int2::matrix_int2_ord_index_engages_btree +scalars::int2::matrix_int2_ord_lt_pivot_max_correctness +scalars::int2::matrix_int2_ord_lt_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_lt_pivot_min_correctness +scalars::int2::matrix_int2_ord_lt_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_lt_pivot_zero_correctness +scalars::int2::matrix_int2_ord_lt_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_lt_supported_null +scalars::int2::matrix_int2_ord_lte_pivot_max_correctness +scalars::int2::matrix_int2_ord_lte_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_lte_pivot_min_correctness +scalars::int2::matrix_int2_ord_lte_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_lte_pivot_zero_correctness +scalars::int2::matrix_int2_ord_lte_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_lte_supported_null +scalars::int2::matrix_int2_ord_native_absent_ops +scalars::int2::matrix_int2_ord_neq_pivot_max_correctness +scalars::int2::matrix_int2_ord_neq_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_neq_pivot_min_correctness +scalars::int2::matrix_int2_ord_neq_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_neq_pivot_zero_correctness +scalars::int2::matrix_int2_ord_neq_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_neq_supported_null +scalars::int2::matrix_int2_ord_ord_routes_through_ob +scalars::int2::matrix_int2_ord_order_by_asc_no_where +scalars::int2::matrix_int2_ord_order_by_asc_nulls_first +scalars::int2::matrix_int2_ord_order_by_asc_nulls_last +scalars::int2::matrix_int2_ord_order_by_asc_with_where +scalars::int2::matrix_int2_ord_order_by_desc_no_where +scalars::int2::matrix_int2_ord_order_by_desc_nulls_first +scalars::int2::matrix_int2_ord_order_by_desc_nulls_last +scalars::int2::matrix_int2_ord_order_by_desc_with_where +scalars::int2::matrix_int2_ord_order_by_using_gt_rejects +scalars::int2::matrix_int2_ord_order_by_using_gte_rejects +scalars::int2::matrix_int2_ord_order_by_using_lt_rejects +scalars::int2::matrix_int2_ord_order_by_using_lte_rejects +scalars::int2::matrix_int2_ord_ore_aggregate_group_by_max +scalars::int2::matrix_int2_ord_ore_aggregate_group_by_min +scalars::int2::matrix_int2_ord_ore_aggregate_max +scalars::int2::matrix_int2_ord_ore_aggregate_max_all_null +scalars::int2::matrix_int2_ord_ore_aggregate_max_empty +scalars::int2::matrix_int2_ord_ore_aggregate_max_mixed_null +scalars::int2::matrix_int2_ord_ore_aggregate_min +scalars::int2::matrix_int2_ord_ore_aggregate_min_all_null +scalars::int2::matrix_int2_ord_ore_aggregate_min_empty +scalars::int2::matrix_int2_ord_ore_aggregate_min_mixed_null +scalars::int2::matrix_int2_ord_ore_aggregate_parallel_safe +scalars::int2::matrix_int2_ord_ore_contained_by_blocker +scalars::int2::matrix_int2_ord_ore_contains_blocker +scalars::int2::matrix_int2_ord_ore_count_distinct_extractor +scalars::int2::matrix_int2_ord_ore_count_path_cast +scalars::int2::matrix_int2_ord_ore_count_typed_column +scalars::int2::matrix_int2_ord_ore_eq_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_eq_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_eq_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_eq_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_eq_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_eq_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_eq_supported_null +scalars::int2::matrix_int2_ord_ore_gt_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_gt_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_gt_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_gt_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_gt_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_gt_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_gt_supported_null +scalars::int2::matrix_int2_ord_ore_gte_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_gte_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_gte_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_gte_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_gte_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_gte_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_gte_supported_null +scalars::int2::matrix_int2_ord_ore_index_engages_btree +scalars::int2::matrix_int2_ord_ore_lt_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_lt_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_lt_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_lt_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_lt_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_lt_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_lt_supported_null +scalars::int2::matrix_int2_ord_ore_lte_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_lte_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_lte_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_lte_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_lte_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_lte_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_lte_supported_null +scalars::int2::matrix_int2_ord_ore_native_absent_ops +scalars::int2::matrix_int2_ord_ore_neq_pivot_max_correctness +scalars::int2::matrix_int2_ord_ore_neq_pivot_max_cross_shape +scalars::int2::matrix_int2_ord_ore_neq_pivot_min_correctness +scalars::int2::matrix_int2_ord_ore_neq_pivot_min_cross_shape +scalars::int2::matrix_int2_ord_ore_neq_pivot_zero_correctness +scalars::int2::matrix_int2_ord_ore_neq_pivot_zero_cross_shape +scalars::int2::matrix_int2_ord_ore_neq_supported_null +scalars::int2::matrix_int2_ord_ore_ord_routes_through_ob +scalars::int2::matrix_int2_ord_ore_order_by_asc_no_where +scalars::int2::matrix_int2_ord_ore_order_by_asc_nulls_first +scalars::int2::matrix_int2_ord_ore_order_by_asc_nulls_last +scalars::int2::matrix_int2_ord_ore_order_by_asc_with_where +scalars::int2::matrix_int2_ord_ore_order_by_desc_no_where +scalars::int2::matrix_int2_ord_ore_order_by_desc_nulls_first +scalars::int2::matrix_int2_ord_ore_order_by_desc_nulls_last +scalars::int2::matrix_int2_ord_ore_order_by_desc_with_where +scalars::int2::matrix_int2_ord_ore_order_by_using_gt_rejects +scalars::int2::matrix_int2_ord_ore_order_by_using_gte_rejects +scalars::int2::matrix_int2_ord_ore_order_by_using_lt_rejects +scalars::int2::matrix_int2_ord_ore_order_by_using_lte_rejects +scalars::int2::matrix_int2_ord_ore_ore_injectivity +scalars::int2::matrix_int2_ord_ore_path_op_blockers +scalars::int2::matrix_int2_ord_ore_payload_check +scalars::int2::matrix_int2_ord_ore_planner_metadata_eq +scalars::int2::matrix_int2_ord_ore_planner_metadata_ord +scalars::int2::matrix_int2_ord_ore_sanity +scalars::int2::matrix_int2_ord_ore_typed_column_blocker +scalars::int2::matrix_int2_ord_path_op_blockers +scalars::int2::matrix_int2_ord_payload_check +scalars::int2::matrix_int2_ord_planner_metadata_eq +scalars::int2::matrix_int2_ord_planner_metadata_ord +scalars::int2::matrix_int2_ord_sanity +scalars::int2::matrix_int2_ord_scale_preference_default_btree +scalars::int2::matrix_int2_ord_typed_column_blocker +scalars::int2::matrix_int2_storage_aggregate_typecheck_max +scalars::int2::matrix_int2_storage_aggregate_typecheck_min +scalars::int2::matrix_int2_storage_contained_by_blocker +scalars::int2::matrix_int2_storage_contains_blocker +scalars::int2::matrix_int2_storage_count_path_cast +scalars::int2::matrix_int2_storage_count_typed_column +scalars::int2::matrix_int2_storage_eq_blocker +scalars::int2::matrix_int2_storage_gt_blocker +scalars::int2::matrix_int2_storage_gte_blocker +scalars::int2::matrix_int2_storage_lt_blocker +scalars::int2::matrix_int2_storage_lte_blocker +scalars::int2::matrix_int2_storage_native_absent_ops +scalars::int2::matrix_int2_storage_neq_blocker +scalars::int2::matrix_int2_storage_path_op_blockers +scalars::int2::matrix_int2_storage_payload_check +scalars::int2::matrix_int2_storage_sanity +scalars::int2::matrix_int2_storage_typed_column_blocker diff --git a/tests/sqlx/src/fixtures/eql_plaintext.rs b/tests/sqlx/src/fixtures/eql_plaintext.rs index 0db9482a..36b348fd 100644 --- a/tests/sqlx/src/fixtures/eql_plaintext.rs +++ b/tests/sqlx/src/fixtures/eql_plaintext.rs @@ -53,6 +53,7 @@ pub struct PlaintextSqlType(&'static str); impl PlaintextSqlType { pub const INTEGER: PlaintextSqlType = PlaintextSqlType("integer"); + pub const SMALLINT: PlaintextSqlType = PlaintextSqlType("smallint"); pub fn as_str(&self) -> &'static str { self.0 @@ -68,6 +69,7 @@ impl fmt::Display for PlaintextSqlType { mod sealed { pub trait Sealed {} impl Sealed for i32 {} + impl Sealed for i16 {} } /// A Rust type usable as a fixture `plaintext` value, carrying its EQL cast @@ -96,6 +98,15 @@ impl EqlPlaintext for i32 { } } +impl EqlPlaintext for i16 { + const CAST: Cast = Cast::SMALL_INT; + const PLAINTEXT_SQL_TYPE: PlaintextSqlType = PlaintextSqlType::SMALLINT; + + fn to_plaintext(&self) -> Plaintext { + Plaintext::SmallInt(Some(*self)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -122,4 +133,27 @@ mod tests { other => panic!("expected Plaintext::Int(Some(42)), got {other:?}"), } } + + #[test] + fn i16_casts_to_small_int() { + assert_eq!(::CAST.as_str(), "small_int"); + } + + #[test] + fn i16_plaintext_sql_type_is_smallint() { + assert_eq!( + ::PLAINTEXT_SQL_TYPE.as_str(), + "smallint" + ); + } + + #[test] + fn i16_to_plaintext_wraps_in_small_int_variant() { + // i16 must lift into the SmallInt variant so the fixture driver + // encrypts it under the `small_int` cast, not `int`. + match 42_i16.to_plaintext() { + Plaintext::SmallInt(Some(value)) => assert_eq!(value, 42), + other => panic!("expected Plaintext::SmallInt(Some(42)), got {other:?}"), + } + } } diff --git a/tests/sqlx/src/fixtures/eql_v2_int2.rs b/tests/sqlx/src/fixtures/eql_v2_int2.rs new file mode 100644 index 00000000..ec4a1333 --- /dev/null +++ b/tests/sqlx/src/fixtures/eql_v2_int2.rs @@ -0,0 +1,12 @@ +//! The `eql_v2_int2` fixture — the int4 reference, clamped to 16 bits. +//! +//! 19 integers spanning a negative boundary, the i16 signed extremes +//! (`MIN`/`MAX`), zero, a pair near the ±32767 boundary, and +//! small/medium/large magnitudes. The generated +//! `tests/sqlx/fixtures/eql_v2_int2.sql` is a plain `jsonb`-payload table with +//! no EQL dependency; the `eql_v2_int2` domain is layered on top by casting +//! `payload` per query. + +use super::int2_values::VALUES; + +crate::scalar_fixture!("eql_v2_int2", i16, VALUES); diff --git a/tests/sqlx/src/fixtures/int2_values.rs b/tests/sqlx/src/fixtures/int2_values.rs new file mode 100644 index 00000000..74aff1c4 --- /dev/null +++ b/tests/sqlx/src/fixtures/int2_values.rs @@ -0,0 +1,33 @@ +// AUTO-GENERATED — DO NOT EDIT. +// Regenerated by `mise run build` (or `mise run codegen:domain `). +// Source of truth: tasks/codegen/types/.toml `[fixture] values`. +// This file IS committed and verified in CI (git diff --exit-code). +//! Fixture plaintext values for the int2 encrypted-domain family. +//! +//! Generated from tasks/codegen/types/int2.toml `[fixture] values` — +//! the single source of truth shared by the fixture generator +//! (`fixtures::eql_v2_int2`) and the matrix oracle +//! (`ScalarType::FIXTURE_VALUES`). + +/// Distinct plaintext values present in the `eql_v2_int2` fixture. +pub const VALUES: &[i16] = &[ + i16::MIN, + -30000, + -100, + -1, + 0, + 1, + 2, + 5, + 10, + 17, + 25, + 42, + 50, + 100, + 250, + 1000, + 9999, + 30000, + i16::MAX, +]; diff --git a/tests/sqlx/src/fixtures/mod.rs b/tests/sqlx/src/fixtures/mod.rs index ee087d1d..ac363a49 100644 --- a/tests/sqlx/src/fixtures/mod.rs +++ b/tests/sqlx/src/fixtures/mod.rs @@ -31,3 +31,9 @@ pub mod driver; pub mod int4_values; pub mod eql_v2_int4; + +/// Generated from tasks/codegen/types/int2.toml `[fixture] values`. +/// Committed and verified by CI; never hand-edit (`mise run codegen:domain int2`). +pub mod int2_values; + +pub mod eql_v2_int2; diff --git a/tests/sqlx/src/scalar_domains.rs b/tests/sqlx/src/scalar_domains.rs index e7567584..c3acc428 100644 --- a/tests/sqlx/src/scalar_domains.rs +++ b/tests/sqlx/src/scalar_domains.rs @@ -81,6 +81,15 @@ impl ScalarType for i32 { const FIXTURE_VALUES: &'static [i32] = crate::fixtures::int4_values::VALUES; } +impl ScalarType for i16 { + const PG_TYPE: &'static str = "int2"; + /// Single-sourced from `tasks/codegen/types/int2.toml` `[fixture] values` + /// via the generated `fixtures::int2_values::VALUES` const — the same list + /// the fixture generator encrypts, so the oracle cannot drift from the + /// fixture. Spans the negative boundary, the i16 signed extremes, and zero. + const FIXTURE_VALUES: &'static [i16] = crate::fixtures::int2_values::VALUES; +} + /// Per-domain capability + payload shape. Storage carries no terms, `Eq` /// adds `hm`, `Ord`/`OrdOre` add `ob`. `Ord` and `OrdOre` are deliberate /// twins — same operator surface, different SQL domain names — for the diff --git a/tests/sqlx/tests/encrypted_domain/scalars/int2.rs b/tests/sqlx/tests/encrypted_domain/scalars/int2.rs new file mode 100644 index 00000000..7a8e9331 --- /dev/null +++ b/tests/sqlx/tests/encrypted_domain/scalars/int2.rs @@ -0,0 +1,14 @@ +//! `eql_v2_int2` — the int4 reference scalar, clamped to 16 bits. +//! +//! Adding a new ordered numeric scalar (i64, f64, date, ...) is one +//! `impl ScalarType` in `tests/sqlx/src/scalar_domains.rs` plus an +//! `ordered_numeric_matrix!` invocation like this one. The matrix covers +//! everything generic over `T: ScalarType`. + +use eql_tests::ordered_numeric_matrix; + +ordered_numeric_matrix! { + suite = int2, + scalar = i16, + eql_type = "eql_v2_int2", +} diff --git a/tests/sqlx/tests/encrypted_domain/scalars/mod.rs b/tests/sqlx/tests/encrypted_domain/scalars/mod.rs index 8abc1857..f42cfb5f 100644 --- a/tests/sqlx/tests/encrypted_domain/scalars/mod.rs +++ b/tests/sqlx/tests/encrypted_domain/scalars/mod.rs @@ -2,3 +2,5 @@ //! additions (`int8`, `bool`, `date`, …) become sibling modules here. pub mod int4; + +pub mod int2;