Skip to main content

fluidattacks_database_core_domain/table/
schema.rs

1use alloc::vec::Vec;
2
3use super::error::SchemaError;
4use crate::cell::{valid_decimal, CellType};
5use crate::core::ColumnId;
6
7/// One column of a table: its name, its cell type and whether it accepts nulls.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Column {
10    name: ColumnId,
11    cell_type: CellType,
12    nullable: bool,
13}
14
15impl Column {
16    #[must_use]
17    pub const fn new(name: ColumnId, cell_type: CellType, nullable: bool) -> Self {
18        Self {
19            name,
20            cell_type,
21            nullable,
22        }
23    }
24
25    #[must_use]
26    pub const fn name(&self) -> &ColumnId {
27        &self.name
28    }
29
30    #[must_use]
31    pub const fn cell_type(&self) -> &CellType {
32        &self.cell_type
33    }
34
35    #[must_use]
36    pub const fn nullable(&self) -> bool {
37        self.nullable
38    }
39}
40
41/// The columns of a table, in the order of the row values, and its key. An
42/// empty key is a table with no key.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Schema {
45    columns: Vec<Column>,
46    key: Vec<ColumnId>,
47}
48
49/// The first `(precision, scale)` of a decimal in `cell_type` that no target
50/// can hold.
51fn invalid_decimal(cell_type: &CellType) -> Option<(u8, u8)> {
52    match cell_type {
53        CellType::Decimal { precision, scale } => {
54            (!valid_decimal(*precision, *scale)).then_some((*precision, *scale))
55        }
56        CellType::Array { element, .. } => invalid_decimal(element),
57        _ => None,
58    }
59}
60
61/// The first name in `items` that an earlier item repeats.
62fn first_repeated<T>(items: &[T], name: impl Fn(&T) -> &ColumnId) -> Option<&ColumnId> {
63    items
64        .iter()
65        .enumerate()
66        .find(|(i, item)| {
67            items
68                .iter()
69                .take(*i)
70                .any(|earlier| name(earlier) == name(item))
71        })
72        .map(|(_, item)| name(item))
73}
74
75impl Schema {
76    pub fn new(columns: Vec<Column>, key: Vec<ColumnId>) -> Result<Self, SchemaError> {
77        if columns.is_empty() {
78            return Err(SchemaError::NoColumns);
79        }
80        if let Some(name) = first_repeated(&columns, Column::name) {
81            return Err(SchemaError::DuplicateColumn(name.clone()));
82        }
83        if let Some((column, (precision, scale))) = columns
84            .iter()
85            .find_map(|c| invalid_decimal(c.cell_type()).map(|d| (c.name(), d)))
86        {
87            return Err(SchemaError::InvalidDecimal {
88                column: column.clone(),
89                precision,
90                scale,
91            });
92        }
93        if let Some(name) = first_repeated(&key, |name| name) {
94            return Err(SchemaError::DuplicateKeyColumn(name.clone()));
95        }
96        for name in &key {
97            match columns.iter().find(|c| c.name() == name) {
98                None => return Err(SchemaError::UnknownKeyColumn(name.clone())),
99                Some(c) if c.nullable() => {
100                    return Err(SchemaError::NullableKeyColumn(name.clone()))
101                }
102                Some(_) => {}
103            }
104        }
105        Ok(Self { columns, key })
106    }
107
108    #[must_use]
109    pub fn columns(&self) -> &[Column] {
110        &self.columns
111    }
112
113    #[must_use]
114    pub fn key(&self) -> &[ColumnId] {
115        &self.key
116    }
117
118    #[must_use]
119    pub fn column(&self, name: &ColumnId) -> Option<&Column> {
120        self.columns.iter().find(|c| c.name() == name)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use alloc::boxed::Box;
127    use alloc::vec;
128    use alloc::vec::Vec;
129
130    use super::{Column, Schema};
131    use crate::cell::CellType;
132    use crate::core::{ColumnId, Identifier};
133    use crate::table::SchemaError;
134
135    fn id(name: &str) -> ColumnId {
136        ColumnId::new(Identifier::new(name))
137    }
138
139    fn column(name: &str, cell_type: CellType, nullable: bool) -> Column {
140        Column::new(id(name), cell_type, nullable)
141    }
142
143    fn decimal(precision: u8, scale: u8) -> CellType {
144        CellType::Decimal { precision, scale }
145    }
146
147    #[test]
148    fn a_valid_schema_keeps_its_columns_and_key() {
149        let columns = vec![
150            column("id", CellType::Integer, false),
151            column("amount", decimal(38, 37), true),
152        ];
153        let schema = Schema::new(columns.clone(), vec![id("id")]);
154        assert_eq!(schema.as_ref().map(Schema::columns), Ok(columns.as_slice()));
155        assert_eq!(schema.as_ref().map(Schema::key), Ok([id("id")].as_slice()));
156    }
157
158    #[test]
159    fn a_table_may_have_no_key() {
160        let schema = Schema::new(vec![column("id", CellType::Integer, true)], Vec::new());
161        assert_eq!(schema.map(|s| s.key().is_empty()), Ok(true));
162    }
163
164    #[test]
165    fn a_table_needs_columns() {
166        assert_eq!(
167            Schema::new(Vec::new(), Vec::new()),
168            Err(SchemaError::NoColumns)
169        );
170    }
171
172    #[test]
173    fn repeated_columns_are_rejected_by_exact_name() {
174        let repeated = vec![
175            column("date", CellType::Date, false),
176            column("date", CellType::Text, false),
177        ];
178        assert_eq!(
179            Schema::new(repeated, Vec::new()),
180            Err(SchemaError::DuplicateColumn(id("date")))
181        );
182        let cased = vec![
183            column("subject", CellType::Text, false),
184            column("SUBJECT", CellType::Text, false),
185        ];
186        assert!(Schema::new(cased, Vec::new()).is_ok());
187    }
188
189    #[test]
190    fn invalid_decimals_are_rejected_inside_arrays_too() {
191        let over_scale = vec![column("amount", decimal(38, 38), false)];
192        assert_eq!(
193            Schema::new(over_scale, Vec::new()),
194            Err(SchemaError::InvalidDecimal {
195                column: id("amount"),
196                precision: 38,
197                scale: 38
198            })
199        );
200        let in_array = vec![column(
201            "amounts",
202            CellType::Array {
203                element: Box::new(decimal(10, 12)),
204                nullable_elements: false,
205            },
206            false,
207        )];
208        assert_eq!(
209            Schema::new(in_array, Vec::new()),
210            Err(SchemaError::InvalidDecimal {
211                column: id("amounts"),
212                precision: 10,
213                scale: 12
214            })
215        );
216        let zero_precision = vec![column("amount", decimal(0, 0), false)];
217        assert!(Schema::new(zero_precision, Vec::new()).is_err());
218    }
219
220    #[test]
221    fn key_columns_must_exist_once_and_not_be_nullable() {
222        let columns = || {
223            vec![
224                column("id", CellType::Integer, false),
225                column("subject", CellType::Text, true),
226            ]
227        };
228        assert_eq!(
229            Schema::new(columns(), vec![id("ID")]),
230            Err(SchemaError::UnknownKeyColumn(id("ID")))
231        );
232        assert_eq!(
233            Schema::new(columns(), vec![id("id"), id("id")]),
234            Err(SchemaError::DuplicateKeyColumn(id("id")))
235        );
236        assert_eq!(
237            Schema::new(columns(), vec![id("subject")]),
238            Err(SchemaError::NullableKeyColumn(id("subject")))
239        );
240    }
241
242    #[test]
243    fn a_column_is_found_by_its_exact_name() {
244        let schema = Schema::new(vec![column("subject", CellType::Text, true)], Vec::new());
245        assert_eq!(
246            schema.as_ref().map(|s| s.column(&id("subject")).is_some()),
247            Ok(true)
248        );
249        assert_eq!(
250            schema.as_ref().map(|s| s.column(&id("SUBJECT")).is_none()),
251            Ok(true)
252        );
253    }
254}