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