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