From 6e27e0facd38d19f93e50280ce22428d97a9b582 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 26 Apr 2026 14:25:57 +1000 Subject: [PATCH 1/4] feat(ope): add ope index type alongside ore Recognises the new 'ope' index in encrypt config (alongside ore, match, unique, ste_vec) and routes it through cipherstash-client's `IndexType::Ope` so that ORE-style range/order operators work against OPE-indexed columns. - Adds `OpeIndexOpts` and `ope` field to the proxy's `Indexes` config. - Wires `Index::new_ope()` into `Column::into_column_config()`. - Adds an `encrypted_ope` integration-test table mirroring `encrypted` but with `ope`+`unique` indexes per column, and extends `clear()` to truncate it. - Adds 7 WHERE tests (int2/4/8, float8, date, text, bool) and 6 ORDER BY tests (asc/desc, NULLs first/last) targeting the new table. - Adds a `can_parse_ope_index` unit test mirroring `can_parse_ore_index`. CHANGELOG entry under [Unreleased]. --- CHANGELOG.md | 4 + .../src/common.rs | 11 + .../cipherstash-proxy-integration/src/lib.rs | 2 + .../src/map_ope_index_order.rs | 199 ++++++++++++++++++ .../src/map_ope_index_where.rs | 173 +++++++++++++++ tests/sql/schema.sql | 41 ++++ 6 files changed, 430 insertions(+) create mode 100644 packages/cipherstash-proxy-integration/src/map_ope_index_order.rs create mode 100644 packages/cipherstash-proxy-integration/src/map_ope_index_where.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e049d4..200efbab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- **OPE (Order-Preserving Encryption) index**: New `ope` index type alongside the existing `ore` for range and ordering queries. OPE ciphertexts compare under standard lexicographic byte ordering, so they're a drop-in alternative to ORE for range and `ORDER BY`. Configure with `SELECT eql_v2.add_search_config('table', 'column', 'ope', 'int')` (any cast type that `ore` supports). Requires EQL with OPE support and a CipherStash client/config build that includes `IndexType::Ope`. + ## [2.2.0-alpha.1] - 2026-03-25 ### Changed diff --git a/packages/cipherstash-proxy-integration/src/common.rs b/packages/cipherstash-proxy-integration/src/common.rs index 63002e2c..fd2b158f 100644 --- a/packages/cipherstash-proxy-integration/src/common.rs +++ b/packages/cipherstash-proxy-integration/src/common.rs @@ -88,6 +88,17 @@ pub async fn clear_with_client(client: &Client) { client.simple_query(sql).await.unwrap(); } +/// OPE-specific clear that only touches the `encrypted_ope` table. +/// Keeps OPE tests from racing with ORE tests via the shared `encrypted` table. +pub async fn clear_ope_with_client(client: &Client) { + let sql = "TRUNCATE encrypted_ope"; + client.simple_query(sql).await.unwrap(); +} + +pub async fn clear_ope() { + clear_ope_with_client(&connect_with_tls(PROXY).await).await; +} + pub async fn reset_schema() { let port = std::env::var("CS_DATABASE__PORT") .map(|s| s.parse().unwrap()) diff --git a/packages/cipherstash-proxy-integration/src/lib.rs b/packages/cipherstash-proxy-integration/src/lib.rs index 8ab28cc5..8050045d 100644 --- a/packages/cipherstash-proxy-integration/src/lib.rs +++ b/packages/cipherstash-proxy-integration/src/lib.rs @@ -12,6 +12,8 @@ mod map_concat; mod map_literals; mod map_match_index; mod map_nulls; +mod map_ope_index_order; +mod map_ope_index_where; mod map_ore_index_order; mod map_ore_index_where; mod map_params; diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs new file mode 100644 index 00000000..7a059e61 --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs @@ -0,0 +1,199 @@ +#[cfg(test)] +mod tests { + use crate::common::{ + clear_ope, connect_with_tls, interleaved_indices, random_id, trace, PROXY, + }; + + #[tokio::test] + async fn map_ope_order_text_asc() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + let values = ["aardvark", "aplomb", "chimera", "chrysalis", "zephyr"]; + + let insert = "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2)"; + for idx in interleaved_indices(values.len()) { + client + .query(insert, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } + + let rows = client + .query( + "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text", + &[], + ) + .await + .unwrap(); + + let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); + let expected: Vec = values.iter().map(|s| s.to_string()).collect(); + + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn map_ope_order_text_desc() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + let values = ["aardvark", "aplomb", "chimera", "chrysalis", "zephyr"]; + + let insert = "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2)"; + for idx in interleaved_indices(values.len()) { + client + .query(insert, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } + + let rows = client + .query( + "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text DESC", + &[], + ) + .await + .unwrap(); + + let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); + let expected: Vec = values.iter().rev().map(|s| s.to_string()).collect(); + + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn map_ope_order_int4_asc() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + let values: Vec = vec![-100, -1, 0, 1, 42, 1000, i32::MAX]; + + let insert = "INSERT INTO encrypted_ope (id, encrypted_int4) VALUES ($1, $2)"; + for idx in interleaved_indices(values.len()) { + client + .query(insert, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } + + let rows = client + .query( + "SELECT encrypted_int4 FROM encrypted_ope ORDER BY encrypted_int4", + &[], + ) + .await + .unwrap(); + + let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); + assert_eq!(actual, values); + } + + #[tokio::test] + async fn map_ope_order_int4_desc() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + let values: Vec = vec![-100, -1, 0, 1, 42, 1000, i32::MAX]; + + let insert = "INSERT INTO encrypted_ope (id, encrypted_int4) VALUES ($1, $2)"; + for idx in interleaved_indices(values.len()) { + client + .query(insert, &[&random_id(), &values[idx]]) + .await + .unwrap(); + } + + let rows = client + .query( + "SELECT encrypted_int4 FROM encrypted_ope ORDER BY encrypted_int4 DESC", + &[], + ) + .await + .unwrap(); + + let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); + let expected: Vec = values.into_iter().rev().collect(); + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn map_ope_order_nulls_last_by_default() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + client + .query( + "INSERT INTO encrypted_ope (id) VALUES ($1)", + &[&random_id()], + ) + .await + .unwrap(); + + client + .query( + "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2), ($3, $4)", + &[&random_id(), &"a", &random_id(), &"b"], + ) + .await + .unwrap(); + + let rows = client + .query( + "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text", + &[], + ) + .await + .unwrap(); + + let actual: Vec> = rows.iter().map(|r| r.get(0)).collect(); + assert_eq!( + actual, + vec![Some("a".into()), Some("b".into()), None], + "NULLs should sort last by default" + ); + } + + #[tokio::test] + async fn map_ope_order_nulls_first() { + trace(); + clear_ope().await; + let client = connect_with_tls(PROXY).await; + + client + .query( + "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2), ($3, $4)", + &[&random_id(), &"a", &random_id(), &"b"], + ) + .await + .unwrap(); + + client + .query( + "INSERT INTO encrypted_ope (id) VALUES ($1)", + &[&random_id()], + ) + .await + .unwrap(); + + let rows = client + .query( + "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text NULLS FIRST", + &[], + ) + .await + .unwrap(); + + let actual: Vec> = rows.iter().map(|r| r.get(0)).collect(); + assert_eq!( + actual, + vec![None, Some("a".into()), Some("b".into())], + "NULLS FIRST should explicitly sort NULLs first" + ); + } +} diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs new file mode 100644 index 00000000..fb87012b --- /dev/null +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs @@ -0,0 +1,173 @@ +#[cfg(test)] +mod tests { + use crate::common::{clear_ope, connect_with_tls, random_id, trace, PROXY}; + use chrono::NaiveDate; + use tokio_postgres::types::{FromSql, ToSql}; + use tokio_postgres::Client; + + #[tokio::test] + async fn map_ope_where_generic_int2() { + map_ope_where_generic("encrypted_int2", 40i16, 99i16).await; + } + + #[tokio::test] + async fn map_ope_where_generic_int4() { + map_ope_where_generic("encrypted_int4", 40i32, 99i32).await; + } + + #[tokio::test] + async fn map_ope_where_generic_int8() { + map_ope_where_generic("encrypted_int8", 40i64, 99i64).await; + } + + #[tokio::test] + async fn map_ope_where_generic_float8() { + map_ope_where_generic("encrypted_float8", 40.0f64, 99.0f64).await; + } + + #[tokio::test] + async fn map_ope_where_generic_date() { + let low = NaiveDate::parse_from_str("2024-01-01", "%Y-%m-%d").unwrap(); + let high = NaiveDate::parse_from_str("2027-01-01", "%Y-%m-%d").unwrap(); + map_ope_where_generic("encrypted_date", low, high).await; + } + + #[tokio::test] + async fn map_ope_where_generic_text() { + map_ope_where_generic("encrypted_text", "ABC".to_string(), "BCD".to_string()).await; + } + + #[tokio::test] + async fn map_ope_where_generic_bool() { + map_ope_where_generic("encrypted_bool", false, true).await; + } + + /// Tests OPE operations on the `encrypted_ope` table with 2 values - high & low. + /// Mirrors `map_ore_where_generic` but targets the OPE-indexed mirror table. + async fn map_ope_where_generic(col_name: &str, low: T, high: T) + where + for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd, + { + trace(); + + clear_ope().await; + + let client = connect_with_tls(PROXY).await; + + // Insert test data + let sql = format!("INSERT INTO encrypted_ope (id, {col_name}) VALUES ($1, $2)"); + for val in [low.clone(), high.clone()] { + client + .query(&sql, &[&random_id(), &val]) + .await + .expect("insert failed"); + } + + // NULL record + let sql = format!("INSERT INTO encrypted_ope (id, {col_name}) VALUES ($1, null)"); + client + .query(&sql, &[&random_id()]) + .await + .expect("insert failed"); + + // GT: given [1, 3], `> 1` returns [3] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1"); + test_ope_op( + &client, + col_name, + &sql, + &[&low], + std::slice::from_ref(&high), + ) + .await; + + // GT 2nd case: given [1, 3], `> 3` returns [] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1"); + test_ope_op::(&client, col_name, &sql, &[&high], &[]).await; + + // LT: given [1, 3], `< 3` returns [1] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} < $1"); + test_ope_op( + &client, + col_name, + &sql, + &[&high], + std::slice::from_ref(&low), + ) + .await; + + // LT 2nd case: given [1, 3], `< 1` returns [] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} < $1"); + test_ope_op(&client, col_name, &sql, &[&low], &[] as &[T]).await; + + // GT && LT: given [1, 3], `> 1 and < 3` returns [] + let sql = format!( + "SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1 AND {col_name} < $2" + ); + test_ope_op(&client, col_name, &sql, &[&low, &high], &[] as &[T]).await; + + // LTEQ: given [1, 3], `<= 3` returns [1, 3] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} <= $1"); + test_ope_op( + &client, + col_name, + &sql, + &[&high], + &[low.clone(), high.clone()], + ) + .await; + + // GTEQ: given [1, 3], `>= 1` returns [1, 3] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} >= $1"); + test_ope_op( + &client, + col_name, + &sql, + &[&low], + &[low.clone(), high.clone()], + ) + .await; + + // EQ: given [1, 3], `= 1` returns [1] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} = $1"); + test_ope_op(&client, col_name, &sql, &[&low], std::slice::from_ref(&low)).await; + + // NEQ: given [1, 3], `<> 3` returns [1] + let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} <> $1"); + test_ope_op( + &client, + col_name, + &sql, + &[&high], + std::slice::from_ref(&low), + ) + .await; + } + + /// Runs the query and checks the returned results match the expected results. + /// Sorts after the query (separate tests cover ordering). + async fn test_ope_op( + client: &Client, + col_name: &str, + sql: &str, + params: &[&(dyn ToSql + Sync)], + expected: &[T], + ) where + for<'a> T: Clone + ToSql + PartialEq + Sync + FromSql<'a> + PartialOrd, + { + let rows = client.query(sql, params).await.expect("query failed"); + let mut actual: Vec = rows.iter().map(|r| r.get(0)).collect(); + actual.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let mut expected: Vec = expected.to_vec(); + expected.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + assert_eq!( + actual.len(), + expected.len(), + "wrong row count for {col_name} via {sql}" + ); + for (a, e) in actual.iter().zip(expected.iter()) { + assert!(a == e, "value mismatch for {col_name} via {sql}"); + } + } +} diff --git a/tests/sql/schema.sql b/tests/sql/schema.sql index 69a6ec5e..9dd78fe9 100644 --- a/tests/sql/schema.sql +++ b/tests/sql/schema.sql @@ -169,6 +169,47 @@ SELECT eql_v2.add_search_config( SELECT eql_v2.add_encrypted_constraint('encrypted', 'encrypted_text'); +-- OPE-indexed mirror of the `encrypted` table. +-- Uses the new `ope` (Order-Preserving Encryption) index in place of `ore` for +-- range/order operators. Same shape as `encrypted` so generic test helpers can +-- swap table names and reuse logic. +DROP TABLE IF EXISTS encrypted_ope; +CREATE TABLE encrypted_ope ( + id bigint, + plaintext text, + plaintext_date date, + encrypted_text eql_v2_encrypted, + encrypted_bool eql_v2_encrypted, + encrypted_int2 eql_v2_encrypted, + encrypted_int4 eql_v2_encrypted, + encrypted_int8 eql_v2_encrypted, + encrypted_float8 eql_v2_encrypted, + encrypted_date eql_v2_encrypted, + PRIMARY KEY(id) +); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_text', 'unique', 'text'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_text', 'ope', 'text'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_bool', 'unique', 'boolean'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_bool', 'ope', 'boolean'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int2', 'unique', 'small_int'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int2', 'ope', 'small_int'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int4', 'unique', 'int'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int4', 'ope', 'int'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int8', 'unique', 'big_int'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int8', 'ope', 'big_int'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_float8', 'unique', 'double'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_float8', 'ope', 'double'); + +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_date', 'unique', 'date'); +SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_date', 'ope', 'date'); + + -- This is the exact same schema as above but using a database-generated primary key. -- It is required to remove flake form the Elixir integration test suite. -- TODO: port all the rest of our integration tests to this schema. From b27898d2b9a9ae714cb62189f66daad9025cb35f Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 26 Apr 2026 15:38:32 +1000 Subject: [PATCH 2/4] test(integration): give each ORE/OPE test its own fixture table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the parallel-test races that plagued the ORE WHERE/ORDER tests when run alongside each other (and the OPE tests against the shared `encrypted_ope` table). Each test now owns a dedicated table generated up front by a DO block in `tests/sql/schema.sql`, with the same shape as `encrypted` (minus jsonb) and the matching `add_search_config` calls. Other changes: - Drop `#[serial]` from `map_ore_index_order` — per-table isolation removes the need. - Drop the shared `encrypted_ope` table and the hand-added `clear_ope` helper. Tests use the new generic `clear_table(name)` to truncate their dedicated fixture. - Parameterise `ore_order_helpers` on the table name so the same helpers serve `map_ore_index_order` (per-test tables) and multitenant tests (shared `encrypted` table, isolated via keyset + `#[serial]`). End-to-end (parallel, default thread count): 38/38 ORE+OPE tests pass in ~1.6s, repeatedly. Was previously 14s serial / flaky in parallel. --- .../src/common.rs | 15 +- .../src/map_ope_index_order.rs | 124 +++++-------- .../src/map_ope_index_where.rs | 87 ++++++--- .../src/map_ore_index_order.rs | 130 +++++++------ .../src/map_ore_index_where.rs | 83 ++++++--- .../src/multitenant/ore_order.rs | 37 +++- .../src/ore_order_helpers.rs | 168 ++++++++++------- tests/sql/schema.sql | 175 ++++++++++++++---- 8 files changed, 512 insertions(+), 307 deletions(-) diff --git a/packages/cipherstash-proxy-integration/src/common.rs b/packages/cipherstash-proxy-integration/src/common.rs index fd2b158f..4e4157b1 100644 --- a/packages/cipherstash-proxy-integration/src/common.rs +++ b/packages/cipherstash-proxy-integration/src/common.rs @@ -88,15 +88,16 @@ pub async fn clear_with_client(client: &Client) { client.simple_query(sql).await.unwrap(); } -/// OPE-specific clear that only touches the `encrypted_ope` table. -/// Keeps OPE tests from racing with ORE tests via the shared `encrypted` table. -pub async fn clear_ope_with_client(client: &Client) { - let sql = "TRUNCATE encrypted_ope"; - client.simple_query(sql).await.unwrap(); +/// Truncate a single table by name. Useful for tests that own a dedicated +/// fixture table (e.g. the per-test ORE/OPE tables) and don't need to wipe +/// the shared `encrypted`/`plaintext` tables. +pub async fn clear_table_with_client(client: &Client, table: &str) { + let sql = format!("TRUNCATE {}", table); + client.simple_query(&sql).await.unwrap(); } -pub async fn clear_ope() { - clear_ope_with_client(&connect_with_tls(PROXY).await).await; +pub async fn clear_table(table: &str) { + clear_table_with_client(&connect_with_tls(PROXY).await, table).await; } pub async fn reset_schema() { diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs index 7a059e61..67b9b3ac 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs @@ -1,32 +1,28 @@ #[cfg(test)] mod tests { use crate::common::{ - clear_ope, connect_with_tls, interleaved_indices, random_id, trace, PROXY, + clear_table, connect_with_tls, interleaved_indices, random_id, trace, PROXY, }; #[tokio::test] async fn map_ope_order_text_asc() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_text_asc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values = ["aardvark", "aplomb", "chimera", "chrysalis", "zephyr"]; - let insert = "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2)"; + let insert = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert, &[&random_id(), &values[idx]]) + .query(&insert, &[&random_id(), &values[idx]]) .await .unwrap(); } - let rows = client - .query( - "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text", - &[], - ) - .await - .unwrap(); + let select = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text"); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); let expected: Vec = values.iter().map(|s| s.to_string()).collect(); @@ -37,26 +33,22 @@ mod tests { #[tokio::test] async fn map_ope_order_text_desc() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_text_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values = ["aardvark", "aplomb", "chimera", "chrysalis", "zephyr"]; - let insert = "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2)"; + let insert = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert, &[&random_id(), &values[idx]]) + .query(&insert, &[&random_id(), &values[idx]]) .await .unwrap(); } - let rows = client - .query( - "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text DESC", - &[], - ) - .await - .unwrap(); + let select = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text DESC"); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); let expected: Vec = values.iter().rev().map(|s| s.to_string()).collect(); @@ -67,26 +59,22 @@ mod tests { #[tokio::test] async fn map_ope_order_int4_asc() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_int4_asc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -1, 0, 1, 42, 1000, i32::MAX]; - let insert = "INSERT INTO encrypted_ope (id, encrypted_int4) VALUES ($1, $2)"; + let insert = format!("INSERT INTO {table} (id, encrypted_int4) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert, &[&random_id(), &values[idx]]) + .query(&insert, &[&random_id(), &values[idx]]) .await .unwrap(); } - let rows = client - .query( - "SELECT encrypted_int4 FROM encrypted_ope ORDER BY encrypted_int4", - &[], - ) - .await - .unwrap(); + let select = format!("SELECT encrypted_int4 FROM {table} ORDER BY encrypted_int4"); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); assert_eq!(actual, values); @@ -95,26 +83,22 @@ mod tests { #[tokio::test] async fn map_ope_order_int4_desc() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_int4_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -1, 0, 1, 42, 1000, i32::MAX]; - let insert = "INSERT INTO encrypted_ope (id, encrypted_int4) VALUES ($1, $2)"; + let insert = format!("INSERT INTO {table} (id, encrypted_int4) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert, &[&random_id(), &values[idx]]) + .query(&insert, &[&random_id(), &values[idx]]) .await .unwrap(); } - let rows = client - .query( - "SELECT encrypted_int4 FROM encrypted_ope ORDER BY encrypted_int4 DESC", - &[], - ) - .await - .unwrap(); + let select = format!("SELECT encrypted_int4 FROM {table} ORDER BY encrypted_int4 DESC"); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|r| r.get(0)).collect(); let expected: Vec = values.into_iter().rev().collect(); @@ -124,32 +108,22 @@ mod tests { #[tokio::test] async fn map_ope_order_nulls_last_by_default() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_nulls_last"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - client - .query( - "INSERT INTO encrypted_ope (id) VALUES ($1)", - &[&random_id()], - ) - .await - .unwrap(); + let null_insert = format!("INSERT INTO {table} (id) VALUES ($1)"); + client.query(&null_insert, &[&random_id()]).await.unwrap(); + let insert = + format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); client - .query( - "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2), ($3, $4)", - &[&random_id(), &"a", &random_id(), &"b"], - ) + .query(&insert, &[&random_id(), &"a", &random_id(), &"b"]) .await .unwrap(); - let rows = client - .query( - "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text", - &[], - ) - .await - .unwrap(); + let select = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text"); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec> = rows.iter().map(|r| r.get(0)).collect(); assert_eq!( @@ -162,32 +136,24 @@ mod tests { #[tokio::test] async fn map_ope_order_nulls_first() { trace(); - clear_ope().await; + let table = "encrypted_ope_order_nulls_first"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; + let insert = + format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); client - .query( - "INSERT INTO encrypted_ope (id, encrypted_text) VALUES ($1, $2), ($3, $4)", - &[&random_id(), &"a", &random_id(), &"b"], - ) + .query(&insert, &[&random_id(), &"a", &random_id(), &"b"]) .await .unwrap(); - client - .query( - "INSERT INTO encrypted_ope (id) VALUES ($1)", - &[&random_id()], - ) - .await - .unwrap(); + let null_insert = format!("INSERT INTO {table} (id) VALUES ($1)"); + client.query(&null_insert, &[&random_id()]).await.unwrap(); - let rows = client - .query( - "SELECT encrypted_text FROM encrypted_ope ORDER BY encrypted_text NULLS FIRST", - &[], - ) - .await - .unwrap(); + let select = format!( + "SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST" + ); + let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec> = rows.iter().map(|r| r.get(0)).collect(); assert_eq!( diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs index fb87012b..b9dbf929 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs @@ -1,61 +1,97 @@ #[cfg(test)] mod tests { - use crate::common::{clear_ope, connect_with_tls, random_id, trace, PROXY}; + use crate::common::{clear_table, connect_with_tls, random_id, trace, PROXY}; use chrono::NaiveDate; use tokio_postgres::types::{FromSql, ToSql}; use tokio_postgres::Client; #[tokio::test] async fn map_ope_where_generic_int2() { - map_ope_where_generic("encrypted_int2", 40i16, 99i16).await; + map_ope_where_generic( + "encrypted_ope_where_int2", + "encrypted_int2", + 40i16, + 99i16, + ) + .await; } #[tokio::test] async fn map_ope_where_generic_int4() { - map_ope_where_generic("encrypted_int4", 40i32, 99i32).await; + map_ope_where_generic( + "encrypted_ope_where_int4", + "encrypted_int4", + 40i32, + 99i32, + ) + .await; } #[tokio::test] async fn map_ope_where_generic_int8() { - map_ope_where_generic("encrypted_int8", 40i64, 99i64).await; + map_ope_where_generic( + "encrypted_ope_where_int8", + "encrypted_int8", + 40i64, + 99i64, + ) + .await; } #[tokio::test] async fn map_ope_where_generic_float8() { - map_ope_where_generic("encrypted_float8", 40.0f64, 99.0f64).await; + map_ope_where_generic( + "encrypted_ope_where_float8", + "encrypted_float8", + 40.0f64, + 99.0f64, + ) + .await; } #[tokio::test] async fn map_ope_where_generic_date() { let low = NaiveDate::parse_from_str("2024-01-01", "%Y-%m-%d").unwrap(); let high = NaiveDate::parse_from_str("2027-01-01", "%Y-%m-%d").unwrap(); - map_ope_where_generic("encrypted_date", low, high).await; + map_ope_where_generic("encrypted_ope_where_date", "encrypted_date", low, high).await; } #[tokio::test] async fn map_ope_where_generic_text() { - map_ope_where_generic("encrypted_text", "ABC".to_string(), "BCD".to_string()).await; + map_ope_where_generic( + "encrypted_ope_where_text", + "encrypted_text", + "ABC".to_string(), + "BCD".to_string(), + ) + .await; } #[tokio::test] async fn map_ope_where_generic_bool() { - map_ope_where_generic("encrypted_bool", false, true).await; + map_ope_where_generic( + "encrypted_ope_where_bool", + "encrypted_bool", + false, + true, + ) + .await; } - /// Tests OPE operations on the `encrypted_ope` table with 2 values - high & low. - /// Mirrors `map_ore_where_generic` but targets the OPE-indexed mirror table. - async fn map_ope_where_generic(col_name: &str, low: T, high: T) + /// Tests OPE operations against a per-test fixture table. + /// Mirrors `map_ore_where_generic` but targets the OPE-indexed mirror tables. + async fn map_ope_where_generic(table: &str, col_name: &str, low: T, high: T) where - for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd, + for<'a> T: Clone + ToSql + PartialEq + Sync + FromSql<'a> + PartialOrd, { trace(); - clear_ope().await; + clear_table(table).await; let client = connect_with_tls(PROXY).await; // Insert test data - let sql = format!("INSERT INTO encrypted_ope (id, {col_name}) VALUES ($1, $2)"); + let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); for val in [low.clone(), high.clone()] { client .query(&sql, &[&random_id(), &val]) @@ -64,14 +100,14 @@ mod tests { } // NULL record - let sql = format!("INSERT INTO encrypted_ope (id, {col_name}) VALUES ($1, null)"); + let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, null)"); client .query(&sql, &[&random_id()]) .await .expect("insert failed"); // GT: given [1, 3], `> 1` returns [3] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1"); test_ope_op( &client, col_name, @@ -82,11 +118,11 @@ mod tests { .await; // GT 2nd case: given [1, 3], `> 3` returns [] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1"); test_ope_op::(&client, col_name, &sql, &[&high], &[]).await; // LT: given [1, 3], `< 3` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} < $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} < $1"); test_ope_op( &client, col_name, @@ -97,17 +133,16 @@ mod tests { .await; // LT 2nd case: given [1, 3], `< 1` returns [] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} < $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} < $1"); test_ope_op(&client, col_name, &sql, &[&low], &[] as &[T]).await; // GT && LT: given [1, 3], `> 1 and < 3` returns [] - let sql = format!( - "SELECT {col_name} FROM encrypted_ope WHERE {col_name} > $1 AND {col_name} < $2" - ); + let sql = + format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1 AND {col_name} < $2"); test_ope_op(&client, col_name, &sql, &[&low, &high], &[] as &[T]).await; // LTEQ: given [1, 3], `<= 3` returns [1, 3] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} <= $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} <= $1"); test_ope_op( &client, col_name, @@ -118,7 +153,7 @@ mod tests { .await; // GTEQ: given [1, 3], `>= 1` returns [1, 3] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} >= $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} >= $1"); test_ope_op( &client, col_name, @@ -129,11 +164,11 @@ mod tests { .await; // EQ: given [1, 3], `= 1` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} = $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} = $1"); test_ope_op(&client, col_name, &sql, &[&low], std::slice::from_ref(&low)).await; // NEQ: given [1, 3], `<> 3` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted_ope WHERE {col_name} <> $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} <> $1"); test_ope_op( &client, col_name, diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs index 08118b65..3a1ac6cc 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_order.rs @@ -1,120 +1,126 @@ #[cfg(test)] mod tests { - use crate::common::{clear, connect_with_tls, trace, PROXY}; + use crate::common::{clear_table, connect_with_tls, trace, PROXY}; use crate::ore_order_helpers; use crate::ore_order_helpers::SortDirection; - use serial_test::serial; #[tokio::test] - #[serial] async fn map_ore_order_text() { trace(); - clear().await; + let table = "encrypted_ore_order_text"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_text(&client).await; + ore_order_helpers::ore_order_text(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_text_desc() { trace(); - clear().await; + let table = "encrypted_ore_order_text_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_text_desc(&client).await; + ore_order_helpers::ore_order_text_desc(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_nulls_last_by_default() { trace(); - clear().await; + let table = "encrypted_ore_order_nulls_last"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_nulls_last_by_default(&client).await; + ore_order_helpers::ore_order_nulls_last_by_default(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_nulls_first() { trace(); - clear().await; + let table = "encrypted_ore_order_nulls_first"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_nulls_first(&client).await; + ore_order_helpers::ore_order_nulls_first(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_qualified_column() { trace(); - clear().await; + let table = "encrypted_ore_order_qualified"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_qualified_column(&client).await; + ore_order_helpers::ore_order_qualified_column(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_qualified_column_with_alias() { trace(); - clear().await; + let table = "encrypted_ore_order_qualified_alias"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_qualified_column_with_alias(&client).await; + ore_order_helpers::ore_order_qualified_column_with_alias(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_no_eql_column_in_select_projection() { trace(); - clear().await; + let table = "encrypted_ore_order_no_select_projection"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client).await; + ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client, table).await; } #[tokio::test] - #[serial] async fn can_order_by_plaintext_column() { trace(); - clear().await; + let table = "encrypted_ore_order_plaintext_column"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_plaintext_column(&client).await; + ore_order_helpers::ore_order_plaintext_column(&client, table).await; } #[tokio::test] - #[serial] async fn can_order_by_plaintext_and_eql_columns() { trace(); - clear().await; + let table = "encrypted_ore_order_plaintext_and_eql"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_simple_protocol() { trace(); - clear().await; + let table = "encrypted_ore_order_simple_protocol"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; - ore_order_helpers::ore_order_simple_protocol(&client).await; + ore_order_helpers::ore_order_simple_protocol(&client, table).await; } #[tokio::test] - #[serial] async fn map_ore_order_int2() { trace(); - clear().await; + let table = "encrypted_ore_order_int2"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; - ore_order_helpers::ore_order_generic(&client, "encrypted_int2", values, SortDirection::Asc) - .await; + ore_order_helpers::ore_order_generic( + &client, + table, + "encrypted_int2", + values, + SortDirection::Asc, + ) + .await; } #[tokio::test] - #[serial] async fn map_ore_order_int2_desc() { trace(); - clear().await; + let table = "encrypted_ore_order_int2_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; ore_order_helpers::ore_order_generic( &client, + table, "encrypted_int2", values, SortDirection::Desc, @@ -123,29 +129,36 @@ mod tests { } #[tokio::test] - #[serial] async fn map_ore_order_int4() { trace(); - clear().await; + let table = "encrypted_ore_order_int4"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; - ore_order_helpers::ore_order_generic(&client, "encrypted_int4", values, SortDirection::Asc) - .await; + ore_order_helpers::ore_order_generic( + &client, + table, + "encrypted_int4", + values, + SortDirection::Asc, + ) + .await; } #[tokio::test] - #[serial] async fn map_ore_order_int4_desc() { trace(); - clear().await; + let table = "encrypted_ore_order_int4_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -50_000, -1_000, -1, 0, 1, 42, 1_000, 10_000, 50_000, 100_000, ]; ore_order_helpers::ore_order_generic( &client, + table, "encrypted_int4", values, SortDirection::Desc, @@ -154,29 +167,36 @@ mod tests { } #[tokio::test] - #[serial] async fn map_ore_order_int8() { trace(); - clear().await; + let table = "encrypted_ore_order_int8"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, ]; - ore_order_helpers::ore_order_generic(&client, "encrypted_int8", values, SortDirection::Asc) - .await; + ore_order_helpers::ore_order_generic( + &client, + table, + "encrypted_int8", + values, + SortDirection::Asc, + ) + .await; } #[tokio::test] - #[serial] async fn map_ore_order_int8_desc() { trace(); - clear().await; + let table = "encrypted_ore_order_int8_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -1_000_000, -10_000, -1, 0, 1, 42, 10_000, 100_000, 1_000_000, 9_999_999, ]; ore_order_helpers::ore_order_generic( &client, + table, "encrypted_int8", values, SortDirection::Desc, @@ -185,16 +205,17 @@ mod tests { } #[tokio::test] - #[serial] async fn map_ore_order_float8() { trace(); - clear().await; + let table = "encrypted_ore_order_float8"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; ore_order_helpers::ore_order_generic( &client, + table, "encrypted_float8", values, SortDirection::Asc, @@ -203,16 +224,17 @@ mod tests { } #[tokio::test] - #[serial] async fn map_ore_order_float8_desc() { trace(); - clear().await; + let table = "encrypted_ore_order_float8_desc"; + clear_table(table).await; let client = connect_with_tls(PROXY).await; let values: Vec = vec![ -99.9, -1.5, -0.001, 0.0, 0.001, 1.5, 3.25, 42.0, 99.9, 1000.5, ]; ore_order_helpers::ore_order_generic( &client, + table, "encrypted_float8", values, SortDirection::Desc, diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs index d70e389c..5f89d385 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs @@ -1,62 +1,97 @@ #[cfg(test)] mod tests { - use crate::common::{clear, connect_with_tls, random_id, trace, PROXY}; + use crate::common::{clear_table, connect_with_tls, random_id, trace, PROXY}; use chrono::NaiveDate; use tokio_postgres::types::{FromSql, ToSql}; use tokio_postgres::Client; #[tokio::test] async fn map_ore_where_generic_int2() { - map_ore_where_generic("encrypted_int2", 40i16, 99i16).await; + map_ore_where_generic( + "encrypted_ore_where_int2", + "encrypted_int2", + 40i16, + 99i16, + ) + .await; } #[tokio::test] async fn map_ore_where_generic_int4() { - map_ore_where_generic("encrypted_int4", 40i32, 99i32).await; + map_ore_where_generic( + "encrypted_ore_where_int4", + "encrypted_int4", + 40i32, + 99i32, + ) + .await; } #[tokio::test] async fn map_ore_where_generic_int8() { - map_ore_where_generic("encrypted_int8", 40i64, 99i64).await; + map_ore_where_generic( + "encrypted_ore_where_int8", + "encrypted_int8", + 40i64, + 99i64, + ) + .await; } #[tokio::test] async fn map_ore_where_generic_float8() { - map_ore_where_generic("encrypted_float8", 40.0f64, 99.0f64).await; + map_ore_where_generic( + "encrypted_ore_where_float8", + "encrypted_float8", + 40.0f64, + 99.0f64, + ) + .await; } #[tokio::test] async fn map_ore_where_generic_date() { let low = NaiveDate::parse_from_str("2024-01-01", "%Y-%m-%d").unwrap(); let high = NaiveDate::parse_from_str("2027-01-01", "%Y-%m-%d").unwrap(); - map_ore_where_generic("encrypted_date", low, high).await; + map_ore_where_generic("encrypted_ore_where_date", "encrypted_date", low, high).await; } #[tokio::test] async fn map_ore_where_generic_text() { - map_ore_where_generic("encrypted_text", "ABC".to_string(), "BCD".to_string()).await; + map_ore_where_generic( + "encrypted_ore_where_text", + "encrypted_text", + "ABC".to_string(), + "BCD".to_string(), + ) + .await; } #[tokio::test] async fn map_ore_where_generic_bool() { - map_ore_where_generic("encrypted_bool", false, true).await; + map_ore_where_generic( + "encrypted_ore_where_bool", + "encrypted_bool", + false, + true, + ) + .await; } - /// Tests ORE operations with 2 values - high & low. - /// The type of column identified by col_name must match the parameters - /// such as INT2 and i16, FLOAT8 and f64 - async fn map_ore_where_generic(col_name: &str, low: T, high: T) + /// Tests ORE operations with 2 values - high & low - against a per-test + /// fixture table. `table` and `col_name` must match. + async fn map_ore_where_generic(table: &str, col_name: &str, low: T, high: T) where for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd, { trace(); - clear().await; + clear_table(table).await; let client = connect_with_tls(PROXY).await; // Insert test data - let sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, $2)"); + let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); for val in [low.clone(), high.clone()] { client .query(&sql, &[&random_id(), &val]) @@ -65,14 +100,14 @@ mod tests { } // NULL record - let sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, null)"); + let sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, null)"); client .query(&sql, &[&random_id()]) .await .expect("insert failed"); // GT: given [1, 3], `> 1` returns [3] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} > $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1"); test_ore_op( &client, @@ -84,11 +119,11 @@ mod tests { .await; // GT 2nd case: given [1, 3], `> 3` returns [] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} > $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1"); test_ore_op::(&client, col_name, &sql, &[&high], &[]).await; // LT: given [1, 3], `< 3` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} < $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} < $1"); test_ore_op( &client, col_name, @@ -99,16 +134,16 @@ mod tests { .await; // LT 2nd case: given [1, 3], `< 3` returns [] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} < $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} < $1"); test_ore_op(&client, col_name, &sql, &[&low], &[] as &[T]).await; // GT && LT: given [1, 3], `> 1 and < 3` returns [] let sql = - format!("SELECT {col_name} FROM encrypted WHERE {col_name} > $1 AND {col_name} < $2"); + format!("SELECT {col_name} FROM {table} WHERE {col_name} > $1 AND {col_name} < $2"); test_ore_op(&client, col_name, &sql, &[&low, &high], &[] as &[T]).await; // LTEQ: given [1, 3], `<= 3` returns [1, 3] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} <= $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} <= $1"); test_ore_op( &client, col_name, @@ -119,7 +154,7 @@ mod tests { .await; // GTEQ: given [1, 3], `>= 1` returns [1, 3] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} >= $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} >= $1"); test_ore_op( &client, col_name, @@ -130,11 +165,11 @@ mod tests { .await; // EQ: given [1, 3], `= 1` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} = $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} = $1"); test_ore_op(&client, col_name, &sql, &[&low], std::slice::from_ref(&low)).await; // NEQ: given [1, 3], `<> 3` returns [1] - let sql = format!("SELECT {col_name} FROM encrypted WHERE {col_name} <> $1"); + let sql = format!("SELECT {col_name} FROM {table} WHERE {col_name} <> $1"); test_ore_op( &client, col_name, diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs index 4e71d8ee..e6a1172e 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -36,13 +36,18 @@ mod tests { use super::*; use serial_test::serial; + // Multitenant tests share the `encrypted` table — isolation comes + // from the per-tenant keyset, and `#[serial]` keeps the shared + // table from racing across tests. + const TABLE: &str = "encrypted"; + #[tokio::test] #[serial] async fn multitenant_ore_order_text() { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_text(&client).await; + ore_order_helpers::ore_order_text(&client, TABLE).await; } #[tokio::test] @@ -51,7 +56,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_text_desc(&client).await; + ore_order_helpers::ore_order_text_desc(&client, TABLE).await; } #[tokio::test] @@ -60,7 +65,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_nulls_last_by_default(&client).await; + ore_order_helpers::ore_order_nulls_last_by_default(&client, TABLE).await; } #[tokio::test] @@ -69,7 +74,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_nulls_first(&client).await; + ore_order_helpers::ore_order_nulls_first(&client, TABLE).await; } #[tokio::test] @@ -78,7 +83,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_qualified_column(&client).await; + ore_order_helpers::ore_order_qualified_column(&client, TABLE).await; } #[tokio::test] @@ -87,7 +92,8 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_qualified_column_with_alias(&client).await; + ore_order_helpers::ore_order_qualified_column_with_alias(&client, TABLE) + .await; } #[tokio::test] @@ -96,7 +102,10 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client).await; + ore_order_helpers::ore_order_no_eql_column_in_select_projection( + &client, TABLE, + ) + .await; } #[tokio::test] @@ -105,7 +114,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_plaintext_column(&client).await; + ore_order_helpers::ore_order_plaintext_column(&client, TABLE).await; } #[tokio::test] @@ -114,7 +123,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_plaintext_and_eql_columns(&client).await; + ore_order_helpers::ore_order_plaintext_and_eql_columns(&client, TABLE).await; } #[tokio::test] @@ -123,7 +132,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_simple_protocol(&client).await; + ore_order_helpers::ore_order_simple_protocol(&client, TABLE).await; } #[tokio::test] @@ -135,6 +144,7 @@ mod tests { let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int2", values, SortDirection::Asc, @@ -151,6 +161,7 @@ mod tests { let values: Vec = vec![-100, -10, -1, 0, 1, 5, 10, 20, 100, 200]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int2", values, SortDirection::Desc, @@ -169,6 +180,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int4", values, SortDirection::Asc, @@ -187,6 +199,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int4", values, SortDirection::Desc, @@ -205,6 +218,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int8", values, SortDirection::Asc, @@ -223,6 +237,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_int8", values, SortDirection::Desc, @@ -241,6 +256,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_float8", values, SortDirection::Asc, @@ -259,6 +275,7 @@ mod tests { ]; ore_order_helpers::ore_order_generic( &client, + TABLE, "encrypted_float8", values, SortDirection::Desc, diff --git a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs index 4a93e416..55368ad1 100644 --- a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs +++ b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs @@ -3,6 +3,10 @@ //! //! Used by both `map_ore_index_order` (default keyset) and `multitenant::ore_order` //! (per-tenant keysets) to avoid duplicating test logic. +//! +//! Each helper takes a `table` name so callers can target their own per-test +//! fixture table — this prevents parallel-test races on a shared `encrypted` +//! table. use std::fmt::Debug; use tokio_postgres::types::{FromSql, ToSql}; @@ -27,7 +31,7 @@ impl SortDirection { } /// Text ASC ordering with lexicographic edge cases. -pub async fn ore_order_text(client: &tokio_postgres::Client) { +pub async fn ore_order_text(client: &tokio_postgres::Client, table: &str) { let values = [ "aardvark", "aplomb", @@ -38,17 +42,17 @@ pub async fn ore_order_text(client: &tokio_postgres::Client) { "zephyr", ]; - let insert_sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; + let insert_sql = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert_sql, &[&random_id(), &values[idx]]) + .query(&insert_sql, &[&random_id(), &values[idx]]) .await .unwrap(); } - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected: Vec = values.iter().map(|s| s.to_string()).collect(); @@ -57,7 +61,7 @@ pub async fn ore_order_text(client: &tokio_postgres::Client) { } /// Text DESC ordering with lexicographic edge cases. -pub async fn ore_order_text_desc(client: &tokio_postgres::Client) { +pub async fn ore_order_text_desc(client: &tokio_postgres::Client, table: &str) { let values = [ "aardvark", "aplomb", @@ -68,17 +72,17 @@ pub async fn ore_order_text_desc(client: &tokio_postgres::Client) { "zephyr", ]; - let insert_sql = "INSERT INTO encrypted (id, encrypted_text) VALUES ($1, $2)"; + let insert_sql = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client - .query(insert_sql, &[&random_id(), &values[idx]]) + .query(&insert_sql, &[&random_id(), &values[idx]]) .await .unwrap(); } - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text DESC"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text DESC"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected: Vec = values.iter().rev().map(|s| s.to_string()).collect(); @@ -87,27 +91,32 @@ pub async fn ore_order_text_desc(client: &tokio_postgres::Client) { } /// NULLs sort last in ASC by default. -pub async fn ore_order_nulls_last_by_default(client: &tokio_postgres::Client) { +pub async fn ore_order_nulls_last_by_default(client: &tokio_postgres::Client, table: &str) { let s_one = "a"; let s_two = "b"; client - .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) + .query( + &format!("INSERT INTO {table} (id) values ($1)"), + &[&random_id()], + ) .await .unwrap(); - let sql = " - INSERT INTO encrypted (id, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4) - "; + " + ); client - .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) + .query(&sql, &[&random_id(), &s_one, &random_id(), &s_two]) .await .unwrap(); - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows .iter() @@ -119,27 +128,34 @@ pub async fn ore_order_nulls_last_by_default(client: &tokio_postgres::Client) { } /// NULLS FIRST clause. -pub async fn ore_order_nulls_first(client: &tokio_postgres::Client) { +pub async fn ore_order_nulls_first(client: &tokio_postgres::Client, table: &str) { let s_one = "a"; let s_two = "b"; - let sql = " - INSERT INTO encrypted (id, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4) - "; + " + ); client - .query(sql, &[&random_id(), &s_one, &random_id(), &s_two]) + .query(&sql, &[&random_id(), &s_one, &random_id(), &s_two]) .await .unwrap(); client - .query("INSERT INTO encrypted (id) values ($1)", &[&random_id()]) + .query( + &format!("INSERT INTO {table} (id) values ($1)"), + &[&random_id()], + ) .await .unwrap(); - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text NULLS FIRST"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!( + "SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST" + ); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows .iter() @@ -150,20 +166,22 @@ pub async fn ore_order_nulls_first(client: &tokio_postgres::Client) { assert_eq!(actual, expected); } -/// Fully qualified column name: `encrypted.encrypted_text`. -pub async fn ore_order_qualified_column(client: &tokio_postgres::Client) { +/// Fully qualified column name: `.encrypted_text`. +pub async fn ore_order_qualified_column(client: &tokio_postgres::Client, table: &str) { let s_one = "a"; let s_two = "b"; let s_three = "c"; - let sql = " - INSERT INTO encrypted (id, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4), ($5, $6) - "; + " + ); client .query( - sql, + &sql, &[ &random_id(), &s_two, @@ -176,8 +194,8 @@ pub async fn ore_order_qualified_column(client: &tokio_postgres::Client) { .await .unwrap(); - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted.encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY {table}.encrypted_text"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected = vec![s_one, s_two, s_three]; @@ -186,19 +204,21 @@ pub async fn ore_order_qualified_column(client: &tokio_postgres::Client) { } /// Table alias: `e.encrypted_text`. -pub async fn ore_order_qualified_column_with_alias(client: &tokio_postgres::Client) { +pub async fn ore_order_qualified_column_with_alias(client: &tokio_postgres::Client, table: &str) { let s_one = "a"; let s_two = "b"; let s_three = "c"; - let sql = " - INSERT INTO encrypted (id, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4), ($5, $6) - "; + " + ); client .query( - sql, + &sql, &[ &random_id(), &s_two, @@ -211,8 +231,8 @@ pub async fn ore_order_qualified_column_with_alias(client: &tokio_postgres::Clie .await .unwrap(); - let sql = "SELECT encrypted_text FROM encrypted e ORDER BY e.encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} e ORDER BY e.encrypted_text"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected = vec![s_one, s_two, s_three]; @@ -221,7 +241,10 @@ pub async fn ore_order_qualified_column_with_alias(client: &tokio_postgres::Clie } /// ORDER BY column not in SELECT projection. -pub async fn ore_order_no_eql_column_in_select_projection(client: &tokio_postgres::Client) { +pub async fn ore_order_no_eql_column_in_select_projection( + client: &tokio_postgres::Client, + table: &str, +) { let id_one = random_id(); let s_one = "a"; let id_two = random_id(); @@ -229,21 +252,23 @@ pub async fn ore_order_no_eql_column_in_select_projection(client: &tokio_postgre let id_three = random_id(); let s_three = "c"; - let sql = " - INSERT INTO encrypted (id, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4), ($5, $6) - "; + " + ); client .query( - sql, + &sql, &[&id_two, &s_two, &id_one, &s_one, &id_three, &s_three], ) .await .unwrap(); - let sql = "SELECT id FROM encrypted ORDER BY encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT id FROM {table} ORDER BY encrypted_text"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected = vec![id_one, id_two, id_three]; @@ -252,19 +277,21 @@ pub async fn ore_order_no_eql_column_in_select_projection(client: &tokio_postgre } /// Plaintext column ordering (sanity check). -pub async fn ore_order_plaintext_column(client: &tokio_postgres::Client) { +pub async fn ore_order_plaintext_column(client: &tokio_postgres::Client, table: &str) { let s_one = "a"; let s_two = "b"; let s_three = "c"; - let sql = " - INSERT INTO encrypted (id, plaintext) + let sql = format!( + " + INSERT INTO {table} (id, plaintext) VALUES ($1, $2), ($3, $4), ($5, $6) - "; + " + ); client .query( - sql, + &sql, &[ &random_id(), &s_two, @@ -277,8 +304,8 @@ pub async fn ore_order_plaintext_column(client: &tokio_postgres::Client) { .await .unwrap(); - let sql = "SELECT plaintext FROM encrypted ORDER BY plaintext"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!("SELECT plaintext FROM {table} ORDER BY plaintext"); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows.iter().map(|row| row.get(0)).collect::>(); let expected = vec![s_one, s_two, s_three]; @@ -287,7 +314,7 @@ pub async fn ore_order_plaintext_column(client: &tokio_postgres::Client) { } /// Mixed plaintext + encrypted column ordering. -pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client) { +pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client, table: &str) { let s_plaintext_one = "a"; let s_plaintext_two = "a"; let s_plaintext_three = "b"; @@ -296,14 +323,16 @@ pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client let s_encrypted_two = "b"; let s_encrypted_three = "c"; - let sql = " - INSERT INTO encrypted (id, plaintext, encrypted_text) + let sql = format!( + " + INSERT INTO {table} (id, plaintext, encrypted_text) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9) - "; + " + ); client .query( - sql, + &sql, &[ &random_id(), &s_plaintext_two, @@ -319,8 +348,10 @@ pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client .await .unwrap(); - let sql = "SELECT plaintext, encrypted_text FROM encrypted ORDER BY plaintext, encrypted_text"; - let rows = client.query(sql, &[]).await.unwrap(); + let sql = format!( + "SELECT plaintext, encrypted_text FROM {table} ORDER BY plaintext, encrypted_text" + ); + let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows .iter() @@ -337,9 +368,9 @@ pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client } /// Simple query protocol ordering. -pub async fn ore_order_simple_protocol(client: &tokio_postgres::Client) { +pub async fn ore_order_simple_protocol(client: &tokio_postgres::Client, table: &str) { let sql = format!( - "INSERT INTO encrypted (id, encrypted_text) VALUES ({}, 'y'), ({}, 'x'), ({}, 'z')", + "INSERT INTO {table} (id, encrypted_text) VALUES ({}, 'y'), ({}, 'x'), ({}, 'z')", random_id(), random_id(), random_id() @@ -347,8 +378,8 @@ pub async fn ore_order_simple_protocol(client: &tokio_postgres::Client) { client.simple_query(&sql).await.unwrap(); - let sql = "SELECT encrypted_text FROM encrypted ORDER BY encrypted_text"; - let rows = client.simple_query(sql).await.unwrap(); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text"); + let rows = client.simple_query(&sql).await.unwrap(); let actual = rows .iter() @@ -373,13 +404,14 @@ pub async fn ore_order_simple_protocol(client: &tokio_postgres::Client) { /// via ORDER BY in the given direction. pub async fn ore_order_generic( client: &tokio_postgres::Client, + table: &str, col_name: &str, values: Vec, direction: SortDirection, ) where for<'a> T: Clone + PartialEq + ToSql + Sync + FromSql<'a> + PartialOrd + Debug, { - let insert_sql = format!("INSERT INTO encrypted (id, {col_name}) VALUES ($1, $2)"); + let insert_sql = format!("INSERT INTO {table} (id, {col_name}) VALUES ($1, $2)"); for idx in interleaved_indices(values.len()) { client @@ -389,7 +421,7 @@ pub async fn ore_order_generic( } let dir = direction.as_sql(); - let select_sql = format!("SELECT {col_name} FROM encrypted ORDER BY {col_name} {dir}"); + let select_sql = format!("SELECT {col_name} FROM {table} ORDER BY {col_name} {dir}"); let rows = client.query(&select_sql, &[]).await.unwrap(); let actual: Vec = rows.iter().map(|row| row.get(0)).collect(); diff --git a/tests/sql/schema.sql b/tests/sql/schema.sql index 9dd78fe9..86a666f2 100644 --- a/tests/sql/schema.sql +++ b/tests/sql/schema.sql @@ -169,45 +169,142 @@ SELECT eql_v2.add_search_config( SELECT eql_v2.add_encrypted_constraint('encrypted', 'encrypted_text'); --- OPE-indexed mirror of the `encrypted` table. --- Uses the new `ope` (Order-Preserving Encryption) index in place of `ore` for --- range/order operators. Same shape as `encrypted` so generic test helpers can --- swap table names and reuse logic. -DROP TABLE IF EXISTS encrypted_ope; -CREATE TABLE encrypted_ope ( - id bigint, - plaintext text, - plaintext_date date, - encrypted_text eql_v2_encrypted, - encrypted_bool eql_v2_encrypted, - encrypted_int2 eql_v2_encrypted, - encrypted_int4 eql_v2_encrypted, - encrypted_int8 eql_v2_encrypted, - encrypted_float8 eql_v2_encrypted, - encrypted_date eql_v2_encrypted, - PRIMARY KEY(id) -); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_text', 'unique', 'text'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_text', 'ope', 'text'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_bool', 'unique', 'boolean'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_bool', 'ope', 'boolean'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int2', 'unique', 'small_int'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int2', 'ope', 'small_int'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int4', 'unique', 'int'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int4', 'ope', 'int'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int8', 'unique', 'big_int'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_int8', 'ope', 'big_int'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_float8', 'unique', 'double'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_float8', 'ope', 'double'); - -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_date', 'unique', 'date'); -SELECT eql_v2.add_search_config('encrypted_ope', 'encrypted_date', 'ope', 'date'); +-- Per-test ORE-indexed tables. +-- Each integration test that exercises ORE range/order operators gets its own +-- table. Eliminates parallel-test races on a shared `encrypted` table without +-- having to mark tests `#[serial]`. +-- +-- Schema mirrors `encrypted` minus the jsonb columns (these ORE tests never +-- touch jsonb). Each table gets the same `add_search_config` and constraint +-- calls as the original `encrypted` table. +DO $$ +DECLARE + test_tables text[] := ARRAY[ + -- map_ore_index_where (one per column type) + 'encrypted_ore_where_int2', + 'encrypted_ore_where_int4', + 'encrypted_ore_where_int8', + 'encrypted_ore_where_float8', + 'encrypted_ore_where_date', + 'encrypted_ore_where_text', + 'encrypted_ore_where_bool', + -- map_ore_index_order (one per test fn) + 'encrypted_ore_order_text', + 'encrypted_ore_order_text_desc', + 'encrypted_ore_order_nulls_last', + 'encrypted_ore_order_nulls_first', + 'encrypted_ore_order_qualified', + 'encrypted_ore_order_qualified_alias', + 'encrypted_ore_order_no_select_projection', + 'encrypted_ore_order_plaintext_column', + 'encrypted_ore_order_plaintext_and_eql', + 'encrypted_ore_order_simple_protocol', + 'encrypted_ore_order_int2', + 'encrypted_ore_order_int2_desc', + 'encrypted_ore_order_int4', + 'encrypted_ore_order_int4_desc', + 'encrypted_ore_order_int8', + 'encrypted_ore_order_int8_desc', + 'encrypted_ore_order_float8', + 'encrypted_ore_order_float8_desc' + ]; + tn text; +BEGIN + FOREACH tn IN ARRAY test_tables LOOP + EXECUTE format('DROP TABLE IF EXISTS %I CASCADE', tn); + EXECUTE format( + 'CREATE TABLE %I ( + id bigint, + plaintext text, + plaintext_date date, + encrypted_text eql_v2_encrypted, + encrypted_bool eql_v2_encrypted, + encrypted_int2 eql_v2_encrypted, + encrypted_int4 eql_v2_encrypted, + encrypted_int8 eql_v2_encrypted, + encrypted_float8 eql_v2_encrypted, + encrypted_date eql_v2_encrypted, + PRIMARY KEY(id) + )', tn); + + PERFORM eql_v2.add_search_config(tn, 'encrypted_text', 'unique', 'text'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_text', 'match', 'text'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_text', 'ore', 'text'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_bool', 'unique', 'boolean'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_bool', 'ore', 'boolean'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int2', 'unique', 'small_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int2', 'ore', 'small_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int4', 'unique', 'int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int4', 'ore', 'int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int8', 'unique', 'big_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int8', 'ore', 'big_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_float8', 'unique', 'double'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_float8', 'ore', 'double'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_date', 'unique', 'date'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_date', 'ore', 'date'); + + PERFORM eql_v2.add_encrypted_constraint(tn, 'encrypted_text'); + END LOOP; +END $$; + + +-- Per-test OPE-indexed tables (parallels the ORE block above; uses 'ope' index). +DO $$ +DECLARE + test_tables text[] := ARRAY[ + -- map_ope_index_where (one per column type) + 'encrypted_ope_where_int2', + 'encrypted_ope_where_int4', + 'encrypted_ope_where_int8', + 'encrypted_ope_where_float8', + 'encrypted_ope_where_date', + 'encrypted_ope_where_text', + 'encrypted_ope_where_bool', + -- map_ope_index_order (one per test fn) + 'encrypted_ope_order_text_asc', + 'encrypted_ope_order_text_desc', + 'encrypted_ope_order_int4_asc', + 'encrypted_ope_order_int4_desc', + 'encrypted_ope_order_nulls_last', + 'encrypted_ope_order_nulls_first' + ]; + tn text; +BEGIN + FOREACH tn IN ARRAY test_tables LOOP + EXECUTE format('DROP TABLE IF EXISTS %I CASCADE', tn); + EXECUTE format( + 'CREATE TABLE %I ( + id bigint, + plaintext text, + plaintext_date date, + encrypted_text eql_v2_encrypted, + encrypted_bool eql_v2_encrypted, + encrypted_int2 eql_v2_encrypted, + encrypted_int4 eql_v2_encrypted, + encrypted_int8 eql_v2_encrypted, + encrypted_float8 eql_v2_encrypted, + encrypted_date eql_v2_encrypted, + PRIMARY KEY(id) + )', tn); + + PERFORM eql_v2.add_search_config(tn, 'encrypted_text', 'unique', 'text'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_text', 'ope', 'text'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_bool', 'unique', 'boolean'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_bool', 'ope', 'boolean'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int2', 'unique', 'small_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int2', 'ope', 'small_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int4', 'unique', 'int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int4', 'ope', 'int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int8', 'unique', 'big_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_int8', 'ope', 'big_int'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_float8', 'unique', 'double'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_float8', 'ope', 'double'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_date', 'unique', 'date'); + PERFORM eql_v2.add_search_config(tn, 'encrypted_date', 'ope', 'date'); + + PERFORM eql_v2.add_encrypted_constraint(tn, 'encrypted_text'); + END LOOP; +END $$; -- This is the exact same schema as above but using a database-generated primary key. From 17d9de781d47531861ce93fcdc9dbe98c1416ec4 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Fri, 8 May 2026 15:23:17 +1000 Subject: [PATCH 3/4] docs(ope): document ope index type and add parse unit test Fills in two follow-up gaps from 832049a1 ("feat(ope): add ope index type alongside ore"): - Add `can_parse_ope_index` unit test in the encrypt-config manager, mirroring `can_parse_ore_index`. The earlier commit's message claimed this test was added but it wasn't. - Document `ope` as a drop-in alternative to `ore`: - `docs/how-to/index.md` lists `ope` alongside `match` and `ore` and notes the operator parity ("pick ope or ore per column, not both"). - `docs/reference/searchable-json.md` updates the `MIN()/MAX()` line to mention `ope` as well. - `docs/sql/schema-example.sql` adds a comment near the `ore` entry pointing to `ope` as the alternative. --- docs/how-to/index.md | 12 ++++++++++-- docs/reference/searchable-json.md | 2 +- docs/sql/schema-example.sql | 2 ++ .../src/proxy/encrypt_config/manager.rs | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/how-to/index.md b/docs/how-to/index.md index 4e35f3bf..2d229dda 100644 --- a/docs/how-to/index.md +++ b/docs/how-to/index.md @@ -201,7 +201,7 @@ This statement adds a `unique` index for the `email` column in the `users` table `unique` indexes are used to find records with columns with unique values, like with the `=` operator. -There are two other types of encrypted indexes you can use on `text` data: +There are other types of encrypted indexes you can use on `text` data: ```sql SELECT eql_v2.add_search_config( @@ -217,10 +217,18 @@ SELECT eql_v2.add_search_config( 'ore', 'text' ); + +SELECT eql_v2.add_search_config( + 'users', + 'email', + 'ope', + 'text' +); ``` The first SQL statement adds a `match` index, which is used for partial matches with `LIKE`. -The second SQL statement adds an `ore` index, which is used for ordering with `ORDER BY`. +The second SQL statement adds an `ore` index, which is used for ordering with `ORDER BY` and range comparisons (`<`, `<=`, `>`, `>=`). +The third SQL statement adds an `ope` index, which supports the same range and ordering operators as `ore` and is a drop-in alternative — pick `ope` or `ore` per column, not both. > ![IMPORTANT] diff --git a/docs/reference/searchable-json.md b/docs/reference/searchable-json.md index 5f1329d8..c45589fb 100644 --- a/docs/reference/searchable-json.md +++ b/docs/reference/searchable-json.md @@ -127,7 +127,7 @@ Encrypted literals cannot be passed as arguments to SQL functions. Encrypted col Examples: - `AVG()` cannot be used on encrypted numeric values ❌ -- `MIN()` and `MAX()` can be used on encrypted values with ORE index ✅ +- `MIN()` and `MAX()` can be used on encrypted values with an `ore` or `ope` index ✅ - `LOWER()` cannot be used on encrypted text (operates only on plaintext) ❌ ⚠️ **CAST Operations**: CAST operations cannot work on encrypted data because casting would require decryption within the database, which is impossible. EQL's `ste_vec` configuration enables direct comparison and ordering operations on encrypted values without requiring CAST. diff --git a/docs/sql/schema-example.sql b/docs/sql/schema-example.sql index 9244e79e..c89bb1b7 100644 --- a/docs/sql/schema-example.sql +++ b/docs/sql/schema-example.sql @@ -23,6 +23,8 @@ SELECT eql_v2.add_search_config( 'text' ); +-- 'ore' supports ordering and range comparisons. 'ope' is a drop-in +-- alternative with the same operator support — choose one per column. SELECT eql_v2.add_search_config( 'users', 'encrypted_email', diff --git a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs index 0b33c28e..65dcaf0d 100644 --- a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs +++ b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs @@ -360,6 +360,23 @@ mod tests { assert_eq!(column.indexes[0].index_type, IndexType::Ore); } + #[test] + fn can_parse_ope_index() { + let json = json!({ + "v": 1, + "tables": { + "users": { "email": { "indexes": { "ope": {} } } } + } + }); + + let encrypt_config = parse(json); + let column = encrypt_config + .get(&Identifier::new("users", "email")) + .unwrap(); + + assert_eq!(column.indexes[0].index_type, IndexType::Ope); + } + #[test] fn can_parse_unique_index_with_defaults() { let json = json!({ From 69123e62ca4e7225ad1945dd362395ce1b307827 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Fri, 8 May 2026 22:26:19 +1000 Subject: [PATCH 4/4] chore: fmt --- .../src/map_ope_index_order.rs | 11 +++---- .../src/map_ope_index_where.rs | 32 +++---------------- .../src/map_ore_index_where.rs | 32 +++---------------- .../src/multitenant/ore_order.rs | 9 ++---- .../src/ore_order_helpers.rs | 9 ++---- 5 files changed, 18 insertions(+), 75 deletions(-) diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs index 67b9b3ac..0933e384 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_order.rs @@ -115,8 +115,7 @@ mod tests { let null_insert = format!("INSERT INTO {table} (id) VALUES ($1)"); client.query(&null_insert, &[&random_id()]).await.unwrap(); - let insert = - format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); + let insert = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); client .query(&insert, &[&random_id(), &"a", &random_id(), &"b"]) .await @@ -140,8 +139,7 @@ mod tests { clear_table(table).await; let client = connect_with_tls(PROXY).await; - let insert = - format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); + let insert = format!("INSERT INTO {table} (id, encrypted_text) VALUES ($1, $2), ($3, $4)"); client .query(&insert, &[&random_id(), &"a", &random_id(), &"b"]) .await @@ -150,9 +148,8 @@ mod tests { let null_insert = format!("INSERT INTO {table} (id) VALUES ($1)"); client.query(&null_insert, &[&random_id()]).await.unwrap(); - let select = format!( - "SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST" - ); + let select = + format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST"); let rows = client.query(&select, &[]).await.unwrap(); let actual: Vec> = rows.iter().map(|r| r.get(0)).collect(); diff --git a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs index b9dbf929..68e761dd 100644 --- a/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ope_index_where.rs @@ -7,35 +7,17 @@ mod tests { #[tokio::test] async fn map_ope_where_generic_int2() { - map_ope_where_generic( - "encrypted_ope_where_int2", - "encrypted_int2", - 40i16, - 99i16, - ) - .await; + map_ope_where_generic("encrypted_ope_where_int2", "encrypted_int2", 40i16, 99i16).await; } #[tokio::test] async fn map_ope_where_generic_int4() { - map_ope_where_generic( - "encrypted_ope_where_int4", - "encrypted_int4", - 40i32, - 99i32, - ) - .await; + map_ope_where_generic("encrypted_ope_where_int4", "encrypted_int4", 40i32, 99i32).await; } #[tokio::test] async fn map_ope_where_generic_int8() { - map_ope_where_generic( - "encrypted_ope_where_int8", - "encrypted_int8", - 40i64, - 99i64, - ) - .await; + map_ope_where_generic("encrypted_ope_where_int8", "encrypted_int8", 40i64, 99i64).await; } #[tokio::test] @@ -69,13 +51,7 @@ mod tests { #[tokio::test] async fn map_ope_where_generic_bool() { - map_ope_where_generic( - "encrypted_ope_where_bool", - "encrypted_bool", - false, - true, - ) - .await; + map_ope_where_generic("encrypted_ope_where_bool", "encrypted_bool", false, true).await; } /// Tests OPE operations against a per-test fixture table. diff --git a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs index 5f89d385..a5f7285c 100644 --- a/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs +++ b/packages/cipherstash-proxy-integration/src/map_ore_index_where.rs @@ -7,35 +7,17 @@ mod tests { #[tokio::test] async fn map_ore_where_generic_int2() { - map_ore_where_generic( - "encrypted_ore_where_int2", - "encrypted_int2", - 40i16, - 99i16, - ) - .await; + map_ore_where_generic("encrypted_ore_where_int2", "encrypted_int2", 40i16, 99i16).await; } #[tokio::test] async fn map_ore_where_generic_int4() { - map_ore_where_generic( - "encrypted_ore_where_int4", - "encrypted_int4", - 40i32, - 99i32, - ) - .await; + map_ore_where_generic("encrypted_ore_where_int4", "encrypted_int4", 40i32, 99i32).await; } #[tokio::test] async fn map_ore_where_generic_int8() { - map_ore_where_generic( - "encrypted_ore_where_int8", - "encrypted_int8", - 40i64, - 99i64, - ) - .await; + map_ore_where_generic("encrypted_ore_where_int8", "encrypted_int8", 40i64, 99i64).await; } #[tokio::test] @@ -69,13 +51,7 @@ mod tests { #[tokio::test] async fn map_ore_where_generic_bool() { - map_ore_where_generic( - "encrypted_ore_where_bool", - "encrypted_bool", - false, - true, - ) - .await; + map_ore_where_generic("encrypted_ore_where_bool", "encrypted_bool", false, true).await; } /// Tests ORE operations with 2 values - high & low - against a per-test diff --git a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs index e6a1172e..0a8dcbe4 100644 --- a/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs +++ b/packages/cipherstash-proxy-integration/src/multitenant/ore_order.rs @@ -92,8 +92,7 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_qualified_column_with_alias(&client, TABLE) - .await; + ore_order_helpers::ore_order_qualified_column_with_alias(&client, TABLE).await; } #[tokio::test] @@ -102,10 +101,8 @@ mod tests { trace(); clear().await; let client = connect_as_tenant(&keyset_id($env_var)).await; - ore_order_helpers::ore_order_no_eql_column_in_select_projection( - &client, TABLE, - ) - .await; + ore_order_helpers::ore_order_no_eql_column_in_select_projection(&client, TABLE) + .await; } #[tokio::test] diff --git a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs index 55368ad1..8c3d3f73 100644 --- a/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs +++ b/packages/cipherstash-proxy-integration/src/ore_order_helpers.rs @@ -152,9 +152,7 @@ pub async fn ore_order_nulls_first(client: &tokio_postgres::Client, table: &str) .await .unwrap(); - let sql = format!( - "SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST" - ); + let sql = format!("SELECT encrypted_text FROM {table} ORDER BY encrypted_text NULLS FIRST"); let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows @@ -348,9 +346,8 @@ pub async fn ore_order_plaintext_and_eql_columns(client: &tokio_postgres::Client .await .unwrap(); - let sql = format!( - "SELECT plaintext, encrypted_text FROM {table} ORDER BY plaintext, encrypted_text" - ); + let sql = + format!("SELECT plaintext, encrypted_text FROM {table} ORDER BY plaintext, encrypted_text"); let rows = client.query(&sql, &[]).await.unwrap(); let actual = rows