Skip to main content

eventsdb_core/
store.rs

1//! The per-stream SPI.
2//!
3//! An [`EventStore`] is one stream's append-only log behind a trait, so a
4//! durable backend takes the same calls the in-memory one does. The SPI is
5//! scoped to a **single** stream: there is no `stream` parameter, because the
6//! stream is the unit of serialization and a handle *is* one. Questions about
7//! the database as a whole belong to [`crate::log::EventLog`], one level up.
8//!
9//! # Append-only is the shape, not a runtime check
10//!
11//! The trait has no `update`, `delete` or `overwrite`. Immutability is
12//! guaranteed by what the trait cannot express, which is a stronger guarantee
13//! than a flag someone can pass.
14//!
15//! # Appends land; decisions are taken inside the write
16//!
17//! [`EventStore::append`] records a fact, and the store decides where it
18//! lands. It is serialized per stream by the backend, so two handles on one
19//! stream both write and the log interleaves in arrival order. An ordinary
20//! append is never refused for an out-of-date view of the head: that would be
21//! asking a fact to prove it knew the future.
22//!
23//! A *command* with an invariant is the other case, and it comes in two
24//! shapes. **Which one you want depends on where the decision was made, not
25//! on whether the check is atomic** — both check inside the write.
26//!
27//! - The decision runs **at** the write: [`EventStore::append_if`]. The
28//!   backend reads the stream, calls the caller's decision and appends what it
29//!   returns, all inside the same serialized write, so "reserve n only if the
30//!   balance covers it" is checked against the stream as it is at that
31//!   instant. Prefer this wherever it fits: it folds rather than comparing, so
32//!   it raises no false conflicts; a decision with nothing to do returns
33//!   `None` and is idempotent for free; and there is no retry loop because
34//!   there is nothing to retry.
35//! - The decision was made **before** the call, somewhere this process cannot
36//!   reach — an HTTP client holding an `ETag`, a form somebody filled in, a
37//!   message that sat in a queue. `Decision` is a `FnOnce` running under the
38//!   lock, so it cannot represent that. [`EventStore::append_expecting`] can:
39//!   it compares one number, which is exactly as much as a caller who left the
40//!   process still knows.
41//!
42//! An earlier version of this paragraph said a compare-and-swap "could only
43//! detect afterwards". That is wrong and worth correcting rather than quietly
44//! deleting: a CAS whose head read is in the same transaction as its insert
45//! detects at the same instant `append_if`'s decision does. What it cannot do
46//! is *fold*. The version that really does detect too late is the one whose
47//! lookup sits outside the transaction, which is not what this offers.
48
49use std::time::Duration;
50
51use async_trait::async_trait;
52use serde_json::{Map, Value};
53
54use crate::error::{Error, Result};
55use crate::position::Committed;
56use crate::upcast::Current;
57
58/// What to write, decided against the stream under the backend's lock.
59///
60/// `FnOnce`: it is called exactly once, so a contended `append_if` is not
61/// retried by the backend — a second attempt would need a second decision,
62/// and there is only one. Contention surfaces as [`Error::Busy`] instead,
63/// which is the class that says another *call* is worth making.
64pub type Decision = Box<dyn FnOnce(&[Current]) -> Option<Map<String, Value>> + Send>;
65
66/// What a caller believes a stream's head to be, as of when it last looked.
67///
68/// A named type rather than an `Option<u64>` or a zero sentinel. The two cases
69/// are not "some head" and "no head" — they are two different claims, and the
70/// empty one is the one everybody gets wrong. Rails Event Store spells it
71/// `-1`, others spell it `0`, and both produce off-by-one bug reports;
72/// Equinox hides the number entirely rather than let it into domain code.
73/// Naming the empty case is the mitigation available to a store that has to
74/// expose it at all.
75///
76/// # Two variants are the surface
77///
78/// Stores that run as a service tend to offer four: these two, an `Any` that
79/// checks nothing, and a `StreamExists` that checks only that the stream was
80/// written to. `Any` is spelled here by not calling
81/// [`EventStore::append_expecting`]. `StreamExists` is absent on purpose.
82/// KurrentDB added it so an append could refuse a *soft-deleted* stream — a
83/// stream that was written, then marked gone. That third state has nothing to
84/// stand on here: the trait has no delete, retention leaves the counter
85/// alone, and a stream has either been written or it has not. Past the
86/// existence check `StreamExists` is `Any`, so it also adds nothing to
87/// concurrency that [`Expected::Seq`] does not already give.
88///
89/// The question it is usually reached for — "did the command that creates
90/// this stream run?" — is a fact about the domain, and it goes where domain
91/// facts go: a key under `meta` on the creating event, read through
92/// [`crate::log::Filter`]. See [`crate::event`] on why lifecycle lives there.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum Expected {
96    /// Nothing has ever been appended to this stream.
97    ///
98    /// **Not the same as "the stream reads empty".** Retention can empty a
99    /// stream whose counter stands at 50, and a caller that meant "this is a
100    /// new order" must not be told yes about an order that was archived. The
101    /// check is against what the stream's counter records, not against the
102    /// rows that survive.
103    Unwritten,
104
105    /// The last event appended to this stream has this `seq`.
106    ///
107    /// A `seq`, not a [`crate::position::Position`]: the claim is about one
108    /// stream, and the two coordinates have different scopes.
109    Seq(u64),
110}
111
112#[async_trait]
113pub trait EventStore: Send + Sync {
114    /// Which stream this handle is.
115    fn stream_id(&self) -> &str;
116
117    /// Validate, stamp and append an event, returning its coordinates.
118    ///
119    /// A rejected event leaves no trace and consumes no sequence number.
120    async fn append(&mut self, event: Map<String, Value>) -> Result<Committed>;
121
122    /// Append `events` as one write, in the order given.
123    ///
124    /// For facts that are one fact: two records of a single occurrence must
125    /// not be separable by a reader, because a stream where the first is
126    /// visible without the second is a stream that never existed.
127    ///
128    /// **All or nothing on a backend that can do it.** The default below is
129    /// the most a backend with no transaction can offer — it appends one at a
130    /// time, so a failure part-way leaves what already landed. Both shipped
131    /// backends override it.
132    async fn append_many(&mut self, events: Vec<Map<String, Value>>) -> Result<Vec<Committed>> {
133        let mut committed = Vec::with_capacity(events.len());
134        for event in events {
135            committed.push(self.append(event).await?);
136        }
137        Ok(committed)
138    }
139
140    /// Decide *inside* the store's serialization: read the stream, ask
141    /// `decide` what to write, and append its answer in the same write.
142    ///
143    /// `kinds` filters what the decision is shown, exactly as it filters
144    /// [`EventStore::read_kinds`] (`None` = the whole stream), and the events
145    /// arrive in `seq` order. What the decision *writes* is unfiltered — the
146    /// event it returns is appended whatever its kind. Returning `None`
147    /// records nothing and leaves the stream untouched.
148    async fn append_if(
149        &mut self,
150        kinds: Option<&[&str]>,
151        decide: Decision,
152    ) -> Result<Option<Committed>>;
153
154    /// Record an event with a time it already has, rather than the wall clock
155    /// of this call.
156    ///
157    /// The backfill counterpart to [`EventStore::append`]: history from a
158    /// system that has its own notion of when, brought in without discarding
159    /// that timeline. Identical to `append` in every other respect — the
160    /// envelope is validated, `seq` and the position are this store's, the
161    /// schema version is the author's.
162    ///
163    /// **Use `append` for ordinary writes.** For moving an *eventsdb* log,
164    /// neither this nor `append` is the verb — `import` is, because it also
165    /// carries the schema version each event was written under, which this
166    /// cannot.
167    ///
168    /// See [`crate::event`] for what the time coordinate means and why the
169    /// verb rather than a field is what says which moment it is. In short:
170    /// positions order the log, the coordinate never does, and this call does
171    /// not enforce that the time it is given is at or after the stream's head.
172    /// Backfilling into an empty stream in chronological order keeps that
173    /// property by construction.
174    async fn append_at(&mut self, epoch_ms: u64, event: Map<String, Value>) -> Result<Committed> {
175        let _ = (epoch_ms, event);
176        Err(Error::Unsupported(
177            "this store cannot record an event at a time other than now".to_string(),
178        ))
179    }
180
181    /// Append `event` only if the stream's head is `expected`, refusing with
182    /// [`Error::HeadMismatch`] if it is not.
183    ///
184    /// For a decision taken **before** this call, somewhere this process
185    /// cannot reach: an HTTP client holding an `ETag`, a form somebody filled
186    /// in, a message that sat in a queue. The caller read the stream at some
187    /// head, went away, and is now saying "apply this only if nothing moved".
188    ///
189    /// [`EventStore::append_if`] is the other case and the better one wherever
190    /// it applies — a decision folded from the stream *at* the write, which
191    /// this cannot express because it compares one number and never looks at
192    /// the events. Reach for this only when the decision could not have run
193    /// under the lock.
194    ///
195    /// **The comparison happens inside the write.** The head is read in the
196    /// same transaction that inserts, so a writer arriving between them
197    /// serializes behind the write lock rather than slipping past the check. A
198    /// refused append leaves no trace and consumes no sequence number, exactly
199    /// as a rejected [`EventStore::append`] does.
200    ///
201    /// The default declines. A backend that cannot make the read and the
202    /// insert one write has no honest answer here, and an imitation that
203    /// checked separately would be worse than none — that gap between lookup
204    /// and save is the documented weakness of the version-only form elsewhere.
205    async fn append_expecting(
206        &mut self,
207        expected: Expected,
208        event: Map<String, Value>,
209    ) -> Result<Committed> {
210        let _ = (expected, event);
211        Err(Error::Unsupported(
212            "this store cannot make a head check and an append one write".to_string(),
213        ))
214    }
215
216    /// Queue an append and return without waiting for it to land.
217    ///
218    /// For a fact that has to be recorded from somewhere that cannot await:
219    /// a `Drop` closing a session is the case this exists for, and there
220    /// blocking is not allowed either, so neither `.await` nor
221    /// `block_on` is available. Hence a plain `fn` — an `async fn` would need
222    /// an executor the caller does not have.
223    ///
224    /// **`&self`, not `&mut self`.** A `Drop` has whatever reference it has,
225    /// and demanding a unique one is the difference between reachable from
226    /// there and not. Every other write on this trait takes `&mut self`
227    /// because it returns coordinates the caller is expected to use; this one
228    /// returns nothing to hold.
229    ///
230    /// **The envelope is checked here; the write is not reported.** A
231    /// malformed event is refused synchronously, before the call returns.
232    /// After that there is no channel back: a storage failure is dropped and
233    /// nothing is woken, because the caller has already gone. Use it for the
234    /// boundary record whose absence a reader would notice — the close of a
235    /// session that would otherwise read as still open — not for a fact
236    /// nothing else in the system knows.
237    ///
238    /// **Ordering is the backend's to keep.** The queued write must land
239    /// before anything submitted after it; a backend that spawns a task which
240    /// later calls [`EventStore::append`] has left the queue and races every
241    /// other writer, which is worse than declining.
242    ///
243    /// The default declines rather than dropping the event quietly. A store
244    /// with nowhere to queue it has no way to keep that ordering, and silence
245    /// here would leave a stream looking open for ever with nothing to say
246    /// why.
247    fn detach_append(&self, event: Map<String, Value>) -> Result<()> {
248        let _ = event;
249        Err(Error::Unsupported(
250            "this store cannot accept a write it is not awaited for".to_string(),
251        ))
252    }
253
254    /// Events of `kinds` with `seq >= from_seq`, at most `limit`, in `seq`
255    /// order and already upcasted.
256    ///
257    /// `None` reads every kind. `Some(kinds)` reads only those, and `limit`
258    /// counts what came back rather than what was skipped. An empty slice
259    /// selects nothing.
260    ///
261    /// **The kind selected on is the stored one**, because the selection
262    /// happens in the backend, before the upcaster chain runs. A step that
263    /// renames a kind therefore obliges every filtered read of it to name
264    /// both spellings — which is a cost the chain's author pays knowingly,
265    /// and the reason a rename is not free.
266    async fn read_kinds(
267        &self,
268        kinds: Option<&[&str]>,
269        from_seq: u64,
270        limit: usize,
271    ) -> Result<Vec<Current>>;
272
273    /// Every event with `seq >= from_seq`, at most `limit`.
274    async fn read(&self, from_seq: u64, limit: usize) -> Result<Vec<Current>> {
275        self.read_kinds(None, from_seq, limit).await
276    }
277
278    /// The last `n` events, in `seq` order.
279    ///
280    /// A read *from the end*, which the range reads cannot express: they
281    /// start at a `seq` and count forward, so "the last five" could only be
282    /// asked for by reading the whole stream and throwing the front away. The
283    /// default does exactly that, and both shipped backends override it.
284    async fn read_last(&self, n: usize) -> Result<Vec<Current>> {
285        let mut all = self.read(0, usize::MAX).await?;
286        if all.len() > n {
287            all.drain(..all.len() - n);
288        }
289        Ok(all)
290    }
291
292    /// The highest `seq`, or `None` for an empty stream.
293    ///
294    /// Fallible on purpose: a transient failure must not read as an empty
295    /// stream, or a caller deciding open-vs-resume takes the wrong branch.
296    async fn head(&self) -> Result<Option<u64>>;
297
298    /// Number of recorded events.
299    async fn len(&self) -> Result<usize>;
300
301    /// Whether nothing has been recorded yet.
302    async fn is_empty(&self) -> Result<bool> {
303        Ok(self.head().await?.is_none())
304    }
305
306    /// Which database this stream lives in, or `None` for a backend that is
307    /// not one.
308    ///
309    /// Two handles answer with the same string exactly when they are the same
310    /// database. It is an identity, not a path a caller should take apart.
311    fn database(&self) -> Option<&str> {
312        None
313    }
314
315    /// Answer a caller's own read-only SQL over the log.
316    ///
317    /// **Queries read the stored shape, not the upcasted one**: every other
318    /// read goes through the chain, but SQL runs against the bytes as they
319    /// were written, because the chain is Rust and the query is SQLite's. A
320    /// caller reading across a schema change reads the versions it finds.
321    ///
322    /// **The limit is yours, and so is knowing whether it cut.** There is no
323    /// `truncated` flag here because the `LIMIT` is in your text, not in a
324    /// parameter this store owns: ask for `n + 1` rows and compare, which is
325    /// the whole of what such a flag would tell you. Owning the limit instead
326    /// would mean wrapping your statement to attach one, and this call
327    /// deliberately never rewrites or parses what it is given — the read-only
328    /// check above is SQLite's answer about your text, not a reading of it.
329    ///
330    /// **Parameters are positional here, and only here.** SQLite binds by name
331    /// as well, and [`crate::Params`] is how a backend's own `query` offers
332    /// both — but this trait is used as `Box<dyn EventStore>`, a dispatchable
333    /// method may not have type parameters, and argument-position `impl Trait`
334    /// is one. So the trait takes the `Vec` and a caller wanting `:name` calls
335    /// the concrete log.
336    ///
337    /// The default refuses, because a store that is not a database has no
338    /// answer to give.
339    async fn query(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Map<String, Value>>> {
340        let _ = (sql, params);
341        Err(Error::Unsupported(
342            "this store is not a database and cannot answer SQL".to_string(),
343        ))
344    }
345
346    /// [`EventStore::query`] with a bound on how long the statement may run.
347    ///
348    /// **Not the same thing as `busy_timeout`.** That one bounds *waiting for
349    /// a lock*; this bounds a statement that took its lock immediately and is
350    /// simply expensive — a recursive CTE with a runaway bound, a join with no
351    /// usable index. Nothing else stops one, and a caller that hands SQL to
352    /// somebody else (a shell, a script, a user) cannot know in advance which
353    /// kind it is getting.
354    ///
355    /// A backend serving SQL from the same place it serves writes has the
356    /// stronger reason: one expensive statement there stalls every append
357    /// until it finishes.
358    ///
359    /// The deadline is reported as [`Error::Timeout`] — the caller's own bound
360    /// arriving, not the database failing — so retrying a narrower query is
361    /// the sensible next move.
362    ///
363    /// The default declines rather than falling back to [`EventStore::query`]:
364    /// running an unbounded statement for a caller who asked for a bound is
365    /// the one answer that is worse than none.
366    async fn query_timeout(
367        &self,
368        sql: &str,
369        params: Vec<Value>,
370        timeout: Duration,
371    ) -> Result<Vec<Map<String, Value>>> {
372        let _ = (sql, params, timeout);
373        Err(Error::Unsupported(
374            "this store cannot bound how long a statement runs".to_string(),
375        ))
376    }
377}