Skip to main content

cipherstash_config/column/
config.rs

1use std::collections::HashSet;
2
3use super::{index::Index, IndexType, TokenFilter};
4use crate::list::ListEntry;
5use crate::operator::Operator;
6use serde::{Deserialize, Serialize};
7
8// All types should be handled here I guess
9#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)]
10#[serde(rename_all = "kebab-case")]
11pub enum ColumnType {
12    BigInt,
13    BigUInt,
14    Boolean,
15    Date,
16    Decimal,
17    Float,
18    Int,
19    SmallInt,
20    Timestamp,
21    #[serde(alias = "utf8-str")]
22    Text,
23    #[serde(rename = "json", alias = "jsonb")]
24    Json,
25    // TODO: What else do we need to add here?
26}
27
28impl std::fmt::Display for ColumnType {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        let text = match self {
31            ColumnType::BigInt => "BigInt",
32            ColumnType::BigUInt => "BigUInt",
33            ColumnType::Boolean => "Boolean",
34            ColumnType::Date => "Date",
35            ColumnType::Decimal => "Decimal",
36            ColumnType::Float => "Float",
37            ColumnType::Int => "Int",
38            ColumnType::SmallInt => "SmallInt",
39            ColumnType::Timestamp => "Timestamp",
40            ColumnType::Text => "Text",
41            ColumnType::Json => "Json",
42        };
43
44        write!(f, "{text}")
45    }
46}
47
48#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
49#[serde(rename_all = "kebab-case")]
50pub enum ColumnMode {
51    /// Store both the plaintext and encrypted data - all operations will continue to be performed
52    /// against the plaintext data. This mode should be used while migrating existing data.
53    PlaintextDuplicate = 1,
54    /// Store both the plaintext and encrypted data, but all operations will be mapped to encrypted
55    /// data. In this mode the plaintext is just a backup.
56    EncryptedDuplicate = 2,
57    /// Only store the encrypted data. This mode should be used once migration is complete so
58    /// columns get the maximum protection.
59    Encrypted = 3,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ColumnConfig {
64    pub name: String,
65    pub in_place: bool,
66    pub cast_type: ColumnType,
67    pub indexes: Vec<Index>,
68    pub mode: ColumnMode,
69}
70
71impl ListEntry for ColumnConfig {}
72
73// Configs must be unique by name
74impl PartialEq for ColumnConfig {
75    fn eq(&self, other: &Self) -> bool {
76        self.name == other.name
77    }
78}
79
80// Compare a string to a Config based on its column name
81impl PartialEq<String> for ColumnConfig {
82    fn eq(&self, other: &String) -> bool {
83        self.name == *other
84    }
85}
86
87impl ColumnConfig {
88    /// Builds a field with the following defaults:
89    ///
90    /// Type: Text,
91    /// Mode: EncryptedDuplicate
92    /// In Place: false
93    pub fn build(name: impl Into<String>) -> Self {
94        Self {
95            name: name.into(),
96            in_place: false,
97            cast_type: ColumnType::Text,
98            indexes: Default::default(),
99            mode: ColumnMode::EncryptedDuplicate,
100        }
101    }
102
103    /// Consumes self and sets the field_type to the given
104    /// value
105    pub fn casts_as(mut self, field_type: ColumnType) -> Self {
106        self.cast_type = field_type;
107        self
108    }
109
110    /// Consumes self and adds the given index to the list
111    /// of indexes
112    pub fn add_index(mut self, index: Index) -> Self {
113        // TODO: Not all indexes are allowed on all types
114        // check first
115        self.indexes.push(index);
116        self
117    }
118
119    pub fn mode(mut self, mode: ColumnMode) -> Self {
120        self.mode = mode;
121        self
122    }
123
124    pub fn supports_operator(&self, op: &Operator) -> bool {
125        self.index_for_operator(op).is_some()
126    }
127
128    pub fn supported_operations(&self) -> Vec<Operator> {
129        let hash: HashSet<Operator> = self
130            .indexes
131            .iter()
132            .flat_map(|i| i.index_type.supported_operations(&self.cast_type))
133            .collect();
134
135        hash.into_iter().collect()
136    }
137
138    pub fn index_for_operator(&self, op: &Operator) -> Option<&Index> {
139        self.indexes
140            .iter()
141            .find(|i| i.supports(op, &self.cast_type))
142    }
143
144    pub fn index_for_sort(&self) -> Option<&Index> {
145        self.indexes.iter().find(|i| i.is_orderable())
146    }
147
148    /// Sorts indexes by type. Indexes are sorted in place.
149    pub fn sort_indexes_by_type(&mut self) {
150        self.indexes
151            .sort_by(|a, b| a.index_type.as_str().cmp(b.index_type.as_str()));
152    }
153
154    pub fn has_unique_index_with_downcase(&self) -> bool {
155        self.indexes.iter().any(|index| {
156            if let IndexType::Unique { token_filters } = &index.index_type {
157                token_filters
158                    .iter()
159                    .any(|filter| matches!(filter, TokenFilter::Downcase))
160            } else {
161                false
162            }
163        })
164    }
165
166    pub fn into_match_index(self) -> Option<Index> {
167        self.indexes.into_iter().find(|i| i.is_match())
168    }
169
170    pub fn into_ore_index(self) -> Option<Index> {
171        self.indexes.into_iter().find(|i| i.is_ore())
172    }
173
174    pub fn into_unique_index(self) -> Option<Index> {
175        self.indexes.into_iter().find(|i| i.is_unique())
176    }
177
178    pub fn into_ste_vec_index(self) -> Option<Index> {
179        self.indexes.into_iter().find(|i| i.is_ste_vec())
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn text_serializes_as_text_in_kebab_case() {
189        let json = serde_json::to_string(&ColumnType::Text).unwrap();
190        assert_eq!(json, "\"text\"");
191    }
192
193    #[test]
194    fn text_deserializes_from_text() {
195        let result: ColumnType = serde_json::from_str("\"text\"").unwrap();
196        assert_eq!(result, ColumnType::Text);
197    }
198
199    #[test]
200    fn text_deserializes_from_legacy_utf8_str() {
201        let result: ColumnType = serde_json::from_str("\"utf8-str\"").unwrap();
202        assert_eq!(result, ColumnType::Text);
203    }
204
205    #[test]
206    fn text_display_shows_text() {
207        assert_eq!(format!("{}", ColumnType::Text), "Text");
208    }
209
210    #[test]
211    fn build_defaults_to_text_type() {
212        let config = ColumnConfig::build("test_column");
213        assert_eq!(config.cast_type, ColumnType::Text);
214    }
215}