Skip to main content

es_entity/
snapshot.rs

1//! Core types for entity snapshotting.
2//!
3//! A repo with `#[es_repo(snapshot)]` persists, in the same statement as the
4//! events it appends, an author-defined state `S` (the fold of events
5//! `1..=k`) into `<tbl>_snapshots`. Hydration then loads `S` plus only the
6//! events after `k` in one round trip.
7
8use chrono::{DateTime, Utc};
9
10/// Matches no stored snapshot; binds on full-history loads and is
11/// `NoSnapshot`'s fingerprint.
12pub const NO_SNAPSHOT_FINGERPRINT: i64 = i64::MIN;
13
14/// Implemented by `#[derive(EsSnapshot)]` for user state types and by hand for
15/// [`NoSnapshot`].
16pub trait EsSnapshot:
17    serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Send + Sync + 'static
18{
19    /// `false` only for [`NoSnapshot`].
20    const IS_SNAPSHOT: bool;
21    const FINGERPRINT: i64;
22    #[doc(hidden)]
23    const HAS_FORGETTABLE_FIELDS: bool;
24    /// JSON keys (field idents) of the top-level `Forgettable<T>` fields.
25    #[doc(hidden)]
26    const FORGETTABLE_JSON_FIELDS: &'static [&'static str];
27    #[doc(hidden)]
28    fn extract_forgettable_payloads(&self) -> Option<serde_json::Value>;
29    #[doc(hidden)]
30    fn forget_forgettable_payloads(&mut self);
31}
32
33/// The default `S` of `EntityEvents<E, S>`: this entity has no snapshot.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum NoSnapshot {}
36
37impl serde::Serialize for NoSnapshot {
38    fn serialize<Ser: serde::Serializer>(&self, _serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
39        match *self {}
40    }
41}
42
43impl<'de> serde::Deserialize<'de> for NoSnapshot {
44    fn deserialize<D: serde::Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
45        Err(serde::de::Error::custom(
46            "NoSnapshot cannot be deserialized",
47        ))
48    }
49}
50
51impl EsSnapshot for NoSnapshot {
52    const IS_SNAPSHOT: bool = false;
53    const FINGERPRINT: i64 = NO_SNAPSHOT_FINGERPRINT;
54    const HAS_FORGETTABLE_FIELDS: bool = false;
55    const FORGETTABLE_JSON_FIELDS: &'static [&'static str] = &[];
56
57    fn extract_forgettable_payloads(&self) -> Option<serde_json::Value> {
58        match *self {}
59    }
60
61    fn forget_forgettable_payloads(&mut self) {
62        match *self {}
63    }
64}
65
66/// A persisted snapshot as loaded: the state plus the metadata of the events
67/// it summarises.
68pub struct SnapshotRecord<S> {
69    /// Events `1..=sequence` are folded into `state`.
70    pub sequence: usize,
71    pub state: S,
72    pub recorded_at: DateTime<Utc>,
73    /// `recorded_at` of event 1 — keeps `entity_first_persisted_at()` O(1).
74    pub first_recorded_at: DateTime<Utc>,
75}
76
77impl<S: std::fmt::Debug> std::fmt::Debug for SnapshotRecord<S> {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("SnapshotRecord")
80            .field("sequence", &self.sequence)
81            .field("state", &self.state)
82            .field("recorded_at", &self.recorded_at)
83            .field("first_recorded_at", &self.first_recorded_at)
84            .finish()
85    }
86}
87
88impl<S: Clone> Clone for SnapshotRecord<S> {
89    fn clone(&self) -> Self {
90        Self {
91            sequence: self.sequence,
92            state: self.state.clone(),
93            recorded_at: self.recorded_at,
94            first_recorded_at: self.first_recorded_at,
95        }
96    }
97}
98
99impl<S: PartialEq> PartialEq for SnapshotRecord<S> {
100    fn eq(&self, other: &Self) -> bool {
101        self.sequence == other.sequence
102            && self.state == other.state
103            && self.recorded_at == other.recorded_at
104            && self.first_recorded_at == other.first_recorded_at
105    }
106}
107
108/// One item of an entity's replay. The snapshot, when present, is the FIRST
109/// item in forward order and the LAST item in reverse order. Deliberately
110/// exhaustive — do not add `#[non_exhaustive]`.
111#[derive(Debug, Clone, Copy)]
112pub enum Replay<'a, E, S> {
113    Snapshot(&'a S),
114    Event(&'a E),
115}
116
117/// Normalises `idempotency_guard!` input: a plain `&E` stream (today's call
118/// sites) and a `replay()` stream both become [`Replay`]. Do not implement
119/// for other types.
120pub trait IntoReplay<'a, E, S> {
121    fn into_replay(self) -> Replay<'a, E, S>;
122}
123
124impl<'a, E> IntoReplay<'a, E, NoSnapshot> for &'a E {
125    fn into_replay(self) -> Replay<'a, E, NoSnapshot> {
126        Replay::Event(self)
127    }
128}
129
130impl<'a, E, S> IntoReplay<'a, E, S> for Replay<'a, E, S> {
131    fn into_replay(self) -> Self {
132        self
133    }
134}
135
136/// Reached by `idempotency_guard!` when a stream yields `Replay::Snapshot`
137/// but the guard has no `snapshot:` clause. Only [`NoSnapshot`] (and
138/// `SnapshotRecord<NoSnapshot>`) implement it, so on a real snapshot this is
139/// a compile error with the message below.
140#[diagnostic::on_unimplemented(
141    message = "`idempotency_guard!` over a snapshotted event stream needs a `snapshot: <pattern> [if <guard>]` clause",
142    label = "this guard iterates `replay()` of an entity whose snapshot type is `{Self}`",
143    note = "add `snapshot: s if <what the snapshot says about this operation>`; use `snapshot: _ if false` if the snapshot can never imply the operation was applied"
144)]
145pub trait GuardWithoutSnapshotClause {
146    fn reached<R>(&self) -> R;
147}
148
149impl GuardWithoutSnapshotClause for NoSnapshot {
150    fn reached<R>(&self) -> R {
151        match *self {}
152    }
153}
154
155impl GuardWithoutSnapshotClause for SnapshotRecord<NoSnapshot> {
156    fn reached<R>(&self) -> R {
157        match self.state {}
158    }
159}
160
161/// Implemented by every entity whose repo enables `snapshot`.
162///
163/// The `#[derive(EsRepo)]` macro checks that the entity's own `Snapshot`
164/// associated type and the repo's `snapshot` flag agree, so widening the
165/// repo without also widening the entity's `EntityEvents<E, S>` (or the
166/// reverse) does not compile:
167///
168/// ```ignore
169/// impl HeadSnapshot for Meter {
170///     fn capture(&self) -> Option<MeterSnapshot> {
171///         (self.events.tail_len() >= 4).then(|| MeterSnapshot { .. })
172///     }
173/// }
174/// ```
175///
176/// ```compile_fail
177/// use es_entity::*;
178/// use serde::{Serialize, Deserialize};
179/// # fn main() {}
180/// # es_entity::entity_id! { SnapGuardMeterId }
181/// # #[derive(EsEvent, Debug, Clone, Serialize, Deserialize)]
182/// # #[serde(tag = "type", rename_all = "snake_case")]
183/// # #[es_event(id = "SnapGuardMeterId")]
184/// # pub enum SnapGuardMeterEvent {
185/// #     Initialized { id: SnapGuardMeterId },
186/// # }
187/// # pub struct NewSnapGuardMeter;
188/// # impl IntoEvents<SnapGuardMeterEvent> for NewSnapGuardMeter {
189/// #     type IdSource = FromRepo;
190/// #     fn into_events(self, builder: EntityEventsBuilder<SnapGuardMeterEvent, FromRepo>) -> EntityEvents<SnapGuardMeterEvent> {
191/// #         let id = builder.id();
192/// #         builder.initial_events([SnapGuardMeterEvent::Initialized { id }])
193/// #     }
194/// # }
195/// // Missing: a real `Snapshot` — `events` stays
196/// // `EntityEvents<SnapGuardMeterEvent>` (implicit `NoSnapshot`).
197/// #[derive(EsEntity)]
198/// pub struct SnapGuardMeter {
199///     pub id: SnapGuardMeterId,
200///     events: EntityEvents<SnapGuardMeterEvent>,
201/// }
202/// # impl TryFromEvents<SnapGuardMeterEvent> for SnapGuardMeter {
203/// #     fn try_from_events(events: EntityEvents<SnapGuardMeterEvent>) -> Result<Self, EntityHydrationError> {
204/// #         Ok(SnapGuardMeter { id: *events.id(), events })
205/// #     }
206/// # }
207/// # impl HeadSnapshot for SnapGuardMeter {
208/// #     fn capture(&self) -> Option<NoSnapshot> { None }
209/// # }
210/// // error: entity snapshot type and `#[es_repo(snapshot)]` disagree.
211/// #[derive(EsRepo, Debug)]
212/// #[es_repo(
213///     entity = "SnapGuardMeter",
214///     tbl = "meters",
215///     events_tbl = "meter_events",
216///     snapshot,
217///     snapshot_tbl = "meter_snapshots"
218/// )]
219/// pub struct SnapGuardMeters {
220///     pool: es_entity::db::Pool,
221/// }
222/// ```
223///
224/// A snapshot type with a `Forgettable<T>` field also requires the repo to
225/// enable `forgettable` — otherwise the payload would never be scrubbed:
226///
227/// ```compile_fail
228/// use es_entity::*;
229/// use serde::{Serialize, Deserialize};
230/// # fn main() {}
231/// # es_entity::entity_id! { SnapGuardClientId }
232/// # #[derive(EsEvent, Debug, Clone, Serialize, Deserialize)]
233/// # #[serde(tag = "type", rename_all = "snake_case")]
234/// # #[es_event(id = "SnapGuardClientId")]
235/// # pub enum SnapGuardClientEvent {
236/// #     Initialized { id: SnapGuardClientId, email: Forgettable<String> },
237/// # }
238/// #[derive(EsSnapshot, Debug, Clone, Serialize, Deserialize)]
239/// #[es_snapshot(version = 1)]
240/// pub struct SnapGuardClientSnapshot {
241///     pub id: SnapGuardClientId,
242///     pub email: Forgettable<String>,
243/// }
244/// # pub struct NewSnapGuardClient { email: String }
245/// # impl IntoEvents<SnapGuardClientEvent> for NewSnapGuardClient {
246/// #     type IdSource = FromRepo;
247/// #     fn into_events(self, builder: EntityEventsBuilder<SnapGuardClientEvent, FromRepo>) -> EntityEvents<SnapGuardClientEvent> {
248/// #         let id = builder.id();
249/// #         builder.initial_events([SnapGuardClientEvent::Initialized { id, email: Forgettable::new(self.email) }])
250/// #     }
251/// # }
252/// # #[derive(EsEntity)]
253/// # pub struct SnapGuardClient {
254/// #     pub id: SnapGuardClientId,
255/// #     events: EntityEvents<SnapGuardClientEvent, SnapGuardClientSnapshot>,
256/// # }
257/// # impl HeadSnapshot for SnapGuardClient {
258/// #     fn capture(&self) -> Option<SnapGuardClientSnapshot> { None }
259/// # }
260/// # impl TryFromEvents<SnapGuardClientEvent, SnapGuardClientSnapshot> for SnapGuardClient {
261/// #     fn try_from_events(events: EntityEvents<SnapGuardClientEvent, SnapGuardClientSnapshot>) -> Result<Self, EntityHydrationError> {
262/// #         Ok(SnapGuardClient { id: *events.id(), events })
263/// #     }
264/// # }
265/// // error: snapshot type has Forgettable fields but this repo does not
266/// // enable `forgettable`.
267/// #[derive(EsRepo, Debug)]
268/// #[es_repo(
269///     entity = "SnapGuardClient",
270///     tbl = "clients",
271///     events_tbl = "client_events",
272///     snapshot,
273///     snapshot_tbl = "client_snapshots"
274/// )]
275/// pub struct SnapGuardClients {
276///     pool: es_entity::db::Pool,
277/// }
278/// ```
279pub trait HeadSnapshot: crate::EsEntity {
280    /// Called on every write the entity passes through — with staged events,
281    /// or with none when the loaded state had no matching snapshot. `Some(s)`
282    /// = persist `s` as the snapshot at the current head; `None` = keep
283    /// whatever snapshot exists and let the tail grow.
284    fn capture(&self) -> Option<Self::Snapshot>;
285}