Skip to content

Commit 095ab03

Browse files
committed
test(v3): rename property-test tiers to catalog/fixture/e2e (CIP-3141)
Replace the abstract 'Tier A/B/C' labels with concrete names matching what each suite does and where it lives: - Tier C -> catalog (unit, pure Rust, no DB) - Tier A -> fixture (integration, committed ciphertext) - Tier B -> e2e (integration, fresh ZeroKMS->Postgres each run) Full-depth rename: live_oracle.rs -> e2e_oracle.rs, cargo feature proptest-live -> proptest-e2e (Cargo.toml, mise.toml, #[cfg]), run_live_property -> run_e2e_property, *_oracle_live -> *_oracle_e2e, DB tables proptest_live_* -> proptest_e2e_*, plus all prose in comments, README, CHANGELOG and the implementation plan. Unrelated 'Tier 1/2' usages (benchmarks, pg_stat_statements, jsonb builders) left untouched. Test-only; no behaviour change.
1 parent ef1f04e commit 095ab03

11 files changed

Lines changed: 50 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Each entry that ships in a published release links to the PR that introduced it.
3333
- **`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))
3434
- **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.<symbol>` 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))
3535
- **`eql_v3.text` encrypted-domain family (`text`, `text_eq`, `text_match`, `text_ord`, `text_ord_ore`).** 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`). 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` domain — deliberately *not* SQL `LIKE` (no wildcard/anchoring; probabilistic ngram containment) — and never backs equality (which always routes through `Hm`). ([#260](https://github.com/cipherstash/encrypt-query-language/pull/260))
36-
- **Property-based tests for the `eql_v3` encrypted scalar domains.** A three-tier harness asserts SQL operator results agree with a plaintext oracle across a generated input space: a pure-Rust catalog-invariant tier (no database), a fixture-corpus tier that samples the live-encrypted fixtures and checks all ordered pairs in each sampled corpus, and a live-encryption tier (gated behind the `proptest-live` cargo feature) that batch-encrypts freshly generated plaintexts each run. Covers the equality (`=`/`<>`) and ordering (`<`/`<=`/`>`/`>=`, `ord_term` sort order) oracles plus NULL/blocker/CHECK edge cases. Why: the prior matrix exercised fixed pivots only; property tests catch operator/oracle disagreements across the whole value space. ([#275](https://github.com/cipherstash/encrypt-query-language/pull/275))
36+
- **Property-based tests for the `eql_v3` encrypted scalar domains.** A harness of three suites asserts SQL operator results agree with a plaintext oracle across a generated input space: a pure-Rust **catalog** suite (no database) over the term/scalar catalog, a **fixture** suite that samples the committed fixture corpus (real ciphertext) and checks all ordered pairs in each sampled corpus, and an **e2e** suite (gated behind the `proptest-e2e` cargo feature) that batch-encrypts freshly generated plaintexts end-to-end through ZeroKMS each run. Covers the equality (`=`/`<>`) and ordering (`<`/`<=`/`>`/`>=`, `ord_term` sort order) oracles plus NULL/blocker/CHECK edge cases. Why: the prior matrix exercised fixed pivots only; property tests catch operator/oracle disagreements across the whole value space. ([#275](https://github.com/cipherstash/encrypt-query-language/pull/275))
3737
- **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.<symbol>` 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))
3838
- **`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))
3939

crates/eql-scalars/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ workspace = true
1515
# Dev-only. proptest is NOT a runtime dependency: it never compiles on the SQL
1616
# build path (eql-codegen depends on eql-scalars' lib, not its tests), so the
1717
# "INTENTIONALLY no dependencies" rule above — which is about build-path deps —
18-
# is preserved. Used by src/proptest_invariants.rs (Tier C catalog property
19-
# tests, no DB, runs in the lean `mise run test:crates` / fork CI path).
18+
# is preserved. Used by src/proptest_invariants.rs (the catalog suite of
19+
# property tests, no DB, runs in the lean `mise run test:crates` / fork CI path).
2020
[dev-dependencies]
2121
proptest = "1"

crates/eql-scalars/src/proptest_invariants.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
//! Property-based invariants over the scalar/term catalog (CIP-3141, Tier C).
1+
//! Catalog suite (CIP-3141): property-based invariants over the scalar/term catalog.
22
//!
33
//! Pure Rust — no database, no encryption, no creds. These run in the lean
44
//! `cargo test -p eql-scalars` path (fork CI). They assert the *catalog* is
5-
//! internally consistent for any generated input; the DB-backed oracle tiers
6-
//! (A/B) live in `tests/sqlx`.
5+
//! internally consistent for any generated input; the DB-backed oracle suites
6+
//! (fixture/e2e) live in `tests/sqlx`.
77
88
use crate::{ScalarKind, Term, CATALOG};
99
use proptest::prelude::*;

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ depends = ["test:sqlx:prep"]
8686
dir = "{{config_root}}/tests/sqlx"
8787
run = """
8888
echo "Running Rust tests..."
89-
cargo test --features proptest-live
89+
cargo test --features proptest-e2e
9090
"""
9191

9292
[tasks."test:sqlx:watch"]

tests/sqlx/Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ bench = []
4545
# *usable*. Off by default to keep `mise run test` fast; CI runs with
4646
# `--features scale`.
4747
scale = []
48-
# Opt-in to the live-encryption property tests (Tier B, CIP-3141). They
49-
# generate fresh random plaintexts each run and encrypt them via
50-
# cipherstash-client, so they need the same CS_* creds as fixture generation.
48+
# Opt-in to the e2e property suite (CIP-3141). It generates fresh random
49+
# plaintexts each run and encrypts them end-to-end through ZeroKMS via
50+
# cipherstash-client, so it needs the same CS_* creds as fixture generation.
5151
# `mise run test:sqlx` enables this (CI has the secrets). A bare `cargo test`
52-
# without it compiles the live tier out, so credential-less quick runs and the
53-
# lean crate path are unaffected. Tier A (fixture-corpus) and Tier C (catalog)
54-
# are NOT gated — they need no fresh encryption.
55-
proptest-live = []
52+
# without it compiles the e2e suite out, so credential-less quick runs and the
53+
# lean crate path are unaffected. The fixture suite and the catalog suite are
54+
# NOT gated — they need no fresh encryption.
55+
proptest-e2e = []
5656
# Opt-in to compiling the fixture generators. Without this feature the
5757
# `#[cfg(feature = "fixture-gen")]` generator tests do not exist, so
5858
# `cargo test` and CI never see them. Generators need a live Postgres and,

tests/sqlx/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,9 @@ Tests connect to PostgreSQL database configured by SQLx:
276276

277277
-~~Convert remaining SQL tests~~ **COMPLETE!**
278278
- Property-based tests: implemented in `tests/encrypted_domain/property/` and
279-
`crates/eql-scalars/src/proptest_invariants.rs` (CIP-3141). Three tiers:
280-
catalog invariants (no DB), fixture-corpus oracle, and live-encryption oracle
281-
(`--features proptest-live`).
279+
`crates/eql-scalars/src/proptest_invariants.rs` (CIP-3141). One unit-level
280+
**catalog** suite (no DB) plus two integration suites — **fixture** (oracle
281+
over the committed fixture corpus) and **e2e** (oracle over fresh end-to-end
282+
encryption, `--features proptest-e2e`).
282283
- Performance benchmarks: Measure query performance with encrypted data
283284
- Integration tests: Test with CipherStash Proxy

tests/sqlx/src/property.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
//!
33
//! `assert_eq_oracle` / `assert_ord_oracle` take a corpus of
44
//! `(plaintext, payload_json)` rows and check SQL operator results against the
5-
//! plaintext oracle over every ordered pair. Tier A feeds them rows read from
6-
//! the live-encrypted fixture; Tier B feeds them rows it batch-encrypts from
7-
//! freshly generated plaintexts. The engine is identical for both.
5+
//! plaintext oracle over every ordered pair. The fixture suite feeds them rows
6+
//! read from the committed fixture corpus (real ciphertext); the e2e suite feeds
7+
//! them rows it batch-encrypts from freshly generated plaintexts. The engine is
8+
//! identical for both.
89
//!
910
//! Operator evaluation is read-only (`SELECT <a> op <b>`), so these helpers take
1011
//! a `&PgPool` and need no per-test schema isolation.
@@ -21,11 +22,11 @@ use tokio::sync::Mutex;
2122
/// `fixtures.eql_v2_<T>` table exactly once.
2223
static FIXTURE_LOADED: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
2324

24-
/// Materialise the live-encrypted fixture corpus for `T` into the connected DB.
25+
/// Materialise the committed fixture corpus (real ciphertext) for `T` into the connected DB.
2526
///
2627
/// The fixture `.sql` files (`tests/sqlx/fixtures/eql_v2_<T>.sql`) are normally
2728
/// loaded only into `#[sqlx::test]`'s ephemeral per-test databases. The property
28-
/// tiers connect to the shared test DB directly (they cannot use
29+
/// suites connect to the shared test DB directly (they cannot use
2930
/// `#[sqlx::test]`'s injected pool from a sync `proptest!` body), so the corpus
3031
/// is not present there. This loads it on demand: the script is self-contained
3132
/// and idempotent (`CREATE SCHEMA IF NOT EXISTS` / `DROP TABLE IF EXISTS` /
@@ -174,7 +175,7 @@ fn redact_url(url: &str) -> String {
174175

175176
/// Connect to the shared SQLx test database. Reads `DATABASE_URL`, falling back
176177
/// to the documented local default (`localhost:7432`, cipherstash/password).
177-
/// Used by the proptest tiers, which cannot use `#[sqlx::test]`'s injected pool
178+
/// Used by the proptest suites, which cannot use `#[sqlx::test]`'s injected pool
178179
/// from a (sync) `proptest!` body.
179180
pub async fn connect_pool() -> Result<PgPool> {
180181
let url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {

tests/sqlx/tests/encrypted_domain.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ mod signed;
3434
#[path = "encrypted_domain/jsonb_entry.rs"]
3535
mod jsonb_entry;
3636

37-
// Property-based + edge-case tests (CIP-3141). Three tiers under `property::`,
38-
// kept outside `scalars::` so the matrix-inventory gate does not mis-read them
39-
// as scalar types. See `encrypted_domain/property/mod.rs`.
37+
// Property-based + edge-case tests (CIP-3141). Three suites under `property::`
38+
// (catalog, fixture, e2e), kept outside `scalars::` so the matrix-inventory gate
39+
// does not mis-read them as scalar types. See `encrypted_domain/property/mod.rs`.
4040
#[path = "encrypted_domain/property/mod.rs"]
4141
mod property;

tests/sqlx/tests/encrypted_domain/property/live_oracle.rs renamed to tests/sqlx/tests/encrypted_domain/property/e2e_oracle.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
//! Tier B (CIP-3141): property tests over freshly generated, live-encrypted
2-
//! values. Gated behind `proptest-live` (declared in property/mod.rs) — needs
3-
//! CS_* creds, which `mise run test:sqlx` enables for CI/local full SQLx runs.
1+
//! e2e suite (CIP-3141): property tests over freshly generated values encrypted
2+
//! end-to-end through ZeroKMS each run. Gated behind `proptest-e2e` (declared in
3+
//! property/mod.rs) — needs CS_* creds, which `mise run test:sqlx` enables for
4+
//! CI/local full SQLx runs.
45
//! Each proptest case generates one corpus of random integers — seeded with
56
//! type-specific extremes, zero, and deliberate duplicates so the equality-true
67
//! branch fires across distinct ciphertexts of the same plaintext — encrypts it
@@ -47,7 +48,7 @@ where
4748

4849
/// Drive proptest: each case is a corpus of integers. Generation is in-process;
4950
/// encryption + oracle is async on a current-thread runtime.
50-
fn run_live_property<T>(
51+
fn run_e2e_property<T>(
5152
table: &str,
5253
cast: Cast,
5354
cases: u32,
@@ -64,9 +65,9 @@ where
6465
.build()?;
6566
let pool: PgPool = rt.block_on(connect_pool())?;
6667

67-
// Shrinking is disabled for the live tier: every failed shrink attempt would
68+
// Shrinking is disabled for the e2e suite: every failed shrink attempt would
6869
// trigger another ZeroKMS batch, and ciphertext cannot be meaningfully
69-
// shrunk anyway. Tier C keeps normal shrinking.
70+
// shrunk anyway. The catalog suite keeps normal shrinking.
7071
let mut runner = TestRunner::new(Config {
7172
cases,
7273
max_shrink_iters: 0,
@@ -102,14 +103,14 @@ where
102103
.map_err(|e| TestCaseError::fail(format!("oracle: {e}")))?;
103104
Ok(())
104105
})
105-
.map_err(|e| anyhow::anyhow!("live property failed: {e}"))
106+
.map_err(|e| anyhow::anyhow!("e2e property failed: {e}"))
106107
}
107108

108109
#[test]
109-
fn prop_int4_eq_and_ord_oracle_live() -> Result<()> {
110+
fn prop_int4_eq_and_ord_oracle_e2e() -> Result<()> {
110111
// Low case count: each case is a ZeroKMS round trip. 8 keeps CI bounded.
111-
run_live_property::<i32>(
112-
"proptest_live_int4",
112+
run_e2e_property::<i32>(
113+
"proptest_e2e_int4",
113114
Cast::INT,
114115
8,
115116
true,
@@ -118,9 +119,9 @@ fn prop_int4_eq_and_ord_oracle_live() -> Result<()> {
118119
}
119120

120121
#[test]
121-
fn prop_int2_eq_and_ord_oracle_live() -> Result<()> {
122-
run_live_property::<i16>(
123-
"proptest_live_int2",
122+
fn prop_int2_eq_and_ord_oracle_e2e() -> Result<()> {
123+
run_e2e_property::<i16>(
124+
"proptest_e2e_int2",
124125
Cast::SMALL_INT,
125126
8,
126127
true,
@@ -129,9 +130,9 @@ fn prop_int2_eq_and_ord_oracle_live() -> Result<()> {
129130
}
130131

131132
#[test]
132-
fn prop_int8_eq_and_ord_oracle_live() -> Result<()> {
133-
run_live_property::<i64>(
134-
"proptest_live_int8",
133+
fn prop_int8_eq_and_ord_oracle_e2e() -> Result<()> {
134+
run_e2e_property::<i64>(
135+
"proptest_e2e_int8",
135136
Cast::BIG_INT,
136137
8,
137138
true,

tests/sqlx/tests/encrypted_domain/property/fixture_oracle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Tier A (CIP-3141): property tests over the real, committed fixture corpus.
1+
//! fixture suite (CIP-3141): property tests over the real, committed fixture corpus.
22
//!
33
//! The fixture table `fixtures.eql_v2_<T>` carries `(plaintext, payload)` rows
44
//! encrypted by cipherstash-client during `test:sqlx:prep`. proptest selects a

0 commit comments

Comments
 (0)