use std::collections::HashMap;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::column::index::{K_DEFAULT, M_DEFAULT};
use crate::column::{ArrayIndexMode, Index, IndexType, TokenFilter, Tokenizer};
use crate::errors::ConfigError;
use crate::{ColumnConfig, ColumnType};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlaintextType {
BigInt,
Boolean,
Date,
Decimal,
#[serde(alias = "real", alias = "double")]
Float,
Int,
#[serde(rename = "json", alias = "jsonb")]
Json,
SmallInt,
#[default]
Text,
Timestamp,
}
impl std::fmt::Display for PlaintextType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BigInt => write!(f, "big_int"),
Self::Boolean => write!(f, "boolean"),
Self::Date => write!(f, "date"),
Self::Decimal => write!(f, "decimal"),
Self::Float => write!(f, "float"),
Self::Int => write!(f, "int"),
Self::Json => write!(f, "json"),
Self::SmallInt => write!(f, "small_int"),
Self::Text => write!(f, "text"),
Self::Timestamp => write!(f, "timestamp"),
}
}
}
impl From<PlaintextType> for ColumnType {
fn from(pt: PlaintextType) -> Self {
match pt {
PlaintextType::BigInt => ColumnType::BigInt,
PlaintextType::Boolean => ColumnType::Boolean,
PlaintextType::Date => ColumnType::Date,
PlaintextType::Decimal => ColumnType::Decimal,
PlaintextType::Float => ColumnType::Float,
PlaintextType::Int => ColumnType::Int,
PlaintextType::Json => ColumnType::Json,
PlaintextType::SmallInt => ColumnType::SmallInt,
PlaintextType::Text => ColumnType::Text,
PlaintextType::Timestamp => ColumnType::Timestamp,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Identifier {
#[serde(rename = "t")]
pub table: String,
#[serde(rename = "c")]
pub column: String,
}
impl Identifier {
pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
Self {
table: table.into(),
column: column.into(),
}
}
}
impl std::fmt::Display for Identifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.table, self.column)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanonicalEncryptionConfig {
#[serde(rename = "v")]
pub version: u32,
pub tables: Tables,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tables(pub HashMap<String, Table>);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Table(pub HashMap<String, Column>);
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Column {
#[serde(default, alias = "cast_as")]
pub plaintext_type: PlaintextType,
#[serde(default)]
pub indexes: Indexes,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Indexes {
pub ore: Option<OreIndexOpts>,
pub ope: Option<OpeIndexOpts>,
pub unique: Option<UniqueIndexOpts>,
#[serde(rename = "match")]
pub match_index: Option<MatchIndexOpts>,
pub ste_vec: Option<SteVecIndexOpts>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OreIndexOpts {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpeIndexOpts {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniqueIndexOpts {
#[serde(default)]
pub token_filters: Vec<TokenFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchIndexOpts {
#[serde(default = "default_tokenizer")]
pub tokenizer: Tokenizer,
#[serde(default)]
pub token_filters: Vec<TokenFilter>,
#[serde(default = "default_k")]
pub k: usize,
#[serde(default = "default_m")]
pub m: usize,
#[serde(default)]
pub include_original: bool,
}
impl Default for MatchIndexOpts {
fn default() -> Self {
Self {
tokenizer: Tokenizer::Standard,
token_filters: vec![],
k: K_DEFAULT,
m: M_DEFAULT,
include_original: false,
}
}
}
fn default_tokenizer() -> Tokenizer {
Tokenizer::Standard
}
fn default_k() -> usize {
K_DEFAULT
}
fn default_m() -> usize {
M_DEFAULT
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SteVecIndexOpts {
pub prefix: String,
#[serde(default)]
pub term_filters: Vec<TokenFilter>,
#[serde(default = "default_array_index_mode")]
pub array_index_mode: ArrayIndexMode,
}
fn default_array_index_mode() -> ArrayIndexMode {
ArrayIndexMode::ALL
}
impl FromStr for CanonicalEncryptionConfig {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).map_err(|e| ConfigError::ParseError(e.to_string()))
}
}
impl CanonicalEncryptionConfig {
pub fn into_config_map(self) -> Result<HashMap<Identifier, ColumnConfig>, ConfigError> {
if self.version != 1 {
return Err(ConfigError::UnsupportedVersion {
version: self.version,
expected: 1,
});
}
let mut map = HashMap::new();
for (table_name, table) in self.tables.0 {
for (column_name, column) in table.0 {
let identifier = Identifier::new(&table_name, &column_name);
let config = column.into_column_config(&table_name, &column_name)?;
map.insert(identifier, config);
}
}
Ok(map)
}
}
impl Column {
fn into_column_config(
self,
table_name: &str,
column_name: &str,
) -> Result<ColumnConfig, ConfigError> {
let column_type: ColumnType = self.plaintext_type.into();
if self.indexes.ste_vec.is_some() && self.plaintext_type != PlaintextType::Json {
return Err(ConfigError::SteVecRequiresJson {
table: table_name.to_owned(),
column: column_name.to_owned(),
found_plaintext_type: self.plaintext_type.to_string(),
});
}
if self.indexes.match_index.is_some() && self.plaintext_type != PlaintextType::Text {
return Err(ConfigError::MatchRequiresText {
table: table_name.to_owned(),
column: column_name.to_owned(),
found_plaintext_type: self.plaintext_type.to_string(),
});
}
let mut config = ColumnConfig::build(column_name).casts_as(column_type);
if self.indexes.ore.is_some() {
config = config.add_index(Index::new_ore());
}
if self.indexes.ope.is_some() {
config = config.add_index(Index::new_ope());
}
if let Some(unique_opts) = self.indexes.unique {
config = config.add_index(Index::new(IndexType::Unique {
token_filters: unique_opts.token_filters,
}));
}
if let Some(match_opts) = self.indexes.match_index {
config = config.add_index(Index::new(IndexType::Match {
tokenizer: match_opts.tokenizer,
token_filters: match_opts.token_filters,
k: match_opts.k,
m: match_opts.m,
include_original: match_opts.include_original,
}));
}
if let Some(ste_vec_opts) = self.indexes.ste_vec {
config = config.add_index(Index::new(IndexType::SteVec {
prefix: ste_vec_opts.prefix,
term_filters: ste_vec_opts.term_filters,
array_index_mode: ste_vec_opts.array_index_mode,
}));
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn it_deserializes_all_plaintext_types() {
let cases = vec![
("text", PlaintextType::Text),
("int", PlaintextType::Int),
("small_int", PlaintextType::SmallInt),
("big_int", PlaintextType::BigInt),
("float", PlaintextType::Float),
("boolean", PlaintextType::Boolean),
("date", PlaintextType::Date),
("json", PlaintextType::Json),
("decimal", PlaintextType::Decimal),
("timestamp", PlaintextType::Timestamp),
];
for (input, expected) in cases {
let result: PlaintextType = serde_json::from_value(json!(input)).unwrap();
assert_eq!(result, expected, "Failed for input: {input}");
}
}
#[test]
fn it_defaults_to_text() {
let pt: PlaintextType = Default::default();
assert_eq!(pt, PlaintextType::Text);
}
#[test]
fn it_accepts_jsonb_alias() {
let result: PlaintextType = serde_json::from_value(json!("jsonb")).unwrap();
assert_eq!(result, PlaintextType::Json);
}
#[test]
fn it_accepts_real_alias() {
let result: PlaintextType = serde_json::from_value(json!("real")).unwrap();
assert_eq!(result, PlaintextType::Float);
}
#[test]
fn it_accepts_double_alias() {
let result: PlaintextType = serde_json::from_value(json!("double")).unwrap();
assert_eq!(result, PlaintextType::Float);
}
#[test]
fn it_converts_to_column_type() {
assert_eq!(ColumnType::from(PlaintextType::Text), ColumnType::Text);
assert_eq!(ColumnType::from(PlaintextType::Int), ColumnType::Int);
assert_eq!(
ColumnType::from(PlaintextType::SmallInt),
ColumnType::SmallInt
);
assert_eq!(ColumnType::from(PlaintextType::BigInt), ColumnType::BigInt);
assert_eq!(ColumnType::from(PlaintextType::Float), ColumnType::Float);
assert_eq!(
ColumnType::from(PlaintextType::Boolean),
ColumnType::Boolean
);
assert_eq!(ColumnType::from(PlaintextType::Date), ColumnType::Date);
assert_eq!(ColumnType::from(PlaintextType::Json), ColumnType::Json);
assert_eq!(
ColumnType::from(PlaintextType::Decimal),
ColumnType::Decimal
);
assert_eq!(
ColumnType::from(PlaintextType::Timestamp),
ColumnType::Timestamp
);
}
#[test]
fn it_serializes_to_canonical_names() {
assert_eq!(
serde_json::to_value(PlaintextType::Text).unwrap(),
json!("text")
);
assert_eq!(
serde_json::to_value(PlaintextType::Json).unwrap(),
json!("json")
);
assert_eq!(
serde_json::to_value(PlaintextType::BigInt).unwrap(),
json!("big_int")
);
}
#[test]
fn it_parses_minimal_config() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
assert_eq!(config.version, 1);
}
#[test]
fn it_accepts_cast_as_field_name() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"cast_as": "int"
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let table = config.tables.0.get("users").unwrap();
let column = table.0.get("email").unwrap();
assert_eq!(column.plaintext_type, PlaintextType::Int);
}
#[test]
fn it_accepts_plaintext_type_field_name() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"plaintext_type": "int"
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let table = config.tables.0.get("users").unwrap();
let column = table.0.get("email").unwrap();
assert_eq!(column.plaintext_type, PlaintextType::Int);
}
#[test]
fn it_parses_ore_index() {
let input = json!({
"v": 1,
"tables": {
"users": {
"age": {
"plaintext_type": "int",
"indexes": { "ore": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
assert!(col.indexes.ore.is_some());
}
#[test]
fn it_parses_ope_index() {
let input = json!({
"v": 1,
"tables": {
"users": {
"age": {
"plaintext_type": "int",
"indexes": { "ope": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
assert!(col.indexes.ope.is_some());
}
#[test]
fn it_round_trips_ope_index_through_json() {
let input = json!({
"v": 1,
"tables": {
"users": {
"age": {
"plaintext_type": "int",
"indexes": { "ope": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input.clone()).unwrap();
let serialized = serde_json::to_value(&config).unwrap();
let reparsed: CanonicalEncryptionConfig = serde_json::from_value(serialized).unwrap();
let col = reparsed
.tables
.0
.get("users")
.unwrap()
.0
.get("age")
.unwrap();
assert!(col.indexes.ope.is_some());
assert!(col.indexes.ore.is_none());
}
#[test]
fn it_parses_match_index_with_defaults() {
let input = json!({
"v": 1,
"tables": {
"users": {
"name": {
"plaintext_type": "text",
"indexes": { "match": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config.tables.0.get("users").unwrap().0.get("name").unwrap();
let match_opts = col.indexes.match_index.as_ref().unwrap();
assert_eq!(match_opts.tokenizer, Tokenizer::Standard);
assert_eq!(match_opts.k, 6);
assert_eq!(match_opts.m, 2048);
assert!(!match_opts.include_original);
assert!(match_opts.token_filters.is_empty());
}
#[test]
fn it_parses_unique_index() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"plaintext_type": "text",
"indexes": {
"unique": {
"token_filters": [{ "kind": "downcase" }]
}
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config
.tables
.0
.get("users")
.unwrap()
.0
.get("email")
.unwrap();
let unique_opts = col.indexes.unique.as_ref().unwrap();
assert_eq!(unique_opts.token_filters.len(), 1);
}
#[test]
fn it_parses_ste_vec_index() {
let input = json!({
"v": 1,
"tables": {
"events": {
"data": {
"plaintext_type": "json",
"indexes": {
"ste_vec": {
"prefix": "event-data",
"array_index_mode": "all"
}
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config
.tables
.0
.get("events")
.unwrap()
.0
.get("data")
.unwrap();
let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
assert_eq!(ste_vec_opts.prefix, "event-data");
}
#[test]
fn it_parses_empty_indexes() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"plaintext_type": "text",
"indexes": {}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let col = config
.tables
.0
.get("users")
.unwrap()
.0
.get("email")
.unwrap();
assert!(col.indexes.ore.is_none());
assert!(col.indexes.unique.is_none());
assert!(col.indexes.match_index.is_none());
assert!(col.indexes.ste_vec.is_none());
}
#[test]
fn it_converts_to_config_map() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"plaintext_type": "text",
"indexes": {
"ore": {},
"unique": { "token_filters": [{ "kind": "downcase" }] }
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let id = Identifier::new("users", "email");
let col = map.get(&id).unwrap();
assert_eq!(col.cast_type, ColumnType::Text);
assert_eq!(col.indexes.len(), 2);
}
#[test]
fn it_defaults_empty_column_to_text() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let id = Identifier::new("users", "email");
let col = map.get(&id).unwrap();
assert_eq!(col.cast_type, ColumnType::Text);
assert!(col.indexes.is_empty());
}
#[test]
fn it_rejects_ste_vec_on_non_json_column() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"plaintext_type": "text",
"indexes": {
"ste_vec": { "prefix": "test" }
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let result = config.into_config_map();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("ste_vec"),
"Error should mention ste_vec: {err}"
);
assert!(err.contains("json"), "Error should mention json: {err}");
}
#[test]
fn it_allows_ste_vec_on_json_column() {
let input = json!({
"v": 1,
"tables": {
"events": {
"data": {
"plaintext_type": "json",
"indexes": {
"ste_vec": { "prefix": "event-data" }
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let id = Identifier::new("events", "data");
let col = map.get(&id).unwrap();
assert_eq!(col.cast_type, ColumnType::Json);
}
#[test]
fn it_parses_from_json_string() {
let json_str =
r#"{"v":1,"tables":{"t":{"c":{"plaintext_type":"int","indexes":{"ore":{}}}}}}"#;
let config: CanonicalEncryptionConfig = json_str.parse().unwrap();
let map = config.into_config_map().unwrap();
let col = map.get(&Identifier::new("t", "c")).unwrap();
assert_eq!(col.cast_type, ColumnType::Int);
}
#[test]
fn it_handles_backwards_compat_cast_as_jsonb() {
let input = json!({
"v": 1,
"tables": {
"events": {
"data": {
"cast_as": "jsonb",
"indexes": {
"ste_vec": { "prefix": "test" }
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let id = Identifier::new("events", "data");
let col = map.get(&id).unwrap();
assert_eq!(col.cast_type, ColumnType::Json);
}
#[test]
fn it_produces_correct_index_types_for_multi_index_column() {
let input = json!({
"v": 1,
"tables": {
"encrypted": {
"encrypted_text": {
"cast_as": "text",
"indexes": {
"unique": {},
"match": {},
"ore": {}
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let id = Identifier::new("encrypted", "encrypted_text");
let col = map.get(&id).unwrap();
assert_eq!(col.cast_type, ColumnType::Text);
assert_eq!(col.name, "encrypted_text");
assert_eq!(col.indexes.len(), 3);
let index_types: Vec<_> = col.indexes.iter().map(|i| &i.index_type).collect();
assert!(index_types.contains(&&IndexType::Ore));
assert!(index_types
.iter()
.any(|t| matches!(t, IndexType::Unique { .. })));
assert!(index_types
.iter()
.any(|t| matches!(t, IndexType::Match { .. })));
}
#[test]
fn it_maps_all_cast_as_values_to_correct_column_types() {
let cases = vec![
("text", ColumnType::Text),
("int", ColumnType::Int),
("small_int", ColumnType::SmallInt),
("big_int", ColumnType::BigInt),
("boolean", ColumnType::Boolean),
("date", ColumnType::Date),
("float", ColumnType::Float),
("decimal", ColumnType::Decimal),
("timestamp", ColumnType::Timestamp),
("double", ColumnType::Float),
("real", ColumnType::Float),
("jsonb", ColumnType::Json),
("json", ColumnType::Json),
];
for (cast_as, expected_type) in cases {
let input = json!({
"v": 1,
"tables": {
"t": {
"c": { "cast_as": cast_as }
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let col = map.get(&Identifier::new("t", "c")).unwrap();
assert_eq!(
col.cast_type, expected_type,
"Failed for cast_as: {cast_as}"
);
}
}
#[test]
fn it_preserves_match_index_defaults_in_config_map() {
let input = json!({
"v": 1,
"tables": {
"t": {
"c": {
"cast_as": "text",
"indexes": { "match": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let col = map.get(&Identifier::new("t", "c")).unwrap();
assert_eq!(col.indexes.len(), 1);
assert_eq!(
col.indexes[0].index_type,
IndexType::Match {
tokenizer: Tokenizer::Standard,
token_filters: vec![],
k: 6,
m: 2048,
include_original: false,
}
);
}
#[test]
fn it_parses_real_eql_integration_test_config() {
let input = json!({
"v": 1,
"tables": {
"encrypted": {
"encrypted_text": {
"cast_as": "text",
"indexes": {
"unique": {},
"match": {},
"ore": {}
}
},
"encrypted_bool": {
"cast_as": "boolean",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_int2": {
"cast_as": "small_int",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_int4": {
"cast_as": "int",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_int8": {
"cast_as": "big_int",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_float8": {
"cast_as": "double",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_date": {
"cast_as": "date",
"indexes": {
"unique": {},
"ore": {}
}
},
"encrypted_jsonb": {
"cast_as": "jsonb",
"indexes": {
"ste_vec": {
"prefix": "encrypted/encrypted_jsonb"
}
}
},
"encrypted_jsonb_filtered": {
"cast_as": "jsonb",
"indexes": {
"ste_vec": {
"prefix": "encrypted/encrypted_jsonb_filtered",
"term_filters": [{ "kind": "downcase" }]
}
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
assert_eq!(map.len(), 9);
let text_col = map
.get(&Identifier::new("encrypted", "encrypted_text"))
.unwrap();
assert_eq!(text_col.cast_type, ColumnType::Text);
assert_eq!(text_col.indexes.len(), 3);
let bool_col = map
.get(&Identifier::new("encrypted", "encrypted_bool"))
.unwrap();
assert_eq!(bool_col.cast_type, ColumnType::Boolean);
assert_eq!(bool_col.indexes.len(), 2);
let float_col = map
.get(&Identifier::new("encrypted", "encrypted_float8"))
.unwrap();
assert_eq!(float_col.cast_type, ColumnType::Float);
let jsonb_col = map
.get(&Identifier::new("encrypted", "encrypted_jsonb"))
.unwrap();
assert_eq!(jsonb_col.cast_type, ColumnType::Json); assert_eq!(jsonb_col.indexes.len(), 1);
assert!(matches!(
jsonb_col.indexes[0].index_type,
IndexType::SteVec { ref prefix, .. } if prefix == "encrypted/encrypted_jsonb"
));
let filtered_col = map
.get(&Identifier::new("encrypted", "encrypted_jsonb_filtered"))
.unwrap();
assert!(matches!(
&filtered_col.indexes[0].index_type,
IndexType::SteVec { term_filters, .. } if term_filters.len() == 1
));
}
#[test]
fn it_rejects_unsupported_version() {
let input = json!({
"v": 2,
"tables": {
"users": {
"email": {}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let result = config.into_config_map();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("unsupported config version"), "Error: {err}");
}
#[test]
fn it_rejects_match_index_on_non_text_column() {
let input = json!({
"v": 1,
"tables": {
"users": {
"age": {
"plaintext_type": "int",
"indexes": { "match": {} }
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let result = config.into_config_map();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("match"), "Error should mention match: {err}");
assert!(err.contains("text"), "Error should mention text: {err}");
}
#[test]
fn it_displays_identifier() {
let id = Identifier::new("users", "email");
assert_eq!(id.to_string(), "users.email");
}
#[test]
fn it_silently_ignores_dropped_legacy_fields() {
let input = json!({
"v": 1,
"tables": {
"users": {
"email": {
"cast_as": "text",
"mode": "encrypted",
"in_place": true,
"indexes": {
"unique": {
"token_filters": [{ "kind": "downcase" }],
"mode": "encrypted",
"in_place": false
}
}
}
}
}
});
let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
let map = config.into_config_map().unwrap();
let col = map.get(&Identifier::new("users", "email")).unwrap();
assert_eq!(col.cast_type, ColumnType::Text);
assert_eq!(col.indexes.len(), 1);
}
}