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_compat() {
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::Compat);
610 }
611
612 #[test]
613 fn it_serializes_defaulted_ste_vec_mode_as_compat() {
614 let input = json!({
617 "v": 1,
618 "tables": {
619 "events": {
620 "data": {
621 "plaintext_type": "json",
622 "indexes": {
623 "ste_vec": { "prefix": "event-data" }
624 }
625 }
626 }
627 }
628 });
629
630 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
631 let col = config
632 .tables
633 .0
634 .get("events")
635 .unwrap()
636 .0
637 .get("data")
638 .unwrap();
639 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
640 let serialized = serde_json::to_value(ste_vec_opts).unwrap();
641 assert_eq!(serialized["mode"], json!("compat"));
642 }
643
644 #[test]
645 fn it_parses_ste_vec_mode_standard() {
646 let input = json!({
647 "v": 1,
648 "tables": {
649 "events": {
650 "data": {
651 "plaintext_type": "json",
652 "indexes": {
653 "ste_vec": { "prefix": "event-data", "mode": "standard" }
654 }
655 }
656 }
657 }
658 });
659
660 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
661 let col = config
662 .tables
663 .0
664 .get("events")
665 .unwrap()
666 .0
667 .get("data")
668 .unwrap();
669 let ste_vec_opts = col.indexes.ste_vec.as_ref().unwrap();
670 assert_eq!(ste_vec_opts.mode, SteVecMode::Standard);
671 }
672
673 #[test]
674 fn it_propagates_ste_vec_mode_into_config_map() {
675 let input = json!({
676 "v": 1,
677 "tables": {
678 "events": {
679 "data": {
680 "plaintext_type": "json",
681 "indexes": {
682 "ste_vec": { "prefix": "event-data", "mode": "standard" }
683 }
684 }
685 }
686 }
687 });
688
689 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
690 let map = config.into_config_map().unwrap();
691 let col = map.get(&Identifier::new("events", "data")).unwrap();
692
693 assert!(matches!(
694 col.indexes[0].index_type,
695 IndexType::SteVec {
696 mode: SteVecMode::Standard,
697 ..
698 }
699 ));
700 }
701
702 #[test]
703 fn it_parses_empty_indexes() {
704 let input = json!({
705 "v": 1,
706 "tables": {
707 "users": {
708 "email": {
709 "plaintext_type": "text",
710 "indexes": {}
711 }
712 }
713 }
714 });
715
716 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
717 let col = config
718 .tables
719 .0
720 .get("users")
721 .unwrap()
722 .0
723 .get("email")
724 .unwrap();
725 assert!(col.indexes.ore.is_none());
726 assert!(col.indexes.unique.is_none());
727 assert!(col.indexes.match_index.is_none());
728 assert!(col.indexes.ste_vec.is_none());
729 }
730
731 #[test]
732 fn it_converts_to_config_map() {
733 let input = json!({
734 "v": 1,
735 "tables": {
736 "users": {
737 "email": {
738 "plaintext_type": "text",
739 "indexes": {
740 "ore": {},
741 "unique": { "token_filters": [{ "kind": "downcase" }] }
742 }
743 }
744 }
745 }
746 });
747
748 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
749 let map = config.into_config_map().unwrap();
750
751 let id = Identifier::new("users", "email");
752 let col = map.get(&id).unwrap();
753 assert_eq!(col.cast_type, ColumnType::Text);
754 assert_eq!(col.indexes.len(), 2);
755 }
756
757 #[test]
758 fn it_defaults_empty_column_to_text() {
759 let input = json!({
760 "v": 1,
761 "tables": {
762 "users": {
763 "email": {}
764 }
765 }
766 });
767
768 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
769 let map = config.into_config_map().unwrap();
770
771 let id = Identifier::new("users", "email");
772 let col = map.get(&id).unwrap();
773 assert_eq!(col.cast_type, ColumnType::Text);
774 assert!(col.indexes.is_empty());
775 }
776
777 #[test]
778 fn it_rejects_ste_vec_on_non_json_column() {
779 let input = json!({
780 "v": 1,
781 "tables": {
782 "users": {
783 "email": {
784 "plaintext_type": "text",
785 "indexes": {
786 "ste_vec": { "prefix": "test" }
787 }
788 }
789 }
790 }
791 });
792
793 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
794 let result = config.into_config_map();
795 assert!(result.is_err());
796 let err = result.unwrap_err().to_string();
797 assert!(
798 err.contains("ste_vec"),
799 "Error should mention ste_vec: {err}"
800 );
801 assert!(err.contains("json"), "Error should mention json: {err}");
802 }
803
804 #[test]
805 fn it_allows_ste_vec_on_json_column() {
806 let input = json!({
807 "v": 1,
808 "tables": {
809 "events": {
810 "data": {
811 "plaintext_type": "json",
812 "indexes": {
813 "ste_vec": { "prefix": "event-data" }
814 }
815 }
816 }
817 }
818 });
819
820 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
821 let map = config.into_config_map().unwrap();
822
823 let id = Identifier::new("events", "data");
824 let col = map.get(&id).unwrap();
825 assert_eq!(col.cast_type, ColumnType::Json);
826 }
827
828 #[test]
829 fn it_parses_from_json_string() {
830 let json_str =
831 r#"{"v":1,"tables":{"t":{"c":{"plaintext_type":"int","indexes":{"ore":{}}}}}}"#;
832 let config: CanonicalEncryptionConfig = json_str.parse().unwrap();
833 let map = config.into_config_map().unwrap();
834 let col = map.get(&Identifier::new("t", "c")).unwrap();
835 assert_eq!(col.cast_type, ColumnType::Int);
836 }
837
838 #[test]
839 fn it_handles_backwards_compat_cast_as_jsonb() {
840 let input = json!({
841 "v": 1,
842 "tables": {
843 "events": {
844 "data": {
845 "cast_as": "jsonb",
846 "indexes": {
847 "ste_vec": { "prefix": "test" }
848 }
849 }
850 }
851 }
852 });
853
854 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
855 let map = config.into_config_map().unwrap();
856 let id = Identifier::new("events", "data");
857 let col = map.get(&id).unwrap();
858 assert_eq!(col.cast_type, ColumnType::Json);
859 }
860
861 #[test]
862 fn it_produces_correct_index_types_for_multi_index_column() {
863 let input = json!({
864 "v": 1,
865 "tables": {
866 "encrypted": {
867 "encrypted_text": {
868 "cast_as": "text",
869 "indexes": {
870 "unique": {},
871 "match": {},
872 "ore": {}
873 }
874 }
875 }
876 }
877 });
878
879 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
880 let map = config.into_config_map().unwrap();
881
882 let id = Identifier::new("encrypted", "encrypted_text");
883 let col = map.get(&id).unwrap();
884
885 assert_eq!(col.cast_type, ColumnType::Text);
886 assert_eq!(col.name, "encrypted_text");
887 assert_eq!(col.indexes.len(), 3);
888
889 let index_types: Vec<_> = col.indexes.iter().map(|i| &i.index_type).collect();
890 assert!(index_types.contains(&&IndexType::Ore));
891 assert!(index_types
892 .iter()
893 .any(|t| matches!(t, IndexType::Unique { .. })));
894 assert!(index_types
895 .iter()
896 .any(|t| matches!(t, IndexType::Match { .. })));
897 }
898
899 #[test]
900 fn it_maps_all_cast_as_values_to_correct_column_types() {
901 let cases = vec![
902 ("text", ColumnType::Text),
903 ("int", ColumnType::Int),
904 ("small_int", ColumnType::SmallInt),
905 ("big_int", ColumnType::BigInt),
906 ("boolean", ColumnType::Boolean),
907 ("date", ColumnType::Date),
908 ("float", ColumnType::Float),
909 ("decimal", ColumnType::Decimal),
910 ("timestamp", ColumnType::Timestamp),
911 ("double", ColumnType::Float),
913 ("real", ColumnType::Float),
914 ("jsonb", ColumnType::Json),
915 ("json", ColumnType::Json),
916 ];
917
918 for (cast_as, expected_type) in cases {
919 let input = json!({
920 "v": 1,
921 "tables": {
922 "t": {
923 "c": { "cast_as": cast_as }
924 }
925 }
926 });
927
928 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
929 let map = config.into_config_map().unwrap();
930 let col = map.get(&Identifier::new("t", "c")).unwrap();
931 assert_eq!(
932 col.cast_type, expected_type,
933 "Failed for cast_as: {cast_as}"
934 );
935 }
936 }
937
938 #[test]
939 fn it_preserves_match_index_defaults_in_config_map() {
940 let input = json!({
941 "v": 1,
942 "tables": {
943 "t": {
944 "c": {
945 "cast_as": "text",
946 "indexes": { "match": {} }
947 }
948 }
949 }
950 });
951
952 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
953 let map = config.into_config_map().unwrap();
954 let col = map.get(&Identifier::new("t", "c")).unwrap();
955
956 assert_eq!(col.indexes.len(), 1);
957 assert_eq!(
958 col.indexes[0].index_type,
959 IndexType::Match {
960 tokenizer: Tokenizer::Standard,
961 token_filters: vec![],
962 k: 6,
963 m: 2048,
964 include_original: false,
965 }
966 );
967 }
968
969 #[test]
973 fn it_parses_real_eql_integration_test_config() {
974 let input = json!({
975 "v": 1,
976 "tables": {
977 "encrypted": {
978 "encrypted_text": {
979 "cast_as": "text",
980 "indexes": {
981 "unique": {},
982 "match": {},
983 "ore": {}
984 }
985 },
986 "encrypted_bool": {
987 "cast_as": "boolean",
988 "indexes": {
989 "unique": {},
990 "ore": {}
991 }
992 },
993 "encrypted_int2": {
994 "cast_as": "small_int",
995 "indexes": {
996 "unique": {},
997 "ore": {}
998 }
999 },
1000 "encrypted_int4": {
1001 "cast_as": "int",
1002 "indexes": {
1003 "unique": {},
1004 "ore": {}
1005 }
1006 },
1007 "encrypted_int8": {
1008 "cast_as": "big_int",
1009 "indexes": {
1010 "unique": {},
1011 "ore": {}
1012 }
1013 },
1014 "encrypted_float8": {
1015 "cast_as": "double",
1016 "indexes": {
1017 "unique": {},
1018 "ore": {}
1019 }
1020 },
1021 "encrypted_date": {
1022 "cast_as": "date",
1023 "indexes": {
1024 "unique": {},
1025 "ore": {}
1026 }
1027 },
1028 "encrypted_jsonb": {
1029 "cast_as": "jsonb",
1030 "indexes": {
1031 "ste_vec": {
1032 "prefix": "encrypted/encrypted_jsonb"
1033 }
1034 }
1035 },
1036 "encrypted_jsonb_filtered": {
1037 "cast_as": "jsonb",
1038 "indexes": {
1039 "ste_vec": {
1040 "prefix": "encrypted/encrypted_jsonb_filtered",
1041 "term_filters": [{ "kind": "downcase" }]
1042 }
1043 }
1044 }
1045 }
1046 }
1047 });
1048
1049 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1050 let map = config.into_config_map().unwrap();
1051
1052 assert_eq!(map.len(), 9);
1054
1055 let text_col = map
1057 .get(&Identifier::new("encrypted", "encrypted_text"))
1058 .unwrap();
1059 assert_eq!(text_col.cast_type, ColumnType::Text);
1060 assert_eq!(text_col.indexes.len(), 3);
1061
1062 let bool_col = map
1063 .get(&Identifier::new("encrypted", "encrypted_bool"))
1064 .unwrap();
1065 assert_eq!(bool_col.cast_type, ColumnType::Boolean);
1066 assert_eq!(bool_col.indexes.len(), 2);
1067
1068 let float_col = map
1069 .get(&Identifier::new("encrypted", "encrypted_float8"))
1070 .unwrap();
1071 assert_eq!(float_col.cast_type, ColumnType::Float); let jsonb_col = map
1074 .get(&Identifier::new("encrypted", "encrypted_jsonb"))
1075 .unwrap();
1076 assert_eq!(jsonb_col.cast_type, ColumnType::Json); assert_eq!(jsonb_col.indexes.len(), 1);
1078 assert!(matches!(
1079 jsonb_col.indexes[0].index_type,
1080 IndexType::SteVec { ref prefix, .. } if prefix == "encrypted/encrypted_jsonb"
1081 ));
1082
1083 let filtered_col = map
1084 .get(&Identifier::new("encrypted", "encrypted_jsonb_filtered"))
1085 .unwrap();
1086 assert!(matches!(
1087 &filtered_col.indexes[0].index_type,
1088 IndexType::SteVec { term_filters, .. } if term_filters.len() == 1
1089 ));
1090 }
1091
1092 #[test]
1093 fn it_rejects_unsupported_version() {
1094 let input = json!({
1095 "v": 2,
1096 "tables": {
1097 "users": {
1098 "email": {}
1099 }
1100 }
1101 });
1102
1103 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1104 let result = config.into_config_map();
1105 assert!(result.is_err());
1106 let err = result.unwrap_err().to_string();
1107 assert!(err.contains("unsupported config version"), "Error: {err}");
1108 }
1109
1110 #[test]
1111 fn it_rejects_match_index_on_non_text_column() {
1112 let input = json!({
1113 "v": 1,
1114 "tables": {
1115 "users": {
1116 "age": {
1117 "plaintext_type": "int",
1118 "indexes": { "match": {} }
1119 }
1120 }
1121 }
1122 });
1123
1124 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1125 let result = config.into_config_map();
1126 assert!(result.is_err());
1127 let err = result.unwrap_err().to_string();
1128 assert!(err.contains("match"), "Error should mention match: {err}");
1129 assert!(err.contains("text"), "Error should mention text: {err}");
1130 }
1131
1132 #[test]
1133 fn it_displays_identifier() {
1134 let id = Identifier::new("users", "email");
1135 assert_eq!(id.to_string(), "users.email");
1136 }
1137
1138 #[test]
1139 fn it_silently_ignores_dropped_legacy_fields() {
1140 let input = json!({
1141 "v": 1,
1142 "tables": {
1143 "users": {
1144 "email": {
1145 "cast_as": "text",
1146 "mode": "encrypted",
1147 "in_place": true,
1148 "indexes": {
1149 "unique": {
1150 "token_filters": [{ "kind": "downcase" }],
1151 "mode": "encrypted",
1152 "in_place": false
1153 }
1154 }
1155 }
1156 }
1157 }
1158 });
1159
1160 let config: CanonicalEncryptionConfig = serde_json::from_value(input).unwrap();
1161 let map = config.into_config_map().unwrap();
1162 let col = map.get(&Identifier::new("users", "email")).unwrap();
1163 assert_eq!(col.cast_type, ColumnType::Text);
1164 assert_eq!(col.indexes.len(), 1);
1165 }
1166}