Skip to main content

distributed/read_model/
mod.rs

1//! Read Models - storage-backed projections and read-optimized views.
2//!
3//! Relational models stage explicit row mutations:
4//!
5//! ```ignore
6//! use distributed::read_model::{ReadModelWritePlanBuilder, ReadModelWritePlanCommitExt};
7//!
8//! let mut read_models = ReadModelWritePlanBuilder::new();
9//! read_models.upsert(&player)?;
10//! read_models.upsert_related(&player, "weapons", &weapon)?;
11//! repo.read_models(read_models).commit(&mut aggregate).await?;
12//! ```
13//!
14//! Persistent repositories expose the staging shape through
15//! `ReadModelWritePlanCommitExt::read_models`, returning a future
16//! from `commit`.
17//!
18//! Distributed projectors can commit a write plan directly against a read-model
19//! adapter. Projection handlers should make those writes idempotent so bus
20//! retries can safely replay the same message:
21//!
22//! ```ignore
23//! let mut read_models = ReadModelWritePlanBuilder::new();
24//! read_models.upsert(&view)?;
25//! let outcome = read_models.commit(&read_store)?;
26//! ```
27
28mod capabilities;
29pub mod change;
30pub(crate) mod in_memory;
31mod load;
32mod plan;
33mod workspace;
34
35pub use change::ReadModelChange;
36
37use serde::{de::DeserializeOwned, Serialize};
38
39use crate::table::{RowKey, RowValues, TableSchema, TableStoreError};
40
41/// Trait implemented by the derive macro for read-model identity metadata.
42pub trait ReadModel: Serialize + DeserializeOwned + Clone + Send + Sync {
43    /// The declared storage name for this read model type.
44    const COLLECTION: &'static str;
45
46    /// Returns the unique identifier for this read model instance.
47    fn id(&self) -> &str;
48}
49
50/// A versioned wrapper around read model data for optimistic concurrency control.
51#[derive(Debug, Clone, PartialEq)]
52pub struct Versioned<T> {
53    pub data: T,
54    pub version: u64,
55}
56
57/// Opt-in trait for table-mapped relational read models.
58pub trait RelationalReadModel: Clone + Send + Sync + Sized {
59    /// The model's schema. Static because a model's schema is fixed at compile
60    /// time; the derive macro backs this with a `LazyLock` so staging mutations
61    /// never rebuilds or clones schema metadata.
62    fn schema() -> &'static TableSchema;
63
64    /// Stable declared read-model identity.
65    ///
66    /// This is `#[readmodel(name = "...")]` when present, otherwise the Rust
67    /// type name stored on [`TableSchema::model_name`]. It is not the SQL
68    /// table, process owner, or GraphQL field.
69    fn read_model_id() -> &'static str {
70        Self::schema().model_name.as_str()
71    }
72
73    fn primary_key(&self) -> Result<RowKey, TableStoreError>;
74    fn to_row(&self) -> Result<RowValues, TableStoreError>;
75    fn from_row(row: RowValues) -> Result<Self, TableStoreError>;
76}
77
78/// Relationship hydration hooks generated for table-mapped read models.
79pub trait RelationalReadModelIncludes: RelationalReadModel {
80    fn hydrate_include(
81        &mut self,
82        include: &str,
83        rows: Vec<RowValues>,
84    ) -> Result<(), TableStoreError>;
85
86    fn include_rows(&self, include: &str) -> Result<Vec<RowValues>, TableStoreError>;
87
88    /// Schema of the model targeted by the named relationship.
89    fn include_target_schema(include: &str) -> Result<&'static TableSchema, TableStoreError>;
90}
91
92pub use capabilities::ReadModelQueryCapabilities;
93pub use in_memory::InMemoryReadModelStore;
94pub use load::{
95    ReadModelIncludeRows, ReadModelLoadBuilder, ReadModelLoadGraph, ReadModelLoadRequest,
96};
97pub use plan::ReadModelWritePlanBuilder;
98pub use workspace::{ReadModelWorkspace, ReadModelWorkspaceExt};