1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3use std::marker::PhantomData;
4
5use serde::Serialize;
6
7use crate::repository::{ReadModelWritePlanStore, RelationalReadModelQueryStore};
8
9use super::{
10 ReadModelError, ReadModelSchema, RelationalReadModel, RelationalReadModelIncludes,
11 RelationshipDef, RelationshipKind, RowKey, RowValue, RowValues, Versioned,
12};
13
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
16pub enum ExpectedVersion {
17 #[default]
19 Any,
20 Exact(u64),
22 NotExists,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum RowWriteMode {
29 Insert,
30 Upsert,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum PatchMode {
36 UpdateExisting,
37 InsertMissing,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct ReadModelAdapterCapabilities {
43 pub relational_rows: bool,
44 pub sparse_patches: bool,
45 pub deletes: bool,
46}
47
48impl Default for ReadModelAdapterCapabilities {
49 fn default() -> Self {
50 Self {
51 relational_rows: true,
52 sparse_patches: true,
53 deletes: true,
54 }
55 }
56}
57
58#[derive(Clone, Debug, Default, PartialEq, Eq)]
72pub struct ReadModelCommitOutcome;
73
74impl ReadModelCommitOutcome {
75 pub fn applied() -> Self {
77 Self
78 }
79
80 pub fn was_applied(&self) -> bool {
82 true
83 }
84}
85
86#[derive(Clone, Debug, PartialEq)]
88pub struct ReadModelLoadRequest {
89 pub schema: ReadModelSchema,
90 pub key: RowKey,
91 pub includes: Vec<String>,
92}
93
94impl ReadModelLoadRequest {
95 pub fn validate_for_query_capabilities(
96 &self,
97 capabilities: &ReadModelQueryCapabilities,
98 ) -> Result<(), ReadModelError> {
99 if !self.includes.is_empty() && !capabilities.relationship_includes {
100 return Err(ReadModelError::Metadata(
101 "read-model adapter does not support relationship includes".into(),
102 ));
103 }
104
105 Ok(())
106 }
107}
108
109#[derive(Clone, Debug, Default, PartialEq, Eq)]
111pub struct ReadModelQueryCapabilities {
112 pub relationship_includes: bool,
113}
114
115impl ReadModelQueryCapabilities {
116 pub fn relationship_includes() -> Self {
117 Self {
118 relationship_includes: true,
119 }
120 }
121}
122
123#[derive(Clone, Debug, PartialEq)]
125pub struct ReadModelIncludeRows {
126 pub relationship: RelationshipDef,
127 pub target_schema: ReadModelSchema,
128 pub rows: Vec<Versioned<RowValues>>,
129}
130
131#[derive(Clone, Debug, Default, PartialEq)]
133pub struct ReadModelLoadGraph {
134 pub root: Option<Versioned<RowValues>>,
135 pub includes: BTreeMap<String, ReadModelIncludeRows>,
136}
137
138#[derive(Clone, Debug, Default, PartialEq)]
140pub struct RowPatch {
141 values: RowValues,
142}
143
144impl RowPatch {
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn set(mut self, column: impl Into<String>, value: RowValue) -> Self {
150 self.values.insert(column, value);
151 self
152 }
153
154 pub fn set_serde<T: Serialize + ?Sized>(
155 mut self,
156 column: impl Into<String>,
157 value: &T,
158 ) -> Result<Self, ReadModelError> {
159 self.values.insert_serde(column, value)?;
160 Ok(self)
161 }
162
163 pub fn get(&self, column: &str) -> Option<&RowValue> {
164 self.values.get(column)
165 }
166
167 pub fn iter(&self) -> impl Iterator<Item = (&str, &RowValue)> {
168 self.values.iter()
169 }
170
171 pub fn is_empty(&self) -> bool {
172 self.values.is_empty()
173 }
174
175 pub fn into_values(self) -> RowValues {
176 self.values
177 }
178}
179
180#[derive(Clone, Debug, PartialEq)]
182pub struct RowMutation {
183 pub schema: ReadModelSchema,
184 pub key: RowKey,
185 pub values: RowValues,
186 pub expected_version: ExpectedVersion,
187 pub mode: RowWriteMode,
188}
189
190#[derive(Clone, Debug, PartialEq)]
192pub struct PatchRowMutation {
193 pub schema: ReadModelSchema,
194 pub key: RowKey,
195 pub patch: RowPatch,
196 pub expected_version: ExpectedVersion,
197 pub mode: PatchMode,
198}
199
200#[derive(Clone, Debug, PartialEq)]
202pub struct DeleteRowMutation {
203 pub schema: ReadModelSchema,
204 pub key: RowKey,
205 pub expected_version: ExpectedVersion,
206}
207
208#[derive(Clone, Debug, PartialEq)]
210pub enum ReadModelMutation {
211 UpsertRow(RowMutation),
212 PatchRow(PatchRowMutation),
213 DeleteRow(DeleteRowMutation),
214}
215
216impl ReadModelMutation {
217 pub fn table_name(&self) -> &str {
218 match self {
219 ReadModelMutation::UpsertRow(mutation) => mutation.schema.table_name.as_str(),
220 ReadModelMutation::PatchRow(mutation) => mutation.schema.table_name.as_str(),
221 ReadModelMutation::DeleteRow(mutation) => mutation.schema.table_name.as_str(),
222 }
223 }
224
225 pub fn lock_key(&self) -> String {
226 match self {
227 ReadModelMutation::UpsertRow(mutation) => format!(
228 "{}:{}",
229 mutation.schema.table_name,
230 key_fingerprint(&mutation.key)
231 ),
232 ReadModelMutation::PatchRow(mutation) => format!(
233 "{}:{}",
234 mutation.schema.table_name,
235 key_fingerprint(&mutation.key)
236 ),
237 ReadModelMutation::DeleteRow(mutation) => format!(
238 "{}:{}",
239 mutation.schema.table_name,
240 key_fingerprint(&mutation.key)
241 ),
242 }
243 }
244
245 fn operation_rank(&self) -> u8 {
246 match self {
247 ReadModelMutation::UpsertRow(_) => 1,
248 ReadModelMutation::PatchRow(_) => 2,
249 ReadModelMutation::DeleteRow(_) => 3,
250 }
251 }
252
253 fn schema(&self) -> &ReadModelSchema {
254 match self {
255 ReadModelMutation::UpsertRow(mutation) => &mutation.schema,
256 ReadModelMutation::PatchRow(mutation) => &mutation.schema,
257 ReadModelMutation::DeleteRow(mutation) => &mutation.schema,
258 }
259 }
260
261 fn depends_on_table(&self, table_name: &str) -> bool {
262 let schema = self.schema();
263 schema
264 .foreign_keys
265 .iter()
266 .any(|foreign_key| foreign_key.table == table_name)
267 || schema.columns.iter().any(|column| {
268 column
269 .foreign_key
270 .as_ref()
271 .is_some_and(|foreign_key| foreign_key.table == table_name)
272 })
273 }
274
275 fn dependency_order(&self, other: &Self) -> Option<Ordering> {
276 let self_depends_on_other = self.depends_on_table(other.table_name());
277 let other_depends_on_self = other.depends_on_table(self.table_name());
278
279 match (self_depends_on_other, other_depends_on_self) {
280 (true, false) if self.operation_rank() == 3 && other.operation_rank() == 3 => {
281 Some(Ordering::Less)
282 }
283 (true, false) => Some(Ordering::Greater),
284 (false, true) if self.operation_rank() == 3 && other.operation_rank() == 3 => {
285 Some(Ordering::Greater)
286 }
287 (false, true) => Some(Ordering::Less),
288 _ => None,
289 }
290 }
291
292 fn sort_key(&self) -> String {
293 match self {
294 ReadModelMutation::UpsertRow(mutation) => format!(
295 "1|{}|{}",
296 mutation.schema.table_name,
297 key_fingerprint(&mutation.key)
298 ),
299 ReadModelMutation::PatchRow(mutation) => format!(
300 "2|{}|{}",
301 mutation.schema.table_name,
302 key_fingerprint(&mutation.key)
303 ),
304 ReadModelMutation::DeleteRow(mutation) => format!(
305 "3|{}|{}",
306 mutation.schema.table_name,
307 key_fingerprint(&mutation.key)
308 ),
309 }
310 }
311}
312
313#[derive(Clone, Debug, Default, PartialEq)]
315pub struct ReadModelWritePlan {
316 pub mutations: Vec<ReadModelMutation>,
317}
318
319impl ReadModelWritePlan {
320 pub fn new(mutations: Vec<ReadModelMutation>) -> Self {
321 Self { mutations }
322 }
323
324 pub fn is_empty(&self) -> bool {
325 self.mutations.is_empty()
326 }
327
328 pub fn validate(&self) -> Result<(), ReadModelError> {
329 self.validate_for(&ReadModelAdapterCapabilities::default())
330 }
331
332 pub fn validate_for(
333 &self,
334 capabilities: &ReadModelAdapterCapabilities,
335 ) -> Result<(), ReadModelError> {
336 for mutation in &self.mutations {
337 match mutation {
338 ReadModelMutation::UpsertRow(mutation) => {
339 if !capabilities.relational_rows {
340 return Err(ReadModelError::Metadata(
341 "read-model adapter does not support relational row writes".into(),
342 ));
343 }
344 validate_row_mutation(mutation)?;
345 }
346 ReadModelMutation::PatchRow(mutation) => {
347 if !capabilities.relational_rows || !capabilities.sparse_patches {
348 return Err(ReadModelError::Metadata(
349 "read-model adapter does not support sparse row patches".into(),
350 ));
351 }
352 validate_patch_mutation(mutation)?;
353 }
354 ReadModelMutation::DeleteRow(mutation) => {
355 if !capabilities.relational_rows || !capabilities.deletes {
356 return Err(ReadModelError::Metadata(
357 "read-model adapter does not support row deletes".into(),
358 ));
359 }
360 validate_delete_mutation(mutation)?;
361 }
362 }
363 }
364
365 Ok(())
366 }
367}
368
369#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
370struct RowIdentity {
371 table_name: String,
372 key: String,
373}
374
375#[derive(Clone, Debug)]
376struct StagedMutation {
377 sequence: u64,
378 mutation: ReadModelMutation,
379}
380
381#[derive(Clone, Debug, Default)]
383pub struct ReadModelWritePlanBuilder {
384 mutations: Vec<StagedMutation>,
385 expected_versions: BTreeMap<RowIdentity, u64>,
386 next_sequence: u64,
387}
388
389impl ReadModelWritePlanBuilder {
390 pub fn new() -> Self {
391 Self::default()
392 }
393
394 pub fn is_empty(&self) -> bool {
395 self.mutations.is_empty()
396 }
397
398 pub fn load<M>(&self, key: RowKey) -> Result<ReadModelLoadRequest, ReadModelError>
399 where
400 M: RelationalReadModel,
401 {
402 self.load_with::<M, Vec<String>, String>(key, Vec::new())
403 }
404
405 pub fn load_with<M, I, S>(
406 &self,
407 key: RowKey,
408 includes: I,
409 ) -> Result<ReadModelLoadRequest, ReadModelError>
410 where
411 M: RelationalReadModel,
412 I: IntoIterator<Item = S>,
413 S: Into<String>,
414 {
415 let schema = validated_schema::<M>()?;
416 validate_key(&schema, &key)?;
417 let includes: Vec<String> = includes.into_iter().map(Into::into).collect();
418 for include in &includes {
419 if !schema
420 .relationships
421 .iter()
422 .any(|relationship| relationship.field_name == *include)
423 {
424 return Err(ReadModelError::Metadata(format!(
425 "read model `{}` has no relationship `{}`",
426 schema.model_name, include
427 )));
428 }
429 }
430
431 Ok(ReadModelLoadRequest {
432 schema,
433 key,
434 includes,
435 })
436 }
437
438 pub fn track_loaded<M>(&mut self, versioned: &Versioned<M>) -> Result<&mut Self, ReadModelError>
439 where
440 M: RelationalReadModel,
441 {
442 self.expect_version::<M>(versioned.data.primary_key()?, versioned.version)
443 }
444
445 pub fn expect_version<M>(
446 &mut self,
447 key: RowKey,
448 expected_version: u64,
449 ) -> Result<&mut Self, ReadModelError>
450 where
451 M: RelationalReadModel,
452 {
453 let schema = validated_schema::<M>()?;
454 validate_key(&schema, &key)?;
455 validate_expected_version(&ExpectedVersion::Exact(expected_version), &schema)?;
456 self.expected_versions.insert(
457 RowIdentity {
458 table_name: schema.table_name,
459 key: key_fingerprint(&key),
460 },
461 expected_version,
462 );
463 Ok(self)
464 }
465
466 pub fn insert<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
467 where
468 M: RelationalReadModel,
469 {
470 self.stage_full_row(
471 model,
472 RowWriteMode::Insert,
473 Some(ExpectedVersion::NotExists),
474 )
475 }
476
477 pub fn upsert<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
478 where
479 M: RelationalReadModel,
480 {
481 self.stage_full_row(model, RowWriteMode::Upsert, None)
482 }
483
484 pub fn insert_related<P, C>(
485 &mut self,
486 parent: &P,
487 relationship_field: &str,
488 child: &C,
489 ) -> Result<&mut Self, ReadModelError>
490 where
491 P: RelationalReadModel,
492 C: RelationalReadModel,
493 {
494 self.stage_related_row(parent, relationship_field, child, RowWriteMode::Insert)
495 }
496
497 pub fn upsert_related<P, C>(
498 &mut self,
499 parent: &P,
500 relationship_field: &str,
501 child: &C,
502 ) -> Result<&mut Self, ReadModelError>
503 where
504 P: RelationalReadModel,
505 C: RelationalReadModel,
506 {
507 self.stage_related_row(parent, relationship_field, child, RowWriteMode::Upsert)
508 }
509
510 pub fn patch<M>(&mut self, key: RowKey, patch: RowPatch) -> Result<&mut Self, ReadModelError>
511 where
512 M: RelationalReadModel,
513 {
514 self.stage_patch::<M>(key, patch, PatchMode::UpdateExisting)
515 }
516
517 pub fn upsert_patch<M>(
518 &mut self,
519 key: RowKey,
520 patch: RowPatch,
521 ) -> Result<&mut Self, ReadModelError>
522 where
523 M: RelationalReadModel,
524 {
525 self.stage_patch::<M>(key, patch, PatchMode::InsertMissing)
526 }
527
528 pub fn delete<M>(&mut self, key: RowKey) -> Result<&mut Self, ReadModelError>
529 where
530 M: RelationalReadModel,
531 {
532 let schema = validated_schema::<M>()?;
533 validate_key(&schema, &key)?;
534 let expected_version = self.expected_for(&schema, &key);
535 let mutation = DeleteRowMutation {
536 schema,
537 key,
538 expected_version,
539 };
540 self.push(ReadModelMutation::DeleteRow(mutation));
541 Ok(self)
542 }
543
544 pub fn delete_model<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
545 where
546 M: RelationalReadModel,
547 {
548 self.delete::<M>(model.primary_key()?)
549 }
550
551 pub fn into_write_plan(self) -> Result<ReadModelWritePlan, ReadModelError> {
552 let mut mutations = self.mutations;
553 mutations.sort_by(|left, right| {
554 left.mutation
555 .operation_rank()
556 .cmp(&right.mutation.operation_rank())
557 .then_with(|| {
558 left.mutation
559 .dependency_order(&right.mutation)
560 .unwrap_or(Ordering::Equal)
561 })
562 .then_with(|| left.mutation.sort_key().cmp(&right.mutation.sort_key()))
563 .then(left.sequence.cmp(&right.sequence))
564 });
565 let mutations = mutations
566 .into_iter()
567 .map(|staged| staged.mutation)
568 .collect::<Vec<_>>();
569 let plan = ReadModelWritePlan::new(mutations);
570 plan.validate()?;
571 Ok(plan)
572 }
573
574 pub async fn commit<S>(self, store: &S) -> Result<ReadModelCommitOutcome, ReadModelError>
575 where
576 S: ReadModelWritePlanStore + ?Sized,
577 {
578 store.commit_write_plan(self.into_write_plan()?).await
579 }
580
581 fn stage_full_row<M>(
582 &mut self,
583 model: &M,
584 mode: RowWriteMode,
585 expected_version: Option<ExpectedVersion>,
586 ) -> Result<&mut Self, ReadModelError>
587 where
588 M: RelationalReadModel,
589 {
590 let schema = validated_schema::<M>()?;
591 let key = model.primary_key()?;
592 let values = model.to_row()?;
593 validate_key(&schema, &key)?;
594 let expected_version = expected_version.unwrap_or_else(|| self.expected_for(&schema, &key));
595 let mutation = RowMutation {
596 schema,
597 key,
598 values,
599 expected_version,
600 mode,
601 };
602 self.push(ReadModelMutation::UpsertRow(mutation));
603 Ok(self)
604 }
605
606 fn stage_related_row<P, C>(
607 &mut self,
608 parent: &P,
609 relationship_field: &str,
610 child: &C,
611 mode: RowWriteMode,
612 ) -> Result<&mut Self, ReadModelError>
613 where
614 P: RelationalReadModel,
615 C: RelationalReadModel,
616 {
617 let parent_schema = validated_schema::<P>()?;
618 let child_schema = validated_schema::<C>()?;
619 let relationship = parent_schema
620 .relationships
621 .iter()
622 .find(|relationship| relationship.field_name == relationship_field)
623 .ok_or_else(|| {
624 ReadModelError::Metadata(format!(
625 "read model `{}` has no relationship `{}`",
626 parent_schema.model_name, relationship_field
627 ))
628 })?;
629
630 if relationship.target_model != child_schema.model_name {
631 return Err(ReadModelError::Metadata(format!(
632 "relationship `{}` targets `{}`, not `{}`",
633 relationship.field_name, relationship.target_model, child_schema.model_name
634 )));
635 }
636
637 let parent_row = parent.to_row()?;
638 let mut child_row = child.to_row()?;
639 populate_delegated_relationship_values(
640 &parent_schema,
641 &parent_row,
642 relationship,
643 &child_schema,
644 &mut child_row,
645 )?;
646 let key = key_from_row(&child_schema, &child_row)?;
647 let expected_version = match mode {
648 RowWriteMode::Insert => ExpectedVersion::NotExists,
649 RowWriteMode::Upsert => self.expected_for(&child_schema, &key),
650 };
651 let mutation = RowMutation {
652 schema: child_schema,
653 key,
654 values: child_row,
655 expected_version,
656 mode,
657 };
658 self.push(ReadModelMutation::UpsertRow(mutation));
659 Ok(self)
660 }
661
662 fn stage_patch<M>(
663 &mut self,
664 key: RowKey,
665 patch: RowPatch,
666 mode: PatchMode,
667 ) -> Result<&mut Self, ReadModelError>
668 where
669 M: RelationalReadModel,
670 {
671 let schema = validated_schema::<M>()?;
672 validate_key(&schema, &key)?;
673 let expected_version = self.expected_for(&schema, &key);
674 let mutation = PatchRowMutation {
675 schema,
676 key,
677 patch,
678 expected_version,
679 mode,
680 };
681 self.push(ReadModelMutation::PatchRow(mutation));
682 Ok(self)
683 }
684
685 fn push(&mut self, mutation: ReadModelMutation) {
686 let sequence = self.next_sequence;
687 self.next_sequence = self.next_sequence.saturating_add(1);
688 self.mutations.push(StagedMutation { sequence, mutation });
689 }
690
691 fn expected_for(&self, schema: &ReadModelSchema, key: &RowKey) -> ExpectedVersion {
692 self.expected_versions
693 .get(&RowIdentity {
694 table_name: schema.table_name.clone(),
695 key: key_fingerprint(key),
696 })
697 .copied()
698 .map(ExpectedVersion::Exact)
699 .unwrap_or(ExpectedVersion::Any)
700 }
701}
702
703#[derive(Clone, Debug)]
704struct TrackedRowBaseline {
705 key: RowKey,
706 row: RowValues,
707 version: u64,
708}
709
710#[derive(Clone, Debug)]
711struct TrackedIncludeBaseline {
712 relationship: RelationshipDef,
713 target_schema: ReadModelSchema,
714 rows: BTreeMap<String, TrackedRowBaseline>,
715}
716
717#[derive(Clone, Debug)]
718struct TrackedModelBaseline {
719 root_schema: ReadModelSchema,
720 root_key: RowKey,
721 root_row: RowValues,
722 root_version: u64,
723 includes: BTreeMap<String, TrackedIncludeBaseline>,
724}
725
726const INITIAL_TRACKED_ROW_VERSION: u64 = 1;
727
728pub struct ReadModelWorkspace<'a, S> {
733 store: &'a S,
734 writes: ReadModelWritePlanBuilder,
735 baselines: Vec<TrackedModelBaseline>,
736}
737
738impl<'a, S> ReadModelWorkspace<'a, S> {
739 pub fn new(store: &'a S) -> Self {
740 Self {
741 store,
742 writes: ReadModelWritePlanBuilder::new(),
743 baselines: Vec::new(),
744 }
745 }
746
747 pub fn is_empty(&self) -> bool {
748 self.writes.is_empty()
749 }
750
751 pub fn sync<M>(&mut self, model: M) -> Result<&mut Self, ReadModelError>
752 where
753 M: RelationalReadModel + RelationalReadModelIncludes,
754 {
755 let schema = validated_schema::<M>()?;
756 let key = model.primary_key()?;
757 validate_key(&schema, &key)?;
758 let identity = RowIdentity {
759 table_name: schema.table_name.clone(),
760 key: key_fingerprint(&key),
761 };
762 let baseline_index = self
763 .baselines
764 .iter()
765 .position(|baseline| {
766 baseline.root_schema.table_name == identity.table_name
767 && key_fingerprint(&baseline.root_key) == identity.key
768 })
769 .ok_or_else(|| {
770 ReadModelError::Metadata(format!(
771 "read model `{}` has no tracked baseline for sync",
772 schema.model_name
773 ))
774 })?;
775 let baseline = self.baselines[baseline_index].clone();
776 let current_row = model.to_row()?;
777
778 let root_version = self
779 .stage_row_diff(
780 schema.clone(),
781 key.clone(),
782 &baseline.root_row,
783 ¤t_row,
784 baseline.root_version,
785 )?
786 .unwrap_or(baseline.root_version);
787
788 let mut refreshed_includes = BTreeMap::new();
789 for (include_name, include) in &baseline.includes {
790 let current_rows = model.include_rows(include_name)?;
791 let refreshed_include =
792 self.stage_include_changes(&schema, ¤t_row, include, current_rows)?;
793 refreshed_includes.insert(include_name.clone(), refreshed_include);
794 }
795
796 self.writes.expected_versions.insert(identity, root_version);
797 self.baselines[baseline_index] = TrackedModelBaseline {
798 root_schema: schema,
799 root_key: key,
800 root_row: current_row,
801 root_version,
802 includes: refreshed_includes,
803 };
804
805 Ok(self)
806 }
807
808 pub fn upsert<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
809 where
810 M: RelationalReadModel,
811 {
812 self.writes.upsert(model)?;
813 Ok(self)
814 }
815
816 pub fn insert<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
817 where
818 M: RelationalReadModel,
819 {
820 self.writes.insert(model)?;
821 Ok(self)
822 }
823
824 pub fn upsert_related<P, C>(
825 &mut self,
826 parent: &P,
827 relationship_field: &str,
828 child: &C,
829 ) -> Result<&mut Self, ReadModelError>
830 where
831 P: RelationalReadModel,
832 C: RelationalReadModel,
833 {
834 self.writes
835 .upsert_related(parent, relationship_field, child)?;
836 Ok(self)
837 }
838
839 pub fn insert_related<P, C>(
840 &mut self,
841 parent: &P,
842 relationship_field: &str,
843 child: &C,
844 ) -> Result<&mut Self, ReadModelError>
845 where
846 P: RelationalReadModel,
847 C: RelationalReadModel,
848 {
849 self.writes
850 .insert_related(parent, relationship_field, child)?;
851 Ok(self)
852 }
853
854 pub fn patch<M>(&mut self, key: RowKey, patch: RowPatch) -> Result<&mut Self, ReadModelError>
855 where
856 M: RelationalReadModel,
857 {
858 self.writes.patch::<M>(key, patch)?;
859 Ok(self)
860 }
861
862 pub fn upsert_patch<M>(
863 &mut self,
864 key: RowKey,
865 patch: RowPatch,
866 ) -> Result<&mut Self, ReadModelError>
867 where
868 M: RelationalReadModel,
869 {
870 self.writes.upsert_patch::<M>(key, patch)?;
871 Ok(self)
872 }
873
874 pub fn delete<M>(&mut self, key: RowKey) -> Result<&mut Self, ReadModelError>
875 where
876 M: RelationalReadModel,
877 {
878 self.writes.delete::<M>(key)?;
879 Ok(self)
880 }
881
882 pub fn delete_model<M>(&mut self, model: &M) -> Result<&mut Self, ReadModelError>
883 where
884 M: RelationalReadModel,
885 {
886 self.writes.delete_model(model)?;
887 Ok(self)
888 }
889
890 pub fn into_write_plan(self) -> Result<ReadModelWritePlan, ReadModelError> {
891 self.writes.into_write_plan()
892 }
893
894 fn track_graph(
895 &mut self,
896 schema: ReadModelSchema,
897 root: Versioned<RowValues>,
898 includes: BTreeMap<String, ReadModelIncludeRows>,
899 ) -> Result<(), ReadModelError> {
900 let root_key = key_from_row(&schema, &root.data)?;
901 let root_identity = RowIdentity {
902 table_name: schema.table_name.clone(),
903 key: key_fingerprint(&root_key),
904 };
905 self.writes
906 .expected_versions
907 .insert(root_identity, root.version);
908
909 let mut tracked_includes = BTreeMap::new();
910 for (include_name, include_rows) in includes {
911 let mut rows = BTreeMap::new();
912 for row in include_rows.rows {
913 let key = key_from_row(&include_rows.target_schema, &row.data)?;
914 rows.insert(
915 key_fingerprint(&key),
916 TrackedRowBaseline {
917 key,
918 row: row.data,
919 version: row.version,
920 },
921 );
922 }
923 tracked_includes.insert(
924 include_name,
925 TrackedIncludeBaseline {
926 relationship: include_rows.relationship,
927 target_schema: include_rows.target_schema,
928 rows,
929 },
930 );
931 }
932
933 let fingerprint = key_fingerprint(&root_key);
934 self.baselines.retain(|baseline| {
935 baseline.root_schema.table_name != schema.table_name
936 || key_fingerprint(&baseline.root_key) != fingerprint
937 });
938 self.baselines.push(TrackedModelBaseline {
939 root_schema: schema,
940 root_key,
941 root_row: root.data,
942 root_version: root.version,
943 includes: tracked_includes,
944 });
945 Ok(())
946 }
947
948 fn stage_include_changes(
949 &mut self,
950 root_schema: &ReadModelSchema,
951 root_row: &RowValues,
952 baseline: &TrackedIncludeBaseline,
953 current_rows: Vec<RowValues>,
954 ) -> Result<TrackedIncludeBaseline, ReadModelError> {
955 if matches!(baseline.relationship.kind, RelationshipKind::BelongsTo)
956 && current_rows.len() > 1
957 {
958 return Err(ReadModelError::Metadata(format!(
959 "belongs_to relationship `{}` can sync at most one related row",
960 baseline.relationship.field_name
961 )));
962 }
963
964 let mut current_fingerprints = BTreeSet::new();
965 let mut refreshed_rows = BTreeMap::new();
966 for mut current_row in current_rows {
967 match baseline.relationship.kind {
968 RelationshipKind::HasMany => populate_delegated_relationship_values(
969 root_schema,
970 root_row,
971 &baseline.relationship,
972 &baseline.target_schema,
973 &mut current_row,
974 )?,
975 RelationshipKind::BelongsTo => {}
976 RelationshipKind::ManyToMany => {
977 return Err(ReadModelError::Metadata(format!(
978 "many-to-many relationship `{}` includes are not supported yet",
979 baseline.relationship.field_name
980 )));
981 }
982 }
983
984 let key = key_from_row(&baseline.target_schema, ¤t_row)?;
985 let fingerprint = key_fingerprint(&key);
986 current_fingerprints.insert(fingerprint.clone());
987 if let Some(loaded) = baseline.rows.get(&fingerprint) {
988 let version = self
989 .stage_row_diff(
990 baseline.target_schema.clone(),
991 loaded.key.clone(),
992 &loaded.row,
993 ¤t_row,
994 loaded.version,
995 )?
996 .unwrap_or(loaded.version);
997 refreshed_rows.insert(
998 fingerprint,
999 TrackedRowBaseline {
1000 key,
1001 row: current_row,
1002 version,
1003 },
1004 );
1005 } else {
1006 self.stage_upsert_row(
1007 baseline.target_schema.clone(),
1008 key.clone(),
1009 current_row.clone(),
1010 )?;
1011 refreshed_rows.insert(
1012 fingerprint,
1013 TrackedRowBaseline {
1014 key,
1015 row: current_row,
1016 version: INITIAL_TRACKED_ROW_VERSION,
1017 },
1018 );
1019 }
1020 }
1021
1022 if matches!(baseline.relationship.kind, RelationshipKind::HasMany) {
1026 for (fingerprint, loaded) in &baseline.rows {
1027 if !current_fingerprints.contains(fingerprint) {
1028 self.stage_delete_row(
1029 baseline.target_schema.clone(),
1030 loaded.key.clone(),
1031 loaded.version,
1032 )?;
1033 }
1034 }
1035 } else {
1036 for (fingerprint, loaded) in &baseline.rows {
1037 if !current_fingerprints.contains(fingerprint) {
1038 refreshed_rows.insert(fingerprint.clone(), loaded.clone());
1039 }
1040 }
1041 }
1042
1043 Ok(TrackedIncludeBaseline {
1044 relationship: baseline.relationship.clone(),
1045 target_schema: baseline.target_schema.clone(),
1046 rows: refreshed_rows,
1047 })
1048 }
1049
1050 fn stage_row_diff(
1051 &mut self,
1052 schema: ReadModelSchema,
1053 key: RowKey,
1054 before: &RowValues,
1055 after: &RowValues,
1056 expected_version: u64,
1057 ) -> Result<Option<u64>, ReadModelError> {
1058 let patch = diff_rows(before, after);
1059 if patch.is_empty() {
1060 return Ok(None);
1061 }
1062 let next_version = next_tracked_version(&schema, &key, expected_version)?;
1063
1064 let mutation = PatchRowMutation {
1065 schema,
1066 key,
1067 patch,
1068 expected_version: ExpectedVersion::Exact(expected_version),
1069 mode: PatchMode::UpdateExisting,
1070 };
1071 validate_patch_mutation(&mutation)?;
1072 self.writes.push(ReadModelMutation::PatchRow(mutation));
1073 Ok(Some(next_version))
1074 }
1075
1076 fn stage_upsert_row(
1077 &mut self,
1078 schema: ReadModelSchema,
1079 key: RowKey,
1080 values: RowValues,
1081 ) -> Result<(), ReadModelError> {
1082 let mutation = RowMutation {
1083 schema,
1084 key,
1085 values,
1086 expected_version: ExpectedVersion::Any,
1087 mode: RowWriteMode::Upsert,
1088 };
1089 validate_row_mutation(&mutation)?;
1090 self.writes.push(ReadModelMutation::UpsertRow(mutation));
1091 Ok(())
1092 }
1093
1094 fn stage_delete_row(
1095 &mut self,
1096 schema: ReadModelSchema,
1097 key: RowKey,
1098 expected_version: u64,
1099 ) -> Result<(), ReadModelError> {
1100 let mutation = DeleteRowMutation {
1101 schema,
1102 key,
1103 expected_version: ExpectedVersion::Exact(expected_version),
1104 };
1105 validate_delete_mutation(&mutation)?;
1106 self.writes.push(ReadModelMutation::DeleteRow(mutation));
1107 Ok(())
1108 }
1109}
1110
1111impl<'a, S> ReadModelWorkspace<'a, S>
1112where
1113 S: ReadModelWritePlanStore + RelationalReadModelQueryStore,
1114{
1115 pub fn load<M>(&mut self, key: RowKey) -> ReadModelLoadBuilder<'_, 'a, S, M>
1117 where
1118 M: RelationalReadModel + RelationalReadModelIncludes,
1119 {
1120 ReadModelLoadBuilder {
1121 unit: self,
1122 key,
1123 includes: Vec::new(),
1124 _marker: PhantomData,
1125 }
1126 }
1127
1128 pub async fn commit(self) -> Result<ReadModelCommitOutcome, ReadModelError> {
1130 self.writes.commit(self.store).await
1131 }
1132}
1133
1134pub struct ReadModelLoadBuilder<'workspace, 'store, S, M>
1136where
1137 S: ReadModelWritePlanStore + RelationalReadModelQueryStore,
1138{
1139 unit: &'workspace mut ReadModelWorkspace<'store, S>,
1140 key: RowKey,
1141 includes: Vec<String>,
1142 _marker: PhantomData<M>,
1143}
1144
1145impl<'workspace, 'store, S, M> ReadModelLoadBuilder<'workspace, 'store, S, M>
1146where
1147 S: ReadModelWritePlanStore + RelationalReadModelQueryStore,
1148 M: RelationalReadModel + RelationalReadModelIncludes,
1149{
1150 pub fn include(mut self, relationship: impl Into<String>) -> Self {
1151 self.includes.push(relationship.into());
1152 self
1153 }
1154
1155 pub async fn one(self) -> Result<Option<Versioned<M>>, ReadModelError> {
1156 let request = self
1157 .unit
1158 .writes
1159 .load_with::<M, _, _>(self.key, self.includes)?;
1160 let graph = self.unit.store.load_graph(request.clone()).await?;
1161 let Some(root) = graph.root else {
1162 return Ok(None);
1163 };
1164
1165 let mut model = M::from_row(root.data.clone())?;
1166 for (include_name, include_rows) in &graph.includes {
1167 let rows = include_rows
1168 .rows
1169 .iter()
1170 .map(|row| row.data.clone())
1171 .collect::<Vec<_>>();
1172 model.hydrate_include(include_name, rows)?;
1173 }
1174
1175 self.unit
1176 .track_graph(request.schema, root.clone(), graph.includes)?;
1177 Ok(Some(Versioned {
1178 data: model,
1179 version: root.version,
1180 }))
1181 }
1182}
1183
1184pub trait ReadModelWorkspaceExt:
1186 ReadModelWritePlanStore + RelationalReadModelQueryStore + Sized
1187{
1188 fn workspace(&self) -> ReadModelWorkspace<'_, Self> {
1189 ReadModelWorkspace::new(self)
1190 }
1191}
1192
1193impl<S> ReadModelWorkspaceExt for S where S: ReadModelWritePlanStore + RelationalReadModelQueryStore {}
1194
1195fn diff_rows(before: &RowValues, after: &RowValues) -> RowPatch {
1196 let mut patch = RowPatch::new();
1197 for (column, value) in after.iter() {
1198 if before.get(column) != Some(value) {
1199 patch = patch.set(column.to_string(), value.clone());
1200 }
1201 }
1202 patch
1203}
1204
1205fn next_tracked_version(
1206 schema: &ReadModelSchema,
1207 key: &RowKey,
1208 current_version: u64,
1209) -> Result<u64, ReadModelError> {
1210 current_version.checked_add(1).ok_or_else(|| {
1211 ReadModelError::Storage(format!(
1212 "read model version overflow for {}:{}",
1213 schema.table_name,
1214 key_fingerprint(key)
1215 ))
1216 })
1217}
1218
1219fn validated_schema<M>() -> Result<ReadModelSchema, ReadModelError>
1220where
1221 M: RelationalReadModel,
1222{
1223 let schema = M::schema();
1224 schema.validate()?;
1225 Ok(schema)
1226}
1227
1228fn validate_row_mutation(mutation: &RowMutation) -> Result<(), ReadModelError> {
1229 mutation.schema.validate()?;
1230 validate_key(&mutation.schema, &mutation.key)?;
1231 validate_expected_version(&mutation.expected_version, &mutation.schema)?;
1232 validate_row_values(&mutation.schema, &mutation.values, true)
1233}
1234
1235fn validate_patch_mutation(mutation: &PatchRowMutation) -> Result<(), ReadModelError> {
1236 mutation.schema.validate()?;
1237 validate_key(&mutation.schema, &mutation.key)?;
1238 validate_expected_version(&mutation.expected_version, &mutation.schema)?;
1239 if mutation.patch.is_empty() {
1240 return Err(ReadModelError::Metadata(format!(
1241 "read model `{}` patch must set at least one column",
1242 mutation.schema.model_name
1243 )));
1244 }
1245 validate_row_values(&mutation.schema, &mutation.patch.values, false)
1246}
1247
1248fn validate_delete_mutation(mutation: &DeleteRowMutation) -> Result<(), ReadModelError> {
1249 mutation.schema.validate()?;
1250 validate_key(&mutation.schema, &mutation.key)?;
1251 validate_expected_version(&mutation.expected_version, &mutation.schema)
1252}
1253
1254fn validate_expected_version(
1255 expected_version: &ExpectedVersion,
1256 schema: &ReadModelSchema,
1257) -> Result<(), ReadModelError> {
1258 if matches!(expected_version, ExpectedVersion::Exact(0)) {
1259 return Err(ReadModelError::Metadata(format!(
1260 "read model `{}` expected version must be greater than zero",
1261 schema.model_name
1262 )));
1263 }
1264 Ok(())
1265}
1266
1267pub(crate) fn validate_key(schema: &ReadModelSchema, key: &RowKey) -> Result<(), ReadModelError> {
1268 if key.is_empty() {
1269 return Err(ReadModelError::Metadata(format!(
1270 "read model `{}` row key cannot be empty",
1271 schema.model_name
1272 )));
1273 }
1274
1275 for column in &schema.primary_key.columns {
1276 match key.get(column) {
1277 Some(RowValue::Null) => {
1278 return Err(ReadModelError::Metadata(format!(
1279 "read model `{}` primary-key column `{}` cannot be null",
1280 schema.model_name, column
1281 )));
1282 }
1283 Some(_) => {}
1284 None => {
1285 return Err(ReadModelError::Metadata(format!(
1286 "read model `{}` row key is missing primary-key column `{}`",
1287 schema.model_name, column
1288 )));
1289 }
1290 }
1291 }
1292
1293 for (column, _) in key.iter() {
1294 if !schema.primary_key.columns.iter().any(|key| key == column) {
1295 return Err(ReadModelError::Metadata(format!(
1296 "read model `{}` row key includes non-primary-key column `{}`",
1297 schema.model_name, column
1298 )));
1299 }
1300 }
1301
1302 Ok(())
1303}
1304
1305pub(crate) fn validate_row_values(
1306 schema: &ReadModelSchema,
1307 values: &RowValues,
1308 full_row: bool,
1309) -> Result<(), ReadModelError> {
1310 for (column_name, value) in values.iter() {
1311 let column = schema
1312 .columns
1313 .iter()
1314 .find(|column| column.column_name == column_name)
1315 .ok_or_else(|| {
1316 ReadModelError::Metadata(format!(
1317 "read model `{}` write references missing column `{}`",
1318 schema.model_name, column_name
1319 ))
1320 })?;
1321
1322 if matches!(value, RowValue::Null) {
1323 if column.primary_key {
1324 return Err(ReadModelError::Metadata(format!(
1325 "read model `{}` primary-key column `{}` cannot be null",
1326 schema.model_name, column.column_name
1327 )));
1328 }
1329 if !column.nullable && !column.has_default {
1330 return Err(ReadModelError::Metadata(format!(
1331 "read model `{}` column `{}` is not nullable",
1332 schema.model_name, column.column_name
1333 )));
1334 }
1335 }
1336 }
1337
1338 if full_row {
1339 for column in &schema.columns {
1340 if column.skipped || column.nullable || column.has_default {
1341 continue;
1342 }
1343 if !values.contains_key(&column.column_name) {
1344 return Err(ReadModelError::Metadata(format!(
1345 "read model `{}` row is missing required column `{}`",
1346 schema.model_name, column.column_name
1347 )));
1348 }
1349 }
1350
1351 for column in schema
1352 .columns
1353 .iter()
1354 .filter(|column| column.delegated_from.is_some())
1355 {
1356 match values.get(&column.column_name) {
1357 Some(RowValue::Null) | None => {
1358 return Err(ReadModelError::Metadata(format!(
1359 "read model `{}` delegated column `{}` must be populated before write",
1360 schema.model_name, column.column_name
1361 )));
1362 }
1363 Some(_) => {}
1364 }
1365 }
1366 }
1367
1368 Ok(())
1369}
1370
1371pub(crate) fn key_from_row(
1372 schema: &ReadModelSchema,
1373 row: &RowValues,
1374) -> Result<RowKey, ReadModelError> {
1375 let mut key = RowKey::default();
1376 for column in &schema.primary_key.columns {
1377 let value = row.get(column).cloned().ok_or_else(|| {
1378 ReadModelError::Metadata(format!(
1379 "read model `{}` row is missing primary-key column `{}`",
1380 schema.model_name, column
1381 ))
1382 })?;
1383 key.insert(column.clone(), value);
1384 }
1385 validate_key(schema, &key)?;
1386 Ok(key)
1387}
1388
1389fn populate_delegated_relationship_values(
1390 parent_schema: &ReadModelSchema,
1391 parent_row: &RowValues,
1392 relationship: &RelationshipDef,
1393 child_schema: &ReadModelSchema,
1394 child_row: &mut RowValues,
1395) -> Result<(), ReadModelError> {
1396 let mut populated = 0;
1397 for column in child_schema
1398 .columns
1399 .iter()
1400 .filter(|column| column.delegated_from.is_some())
1401 {
1402 let delegated_from = column.delegated_from.as_deref().unwrap_or_default();
1403 let Some((model_name, source_name)) = delegated_from.split_once('.') else {
1404 return Err(ReadModelError::Metadata(format!(
1405 "read model `{}` delegated column `{}` has invalid source `{}`",
1406 child_schema.model_name, column.column_name, delegated_from
1407 )));
1408 };
1409
1410 if model_name != parent_schema.model_name {
1411 continue;
1412 }
1413
1414 let source_column = column_name_for(parent_schema, source_name).ok_or_else(|| {
1415 ReadModelError::Metadata(format!(
1416 "read model `{}` delegated source `{}` is not a parent column",
1417 child_schema.model_name, delegated_from
1418 ))
1419 })?;
1420 let value = parent_row.get(&source_column).cloned().ok_or_else(|| {
1421 ReadModelError::Metadata(format!(
1422 "read model `{}` parent row is missing delegated source column `{}`",
1423 parent_schema.model_name, source_column
1424 ))
1425 })?;
1426 child_row.insert(column.column_name.clone(), value);
1427 populated += 1;
1428 }
1429
1430 if populated == 0 {
1431 let foreign_key = relationship.foreign_key.as_deref().ok_or_else(|| {
1432 ReadModelError::Metadata(format!(
1433 "read model `{}` relationship `{}` must declare a foreign key",
1434 parent_schema.model_name, relationship.field_name
1435 ))
1436 })?;
1437 let child_column = column_name_for(child_schema, foreign_key).ok_or_else(|| {
1438 ReadModelError::Metadata(format!(
1439 "relationship `{}` foreign key `{}` is not a child column",
1440 relationship.field_name, foreign_key
1441 ))
1442 })?;
1443 let parent_column = column_name_for(parent_schema, foreign_key)
1444 .or_else(|| parent_schema.primary_key.columns.first().cloned())
1445 .ok_or_else(|| {
1446 ReadModelError::Metadata(format!(
1447 "relationship `{}` has no parent key to delegate",
1448 relationship.field_name
1449 ))
1450 })?;
1451 let value = parent_row.get(&parent_column).cloned().ok_or_else(|| {
1452 ReadModelError::Metadata(format!(
1453 "read model `{}` parent row is missing relationship key `{}`",
1454 parent_schema.model_name, parent_column
1455 ))
1456 })?;
1457 child_row.insert(child_column, value);
1458 }
1459
1460 Ok(())
1461}
1462
1463pub(crate) fn column_name_for(schema: &ReadModelSchema, field_or_column: &str) -> Option<String> {
1464 schema
1465 .columns
1466 .iter()
1467 .find(|column| {
1468 column.field_name == field_or_column || column.column_name == field_or_column
1469 })
1470 .map(|column| column.column_name.clone())
1471}
1472
1473pub(crate) fn key_fingerprint(key: &RowKey) -> String {
1474 let mut fingerprint = String::new();
1475 for (column, value) in key.iter() {
1476 push_fingerprint_part(&mut fingerprint, column);
1477 push_fingerprint_part(&mut fingerprint, &value_fingerprint(value));
1478 }
1479 fingerprint
1480}
1481
1482fn push_fingerprint_part(fingerprint: &mut String, part: &str) {
1483 fingerprint.push_str(&part.len().to_string());
1484 fingerprint.push(':');
1485 fingerprint.push_str(part);
1486 fingerprint.push(';');
1487}
1488
1489fn value_fingerprint(value: &RowValue) -> String {
1490 match value {
1491 RowValue::Null => "null".into(),
1492 RowValue::Bool(value) => format!("bool:{value}"),
1493 RowValue::I64(value) => format!("i64:{value}"),
1494 RowValue::U64(value) => format!("u64:{value}"),
1495 RowValue::F64(value) => format!("f64:{value:?}"),
1496 RowValue::String(value) => format!("string:{value}"),
1497 RowValue::Bytes(value) => format!("bytes:{value:?}"),
1498 RowValue::Json(value) => format!(
1499 "json:{}",
1500 serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
1501 ),
1502 }
1503}
1504
1505#[cfg(test)]
1506mod tests {
1507 use super::*;
1508
1509 #[test]
1510 fn key_fingerprint_distinguishes_delimiter_collisions() {
1511 let left = RowKey::new([
1512 ("a", RowValue::String("x,b=y".into())),
1513 ("b", RowValue::String("z".into())),
1514 ]);
1515 let right = RowKey::new([
1516 ("a", RowValue::String("x".into())),
1517 ("b", RowValue::String("y,b=z".into())),
1518 ]);
1519
1520 assert_ne!(key_fingerprint(&left), key_fingerprint(&right));
1521 }
1522
1523 #[test]
1524 fn key_fingerprint_distinguishes_row_value_types() {
1525 let integer = RowKey::new([("id", RowValue::I64(1))]);
1526 let string = RowKey::new([("id", RowValue::String("1".into()))]);
1527
1528 assert_ne!(key_fingerprint(&integer), key_fingerprint(&string));
1529 }
1530}