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 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 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 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 #[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#[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
232fn 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
246fn 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#[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#[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 const UNIQUE: &'static [&'static str] = &[];
354 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 #[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#[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#[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#[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 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
501pub(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
559fn 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 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
650struct 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}