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, SteVecMode, 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 #[serde(default)]
182 pub mode: SteVecMode,
183}
184
185fn default_array_index_mode() -> ArrayIndexMode {
186 ArrayIndexMode::ALL
187}
188
189impl FromStr for CanonicalEncryptionConfig {
190 type Err = ConfigError;
191
192 fn from_str(s: &str) -> Result<Self, Self::Err> {
193 serde_json::from_str(s).map_err(|e| ConfigError::ParseError(e.to_string()))
194 }
195}
196
197impl CanonicalEncryptionConfig {
198 pub fn into_config_map(self) -> Result<HashMap<Identifier, ColumnConfig>, ConfigError> {
199 if self.version != 1 {
200 return Err(ConfigError::UnsupportedVersion {
201 version: self.version,
202 expected: 1,
203 });
204 }
205
206 let mut map = HashMap::new();
207
208 for (table_name, table) in self.tables.0 {
209 for (column_name, column) in table.0 {
210 let identifier = Identifier::new(&table_name, &column_name);
211 let config = column.into_column_config(&table_name, &column_name)?;
212 map.insert(identifier, config);
213 }
214 }
215
216 Ok(map)
217 }
218}
219
220impl Column {
221 fn into_column_config(
222 self,
223 table_name: &str,
224 column_name: &str,
225 ) -> Result<ColumnConfig, ConfigError> {
226 let column_type: ColumnType = self.plaintext_type.into();
227
228 if self.indexes.ste_vec.is_some() && self.plaintext_type != PlaintextType::Json {
229 return Err(ConfigError::SteVecRequiresJson {
230 table: table_name.to_owned(),
231 column: column_name.to_owned(),
232 found_plaintext_type: self.plaintext_type.to_string(),
233 });
234 }
235
236 if self.indexes.match_index.is_some() && self.plaintext_type != PlaintextType::Text {
237 return Err(ConfigError::MatchRequiresText {
238 table: table_name.to_owned(),
239 column: column_name.to_owned(),
240 found_plaintext_type: self.plaintext_type.to_string(),
241 });
242 }
243
244 let mut config = ColumnConfig::build(column_name).casts_as(column_type);
245
246 if self.indexes.ore.is_some() {
247 config = config.add_index(Index::new_ore());
248 }
249
250 if self.indexes.ope.is_some() {
251 config = config.add_index(Index::new_ope());
252 }
253
254 if let Some(unique_opts) = self.indexes.unique {
255 config = config.add_index(Index::new(IndexType::Unique {
256 token_filters: unique_opts.token_filters,
257 }));
258 }
259
260 if let Some(match_opts) = self.indexes.match_index {
261 config = config.add_index(Index::new(IndexType::Match {
262 tokenizer: match_opts.tokenizer,
263 token_filters: match_opts.token_filters,
264 k: match_opts.k,
265 m: match_opts.m,
266 include_original: match_opts.include_original,
267 }));
268 }
269
270 if let Some(ste_vec_opts) = self.indexes.ste_vec {
271 config = config.add_index(Index::new(IndexType::SteVec {
272 prefix: ste_vec_opts.prefix,
273 term_filters: ste_vec_opts.term_filters,
274 array_index_mode: ste_vec_opts.array_index_mode,
275 mode: ste_vec_opts.mode,
276 }));
277 }
278
279 Ok(config)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use serde_json::json;
287
288 #[test]
289 fn it_deserializes_all_plaintext_types() {
290 let cases = vec![
291 ("text", PlaintextType::Text),
292 ("int", PlaintextType::Int),
293 ("small_int", PlaintextType::SmallInt),
294 ("big_int", PlaintextType::BigInt),
295 ("float", PlaintextType::Float),
296 ("boolean", PlaintextType::Boolean),
297 ("date", PlaintextType::Date),
298 ("json", PlaintextType::Json),
299 ("decimal", PlaintextType::Decimal),
300 ("timestamp", PlaintextType::Timestamp),
301 ];
302
303 for (input, expected) in cases {
304 let result: PlaintextType = serde_json::from_value(json!(input)).unwrap();
305 assert_eq!(result, expected, "Failed for input: {input}");
306 }
307 }
308
309 #[test]
310 fn it_defaults_to_text() {
311 let pt: PlaintextType = Default::default();
312 assert_eq!(pt, PlaintextType::Text);
313 }
314
315 #[test]
316 fn it_accepts_jsonb_alias() {
317 let result: PlaintextType = serde_json::from_value(json!("jsonb")).unwrap();
318 assert_eq!(result, PlaintextType::Json);
319 }
320
321 #[test]
322 fn it_accepts_real_alias() {
323 let result: PlaintextType = serde_json::from_value(json!("real")).unwrap();
324 assert_eq!(result, PlaintextType::Float);
325 }
326
327 #[test]
328 fn it_accepts_double_alias() {
329 let result: PlaintextType = serde_json::from_value(json!("double")).unwrap();
330 assert_eq!(result, PlaintextType::Float);
331 }
332
333 #[test]
334 fn it_converts_to_column_type() {
335 assert_eq!(ColumnType::from(PlaintextType::Text), ColumnType::Text);
336 assert_eq!(ColumnType::from(PlaintextType::Int), ColumnType::Int);
337 assert_eq!(
338 ColumnType::from(PlaintextType::SmallInt),
339 ColumnType::SmallInt
340 );
341 assert_eq!(ColumnType::from(PlaintextType::BigInt), ColumnType::BigInt);
342 assert_eq!(ColumnType::from(PlaintextType::Float), ColumnType::Float);
343 assert_eq!(
344 ColumnType::from(PlaintextType::Boolean),
345 ColumnType::Boolean
346 );
347 assert_eq!(ColumnType::from(PlaintextType::Date), ColumnType::Date);
348 assert_eq!(ColumnType::from(PlaintextType::Json), ColumnType::Json);
349 assert_eq!(
350 ColumnType::from(PlaintextType::Decimal),
351 ColumnType::Decimal
352 );
353 assert_eq!(
354 ColumnType::from(PlaintextType::Timestamp),
355 ColumnType::Timestamp
356 );
357 }
358
359 #[test]
360 fn it_serializes_to_canonical_names() {
361 assert_eq!(
362 serde_json::to_value(PlaintextType::Text).unwrap(),
363 json!("text")
364 );
365 assert_eq!(
366 serde_json::to_value(PlaintextType::Json).unwrap(),
367 json!("json")
368 );
369 assert_eq!(
370 serde_json::to_value(PlaintextType::BigInt).unwrap(),
371 json!("big_int")
372 );
373 }
374
375 #[test]
376 fn it_parses_minimal_config() {
377 let input = json!({
378 "v": 1,
379 "tables": {
380 "users": {
381 "email": {}
382 }
383 }
384 });
385
386 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
387 assert_eq!(config.version, 1);
388 }
389
390 #[test]
391 fn it_accepts_cast_as_field_name() {
392 let input = json!({
393 "v": 1,
394 "tables": {
395 "users": {
396 "email": {
397 "cast_as": "int"
398 }
399 }
400 }
401 });
402
403 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
404 let table = config.tables.0.get("users").unwrap();
405 let column = table.0.get("email").unwrap();
406 assert_eq!(column.plaintext_type, PlaintextType::Int);
407 }
408
409 #[test]
410 fn it_accepts_plaintext_type_field_name() {
411 let input = json!({
412 "v": 1,
413 "tables": {
414 "users": {
415 "email": {
416 "plaintext_type": "int"
417 }
418 }
419 }
420 });
421
422 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
423 let table = config.tables.0.get("users").unwrap();
424 let column = table.0.get("email").unwrap();
425 assert_eq!(column.plaintext_type, PlaintextType::Int);
426 }
427
428 #[test]
429 fn it_parses_ore_index() {
430 let input = json!({
431 "v": 1,
432 "tables": {
433 "users": {
434 "age": {
435 "plaintext_type": "int",
436 "indexes": { "ore": {} }
437 }
438 }
439 }
440 });
441
442 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
443 let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
444 assert!(col.indexes.ore.is_some());
445 }
446
447 #[test]
448 fn it_parses_ope_index() {
449 let input = json!({
450 "v": 1,
451 "tables": {
452 "users": {
453 "age": {
454 "plaintext_type": "int",
455 "indexes": { "ope": {} }
456 }
457 }
458 }
459 });
460
461 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
462 let col = config.tables.0.get("users").unwrap().0.get("age").unwrap();
463 assert!(col.indexes.ope.is_some());
464 }
465
466 #[test]
467 fn it_round_trips_ope_index_through_json() {
468 let input = json!({
469 "v": 1,
470 "tables": {
471 "users": {
472 "age": {
473 "plaintext_type": "int",
474 "indexes": { "ope": {} }
475 }
476 }
477 }
478 });
479
480 let config: CanonicalEncryptionConfig = serde_json::from_value(input.clone()).unwrap();
481 let serialized = serde_json::to_value(&config).unwrap();
482 let reparsed: CanonicalEncryptionConfig = serde_json::from_value(serialized).unwrap();
483
484 let col = reparsed
485 .tables
486 .0
487 .get("users")
488 .unwrap()
489 .0
490 .get("age")
491 .unwrap();
492 assert!(col.indexes.ope.is_some());
493 assert!(col.indexes.ore.is_none());
494 }
495
496 #[test]
497 fn it_parses_match_index_with_defaults() {
498 let input = json!({
499 "v": 1,
500 "tables": {
501 "users": {
502 "name": {
503 "plaintext_type": "text",
504 "indexes": { "match": {} }
505 }
506 }
507 }
508 });
509
510 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
511 let col = config.tables.0.get("users").unwrap().0.get("name").unwrap();
512 let match_opts = col.indexes.match_index.as_ref().unwrap();
513 assert_eq!(match_opts.tokenizer, Tokenizer::Standard);
514 assert_eq!(match_opts.k, 6);
515 assert_eq!(match_opts.m, 2048);
516 assert!(!match_opts.include_original);
517 assert!(match_opts.token_filters.is_empty());
518 }
519
520 #[test]
521 fn it_parses_unique_index() {
522 let input = json!({
523 "v": 1,
524 "tables": {
525 "users": {
526 "email": {
527 "plaintext_type": "text",
528 "indexes": {
529 "unique": {
530 "token_filters": [{ "kind": "downcase" }]
531 }
532 }
533 }
534 }
535 }
536 });
537
538 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
539 let col = config
540 .tables
541 .0
542 .get("users")
543 .unwrap()
544 .0
545 .get("email")
546 .unwrap();
547 let unique_opts = col.indexes.unique.as_ref().unwrap();
548 assert_eq!(unique_opts.token_filters.len(), 1);
549 }
550
551 #[test]
552 fn it_parses_ste_vec_index() {
553 let input = json!({
554 "v": 1,
555 "tables": {
556 "events": {
557 "data": {
558 "plaintext_type": "json",
559 "indexes": {
560 "ste_vec": {
561 "prefix": "event-data",
562 "array_index_mode": "all"
563 }
564 }
565 }
566 }
567 }
568 });
569
570 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
571 let col = config
572 .tables
573 .0
574 .get("events")
575 .unwrap()
576 .0
577 .get("data")
578 .unwrap();
579 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
580 assert_eq!(ste_vec_opts.prefix, "event-data");
581 }
582
583 #[test]
584 fn it_defaults_ste_vec_mode_to_standard() {
585 let input = json!({
586 "v": 1,
587 "tables": {
588 "events": {
589 "data": {
590 "plaintext_type": "json",
591 "indexes": {
592 "ste_vec": { "prefix": "event-data" }
593 }
594 }
595 }
596 }
597 });
598
599 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
600 let col = config
601 .tables
602 .0
603 .get("events")
604 .unwrap()
605 .0
606 .get("data")
607 .unwrap();
608 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
609 assert_eq!(ste_vec_opts.mode, SteVecMode::Standard);
610 }
611
612 #[test]
613 fn it_parses_ste_vec_mode_standard() {
614 let input = json!({
615 "v": 1,
616 "tables": {
617 "events": {
618 "data": {
619 "plaintext_type": "json",
620 "indexes": {
621 "ste_vec": { "prefix": "event-data", "mode": "standard" }
622 }
623 }
624 }
625 }
626 });
627
628 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
629 let col = config
630 .tables
631 .0
632 .get("events")
633 .unwrap()
634 .0
635 .get("data")
636 .unwrap();
637 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
638 assert_eq!(ste_vec_opts.mode, SteVecMode::Standard);
639 }
640
641 #[test]
642 fn it_propagates_ste_vec_mode_into_config_map() {
643 let input = json!({
644 "v": 1,
645 "tables": {
646 "events": {
647 "data": {
648 "plaintext_type": "json",
649 "indexes": {
650 "ste_vec": { "prefix": "event-data", "mode": "standard" }
651 }
652 }
653 }
654 }
655 });
656
657 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
658 let map = config.into_config_map().unwrap();
659 let col = map.get(&Identifier::new("events", "data")).unwrap();
660
661 assert!(matches!(
662 col.indexes[0].index_type,
663 IndexType::SteVec {
664 mode: SteVecMode::Standard,
665 ..
666 }
667 ));
668 }
669
670 #[test]
671 fn it_parses_empty_indexes() {
672 let input = json!({
673 "v": 1,
674 "tables": {
675 "users": {
676 "email": {
677 "plaintext_type": "text",
678 "indexes": {}
679 }
680 }
681 }
682 });
683
684 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
685 let col = config
686 .tables
687 .0
688 .get("users")
689 .unwrap()
690 .0
691 .get("email")
692 .unwrap();
693 assert!(col.indexes.ore.is_none());
694 assert!(col.indexes.unique.is_none());
695 assert!(col.indexes.match_index.is_none());
696 assert!(col.indexes.ste_vec.is_none());
697 }
698
699 #[test]
700 fn it_converts_to_config_map() {
701 let input = json!({
702 "v": 1,
703 "tables": {
704 "users": {
705 "email": {
706 "plaintext_type": "text",
707 "indexes": {
708 "ore": {},
709 "unique": { "token_filters": [{ "kind": "downcase" }] }
710 }
711 }
712 }
713 }
714 });
715
716 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
717 let map = config.into_config_map().unwrap();
718
719 let id = Identifier::new("users", "email");
720 let col = map.get(&id).unwrap();
721 assert_eq!(col.cast_type, ColumnType::Text);
722 assert_eq!(col.indexes.len(), 2);
723 }
724
725 #[test]
726 fn it_defaults_empty_column_to_text() {
727 let input = json!({
728 "v": 1,
729 "tables": {
730 "users": {
731 "email": {}
732 }
733 }
734 });
735
736 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
737 let map = config.into_config_map().unwrap();
738
739 let id = Identifier::new("users", "email");
740 let col = map.get(&id).unwrap();
741 assert_eq!(col.cast_type, ColumnType::Text);
742 assert!(col.indexes.is_empty());
743 }
744
745 #[test]
746 fn it_rejects_ste_vec_on_non_json_column() {
747 let input = json!({
748 "v": 1,
749 "tables": {
750 "users": {
751 "email": {
752 "plaintext_type": "text",
753 "indexes": {
754 "ste_vec": { "prefix": "test" }
755 }
756 }
757 }
758 }
759 });
760
761 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
762 let result = config.into_config_map();
763 assert!(result.is_err());
764 let err = result.unwrap_err().to_string();
765 assert!(
766 err.contains("ste_vec"),
767 "Error should mention ste_vec: {err}"
768 );
769 assert!(err.contains("json"), "Error should mention json: {err}");
770 }
771
772 #[test]
773 fn it_allows_ste_vec_on_json_column() {
774 let input = json!({
775 "v": 1,
776 "tables": {
777 "events": {
778 "data": {
779 "plaintext_type": "json",
780 "indexes": {
781 "ste_vec": { "prefix": "event-data" }
782 }
783 }
784 }
785 }
786 });
787
788 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
789 let map = config.into_config_map().unwrap();
790
791 let id = Identifier::new("events", "data");
792 let col = map.get(&id).unwrap();
793 assert_eq!(col.cast_type, ColumnType::Json);
794 }
795
796 #[test]
797 fn it_parses_from_json_string() {
798 let json_str =
799 r#"{"v":1,"tables":{"t":{"c":{"plaintext_type":"int","indexes":{"ore":{}}}}}}"#;
800 let config: CanonicalEncryptionConfig = json_str.parse().unwrap();
801 let map = config.into_config_map().unwrap();
802 let col = map.get(&Identifier::new("t", "c")).unwrap();
803 assert_eq!(col.cast_type, ColumnType::Int);
804 }
805
806 #[test]
807 fn it_handles_backwards_compat_cast_as_jsonb() {
808 let input = json!({
809 "v": 1,
810 "tables": {
811 "events": {
812 "data": {
813 "cast_as": "jsonb",
814 "indexes": {
815 "ste_vec": { "prefix": "test" }
816 }
817 }
818 }
819 }
820 });
821
822 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
823 let map = config.into_config_map().unwrap();
824 let id = Identifier::new("events", "data");
825 let col = map.get(&id).unwrap();
826 assert_eq!(col.cast_type, ColumnType::Json);
827 }
828
829 #[test]
830 fn it_produces_correct_index_types_for_multi_index_column() {
831 let input = json!({
832 "v": 1,
833 "tables": {
834 "encrypted": {
835 "encrypted_text": {
836 "cast_as": "text",
837 "indexes": {
838 "unique": {},
839 "match": {},
840 "ore": {}
841 }
842 }
843 }
844 }
845 });
846
847 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
848 let map = config.into_config_map().unwrap();
849
850 let id = Identifier::new("encrypted", "encrypted_text");
851 let col = map.get(&id).unwrap();
852
853 assert_eq!(col.cast_type, ColumnType::Text);
854 assert_eq!(col.name, "encrypted_text");
855 assert_eq!(col.indexes.len(), 3);
856
857 let index_types: Vec<_> = col.indexes.iter().map(|i| &i.index_type).collect();
858 assert!(index_types.contains(&&IndexType::Ore));
859 assert!(index_types
860 .iter()
861 .any(|t| matches!(t, IndexType::Unique { .. })));
862 assert!(index_types
863 .iter()
864 .any(|t| matches!(t, IndexType::Match { .. })));
865 }
866
867 #[test]
868 fn it_maps_all_cast_as_values_to_correct_column_types() {
869 let cases = vec![
870 ("text", ColumnType::Text),
871 ("int", ColumnType::Int),
872 ("small_int", ColumnType::SmallInt),
873 ("big_int", ColumnType::BigInt),
874 ("boolean", ColumnType::Boolean),
875 ("date", ColumnType::Date),
876 ("float", ColumnType::Float),
877 ("decimal", ColumnType::Decimal),
878 ("timestamp", ColumnType::Timestamp),
879 ("double", ColumnType::Float),
881 ("real", ColumnType::Float),
882 ("jsonb", ColumnType::Json),
883 ("json", ColumnType::Json),
884 ];
885
886 for (cast_as, expected_type) in cases {
887 let input = json!({
888 "v": 1,
889 "tables": {
890 "t": {
891 "c": { "cast_as": cast_as }
892 }
893 }
894 });
895
896 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
897 let map = config.into_config_map().unwrap();
898 let col = map.get(&Identifier::new("t", "c")).unwrap();
899 assert_eq!(
900 col.cast_type, expected_type,
901 "Failed for cast_as: {cast_as}"
902 );
903 }
904 }
905
906 #[test]
907 fn it_preserves_match_index_defaults_in_config_map() {
908 let input = json!({
909 "v": 1,
910 "tables": {
911 "t": {
912 "c": {
913 "cast_as": "text",
914 "indexes": { "match": {} }
915 }
916 }
917 }
918 });
919
920 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
921 let map = config.into_config_map().unwrap();
922 let col = map.get(&Identifier::new("t", "c")).unwrap();
923
924 assert_eq!(col.indexes.len(), 1);
925 assert_eq!(
926 col.indexes[0].index_type,
927 IndexType::Match {
928 tokenizer: Tokenizer::Standard,
929 token_filters: vec![],
930 k: 6,
931 m: 2048,
932 include_original: false,
933 }
934 );
935 }
936
937 #[test]
941 fn it_parses_real_eql_integration_test_config() {
942 let input = json!({
943 "v": 1,
944 "tables": {
945 "encrypted": {
946 "encrypted_text": {
947 "cast_as": "text",
948 "indexes": {
949 "unique": {},
950 "match": {},
951 "ore": {}
952 }
953 },
954 "encrypted_bool": {
955 "cast_as": "boolean",
956 "indexes": {
957 "unique": {},
958 "ore": {}
959 }
960 },
961 "encrypted_int2": {
962 "cast_as": "small_int",
963 "indexes": {
964 "unique": {},
965 "ore": {}
966 }
967 },
968 "encrypted_int4": {
969 "cast_as": "int",
970 "indexes": {
971 "unique": {},
972 "ore": {}
973 }
974 },
975 "encrypted_int8": {
976 "cast_as": "big_int",
977 "indexes": {
978 "unique": {},
979 "ore": {}
980 }
981 },
982 "encrypted_float8": {
983 "cast_as": "double",
984 "indexes": {
985 "unique": {},
986 "ore": {}
987 }
988 },
989 "encrypted_date": {
990 "cast_as": "date",
991 "indexes": {
992 "unique": {},
993 "ore": {}
994 }
995 },
996 "encrypted_jsonb": {
997 "cast_as": "jsonb",
998 "indexes": {
999 "ste_vec": {
1000 "prefix": "encrypted/encrypted_jsonb"
1001 }
1002 }
1003 },
1004 "encrypted_jsonb_filtered": {
1005 "cast_as": "jsonb",
1006 "indexes": {
1007 "ste_vec": {
1008 "prefix": "encrypted/encrypted_jsonb_filtered",
1009 "term_filters": [{ "kind": "downcase" }]
1010 }
1011 }
1012 }
1013 }
1014 }
1015 });
1016
1017 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1018 let map = config.into_config_map().unwrap();
1019
1020 assert_eq!(map.len(), 9);
1022
1023 let text_col = map
1025 .get(&Identifier::new("encrypted", "encrypted_text"))
1026 .unwrap();
1027 assert_eq!(text_col.cast_type, ColumnType::Text);
1028 assert_eq!(text_col.indexes.len(), 3);
1029
1030 let bool_col = map
1031 .get(&Identifier::new("encrypted", "encrypted_bool"))
1032 .unwrap();
1033 assert_eq!(bool_col.cast_type, ColumnType::Boolean);
1034 assert_eq!(bool_col.indexes.len(), 2);
1035
1036 let float_col = map
1037 .get(&Identifier::new("encrypted", "encrypted_float8"))
1038 .unwrap();
1039 assert_eq!(float_col.cast_type, ColumnType::Float); let jsonb_col = map
1042 .get(&Identifier::new("encrypted", "encrypted_jsonb"))
1043 .unwrap();
1044 assert_eq!(jsonb_col.cast_type, ColumnType::Json); assert_eq!(jsonb_col.indexes.len(), 1);
1046 assert!(matches!(
1047 jsonb_col.indexes[0].index_type,
1048 IndexType::SteVec { ref prefix, .. } if prefix == "encrypted/encrypted_jsonb"
1049 ));
1050
1051 let filtered_col = map
1052 .get(&Identifier::new("encrypted", "encrypted_jsonb_filtered"))
1053 .unwrap();
1054 assert!(matches!(
1055 &filtered_col.indexes[0].index_type,
1056 IndexType::SteVec { term_filters, .. } if term_filters.len() == 1
1057 ));
1058 }
1059
1060 #[test]
1061 fn it_rejects_unsupported_version() {
1062 let input = json!({
1063 "v": 2,
1064 "tables": {
1065 "users": {
1066 "email": {}
1067 }
1068 }
1069 });
1070
1071 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1072 let result = config.into_config_map();
1073 assert!(result.is_err());
1074 let err = result.unwrap_err().to_string();
1075 assert!(err.contains("unsupported config version"), "Error: {err}");
1076 }
1077
1078 #[test]
1079 fn it_rejects_match_index_on_non_text_column() {
1080 let input = json!({
1081 "v": 1,
1082 "tables": {
1083 "users": {
1084 "age": {
1085 "plaintext_type": "int",
1086 "indexes": { "match": {} }
1087 }
1088 }
1089 }
1090 });
1091
1092 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1093 let result = config.into_config_map();
1094 assert!(result.is_err());
1095 let err = result.unwrap_err().to_string();
1096 assert!(err.contains("match"), "Error should mention match: {err}");
1097 assert!(err.contains("text"), "Error should mention text: {err}");
1098 }
1099
1100 #[test]
1101 fn it_displays_identifier() {
1102 let id = Identifier::new("users", "email");
1103 assert_eq!(id.to_string(), "users.email");
1104 }
1105
1106 #[test]
1107 fn it_silently_ignores_dropped_legacy_fields() {
1108 let input = json!({
1109 "v": 1,
1110 "tables": {
1111 "users": {
1112 "email": {
1113 "cast_as": "text",
1114 "mode": "encrypted",
1115 "in_place": true,
1116 "indexes": {
1117 "unique": {
1118 "token_filters": [{ "kind": "downcase" }],
1119 "mode": "encrypted",
1120 "in_place": false
1121 }
1122 }
1123 }
1124 }
1125 }
1126 });
1127
1128 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1129 let map = config.into_config_map().unwrap();
1130 let col = map.get(&Identifier::new("users", "email")).unwrap();
1131 assert_eq!(col.cast_type, ColumnType::Text);
1132 assert_eq!(col.indexes.len(), 1);
1133 }
1134}