Skip to main content

sofie_db/
children.rs

1pub(crate) mod through;
2
3use std::ops::{Deref, DerefMut};
4
5use serde_json::Value;
6use sofie_core::encoding::hex;
7use sofie_core::{ColumnReader, ColumnSpec, Error, Result, RowReader, SqlType, SqlValue, Uuid};
8
9use crate::aggregate::{Table, field_columns};
10use crate::selection::Selection;
11use crate::sql::{equals, same_values};
12use crate::{Aggregate, Dialect, Field, Plan, Selected, Step};
13pub use through::{owned_through, refuse_parent_beyond_reach};
14
15#[derive(Debug)]
16pub struct ChildTable {
17    pub module: &'static str,
18    pub name: &'static str,
19    pub fields: &'static [Field],
20}
21
22pub trait Child: Sized + Send + Sync + 'static {
23    const TABLE: ChildTable;
24
25    fn id(&self) -> Uuid;
26
27    fn encode(&self, out: &mut Vec<SqlValue>);
28
29    fn decode(id: Uuid, row: &mut dyn ColumnReader) -> Result<Self>;
30
31    fn refuse_unlisted_moves(&self, loaded: &mut dyn ColumnReader) -> Result<()> {
32        let _ = loaded;
33        Ok(())
34    }
35}
36
37pub struct Children<T> {
38    rows: Vec<T>,
39    loaded: Vec<(Uuid, Vec<SqlValue>)>,
40}
41
42impl<T> Default for Children<T> {
43    fn default() -> Self {
44        Self {
45            rows: Vec::new(),
46            loaded: Vec::new(),
47        }
48    }
49}
50
51impl<T: Child> Children<T> {
52    pub fn push(&mut self, child: T) {
53        self.rows.push(child);
54    }
55
56    pub fn retain(&mut self, keep: impl FnMut(&T) -> bool) {
57        self.rows.retain(keep);
58    }
59
60    pub fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
61        let Value::Array(rows) = row.json()? else {
62            return Err(Error::internal_message(format!(
63                "expected the rows of `{}` as a JSON array",
64                T::TABLE.name
65            )));
66        };
67        let loaded = rows
68            .into_iter()
69            .map(child_columns::<T>)
70            .collect::<Result<Vec<_>>>()?;
71        let rows = loaded
72            .iter()
73            .map(|(id, values)| T::decode(*id, &mut RowReader::new(values)))
74            .collect::<Result<_>>()?;
75        Ok(Self { rows, loaded })
76    }
77
78    fn current(&self) -> Vec<(Uuid, Vec<SqlValue>)> {
79        self.rows
80            .iter()
81            .map(|child| {
82                let mut values = Vec::new();
83                child.encode(&mut values);
84                (child.id(), values)
85            })
86            .collect()
87    }
88}
89
90impl<T> Deref for Children<T> {
91    type Target = [T];
92
93    fn deref(&self) -> &[T] {
94        &self.rows
95    }
96}
97
98impl<T> DerefMut for Children<T> {
99    fn deref_mut(&mut self) -> &mut [T] {
100        &mut self.rows
101    }
102}
103
104pub trait ChildRows: Send + Sync {
105    fn changed(&self) -> bool;
106
107    fn steps(&self, parent: &str, parent_id: Uuid, dialect: &dyn Dialect) -> Vec<Step>;
108
109    fn committed(&mut self);
110
111    fn refuse_unlisted_moves(&self) -> Result<()>;
112}
113
114impl<T: Child> ChildRows for Children<T> {
115    /// By id as [`Self::steps`] writes, so children only put in another order change nothing.
116    fn changed(&self) -> bool {
117        let current = self.current();
118        let kept = |id: &Uuid| current.iter().any(|(kept, _)| kept == id);
119        self.loaded.iter().any(|(id, _)| !kept(id))
120            || current.iter().any(|(id, values)| {
121                self.loaded
122                    .iter()
123                    .find(|(loaded, _)| loaded == id)
124                    .is_none_or(|(_, old)| !same_values(old, values))
125            })
126    }
127
128    fn steps(&self, parent: &str, parent_id: Uuid, dialect: &dyn Dialect) -> Vec<Step> {
129        let current = self.current();
130        let rows = ChildStatements {
131            table: &T::TABLE,
132            parent,
133            parent_id,
134            dialect,
135        };
136        let loaded = |id: &Uuid| self.loaded.iter().find(|(loaded, _)| loaded == id);
137        let deletes = self
138            .loaded
139            .iter()
140            .filter(|(id, _)| !current.iter().any(|(kept, _)| kept == id))
141            .map(|(id, _)| rows.delete(*id));
142        let updates = current.iter().filter_map(|(id, values)| {
143            let (_, old) = loaded(id)?;
144            (!same_values(old, values)).then(|| rows.update(*id, values))
145        });
146        let inserts = current
147            .iter()
148            .filter(|(id, _)| loaded(id).is_none())
149            .map(|(id, values)| rows.insert(*id, values));
150        deletes.chain(updates).chain(inserts).collect()
151    }
152
153    fn committed(&mut self) {
154        self.loaded = self.current();
155    }
156
157    fn refuse_unlisted_moves(&self) -> Result<()> {
158        for child in &self.rows {
159            let id = child.id();
160            if let Some((_, loaded)) = self.loaded.iter().find(|(loaded, _)| *loaded == id) {
161                child.refuse_unlisted_moves(&mut RowReader::new(loaded))?;
162            }
163        }
164        Ok(())
165    }
166}
167
168/// After their parent, whose owner it checks.
169pub(crate) fn plan_children<A: Aggregate>(
170    aggregate: &A,
171    plan: &mut Plan<'_>,
172    owned_through_parent: Option<Uuid>,
173) {
174    let id = aggregate.meta().id();
175    for children in aggregate.children() {
176        for step in children.steps(A::NAME, id, plan.dialect()) {
177            plan.push(step);
178        }
179    }
180    if let Some(owner) = owned_through_parent {
181        plan.push(through::ownership_check::<A>(id, owner, plan.dialect()));
182    }
183}
184
185/// Run before the write, so the row still holds the parent it was loaded with.
186pub(crate) fn check_loaded_parent<A: Aggregate>(
187    aggregate: &A,
188    plan: &mut Plan<'_>,
189    owned_through_parent: Option<Uuid>,
190) {
191    let meta = aggregate.meta();
192    if let (Some(owner), Some(_)) = (owned_through_parent, meta.snapshot.as_ref()) {
193        plan.push(through::ownership_check::<A>(
194            meta.id(),
195            owner,
196            plan.dialect(),
197        ));
198    }
199}
200
201pub(crate) fn committed<A: Aggregate>(aggregate: &mut A) {
202    for children in aggregate.children_mut() {
203        children.committed();
204    }
205}
206
207pub(crate) fn refuse_unlisted_moves<A: Aggregate>(aggregate: &A) -> Result<()> {
208    aggregate
209        .children()
210        .iter()
211        .try_for_each(|children| children.refuse_unlisted_moves())
212}
213
214pub(crate) fn children_changed<A: Aggregate>(aggregate: &A) -> bool {
215    aggregate
216        .children()
217        .iter()
218        .any(|children| children.changed())
219}
220
221struct ChildStatements<'a> {
222    table: &'a ChildTable,
223    parent: &'a str,
224    parent_id: Uuid,
225    dialect: &'a dyn Dialect,
226}
227
228impl ChildStatements<'_> {
229    fn qualified(&self) -> String {
230        self.dialect.qualified(self.table.module, self.table.name)
231    }
232
233    fn insert(&self, id: Uuid, values: &[SqlValue]) -> Step {
234        let mut columns = vec!["id".to_owned(), self.parent.to_owned()];
235        columns.extend(field_columns(self.table.fields));
236        let mut params = vec![SqlValue::Uuid(id), SqlValue::Uuid(self.parent_id)];
237        params.extend_from_slice(values);
238        let sql = crate::sql::insert(self.dialect, &self.qualified(), &columns);
239        Step::statement(sql, params, None)
240    }
241
242    /// Every column, so each table's update is one prepared statement.
243    fn update(&self, id: Uuid, values: &[SqlValue]) -> Step {
244        let mut params = Vec::new();
245        let columns = field_columns(self.table.fields);
246        let assignments: Vec<String> = columns
247            .iter()
248            .zip(values)
249            .map(|(column, value)| equals(self.dialect, column, value.clone(), &mut params))
250            .collect();
251        let condition = self.this_row(id, &mut params);
252        let sql = format!(
253            "UPDATE {} SET {} WHERE {condition}",
254            self.qualified(),
255            assignments.join(", ")
256        );
257        Step::statement(sql, params, None)
258    }
259
260    fn delete(&self, id: Uuid) -> Step {
261        let mut params = Vec::new();
262        let condition = self.this_row(id, &mut params);
263        let sql = format!("DELETE FROM {} WHERE {condition}", self.qualified());
264        Step::statement(sql, params, None)
265    }
266
267    /// Within its parent, so a forged id cannot reach another parent's child.
268    fn this_row(&self, id: Uuid, params: &mut Vec<SqlValue>) -> String {
269        let id = equals(self.dialect, "id", SqlValue::Uuid(id), params);
270        let parent = equals(
271            self.dialect,
272            self.parent,
273            SqlValue::Uuid(self.parent_id),
274            params,
275        );
276        format!("{id} AND {parent}")
277    }
278}
279
280impl ChildTable {
281    fn specs(&self) -> impl Iterator<Item = &'static ColumnSpec> {
282        self.fields.iter().flat_map(|field| field.columns)
283    }
284
285    pub(crate) fn selected(&'static self) -> Selected {
286        Selected(Selection::Children(self))
287    }
288
289    pub(crate) fn render(&self, base: &Table, dialect: &dyn Dialect) -> String {
290        let fields = field_columns(self.fields).into_iter().zip(self.specs());
291        let columns: Vec<String> = std::iter::once(dialect.quote("id"))
292            .chain(fields.map(|(column, spec)| {
293                let quoted = dialect.quote(&column);
294                match spec.ty {
295                    SqlType::Bytes => dialect.bytes_in_json(&quoted),
296                    _ => quoted,
297                }
298            }))
299            .collect();
300        let from = format!(
301            "{} WHERE {} = {}.{}",
302            dialect.qualified(self.module, self.name),
303            dialect.quote(base.name),
304            base.qualified(dialect),
305            dialect.quote("id")
306        );
307        dialect.nested_rows(&columns.join(", "), &from)
308    }
309}
310
311fn child_columns<T: Child>(row: Value) -> Result<(Uuid, Vec<SqlValue>)> {
312    let Value::Array(values) = row else {
313        return Err(Error::internal_message(format!(
314            "expected a row of `{}` as a JSON array",
315            T::TABLE.name
316        )));
317    };
318    let mut values = values.into_iter();
319    let id = match values.next() {
320        Some(Value::String(id)) => id
321            .parse()
322            .map_err(|error| Error::internal("reading a child row's id", error))?,
323        _ => return Err(Error::internal_message("a child row without its id")),
324    };
325    let columns = T::TABLE
326        .specs()
327        .zip(values)
328        .map(|(spec, value)| sql_value(spec.ty, value))
329        .collect::<Result<_>>()?;
330    Ok((id, columns))
331}
332
333fn sql_value(ty: SqlType, value: Value) -> Result<SqlValue> {
334    let unreadable = |error: &dyn std::fmt::Display| {
335        Error::internal_message(format!("reading a {ty:?} child column: {error}"))
336    };
337    Ok(match (ty, value) {
338        (_, Value::Null) => SqlValue::Null,
339        (SqlType::Bool, Value::Bool(value)) => SqlValue::Bool(value),
340        // A database without booleans or a timestamp type writes them as numbers.
341        (SqlType::Bool, Value::Number(number)) => SqlValue::Bool(number.as_i64() != Some(0)),
342        (SqlType::Timestamp, Value::Number(number)) => SqlValue::Timestamp(
343            number
344                .as_i64()
345                .and_then(|micros| sofie_core::Timestamp::from_microsecond(micros).ok())
346                .ok_or_else(|| unreadable(&number))?,
347        ),
348        (SqlType::Int, Value::Number(number)) => {
349            SqlValue::Int(number.as_i64().ok_or_else(|| unreadable(&number))?)
350        }
351        (SqlType::Float, Value::Number(number)) => {
352            SqlValue::Float(number.as_f64().ok_or_else(|| unreadable(&number))?)
353        }
354        // Postgres's JSON has no number for these, so it writes them as text.
355        (SqlType::Float, Value::String(text)) => SqlValue::Float(match text.as_str() {
356            "NaN" => f64::NAN,
357            "Infinity" => f64::INFINITY,
358            "-Infinity" => f64::NEG_INFINITY,
359            _ => return Err(unreadable(&text)),
360        }),
361        (SqlType::Text, Value::String(text)) => SqlValue::Text(text),
362        (SqlType::Uuid, Value::String(text)) => {
363            SqlValue::Uuid(text.parse().map_err(|error| unreadable(&error))?)
364        }
365        (SqlType::Timestamp, Value::String(text)) => {
366            SqlValue::Timestamp(text.parse().map_err(|error| unreadable(&error))?)
367        }
368        (SqlType::Date, Value::String(text)) => {
369            SqlValue::Date(text.parse().map_err(|error| unreadable(&error))?)
370        }
371        (SqlType::DateTime, Value::String(text)) => {
372            SqlValue::DateTime(text.parse().map_err(|error| unreadable(&error))?)
373        }
374        (SqlType::Bytes, Value::String(text)) => {
375            SqlValue::Bytes(hex_bytes(&text).ok_or_else(|| unreadable(&"bad hex"))?)
376        }
377        (_, other) => return Err(unreadable(&other)),
378    })
379}
380
381/// `\x0a1b`, the text form of a byte column.
382fn hex_bytes(text: &str) -> Option<Vec<u8>> {
383    hex::decode(text.strip_prefix("\\x")?)
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn json_columns_read_back_as_their_types() {
392        let read = |ty, value: Value| sql_value(ty, value).unwrap();
393
394        assert_eq!(read(SqlType::Int, 7.into()), SqlValue::Int(7));
395        assert_eq!(
396            read(SqlType::Text, "a".into()),
397            SqlValue::Text("a".to_owned())
398        );
399        assert_eq!(
400            read(SqlType::Bytes, "\\x0aff".into()),
401            SqlValue::Bytes(vec![10, 255])
402        );
403        assert_eq!(read(SqlType::Int, Value::Null), SqlValue::Null);
404        assert!(matches!(
405            read(SqlType::Timestamp, "2026-10-11T09:30:00.5+00:00".into()),
406            SqlValue::Timestamp(_)
407        ));
408        assert!(sql_value(SqlType::Int, "7".into()).is_err());
409    }
410
411    #[test]
412    fn a_float_postgres_writes_as_text_reads_back() {
413        let read = |text: &str| sql_value(SqlType::Float, text.into());
414
415        assert!(matches!(read("NaN"), Ok(SqlValue::Float(value)) if value.is_nan()));
416        assert_eq!(read("Infinity").unwrap(), SqlValue::Float(f64::INFINITY));
417        assert_eq!(
418            read("-Infinity").unwrap(),
419            SqlValue::Float(f64::NEG_INFINITY)
420        );
421        assert!(read("1.5").is_err());
422    }
423
424    struct Weight {
425        id: Uuid,
426        kilograms: f64,
427    }
428
429    impl Child for Weight {
430        const TABLE: ChildTable = ChildTable {
431            module: "stock",
432            name: "weight",
433            fields: &[Field::new(
434                "kilograms",
435                <f64 as sofie_core::Cast>::COLUMNS,
436                false,
437            )],
438        };
439
440        fn id(&self) -> Uuid {
441            self.id
442        }
443
444        fn encode(&self, out: &mut Vec<SqlValue>) {
445            out.push(SqlValue::Float(self.kilograms));
446        }
447
448        fn decode(id: Uuid, row: &mut dyn ColumnReader) -> Result<Self> {
449            Ok(Self {
450                id,
451                kilograms: row.float()?,
452            })
453        }
454    }
455
456    #[test]
457    fn a_child_float_changes_by_its_bits() {
458        let id = Uuid::now_v7().unwrap();
459        let dialect = crate::aggregate::tests::NUMBERED;
460        let mut children = Children::default();
461        children.push(Weight {
462            id,
463            kilograms: f64::NAN,
464        });
465        children.committed();
466
467        assert!(!children.changed());
468        assert!(children.steps("parcel", id, &dialect).is_empty());
469
470        children[0].kilograms = -0.0;
471        children.committed();
472        children[0].kilograms = 0.0;
473
474        assert!(children.changed());
475        assert_eq!(children.steps("parcel", id, &dialect).len(), 1);
476    }
477
478    #[test]
479    fn children_put_in_another_order_are_unchanged() {
480        let weight = |kilograms| Weight {
481            id: Uuid::now_v7().unwrap(),
482            kilograms,
483        };
484        let mut children = Children::default();
485        children.push(weight(1.0));
486        children.push(weight(2.0));
487        children.committed();
488
489        children.reverse();
490
491        assert!(!children.changed());
492        assert!(
493            children
494                .steps(
495                    "parcel",
496                    Uuid::now_v7().unwrap(),
497                    &crate::aggregate::tests::NUMBERED
498                )
499                .is_empty()
500        );
501    }
502
503    struct Label {
504        id: Uuid,
505        text: String,
506        color: String,
507    }
508
509    impl Child for Label {
510        const TABLE: ChildTable = ChildTable {
511            module: "stock",
512            name: "label",
513            fields: &[
514                Field::new("text", <String as sofie_core::Cast>::COLUMNS, false),
515                Field::new("color", <String as sofie_core::Cast>::COLUMNS, false),
516            ],
517        };
518
519        fn id(&self) -> Uuid {
520            self.id
521        }
522
523        fn encode(&self, out: &mut Vec<SqlValue>) {
524            out.push(SqlValue::Text(self.text.clone()));
525            out.push(SqlValue::Text(self.color.clone()));
526        }
527
528        fn decode(id: Uuid, row: &mut dyn ColumnReader) -> Result<Self> {
529            Ok(Self {
530                id,
531                text: row.text()?.to_owned(),
532                color: row.text()?.to_owned(),
533            })
534        }
535    }
536
537    #[test]
538    fn a_child_row_is_updated_by_one_statement_whatever_changed() {
539        let dialect = crate::aggregate::tests::NUMBERED;
540        let mut labels = Children::default();
541        for _ in 0..2 {
542            labels.push(Label {
543                id: Uuid::now_v7().unwrap(),
544                text: "fragile".to_owned(),
545                color: "red".to_owned(),
546            });
547        }
548        labels.committed();
549
550        labels[0].text = "heavy".to_owned();
551        labels[1].color = "blue".to_owned();
552        let statements: Vec<String> = labels
553            .steps("parcel", Uuid::now_v7().unwrap(), &dialect)
554            .into_iter()
555            .map(|step| match step {
556                Step::Statement { sql, .. } | Step::Script(sql) => sql,
557            })
558            .collect();
559
560        assert_eq!(statements.len(), 2);
561        assert_eq!(statements[0], statements[1]);
562    }
563}