From c05f78b6bf77e630a640de5feca5d7f17ea8fae8 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 26 Apr 2026 14:16:55 +1000 Subject: [PATCH 1/3] chore(deps): point cipherstash-client/cts-common at local cipherstash-suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the in-flight OPE work in cipherstash-suite that hasn't been published yet. The newer suite renames Plaintext::Utf8Str → Text and Plaintext::JsonB → Json (same for ColumnType); this commit applies the mechanical follow-on changes in the proxy. Workspace builds clean and proxy unit tests (97) pass. The path override is temporary; the workspace dep should be reverted to the crates.io version once cipherstash-suite is published. --- Cargo.lock | 44 +++++++------------ Cargo.toml | 7 ++- .../src/postgresql/context/column.rs | 6 +-- .../src/postgresql/data/from_sql.rs | 24 +++++----- .../src/postgresql/data/to_sql.rs | 8 ++-- .../src/proxy/encrypt_config/config.rs | 6 +-- 6 files changed, 42 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11adb87b..71cfa818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,9 +762,7 @@ dependencies = [ [[package]] name = "cipherstash-client" -version = "0.34.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200537bf2ab562b085e34df7e3391d0426ab04eea3ed588a7fc27f1bd218ee33" +version = "0.34.1-alpha.3" dependencies = [ "aes-gcm-siv", "anyhow", @@ -776,7 +774,7 @@ dependencies = [ "blake3", "cfg-if", "chrono", - "cipherstash-config 0.34.0-alpha.4", + "cipherstash-config 0.34.1-alpha.3", "cipherstash-core", "cllw-ore", "cts-common", @@ -794,7 +792,7 @@ dependencies = [ "ore-rs", "percent-encoding", "rand 0.8.5", - "recipher 0.2.0", + "recipher 0.2.1", "reqwest", "reqwest-middleware", "reqwest-retry", @@ -837,20 +835,17 @@ dependencies = [ [[package]] name = "cipherstash-config" -version = "0.34.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "333ba6c42338ce6bbbc515fb75e43b57311ece1a9ea41e7daabe50478c342841" +version = "0.34.1-alpha.3" dependencies = [ "bitflags", "serde", + "serde_json", "thiserror 1.0.69", ] [[package]] name = "cipherstash-core" -version = "0.34.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32921e505e39f8f7cae9f55e82462d8dd92764a9148f479b42abf52e60e90437" +version = "0.34.1-alpha.3" dependencies = [ "hmac", "lazy_static", @@ -982,8 +977,6 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cllw-ore" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c676b8e0a3130e6f8b4398d9aa5b287c3ce7074ac89f1ccf1570ebeb22281629" dependencies = [ "blake3", "hex", @@ -1192,9 +1185,7 @@ dependencies = [ [[package]] name = "cts-common" -version = "0.34.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7817fb03b19c6a588bc9120fd876a6d65f531a0b2aa0d39384bc78f3c4c4340" +version = "0.34.1-alpha.3" dependencies = [ "arrayvec", "axum", @@ -1214,7 +1205,9 @@ dependencies = [ "rand 0.8.5", "regex", "serde", + "serde_json", "thiserror 1.0.69", + "tracing", "url", "utoipa", "uuid", @@ -3501,9 +3494,7 @@ dependencies = [ [[package]] name = "recipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061598013445a8bb847d0c95ee33b5e95c1d198d5242b6a8b9f3078aa7437e79" +version = "0.2.1" dependencies = [ "aes", "async-trait", @@ -4349,9 +4340,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stack-auth" -version = "0.34.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e8a681ffc8eb40575fb5f40b8316f1b9e03074eb1e4951e0690b00b0349fed" +version = "0.34.1-alpha.3" dependencies = [ "aquamarine", "cts-common", @@ -4370,13 +4359,12 @@ dependencies = [ "vitaminc", "vitaminc-protected", "zeroize", + "zerokms-protocol", ] [[package]] name = "stack-profile" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56fdb1e5ef2111e616fb46da39ad63485b3f3c82de3245fe3c14ce52e8775112" +version = "0.34.1-alpha.3" dependencies = [ "dirs", "gethostname", @@ -6286,12 +6274,10 @@ dependencies = [ [[package]] name = "zerokms-protocol" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52f1d857d2e6d4fe258c49906d53f8b2666c4841dc2e39e67cfea3717382294" +version = "0.12.8" dependencies = [ "base64", - "cipherstash-config 0.34.0-alpha.4", + "cipherstash-config 0.34.1-alpha.3", "const-hex", "cts-common", "fake 2.10.0", diff --git a/Cargo.toml b/Cargo.toml index d1a09aec..485c472f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,11 @@ debug = true [workspace.dependencies] sqltk = { version = "0.10.0" } -cipherstash-client = { version = "0.34.0-alpha.4" } -cts-common = { version = "0.34.0-alpha.4" } +# TODO: revert to crates.io once the cipherstash-suite version with `IndexType::Ope` +# is published. See https://github.com/cipherstash/cipherstash-suite for the +# OPE-enabled change. +cipherstash-client = { path = "../cipherstash-suite/packages/cipherstash-client" } +cts-common = { path = "../cipherstash-suite/packages/cts-common" } thiserror = "2.0.9" tokio = { version = "1.44.2", features = ["full"] } diff --git a/packages/cipherstash-proxy/src/postgresql/context/column.rs b/packages/cipherstash-proxy/src/postgresql/context/column.rs index 0088f0b3..c4deb993 100644 --- a/packages/cipherstash-proxy/src/postgresql/context/column.rs +++ b/packages/cipherstash-proxy/src/postgresql/context/column.rs @@ -77,8 +77,8 @@ fn column_type_to_postgres_type( (ColumnType::Int, _) => postgres_types::Type::INT4, (ColumnType::SmallInt, _) => postgres_types::Type::INT2, (ColumnType::Timestamp, _) => postgres_types::Type::TIMESTAMPTZ, - (ColumnType::Utf8Str, _) => postgres_types::Type::TEXT, - (ColumnType::JsonB, EqlTermVariant::JsonAccessor) => postgres_types::Type::TEXT, - (ColumnType::JsonB, _) => postgres_types::Type::JSONB, + (ColumnType::Text, _) => postgres_types::Type::TEXT, + (ColumnType::Json, EqlTermVariant::JsonAccessor) => postgres_types::Type::TEXT, + (ColumnType::Json, _) => postgres_types::Type::JSONB, } } diff --git a/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs b/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs index 5736bdee..f70e4962 100644 --- a/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs +++ b/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs @@ -115,7 +115,7 @@ pub fn literal_from_sql( /// /// | Input Type | Target Column Type | Result | /// |------------|--------------------|--------| -/// | `Type::INT4` | `ColumnType::Utf8Str` | `Plaintext::Utf8Str` | +/// | `Type::INT4` | `ColumnType::Text` | `Plaintext::Text` | /// | `Type::INT2` | `ColumnType::Int` | `Plaintext::Int` | /// | `Type::INT8` | `ColumnType::Int` | `Error`` | fn text_from_sql( @@ -126,7 +126,7 @@ fn text_from_sql( debug!(target: ENCODING, ?val, ?eql_term, ?col_type); match (eql_term, col_type) { - (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Utf8Str) => { + (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Text) => { Ok(Plaintext::new(val)) } (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Float) => { @@ -168,7 +168,7 @@ fn text_from_sql( } // If JSONB, JSONPATH values are treated as strings - (EqlTermVariant::JsonPath | EqlTermVariant::JsonAccessor, ColumnType::JsonB) => { + (EqlTermVariant::JsonPath | EqlTermVariant::JsonAccessor, ColumnType::Json) => { let val = if val.starts_with("$.") { val.to_string() } else { @@ -176,12 +176,12 @@ fn text_from_sql( }; Ok(Plaintext::new(val)) } - (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::JsonB) => { + (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Json) => { serde_json::from_str::(val) .map_err(|_| MappingError::CouldNotParseParameter) .map(Plaintext::new) } - (EqlTermVariant::Tokenized, ColumnType::Utf8Str) => Ok(Plaintext::new(val)), + (EqlTermVariant::Tokenized, ColumnType::Text) => Ok(Plaintext::new(val)), (eql_term, col_type) => Err(MappingError::UnsupportedParameterType { eql_term, @@ -202,7 +202,7 @@ fn binary_from_sql( debug!(target: ENCODING, ?pg_type, ?eql_term, ?col_type); match (eql_term, col_type, pg_type) { - (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Utf8Str, _) => { + (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Text, _) => { parse_bytes_from_sql::(bytes, pg_type).map(Plaintext::new) } (EqlTermVariant::Full | EqlTermVariant::Partial, ColumnType::Boolean, _) => { @@ -253,7 +253,7 @@ fn binary_from_sql( } // If JSONB, JSONPATH values are treated as strings - (EqlTermVariant::JsonPath, ColumnType::JsonB, &Type::JSONPATH) => { + (EqlTermVariant::JsonPath, ColumnType::Json, &Type::JSONPATH) => { parse_bytes_from_sql::(bytes, pg_type).map(|val| { let val = if val.starts_with("$.") { val @@ -263,7 +263,7 @@ fn binary_from_sql( Plaintext::new(val) }) } - (EqlTermVariant::JsonAccessor, ColumnType::JsonB, &Type::TEXT | &Type::VARCHAR) => { + (EqlTermVariant::JsonAccessor, ColumnType::Json, &Type::TEXT | &Type::VARCHAR) => { parse_bytes_from_sql::(bytes, pg_type).map(|val| { let val = if val.starts_with("$.") { val @@ -276,7 +276,7 @@ fn binary_from_sql( // Python psycopg sends JSON/B as BYTEA ( EqlTermVariant::Full | EqlTermVariant::Partial, - ColumnType::JsonB, + ColumnType::Json, &Type::JSON | &Type::JSONB | &Type::BYTEA, ) => parse_bytes_from_sql::(bytes, pg_type).map(Plaintext::new), @@ -356,9 +356,9 @@ fn decimal_from_sql( .ok_or(MappingError::CouldNotParseParameter) .map(Plaintext::new), - ColumnType::Utf8Str => Ok(Plaintext::new(decimal.to_string())), + ColumnType::Text => Ok(Plaintext::new(decimal.to_string())), - ColumnType::JsonB => { + ColumnType::Json => { let val: serde_json::Value = serde_json::from_str(&decimal.to_string()) .map_err(|_| MappingError::CouldNotParseParameter)?; Ok(Plaintext::new(val)) @@ -408,7 +408,7 @@ mod tests { config: ColumnConfig { name: "column".to_owned(), in_place: false, - cast_type: ColumnType::Utf8Str, + cast_type: ColumnType::Text, indexes: vec![], mode: ColumnMode::PlaintextDuplicate, }, diff --git a/packages/cipherstash-proxy/src/postgresql/data/to_sql.rs b/packages/cipherstash-proxy/src/postgresql/data/to_sql.rs index 6bc57d73..1007d266 100644 --- a/packages/cipherstash-proxy/src/postgresql/data/to_sql.rs +++ b/packages/cipherstash-proxy/src/postgresql/data/to_sql.rs @@ -16,7 +16,7 @@ pub fn to_sql(plaintext: &Plaintext, format_code: &FormatCode) -> Result Result { let s = match &plaintext { - Plaintext::Utf8Str(Some(x)) => x.to_string(), + Plaintext::Text(Some(x)) => x.to_string(), Plaintext::Int(Some(x)) => x.to_string(), Plaintext::BigInt(Some(x)) => x.to_string(), Plaintext::BigUInt(Some(x)) => x.to_string(), @@ -26,7 +26,7 @@ fn text_to_sql(plaintext: &Plaintext) -> Result { Plaintext::NaiveDate(Some(x)) => x.to_string(), Plaintext::SmallInt(Some(x)) => x.to_string(), Plaintext::Timestamp(Some(x)) => x.to_string(), - Plaintext::JsonB(Some(x)) => x.to_string(), + Plaintext::Json(Some(x)) => x.to_string(), _ => "".to_string(), }; @@ -44,8 +44,8 @@ fn binary_to_sql(plaintext: &Plaintext) -> Result { Plaintext::NaiveDate(x) => x.to_sql_checked(&Type::DATE, &mut bytes), Plaintext::SmallInt(x) => x.to_sql_checked(&Type::INT2, &mut bytes), Plaintext::Timestamp(x) => x.to_sql_checked(&Type::TIMESTAMPTZ, &mut bytes), - Plaintext::Utf8Str(x) => x.to_sql_checked(&Type::TEXT, &mut bytes), - Plaintext::JsonB(x) => x.to_sql_checked(&Type::JSONB, &mut bytes), + Plaintext::Text(x) => x.to_sql_checked(&Type::TEXT, &mut bytes), + Plaintext::Json(x) => x.to_sql_checked(&Type::JSONB, &mut bytes), Plaintext::Decimal(x) => x.to_sql_checked(&Type::NUMERIC, &mut bytes), // TODO: Implement these Plaintext::BigUInt(_x) => unimplemented!(), diff --git a/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs b/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs index 41ae3e13..af132024 100644 --- a/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs +++ b/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs @@ -133,8 +133,8 @@ impl From for ColumnType { CastAs::Boolean => ColumnType::Boolean, CastAs::Date => ColumnType::Date, CastAs::Real | CastAs::Double => ColumnType::Float, - CastAs::Text => ColumnType::Utf8Str, - CastAs::JsonB => ColumnType::JsonB, + CastAs::Text => ColumnType::Text, + CastAs::JsonB => ColumnType::Json, } } } @@ -237,7 +237,7 @@ mod tests { let column = encrypt_config.get(&ident).expect("column exists"); - assert_eq!(column.cast_type, ColumnType::Utf8Str); + assert_eq!(column.cast_type, ColumnType::Text); assert!(column.indexes.is_empty()); } From 2a110dc030467cb9c4ecc23391a0cc073707e8d7 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 26 Apr 2026 14:25:57 +1000 Subject: [PATCH 2/3] 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 +++++++++++++++ .../src/proxy/encrypt_config/config.rs | 33 +++ tests/sql/schema.sql | 41 ++++ 7 files changed, 463 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/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs b/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs index af132024..1b4cdb67 100644 --- a/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs +++ b/packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs @@ -68,6 +68,8 @@ pub enum CastAs { pub struct Indexes { #[serde(rename = "ore")] ore_index: Option, + #[serde(rename = "ope")] + ope_index: Option, #[serde(rename = "unique")] unique_index: Option, #[serde(rename = "match")] @@ -79,6 +81,9 @@ pub struct Indexes { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct OreIndexOpts {} +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct OpeIndexOpts {} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct MatchIndexOpts { #[serde(default = "default_tokenizer")] @@ -174,6 +179,10 @@ impl Column { config = config.add_index(Index::new_ore()); } + if self.indexes.ope_index.is_some() { + config = config.add_index(Index::new_ope()); + } + if let Some(opts) = self.indexes.match_index { config = config.add_index(Index::new(IndexType::Match { tokenizer: opts.tokenizer, @@ -311,6 +320,30 @@ 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 ident = Identifier::new("users", "email"); + + let column = encrypt_config.get(&ident).expect("column exists"); + + assert_eq!(column.indexes[0].index_type, IndexType::Ope); + } + #[test] fn can_parse_unique_index_with_defaults() { let json = json!({ 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 2e3c62b63e097b4d4f1692e542d46edcad0b3cc9 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sun, 26 Apr 2026 15:38:32 +1000 Subject: [PATCH 3/3] 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.