1use std::collections::HashMap;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::column::index::{K_DEFAULT, M_DEFAULT};
7use crate::column::{ArrayIndexMode, Index, IndexType, TokenFilter, Tokenizer};
8use crate::errors::ConfigError;
9use crate::{ColumnConfig, ColumnType};
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PlaintextType {
17 BigInt,
18 Boolean,
19 Date,
20 Decimal,
21 #[serde(alias = "real", alias = "double")]
22 Float,
23 Int,
24 #[serde(rename = "json", alias = "jsonb")]
25 Json,
26 SmallInt,
27 #[default]
28 Text,
29 Timestamp,
30}
31
32impl std::fmt::Display for PlaintextType {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::BigInt => write!(f, "big_int"),
36 Self::Boolean => write!(f, "boolean"),
37 Self::Date => write!(f, "date"),
38 Self::Decimal => write!(f, "decimal"),
39 Self::Float => write!(f, "float"),
40 Self::Int => write!(f, "int"),
41 Self::Json => write!(f, "json"),
42 Self::SmallInt => write!(f, "small_int"),
43 Self::Text => write!(f, "text"),
44 Self::Timestamp => write!(f, "timestamp"),
45 }
46 }
47}
48
49impl From<PlaintextType> for ColumnType {
50 fn from(pt: PlaintextType) -> Self {
51 match pt {
52 PlaintextType::BigInt => ColumnType::BigInt,
53 PlaintextType::Boolean => ColumnType::Boolean,
54 PlaintextType::Date => ColumnType::Date,
55 PlaintextType::Decimal => ColumnType::Decimal,
56 PlaintextType::Float => ColumnType::Float,
57 PlaintextType::Int => ColumnType::Int,
58 PlaintextType::Json => ColumnType::Json,
59 PlaintextType::SmallInt => ColumnType::SmallInt,
60 PlaintextType::Text => ColumnType::Text,
61 PlaintextType::Timestamp => ColumnType::Timestamp,
62 }
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
68pub struct Identifier {
69 #[serde(rename = "t")]
70 pub table: String,
71 #[serde(rename = "c")]
72 pub column: String,
73}
74
75impl Identifier {
76 pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
77 Self {
78 table: table.into(),
79 column: column.into(),
80 }
81 }
82}
83
84impl std::fmt::Display for Identifier {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}.{}", self.table, self.column)
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CanonicalEncryptionConfig {
93 #[serde(rename = "v")]
94 pub version: u32,
95 pub tables: Tables,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Tables(pub HashMap<String, Table>);
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Table(pub HashMap<String, Column>);
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct Column {
108 #[serde(default, alias = "cast_as")]
109 pub plaintext_type: PlaintextType,
110 #[serde(default)]
111 pub indexes: Indexes,
112}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct Indexes {
116 pub ore: Option<OreIndexOpts>,
117 pub ope: Option<OpeIndexOpts>,
118 pub unique: Option<UniqueIndexOpts>,
119 #[serde(rename = "match")]
120 pub match_index: Option<MatchIndexOpts>,
121 pub ste_vec: Option<SteVecIndexOpts>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct OreIndexOpts {}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct OpeIndexOpts {}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct UniqueIndexOpts {
132 #[serde(default)]
133 pub token_filters: Vec<TokenFilter>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct MatchIndexOpts {
138 #[serde(default = "default_tokenizer")]
139 pub tokenizer: Tokenizer,
140 #[serde(default)]
141 pub token_filters: Vec<TokenFilter>,
142 #[serde(default = "default_k")]
143 pub k: usize,
144 #[serde(default = "default_m")]
145 pub m: usize,
146 #[serde(default)]
147 pub include_original: bool,
148}
149
150impl Default for MatchIndexOpts {
151 fn default() -> Self {
152 Self {
153 tokenizer: Tokenizer::Standard,
154 token_filters: vec![],
155 k: K_DEFAULT,
156 m: M_DEFAULT,
157 include_original: false,
158 }
159 }
160}
161
162fn default_tokenizer() -> Tokenizer {
163 Tokenizer::Standard
164}
165
166fn default_k() -> usize {
167 K_DEFAULT
168}
169
170fn default_m() -> usize {
171 M_DEFAULT
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SteVecIndexOpts {
176 pub prefix: String,
177 #[serde(default)]
178 pub term_filters: Vec<TokenFilter>,
179 #[serde(default = "default_array_index_mode")]
180 pub array_index_mode: ArrayIndexMode,
181}
182
183fn default_array_index_mode() -> ArrayIndexMode {
184 ArrayIndexMode::ALL
185}
186
187impl FromStr for CanonicalEncryptionConfig {
188 type Err = ConfigError;
189
190 fn from_str(s: &str) -> Result<Self, Self::Err> {
191 serde_json::from_str(s).map_err(|e| ConfigError::ParseError(e.to_string()))
192 }
193}
194
195impl CanonicalEncryptionConfig {
196 pub fn into_config_map(self) -> Result<HashMap<Identifier, ColumnConfig>, ConfigError> {
197 if self.version != 1 {
198 return Err(ConfigError::UnsupportedVersion {
199 version: self.version,
200 expected: 1,
201 });
202 }
203
204 let mut map = HashMap::new();
205
206 for (table_name, table) in self.tables.0 {
207 for (column_name, column) in table.0 {
208 let identifier = Identifier::new(&table_name, &column_name);
209 let config = column.into_column_config(&table_name, &column_name)?;
210 map.insert(identifier, config);
211 }
212 }
213
214 Ok(map)
215 }
216}
217
218impl Column {
219 fn into_column_config(
220 self,
221 table_name: &str,
222 column_name: &str,
223 ) -> Result<ColumnConfig, ConfigError> {
224 let column_type: ColumnType = self.plaintext_type.into();
225
226 if self.indexes.ste_vec.is_some() && self.plaintext_type != PlaintextType::Json {
227 return Err(ConfigError::SteVecRequiresJson {
228 table: table_name.to_owned(),
229 column: column_name.to_owned(),
230 found_plaintext_type: self.plaintext_type.to_string(),
231 });
232 }
233
234 if self.indexes.match_index.is_some() && self.plaintext_type != PlaintextType::Text {
235 return Err(ConfigError::MatchRequiresText {
236 table: table_name.to_owned(),
237 column: column_name.to_owned(),
238 found_plaintext_type: self.plaintext_type.to_string(),
239 });
240 }
241
242 let mut config = ColumnConfig::build(column_name).casts_as(column_type);
243
244 if self.indexes.ore.is_some() {
245 config = config.add_index(Index::new_ore());
246 }
247
248 if self.indexes.ope.is_some() {
249 config = config.add_index(Index::new_ope());
250 }
251
252 if let Some(unique_opts) = self.indexes.unique {
253 config = config.add_index(Index::new(IndexType::Unique {
254 token_filters: unique_opts.token_filters,
255 }));
256 }
257
258 if let Some(match_opts) = self.indexes.match_index {
259 config = config.add_index(Index::new(IndexType::Match {
260 tokenizer: match_opts.tokenizer,
261 token_filters: match_opts.token_filters,
262 k: match_opts.k,
263 m: match_opts.m,
264 include_original: match_opts.include_original,
265 }));
266 }
267
268 if let Some(ste_vec_opts) = self.indexes.ste_vec {
269 config = config.add_index(Index::new(IndexType::SteVec {
270 prefix: ste_vec_opts.prefix,
271 term_filters: ste_vec_opts.term_filters,
272 array_index_mode: ste_vec_opts.array_index_mode,
273 }));
274 }
275
276 Ok(config)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use serde_json::json;
284
285 #[test]
286 fn it_deserializes_all_plaintext_types() {
287 let cases = vec![
288 ("text", PlaintextType::Text),
289 ("int", PlaintextType::Int),
290 ("small_int", PlaintextType::SmallInt),
291 ("big_int", PlaintextType::BigInt),
292 ("float", PlaintextType::Float),
293 ("boolean", PlaintextType::Boolean),
294 ("date", PlaintextType::Date),
295 ("json", PlaintextType::Json),
296 ("decimal", PlaintextType::Decimal),
297 ("timestamp", PlaintextType::Timestamp),
298 ];
299
300 for (input, expected) in cases {
301 let result: PlaintextType = serde_json::from_value(json!(input)).unwrap();
302 assert_eq!(result, expected, "Failed for input: {input}");
303 }
304 }
305
306 #[test]
307 fn it_defaults_to_text() {
308 let pt: PlaintextType = Default::default();
309 assert_eq!(pt, PlaintextType::Text);
310 }
311
312 #[test]
313 fn it_accepts_jsonb_alias() {
314 let result: PlaintextType = serde_json::from_value(json!("jsonb")).unwrap();
315 assert_eq!(result, PlaintextType::Json);
316 }
317
318 #[test]
319 fn it_accepts_real_alias() {
320 let result: PlaintextType = serde_json::from_value(json!("real")).unwrap();
321 assert_eq!(result, PlaintextType::Float);
322 }
323
324 #[test]
325 fn it_accepts_double_alias() {
326 let result: PlaintextType = serde_json::from_value(json!("double")).unwrap();
327 assert_eq!(result, PlaintextType::Float);
328 }
329
330 #[test]
331 fn it_converts_to_column_type() {
332 assert_eq!(ColumnType::from(PlaintextType::Text), ColumnType::Text);
333 assert_eq!(ColumnType::from(PlaintextType::Int), ColumnType::Int);
334 assert_eq!(
335 ColumnType::from(PlaintextType::SmallInt),
336 ColumnType::SmallInt
337 );
338 assert_eq!(ColumnType::from(PlaintextType::BigInt), ColumnType::BigInt);
339 assert_eq!(ColumnType::from(PlaintextType::Float), ColumnType::Float);
340 assert_eq!(
341 ColumnType::from(PlaintextType::Boolean),
342 ColumnType::Boolean
343 );
344 assert_eq!(ColumnType::from(PlaintextType::Date), ColumnType::Date);
345 assert_eq!(ColumnType::from(PlaintextType::Json), ColumnType::Json);
346 assert_eq!(
347 ColumnType::from(PlaintextType::Decimal),
348 ColumnType::Decimal
349 );
350 assert_eq!(
351 ColumnType::from(PlaintextType::Timestamp),
352 ColumnType::Timestamp
353 );
354 }
355
356 #[test]
357 fn it_serializes_to_canonical_names() {
358 assert_eq!(
359 serde_json::to_value(PlaintextType::Text).unwrap(),
360 json!("text")
361 );
362 assert_eq!(
363 serde_json::to_value(PlaintextType::Json).unwrap(),
364 json!("json")
365 );
366 assert_eq!(
367 serde_json::to_value(PlaintextType::BigInt).unwrap(),
368 json!("big_int")
369 );
370 }
371
372 #[test]
373 fn it_parses_minimal_config() {
374 let input = json!({
375 "v": 1,
376 "tables": {
377 "users": {
378 "email": {}
379 }
380 }
381 });
382
383 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
384 assert_eq!(config.version, 1);
385 }
386
387 #[test]
388 fn it_accepts_cast_as_field_name() {
389 let input = json!({
390 "v": 1,
391 "tables": {
392 "users": {
393 "email": {
394 "cast_as": "int"
395 }
396 }
397 }
398 });
399
400 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
401 let table = config.tables.0.get("users").unwrap();
402 let column = table.0.get("email").unwrap();
403 assert_eq!(column.plaintext_type, PlaintextType::Int);
404 }
405
406 #[test]
407 fn it_accepts_plaintext_type_field_name() {
408 let input = json!({
409 "v": 1,
410 "tables": {
411 "users": {
412 "email": {
413 "plaintext_type": "int"
414 }
415 }
416 }
417 });
418
419 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
420 let table = config.tables.0.get("users").unwrap();
421 let column = table.0.get("email").unwrap();
422 assert_eq!(column.plaintext_type, PlaintextType::Int);
423 }
424
425 #[test]
426 fn it_parses_ore_index() {
427 let input = json!({
428 "v": 1,
429 "tables": {
430 "users": {
431 "age": {
432 "plaintext_type": "int",
433 "indexes": { "ore": {} }
434 }
435 }
436 }
437 });
438
439 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
440 let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
441 assert!(col.indexes.ore.is_some());
442 }
443
444 #[test]
445 fn it_parses_ope_index() {
446 let input = json!({
447 "v": 1,
448 "tables": {
449 "users": {
450 "age": {
451 "plaintext_type": "int",
452 "indexes": { "ope": {} }
453 }
454 }
455 }
456 });
457
458 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
459 let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
460 assert!(col.indexes.ope.is_some());
461 }
462
463 #[test]
464 fn it_round_trips_ope_index_through_json() {
465 let input = json!({
466 "v": 1,
467 "tables": {
468 "users": {
469 "age": {
470 "plaintext_type": "int",
471 "indexes": { "ope": {} }
472 }
473 }
474 }
475 });
476
477 let config: CanonicalEncryptionConfig = serde_json::from_value(input.clone()).unwrap();
478 let serialized = serde_json::to_value(&config).unwrap();
479 let reparsed: CanonicalEncryptionConfig = serde_json::from_value(serialized).unwrap();
480
481 let col = reparsed
482 .tables
483 .0
484 .get("users")
485 .unwrap()
486 .0
487 .get("age")
488 .unwrap();
489 assert!(col.indexes.ope.is_some());
490 assert!(col.indexes.ore.is_none());
491 }
492
493 #[test]
494 fn it_parses_match_index_with_defaults() {
495 let input = json!({
496 "v": 1,
497 "tables": {
498 "users": {
499 "name": {
500 "plaintext_type": "text",
501 "indexes": { "match": {} }
502 }
503 }
504 }
505 });
506
507 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
508 let col = config.tables.0.get("users").unwrap().0.get("name").unwrap();
509 let match_opts = col.indexes.match_index.as_ref().unwrap();
510 assert_eq!(match_opts.tokenizer, Tokenizer::Standard);
511 assert_eq!(match_opts.k, 6);
512 assert_eq!(match_opts.m, 2048);
513 assert!(!match_opts.include_original);
514 assert!(match_opts.token_filters.is_empty());
515 }
516
517 #[test]
518 fn it_parses_unique_index() {
519 let input = json!({
520 "v": 1,
521 "tables": {
522 "users": {
523 "email": {
524 "plaintext_type": "text",
525 "indexes": {
526 "unique": {
527 "token_filters": [{ "kind": "downcase" }]
528 }
529 }
530 }
531 }
532 }
533 });
534
535 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
536 let col = config
537 .tables
538 .0
539 .get("users")
540 .unwrap()
541 .0
542 .get("email")
543 .unwrap();
544 let unique_opts = col.indexes.unique.as_ref().unwrap();
545 assert_eq!(unique_opts.token_filters.len(), 1);
546 }
547
548 #[test]
549 fn it_parses_ste_vec_index() {
550 let input = json!({
551 "v": 1,
552 "tables": {
553 "events": {
554 "data": {
555 "plaintext_type": "json",
556 "indexes": {
557 "ste_vec": {
558 "prefix": "event-data",
559 "array_index_mode": "all"
560 }
561 }
562 }
563 }
564 }
565 });
566
567 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
568 let col = config
569 .tables
570 .0
571 .get("events")
572 .unwrap()
573 .0
574 .get("data")
575 .unwrap();
576 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
577 assert_eq!(ste_vec_opts.prefix, "event-data");
578 }
579
580 #[test]
581 fn it_parses_empty_indexes() {
582 let input = json!({
583 "v": 1,
584 "tables": {
585 "users": {
586 "email": {
587 "plaintext_type": "text",
588 "indexes": {}
589 }
590 }
591 }
592 });
593
594 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
595 let col = config
596 .tables
597 .0
598 .get("users")
599 .unwrap()
600 .0
601 .get("email")
602 .unwrap();
603 assert!(col.indexes.ore.is_none());
604 assert!(col.indexes.unique.is_none());
605 assert!(col.indexes.match_index.is_none());
606 assert!(col.indexes.ste_vec.is_none());
607 }
608
609 #[test]
610 fn it_converts_to_config_map() {
611 let input = json!({
612 "v": 1,
613 "tables": {
614 "users": {
615 "email": {
616 "plaintext_type": "text",
617 "indexes": {
618 "ore": {},
619 "unique": { "token_filters": [{ "kind": "downcase" }] }
620 }
621 }
622 }
623 }
624 });
625
626 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
627 let map = config.into_config_map().unwrap();
628
629 let id = Identifier::new("users", "email");
630 let col = map.get(&id).unwrap();
631 assert_eq!(col.cast_type, ColumnType::Text);
632 assert_eq!(col.indexes.len(), 2);
633 }
634
635 #[test]
636 fn it_defaults_empty_column_to_text() {
637 let input = json!({
638 "v": 1,
639 "tables": {
640 "users": {
641 "email": {}
642 }
643 }
644 });
645
646 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
647 let map = config.into_config_map().unwrap();
648
649 let id = Identifier::new("users", "email");
650 let col = map.get(&id).unwrap();
651 assert_eq!(col.cast_type, ColumnType::Text);
652 assert!(col.indexes.is_empty());
653 }
654
655 #[test]
656 fn it_rejects_ste_vec_on_non_json_column() {
657 let input = json!({
658 "v": 1,
659 "tables": {
660 "users": {
661 "email": {
662 "plaintext_type": "text",
663 "indexes": {
664 "ste_vec": { "prefix": "test" }
665 }
666 }
667 }
668 }
669 });
670
671 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
672 let result = config.into_config_map();
673 assert!(result.is_err());
674 let err = result.unwrap_err().to_string();
675 assert!(
676 err.contains("ste_vec"),
677 "Error should mention ste_vec: {err}"
678 );
679 assert!(err.contains("json"), "Error should mention json: {err}");
680 }
681
682 #[test]
683 fn it_allows_ste_vec_on_json_column() {
684 let input = json!({
685 "v": 1,
686 "tables": {
687 "events": {
688 "data": {
689 "plaintext_type": "json",
690 "indexes": {
691 "ste_vec": { "prefix": "event-data" }
692 }
693 }
694 }
695 }
696 });
697
698 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
699 let map = config.into_config_map().unwrap();
700
701 let id = Identifier::new("events", "data");
702 let col = map.get(&id).unwrap();
703 assert_eq!(col.cast_type, ColumnType::Json);
704 }
705
706 #[test]
707 fn it_parses_from_json_string() {
708 let json_str =
709 r#"{"v":1,"tables":{"t":{"c":{"plaintext_type":"int","indexes":{"ore":{}}}}}}"#;
710 let config: CanonicalEncryptionConfig = json_str.parse().unwrap();
711 let map = config.into_config_map().unwrap();
712 let col = map.get(&Identifier::new("t", "c")).unwrap();
713 assert_eq!(col.cast_type, ColumnType::Int);
714 }
715
716 #[test]
717 fn it_handles_backwards_compat_cast_as_jsonb() {
718 let input = json!({
719 "v": 1,
720 "tables": {
721 "events": {
722 "data": {
723 "cast_as": "jsonb",
724 "indexes": {
725 "ste_vec": { "prefix": "test" }
726 }
727 }
728 }
729 }
730 });
731
732 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
733 let map = config.into_config_map().unwrap();
734 let id = Identifier::new("events", "data");
735 let col = map.get(&id).unwrap();
736 assert_eq!(col.cast_type, ColumnType::Json);
737 }
738
739 #[test]
740 fn it_produces_correct_index_types_for_multi_index_column() {
741 let input = json!({
742 "v": 1,
743 "tables": {
744 "encrypted": {
745 "encrypted_text": {
746 "cast_as": "text",
747 "indexes": {
748 "unique": {},
749 "match": {},
750 "ore": {}
751 }
752 }
753 }
754 }
755 });
756
757 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
758 let map = config.into_config_map().unwrap();
759
760 let id = Identifier::new("encrypted", "encrypted_text");
761 let col = map.get(&id).unwrap();
762
763 assert_eq!(col.cast_type, ColumnType::Text);
764 assert_eq!(col.name, "encrypted_text");
765 assert_eq!(col.indexes.len(), 3);
766
767 let index_types: Vec<_> = col.indexes.iter().map(|i| &i.index_type).collect();
768 assert!(index_types.contains(&&IndexType::Ore));
769 assert!(index_types
770 .iter()
771 .any(|t| matches!(t, IndexType::Unique { .. })));
772 assert!(index_types
773 .iter()
774 .any(|t| matches!(t, IndexType::Match { .. })));
775 }
776
777 #[test]
778 fn it_maps_all_cast_as_values_to_correct_column_types() {
779 let cases = vec![
780 ("text", ColumnType::Text),
781 ("int", ColumnType::Int),
782 ("small_int", ColumnType::SmallInt),
783 ("big_int", ColumnType::BigInt),
784 ("boolean", ColumnType::Boolean),
785 ("date", ColumnType::Date),
786 ("float", ColumnType::Float),
787 ("decimal", ColumnType::Decimal),
788 ("timestamp", ColumnType::Timestamp),
789 ("double", ColumnType::Float),
791 ("real", ColumnType::Float),
792 ("jsonb", ColumnType::Json),
793 ("json", ColumnType::Json),
794 ];
795
796 for (cast_as, expected_type) in cases {
797 let input = json!({
798 "v": 1,
799 "tables": {
800 "t": {
801 "c": { "cast_as": cast_as }
802 }
803 }
804 });
805
806 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
807 let map = config.into_config_map().unwrap();
808 let col = map.get(&Identifier::new("t", "c")).unwrap();
809 assert_eq!(
810 col.cast_type, expected_type,
811 "Failed for cast_as: {cast_as}"
812 );
813 }
814 }
815
816 #[test]
817 fn it_preserves_match_index_defaults_in_config_map() {
818 let input = json!({
819 "v": 1,
820 "tables": {
821 "t": {
822 "c": {
823 "cast_as": "text",
824 "indexes": { "match": {} }
825 }
826 }
827 }
828 });
829
830 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
831 let map = config.into_config_map().unwrap();
832 let col = map.get(&Identifier::new("t", "c")).unwrap();
833
834 assert_eq!(col.indexes.len(), 1);
835 assert_eq!(
836 col.indexes[0].index_type,
837 IndexType::Match {
838 tokenizer: Tokenizer::Standard,
839 token_filters: vec![],
840 k: 6,
841 m: 2048,
842 include_original: false,
843 }
844 );
845 }
846
847 #[test]
851 fn it_parses_real_eql_integration_test_config() {
852 let input = json!({
853 "v": 1,
854 "tables": {
855 "encrypted": {
856 "encrypted_text": {
857 "cast_as": "text",
858 "indexes": {
859 "unique": {},
860 "match": {},
861 "ore": {}
862 }
863 },
864 "encrypted_bool": {
865 "cast_as": "boolean",
866 "indexes": {
867 "unique": {},
868 "ore": {}
869 }
870 },
871 "encrypted_int2": {
872 "cast_as": "small_int",
873 "indexes": {
874 "unique": {},
875 "ore": {}
876 }
877 },
878 "encrypted_int4": {
879 "cast_as": "int",
880 "indexes": {
881 "unique": {},
882 "ore": {}
883 }
884 },
885 "encrypted_int8": {
886 "cast_as": "big_int",
887 "indexes": {
888 "unique": {},
889 "ore": {}
890 }
891 },
892 "encrypted_float8": {
893 "cast_as": "double",
894 "indexes": {
895 "unique": {},
896 "ore": {}
897 }
898 },
899 "encrypted_date": {
900 "cast_as": "date",
901 "indexes": {
902 "unique": {},
903 "ore": {}
904 }
905 },
906 "encrypted_jsonb": {
907 "cast_as": "jsonb",
908 "indexes": {
909 "ste_vec": {
910 "prefix": "encrypted/encrypted_jsonb"
911 }
912 }
913 },
914 "encrypted_jsonb_filtered": {
915 "cast_as": "jsonb",
916 "indexes": {
917 "ste_vec": {
918 "prefix": "encrypted/encrypted_jsonb_filtered",
919 "term_filters": [{ "kind": "downcase" }]
920 }
921 }
922 }
923 }
924 }
925 });
926
927 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
928 let map = config.into_config_map().unwrap();
929
930 assert_eq!(map.len(), 9);
932
933 let text_col = map
935 .get(&Identifier::new("encrypted", "encrypted_text"))
936 .unwrap();
937 assert_eq!(text_col.cast_type, ColumnType::Text);
938 assert_eq!(text_col.indexes.len(), 3);
939
940 let bool_col = map
941 .get(&Identifier::new("encrypted", "encrypted_bool"))
942 .unwrap();
943 assert_eq!(bool_col.cast_type, ColumnType::Boolean);
944 assert_eq!(bool_col.indexes.len(), 2);
945
946 let float_col = map
947 .get(&Identifier::new("encrypted", "encrypted_float8"))
948 .unwrap();
949 assert_eq!(float_col.cast_type, ColumnType::Float); let jsonb_col = map
952 .get(&Identifier::new("encrypted", "encrypted_jsonb"))
953 .unwrap();
954 assert_eq!(jsonb_col.cast_type, ColumnType::Json); assert_eq!(jsonb_col.indexes.len(), 1);
956 assert!(matches!(
957 jsonb_col.indexes[0].index_type,
958 IndexType::SteVec { ref prefix, .. } if prefix == "encrypted/encrypted_jsonb"
959 ));
960
961 let filtered_col = map
962 .get(&Identifier::new("encrypted", "encrypted_jsonb_filtered"))
963 .unwrap();
964 assert!(matches!(
965 &filtered_col.indexes[0].index_type,
966 IndexType::SteVec { term_filters, .. } if term_filters.len() == 1
967 ));
968 }
969
970 #[test]
971 fn it_rejects_unsupported_version() {
972 let input = json!({
973 "v": 2,
974 "tables": {
975 "users": {
976 "email": {}
977 }
978 }
979 });
980
981 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
982 let result = config.into_config_map();
983 assert!(result.is_err());
984 let err = result.unwrap_err().to_string();
985 assert!(err.contains("unsupported config version"), "Error: {err}");
986 }
987
988 #[test]
989 fn it_rejects_match_index_on_non_text_column() {
990 let input = json!({
991 "v": 1,
992 "tables": {
993 "users": {
994 "age": {
995 "plaintext_type": "int",
996 "indexes": { "match": {} }
997 }
998 }
999 }
1000 });
1001
1002 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1003 let result = config.into_config_map();
1004 assert!(result.is_err());
1005 let err = result.unwrap_err().to_string();
1006 assert!(err.contains("match"), "Error should mention match: {err}");
1007 assert!(err.contains("text"), "Error should mention text: {err}");
1008 }
1009
1010 #[test]
1011 fn it_displays_identifier() {
1012 let id = Identifier::new("users", "email");
1013 assert_eq!(id.to_string(), "users.email");
1014 }
1015
1016 #[test]
1017 fn it_silently_ignores_dropped_legacy_fields() {
1018 let input = json!({
1019 "v": 1,
1020 "tables": {
1021 "users": {
1022 "email": {
1023 "cast_as": "text",
1024 "mode": "encrypted",
1025 "in_place": true,
1026 "indexes": {
1027 "unique": {
1028 "token_filters": [{ "kind": "downcase" }],
1029 "mode": "encrypted",
1030 "in_place": false
1031 }
1032 }
1033 }
1034 }
1035 }
1036 });
1037
1038 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1039 let map = config.into_config_map().unwrap();
1040 let col = map.get(&Identifier::new("users", "email")).unwrap();
1041 assert_eq!(col.cast_type, ColumnType::Text);
1042 assert_eq!(col.indexes.len(), 1);
1043 }
1044}