From 16866f6f344e726a84ad1a5de0a95b915cf40616 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 1 Jun 2026 16:03:24 +1000 Subject: [PATCH 1/5] feat(encrypted-domain): add int8 (bigint / i64) scalar type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the int8 ordered numeric scalar to the generated encrypted-domain family on the eql_v3 schema, stacked on the int4 reference and int2. Four jsonb-backed domains (eql_v3.int8{,_eq,_ord,_ord_ore}) are generated from the new tasks/codegen/types/int8.toml manifest by the existing type-generic materializer — no generator behaviour changes. The SQL domains live in eql_v3; the index-term types stay in eql_v2 and are referenced cross-schema, exactly as for int4/int2. - Register the int8 ScalarKind (i64, MIN -2^63 / MAX 2^63-1 / zero) in tasks/codegen/scalars.py, with test_scalars.py coverage; re-point the test_spec.py unknown-scalar-token fixture off int8 (now a valid token) onto bogus. - Commit the generated fixture-value const tests/sqlx/src/fixtures/int8_values.rs (single source of truth shared by the fixture generator and the matrix oracle), with i64-range boundary values (±5_000_000_000) that exercise 64-bit width beyond int4. - Wire the SQLx matrix oracle: impl ScalarType for i64, the eql_v2_int8 fixture via the scalar_fixture! macro, and the sealed EqlPlaintext impl for i64 (big_int cast, Plaintext::BigInt, bigint oracle column). - Add the ordered_numeric_matrix! invocation, the int8 matrix test-name inventory snapshot, the mise test:matrix:inventory int8 block, and the CI staleness-guard entry. - Record the new family in CHANGELOG.md. Verified: mise run test:codegen (150 passed); clean && build emits the eql_v3.int8 domains + aggregates; full encrypted_domain SQLx matrix against an eql_v3 install (661 passed, 0 failed — int4 + int2 + int8 + family). --- .github/workflows/test-eql.yml | 1 + CHANGELOG.md | 1 + mise.toml | 8 +- tasks/codegen/scalars.py | 9 + tasks/codegen/test_scalars.py | 18 ++ tasks/codegen/test_spec.py | 6 +- tasks/codegen/types/int8.toml | 20 ++ tests/sqlx/snapshots/int8_matrix_tests.txt | 211 ++++++++++++++++++ tests/sqlx/src/fixtures/eql_plaintext.rs | 31 +++ tests/sqlx/src/fixtures/eql_v2_int8.rs | 12 + tests/sqlx/src/fixtures/int8_values.rs | 33 +++ tests/sqlx/src/fixtures/mod.rs | 6 + tests/sqlx/src/scalar_domains.rs | 10 + .../tests/encrypted_domain/scalars/int8.rs | 14 ++ .../tests/encrypted_domain/scalars/mod.rs | 4 +- 15 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 tasks/codegen/types/int8.toml create mode 100644 tests/sqlx/snapshots/int8_matrix_tests.txt create mode 100644 tests/sqlx/src/fixtures/eql_v2_int8.rs create mode 100644 tests/sqlx/src/fixtures/int8_values.rs create mode 100644 tests/sqlx/tests/encrypted_domain/scalars/int8.rs diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index ff82bb42..a4d6c49f 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -122,6 +122,7 @@ jobs: mise run test:matrix:inventory git diff --exit-code -- tests/sqlx/snapshots/int4_matrix_tests.txt \ tests/sqlx/snapshots/int2_matrix_tests.txt \ + tests/sqlx/snapshots/int8_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 c75b70db..5bd22ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Each entry that ships in a published release links to the PR that introduced it. - **`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)) +- **`eql_v3.int8` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int8` (Postgres `bigint` / Rust `i64`) 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 `tasks/codegen/types/int8.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 64-bit integer column for callers whose values exceed the `int4` range. ([#244](https://github.com/cipherstash/encrypt-query-language/pull/244)) - **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/mise.toml b/mise.toml index 8eecd6b6..b972fdc9 100644 --- a/mise.toml +++ b/mise.toml @@ -99,7 +99,7 @@ mise exec python -- python -m pytest tasks/codegen -q """ [tasks."test:matrix:inventory"] -description = "Regenerate the int4/int2 matrix test-name inventory snapshots (no database required)" +description = "Regenerate the int4/int2/int8 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 @@ -108,7 +108,7 @@ 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, scalars::int2); the per-scalar `grep` +# family::mutations, scalars::int4, scalars::int2, scalars::int8); 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). @@ -122,4 +122,8 @@ 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 +cargo test --no-default-features --test encrypted_domain -- --list | + sed -n 's/: test$//p' | + grep '^scalars::int8' | + LC_ALL=C sort > snapshots/int8_matrix_tests.txt """ diff --git a/tasks/codegen/scalars.py b/tasks/codegen/scalars.py index eee01dc4..cbe6cb87 100644 --- a/tasks/codegen/scalars.py +++ b/tasks/codegen/scalars.py @@ -88,6 +88,15 @@ def render_literal(self, value: str) -> str: min_value=-32768, max_value=32767, ), + "int8": ScalarKind( + token="int8", + rust_type="i64", + min_symbol="i64::MIN", + max_symbol="i64::MAX", + zero_symbol="0", + min_value=-9223372036854775808, + max_value=9223372036854775807, + ), } diff --git a/tasks/codegen/test_scalars.py b/tasks/codegen/test_scalars.py index 1f15f1c3..42f97357 100644 --- a/tasks/codegen/test_scalars.py +++ b/tasks/codegen/test_scalars.py @@ -80,3 +80,21 @@ def test_int2_kind_rejects_out_of_range(): kind = require_scalar("int2") with pytest.raises(ScalarError, match="out of range"): kind.numeric_value("40000") + + +def test_int8_kind_resolves_and_renders(): + kind = require_scalar("int8") + assert kind.rust_type == "i64" + assert kind.numeric_value("MIN") == -9223372036854775808 + assert kind.numeric_value("MAX") == 9223372036854775807 + assert kind.numeric_value("ZERO") == 0 + assert kind.render_literal("MIN") == "i64::MIN" + assert kind.render_literal("MAX") == "i64::MAX" + assert kind.render_literal("ZERO") == "0" + assert kind.render_literal("5000000000") == "5000000000" + + +def test_int8_kind_rejects_out_of_range(): + kind = require_scalar("int8") + with pytest.raises(ScalarError, match="out of range"): + kind.numeric_value("9223372036854775808") # i64::MAX + 1 diff --git a/tasks/codegen/test_spec.py b/tasks/codegen/test_spec.py index 151a03cb..709142fc 100644 --- a/tasks/codegen/test_spec.py +++ b/tasks/codegen/test_spec.py @@ -199,10 +199,10 @@ def test_fixture_values_reject_sentinel_literal_alias(tmp_path): def test_fixture_for_unknown_scalar_token_raises(tmp_path): bad = textwrap.dedent(""" [domain] - int8 = [] + bogus = [] [fixture] values = ["1"] """) - with pytest.raises(SpecError, match="unknown scalar token 'int8'"): - load_spec(write(tmp_path, "int8.toml", bad)) + with pytest.raises(SpecError, match="unknown scalar token 'bogus'"): + load_spec(write(tmp_path, "bogus.toml", bad)) diff --git a/tasks/codegen/types/int8.toml b/tasks/codegen/types/int8.toml new file mode 100644 index 00000000..bf4511c8 --- /dev/null +++ b/tasks/codegen/types/int8.toml @@ -0,0 +1,20 @@ +# Encrypted-domain scalar manifest for int8. +# The filename supplies the type token. Each domain lists the index terms +# it carries; term capabilities are fixed in tasks/codegen/terms.py. + +[domain] +int8 = [] +int8_eq = ["hm"] +int8_ord_ore = ["ore"] +int8_ord = ["ore"] + +# Single source of truth for the int8 fixture plaintext list. Drives the +# generated tests/sqlx/src/fixtures/int8_values.rs const, shared by the fixture +# generator and the matrix oracle. Sentinels MIN/MAX/ZERO map to i64 named +# consts; the set MUST include MIN, MAX, and zero (matrix comparison pivots). +# Includes values outside the int4 range (|x| > 2^31) to exercise 64-bit width. +[fixture] +values = [ + "MIN", "-5000000000", "-100", "-1", "ZERO", "1", "2", "5", "10", "17", "25", + "42", "50", "100", "250", "1000", "9999", "5000000000", "MAX", +] diff --git a/tests/sqlx/snapshots/int8_matrix_tests.txt b/tests/sqlx/snapshots/int8_matrix_tests.txt new file mode 100644 index 00000000..f4d3cc7d --- /dev/null +++ b/tests/sqlx/snapshots/int8_matrix_tests.txt @@ -0,0 +1,211 @@ +scalars::int8::matrix_int8_eq_aggregate_typecheck_max +scalars::int8::matrix_int8_eq_aggregate_typecheck_min +scalars::int8::matrix_int8_eq_contained_by_blocker +scalars::int8::matrix_int8_eq_contains_blocker +scalars::int8::matrix_int8_eq_count_distinct_extractor +scalars::int8::matrix_int8_eq_count_path_cast +scalars::int8::matrix_int8_eq_count_typed_column +scalars::int8::matrix_int8_eq_eq_pivot_max_correctness +scalars::int8::matrix_int8_eq_eq_pivot_max_cross_shape +scalars::int8::matrix_int8_eq_eq_pivot_min_correctness +scalars::int8::matrix_int8_eq_eq_pivot_min_cross_shape +scalars::int8::matrix_int8_eq_eq_pivot_zero_correctness +scalars::int8::matrix_int8_eq_eq_pivot_zero_cross_shape +scalars::int8::matrix_int8_eq_eq_supported_null +scalars::int8::matrix_int8_eq_gt_blocker +scalars::int8::matrix_int8_eq_gte_blocker +scalars::int8::matrix_int8_eq_index_engages_btree +scalars::int8::matrix_int8_eq_index_engages_hash +scalars::int8::matrix_int8_eq_lt_blocker +scalars::int8::matrix_int8_eq_lte_blocker +scalars::int8::matrix_int8_eq_native_absent_ops +scalars::int8::matrix_int8_eq_neq_pivot_max_correctness +scalars::int8::matrix_int8_eq_neq_pivot_max_cross_shape +scalars::int8::matrix_int8_eq_neq_pivot_min_correctness +scalars::int8::matrix_int8_eq_neq_pivot_min_cross_shape +scalars::int8::matrix_int8_eq_neq_pivot_zero_correctness +scalars::int8::matrix_int8_eq_neq_pivot_zero_cross_shape +scalars::int8::matrix_int8_eq_neq_supported_null +scalars::int8::matrix_int8_eq_path_op_blockers +scalars::int8::matrix_int8_eq_payload_check +scalars::int8::matrix_int8_eq_planner_metadata_eq +scalars::int8::matrix_int8_eq_sanity +scalars::int8::matrix_int8_eq_typed_column_blocker +scalars::int8::matrix_int8_fixture_shape +scalars::int8::matrix_int8_ord_aggregate_group_by_max +scalars::int8::matrix_int8_ord_aggregate_group_by_min +scalars::int8::matrix_int8_ord_aggregate_max +scalars::int8::matrix_int8_ord_aggregate_max_all_null +scalars::int8::matrix_int8_ord_aggregate_max_empty +scalars::int8::matrix_int8_ord_aggregate_max_mixed_null +scalars::int8::matrix_int8_ord_aggregate_min +scalars::int8::matrix_int8_ord_aggregate_min_all_null +scalars::int8::matrix_int8_ord_aggregate_min_empty +scalars::int8::matrix_int8_ord_aggregate_min_mixed_null +scalars::int8::matrix_int8_ord_aggregate_parallel_safe +scalars::int8::matrix_int8_ord_contained_by_blocker +scalars::int8::matrix_int8_ord_contains_blocker +scalars::int8::matrix_int8_ord_count_distinct_extractor +scalars::int8::matrix_int8_ord_count_path_cast +scalars::int8::matrix_int8_ord_count_typed_column +scalars::int8::matrix_int8_ord_eq_pivot_max_correctness +scalars::int8::matrix_int8_ord_eq_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_eq_pivot_min_correctness +scalars::int8::matrix_int8_ord_eq_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_eq_pivot_zero_correctness +scalars::int8::matrix_int8_ord_eq_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_eq_supported_null +scalars::int8::matrix_int8_ord_gt_pivot_max_correctness +scalars::int8::matrix_int8_ord_gt_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_gt_pivot_min_correctness +scalars::int8::matrix_int8_ord_gt_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_gt_pivot_zero_correctness +scalars::int8::matrix_int8_ord_gt_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_gt_supported_null +scalars::int8::matrix_int8_ord_gte_pivot_max_correctness +scalars::int8::matrix_int8_ord_gte_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_gte_pivot_min_correctness +scalars::int8::matrix_int8_ord_gte_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_gte_pivot_zero_correctness +scalars::int8::matrix_int8_ord_gte_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_gte_supported_null +scalars::int8::matrix_int8_ord_index_engages_btree +scalars::int8::matrix_int8_ord_lt_pivot_max_correctness +scalars::int8::matrix_int8_ord_lt_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_lt_pivot_min_correctness +scalars::int8::matrix_int8_ord_lt_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_lt_pivot_zero_correctness +scalars::int8::matrix_int8_ord_lt_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_lt_supported_null +scalars::int8::matrix_int8_ord_lte_pivot_max_correctness +scalars::int8::matrix_int8_ord_lte_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_lte_pivot_min_correctness +scalars::int8::matrix_int8_ord_lte_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_lte_pivot_zero_correctness +scalars::int8::matrix_int8_ord_lte_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_lte_supported_null +scalars::int8::matrix_int8_ord_native_absent_ops +scalars::int8::matrix_int8_ord_neq_pivot_max_correctness +scalars::int8::matrix_int8_ord_neq_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_neq_pivot_min_correctness +scalars::int8::matrix_int8_ord_neq_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_neq_pivot_zero_correctness +scalars::int8::matrix_int8_ord_neq_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_neq_supported_null +scalars::int8::matrix_int8_ord_ord_routes_through_ob +scalars::int8::matrix_int8_ord_order_by_asc_no_where +scalars::int8::matrix_int8_ord_order_by_asc_nulls_first +scalars::int8::matrix_int8_ord_order_by_asc_nulls_last +scalars::int8::matrix_int8_ord_order_by_asc_with_where +scalars::int8::matrix_int8_ord_order_by_desc_no_where +scalars::int8::matrix_int8_ord_order_by_desc_nulls_first +scalars::int8::matrix_int8_ord_order_by_desc_nulls_last +scalars::int8::matrix_int8_ord_order_by_desc_with_where +scalars::int8::matrix_int8_ord_order_by_using_gt_rejects +scalars::int8::matrix_int8_ord_order_by_using_gte_rejects +scalars::int8::matrix_int8_ord_order_by_using_lt_rejects +scalars::int8::matrix_int8_ord_order_by_using_lte_rejects +scalars::int8::matrix_int8_ord_ore_aggregate_group_by_max +scalars::int8::matrix_int8_ord_ore_aggregate_group_by_min +scalars::int8::matrix_int8_ord_ore_aggregate_max +scalars::int8::matrix_int8_ord_ore_aggregate_max_all_null +scalars::int8::matrix_int8_ord_ore_aggregate_max_empty +scalars::int8::matrix_int8_ord_ore_aggregate_max_mixed_null +scalars::int8::matrix_int8_ord_ore_aggregate_min +scalars::int8::matrix_int8_ord_ore_aggregate_min_all_null +scalars::int8::matrix_int8_ord_ore_aggregate_min_empty +scalars::int8::matrix_int8_ord_ore_aggregate_min_mixed_null +scalars::int8::matrix_int8_ord_ore_aggregate_parallel_safe +scalars::int8::matrix_int8_ord_ore_contained_by_blocker +scalars::int8::matrix_int8_ord_ore_contains_blocker +scalars::int8::matrix_int8_ord_ore_count_distinct_extractor +scalars::int8::matrix_int8_ord_ore_count_path_cast +scalars::int8::matrix_int8_ord_ore_count_typed_column +scalars::int8::matrix_int8_ord_ore_eq_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_eq_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_eq_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_eq_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_eq_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_eq_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_eq_supported_null +scalars::int8::matrix_int8_ord_ore_gt_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_gt_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_gt_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_gt_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_gt_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_gt_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_gt_supported_null +scalars::int8::matrix_int8_ord_ore_gte_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_gte_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_gte_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_gte_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_gte_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_gte_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_gte_supported_null +scalars::int8::matrix_int8_ord_ore_index_engages_btree +scalars::int8::matrix_int8_ord_ore_lt_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_lt_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_lt_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_lt_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_lt_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_lt_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_lt_supported_null +scalars::int8::matrix_int8_ord_ore_lte_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_lte_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_lte_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_lte_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_lte_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_lte_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_lte_supported_null +scalars::int8::matrix_int8_ord_ore_native_absent_ops +scalars::int8::matrix_int8_ord_ore_neq_pivot_max_correctness +scalars::int8::matrix_int8_ord_ore_neq_pivot_max_cross_shape +scalars::int8::matrix_int8_ord_ore_neq_pivot_min_correctness +scalars::int8::matrix_int8_ord_ore_neq_pivot_min_cross_shape +scalars::int8::matrix_int8_ord_ore_neq_pivot_zero_correctness +scalars::int8::matrix_int8_ord_ore_neq_pivot_zero_cross_shape +scalars::int8::matrix_int8_ord_ore_neq_supported_null +scalars::int8::matrix_int8_ord_ore_ord_routes_through_ob +scalars::int8::matrix_int8_ord_ore_order_by_asc_no_where +scalars::int8::matrix_int8_ord_ore_order_by_asc_nulls_first +scalars::int8::matrix_int8_ord_ore_order_by_asc_nulls_last +scalars::int8::matrix_int8_ord_ore_order_by_asc_with_where +scalars::int8::matrix_int8_ord_ore_order_by_desc_no_where +scalars::int8::matrix_int8_ord_ore_order_by_desc_nulls_first +scalars::int8::matrix_int8_ord_ore_order_by_desc_nulls_last +scalars::int8::matrix_int8_ord_ore_order_by_desc_with_where +scalars::int8::matrix_int8_ord_ore_order_by_using_gt_rejects +scalars::int8::matrix_int8_ord_ore_order_by_using_gte_rejects +scalars::int8::matrix_int8_ord_ore_order_by_using_lt_rejects +scalars::int8::matrix_int8_ord_ore_order_by_using_lte_rejects +scalars::int8::matrix_int8_ord_ore_ore_injectivity +scalars::int8::matrix_int8_ord_ore_path_op_blockers +scalars::int8::matrix_int8_ord_ore_payload_check +scalars::int8::matrix_int8_ord_ore_planner_metadata_eq +scalars::int8::matrix_int8_ord_ore_planner_metadata_ord +scalars::int8::matrix_int8_ord_ore_sanity +scalars::int8::matrix_int8_ord_ore_typed_column_blocker +scalars::int8::matrix_int8_ord_path_op_blockers +scalars::int8::matrix_int8_ord_payload_check +scalars::int8::matrix_int8_ord_planner_metadata_eq +scalars::int8::matrix_int8_ord_planner_metadata_ord +scalars::int8::matrix_int8_ord_sanity +scalars::int8::matrix_int8_ord_scale_preference_default_btree +scalars::int8::matrix_int8_ord_typed_column_blocker +scalars::int8::matrix_int8_storage_aggregate_typecheck_max +scalars::int8::matrix_int8_storage_aggregate_typecheck_min +scalars::int8::matrix_int8_storage_contained_by_blocker +scalars::int8::matrix_int8_storage_contains_blocker +scalars::int8::matrix_int8_storage_count_path_cast +scalars::int8::matrix_int8_storage_count_typed_column +scalars::int8::matrix_int8_storage_eq_blocker +scalars::int8::matrix_int8_storage_gt_blocker +scalars::int8::matrix_int8_storage_gte_blocker +scalars::int8::matrix_int8_storage_lt_blocker +scalars::int8::matrix_int8_storage_lte_blocker +scalars::int8::matrix_int8_storage_native_absent_ops +scalars::int8::matrix_int8_storage_neq_blocker +scalars::int8::matrix_int8_storage_path_op_blockers +scalars::int8::matrix_int8_storage_payload_check +scalars::int8::matrix_int8_storage_sanity +scalars::int8::matrix_int8_storage_typed_column_blocker diff --git a/tests/sqlx/src/fixtures/eql_plaintext.rs b/tests/sqlx/src/fixtures/eql_plaintext.rs index 36b348fd..df0bdc57 100644 --- a/tests/sqlx/src/fixtures/eql_plaintext.rs +++ b/tests/sqlx/src/fixtures/eql_plaintext.rs @@ -54,6 +54,7 @@ pub struct PlaintextSqlType(&'static str); impl PlaintextSqlType { pub const INTEGER: PlaintextSqlType = PlaintextSqlType("integer"); pub const SMALLINT: PlaintextSqlType = PlaintextSqlType("smallint"); + pub const BIGINT: PlaintextSqlType = PlaintextSqlType("bigint"); pub fn as_str(&self) -> &'static str { self.0 @@ -70,6 +71,7 @@ mod sealed { pub trait Sealed {} impl Sealed for i32 {} impl Sealed for i16 {} + impl Sealed for i64 {} } /// A Rust type usable as a fixture `plaintext` value, carrying its EQL cast @@ -107,6 +109,15 @@ impl EqlPlaintext for i16 { } } +impl EqlPlaintext for i64 { + const CAST: Cast = Cast::BIG_INT; + const PLAINTEXT_SQL_TYPE: PlaintextSqlType = PlaintextSqlType::BIGINT; + + fn to_plaintext(&self) -> Plaintext { + Plaintext::BigInt(Some(*self)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -156,4 +167,24 @@ mod tests { other => panic!("expected Plaintext::SmallInt(Some(42)), got {other:?}"), } } + + #[test] + fn i64_casts_to_big_int() { + assert_eq!(::CAST.as_str(), "big_int"); + } + + #[test] + fn i64_plaintext_sql_type_is_bigint() { + assert_eq!(::PLAINTEXT_SQL_TYPE.as_str(), "bigint"); + } + + #[test] + fn i64_to_plaintext_wraps_in_big_int_variant() { + // i64 must lift into the BigInt variant so the fixture driver + // encrypts it under the `big_int` cast, not `int`. + match 42_i64.to_plaintext() { + Plaintext::BigInt(Some(value)) => assert_eq!(value, 42), + other => panic!("expected Plaintext::BigInt(Some(42)), got {other:?}"), + } + } } diff --git a/tests/sqlx/src/fixtures/eql_v2_int8.rs b/tests/sqlx/src/fixtures/eql_v2_int8.rs new file mode 100644 index 00000000..ff72fe44 --- /dev/null +++ b/tests/sqlx/src/fixtures/eql_v2_int8.rs @@ -0,0 +1,12 @@ +//! The `eql_v2_int8` fixture — the int4 reference, widened to 64 bits. +//! +//! 19 integers spanning a negative boundary, the i64 signed extremes +//! (`MIN`/`MAX`), zero, a pair beyond the ±2^31 int4 boundary, and +//! small/medium/large magnitudes. The generated +//! `tests/sqlx/fixtures/eql_v2_int8.sql` is a plain `jsonb`-payload table with +//! no EQL dependency; the `eql_v2_int8` domain is layered on top by casting +//! `payload` per query. + +use super::int8_values::VALUES; + +crate::scalar_fixture!("eql_v2_int8", i64, VALUES); diff --git a/tests/sqlx/src/fixtures/int8_values.rs b/tests/sqlx/src/fixtures/int8_values.rs new file mode 100644 index 00000000..c56d7ec9 --- /dev/null +++ b/tests/sqlx/src/fixtures/int8_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 int8 encrypted-domain family. +//! +//! Generated from tasks/codegen/types/int8.toml `[fixture] values` — +//! the single source of truth shared by the fixture generator +//! (`fixtures::eql_v2_int8`) and the matrix oracle +//! (`ScalarType::FIXTURE_VALUES`). + +/// Distinct plaintext values present in the `eql_v2_int8` fixture. +pub const VALUES: &[i64] = &[ + i64::MIN, + -5000000000, + -100, + -1, + 0, + 1, + 2, + 5, + 10, + 17, + 25, + 42, + 50, + 100, + 250, + 1000, + 9999, + 5000000000, + i64::MAX, +]; diff --git a/tests/sqlx/src/fixtures/mod.rs b/tests/sqlx/src/fixtures/mod.rs index ac363a49..dddf2993 100644 --- a/tests/sqlx/src/fixtures/mod.rs +++ b/tests/sqlx/src/fixtures/mod.rs @@ -37,3 +37,9 @@ pub mod eql_v2_int4; pub mod int2_values; pub mod eql_v2_int2; + +/// Generated from tasks/codegen/types/int8.toml `[fixture] values`. +/// Committed and verified by CI; never hand-edit (`mise run codegen:domain int8`). +pub mod int8_values; + +pub mod eql_v2_int8; diff --git a/tests/sqlx/src/scalar_domains.rs b/tests/sqlx/src/scalar_domains.rs index c3acc428..b53a2bcc 100644 --- a/tests/sqlx/src/scalar_domains.rs +++ b/tests/sqlx/src/scalar_domains.rs @@ -90,6 +90,16 @@ impl ScalarType for i16 { const FIXTURE_VALUES: &'static [i16] = crate::fixtures::int2_values::VALUES; } +impl ScalarType for i64 { + const PG_TYPE: &'static str = "int8"; + /// Single-sourced from `tasks/codegen/types/int8.toml` `[fixture] values` + /// via the generated `fixtures::int8_values::VALUES` const — the same list + /// the fixture generator encrypts, so the oracle cannot drift from the + /// fixture. Spans negative/positive values outside the int4 range, the i64 + /// signed extremes, and zero. + const FIXTURE_VALUES: &'static [i64] = crate::fixtures::int8_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/int8.rs b/tests/sqlx/tests/encrypted_domain/scalars/int8.rs new file mode 100644 index 00000000..eb20fbee --- /dev/null +++ b/tests/sqlx/tests/encrypted_domain/scalars/int8.rs @@ -0,0 +1,14 @@ +//! `eql_v2_int8` — the int4 reference scalar, widened to 64 bits. +//! +//! Adding a new ordered numeric scalar (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 = int8, + scalar = i64, + eql_type = "eql_v2_int8", +} diff --git a/tests/sqlx/tests/encrypted_domain/scalars/mod.rs b/tests/sqlx/tests/encrypted_domain/scalars/mod.rs index f42cfb5f..f5895526 100644 --- a/tests/sqlx/tests/encrypted_domain/scalars/mod.rs +++ b/tests/sqlx/tests/encrypted_domain/scalars/mod.rs @@ -1,6 +1,8 @@ //! Per-scalar tests. Each subdirectory targets one scalar type; future -//! additions (`int8`, `bool`, `date`, …) become sibling modules here. +//! additions (`bool`, `date`, …) become sibling modules here. pub mod int4; pub mod int2; + +pub mod int8; From 4bf98cd2309731836fb6dbd85c6b91ad22f8e36b Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 1 Jun 2026 17:42:27 +1000 Subject: [PATCH 2/5] refactor(matrix): enumerate matrix inventory snapshots from type manifests Replace the three hardcoded per-type grep blocks in test:matrix:inventory with a loop over tasks/codegen/types/*.toml (manifests with a [fixture] table), mirroring fixture:generate:all. Run cargo --list once and reuse it. Add a manifest<->snapshot reconciliation so a stale snapshot from a removed type fails the task. (#249) --- mise.toml | 74 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/mise.toml b/mise.toml index b972fdc9..f92ab60d 100644 --- a/mise.toml +++ b/mise.toml @@ -99,31 +99,71 @@ mise exec python -- python -m pytest tasks/codegen -q """ [tasks."test:matrix:inventory"] -description = "Regenerate the int4/int2/int8 matrix test-name inventory snapshots (no database required)" +description = "Regenerate the per-scalar matrix test-name inventory snapshots from type manifests (no database required)" dir = "{{config_root}}/tests/sqlx" run = """ +# Regenerate one committed snapshot per scalar encrypted-domain type, enumerated +# dynamically from the type manifests — the SAME source of truth that +# `fixture:generate:all` and `codegen:domain:all` use. Adding a new scalar type +# (a new .toml with a [fixture] table) is picked up automatically; this task +# never hand-lists a type. The manifests live at the repo root, but this task +# runs in tests/sqlx (dir = .../tests/sqlx), so the glob is ../../tasks/... +# # Pin an explicit feature set so the inventory is deterministic regardless of # the caller's local flags. `--no-default-features` keeps the `scale` arm # (`#[cfg(feature = "scale")]`) excluded — its add/delete is a known blind spot # 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, scalars::int2, scalars::int8); 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). +# family::mutations, scalars:: for each scalar); 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). `--list` is run ONCE and +# reused — its output is deterministic, so the single invocation is faithful and +# avoids three redundant cargo passes. 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 -cargo test --no-default-features --test encrypted_domain -- --list | - sed -n 's/: test$//p' | - grep '^scalars::int8' | - LC_ALL=C sort > snapshots/int8_matrix_tests.txt + +# One enumeration pass for the whole binary, reused per type below. +listing=$(cargo test --no-default-features --test encrypted_domain -- --list | sed -n 's/: test$//p') + +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 participate in the matrix suite. + grep -qE '^\\[fixture\\]' "$manifest" || continue + token=$(basename "$manifest" .toml) + printf '%s\\n' "$listing" | + grep "^scalars::${token}::" | + LC_ALL=C sort > "snapshots/${token}_matrix_tests.txt" + # A manifest with a [fixture] table MUST have matching matrix tests. An empty + # snapshot means the scalars:: module is missing or mis-named — fail + # loudly rather than committing an empty baseline. + if [ ! -s "snapshots/${token}_matrix_tests.txt" ]; then + echo "No 'scalars::${token}::*' tests found for manifest ${manifest} — matrix suite missing or mis-named." >&2 + exit 1 + fi + 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 + +# Reconcile the other direction: every committed snapshot must map to a manifest +# with a [fixture] table. A stale snapshot left behind when a scalar type is +# removed would otherwise silently survive (git diff sees no change to it). +for snapshot in snapshots/*_matrix_tests.txt; do + [ -e "$snapshot" ] || continue + token=$(basename "$snapshot" _matrix_tests.txt) + manifest="../../tasks/codegen/types/${token}.toml" + if [ ! -f "$manifest" ] || ! grep -qE '^\\[fixture\\]' "$manifest"; then + echo "Stale snapshot ${snapshot}: no type manifest with a [fixture] table at ${manifest}. Remove the snapshot or restore the manifest." >&2 + exit 1 + fi +done + +echo "Regenerated ${generated} matrix inventory snapshot(s)." """ From e09e63ecba4e62c4d5d7a667192b13c5554de529 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 1 Jun 2026 17:42:52 +1000 Subject: [PATCH 3/5] ci(matrix): diff whole snapshots/ dir instead of hardcoding type files Replace the hardcoded int4/int2/int8 git diff in the matrix-coverage job with a whole-directory diff plus git add -N, so adding a scalar type needs no CI edit and an uncommitted new snapshot also fails the job. (#249) --- .github/workflows/test-eql.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index a4d6c49f..9f7c38b9 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -120,10 +120,15 @@ jobs: - name: Regenerate and verify the matrix test-name inventory run: | mise run test:matrix:inventory - git diff --exit-code -- tests/sqlx/snapshots/int4_matrix_tests.txt \ - tests/sqlx/snapshots/int2_matrix_tests.txt \ - tests/sqlx/snapshots/int8_matrix_tests.txt \ - || { echo "Coverage inventory stale — run 'mise run test:matrix:inventory' and commit."; exit 1; } + # `git add -N` registers any brand-new untracked snapshot (e.g. a new + # scalar type whose baseline was never committed) so a forgotten + # commit also trips the diff — `git diff --exit-code` ignores wholly + # untracked files otherwise. Diff the whole snapshots/ directory so no + # per-type file is hardcoded here; the mise task already enumerates the + # type set from the manifests and fails on a missing/stale snapshot. + git add -N tests/sqlx/snapshots + git diff --exit-code -- tests/sqlx/snapshots \ + || { echo "Coverage inventory stale or uncommitted — run 'mise run test:matrix:inventory' and commit tests/sqlx/snapshots."; exit 1; } test: name: "Test & Validate EQL (Postgres ${{ matrix.postgres-version }})" From 671d5e23b5f7d446d4ae35a7028a689651d5c8d9 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 1 Jun 2026 17:43:31 +1000 Subject: [PATCH 4/5] docs(matrix): describe dynamic manifest-driven snapshot enumeration Stop hand-listing scalar types (was missing int8) and document the manifest-enumeration + reconciliation behaviour. (#249) --- tests/sqlx/snapshots/README.md | 48 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/tests/sqlx/snapshots/README.md b/tests/sqlx/snapshots/README.md index a4ce5ae9..7e4082b8 100644 --- a/tests/sqlx/snapshots/README.md +++ b/tests/sqlx/snapshots/README.md @@ -1,12 +1,11 @@ # 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 +This directory holds one committed snapshot per scalar encrypted-domain type, +named `_matrix_tests.txt` (one per type manifest in +`tasks/codegen/types/` that declares a `[fixture]` table — currently `int2`, +`int4`, `int8`, but the set is **not** hand-listed anywhere). 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 @@ -25,11 +24,21 @@ Run: 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. +The task (`mise.toml`, `[tasks."test:matrix:inventory"]`) enumerates the +`encrypted_domain` binary **once** with +`cargo test --no-default-features --test encrypted_domain -- --list`, then loops +over every manifest in `tasks/codegen/types/*.toml` that declares a `[fixture]` +table — the SAME source of truth `fixture:generate:all` and `codegen:domain:all` +use — derives the type token from the manifest filename, greps that +`scalars::::` matrix into its own file, and `LC_ALL=C sort`s for ordering +that is byte-stable across locales. Adding a new scalar type is picked up +automatically; no list of types is maintained in the task or in CI. No database +is required — `--list` only enumerates; the suite uses runtime queries. + +The task also reconciles both directions and fails if a manifest with a +`[fixture]` table produces no `scalars::::*` tests, or if a +`_matrix_tests.txt` snapshot has no matching manifest (a stale snapshot left +behind when a type is removed). It pins `--no-default-features` so the inventory is deterministic regardless of the caller's local flags. That deliberately excludes the `scale` feature arm @@ -39,14 +48,21 @@ 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: +the same pinned feature set, runs `git add -N tests/sqlx/snapshots`, then +`git diff --exit-code -- tests/sqlx/snapshots` over the whole directory (no +per-type file is hardcoded in CI). The `git add -N` makes a brand-new, +never-committed snapshot trip the diff too. A divergence fails the job with: -> Coverage inventory stale — run 'mise run test:matrix:inventory' and commit. +> Coverage inventory stale or uncommitted — run 'mise run test:matrix:inventory' and commit tests/sqlx/snapshots. ## When you must update these -- **Adding a new scalar type** → a new `_matrix_tests.txt` appears; commit it. +- **Adding a new scalar type** → add its `tasks/codegen/types/.toml` manifest + (with a `[fixture]` table), run `mise run test:matrix:inventory`, and commit + the new `_matrix_tests.txt` it generates. No task or CI edit is needed — + the type set is enumerated from the manifests. +- **Removing a scalar type** → delete its manifest and the matching snapshot in + the same change; the reconciliation check fails on a stale snapshot otherwise. - **Adding / removing / renaming matrix tests** → regenerate and commit the affected snapshot in the same change. From 84826a796d9d1d7c613e6206bf25e3f0fb07d6fe Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 1 Jun 2026 17:49:43 +1000 Subject: [PATCH 5/5] docs(encrypted-domain): document matrix-oracle wiring and #249 enumeration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streamline the add-a-scalar-type process in the implementation spec: - Add a six-file Rust matrix-oracle wiring sub-checklist to §2 (the EqlPlaintext impl, scalar_fixture! invocation, the two mod.rs declarations, the ScalarType impl, and the ordered_numeric_matrix! invocation) — previously undocumented and reverse-engineered from int4. - Record that adding a type no longer requires a mise.toml or CI edit (#249): the inventory task and matrix-coverage job enumerate the type set from the [fixture] manifests and reconcile both directions. Fix the quoted CI error string ('stale or uncommitted'). - Trim §8 to cross-link tests/sqlx/snapshots/README.md as the single source of truth for the inventory mechanics, removing the duplicated detail that had already drifted (README was missing int8). --- .../encrypted-domain-implementation-spec.md | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/docs/reference/encrypted-domain-implementation-spec.md b/docs/reference/encrypted-domain-implementation-spec.md index e21c8d6a..343a4e72 100644 --- a/docs/reference/encrypted-domain-implementation-spec.md +++ b/docs/reference/encrypted-domain-implementation-spec.md @@ -92,12 +92,38 @@ future migration. 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). +- [ ] Wire the SQLx matrix oracle. The generated SQL is enough to install the + domains, but the `ordered_numeric_matrix!` suite only runs once the Rust + harness knows about the scalar. Copy each piece from the `int4`/`int8` + reference — six files, each a small registration: + + | File | Add | + |------|-----| + | `tests/sqlx/src/fixtures/eql_plaintext.rs` | A sealed `EqlPlaintext` impl for the scalar's Rust type: `impl Sealed for {}`, a `PlaintextSqlType` const for its base column type, `impl EqlPlaintext for ` (`CAST`, `PLAINTEXT_SQL_TYPE`, `to_plaintext` → the right `Plaintext` variant), plus the two `#[test]` casts. | + | `tests/sqlx/src/fixtures/eql_v2_.rs` | `crate::scalar_fixture!("eql_v2_", , VALUES);` (pulls `super::_values::VALUES`). | + | `tests/sqlx/src/fixtures/mod.rs` | `pub mod _values;` and `pub mod eql_v2_;`. | + | `tests/sqlx/src/scalar_domains.rs` | `impl ScalarType for ` — `PG_TYPE` (the base PG type, e.g. `"int8"`) and `FIXTURE_VALUES = crate::fixtures::_values::VALUES`. | + | `tests/sqlx/tests/encrypted_domain/scalars/.rs` | `ordered_numeric_matrix! { suite = , scalar = , eql_type = "eql_v2_" }`. | + | `tests/sqlx/tests/encrypted_domain/scalars/mod.rs` | `pub mod ;`. | + + `` is the scalar's Rust type (`i32` for `int4`, `i64` for `int8`). The + two `mod.rs` declarations and the `ScalarType` / `EqlPlaintext` impls are + hand-maintained registration lists: forget one and the matrix simply does + not run for the type (the inventory snapshot in the next step is the guard + that surfaces it). - [ ] 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. + `scalars::::*` test name in the `encrypted_domain` binary. **You do not + edit `mise.toml` or `.github/workflows/test-eql.yml` for this** (#249): the + task enumerates every manifest with a `[fixture]` table and the CI job + diffs the whole `snapshots/` directory, so authoring the `.toml` + manifest is enough for the new snapshot to be generated and gated. The + task fails if a `[fixture]` manifest produces no `scalars::::*` tests + (oracle not wired — see the previous step) or if a snapshot has no manifest + (stale, from a removed type). A stale or uncommitted snapshot fails the CI + `matrix-coverage` job with "Coverage inventory stale or uncommitted". This + baseline is what catches a silently dropped, renamed, or `#[cfg]`-gated + matrix test. See §8 and `tests/sqlx/snapshots/README.md`. - [ ] Run `mise run test:codegen`, the relevant SQLx suites, and the PostgreSQL matrix before merging. @@ -283,21 +309,20 @@ 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`. +The *set of test names* the matrix emits is itself guarded by one committed +snapshot per scalar at `tests/sqlx/snapshots/_matrix_tests.txt` — the sorted +inventory of every `scalars::::*` test name. This is the guard that catches a +silently dropped, renamed, or `#[cfg]`-gated matrix test, a behaviour the SQLx +assertions above cannot see (a deleted test simply stops running). The snapshots +are a committed test baseline, **not** gitignored generated SQL. + +`mise run test:matrix:inventory` regenerates them and the CI `matrix-coverage` +job gates them; both enumerate the scalar set from the `[fixture]` manifests in +`tasks/codegen/types/` rather than a hand-maintained list, so adding `.toml` +is enough — no task or workflow edit. **`tests/sqlx/snapshots/README.md` is the +source of truth** for the mechanics (pinned feature set, the manifest⇄snapshot +reconciliation, the CI diff, and when to regenerate); see it rather than +duplicating the detail here. ## 9. Fixtures