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 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
168pub(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
185pub(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 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 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("ed),
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 (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 (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
381fn 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}