Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
398e78f
feat: unify read-model ORM write plans
patrickleet May 23, 2026
55dc48f
fix: harden read-model derive metadata parsing
patrickleet May 23, 2026
21cefef
fix: avoid relational row key fingerprint collisions
patrickleet May 23, 2026
10932b3
fix: validate read-model relationship foreign keys
patrickleet May 23, 2026
6b1f073
feat: add read-model helper attributes
patrickleet May 23, 2026
5f23048
test: organize distributed read model services
patrickleet May 23, 2026
532c6c1
feat: add tracked read model relationship includes
patrickleet May 23, 2026
e46e851
fix: address read model review feedback
patrickleet May 23, 2026
6313605
fix: guard primary keys in row patches
patrickleet May 23, 2026
b81a895
test: assert failed sparse insert leaves no row
patrickleet May 23, 2026
3271575
feat: delete removed has_many children on save_changes
patrickleet May 23, 2026
2da5196
test: distributed relational read-model examples with fulfillment saga
patrickleet May 24, 2026
c73f330
Refine distributed read model services
patrickleet May 24, 2026
7b719b6
Align distributed saga event flow
patrickleet May 24, 2026
58c6516
fix: store binary read-model rows as bytes
patrickleet May 24, 2026
856d9e3
fix: reject duplicate read-model relationship attrs
patrickleet May 24, 2026
743f4d9
fix: fail document plans on unsupported mutations
patrickleet May 24, 2026
04154ff
fix: reject duplicate read-model index names
patrickleet May 24, 2026
1d48d4a
fix: fingerprint document read-model keys
patrickleet May 24, 2026
5dc2b77
fix: release queued read-model locks once
patrickleet May 24, 2026
ac49477
fix: fail board projection on malformed event versions
patrickleet May 24, 2026
d7a6b41
fix: reject non-positive product creation prices
patrickleet May 24, 2026
bc714c8
fix: validate distributed inventory quantities
patrickleet May 24, 2026
aa848da
fix: guard distributed order line edits
patrickleet May 24, 2026
bb0d6af
fix: fail order projection on malformed event versions
patrickleet May 24, 2026
8b1ce83
test: assert idempotency not-found variants
patrickleet May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ microsvc::serve(service.clone(), "0.0.0.0:3000").await?;

## Read Models

Read models are denormalized views derived from event-sourced aggregates. They give you fast, purpose-built query models shaped for your UI or API consumers.
Read models are query-optimized projections derived from aggregates, event records, or published messages. Document read models store a whole view in a document payload column; normalized relational read models use table metadata plus `ReadModelSession` write plans.

### Defining a Read Model

Expand All @@ -1198,9 +1198,9 @@ use serde::{Deserialize, Serialize};
use sourced_rust::ReadModel;

#[derive(Clone, Debug, Serialize, Deserialize, ReadModel)]
#[readmodel(collection = "game_views")]
#[collection("game_views")]
pub struct GameView {
#[readmodel(id)]
#[id]
pub id: String,
pub player_name: String,
pub score: i32,
Expand All @@ -1226,9 +1226,31 @@ repo.readmodel(&view).commit(&mut game)?;
// Return `view` to the client — it reflects the committed state
```

This is a deliberate CAP theorem tradeoff: you're choosing **consistency** over **partition tolerance**. The read model is in sync with the aggregate when the repository implements `TransactionalCommit` and can write both in the same transaction boundary. For cross-service or cross-database views, use the eventually consistent outbox pattern instead.
For relational read models, stage structured row mutations in a session:

See [`docs/read-models.md`](docs/read-models.md) for the full guide, including eventually consistent projections, `QueuedReadModelStore`, and a decision flowchart.
```rust
use sourced_rust::{ReadModelSession, ReadModelSessionCommitExt};

let mut read_models = ReadModelSession::new();
read_models.save(&player_view)?;
read_models.save_related(&player_view, "weapons", &weapon_view)?;

repo.read_models(read_models).commit(&mut game)?;
```

Distributed projectors can commit the same session shape directly against a read-model adapter and mark messages processed in the same adapter transaction:

```rust
let mut read_models = ReadModelSession::new();
read_models.document(&view)?.mark_processed("game-view-projector", event_id);
let outcome = read_models.commit(&read_store)?;
```

This is a deliberate consistency tradeoff. The read model is in sync with the aggregate only when the repository implements `TransactionalCommit` and can write both in the same transaction boundary. For cross-service or cross-database views, use the eventually consistent outbox/projector pattern instead.

Bomberman `BoardView` remains a document-row example backed by a whole-view payload, not a normalized relational ORM example.

See [`docs/read-models.md`](docs/read-models.md) for the full guide, including relational metadata, document rows, session commits, schema bootstrap, distributed idempotency, and non-goals.

## Snapshots

Expand Down
Loading
Loading