diff --git a/Cargo.lock b/Cargo.lock index dd2ac62..5294f73 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", @@ -1327,7 +1327,7 @@ dependencies = [ [[package]] name = "eql-bindings" -version = "0.1.0" +version = "0.4.2" dependencies = [ "schemars", "serde", @@ -1348,7 +1348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2532,7 +2532,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]] @@ -3554,7 +3554,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3567,7 +3567,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3624,7 +3624,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3979,7 +3979,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]] @@ -4198,9 +4198,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", @@ -4226,9 +4226,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", @@ -4377,7 +4377,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]] @@ -5313,7 +5313,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]] @@ -5419,15 +5419,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 675be24..ea9e237 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" # EQL v2.3 → v3 wire conversion (`from_v2`) + canonical v3 payload types. # Consumed as a path dependency off the EQL repo's `feat/eql-bindings-from-v2` # branch while the crate is unreleased. @@ -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 23f3d44..bb819d8 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,10 @@ 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). | +| *(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` / `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. | @@ -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/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..430f85e 100644 --- a/benches/ore_v3.rs +++ b/benches/ore_v3.rs @@ -1,11 +1,13 @@ //! 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`), +//! plus the v3-only OPE ordering scenarios against +//! `integer_ope_encrypted_v3_` (column typed `eql_v3.integer_ord_ope`). //! //! 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,35 +21,19 @@ //! 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` -//! (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, @@ -70,51 +56,97 @@ 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", ), ]; +// 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("int4_ord_ore").expect("int4_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 97cf4b5..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.int4_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 04542ff..3ae6813 100755 --- a/report_benchmarks.py +++ b/report_benchmarks.py @@ -444,29 +444,40 @@ 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" + ), + "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": { @@ -843,32 +854,51 @@ 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." + ), + "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": { @@ -891,7 +921,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 +1123,12 @@ 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).", + "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/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/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 6b0777a..5a0a804 100644 --- a/sql/schema-v3.sql +++ b/sql/schema-v3.sql @@ -15,12 +15,17 @@ -- 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. +-- 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.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 @@ -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. @@ -49,7 +55,12 @@ 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 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 ( @@ -82,22 +93,42 @@ 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 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 ( @@ -123,28 +154,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_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(()) +} 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}" ); }