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")]
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    /// Runs when retention hard-deletes `ids` from the aggregate's table `schema.name`.
41    #[cfg(feature = "retention")]
42    fn pruned(&self, schema: &str, name: &str, ids: &[Uuid], plan: &mut Plan<'_>) {
43        let _ = (schema, name, ids, plan);
44    }
45
46    /// Names the behavior in errors, which see it only as `dyn Behavior`.
47    fn name(&self) -> &'static str {
48        std::any::type_name::<Self>()
49    }
50}
51
52pub struct Write<'a> {
53    pub id: Uuid,
54    pub owner: Option<Uuid>,
55    pub stamp: Stamp,
56    pub(crate) table: &'a Table,
57    pub(crate) loaded: Option<&'a Snapshot>,
58    pub(crate) fields: &'a [SqlValue],
59    pub(crate) stored: &'a [SqlValue],
60}
61
62impl Write<'_> {
63    #[must_use]
64    pub fn loaded(&self, column: &str) -> Option<&SqlValue> {
65        let loaded = self.loaded?;
66        self.lookup(column, &loaded.stored, &loaded.fields)
67    }
68
69    #[must_use]
70    pub fn value(&self, column: &str) -> Option<&SqlValue> {
71        self.lookup(column, self.stored, self.fields)
72    }
73
74    #[cfg(any(feature = "codes", feature = "history"))]
75    pub(crate) fn deleted(&self) -> (bool, bool) {
76        let deleted =
77            |value: Option<&SqlValue>| value.is_some_and(|value| *value != SqlValue::Null);
78        (
79            deleted(self.loaded("deleted_at")),
80            deleted(self.value("deleted_at")),
81        )
82    }
83
84    fn lookup<'v>(
85        &self,
86        column: &str,
87        stored: &'v [SqlValue],
88        fields: &'v [SqlValue],
89    ) -> Option<&'v SqlValue> {
90        let position = |columns: &[String]| columns.iter().position(|name| name == column);
91        match position(&self.table.stored) {
92            Some(index) => stored.get(index),
93            None => fields.get(position(&self.table.columns)?),
94        }
95    }
96}
97
98pub trait Uses<B: Behavior>: Aggregate {}
99
100/// Every aggregate has it, first.
101pub struct Versioned;
102
103impl Behavior for Versioned {
104    fn columns(&self) -> &'static [Field] {
105        const { &[Field::new("version", i64::COLUMNS, false)] }
106    }
107
108    fn written(&self, write: &Write<'_>, values: &mut [SqlValue]) {
109        values[0] = SqlValue::Int(version(write.loaded("version")) + 1);
110    }
111
112    fn guards(&self) -> bool {
113        true
114    }
115}
116
117pub(crate) fn version(stored: Option<&SqlValue>) -> i64 {
118    match stored {
119        Some(SqlValue::Int(version)) => *version,
120        _ => 0,
121    }
122}
123
124pub struct Timestamps;
125
126impl Behavior for Timestamps {
127    fn columns(&self) -> &'static [Field] {
128        const {
129            &[
130                Field::new("created_at", Instant::COLUMNS, false),
131                Field::new("updated_at", Instant::COLUMNS, false),
132            ]
133        }
134    }
135
136    fn written(&self, write: &Write<'_>, values: &mut [SqlValue]) {
137        let now = SqlValue::Timestamp(write.stamp.now.timestamp());
138        values[0] = write.loaded("created_at").unwrap_or(&now).clone();
139        values[1] = now;
140    }
141}
142
143pub trait Timestamped: Aggregate {
144    const CREATED_AT: Column<Self, Instant> = Column::new("created_at");
145    const UPDATED_AT: Column<Self, Instant> = Column::new("updated_at");
146
147    fn created_at(&self) -> Option<Instant> {
148        stored_time::<Self>(self, "created_at")
149    }
150
151    fn updated_at(&self) -> Option<Instant> {
152        stored_time::<Self>(self, "updated_at")
153    }
154}
155
156impl<A: Uses<Timestamps>> Timestamped for A {}
157
158pub(crate) fn stored<'a, A: Aggregate>(aggregate: &'a A, column: &str) -> Option<&'a SqlValue> {
159    aggregate
160        .meta()
161        .stored
162        .get(table::<A>().stored_index(column))
163}
164
165pub(crate) fn stored_time<A: Aggregate>(aggregate: &A, column: &str) -> Option<Instant> {
166    match stored(aggregate, column) {
167        Some(SqlValue::Timestamp(time)) => Some(Instant::from(*time)),
168        _ => None,
169    }
170}