Skip to main content

sofie_db/
aggregate.rs

1use std::any::TypeId;
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::ops::Range;
6
7use serde::de::DeserializeOwned;
8use serde_json::Value;
9use sofie_core::{
10    Cast, ColumnReader, ColumnSpec, Error, FieldError, Instant, Result, RowReader, SqlType,
11    SqlValue, Uuid,
12};
13
14use crate::condition::Kind;
15use crate::sql::column_name;
16use crate::type_cache::TypeCache;
17use crate::{Behavior, Column, Condition, Dialect, Event, Id, Selected, Versioned};
18
19#[derive(Clone, Debug)]
20pub struct Meta {
21    id: Uuid,
22    /// `version` first; empty until first saved.
23    pub(crate) stored: Vec<SqlValue>,
24    pub(crate) snapshot: Option<Snapshot>,
25    events: Vec<RaisedEvent>,
26}
27
28#[derive(Clone, Debug)]
29pub(crate) struct Snapshot {
30    pub(crate) stored: Vec<SqlValue>,
31    pub(crate) fields: Vec<SqlValue>,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct RaisedEvent {
36    pub(crate) name: &'static str,
37    pub(crate) payload: Value,
38}
39
40impl RaisedEvent {
41    #[must_use]
42    pub fn name(&self) -> &'static str {
43        self.name
44    }
45
46    #[must_use]
47    pub fn payload(&self) -> &Value {
48        &self.payload
49    }
50}
51
52impl Meta {
53    pub fn new() -> Result<Self> {
54        Ok(Self {
55            id: Uuid::now_v7()?,
56            stored: Vec::new(),
57            snapshot: None,
58            events: Vec::new(),
59        })
60    }
61
62    #[must_use]
63    pub fn id(&self) -> Uuid {
64        self.id
65    }
66
67    #[must_use]
68    pub fn version(&self) -> i64 {
69        crate::behavior::version(self.stored.first())
70    }
71
72    /// Refuses text holding NUL, which the database cannot store.
73    pub fn raise<E: Event>(&mut self, event: E) -> Result<()> {
74        let payload = serde_json::to_value(event)
75            .map_err(|error| Error::internal(format!("serializing event `{}`", E::NAME), error))?;
76        if holds_nul(&payload) {
77            return Err(Error::BadRequest(format!(
78                "event `{}` holds a NUL character, which cannot be stored",
79                E::NAME
80            )));
81        }
82        if crate::event::is_pruned(&payload) {
83            return Err(Error::internal_message(format!(
84                "event `{}` sets `{}`, which only a pruned record's change event carries",
85                E::NAME,
86                crate::event::PRUNED
87            )));
88        }
89        self.events.push(RaisedEvent {
90            name: E::NAME,
91            payload,
92        });
93        Ok(())
94    }
95
96    #[must_use]
97    pub fn events(&self) -> &[RaisedEvent] {
98        &self.events
99    }
100
101    pub(crate) fn written(&mut self, written: Option<Snapshot>) {
102        self.events.clear();
103        if let Some(written) = written {
104            self.stored.clone_from(&written.stored);
105            self.snapshot = Some(written);
106        }
107    }
108}
109
110fn holds_nul(value: &Value) -> bool {
111    match value {
112        Value::String(text) => text.contains('\0'),
113        Value::Array(values) => values.iter().any(holds_nul),
114        Value::Object(fields) => fields
115            .iter()
116            .any(|(name, value)| name.contains('\0') || holds_nul(value)),
117        Value::Null | Value::Bool(_) | Value::Number(_) => false,
118    }
119}
120
121#[derive(Debug)]
122pub struct Field {
123    pub name: &'static str,
124    pub columns: &'static [ColumnSpec],
125    pub nullable: bool,
126    pub parse_text: fn(&str) -> Option<Result<SqlValue, FieldError>>,
127    /// Whether the server can read the stored value back, so an edit left blank keeps it.
128    pub handed_back: fn() -> bool,
129}
130
131impl Field {
132    #[must_use]
133    pub const fn new(name: &'static str, columns: &'static [ColumnSpec], nullable: bool) -> Self {
134        Self {
135            name,
136            columns,
137            nullable,
138            parse_text: |_| None,
139            handed_back: || true,
140        }
141    }
142
143    #[must_use]
144    pub const fn handed_back_by(self, handed_back: fn() -> bool) -> Self {
145        Self {
146            handed_back,
147            ..self
148        }
149    }
150
151    /// A list filter reads the text by the column's type only when this gives `None`, no reader.
152    #[must_use]
153    pub const fn parsed_by(
154        self,
155        parse_text: fn(&str) -> Option<Result<SqlValue, FieldError>>,
156    ) -> Self {
157        Self { parse_text, ..self }
158    }
159
160    #[doc(hidden)]
161    #[must_use]
162    pub const fn stores(fields: &[Self], column: &str) -> bool {
163        let mut index = 0;
164        while index < fields.len() {
165            let field = &fields[index];
166            let mut spec = 0;
167            while spec < field.columns.len() {
168                if names_column(field.name, field.columns[spec].suffix, column) {
169                    return true;
170                }
171                spec += 1;
172            }
173            index += 1;
174        }
175        false
176    }
177}
178
179const fn names_column(name: &str, suffix: &str, column: &str) -> bool {
180    let (name, suffix, column) = (name.as_bytes(), suffix.as_bytes(), column.as_bytes());
181    if name.len() + suffix.len() != column.len() {
182        return false;
183    }
184    let mut index = 0;
185    while index < column.len() {
186        let byte = if index < name.len() {
187            name[index]
188        } else {
189            suffix[index - name.len()]
190        };
191        if byte != column[index] {
192            return false;
193        }
194        index += 1;
195    }
196    true
197}
198
199/// Autoref specialization: `(&&TextOf::<T>(PhantomData)).read(text)` reads JSON text (a string,
200/// else a number or bool) as `T` does when `T` deserializes, and gives nothing otherwise.
201#[doc(hidden)]
202pub struct TextOf<T>(pub PhantomData<T>);
203
204#[doc(hidden)]
205pub trait ReadsText {
206    fn read(&self, text: &str) -> Option<Result<SqlValue, FieldError>>;
207}
208
209impl<T: DeserializeOwned + Cast> ReadsText for &TextOf<T> {
210    fn read(&self, text: &str) -> Option<Result<SqlValue, FieldError>> {
211        Some(read_text::<T>(text))
212    }
213}
214
215fn read_text<T: DeserializeOwned + Cast>(text: &str) -> Result<SqlValue, FieldError> {
216    let value: T = serde_json::from_value(Value::String(text.to_owned())).or_else(|refusal| {
217        match serde_json::from_str::<Value>(text) {
218            Ok(scalar @ (Value::Number(_) | Value::Bool(_))) => {
219                serde_json::from_value(scalar).map_err(|_| refusal_of::<T>(text, &refusal))
220            }
221            _ => Err(refusal_of::<T>(text, &refusal)),
222        }
223    })?;
224    let mut encoded = Vec::with_capacity(1);
225    value.encode(&mut encoded);
226    match (encoded.pop(), encoded.is_empty()) {
227        (Some(single), true) => Ok(single),
228        _ => Err(FieldError::invalid("", "it is not a value of one column")),
229    }
230}
231
232/// serde words a kind mismatch "invalid type: …, expected i64", naming Rust types, and an id
233/// refuses in Uuid's technical words; a type's own refusal, such as a bound or an instant's
234/// repeated wall clock, is kept as the type wrote it.
235fn refusal_of<T: Cast>(text: &str, refusal: &serde_json::Error) -> FieldError {
236    let message = refusal.to_string();
237    match T::COLUMNS {
238        [column] if column.ty == SqlType::Uuid || message.starts_with("invalid type: ") => {
239            expected(column.ty)
240        }
241        [column] if column.ty == SqlType::Timestamp => instant_refusal(text, message),
242        _ => FieldError::invalid("", message),
243    }
244}
245
246/// serde carries only a refusal's words, so an instant's is read again for its key and arguments.
247fn instant_refusal(text: &str, message: String) -> FieldError {
248    match Instant::parse(text) {
249        Err(Error::Invalid(mut errors)) if errors.len() == 1 && errors[0].message == message => {
250            errors.remove(0)
251        }
252        _ => FieldError::invalid("", message),
253    }
254}
255
256pub(crate) fn expected(ty: SqlType) -> FieldError {
257    let (key, message) = match ty {
258        SqlType::Int => ("validation.kind.whole_number", "must be a whole number"),
259        SqlType::Float => ("validation.kind.number", "must be a number"),
260        SqlType::Bool => ("validation.kind.true_or_false", "must be true or false"),
261        SqlType::Uuid => ("validation.kind.id", "must be an id"),
262        SqlType::Date => ("validation.kind.date", "must be a date"),
263        SqlType::DateTime | SqlType::Timestamp => {
264            ("validation.kind.date_time", "must be a date and time")
265        }
266        SqlType::Text | SqlType::Bytes => ("validation.kind.text", "is not valid"),
267    };
268    FieldError::new("", key, message)
269}
270
271#[doc(hidden)]
272pub trait ReadsNoText {
273    fn read(&self, text: &str) -> Option<Result<SqlValue, FieldError>>;
274}
275
276impl<T> ReadsNoText for TextOf<T> {
277    fn read(&self, _text: &str) -> Option<Result<SqlValue, FieldError>> {
278        None
279    }
280}
281
282/// Autoref specialization, as [`TextOf`]: whether a type serializes, asked without a value.
283#[doc(hidden)]
284pub struct JsonProbe<T>(pub PhantomData<T>);
285
286#[doc(hidden)]
287pub trait Serializes {
288    fn serializes(&self) -> bool;
289}
290
291impl<T: serde::Serialize> Serializes for &JsonProbe<T> {
292    fn serializes(&self) -> bool {
293        true
294    }
295}
296
297#[doc(hidden)]
298pub trait SerializesNot {
299    fn serializes(&self) -> bool;
300}
301
302impl<T> SerializesNot for JsonProbe<T> {
303    fn serializes(&self) -> bool {
304        false
305    }
306}
307
308/// Autoref specialization, as [`TextOf`]: a field's JSON when its type serializes.
309#[doc(hidden)]
310pub struct JsonOf<'a, T>(pub &'a T);
311
312#[doc(hidden)]
313pub trait WritesJson {
314    fn json(&self) -> Option<Value>;
315}
316
317impl<T: serde::Serialize> WritesJson for &JsonOf<'_, T> {
318    fn json(&self) -> Option<Value> {
319        serde_json::to_value(self.0).ok()
320    }
321}
322
323#[doc(hidden)]
324pub trait WritesNoJson {
325    fn json(&self) -> Option<Value>;
326}
327
328impl<T> WritesNoJson for JsonOf<'_, T> {
329    fn json(&self) -> Option<Value> {
330        None
331    }
332}
333
334pub trait Source: Sized + Send + 'static {
335    type Base: Aggregate;
336
337    fn columns() -> Vec<Selected>;
338
339    fn column_types() -> Vec<Option<SqlType>>;
340
341    fn decode(row: &mut dyn ColumnReader) -> Result<Self>;
342}
343
344pub trait Aggregate: Sized + Send + Sync + 'static {
345    const NAME: &'static str;
346    const MODULE: &'static str;
347    const FIELDS: &'static [Field];
348    const OWNER: Option<&'static str> = None;
349    const BEHAVIORS: &'static [&'static dyn Behavior] = &[];
350    const CHANGED: Option<&'static str> = None;
351    const PRUNED: Option<&'static str> = None;
352    /// Fields held unique by the database, answered as their own field errors.
353    const UNIQUE: &'static [&'static str] = &[];
354    /// `#[sensitive]` fields, which a list can neither filter nor sort by.
355    const SENSITIVE: &'static [&'static str] = &[];
356    const ID: Column<Self, Id<Self>> = Column::new("id");
357
358    fn meta(&self) -> &Meta;
359    fn meta_mut(&mut self) -> &mut Meta;
360    fn encode(&self, out: &mut Vec<SqlValue>);
361
362    fn decode(meta: Meta, row: &mut dyn ColumnReader) -> Result<Self>;
363
364    fn id(&self) -> Id<Self> {
365        Id::from_uuid(self.meta().id)
366    }
367
368    /// Fields of a sensitive type, left out of its JSON; read on the server so a blank edit keeps them.
369    #[doc(hidden)]
370    fn sensitive_json(&self) -> serde_json::Map<String, Value> {
371        serde_json::Map::new()
372    }
373
374    #[cfg(feature = "children")]
375    const CHILDREN: &'static [&'static crate::ChildTable] = &[];
376
377    fn owner(&self) -> Option<Uuid> {
378        None
379    }
380
381    #[must_use]
382    fn owned_by(owner: Uuid) -> Option<Condition<Self>> {
383        Some(Condition::new(uuid_equals(Self::OWNER?, owner)))
384    }
385
386    #[cfg(feature = "children")]
387    fn children(&self) -> Vec<&dyn crate::ChildRows> {
388        Vec::new()
389    }
390
391    #[cfg(feature = "children")]
392    fn children_mut(&mut self) -> Vec<&mut dyn crate::ChildRows> {
393        Vec::new()
394    }
395
396    #[cfg(feature = "contracts")]
397    fn exposed_fields(&self) -> Option<Result<serde_json::Map<String, Value>>> {
398        None
399    }
400
401    fn refuse_unlisted_moves(&self, loaded: &mut dyn ColumnReader) -> Result<()> {
402        let _ = loaded;
403        Ok(())
404    }
405}
406
407/// The column holding the owner itself, unless the owner is reached through a parent.
408#[cfg(feature = "search-engine")]
409pub(crate) fn direct_owner<A: Aggregate>() -> Option<&'static str> {
410    let column = A::OWNER?;
411    let probe = Uuid::from_u128(0);
412    (A::owned_by(probe)?.kind == uuid_equals(column, probe)).then_some(column)
413}
414
415/// How many of an aggregate's fields are sensitive, by attribute or by type; for the derive.
416#[doc(hidden)]
417#[must_use]
418pub const fn sensitive_count(fields: &[(&str, bool)]) -> usize {
419    let (mut index, mut count) = (0, 0);
420    while index < fields.len() {
421        if fields[index].1 {
422            count += 1;
423        }
424        index += 1;
425    }
426    count
427}
428
429/// The names of the sensitive fields, so `Aggregate::SENSITIVE` stays a constant.
430#[doc(hidden)]
431#[must_use]
432pub const fn sensitive_names<const N: usize>(fields: &[(&'static str, bool)]) -> [&'static str; N] {
433    let mut names = [""; N];
434    let (mut index, mut kept) = (0, 0);
435    while index < fields.len() {
436        if fields[index].1 {
437            names[kept] = fields[index].0;
438            kept += 1;
439        }
440        index += 1;
441    }
442    names
443}
444
445#[cfg(test)]
446mod sensitive_tests {
447    use super::*;
448
449    #[test]
450    fn the_sensitive_names_are_the_marked_fields_in_order() {
451        const FIELDS: &[(&str, bool)] = &[("name", false), ("iban", true), ("pin", true)];
452        const NAMES: [&str; sensitive_count(FIELDS)] = sensitive_names(FIELDS);
453
454        assert_eq!(NAMES, ["iban", "pin"]);
455    }
456}
457
458pub(crate) fn uuid_equals(column: &str, owner: Uuid) -> Kind {
459    Kind::Compare {
460        columns: vec![column.to_owned()],
461        operator: "=",
462        values: vec![SqlValue::Uuid(owner)],
463    }
464}
465
466pub(crate) struct Table {
467    pub(crate) schema: &'static str,
468    pub(crate) name: &'static str,
469    pub(crate) behaviors: Vec<(&'static dyn Behavior, Range<usize>)>,
470    pub(crate) stored: Vec<String>,
471    pub(crate) columns: Vec<String>,
472    /// Of the whole selected row: id, `stored`, `columns`, then child rows.
473    types: Vec<Option<SqlType>>,
474    #[cfg(any(feature = "flags", feature = "search-engine"))]
475    pub(crate) owner: Option<&'static str>,
476}
477
478impl Table {
479    pub(crate) fn qualified(&self, dialect: &dyn Dialect) -> String {
480        dialect.qualified(self.schema, self.name)
481    }
482
483    pub(crate) fn stored_index(&self, column: &str) -> usize {
484        self.stored
485            .iter()
486            .position(|stored| stored == column)
487            .unwrap_or_else(|| panic!("`{}` has no behavior column `{column}`", self.name))
488    }
489
490    pub(crate) fn row_columns(&self) -> impl Iterator<Item = &str> {
491        std::iter::once("id").chain(self.stored.iter().chain(&self.columns).map(String::as_str))
492    }
493
494    pub(crate) fn hiding(&self) -> impl Iterator<Item = &'static str> {
495        self.behaviors
496            .iter()
497            .filter_map(|(behavior, _)| behavior.hides())
498    }
499}
500
501/// Read per query and per decoded row, so each thread keeps its own copy of the lookup and
502/// takes the shared lock only the first time it meets a type.
503pub(crate) fn table<A: Aggregate>() -> &'static Table {
504    static TABLES: TypeCache<TypeId, &'static Table> = TypeCache::new();
505    thread_local! {
506        static SEEN: RefCell<HashMap<TypeId, &'static Table>> = RefCell::default();
507    }
508    let key = TypeId::of::<A>();
509    SEEN.with_borrow_mut(|seen| {
510        *seen.entry(key).or_insert_with(|| {
511            TABLES.get_or_insert_with(key, || Box::leak(Box::new(describe::<A>())))
512        })
513    })
514}
515
516fn describe<A: Aggregate>() -> Table {
517    let mut behaviors = Vec::new();
518    let mut stored = Vec::new();
519    let mut types = vec![Some(SqlType::Uuid)];
520    for behavior in std::iter::once(&Versioned as &dyn Behavior).chain(A::BEHAVIORS.iter().copied())
521    {
522        let start = stored.len();
523        stored.extend(field_columns(behavior.columns()));
524        types.extend(field_types(behavior.columns()));
525        behaviors.push((behavior, start..stored.len()));
526    }
527    types.extend(field_types(A::FIELDS));
528    #[cfg(feature = "children")]
529    types.extend(A::CHILDREN.iter().map(|_| None));
530    let table = Table {
531        schema: A::MODULE,
532        name: A::NAME,
533        behaviors,
534        stored,
535        columns: field_columns(A::FIELDS),
536        types,
537        #[cfg(any(feature = "flags", feature = "search-engine"))]
538        owner: A::OWNER,
539    };
540    refuse_repeated_columns(&table, A::FIELDS);
541    #[cfg(feature = "flags")]
542    refuse_broadcast_defaults(&table);
543    table
544}
545
546#[cfg(feature = "flags")]
547fn refuse_broadcast_defaults(table: &Table) {
548    let mut behaviors = table.behaviors.iter().map(|(behavior, _)| *behavior);
549    let default = std::any::type_name::<crate::Default>();
550    let defaults = behaviors.clone().any(|behavior| behavior.name() == default);
551    assert!(
552        !(defaults && behaviors.any(Behavior::broadcasts)),
553        "`{}` uses both `Default` and `Broadcasts`: the records a new default clears would \
554         change without a change event",
555        table.name
556    );
557}
558
559/// A column stored twice fails only at the first write, as Postgres's 42701. The derive refuses
560/// the columns every aggregate has, but an opt-in behavior's are known only through the dyn
561/// `Behavior::columns`, so the check runs here, when the type is first used.
562fn refuse_repeated_columns(table: &Table, fields: &[Field]) {
563    let by_behaviors = table.behaviors.iter().flat_map(|(behavior, range)| {
564        table.stored[range.clone()]
565            .iter()
566            .map(|column| (column, format!("behavior `{}`", behavior.name())))
567    });
568    let by_fields = fields
569        .iter()
570        .flat_map(|field| {
571            field
572                .columns
573                .iter()
574                .map(|_| format!("field `{}`", field.name))
575        })
576        .zip(&table.columns)
577        .map(|(owner, column)| (column, owner));
578    let mut seen = vec![("id", "the record id".to_owned())];
579    for (column, owner) in by_behaviors.chain(by_fields) {
580        if let Some((_, earlier)) = seen.iter().find(|(name, _)| name == column) {
581            panic!(
582                "`{}` has two columns named `{column}`: {owner} repeats {earlier}; a field \
583                 cannot take the name of a behavior's column",
584                table.name
585            );
586        }
587        seen.push((column, owner));
588    }
589}
590
591fn field_types(fields: &[Field]) -> impl Iterator<Item = Option<SqlType>> {
592    fields
593        .iter()
594        .flat_map(|field| field.columns.iter().map(|column| Some(column.ty)))
595}
596
597pub(crate) fn field_columns(fields: &[Field]) -> Vec<String> {
598    fields
599        .iter()
600        .flat_map(|field| {
601            field
602                .columns
603                .iter()
604                .map(|column| column_name(field.name, column))
605        })
606        .collect()
607}
608
609impl<A: Aggregate> Source for A {
610    type Base = A;
611
612    fn columns() -> Vec<Selected> {
613        let table = table::<A>();
614        let columns = table.row_columns().map(Selected::column);
615        #[cfg(feature = "children")]
616        let columns = columns.chain(A::CHILDREN.iter().map(|child| child.selected()));
617        columns.collect()
618    }
619
620    fn column_types() -> Vec<Option<SqlType>> {
621        table::<A>().types.clone()
622    }
623
624    /// Its stored columns are kept as values, as a save compares them to tell what changed.
625    fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
626        let table = table::<A>();
627        let id = Uuid::decode(row)?;
628        let stored = values(row, table.stored.len())?;
629        let fields = values(row, table.columns.len())?;
630        let meta = Meta {
631            id,
632            stored: stored.clone(),
633            snapshot: None,
634            events: Vec::new(),
635        };
636        let mut rest = FieldsThenRow {
637            fields: RowReader::new(&fields),
638            row,
639        };
640        let mut aggregate = <A as Aggregate>::decode(meta, &mut rest)?;
641        aggregate.meta_mut().snapshot = Some(Snapshot { stored, fields });
642        Ok(aggregate)
643    }
644}
645
646fn values(row: &mut dyn ColumnReader, count: usize) -> Result<Vec<SqlValue>> {
647    (0..count).map(|_| row.value()).collect()
648}
649
650/// Child rows are read from the row itself, so their JSON is not copied into the snapshot.
651struct FieldsThenRow<'a> {
652    fields: RowReader<'a>,
653    row: &'a mut dyn ColumnReader,
654}
655
656impl FieldsThenRow<'_> {
657    fn next(&mut self) -> &mut dyn ColumnReader {
658        if self.fields.remaining() > 0 {
659            return &mut self.fields;
660        }
661        &mut *self.row
662    }
663}
664
665macro_rules! from_next {
666    ($($method:ident -> $type:ty),*) => {
667        $(fn $method(&mut self) -> Result<$type> {
668            self.next().$method()
669        })*
670    };
671}
672
673impl ColumnReader for FieldsThenRow<'_> {
674    fn remaining(&self) -> usize {
675        self.fields.remaining() + self.row.remaining()
676    }
677
678    fn is_null_at(&self, ahead: usize) -> Result<bool> {
679        let fields = self.fields.remaining();
680        if ahead < fields {
681            return self.fields.is_null_at(ahead);
682        }
683        self.row.is_null_at(ahead - fields)
684    }
685
686    from_next!(
687        text -> &str, bytes -> &[u8], skip -> (), bool -> bool, int -> i64, float -> f64, uuid -> Uuid,
688        timestamp -> sofie_core::Timestamp, date -> sofie_core::Date,
689        date_time -> sofie_core::DateTime,
690        json -> Value, value -> SqlValue
691    );
692}
693
694#[must_use]
695pub fn read_columns<A: Aggregate>(fields: &[&str]) -> Vec<Selected> {
696    let table = table::<A>();
697    fields
698        .iter()
699        .flat_map(|name| {
700            if *name == "id" || table.stored.iter().any(|column| column == name) {
701                return vec![Selected::column(*name)];
702            }
703            let field = A::FIELDS
704                .iter()
705                .find(|field| field.name == *name)
706                .unwrap_or_else(|| panic!("`{name}` is not a field of `{}`", A::NAME));
707            field_columns(std::slice::from_ref(field))
708                .into_iter()
709                .map(Selected::column)
710                .collect()
711        })
712        .collect()
713}
714
715#[cfg(test)]
716pub(crate) mod tests {
717    use super::*;
718    use crate::{MessageSql, Migrations, SoftDelete, Timestamps, Uses};
719
720    pub(crate) struct Note {
721        pub(crate) meta: Meta,
722        pub(crate) title: String,
723    }
724
725    impl Aggregate for Note {
726        const NAME: &'static str = "note";
727        const MODULE: &'static str = "notes";
728        const FIELDS: &'static [Field] = &[Field::new("title", String::COLUMNS, false)];
729        const OWNER: Option<&'static str> = Some("id");
730        const BEHAVIORS: &'static [&'static dyn Behavior] = &[&Timestamps, &SoftDelete];
731
732        fn meta(&self) -> &Meta {
733            &self.meta
734        }
735
736        fn meta_mut(&mut self) -> &mut Meta {
737            &mut self.meta
738        }
739
740        fn owner(&self) -> Option<Uuid> {
741            Some(self.meta.id())
742        }
743
744        fn encode(&self, out: &mut Vec<SqlValue>) {
745            self.title.encode(out);
746        }
747
748        fn decode(meta: Meta, row: &mut dyn ColumnReader) -> Result<Self> {
749            Ok(Self {
750                meta,
751                title: String::decode(row)?,
752            })
753        }
754    }
755
756    pub(crate) struct Numbered(pub(crate) &'static MessageSql);
757
758    pub(crate) const NUMBERED: Numbered = Numbered(&MESSAGES);
759
760    pub(crate) const MESSAGES: MessageSql = MessageSql {
761        insert: "INSERT INTO sofie.messages",
762        fan_out: "",
763        fan_out_events: "",
764        claim: "",
765        renew: "",
766        complete: "",
767        retry: "",
768        bury: "",
769        to_job: "",
770        add_schedule: "",
771        fire_schedule: "",
772        notify: None,
773    };
774
775    impl Dialect for Numbered {
776        fn push_placeholder(&self, sql: &mut String, index: usize) {
777            sql.push('$');
778            sql.push_str(&index.to_string());
779        }
780
781        fn migrations_setup(&self) -> &'static str {
782            unimplemented!()
783        }
784
785        fn migrations_lock(&self) -> &'static str {
786            unimplemented!()
787        }
788
789        fn enter_schema(&self, _schema: &str) -> String {
790            unimplemented!()
791        }
792
793        fn framework_migrations(&self) -> Migrations {
794            unimplemented!()
795        }
796
797        fn messages(&self) -> &'static MessageSql {
798            self.0
799        }
800    }
801
802    impl Uses<Timestamps> for Note {}
803    impl Uses<SoftDelete> for Note {}
804
805    struct Clash {
806        meta: Meta,
807    }
808
809    impl Aggregate for Clash {
810        const NAME: &'static str = "clash";
811        const MODULE: &'static str = "notes";
812        const FIELDS: &'static [Field] = &[Field::new("deleted_at", String::COLUMNS, false)];
813        const BEHAVIORS: &'static [&'static dyn Behavior] = &[&Timestamps, &SoftDelete];
814
815        fn meta(&self) -> &Meta {
816            &self.meta
817        }
818
819        fn meta_mut(&mut self) -> &mut Meta {
820            &mut self.meta
821        }
822
823        fn encode(&self, _out: &mut Vec<SqlValue>) {}
824
825        fn decode(meta: Meta, _row: &mut dyn ColumnReader) -> Result<Self> {
826            Ok(Self { meta })
827        }
828    }
829
830    fn names(selection: Vec<Selected>) -> Vec<String> {
831        selection
832            .into_iter()
833            .map(|selected| selected.render(table::<Note>(), &NUMBERED, None, &mut Vec::new()))
834            .map(|quoted| quoted.trim_matches('"').to_owned())
835            .collect()
836    }
837
838    #[test]
839    fn an_aggregate_selects_its_id_behavior_columns_then_fields() {
840        assert_eq!(
841            names(<Note as Source>::columns()),
842            [
843                "id",
844                "version",
845                "created_at",
846                "updated_at",
847                "deleted_at",
848                "title"
849            ]
850        );
851    }
852
853    #[test]
854    fn an_aggregate_expects_the_type_of_each_column_it_selects() {
855        let timestamp = Some(SqlType::Timestamp);
856
857        assert_eq!(
858            <Note as Source>::column_types(),
859            [
860                Some(SqlType::Uuid),
861                Some(SqlType::Int),
862                timestamp,
863                timestamp,
864                timestamp,
865                Some(SqlType::Text)
866            ]
867        );
868    }
869
870    #[test]
871    fn every_thread_reads_the_one_description_of_a_type() {
872        let address = || std::ptr::from_ref(table::<Note>()) as usize;
873
874        let elsewhere = std::thread::spawn(address).join().unwrap();
875
876        assert_eq!(address(), elsewhere);
877        assert_eq!(address(), address());
878    }
879
880    #[test]
881    fn what_follows_the_fields_is_read_from_the_row_itself() {
882        let fields = [SqlValue::Int(1)];
883        let children = [SqlValue::Text("child".to_owned())];
884        let mut row = RowReader::new(&children);
885        let mut reader = FieldsThenRow {
886            fields: RowReader::new(&fields),
887            row: &mut row,
888        };
889
890        assert_eq!(reader.remaining(), 2);
891        assert_eq!(reader.int().unwrap(), 1);
892        assert!(!reader.is_null().unwrap());
893        assert_eq!(reader.text().unwrap(), "child");
894        assert!(reader.skip().is_err());
895    }
896
897    #[test]
898    fn a_read_model_selects_fields_and_behavior_columns_of_its_base() {
899        assert_eq!(
900            names(read_columns::<Note>(&["id", "title", "created_at"])),
901            ["id", "title", "created_at"]
902        );
903    }
904
905    #[test]
906    #[should_panic(expected = "`colour` is not a field of `note`")]
907    fn a_read_model_cannot_select_what_its_base_lacks() {
908        let _ = read_columns::<Note>(&["colour"]);
909    }
910
911    struct Renamed {
912        title: String,
913    }
914
915    impl crate::Input for Renamed {
916        fn from_json(_value: Value) -> Result<Self> {
917            unimplemented!("only raised")
918        }
919    }
920
921    impl serde::Serialize for Renamed {
922        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
923            serializer.collect_map([("titles", [&self.title])])
924        }
925    }
926
927    impl Event for Renamed {
928        const NAME: &'static str = "notes.Renamed";
929    }
930
931    struct Masked;
932
933    impl crate::Input for Masked {
934        fn from_json(_: Value) -> Result<Self> {
935            Ok(Self)
936        }
937    }
938
939    impl serde::Serialize for Masked {
940        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
941            serializer.collect_map([("sofie.pruned", true)])
942        }
943    }
944
945    impl Event for Masked {
946        const NAME: &'static str = "notes.Masked";
947    }
948
949    #[test]
950    fn an_app_event_cannot_pass_as_a_pruned_record() {
951        let mut meta = Meta::new().unwrap();
952
953        let refused = meta.raise(Masked);
954
955        assert!(
956            matches!(&refused, Err(Error::Internal { context, .. }) if context.contains("notes.Masked")),
957            "{refused:?}"
958        );
959    }
960
961    #[test]
962    fn an_event_holding_nul_is_refused_when_raised() {
963        let mut meta = Meta::new().unwrap();
964
965        let refused = meta.raise(Renamed {
966            title: "a\0b".to_owned(),
967        });
968
969        assert!(
970            matches!(&refused, Err(Error::BadRequest(message)) if message.contains("notes.Renamed")),
971            "{refused:?}"
972        );
973        assert_eq!(meta.events(), []);
974        meta.raise(Renamed {
975            title: "ab".to_owned(),
976        })
977        .unwrap();
978        assert_eq!(meta.events().len(), 1);
979    }
980
981    #[test]
982    #[should_panic(
983        expected = "`clash` has two columns named `deleted_at`: field `deleted_at` repeats \
984                    behavior `sofie_db::soft_delete::SoftDelete`"
985    )]
986    fn a_field_cannot_take_an_opt_in_behaviors_column() {
987        let _ = <Clash as Source>::columns();
988    }
989}