Skip to main content

sofie_db/
behavior.rs

1use sofie_core::{Cast, Instant, Result, SqlValue, Uuid};
2
3use crate::aggregate::{Snapshot, Table, table};
4use crate::{Aggregate, Column, Field, Plan, Stamp};
5
6/// Never runs I/O.
7pub trait Behavior: Sync {
8    /// Stored after the id and before the aggregate's own fields.
9    fn columns(&self) -> &'static [Field];
10
11    fn written(&self, write: &Write<'_>, values: &mut [SqlValue]);
12
13    fn guards(&self) -> bool {
14        false
15    }
16
17    fn hides(&self) -> Option<&'static str> {
18        None
19    }
20
21    fn broadcasts(&self) -> bool {
22        false
23    }
24
25    fn searched(&self) -> bool {
26        false
27    }
28
29    #[cfg(feature = "search-engine")]
30    fn search_fields(&self) -> &'static [crate::SearchedField] {
31        &[]
32    }
33
34    /// Runs once every behavior has set its columns.
35    fn planned(&self, write: &Write<'_>, plan: &mut Plan<'_>) -> Result<()> {
36        let _ = (write, plan);
37        Ok(())
38    }
39
40    /// Names the behavior in errors, which see it only as `dyn Behavior`.
41    fn name(&self) -> &'static str {
42        std::any::type_name::<Self>()
43    }
44}
45
46pub struct Write<'a> {
47    pub id: Uuid,
48    pub owner: Option<Uuid>,
49    pub stamp: Stamp,
50    pub(crate) table: &'a Table,
51    pub(crate) loaded: Option<&'a Snapshot>,
52    pub(crate) fields: &'a [SqlValue],
53    pub(crate) stored: &'a [SqlValue],
54}
55
56impl Write<'_> {
57    #[must_use]
58    pub fn loaded(&self, column: &str) -> Option<&SqlValue> {
59        let loaded = self.loaded?;
60        self.lookup(column, &loaded.stored, &loaded.fields)
61    }
62
63    #[must_use]
64    pub fn value(&self, column: &str) -> Option<&SqlValue> {
65        self.lookup(column, self.stored, self.fields)
66    }
67
68    #[cfg(any(feature = "codes", feature = "history"))]
69    pub(crate) fn deleted(&self) -> (bool, bool) {
70        let deleted =
71            |value: Option<&SqlValue>| value.is_some_and(|value| *value != SqlValue::Null);
72        (
73            deleted(self.loaded("deleted_at")),
74            deleted(self.value("deleted_at")),
75        )
76    }
77
78    fn lookup<'v>(
79        &self,
80        column: &str,
81        stored: &'v [SqlValue],
82        fields: &'v [SqlValue],
83    ) -> Option<&'v SqlValue> {
84        let position = |columns: &[String]| columns.iter().position(|name| name == column);
85        match position(&self.table.stored) {
86            Some(index) => stored.get(index),
87            None => fields.get(position(&self.table.columns)?),
88        }
89    }
90}
91
92pub trait Uses<B: Behavior>: Aggregate {}
93
94/// Every aggregate has it, first.
95pub struct Versioned;
96
97impl Behavior for Versioned {
98    fn columns(&self) -> &'static [Field] {
99        const { &[Field::new("version", i64::COLUMNS, false)] }
100    }
101
102    fn written(&self, write: &Write<'_>, values: &mut [SqlValue]) {
103        values[0] = SqlValue::Int(version(write.loaded("version")) + 1);
104    }
105
106    fn guards(&self) -> bool {
107        true
108    }
109}
110
111pub(crate) fn version(stored: Option<&SqlValue>) -> i64 {
112    match stored {
113        Some(SqlValue::Int(version)) => *version,
114        _ => 0,
115    }
116}
117
118pub struct Timestamps;
119
120impl Behavior for Timestamps {
121    fn columns(&self) -> &'static [Field] {
122        const {
123            &[
124                Field::new("created_at", Instant::COLUMNS, false),
125                Field::new("updated_at", Instant::COLUMNS, false),
126            ]
127        }
128    }
129
130    fn written(&self, write: &Write<'_>, values: &mut [SqlValue]) {
131        let now = SqlValue::Timestamp(write.stamp.now.timestamp());
132        values[0] = write.loaded("created_at").unwrap_or(&now).clone();
133        values[1] = now;
134    }
135}
136
137pub trait Timestamped: Aggregate {
138    const CREATED_AT: Column<Self, Instant> = Column::new("created_at");
139    const UPDATED_AT: Column<Self, Instant> = Column::new("updated_at");
140
141    fn created_at(&self) -> Option<Instant> {
142        stored_time::<Self>(self, "created_at")
143    }
144
145    fn updated_at(&self) -> Option<Instant> {
146        stored_time::<Self>(self, "updated_at")
147    }
148}
149
150impl<A: Uses<Timestamps>> Timestamped for A {}
151
152pub(crate) fn stored<'a, A: Aggregate>(aggregate: &'a A, column: &str) -> Option<&'a SqlValue> {
153    aggregate
154        .meta()
155        .stored
156        .get(table::<A>().stored_index(column))
157}
158
159pub(crate) fn stored_time<A: Aggregate>(aggregate: &A, column: &str) -> Option<Instant> {
160    match stored(aggregate, column) {
161        Some(SqlValue::Timestamp(time)) => Some(Instant::from(*time)),
162        _ => None,
163    }
164}