1use std::fmt;
2
3use sofie_core::{Cast, Error, Result, SqlType, Uuid};
4
5use crate::{Aggregate, Behavior, BoxFuture, Field, Migrations, Schema, Versioned};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct CatalogColumn {
9 pub name: String,
10 pub ty: Option<SqlType>,
11 pub type_name: String,
12 pub nullable: bool,
13 pub filled: bool,
14}
15
16pub trait Catalog: Send + Sync {
17 fn columns<'a>(
18 &'a self,
19 schema: &'a str,
20 name: &'a str,
21 ) -> BoxFuture<'a, Result<Vec<CatalogColumn>>>;
22}
23
24#[derive(Clone, Copy)]
25pub struct AggregateTable {
26 module: &'static str,
27 name: &'static str,
28 fields: &'static [Field],
29 behaviors: &'static [&'static dyn Behavior],
30 #[cfg(feature = "children")]
31 children: &'static [&'static crate::ChildTable],
32 #[cfg(feature = "search-engine")]
33 owner: fn() -> Option<&'static str>,
34}
35
36impl AggregateTable {
37 #[must_use]
38 pub const fn of<A: Aggregate>() -> Self {
39 Self {
40 module: A::MODULE,
41 name: A::NAME,
42 fields: A::FIELDS,
43 behaviors: A::BEHAVIORS,
44 #[cfg(feature = "children")]
45 children: A::CHILDREN,
46 #[cfg(feature = "search-engine")]
47 owner: crate::aggregate::direct_owner::<A>,
48 }
49 }
50
51 #[cfg(feature = "search-engine")]
52 pub(crate) fn search_index(&self) -> Option<crate::SearchIndex> {
53 let behaviors = self.behaviors;
54 behaviors
55 .iter()
56 .any(|behavior| behavior.searched())
57 .then(|| crate::SearchIndex {
58 module: self.module,
59 name: self.name,
60 owner: (self.owner)(),
61 hidden: behaviors.iter().find_map(|behavior| behavior.hides()),
62 fields: behaviors
63 .iter()
64 .map(|behavior| behavior.search_fields())
65 .find(|fields| !fields.is_empty())
66 .unwrap_or_default(),
67 })
68 }
69
70 fn tables(&self) -> Vec<ExpectedTable> {
71 const ID: Field = Field::new("id", Uuid::COLUMNS, false);
72 let behaviors = std::iter::once(&Versioned as &dyn Behavior)
73 .chain(self.behaviors.iter().copied())
74 .flat_map(Behavior::columns);
75 let own = ExpectedTable::new(
76 self.module,
77 self.name,
78 std::iter::once(&ID).chain(behaviors).chain(self.fields),
79 );
80 #[cfg_attr(not(feature = "children"), expect(unused_mut))]
81 let mut tables = vec![own];
82 #[cfg(feature = "children")]
83 tables.extend(self.children.iter().map(|child| {
84 let parent = Field::new(self.name, Uuid::COLUMNS, false);
85 ExpectedTable::new(
86 child.module,
87 child.name,
88 [&ID, &parent].into_iter().chain(child.fields),
89 )
90 }));
91 tables
92 }
93}
94
95impl fmt::Debug for AggregateTable {
96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(formatter, "AggregateTable({}.{})", self.module, self.name)
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct Mismatch {
103 pub table: String,
104 pub kind: MismatchKind,
105}
106
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub enum MismatchKind {
109 MissingTable,
110 MissingColumn {
111 column: String,
112 expected: SqlType,
113 },
114 Type {
115 column: String,
116 expected: SqlType,
117 found: String,
118 },
119 Nullable {
120 column: String,
121 },
122 NotNull {
123 column: String,
124 },
125 Unwritten {
126 column: String,
127 },
128}
129
130impl fmt::Display for Mismatch {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(formatter, "{}: {}", self.table, self.kind)
133 }
134}
135
136impl fmt::Display for MismatchKind {
137 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 Self::MissingTable => write!(formatter, "the table does not exist"),
140 Self::MissingColumn { column, expected } => {
141 write!(formatter, "column `{column}` ({expected:?}) does not exist")
142 }
143 Self::Type {
144 column,
145 expected,
146 found,
147 } => write!(
148 formatter,
149 "column `{column}` is `{found}`, which does not store {expected:?}"
150 ),
151 Self::Nullable { column } => write!(
152 formatter,
153 "column `{column}` accepts null, which the schema never stores; add NOT NULL"
154 ),
155 Self::NotNull { column } => write!(
156 formatter,
157 "column `{column}` is NOT NULL, but the schema's field is optional"
158 ),
159 Self::Unwritten { column } => write!(
160 formatter,
161 "column `{column}` is NOT NULL without a default, and the schema never writes \
162 it, so every insert fails"
163 ),
164 }
165 }
166}
167
168struct ExpectedTable {
169 module: &'static str,
170 name: &'static str,
171 columns: Vec<ExpectedColumn>,
172}
173
174struct ExpectedColumn {
175 name: String,
176 ty: SqlType,
177 nullable: bool,
178}
179
180impl ExpectedTable {
181 fn new<'a>(
182 module: &'static str,
183 name: &'static str,
184 fields: impl Iterator<Item = &'a Field>,
185 ) -> Self {
186 let columns = fields
187 .flat_map(|field| {
188 field.columns.iter().map(|column| ExpectedColumn {
189 name: crate::sql::column_name(field.name, column),
190 ty: column.ty,
191 nullable: field.nullable,
192 })
193 })
194 .collect();
195 Self {
196 module,
197 name,
198 columns,
199 }
200 }
201
202 async fn mismatches(&self, catalog: &dyn Catalog) -> Result<Vec<Mismatch>> {
203 let found = catalog.columns(self.module, self.name).await?;
204 let kinds = if found.is_empty() {
205 vec![MismatchKind::MissingTable]
206 } else {
207 self.compare(&found)
208 };
209 let table = format!("{}.{}", self.module, self.name);
210 Ok(kinds
211 .into_iter()
212 .map(|kind| Mismatch {
213 table: table.clone(),
214 kind,
215 })
216 .collect())
217 }
218
219 fn compare(&self, found: &[CatalogColumn]) -> Vec<MismatchKind> {
220 let unwritten = found
221 .iter()
222 .filter(|column| !column.nullable && !column.filled)
223 .filter(|column| !self.columns.iter().any(|known| known.name == column.name))
224 .map(|column| MismatchKind::Unwritten {
225 column: column.name.clone(),
226 });
227 self.columns
228 .iter()
229 .filter_map(|column| compare(column, found))
230 .chain(unwritten)
231 .collect()
232 }
233}
234
235impl Schema<'_> {
236 pub async fn mismatches<A: Aggregate>(self) -> Result<Vec<Mismatch>> {
237 self.table_mismatches(&[AggregateTable::of::<A>()]).await
238 }
239
240 pub async fn check(self, migrations: &[Migrations]) -> Result<Vec<Mismatch>> {
241 let aggregates: Vec<AggregateTable> = migrations
242 .iter()
243 .flat_map(|module| module.aggregates.iter().copied())
244 .collect();
245 self.table_mismatches(&aggregates).await
246 }
247
248 async fn table_mismatches(self, aggregates: &[AggregateTable]) -> Result<Vec<Mismatch>> {
249 let catalog = self.db.driver().catalog().ok_or_else(|| {
250 Error::internal_message("this database describes no tables to check a schema against")
251 })?;
252 let mut mismatches = Vec::new();
253 for table in aggregates.iter().flat_map(AggregateTable::tables) {
254 mismatches.extend(table.mismatches(catalog).await?);
255 }
256 Ok(mismatches)
257 }
258}
259
260fn compare(expected: &ExpectedColumn, found: &[CatalogColumn]) -> Option<MismatchKind> {
261 let column = expected.name.clone();
262 let Some(actual) = found.iter().find(|actual| actual.name == expected.name) else {
263 return Some(MismatchKind::MissingColumn {
264 column,
265 expected: expected.ty,
266 });
267 };
268 if actual.ty != Some(expected.ty) {
269 return Some(MismatchKind::Type {
270 column,
271 expected: expected.ty,
272 found: actual.type_name.clone(),
273 });
274 }
275 match (expected.nullable, actual.nullable) {
276 (false, true) => Some(MismatchKind::Nullable { column }),
277 (true, false) => Some(MismatchKind::NotNull { column }),
278 _ => None,
279 }
280}