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 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#[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
212fn 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
226fn 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#[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#[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 const UNIQUE: &'static [&'static str] = &[];
334 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 #[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#[cfg(feature = "search")]
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#[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#[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#[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 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
491pub(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
549fn 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 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
640struct 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}