Skip to main content

sofie_core/
sql.rs

1use crate::moves::{Moves, targets};
2use crate::{Error, Result, Uuid};
3use jiff::Timestamp;
4use jiff::civil::{Date, DateTime};
5
6#[derive(Clone, Debug, PartialEq)]
7pub enum SqlValue {
8    Null,
9    Bool(bool),
10    Int(i64),
11    Float(f64),
12    Text(String),
13    Bytes(Vec<u8>),
14    Uuid(Uuid),
15    Timestamp(Timestamp),
16    Date(Date),
17    DateTime(DateTime),
18    Json(serde_json::Value),
19    /// Bound as one array parameter; never stored.
20    Array(Vec<SqlValue>),
21}
22
23impl SqlValue {
24    /// What kind of value this is, for errors that must not quote the value.
25    #[must_use]
26    pub fn kind(&self) -> &'static str {
27        match self {
28            Self::Null => "null",
29            Self::Bool(_) => "bool",
30            Self::Int(_) => "integer",
31            Self::Float(_) => "float",
32            Self::Text(_) => "text",
33            Self::Bytes(_) => "bytes",
34            Self::Uuid(_) => "uuid",
35            Self::Timestamp(_) => "timestamp",
36            Self::Date(_) => "date",
37            Self::DateTime(_) => "date-time",
38            Self::Json(_) => "json",
39            Self::Array(_) => "array",
40        }
41    }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum SqlType {
46    Bool,
47    Int,
48    Float,
49    Text,
50    Bytes,
51    Uuid,
52    Timestamp,
53    Date,
54    DateTime,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct ColumnSpec {
59    pub suffix: &'static str,
60    pub ty: SqlType,
61    /// A list of records sorts them by the one column of a value that orders it, such as
62    /// an amount's minor units rather than its currency.
63    pub orders: bool,
64    /// The only texts the column holds, as an enum's variant names; empty for any text.
65    pub values: &'static [&'static str],
66}
67
68impl ColumnSpec {
69    #[must_use]
70    pub const fn new(suffix: &'static str, ty: SqlType) -> Self {
71        Self {
72            suffix,
73            ty,
74            orders: true,
75            values: &[],
76        }
77    }
78
79    #[must_use]
80    pub const fn with_values(self, values: &'static [&'static str]) -> Self {
81        Self { values, ..self }
82    }
83
84    #[must_use]
85    pub const fn unordered(self) -> Self {
86        Self {
87            orders: false,
88            ..self
89        }
90    }
91}
92
93/// Whether field `index` stores a column an earlier field stores; a derive asserts it in a constant.
94#[doc(hidden)]
95#[must_use]
96pub const fn stores_earlier_column(fields: &[(&str, &[ColumnSpec])], index: usize) -> bool {
97    let (name, columns) = fields[index];
98    let mut earlier = 0;
99    while earlier < index {
100        let (earlier_name, earlier_columns) = fields[earlier];
101        let mut column = 0;
102        while column < columns.len() {
103            if names_column(earlier_name, earlier_columns, name, columns[column].suffix) {
104                return true;
105            }
106            column += 1;
107        }
108        earlier += 1;
109    }
110    false
111}
112
113const fn names_column(name: &str, columns: &[ColumnSpec], other: &str, suffix: &str) -> bool {
114    let mut column = 0;
115    while column < columns.len() {
116        if same_column(name, columns[column].suffix, other, suffix) {
117            return true;
118        }
119        column += 1;
120    }
121    false
122}
123
124const fn same_column(left: &str, left_suffix: &str, right: &str, right_suffix: &str) -> bool {
125    let length = left.len() + left_suffix.len();
126    if length != right.len() + right_suffix.len() {
127        return false;
128    }
129    let mut index = 0;
130    while index < length {
131        if byte_at(left, left_suffix, index) != byte_at(right, right_suffix, index) {
132            return false;
133        }
134        index += 1;
135    }
136    true
137}
138
139const fn byte_at(name: &str, suffix: &str, index: usize) -> u8 {
140    if index < name.len() {
141        return name.as_bytes()[index];
142    }
143    suffix.as_bytes()[index - name.len()]
144}
145
146pub trait Cast: Sized {
147    const COLUMNS: &'static [ColumnSpec];
148    const NULLABLE: bool = false;
149    /// A field holding it is never filtered, sorted, listed or searched, as `#[sensitive]` ones.
150    const SENSITIVE: bool = false;
151    /// Set for a value moving between listed states; a save of any other value skips the check.
152    const MOVES: Option<Moves<Self>> = None;
153    /// Set when a state anywhere in the value is checked on save, as a field's of several.
154    const HOLDS_STATES: bool = Self::MOVES.is_some();
155
156    fn encode(&self, out: &mut Vec<SqlValue>);
157
158    fn decode(row: &mut dyn ColumnReader) -> Result<Self>;
159
160    fn refuse_unlisted_move(field: &str, stored: &Self, current: &Self) -> Result<()> {
161        crate::moves::refuse_unlisted(field, stored, current)
162    }
163}
164
165/// One row's columns in order, each read once straight into its target, text borrowed meanwhile.
166pub trait ColumnReader {
167    fn remaining(&self) -> usize;
168
169    /// Of the column `ahead` places past the next one, without reading it.
170    fn is_null_at(&self, ahead: usize) -> Result<bool>;
171
172    fn is_null(&self) -> Result<bool> {
173        self.is_null_at(0)
174    }
175
176    fn skip(&mut self) -> Result<()>;
177
178    fn bool(&mut self) -> Result<bool>;
179
180    fn int(&mut self) -> Result<i64>;
181
182    fn float(&mut self) -> Result<f64>;
183
184    fn text(&mut self) -> Result<&str>;
185
186    fn bytes(&mut self) -> Result<&[u8]>;
187
188    fn uuid(&mut self) -> Result<Uuid>;
189
190    fn timestamp(&mut self) -> Result<Timestamp>;
191
192    fn date(&mut self) -> Result<Date>;
193
194    fn date_time(&mut self) -> Result<DateTime>;
195
196    fn json(&mut self) -> Result<serde_json::Value>;
197
198    fn value(&mut self) -> Result<SqlValue>;
199}
200
201pub struct RowReader<'a> {
202    values: std::slice::Iter<'a, SqlValue>,
203}
204
205impl<'a> RowReader<'a> {
206    #[must_use]
207    pub fn new(values: &'a [SqlValue]) -> Self {
208        Self {
209            values: values.iter(),
210        }
211    }
212
213    fn take(&mut self) -> Result<&'a SqlValue> {
214        self.values.next().ok_or_else(short_row)
215    }
216
217    fn read<T>(
218        &mut self,
219        expected: &str,
220        pick: impl FnOnce(&'a SqlValue) -> Option<T>,
221    ) -> Result<T> {
222        let value = self.take()?;
223        pick(value).ok_or_else(|| mismatch(expected, value))
224    }
225}
226
227macro_rules! pick {
228    ($pattern:pat => $value:expr) => {
229        |value| match value {
230            $pattern => Some($value),
231            _ => None,
232        }
233    };
234}
235
236impl ColumnReader for RowReader<'_> {
237    fn remaining(&self) -> usize {
238        self.values.len()
239    }
240
241    fn is_null_at(&self, ahead: usize) -> Result<bool> {
242        let column = self.values.as_slice().get(ahead).ok_or_else(short_row)?;
243        Ok(*column == SqlValue::Null)
244    }
245
246    fn skip(&mut self) -> Result<()> {
247        self.take().map(drop)
248    }
249
250    fn bool(&mut self) -> Result<bool> {
251        self.read("bool", pick!(SqlValue::Bool(value) => *value))
252    }
253
254    fn int(&mut self) -> Result<i64> {
255        self.read("integer", pick!(SqlValue::Int(value) => *value))
256    }
257
258    fn float(&mut self) -> Result<f64> {
259        self.read("float", pick!(SqlValue::Float(value) => *value))
260    }
261
262    fn text(&mut self) -> Result<&str> {
263        self.read("text", pick!(SqlValue::Text(value) => value.as_str()))
264    }
265
266    fn bytes(&mut self) -> Result<&[u8]> {
267        self.read("bytes", pick!(SqlValue::Bytes(value) => value.as_slice()))
268    }
269
270    fn uuid(&mut self) -> Result<Uuid> {
271        self.read("uuid", pick!(SqlValue::Uuid(value) => *value))
272    }
273
274    fn timestamp(&mut self) -> Result<Timestamp> {
275        self.read("timestamp", pick!(SqlValue::Timestamp(value) => *value))
276    }
277
278    fn date(&mut self) -> Result<Date> {
279        self.read("date", pick!(SqlValue::Date(value) => *value))
280    }
281
282    fn date_time(&mut self) -> Result<DateTime> {
283        self.read("date-time", pick!(SqlValue::DateTime(value) => *value))
284    }
285
286    fn json(&mut self) -> Result<serde_json::Value> {
287        self.read("json", pick!(SqlValue::Json(value) => value.clone()))
288    }
289
290    fn value(&mut self) -> Result<SqlValue> {
291        self.take().cloned()
292    }
293}
294
295fn short_row() -> Error {
296    Error::internal_message("row has fewer columns than expected")
297}
298
299fn mismatch(expected: &str, found: &SqlValue) -> Error {
300    Error::internal_message(format!(
301        "expected a {expected} column, found a {} value",
302        found.kind()
303    ))
304}
305
306macro_rules! scalar_cast {
307    ($type:ty, $sql:ident, |$value:ident| $encode:expr, |$row:ident| $decode:expr) => {
308        impl Cast for $type {
309            const COLUMNS: &'static [ColumnSpec] = &[ColumnSpec::new("", SqlType::$sql)];
310
311            fn encode(&self, out: &mut Vec<SqlValue>) {
312                let $value = self;
313                out.push($encode);
314            }
315
316            fn decode($row: &mut dyn ColumnReader) -> Result<Self> {
317                $decode
318            }
319        }
320    };
321}
322
323scalar_cast!(bool, Bool, |value| SqlValue::Bool(*value), |row| row.bool());
324scalar_cast!(i64, Int, |value| SqlValue::Int(*value), |row| row.int());
325scalar_cast!(i32, Int, |value| SqlValue::Int(i64::from(*value)), |row| {
326    i32::try_from(row.int()?).map_err(|error| Error::internal("integer column out of range", error))
327});
328scalar_cast!(f64, Float, |value| SqlValue::Float(*value), |row| row
329    .float());
330scalar_cast!(String, Text, |value| SqlValue::Text(value.clone()), |row| {
331    row.text().map(str::to_owned)
332});
333scalar_cast!(Uuid, Uuid, |value| SqlValue::Uuid(*value), |row| row.uuid());
334scalar_cast!(
335    Timestamp,
336    Timestamp,
337    |value| SqlValue::Timestamp(*value),
338    |row| row.timestamp()
339);
340scalar_cast!(Date, Date, |value| SqlValue::Date(*value), |row| row.date());
341scalar_cast!(
342    DateTime,
343    DateTime,
344    |value| SqlValue::DateTime(*value),
345    |row| row.date_time()
346);
347
348pub trait Ordered: Cast {}
349
350impl Ordered for i64 {}
351impl Ordered for i32 {}
352impl Ordered for f64 {}
353impl Ordered for String {}
354impl Ordered for Timestamp {}
355impl Ordered for Date {}
356impl Ordered for DateTime {}
357impl<T: Ordered> Ordered for Option<T> {}
358
359/// Only all-null columns read back as `None`, so an `Option` of a value that can be all nulls
360/// (an `Option`, or a value of several fields that are all optional) is refused when compiled:
361///
362/// ```compile_fail,E0080
363/// use sofie_core::Cast;
364///
365/// let _ = <Option<Option<i64>>>::COLUMNS;
366/// ```
367impl<T: Cast> Cast for Option<T> {
368    const COLUMNS: &'static [ColumnSpec] = {
369        assert!(
370            !T::NULLABLE,
371            "an `Option` of a nullable value cannot be stored: `Some(None)` would read back as `None`"
372        );
373        T::COLUMNS
374    };
375    const NULLABLE: bool = true;
376    const SENSITIVE: bool = T::SENSITIVE;
377    const MOVES: Option<Moves<Self>> = if T::MOVES.is_some() {
378        Some(|value| value.as_ref().and_then(targets))
379    } else {
380        None
381    };
382    const HOLDS_STATES: bool = T::HOLDS_STATES;
383
384    fn encode(&self, out: &mut Vec<SqlValue>) {
385        match self {
386            Some(value) => value.encode(out),
387            None => out.extend(Self::COLUMNS.iter().map(|_| SqlValue::Null)),
388        }
389    }
390
391    fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
392        for ahead in 0..Self::COLUMNS.len() {
393            if !row.is_null_at(ahead)? {
394                return T::decode(row).map(Some);
395            }
396        }
397        for _ in Self::COLUMNS {
398            row.skip()?;
399        }
400        Ok(None)
401    }
402
403    fn refuse_unlisted_move(field: &str, stored: &Self, current: &Self) -> Result<()> {
404        match (stored, current) {
405            (Some(stored), Some(current)) => T::refuse_unlisted_move(field, stored, current),
406            _ => crate::moves::refuse_unlisted(field, stored, current),
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn option_reads_none_from_null_and_some_from_a_value() {
417        let values = [SqlValue::Null, SqlValue::Int(7)];
418        let mut row = RowReader::new(&values);
419
420        assert_eq!(Option::<i64>::decode(&mut row).unwrap(), None);
421        assert_eq!(Option::<i64>::decode(&mut row).unwrap(), Some(7));
422    }
423
424    #[derive(Debug, PartialEq)]
425    struct Period {
426        from: Option<i64>,
427        until: i64,
428    }
429
430    impl Cast for Period {
431        const COLUMNS: &'static [ColumnSpec] = &[
432            ColumnSpec::new("_from", SqlType::Int),
433            ColumnSpec::new("_until", SqlType::Int),
434        ];
435
436        fn encode(&self, out: &mut Vec<SqlValue>) {
437            self.from.encode(out);
438            self.until.encode(out);
439        }
440
441        fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
442            Ok(Self {
443                from: Cast::decode(row)?,
444                until: Cast::decode(row)?,
445            })
446        }
447    }
448
449    #[test]
450    fn an_option_of_several_columns_is_none_only_when_every_column_is_null() {
451        let period = Some(Period {
452            from: None,
453            until: 9,
454        });
455        let mut values = Vec::new();
456        period.encode(&mut values);
457        None::<Period>.encode(&mut values);
458        values.push(SqlValue::Int(1));
459        let mut row = RowReader::new(&values);
460
461        assert_eq!(Option::<Period>::decode(&mut row).unwrap(), period);
462        assert_eq!(Option::<Period>::decode(&mut row).unwrap(), None);
463        assert_eq!(i64::decode(&mut row).unwrap(), 1);
464    }
465
466    #[test]
467    fn a_value_of_the_wrong_type_is_an_internal_error_that_does_not_quote_it() {
468        let values = [SqlValue::Text("seven".into())];
469
470        let error = i64::decode(&mut RowReader::new(&values)).unwrap_err();
471
472        assert!(matches!(error, Error::Internal { .. }));
473        assert!(!error.to_string().contains("seven"), "{error}");
474    }
475
476    #[test]
477    fn a_short_row_is_refused() {
478        let values = [SqlValue::Int(1)];
479        let mut row = RowReader::new(&values);
480
481        i64::decode(&mut row).unwrap();
482
483        assert!(matches!(i64::decode(&mut row), Err(Error::Internal { .. })));
484        assert!(Option::<i64>::decode(&mut row).is_err());
485    }
486
487    #[test]
488    fn a_field_storing_an_earlier_field_s_column_is_found() {
489        const AMOUNT: &[ColumnSpec] = &[
490            ColumnSpec::new("_minor", SqlType::Int),
491            ColumnSpec::new("_currency", SqlType::Text),
492        ];
493        let fields = [
494            ("total", AMOUNT),
495            ("total_currency", String::COLUMNS),
496            ("totals", String::COLUMNS),
497            ("note", AMOUNT),
498            ("totals", String::COLUMNS),
499        ];
500
501        assert!(stores_earlier_column(&fields, 1));
502        assert!(!stores_earlier_column(&fields, 2));
503        assert!(!stores_earlier_column(&fields, 3));
504        assert!(stores_earlier_column(&fields, 4));
505    }
506
507    #[test]
508    fn an_integer_too_large_for_i32_is_refused() {
509        let values = [SqlValue::Int(i64::MAX)];
510
511        assert!(i32::decode(&mut RowReader::new(&values)).is_err());
512    }
513}