From 58c7916996d2132734191d627f00f3bbf0b3a285 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Fri, 3 Jul 2026 23:28:11 +1000 Subject: [PATCH 1/3] chore(deps): bump cipherstash-client and stack-profile to 0.38.1 Whole cipherstash-suite lockstep moves to 0.38.1 (stack-auth, cts-common, cipherstash-config, cipherstash-core; zerokms-protocol stays 0.12.20). The client's EQL wire-format module is byte-identical across 0.34.1-alpha.9, 0.38.0, and 0.38.1 (verified by diffing the registry sources), so the EQL SQL pin stays at eql-2.3.0. build/clippy/fmt/test all clean with --locked. --- Cargo.lock | 37 ++++++++++++++----------------------- Cargo.toml | 6 +++--- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa93666..8e547e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,9 +638,9 @@ dependencies = [ [[package]] name = "cipherstash-client" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af42b947b1abab17a1aa23d194a1b0154b5c338453e93ac70e2ab644407b907c" +checksum = "7f97f8cfcd346e687a89016bcdac771609142356bde883ea8b317bb3acb3e397" dependencies = [ "aes-gcm-siv", "anyhow", @@ -1331,7 +1331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2515,7 +2515,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3517,7 +3517,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3530,7 +3530,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3587,7 +3587,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3906,7 +3906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4125,9 +4125,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stack-auth" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677a79b9f48a194a8fc24870d27af6b7c62280ac5fcf85967e12165f16690d9d" +checksum = "756fc8b999d012b560a633849cb48aa30f8f4a1ecc0b6f25c4cea6acc7c88885" dependencies = [ "aquamarine", "base64", @@ -4153,9 +4153,9 @@ dependencies = [ [[package]] name = "stack-profile" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f49a439a0bced8c2ff9b6e1e2cf448ec3e923fe9acdc6f40d80c796a208f21" +checksum = "1d83d7e0e1e7b6c8dd50200d6564bcc65df8a285ca28f52010c3fd6dbd402845" dependencies = [ "dirs", "gethostname", @@ -4295,7 +4295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5208,7 +5208,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5314,15 +5314,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" diff --git a/Cargo.toml b/Cargo.toml index 3d3a2b8..9108765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,13 @@ rand = "0.8.5" # cipherstash-client + stack-profile ship from the same cipherstash-suite # workspace and must stay version-locked — cipherstash-client implements # `KeyProvider` for stack-profile's `ProfileStore`, so a source/version -# mismatch produces two incompatible `ProfileStore` types. 0.38.0 keeps the +# mismatch produces two incompatible `ProfileStore` types. 0.38.1 keeps the # EQL v2 wire payloads (`k`/`c`/`v`/`i`) byte-identical to the # 0.34.1-alpha.9 pre-release that EQL 2.3.0 targets — the eql wire formats # module is unchanged between the two — so the EQL release pin in # mise.toml's setup-db task stays at eql-2.3.0. Bump both together. -cipherstash-client = { version = "=0.38.0", features = ["tokio"] } -stack-profile = "=0.38.0" +cipherstash-client = { version = "=0.38.1", features = ["tokio"] } +stack-profile = "=0.38.1" anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" From 273d09e641087703b14ae8ed2b06af5dc970c8bd Mon Sep 17 00:00:00 2001 From: James Sadler Date: Fri, 3 Jul 2026 23:55:48 +1000 Subject: [PATCH 2/3] refactor(eql-v3): rename int4_* domains to integer_* (EQL scalar-family rename) The eql_v3 scalar domain families were renamed (int4 -> integer, int8 -> bigint, ...) upstream; eql-bindings >= 0.3.0 resolves TargetDomain::parse against the renamed catalog, so the old int4_* names fail at runtime. Sweep every int4_ord_ore / int4_eq reference in code, SQL, mise tasks, reporters and docs to the current names. The stale int4_ord_ope references in the CIP-3280 stub comments are rewritten in the follow-up commit that enables the scenario. --- README.md | 4 ++-- benches/combo_v3.rs | 2 +- benches/ore_v3.rs | 18 +++++++------- mise.toml | 2 +- report_benchmarks.py | 24 +++++++++---------- .../v3/combo_encrypted_v3_10000000_up.sql | 2 +- .../v3/combo_encrypted_v3_1000000_up.sql | 2 +- .../v3/combo_encrypted_v3_100000_up.sql | 2 +- .../v3/combo_encrypted_v3_10000_up.sql | 2 +- .../v3/integer_encrypted_v3_10000000_up.sql | 2 +- .../v3/integer_encrypted_v3_1000000_up.sql | 2 +- .../v3/integer_encrypted_v3_100000_up.sql | 2 +- .../v3/integer_encrypted_v3_10000_up.sql | 2 +- sql/schema-v3.sql | 22 ++++++++--------- src/bin/encrypt_combo_v3.rs | 5 ++-- src/bin/encrypt_int_v3.rs | 6 +++-- src/lib.rs | 4 ++-- 17 files changed, 53 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 23f3d44..539f2d3 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,9 @@ WHERE eql_v3.eq_term(value) = eql_v3.eq_term($1::eql_v3.text_search) | v2 table | v3 twin | Domain | Notes | |---|---|---|---| | `string_encrypted` | `string_encrypted_v3` | `eql_v3.text_search` | Only single-column v3 domain serving both EXACT (hm) and MATCH (bf); requires an extra `ob` ORE term the v2 string ingest doesn't encrypt — `encrypt_string_v3` throughput is therefore not directly comparable to `encrypt_string`. | -| `integer_encrypted` | `integer_encrypted_v3` | `eql_v3.int4_ord_ore` | v2 encrypts `i32` (int4). | +| `integer_encrypted` | `integer_encrypted_v3` | `eql_v3.integer_ord_ore` | v2 encrypts `i32` (int4). | | `category_encrypted` | `category_encrypted_v3` | `eql_v3.text_eq` | | -| `combo_encrypted` | `combo_encrypted_v3` | `text_match` / `int4_ord_ore` / `text_eq` | Per-column capability match. | +| `combo_encrypted` | `combo_encrypted_v3` | `text_match` / `integer_ord_ore` / `text_eq` | Per-column capability match. | | `json_ste_vec_small_encrypted` | `json_ste_vec_small_encrypted_v3` | `eql_v3.json` | SteVec document domain. | | plaintext baselines | *(shared)* | — | Version-independent; not duplicated. | diff --git a/benches/combo_v3.rs b/benches/combo_v3.rs index f5c85c5..c6f9c0c 100644 --- a/benches/combo_v3.rs +++ b/benches/combo_v3.rs @@ -1,6 +1,6 @@ //! EQL v3 twin of `benches/combo.rs` — composite-predicate scenarios over //! `combo_encrypted_v3_` (name `eql_v3.text_match`, age -//! `eql_v3.int4_ord_ore`, category `eql_v3.text_eq`). +//! `eql_v3.integer_ord_ore`, category `eql_v3.text_eq`). //! //! The v2 scenarios filter with `name LIKE $1`; v3 removes LIKE, so every //! scenario filters with the bloom containment recipe instead: diff --git a/benches/ore_v3.rs b/benches/ore_v3.rs index 520b03e..f34940f 100644 --- a/benches/ore_v3.rs +++ b/benches/ore_v3.rs @@ -1,11 +1,11 @@ //! EQL v3 twin of `benches/ore.rs` — range + ordered-range queries against -//! `integer_encrypted_v3_` (column typed `eql_v3.int4_ord_ore`). +//! `integer_encrypted_v3_` (column typed `eql_v3.integer_ord_ore`). //! //! Scenario parity with v2: //! //! * The four non-selective range baselines (`range_gt_10/100`, //! `range_lt_10/100`) carry over unchanged in intent — bare-form -//! `value $1::eql_v3.int4_ord_ore` inlines to +//! `value $1::eql_v3.integer_ord_ore` inlines to //! `eql_v3.ord_term(a) eql_v3.ord_term(b)` and structurally //! matches the `btree (eql_v3.ord_term(value))` index. Whether the //! planner uses it is a selectivity question, exactly as in v2 (see @@ -19,7 +19,7 @@ //! operand hides selectivity from the planner in v3 exactly as in v2. //! //! Probe flow: the threshold (5000) is encrypted as a STORAGE payload and -//! converted (target `int4_ord_ore`) because no v3 scalar QUERY wire shape +//! converted (target `integer_ord_ore`) because no v3 scalar QUERY wire shape //! exists — see benches/exact_v3.rs. //! //! TODO(CIP-3280) — `_ord_ope` scenario stub. eql_v3 ships `int4_ord_ope` @@ -70,31 +70,31 @@ use tokio::runtime::Runtime; static QUERY_TEMPLATES: &[(&str, i32, &str)] = &[ ( "SELECT id, value::jsonb FROM {TABLE} \ - WHERE value > $1::eql_v3.int4_ord_ore LIMIT 10", + WHERE value > $1::eql_v3.integer_ord_ore LIMIT 10", 5000, "range_gt_10", ), ( "SELECT id, value::jsonb FROM {TABLE} \ - WHERE value > $1::eql_v3.int4_ord_ore LIMIT 100", + WHERE value > $1::eql_v3.integer_ord_ore LIMIT 100", 5000, "range_gt_100", ), ( "SELECT id, value::jsonb FROM {TABLE} \ - WHERE value < $1::eql_v3.int4_ord_ore LIMIT 10", + WHERE value < $1::eql_v3.integer_ord_ore LIMIT 10", 5000, "range_lt_10", ), ( "SELECT id, value::jsonb FROM {TABLE} \ - WHERE value < $1::eql_v3.int4_ord_ore LIMIT 100", + WHERE value < $1::eql_v3.integer_ord_ore LIMIT 100", 5000, "range_lt_100", ), ( "SELECT id, value::jsonb FROM {TABLE} \ - WHERE value < $1::eql_v3.int4_ord_ore \ + WHERE value < $1::eql_v3.integer_ord_ore \ ORDER BY eql_v3.ord_term(value) LIMIT 10", 5000, "range_lt_ordered_10", @@ -112,7 +112,7 @@ async fn build_query( .add_index(Index::new_ore()); let identifier = Identifier::new(table_name, "value"); - let target = TargetDomain::parse("int4_ord_ore").expect("int4_ord_ore is a v3 domain"); + let target = TargetDomain::parse("integer_ord_ore").expect("integer_ord_ore is a v3 domain"); EncryptedQueryBuilderV3::new(column_config, identifier, target) .statement(query) diff --git a/mise.toml b/mise.toml index 97cf4b5..773ca12 100644 --- a/mise.toml +++ b/mise.toml @@ -1020,7 +1020,7 @@ description = "Run MATCH_V3 query benchmark (bloom containment only — v3 remov run = "mise run bench:query:_run_v3 prepare:v3:string_encrypted match_v3 MATCH_V3 $1" [tasks."bench:query:ore:v3"] -description = "Run ORE_V3 query benchmark (integer_encrypted_v3, eql_v3.int4_ord_ore)" +description = "Run ORE_V3 query benchmark (integer_encrypted_v3, eql_v3.integer_ord_ore)" run = "mise run bench:query:_run_v3 prepare:v3:integer_encrypted ore_v3 ORE_V3 $1" [tasks."bench:query:group_by:v3"] diff --git a/report_benchmarks.py b/report_benchmarks.py index 04542ff..6e6e0fc 100755 --- a/report_benchmarks.py +++ b/report_benchmarks.py @@ -444,27 +444,27 @@ def get_query_sql_and_param(self, query_type: str, query_name: str) -> Tuple[str "ORE_V3": { "range_gt_10": ( "SELECT id, value::jsonb FROM {TABLE} " - "WHERE value > $1::eql_v3.int4_ord_ore LIMIT 10", + "WHERE value > $1::eql_v3.integer_ord_ore LIMIT 10", "5000" ), "range_gt_100": ( "SELECT id, value::jsonb FROM {TABLE} " - "WHERE value > $1::eql_v3.int4_ord_ore LIMIT 100", + "WHERE value > $1::eql_v3.integer_ord_ore LIMIT 100", "5000" ), "range_lt_10": ( "SELECT id, value::jsonb FROM {TABLE} " - "WHERE value < $1::eql_v3.int4_ord_ore LIMIT 10", + "WHERE value < $1::eql_v3.integer_ord_ore LIMIT 10", "5000" ), "range_lt_100": ( "SELECT id, value::jsonb FROM {TABLE} " - "WHERE value < $1::eql_v3.int4_ord_ore LIMIT 100", + "WHERE value < $1::eql_v3.integer_ord_ore LIMIT 100", "5000" ), "range_lt_ordered_10": ( "SELECT id, value::jsonb FROM {TABLE} " - "WHERE value < $1::eql_v3.int4_ord_ore " + "WHERE value < $1::eql_v3.integer_ord_ore " "ORDER BY eql_v3.ord_term(value) LIMIT 10", "5000" ) @@ -843,29 +843,29 @@ def get_query_description(self, query_type: str, query_name: str) -> Tuple[str, "ORE_V3": { "range_gt_10": ( "EQL v3 range query (greater than) returning 10 results", - "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.int4_ord_ore`). " + "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.integer_ord_ore`). " "Index: `btree (eql_v3.ord_term(value))`. Bare-form range operators " "inline to ord_term comparisons and match the index; planner usage is " "a selectivity question exactly as in the v2 family." ), "range_gt_100": ( "EQL v3 range query (greater than) returning 100 results", - "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.int4_ord_ore`). " + "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.integer_ord_ore`). " "Index: `btree (eql_v3.ord_term(value))`." ), "range_lt_10": ( "EQL v3 range query (less than) returning 10 results", - "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.int4_ord_ore`). " + "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.integer_ord_ore`). " "Index: `btree (eql_v3.ord_term(value))`." ), "range_lt_100": ( "EQL v3 range query (less than) returning 100 results", - "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.int4_ord_ore`). " + "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.integer_ord_ore`). " "Index: `btree (eql_v3.ord_term(value))`." ), "range_lt_ordered_10": ( "EQL v3 ordered range query (extractor ORDER BY)", - "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.int4_ord_ore`). " + "Table: `integer_encrypted_v3_{rows}` (column `eql_v3.integer_ord_ore`). " "Index: `btree (eql_v3.ord_term(value))`. `ORDER BY eql_v3.ord_term(value)` " "matches the index expression, so rows stream out already sorted — no " "Sort node. Same sort-key rule as v2." @@ -891,7 +891,7 @@ def get_query_description(self, query_type: str, query_name: str) -> Tuple[str, "bloom_ore_order_limit": ( "EQL v3 composite: bloom containment filter + ORE ORDER BY + LIMIT", "Table: `combo_encrypted_v3_{rows}` (name `eql_v3.text_match`, age " - "`eql_v3.int4_ord_ore`, category `eql_v3.text_eq`). The v2 LIKE filter " + "`eql_v3.integer_ord_ore`, category `eql_v3.text_eq`). The v2 LIKE filter " "becomes `eql_v3.match_term(name) @> eql_v3.match_term($1)` (v3 removes " "LIKE); ORDER BY uses `eql_v3.ord_term(age)`." ), @@ -1093,7 +1093,7 @@ def _write_ingest_section(self, f): "ste_vec_large": "Tests insertion of large JSON objects with SteVec (searchable encrypted vector) indexing.", "int_v3": "EQL v3 twin of `int`: same encrypt workload plus a from_v2 " "v2→v3 conversion per payload, inserting into " - "`integer_encrypted_v3` (eql_v3.int4_ord_ore).", + "`integer_encrypted_v3` (eql_v3.integer_ord_ore).", "string_v3": "EQL v3 twin of `string`, inserting into `string_encrypted_v3` " "(eql_v3.text_search). NOTE: not directly comparable to " "`string` — text_search requires the `ob` term, so this " diff --git a/sql/indexes/v3/combo_encrypted_v3_10000000_up.sql b/sql/indexes/v3/combo_encrypted_v3_10000000_up.sql index bb50902..8a904b4 100644 --- a/sql/indexes/v3/combo_encrypted_v3_10000000_up.sql +++ b/sql/indexes/v3/combo_encrypted_v3_10000000_up.sql @@ -2,7 +2,7 @@ -- capability twins of the v2 combo indexes: -- name eql_v3.text_match → GIN on match_term (bloom containment; -- replaces the v2 bloom_filter GIN + LIKE) --- age eql_v3.int4_ord_ore → btree on ord_term (ORE ordering) +-- age eql_v3.integer_ord_ore → btree on ord_term (ORE ordering) -- category eql_v3.text_eq → hash on eq_term (hmac equality) CREATE INDEX diff --git a/sql/indexes/v3/combo_encrypted_v3_1000000_up.sql b/sql/indexes/v3/combo_encrypted_v3_1000000_up.sql index 528874b..46f1acc 100644 --- a/sql/indexes/v3/combo_encrypted_v3_1000000_up.sql +++ b/sql/indexes/v3/combo_encrypted_v3_1000000_up.sql @@ -2,7 +2,7 @@ -- capability twins of the v2 combo indexes: -- name eql_v3.text_match → GIN on match_term (bloom containment; -- replaces the v2 bloom_filter GIN + LIKE) --- age eql_v3.int4_ord_ore → btree on ord_term (ORE ordering) +-- age eql_v3.integer_ord_ore → btree on ord_term (ORE ordering) -- category eql_v3.text_eq → hash on eq_term (hmac equality) CREATE INDEX diff --git a/sql/indexes/v3/combo_encrypted_v3_100000_up.sql b/sql/indexes/v3/combo_encrypted_v3_100000_up.sql index 906575c..ca05767 100644 --- a/sql/indexes/v3/combo_encrypted_v3_100000_up.sql +++ b/sql/indexes/v3/combo_encrypted_v3_100000_up.sql @@ -2,7 +2,7 @@ -- capability twins of the v2 combo indexes: -- name eql_v3.text_match → GIN on match_term (bloom containment; -- replaces the v2 bloom_filter GIN + LIKE) --- age eql_v3.int4_ord_ore → btree on ord_term (ORE ordering) +-- age eql_v3.integer_ord_ore → btree on ord_term (ORE ordering) -- category eql_v3.text_eq → hash on eq_term (hmac equality) CREATE INDEX diff --git a/sql/indexes/v3/combo_encrypted_v3_10000_up.sql b/sql/indexes/v3/combo_encrypted_v3_10000_up.sql index e9e653b..b4afc00 100644 --- a/sql/indexes/v3/combo_encrypted_v3_10000_up.sql +++ b/sql/indexes/v3/combo_encrypted_v3_10000_up.sql @@ -2,7 +2,7 @@ -- capability twins of the v2 combo indexes: -- name eql_v3.text_match → GIN on match_term (bloom containment; -- replaces the v2 bloom_filter GIN + LIKE) --- age eql_v3.int4_ord_ore → btree on ord_term (ORE ordering) +-- age eql_v3.integer_ord_ore → btree on ord_term (ORE ordering) -- category eql_v3.text_eq → hash on eq_term (hmac equality) CREATE INDEX diff --git a/sql/indexes/v3/integer_encrypted_v3_10000000_up.sql b/sql/indexes/v3/integer_encrypted_v3_10000000_up.sql index c7b15d2..599c8c3 100644 --- a/sql/indexes/v3/integer_encrypted_v3_10000000_up.sql +++ b/sql/indexes/v3/integer_encrypted_v3_10000000_up.sql @@ -1,5 +1,5 @@ -- eql_v3 functional index for integer_encrypted_v3_10000000 --- (column typed eql_v3.int4_ord_ore — ob). +-- (column typed eql_v3.integer_ord_ore — ob). -- -- ord_term returns eql_v3.ore_block_256, whose btree operator class is the -- DEFAULT for the type — bare-form range predicates (`value > $1`) inline diff --git a/sql/indexes/v3/integer_encrypted_v3_1000000_up.sql b/sql/indexes/v3/integer_encrypted_v3_1000000_up.sql index eac7a7c..e6248ce 100644 --- a/sql/indexes/v3/integer_encrypted_v3_1000000_up.sql +++ b/sql/indexes/v3/integer_encrypted_v3_1000000_up.sql @@ -1,5 +1,5 @@ -- eql_v3 functional index for integer_encrypted_v3_1000000 --- (column typed eql_v3.int4_ord_ore — ob). +-- (column typed eql_v3.integer_ord_ore — ob). -- -- ord_term returns eql_v3.ore_block_256, whose btree operator class is the -- DEFAULT for the type — bare-form range predicates (`value > $1`) inline diff --git a/sql/indexes/v3/integer_encrypted_v3_100000_up.sql b/sql/indexes/v3/integer_encrypted_v3_100000_up.sql index cac6a4a..f4a70ee 100644 --- a/sql/indexes/v3/integer_encrypted_v3_100000_up.sql +++ b/sql/indexes/v3/integer_encrypted_v3_100000_up.sql @@ -1,5 +1,5 @@ -- eql_v3 functional index for integer_encrypted_v3_100000 --- (column typed eql_v3.int4_ord_ore — ob). +-- (column typed eql_v3.integer_ord_ore — ob). -- -- ord_term returns eql_v3.ore_block_256, whose btree operator class is the -- DEFAULT for the type — bare-form range predicates (`value > $1`) inline diff --git a/sql/indexes/v3/integer_encrypted_v3_10000_up.sql b/sql/indexes/v3/integer_encrypted_v3_10000_up.sql index 5585c47..1a7ad61 100644 --- a/sql/indexes/v3/integer_encrypted_v3_10000_up.sql +++ b/sql/indexes/v3/integer_encrypted_v3_10000_up.sql @@ -1,5 +1,5 @@ -- eql_v3 functional index for integer_encrypted_v3_10000 --- (column typed eql_v3.int4_ord_ore — ob). +-- (column typed eql_v3.integer_ord_ore — ob). -- -- ord_term returns eql_v3.ore_block_256, whose btree operator class is the -- DEFAULT for the type — bare-form range predicates (`value > $1`) inline diff --git a/sql/schema-v3.sql b/sql/schema-v3.sql index 6b0777a..ee3bfdb 100644 --- a/sql/schema-v3.sql +++ b/sql/schema-v3.sql @@ -15,12 +15,12 @@ -- encrypts an ORE term (`ob`) that v2's encrypt_string does not, which -- is visible in the encrypt_string_v3 ingest numbers (documented in -- the bin and README). --- integer_encrypted → eql_v3.int4_ord_ore (ob) +-- integer_encrypted → eql_v3.integer_ord_ore (ob) -- The v2 int scenarios encrypt `i32` via ColumnType::Int (int4, not -- int8) with an ORE index only. -- category_encrypted → eql_v3.text_eq (hm) -- Equality/GROUP BY only. --- combo_encrypted → name eql_v3.text_match / age eql_v3.int4_ord_ore / +-- combo_encrypted → name eql_v3.text_match / age eql_v3.integer_ord_ore / -- category eql_v3.text_eq -- Per-column capability match for the composite-predicate scenarios -- (bloom containment + ORE order + hmac GROUP BY). v3 removes LIKE, so @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS string_encrypted_v3 ( CREATE TABLE IF NOT EXISTS integer_encrypted_v3 ( id SERIAL PRIMARY KEY, - value eql_v3.int4_ord_ore NOT NULL + value eql_v3.integer_ord_ore NOT NULL ); CREATE TABLE IF NOT EXISTS json_ste_vec_small_encrypted_v3 ( @@ -82,22 +82,22 @@ CREATE TABLE IF NOT EXISTS string_encrypted_v3_10000000 ( CREATE TABLE IF NOT EXISTS integer_encrypted_v3_10000 ( id SERIAL PRIMARY KEY, - value eql_v3.int4_ord_ore NOT NULL + value eql_v3.integer_ord_ore NOT NULL ); CREATE TABLE IF NOT EXISTS integer_encrypted_v3_100000 ( id SERIAL PRIMARY KEY, - value eql_v3.int4_ord_ore NOT NULL + value eql_v3.integer_ord_ore NOT NULL ); CREATE TABLE IF NOT EXISTS integer_encrypted_v3_1000000 ( id SERIAL PRIMARY KEY, - value eql_v3.int4_ord_ore NOT NULL + value eql_v3.integer_ord_ore NOT NULL ); CREATE TABLE IF NOT EXISTS integer_encrypted_v3_10000000 ( id SERIAL PRIMARY KEY, - value eql_v3.int4_ord_ore NOT NULL + value eql_v3.integer_ord_ore NOT NULL ); CREATE TABLE IF NOT EXISTS category_encrypted_v3_10000 ( @@ -123,28 +123,28 @@ CREATE TABLE IF NOT EXISTS category_encrypted_v3_10000000 ( CREATE TABLE IF NOT EXISTS combo_encrypted_v3_10000 ( id SERIAL PRIMARY KEY, name eql_v3.text_match NOT NULL, - age eql_v3.int4_ord_ore NOT NULL, + age eql_v3.integer_ord_ore NOT NULL, category eql_v3.text_eq NOT NULL ); CREATE TABLE IF NOT EXISTS combo_encrypted_v3_100000 ( id SERIAL PRIMARY KEY, name eql_v3.text_match NOT NULL, - age eql_v3.int4_ord_ore NOT NULL, + age eql_v3.integer_ord_ore NOT NULL, category eql_v3.text_eq NOT NULL ); CREATE TABLE IF NOT EXISTS combo_encrypted_v3_1000000 ( id SERIAL PRIMARY KEY, name eql_v3.text_match NOT NULL, - age eql_v3.int4_ord_ore NOT NULL, + age eql_v3.integer_ord_ore NOT NULL, category eql_v3.text_eq NOT NULL ); CREATE TABLE IF NOT EXISTS combo_encrypted_v3_10000000 ( id SERIAL PRIMARY KEY, name eql_v3.text_match NOT NULL, - age eql_v3.int4_ord_ore NOT NULL, + age eql_v3.integer_ord_ore NOT NULL, category eql_v3.text_eq NOT NULL ); diff --git a/src/bin/encrypt_combo_v3.rs b/src/bin/encrypt_combo_v3.rs index fab8b4e..13a1963 100644 --- a/src/bin/encrypt_combo_v3.rs +++ b/src/bin/encrypt_combo_v3.rs @@ -8,7 +8,7 @@ //! * `name` → `eql_v3.text_match` (bf — bloom containment; v3 has //! no LIKE, so the v2 unique+match config's `hm` term is dropped by //! the conversion) -//! * `age` → `eql_v3.int4_ord_ore` (ob — ORE ordering) +//! * `age` → `eql_v3.integer_ord_ore` (ob — ORE ordering) //! * `category` → `eql_v3.text_eq` (hm — hmac equality / GROUP BY) //! //! Environment variables: DATABASE_URL, NUM_RECORDS (default 10000), @@ -72,7 +72,8 @@ async fn main() -> Result<()> { let category_ident = Identifier::new(&table_name, "category"); let name_target = TargetDomain::parse("text_match").expect("text_match is a v3 domain"); - let age_target = TargetDomain::parse("int4_ord_ore").expect("int4_ord_ore is a v3 domain"); + let age_target = + TargetDomain::parse("integer_ord_ore").expect("integer_ord_ore is a v3 domain"); let category_target = TargetDomain::parse("text_eq").expect("text_eq is a v3 domain"); for batch_start in (0..num_records).step_by(batch_size) { diff --git a/src/bin/encrypt_int_v3.rs b/src/bin/encrypt_int_v3.rs index 946cc96..1c8517e 100644 --- a/src/bin/encrypt_int_v3.rs +++ b/src/bin/encrypt_int_v3.rs @@ -3,7 +3,7 @@ //! payload with `eql_bindings::from_v2`, and inserts into //! `integer_encrypted_v3` (or a `TABLE_SUFFIX` variant). //! -//! Target domain: `eql_v3.int4_ord_ore` (ob) — same ColumnType::Int / ORE +//! Target domain: `eql_v3.integer_ord_ore` (ob) — same ColumnType::Int / ORE //! index configuration as the v2 bin, so the encryption workload is //! identical and the ingest numbers differ only by the from_v2 conversion. //! @@ -38,7 +38,9 @@ async fn main() -> Result<()> { .casts_as(ColumnType::Int) .add_index(Index::new_ore()), ) - .convert_to_v3(TargetDomain::parse("int4_ord_ore").expect("int4_ord_ore is a v3 domain")) + .convert_to_v3( + TargetDomain::parse("integer_ord_ore").expect("integer_ord_ore is a v3 domain"), + ) .build()? .ingest::(Faker) .await?; diff --git a/src/lib.rs b/src/lib.rs index a3a0e3d..f290327 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1072,7 +1072,7 @@ mod tests { "c": "OPAQUE", "ob": ["deadbeef"], }); - let target = TargetDomain::parse("int4_eq").unwrap(); + let target = TargetDomain::parse("integer_eq").unwrap(); let err = v2_store_to_v3(&v2, target).unwrap_err(); let msg = format!("{:#}", err); assert!( @@ -1080,7 +1080,7 @@ mod tests { "error should name the missing term: {msg}" ); assert!( - msg.contains("int4_eq"), + msg.contains("integer_eq"), "error should name the target domain: {msg}" ); } From 1f2725335aab61daf46961a9f94c588512dcd201 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 4 Jul 2026 00:03:54 +1000 Subject: [PATCH 3/3] feat(bench): enable the eql_v3 OPE ordering scenario (CIP-3348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable the ORE_V3 family's `_ord_ope` stub (was TODO(CIP-3280)): cipherstash-client 0.38.1 emits the scalar OPE-CLLW `op` term, so the scenario is now drivable end-to-end. - sql/schema-v3.sql: add `integer_ope_encrypted_v3` (+ the four row-count tiers) typed `eql_v3.integer_ord_ope` - sql/indexes/v3/: btree (eql_v3.ord_ope_term(value)) up/down scripts per tier — ord_ope_term returns a bytea domain, so ordering is native bytea comparison (no per-row plpgsql) - src/bin/encrypt_int_ope_v3.rs: ingest bin mirroring encrypt_int_v3 but with an `ope` index (Index::new_ope()) and the integer_ord_ope conversion target - benches/ore_v3.rs: enable OPE_QUERY_TEMPLATES (ope_range_gt_10, ope_range_lt_ordered_10) against the new tables, sharing thresholds with the ORE scenarios so the two ordering implementations report side-by-side in the one ORE_V3 group - mise.toml: prepare:v3:integer_ope_encrypted + bench:ingest:encrypt_int_ope:v3 tasks; bench:query:ore:v3 now preps both tables - report_benchmarks.py / README.md: document the new scenarios Verified against a live Postgres with a current EQL v3 build: the installer + schema-v3.sql + index scripts apply cleanly, and EXPLAIN shows the ope_range_lt_ordered_10 shape streaming from the ord_ope_term btree with no Sort node. --- Cargo.toml | 4 + README.md | 12 +- benches/ore_v3.rs | 132 +++++++++++++----- mise.toml | 21 ++- report_benchmarks.py | 35 +++++ ...integer_ope_encrypted_v3_10000000_down.sql | 1 + .../integer_ope_encrypted_v3_10000000_up.sql | 14 ++ .../integer_ope_encrypted_v3_1000000_down.sql | 1 + .../integer_ope_encrypted_v3_1000000_up.sql | 14 ++ .../integer_ope_encrypted_v3_100000_down.sql | 1 + .../v3/integer_ope_encrypted_v3_100000_up.sql | 14 ++ .../integer_ope_encrypted_v3_10000_down.sql | 1 + .../v3/integer_ope_encrypted_v3_10000_up.sql | 14 ++ sql/schema-v3.sql | 47 +++++-- src/bin/encrypt_int_ope_v3.rs | 50 +++++++ 15 files changed, 311 insertions(+), 50 deletions(-) create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_10000000_down.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_10000000_up.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_1000000_down.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_1000000_up.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_100000_down.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_100000_up.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_10000_down.sql create mode 100644 sql/indexes/v3/integer_ope_encrypted_v3_10000_up.sql create mode 100644 src/bin/encrypt_int_ope_v3.rs diff --git a/Cargo.toml b/Cargo.toml index c0c69b1..ea9e237 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,10 @@ path = "src/bin/encrypt_string_v3.rs" name = "encrypt_int_v3" path = "src/bin/encrypt_int_v3.rs" +[[bin]] +name = "encrypt_int_ope_v3" +path = "src/bin/encrypt_int_ope_v3.rs" + [[bin]] name = "encrypt_category_v3" path = "src/bin/encrypt_category_v3.rs" diff --git a/README.md b/README.md index 539f2d3..bb819d8 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ WHERE eql_v3.eq_term(value) = eql_v3.eq_term($1::eql_v3.text_search) |---|---|---|---| | `string_encrypted` | `string_encrypted_v3` | `eql_v3.text_search` | Only single-column v3 domain serving both EXACT (hm) and MATCH (bf); requires an extra `ob` ORE term the v2 string ingest doesn't encrypt — `encrypt_string_v3` throughput is therefore not directly comparable to `encrypt_string`. | | `integer_encrypted` | `integer_encrypted_v3` | `eql_v3.integer_ord_ore` | v2 encrypts `i32` (int4). | +| *(none — v3-only)* | `integer_ope_encrypted_v3` | `eql_v3.integer_ord_ope` | OPE-CLLW ordering (wire key `op`); requires cipherstash-client >= 0.38.1. | | `category_encrypted` | `category_encrypted_v3` | `eql_v3.text_eq` | | | `combo_encrypted` | `combo_encrypted_v3` | `text_match` / `integer_ord_ore` / `text_eq` | Per-column capability match. | | `json_ste_vec_small_encrypted` | `json_ste_vec_small_encrypted_v3` | `eql_v3.json` | SteVec document domain. | @@ -138,10 +139,13 @@ WHERE eql_v3.eq_term(value) = eql_v3.eq_term($1::eql_v3.text_search) `GIN ((eql_v3.to_ste_vec_query(value))::jsonb jsonb_path_ops)` index); `field_eq/bare` becomes index-capable (the v3 `->` is inlinable SQL, unlike v2's plpgsql). -- **ORE (OPE)**: the `eql_v3.*_ord_ope` domains (OPE-CLLW ordering, wire - key `op`) are scaffolded but **disabled** — cipherstash-client 0.38.0 - does not emit `op` (CIP-3280 unreleased). See the TODOs in - `sql/schema-v3.sql` and `benches/ore_v3.rs`. +- **ORE (OPE)**: the ORE_V3 family adds `ope_*` scenarios against + `integer_ope_encrypted_v3` (`eql_v3.integer_ord_ope` — OPE-CLLW ordering, + wire key `op`, extractor `eql_v3.ord_ope_term`, native bytea comparison + over a `btree (eql_v3.ord_ope_term(value))` index). Requires + cipherstash-client >= 0.38.1, which emits the scalar `op` term + (CIP-3280). The `ope_*` scenarios share thresholds with their ORE twins + for a direct ORE-vs-OPE comparison. - A dedicated **conversion-overhead** ingest family (`convert_overhead_encrypt_only` vs `convert_overhead_encrypt_convert`) quantifies the pure `from_v2` cost: identical encrypt workloads, no diff --git a/benches/ore_v3.rs b/benches/ore_v3.rs index f34940f..430f85e 100644 --- a/benches/ore_v3.rs +++ b/benches/ore_v3.rs @@ -1,5 +1,7 @@ //! EQL v3 twin of `benches/ore.rs` — range + ordered-range queries against -//! `integer_encrypted_v3_` (column typed `eql_v3.integer_ord_ore`). +//! `integer_encrypted_v3_` (column typed `eql_v3.integer_ord_ore`), +//! plus the v3-only OPE ordering scenarios against +//! `integer_ope_encrypted_v3_` (column typed `eql_v3.integer_ord_ope`). //! //! Scenario parity with v2: //! @@ -22,32 +24,16 @@ //! converted (target `integer_ord_ore`) because no v3 scalar QUERY wire shape //! exists — see benches/exact_v3.rs. //! -//! TODO(CIP-3280) — `_ord_ope` scenario stub. eql_v3 ships `int4_ord_ope` -//! (wire key `op`, extractor `eql_v3.ord_ope_term`, ordering by native -//! bytea comparison of the OPE-CLLW ciphertext — no per-row plpgsql -//! compare). cipherstash-client 0.38.0 does not emit the `op` term -//! (CIP-3280 unreleased), so the scenario cannot be driven yet. When a -//! client release emits `op`: -//! 1. create the `integer_ope_encrypted_v3*` tables (schema-v3.sql TODO), -//! 2. add an `encrypt_int_ope_v3` ingest bin targeting `int4_ord_ope`, -//! 3. enable OPE_QUERY_TEMPLATES below against those tables with a -//! `btree (eql_v3.ord_ope_term(value))` index. -// -// static OPE_QUERY_TEMPLATES: &[(&str, i32, &str)] = &[ -// ( -// "SELECT id, value::jsonb FROM {OPE_TABLE} \ -// WHERE value > $1::eql_v3.int4_ord_ope LIMIT 10", -// 5000, -// "ope_range_gt_10", -// ), -// ( -// "SELECT id, value::jsonb FROM {OPE_TABLE} \ -// WHERE value < $1::eql_v3.int4_ord_ope \ -// ORDER BY eql_v3.ord_ope_term(value) LIMIT 10", -// 5000, -// "ope_range_lt_ordered_10", -// ), -// ]; +//! `_ord_ope` scenarios (CIP-3348; was the CIP-3280 stub): eql_v3 ships +//! `integer_ord_ope` (wire key `op`, extractor `eql_v3.ord_ope_term`, +//! ordering by native bytea comparison of the OPE-CLLW ciphertext — no +//! per-row plpgsql compare). cipherstash-client 0.38.1 emits the scalar +//! `op` term (CIP-3280), so OPE_QUERY_TEMPLATES below run against the +//! `integer_ope_encrypted_v3_` tables (populated by the +//! encrypt_int_ope_v3 ingest bin) over a +//! `btree (eql_v3.ord_ope_term(value))` index. The `ope_*` scenarios keep +//! the ORE thresholds so the two ordering implementations line up +//! side-by-side in the reports. use cipherstash_client::{ encryption::ScopedCipher, @@ -101,20 +87,66 @@ static QUERY_TEMPLATES: &[(&str, i32, &str)] = &[ ), ]; +// OPE twins of the two headline ORE shapes: the non-selective range +// baseline and the extractor-ordered stream. Same thresholds, `ope_` +// scenario-name prefix, run against the `integer_ope_encrypted_v3*` +// tables ({OPE_TABLE}). +static OPE_QUERY_TEMPLATES: &[(&str, i32, &str)] = &[ + ( + "SELECT id, value::jsonb FROM {OPE_TABLE} \ + WHERE value > $1::eql_v3.integer_ord_ope LIMIT 10", + 5000, + "ope_range_gt_10", + ), + ( + "SELECT id, value::jsonb FROM {OPE_TABLE} \ + WHERE value < $1::eql_v3.integer_ord_ope \ + ORDER BY eql_v3.ord_ope_term(value) LIMIT 10", + 5000, + "ope_range_lt_ordered_10", + ), +]; + +/// Which ordering implementation a scenario drives — selects the client +/// index config (so the probe carries the right term) and the v3 target +/// domain the storage payload converts into. +#[derive(Clone, Copy)] +enum Ordering { + Ore, + Ope, +} + +impl Ordering { + fn index(self) -> Index { + match self { + Self::Ore => Index::new_ore(), + Self::Ope => Index::new_ope(), + } + } + + fn target(self) -> TargetDomain { + let name = match self { + Self::Ore => "integer_ord_ore", + Self::Ope => "integer_ord_ope", + }; + TargetDomain::parse(name).expect("known v3 ordering domain") + } +} + async fn build_query( cipher: Arc>, query: &str, x: i32, table_name: &str, + ordering: Ordering, ) -> EncryptedQueryV3 { let column_config = ColumnConfig::build("value") .casts_as(ColumnType::Int) - .add_index(Index::new_ore()); + .add_index(ordering.index()); let identifier = Identifier::new(table_name, "value"); - let target = TargetDomain::parse("integer_ord_ore").expect("integer_ord_ore is a v3 domain"); - EncryptedQueryBuilderV3::new(column_config, identifier, target) + EncryptedQueryBuilderV3::new(column_config, identifier, ordering.target()) .statement(query) .build_query(x, cipher) .await @@ -133,6 +165,30 @@ fn criterion_benchmark(c: &mut Criterion) { _ => String::new(), }; let table_name = format!("integer_encrypted_v3{}", table_suffix); + let ope_table_name = format!("integer_ope_encrypted_v3{}", table_suffix); + + // (bound sql, probe, scenario, ordering) — the ORE templates followed + // by their OPE twins, all in the one ORE_V3 criterion group so the + // reports show the two ordering implementations side-by-side. + let scenarios: Vec<(String, i32, &str, Ordering)> = QUERY_TEMPLATES + .iter() + .map(|(tpl, x, scenario)| { + ( + tpl.replace("{TABLE}", &table_name), + *x, + *scenario, + Ordering::Ore, + ) + }) + .chain(OPE_QUERY_TEMPLATES.iter().map(|(tpl, x, scenario)| { + ( + tpl.replace("{OPE_TABLE}", &ope_table_name), + *x, + *scenario, + Ordering::Ope, + ) + })) + .collect(); let (pool, cipher) = rt.block_on(async { let database_url = @@ -152,10 +208,14 @@ fn criterion_benchmark(c: &mut Criterion) { }); let queries = rt.block_on(async { - let mut queries = Vec::with_capacity(QUERY_TEMPLATES.len()); - for (query_template, x, _) in QUERY_TEMPLATES { - let query_str = query_template.replace("{TABLE}", &table_name); - let query = build_query(Arc::clone(&cipher), &query_str, *x, &table_name).await; + let mut queries = Vec::with_capacity(scenarios.len()); + for (query_str, x, _, ordering) in &scenarios { + let query_table = match ordering { + Ordering::Ore => &table_name, + Ordering::Ope => &ope_table_name, + }; + let query = + build_query(Arc::clone(&cipher), query_str, *x, query_table, *ordering).await; queries.push(query); } queries @@ -164,7 +224,7 @@ fn criterion_benchmark(c: &mut Criterion) { let metadata = rt.block_on(async { let mut out = Vec::with_capacity(queries.len()); for (i, query) in queries.iter().enumerate() { - let (_, _, scenario) = QUERY_TEMPLATES[i]; + let (_, _, scenario, _) = &scenarios[i]; let bench_id = format!("ORE_V3/ore/{}/{}", scenario, target_rows); let explain = query.explain(&pool).await.expect("EXPLAIN failed"); let indexes_used = extract_indexes_used(&explain); @@ -193,7 +253,7 @@ fn criterion_benchmark(c: &mut Criterion) { group.sample_size(10); for (i, query) in queries.into_iter().enumerate() { - let (_, _, scenario) = QUERY_TEMPLATES[i]; + let (_, _, scenario, _) = &scenarios[i]; let exec_id = format!("ORE_V3/ore/{}/{}", scenario, target_rows); let decrypt_id = format!("ORE_V3/ore_decrypt/{}/{}", scenario, target_rows); diff --git a/mise.toml b/mise.toml index 773ca12..a7562f3 100644 --- a/mise.toml +++ b/mise.toml @@ -495,6 +495,10 @@ run = "mise run prepare:v3:_table string_encrypted_v3 encrypt_string_v3 $1" description = "Prepare integer_encrypted_v3 table with target row count" run = "mise run prepare:v3:_table integer_encrypted_v3 encrypt_int_v3 $1" +[tasks."prepare:v3:integer_ope_encrypted"] +description = "Prepare integer_ope_encrypted_v3 table (eql_v3.integer_ord_ope, OPE-CLLW ordering) with target row count" +run = "mise run prepare:v3:_table integer_ope_encrypted_v3 encrypt_int_ope_v3 $1" + [tasks."prepare:v3:category_encrypted"] description = "Prepare category_encrypted_v3_ table with ~250 low-cardinality categorical values" run = "mise run prepare:v3:_table category_encrypted_v3 encrypt_category_v3 $1" @@ -1020,8 +1024,17 @@ description = "Run MATCH_V3 query benchmark (bloom containment only — v3 remov run = "mise run bench:query:_run_v3 prepare:v3:string_encrypted match_v3 MATCH_V3 $1" [tasks."bench:query:ore:v3"] -description = "Run ORE_V3 query benchmark (integer_encrypted_v3, eql_v3.integer_ord_ore)" -run = "mise run bench:query:_run_v3 prepare:v3:integer_encrypted ore_v3 ORE_V3 $1" +description = "Run ORE_V3 query benchmark (integer_encrypted_v3 eql_v3.integer_ord_ore + integer_ope_encrypted_v3 eql_v3.integer_ord_ope)" +run = """ +#!/usr/bin/env bash +set -e +# The ORE_V3 bench drives two tables: the ORE scenarios read +# integer_encrypted_v3_ and the ope_* scenarios read +# integer_ope_encrypted_v3_. _run_v3 preps the first; prep the +# OPE table up front. +mise run prepare:v3:integer_ope_encrypted "$1" +mise run bench:query:_run_v3 prepare:v3:integer_encrypted ore_v3 ORE_V3 "$1" +""" [tasks."bench:query:group_by:v3"] description = "Run GROUP_BY_V3 query benchmark (encrypted scenarios only — plaintext baselines stay in the v2 family)" @@ -1122,6 +1135,10 @@ echo "========================================" description = "Run encrypt_int_v3 ingest benchmark (encrypt + from_v2 convert + insert) and combine results" run = "mise run bench:ingest:_run encrypt_int_v3 integer_encrypted_v3" +[tasks."bench:ingest:encrypt_int_ope:v3"] +description = "Run encrypt_int_ope_v3 ingest benchmark (encrypt with an ope index + from_v2 convert + insert) and combine results" +run = "mise run bench:ingest:_run encrypt_int_ope_v3 integer_ope_encrypted_v3" + [tasks."bench:ingest:encrypt_string:v3"] description = "Run encrypt_string_v3 ingest benchmark (encrypt + from_v2 convert + insert; note the extra ORE term vs v2 — see the bin docs)" run = "mise run bench:ingest:_run encrypt_string_v3 string_encrypted_v3" diff --git a/report_benchmarks.py b/report_benchmarks.py index 6e6e0fc..3ae6813 100755 --- a/report_benchmarks.py +++ b/report_benchmarks.py @@ -467,6 +467,17 @@ def get_query_sql_and_param(self, query_type: str, query_name: str) -> Tuple[str "WHERE value < $1::eql_v3.integer_ord_ore " "ORDER BY eql_v3.ord_term(value) LIMIT 10", "5000" + ), + "ope_range_gt_10": ( + "SELECT id, value::jsonb FROM {TABLE} " + "WHERE value > $1::eql_v3.integer_ord_ope LIMIT 10", + "5000" + ), + "ope_range_lt_ordered_10": ( + "SELECT id, value::jsonb FROM {TABLE} " + "WHERE value < $1::eql_v3.integer_ord_ope " + "ORDER BY eql_v3.ord_ope_term(value) LIMIT 10", + "5000" ) }, "GROUP_BY_V3": { @@ -869,6 +880,25 @@ def get_query_description(self, query_type: str, query_name: str) -> Tuple[str, "Index: `btree (eql_v3.ord_term(value))`. `ORDER BY eql_v3.ord_term(value)` " "matches the index expression, so rows stream out already sorted — no " "Sort node. Same sort-key rule as v2." + ), + "ope_range_gt_10": ( + "EQL v3 OPE range query (greater than) returning 10 results", + "Table: `integer_ope_encrypted_v3_{rows}` (column " + "`eql_v3.integer_ord_ope` — the OPE-CLLW `op` term, emitted by " + "cipherstash-client >= 0.38.1). Index: " + "`btree (eql_v3.ord_ope_term(value))`. ord_ope_term returns a bytea " + "domain, so ordering uses bytea's native comparison — no per-row " + "plpgsql compare. Same threshold as `range_gt_10` for a direct " + "ORE-vs-OPE comparison." + ), + "ope_range_lt_ordered_10": ( + "EQL v3 OPE ordered range query (extractor ORDER BY)", + "Table: `integer_ope_encrypted_v3_{rows}` (column " + "`eql_v3.integer_ord_ope`). Index: " + "`btree (eql_v3.ord_ope_term(value))`. " + "`ORDER BY eql_v3.ord_ope_term(value)` matches the index expression, " + "so rows stream out already sorted. OPE twin of " + "`range_lt_ordered_10`." ) }, "GROUP_BY_V3": { @@ -1094,6 +1124,11 @@ def _write_ingest_section(self, f): "int_v3": "EQL v3 twin of `int`: same encrypt workload plus a from_v2 " "v2→v3 conversion per payload, inserting into " "`integer_encrypted_v3` (eql_v3.integer_ord_ore).", + "int_ope_v3": "OPE sibling of `int_v3`: same integer workload encrypted " + "with an `ope` index (scalar OPE-CLLW `op` term, " + "cipherstash-client >= 0.38.1) plus the from_v2 conversion, " + "inserting into `integer_ope_encrypted_v3` " + "(eql_v3.integer_ord_ope).", "string_v3": "EQL v3 twin of `string`, inserting into `string_encrypted_v3` " "(eql_v3.text_search). NOTE: not directly comparable to " "`string` — text_search requires the `ob` term, so this " diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_10000000_down.sql b/sql/indexes/v3/integer_ope_encrypted_v3_10000000_down.sql new file mode 100644 index 0000000..8494c60 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_10000000_down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS integer_ope_encrypted_v3_10000000_ord_ope_term_index; diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_10000000_up.sql b/sql/indexes/v3/integer_ope_encrypted_v3_10000000_up.sql new file mode 100644 index 0000000..c56da1c --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_10000000_up.sql @@ -0,0 +1,14 @@ +-- eql_v3 functional index for integer_ope_encrypted_v3_10000000 +-- (column typed eql_v3.integer_ord_ope — op). +-- +-- ord_ope_term returns eql_v3_internal.ope_cllw (a bytea domain), so the +-- btree uses bytea's native DEFAULT operator class — no per-row plpgsql +-- compare. Bare-form range predicates (`value > $1`) inline to +-- eql_v3.ord_ope_term(value) > eql_v3.ord_ope_term($1) and match this +-- index; extractor-form ORDER BY streams rows out of it already sorted. + +CREATE INDEX +integer_ope_encrypted_v3_10000000_ord_ope_term_index +ON integer_ope_encrypted_v3_10000000 USING btree ( + eql_v3.ord_ope_term(value) +); diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_1000000_down.sql b/sql/indexes/v3/integer_ope_encrypted_v3_1000000_down.sql new file mode 100644 index 0000000..0f76f21 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_1000000_down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS integer_ope_encrypted_v3_1000000_ord_ope_term_index; diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_1000000_up.sql b/sql/indexes/v3/integer_ope_encrypted_v3_1000000_up.sql new file mode 100644 index 0000000..f1d2953 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_1000000_up.sql @@ -0,0 +1,14 @@ +-- eql_v3 functional index for integer_ope_encrypted_v3_1000000 +-- (column typed eql_v3.integer_ord_ope — op). +-- +-- ord_ope_term returns eql_v3_internal.ope_cllw (a bytea domain), so the +-- btree uses bytea's native DEFAULT operator class — no per-row plpgsql +-- compare. Bare-form range predicates (`value > $1`) inline to +-- eql_v3.ord_ope_term(value) > eql_v3.ord_ope_term($1) and match this +-- index; extractor-form ORDER BY streams rows out of it already sorted. + +CREATE INDEX +integer_ope_encrypted_v3_1000000_ord_ope_term_index +ON integer_ope_encrypted_v3_1000000 USING btree ( + eql_v3.ord_ope_term(value) +); diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_100000_down.sql b/sql/indexes/v3/integer_ope_encrypted_v3_100000_down.sql new file mode 100644 index 0000000..1d0f9b0 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_100000_down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS integer_ope_encrypted_v3_100000_ord_ope_term_index; diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_100000_up.sql b/sql/indexes/v3/integer_ope_encrypted_v3_100000_up.sql new file mode 100644 index 0000000..6be8813 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_100000_up.sql @@ -0,0 +1,14 @@ +-- eql_v3 functional index for integer_ope_encrypted_v3_100000 +-- (column typed eql_v3.integer_ord_ope — op). +-- +-- ord_ope_term returns eql_v3_internal.ope_cllw (a bytea domain), so the +-- btree uses bytea's native DEFAULT operator class — no per-row plpgsql +-- compare. Bare-form range predicates (`value > $1`) inline to +-- eql_v3.ord_ope_term(value) > eql_v3.ord_ope_term($1) and match this +-- index; extractor-form ORDER BY streams rows out of it already sorted. + +CREATE INDEX +integer_ope_encrypted_v3_100000_ord_ope_term_index +ON integer_ope_encrypted_v3_100000 USING btree ( + eql_v3.ord_ope_term(value) +); diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_10000_down.sql b/sql/indexes/v3/integer_ope_encrypted_v3_10000_down.sql new file mode 100644 index 0000000..d8d3eb8 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_10000_down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS integer_ope_encrypted_v3_10000_ord_ope_term_index; diff --git a/sql/indexes/v3/integer_ope_encrypted_v3_10000_up.sql b/sql/indexes/v3/integer_ope_encrypted_v3_10000_up.sql new file mode 100644 index 0000000..cdc2a27 --- /dev/null +++ b/sql/indexes/v3/integer_ope_encrypted_v3_10000_up.sql @@ -0,0 +1,14 @@ +-- eql_v3 functional index for integer_ope_encrypted_v3_10000 +-- (column typed eql_v3.integer_ord_ope — op). +-- +-- ord_ope_term returns eql_v3_internal.ope_cllw (a bytea domain), so the +-- btree uses bytea's native DEFAULT operator class — no per-row plpgsql +-- compare. Bare-form range predicates (`value > $1`) inline to +-- eql_v3.ord_ope_term(value) > eql_v3.ord_ope_term($1) and match this +-- index; extractor-form ORDER BY streams rows out of it already sorted. + +CREATE INDEX +integer_ope_encrypted_v3_10000_ord_ope_term_index +ON integer_ope_encrypted_v3_10000 USING btree ( + eql_v3.ord_ope_term(value) +); diff --git a/sql/schema-v3.sql b/sql/schema-v3.sql index ee3bfdb..5a0a804 100644 --- a/sql/schema-v3.sql +++ b/sql/schema-v3.sql @@ -18,6 +18,11 @@ -- integer_encrypted → eql_v3.integer_ord_ore (ob) -- The v2 int scenarios encrypt `i32` via ColumnType::Int (int4, not -- int8) with an ORE index only. +-- integer_ope_encrypted (v3-only) → eql_v3.integer_ord_ope (op) +-- No v2 counterpart — OPE-CLLW ordering is new in v3. Same `i32` +-- ColumnType::Int workload as integer_encrypted, but with an `ope` +-- index so cipherstash-client emits the scalar `op` term +-- (requires cipherstash-client >= 0.38.1). -- category_encrypted → eql_v3.text_eq (hm) -- Equality/GROUP BY only. -- combo_encrypted → name eql_v3.text_match / age eql_v3.integer_ord_ore / @@ -31,14 +36,15 @@ -- are version-independent and shared with the v2 schema — not duplicated -- here. -- --- TODO(CIP-3280): `_ord_ope` scenario scaffold. eql_v3 ships `_ord_ope` --- domains (wire key `op`, extractor `eql_v3.ord_ope_term`, OPE-CLLW ordering --- via native bytea comparison — the Supabase-friendly ordered path), but --- cipherstash-client 0.38.0 does NOT emit the `op` term (CIP-3280 is --- unreleased). Once a client release emits `op`: --- 1. add `integer_ope_encrypted_v3*` tables typed eql_v3.int4_ord_ope, --- 2. add btree indexes on eql_v3.ord_ope_term(value) under sql/indexes/v3/, --- 3. enable the ope scenario stub in benches/ore_v3.rs. +-- `_ord_ope` scenario (CIP-3348; was the CIP-3280 stub): the eql_v3 +-- `_ord_ope` domains order by the OPE-CLLW ciphertext (wire key `op`, +-- extractor `eql_v3.ord_ope_term`, native bytea comparison — the +-- Supabase-friendly ordered path). cipherstash-client 0.38.1 emits the +-- scalar `op` term (CIP-3280), so the scenario is live: the +-- `integer_ope_encrypted_v3*` tables below are populated by the +-- encrypt_int_ope_v3 bin and queried by the ope_* scenarios in +-- benches/ore_v3.rs over the btree (eql_v3.ord_ope_term(value)) indexes +-- under sql/indexes/v3/. -- Base (un-tiered) tables used by the ingest throughput benches. @@ -52,6 +58,11 @@ CREATE TABLE IF NOT EXISTS integer_encrypted_v3 ( value eql_v3.integer_ord_ore NOT NULL ); +CREATE TABLE IF NOT EXISTS integer_ope_encrypted_v3 ( + id SERIAL PRIMARY KEY, + value eql_v3.integer_ord_ope NOT NULL +); + CREATE TABLE IF NOT EXISTS json_ste_vec_small_encrypted_v3 ( id SERIAL PRIMARY KEY, value eql_v3.json NOT NULL @@ -100,6 +111,26 @@ CREATE TABLE IF NOT EXISTS integer_encrypted_v3_10000000 ( value eql_v3.integer_ord_ore NOT NULL ); +CREATE TABLE IF NOT EXISTS integer_ope_encrypted_v3_10000 ( + id SERIAL PRIMARY KEY, + value eql_v3.integer_ord_ope NOT NULL +); + +CREATE TABLE IF NOT EXISTS integer_ope_encrypted_v3_100000 ( + id SERIAL PRIMARY KEY, + value eql_v3.integer_ord_ope NOT NULL +); + +CREATE TABLE IF NOT EXISTS integer_ope_encrypted_v3_1000000 ( + id SERIAL PRIMARY KEY, + value eql_v3.integer_ord_ope NOT NULL +); + +CREATE TABLE IF NOT EXISTS integer_ope_encrypted_v3_10000000 ( + id SERIAL PRIMARY KEY, + value eql_v3.integer_ord_ope NOT NULL +); + CREATE TABLE IF NOT EXISTS category_encrypted_v3_10000 ( id SERIAL PRIMARY KEY, value eql_v3.text_eq NOT NULL diff --git a/src/bin/encrypt_int_ope_v3.rs b/src/bin/encrypt_int_ope_v3.rs new file mode 100644 index 0000000..0e26c78 --- /dev/null +++ b/src/bin/encrypt_int_ope_v3.rs @@ -0,0 +1,50 @@ +//! OPE sibling of `encrypt_int_v3`: encrypts random `i32` values via the +//! existing cipherstash-client (v2 wire) pipeline, converts each storage +//! payload with `eql_bindings::from_v2`, and inserts into +//! `integer_ope_encrypted_v3` (or a `TABLE_SUFFIX` variant). +//! +//! Target domain: `eql_v3.integer_ord_ope` (op) — same ColumnType::Int +//! workload as encrypt_int_v3, but configured with an `ope` index so the +//! client emits the scalar OPE-CLLW `op` term instead of the ORE `ob` +//! blocks (requires cipherstash-client >= 0.38.1, CIP-3280). +//! +//! Environment variables: DATABASE_URL, NUM_RECORDS (default 10000), +//! TABLE_SUFFIX, CS_CLIENT_ID / CS_CLIENT_KEY / CS_WORKSPACE_CRN. + +use anyhow::Result; +use cipherstash_client::{ + eql::Identifier, + schema::{column::Index, ColumnConfig, ColumnType}, +}; +use dbbenches::{v3::TargetDomain, IngestOptionsBuilder}; +use fake::Faker; +use std::env; + +#[tokio::main] +async fn main() -> Result<()> { + let num_records: i32 = env::var("NUM_RECORDS") + .unwrap_or_else(|_| "10000".to_string()) + .parse() + .expect("NUM_RECORDS must be a valid integer"); + + let table_suffix = env::var("TABLE_SUFFIX").unwrap_or_default(); + let table_name = format!("integer_ope_encrypted_v3{}", table_suffix); + + IngestOptionsBuilder::new("encrypt_int_ope_v3") + .num_records(num_records) + .batch_size(1000) + .identifier(Identifier::new(&table_name, "value")) + .column_config( + ColumnConfig::build("value") + .casts_as(ColumnType::Int) + .add_index(Index::new_ope()), + ) + .convert_to_v3( + TargetDomain::parse("integer_ord_ope").expect("integer_ord_ope is a v3 domain"), + ) + .build()? + .ingest::(Faker) + .await?; + + Ok(()) +}