eventsdb_core/log.rs
1//! The database-level SPI: one level above a stream.
2//!
3//! [`crate::store::EventStore`] is scoped to a single stream and stays that
4//! way. Everything that is a question about the database as a whole lives
5//! here: reading across streams, following the log as it grows, remembering
6//! how far a consumer got — and moving the whole thing somewhere else.
7//!
8//! That last one is on the trait rather than on a backend on purpose. A log
9//! that cannot be moved is a log its owner cannot leave, so being movable is a
10//! claim this crate makes, not a convenience one implementation happens to
11//! offer. A caller generic over `EventLog` can therefore write a migration.
12
13use async_trait::async_trait;
14use futures_core::stream::BoxStream;
15
16use crate::error::{Error, Result};
17use crate::position::{Position, Recorded};
18use crate::store::EventStore;
19use crate::transfer::{ExportedEvent, ImportReport};
20
21/// Which events a cross-stream read or a subscription wants.
22///
23/// Both filters are matched on the **stored** kind and the stored stream
24/// name, before the upcaster chain runs — the same rule
25/// [`EventStore::read_kinds`] follows, for the same reason.
26#[derive(Debug, Clone, Default)]
27pub struct Filter {
28 /// Kinds to include. `None` includes every kind; an empty vector selects
29 /// nothing, which is the honest reading of "include these" given none.
30 pub kinds: Option<Vec<String>>,
31 /// Streams to include. `None` reads every stream; an empty vector selects
32 /// nothing, the same reading `kinds` gets.
33 ///
34 /// **A set, not one name.** Restricting to a single stream is the common
35 /// case and [`Filter::stream`] still spells it, but the question a caller
36 /// actually has is often about a *group* — the streams of one session, of
37 /// one tenant, of one run. Given only a single-stream filter that has to
38 /// be answered with one read per stream and a merge in the caller, which
39 /// loses the position order the log exists to provide.
40 pub streams: Option<Vec<String>>,
41}
42
43impl Filter {
44 pub fn all() -> Self {
45 Filter::default()
46 }
47
48 pub fn kinds<I, S>(kinds: I) -> Self
49 where
50 I: IntoIterator<Item = S>,
51 S: Into<String>,
52 {
53 Filter {
54 kinds: Some(kinds.into_iter().map(Into::into).collect()),
55 streams: None,
56 }
57 }
58
59 /// Restrict to one stream.
60 ///
61 /// Replaces whatever set was there, rather than adding to it: reading it
62 /// as "and also this one" would make `.stream("a").stream("b")` mean
63 /// something no reader would guess from the singular name.
64 pub fn stream(mut self, stream: impl Into<String>) -> Self {
65 self.streams = Some(vec![stream.into()]);
66 self
67 }
68
69 /// Restrict to a set of streams.
70 ///
71 /// An empty set selects nothing, which is what "include these" given none
72 /// says. A caller assembling a set from somewhere that can legitimately
73 /// come back empty should check before asking.
74 pub fn streams<I, S>(mut self, streams: I) -> Self
75 where
76 I: IntoIterator<Item = S>,
77 S: Into<String>,
78 {
79 self.streams = Some(streams.into_iter().map(Into::into).collect());
80 self
81 }
82
83 /// Whether this filter can match anything at all. An empty list of either
84 /// kind cannot, and a backend can skip the query entirely.
85 pub fn selects_nothing(&self) -> bool {
86 self.kinds.as_ref().is_some_and(|kinds| kinds.is_empty())
87 || self
88 .streams
89 .as_ref()
90 .is_some_and(|streams| streams.is_empty())
91 }
92}
93
94#[async_trait]
95pub trait EventLog: Send + Sync {
96 /// A handle on one stream. Everything below this is the per-stream SPI,
97 /// unchanged.
98 async fn stream(&self, id: &str) -> Result<Box<dyn EventStore>>;
99
100 /// Events with `position > from`, across every stream, in position order,
101 /// at most `limit`.
102 ///
103 /// Exclusive on `from` so a cursor can be fed straight back in:
104 /// [`Position::BEGINNING`] reads from the start, and the position of the
105 /// last event handled reads the next batch.
106 async fn read_all(
107 &self,
108 from: Position,
109 filter: &Filter,
110 limit: usize,
111 ) -> Result<Vec<Recorded>>;
112
113 /// The newest position in the log, or [`Position::BEGINNING`] if it is
114 /// empty.
115 async fn head_position(&self) -> Result<Position>;
116
117 /// Catch up from `from`, then stay live.
118 ///
119 /// There is no seam a consumer has to handle: the live tail is the same
120 /// range read, resumed. Ordering is by position and nothing is skipped —
121 /// see [`Position`] for why that holds without gap detection.
122 ///
123 /// How the live half learns of a write is the backend's business. The
124 /// SQLite backend wakes subscribers on **the same log** directly, and
125 /// falls back to polling for anything else, because SQLite has no
126 /// notification a writer elsewhere could send.
127 ///
128 /// "Anything else" includes a second log opened on the same file in this
129 /// same process — the wake-up channel belongs to the log, not to the
130 /// database. Nothing is lost either way; the difference is latency, and it
131 /// is about three orders of magnitude [measured: 552µs woken directly
132 /// against 552ms on a 600ms poll, `tests/two_logs.rs`]. Open the file once
133 /// per process and share the log if that matters.
134 fn subscribe(
135 &self,
136 from: Position,
137 filter: Filter,
138 ) -> Result<BoxStream<'static, Result<Recorded>>>;
139
140 /// How far `consumer` has got, or [`Position::BEGINNING`] if it has never
141 /// reported.
142 async fn checkpoint_load(&self, consumer: &str) -> Result<Position>;
143
144 /// Record how far `consumer` has got.
145 ///
146 /// Callers that need the checkpoint to move in the same transaction as
147 /// the work it accounts for must not use this — it is its own write. That
148 /// is what a projection runner is for.
149 async fn checkpoint_save(&self, consumer: &str, at: Position) -> Result<()>;
150
151 /// Read events out in position order, **as they are stored**.
152 ///
153 /// The one read that does not run the upcaster chain. Every other read
154 /// wants the current shape; a transfer wants the bytes, so the receiving
155 /// log can hold exactly what this one held and run its own chain over
156 /// them. Upcasting on the way out would bake this build's reading of an
157 /// old event into the copy and lose the original.
158 ///
159 /// Page with `from` and `limit`, feeding the last returned position back
160 /// in; a short batch is the end. See [`crate::transfer`] for what travels
161 /// and what the receiving log reassigns.
162 ///
163 /// The default declines, because a log with no stored form has nothing to
164 /// hand over that another log could hold.
165 async fn export(
166 &self,
167 from: Position,
168 filter: &Filter,
169 limit: usize,
170 ) -> Result<Vec<ExportedEvent>> {
171 let _ = (from, filter, limit);
172 Err(Error::Unsupported(
173 "this log cannot hand over its stored events".to_string(),
174 ))
175 }
176
177 /// Write exported events into this log, in the order given.
178 ///
179 /// `seq` and `position` are this log's to assign; everything else travels
180 /// unchanged, `epoch_ms` and `_schema_version` included. Keeping the
181 /// version is what leaves an old event within reach of the upcaster
182 /// written for it, and
183 /// [`ImportReport::reproduced_coordinates`] reports whether the batch
184 /// landed where it came from, so a migration can check rather than assume.
185 ///
186 /// The default declines rather than appending one at a time. A backend
187 /// with no transaction could only offer a partial import, and a transfer
188 /// that stopped half way is worse than one that refused: from the outside
189 /// there is no way to tell how far it got.
190 async fn import(&self, events: Vec<ExportedEvent>) -> Result<ImportReport> {
191 let _ = events;
192 Err(Error::Unsupported(
193 "this log cannot take in exported events as one write".to_string(),
194 ))
195 }
196}