fluidattacks_database_core_domain/table/
schema.rs1use alloc::vec::Vec;
2
3use super::error::SchemaError;
4use crate::cell::{valid_decimal, CellType};
5use crate::core::ColumnId;
6
7#[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#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Schema {
45 columns: Vec<Column>,
46 key: Vec<ColumnId>,
47}
48
49fn 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
61fn 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}