1use sofie_core::{Cast, Instant, Result, SqlValue, Uuid};
2
3use crate::aggregate::{Snapshot, Table, table};
4use crate::{Aggregate, Column, Field, Plan, Stamp};
5
6pub trait Behavior: Sync {
8 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 fn planned(&self, write: &Write<'_>, plan: &mut Plan<'_>) -> Result<()> {
36 let _ = (write, plan);
37 Ok(())
38 }
39
40 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
94pub 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}