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}