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