agent_block_core/knl/sqlite_store.rs
1//! The durable [`EventStore`]: one SQLite table, one stream per session.
2//!
3//! [`SqliteEventStore`] takes the same calls [`MemEventStore`] does, so a
4//! session's log survives a process restart without any other code changing.
5//! It is scoped to one `stream` (the session id); several sessions share one
6//! DB file, and the `(stream, seq)` primary key keeps their logs apart.
7//!
8//! # Append-only, store-assigned coordinates
9//!
10//! There is no update or delete — the trait has neither, so a backend cannot
11//! offer one. `seq` and `epoch_ms` are the store's to assign: `append`
12//! computes the next `seq` inside the transaction that inserts, runs the
13//! same [`validate_event`] and [`stamp`] the in-memory store runs, and
14//! returns the coordinates inline.
15//!
16//! # One backend, two kinds of database
17//!
18//! There is no second implementation of [`EventStore`] in the product: a
19//! session's log is a SQLite table whether or not it outlives the process.
20//! [`SqliteEventStore::open`] takes a file; [`SqliteEventStore::open_memory`]
21//! takes a database that lives in memory under a name derived from the
22//! stream (`file:knl-<stream>?mode=memory&cache=shared`), which is what an
23//! ephemeral session gets. The shared-cache URI is not decoration: a second
24//! connection to the same name sees the same database, which is what lets the
25//! read side below exist at all — and it is also why the writer connection
26//! must outlive the session, since an in-memory database is reclaimed when
27//! its last connection closes.
28//!
29//! The one thing the in-memory database cannot do is survive the process.
30//! Within it, a stream is a stream: [`super::Session::resume`] reopens one by
31//! name exactly as it reopens a file.
32//!
33//! # The stored shape is columns, and one of them is the kind's own
34//!
35//! The event's envelope is columns — `stream` / `seq` / `epoch_ms` / `kind` /
36//! `schema_version` / `beat` — and the two objects it carries are one column
37//! each: `meta`, a shallow table of scalars, and `data`, the kind's own
38//! content at any depth ([`super::event`]). A read rebuilds exactly the
39//! object that was written, so a caller sees no difference between this and a
40//! log kept in memory.
41//!
42//! The whole event used to go into a single `payload` column, which put an
43//! envelope key and a kind's own field at the same level for anything reading
44//! the log with SQL: a `json_extract` could not say which of the two it was
45//! reaching into, and a kind changing shape broke a view with nothing to
46//! point at. Now the columns *are* the contract — a view over them is
47//! unaffected by any kind — and the paths that need watching are all inside
48//! `data`.
49//!
50//! # Reads are indexed by kind, and by beat
51//!
52//! The table carries a `(stream, kind, seq)` index beside its `(stream, seq)`
53//! primary key, so a kind-filtered read ([`EventStore::read_kinds`], and the
54//! decision input of [`EventStore::append_if`]) costs the size of the *fold*
55//! rather than the size of the stream: folding the balance reads the
56//! `budget_*` events, not every fact the session ever recorded.
57//!
58//! `beat` has a column and a `(stream, beat, seq)` index of its own, because
59//! it is the one correlation the log itself is grouped by: the events of one
60//! beat are a range of that index rather than a scan with a `json_extract`
61//! in the predicate.
62//!
63//! # The read side is a second connection, and it cannot write
64//!
65//! [`EventStore::query`] answers a caller's own SQL ([`super::query`]) over a
66//! **separate** connection to the same database, opened `READ_ONLY` and put
67//! into `query_only` mode, lazily on the first query and reused after that.
68//! Three independent things therefore have to fail before a query could
69//! change the log: the statement is checked to be a single `SELECT` / `WITH`
70//! before SQLite sees it, the prepared statement is asked whether it writes,
71//! and the connection it runs on has no write capability to lend it. Values
72//! are bound, never interpolated — including the ids `$sessions` expands to.
73//!
74//! A query runs under a deadline: [`AsyncIsle::call_timeout`] interrupts the
75//! statement if it has not finished in time, and that surfaces as
76//! [`KnlError::Timeout`].
77//!
78//! # The connection lives on a thread of its own, and nobody waits on it
79//!
80//! Neither connection is held by this struct: each is owned by a
81//! [`rusqlite_isle::AsyncIsle`], a thread that takes closures and runs them
82//! one at a time. A store method is therefore a closure sent to that thread
83//! and a result **awaited** — the caller's task yields while SQLite works, so
84//! the one thread that must never stop (the Lua VM's, which is the sole worker
85//! of its own runtime) goes on driving every other coroutine, timer and cancel
86//! it owns. That is why the whole SPI below is `async`: an event store that
87//! can only be waited for synchronously is an event store that stops the VM.
88//!
89//! The handle is cloneable and cheap; what is *not* cloneable is the
90//! [`rusqlite_isle::AsyncIsleDriver`] that owns the thread's join handle.
91//! Those go to an [`IsleDrivers`] the host holds, so a session's threads
92//! outlive the session — a dropped handle can still hand its closing event to
93//! the isle without waiting for it — and are drained once, at host shutdown.
94//!
95//! # Concurrency
96//!
97//! `append`, `append_many` and `append_if` read-then-write, so each runs in an
98//! `IMMEDIATE` transaction: the `RESERVED` lock is taken at `BEGIN` rather
99//! than promoted from `SHARED` on the first write, which is the point
100//! `busy_timeout` actually covers — a `DEFERRED` transaction can still hit
101//! `SQLITE_BUSY` on lock *promotion* even with a timeout set. Contention with
102//! another connection is waited out by the busy timeout the isle was opened
103//! with, and `append` / `append_many` sit inside [`AsyncIsle::call_retry`],
104//! which re-submits the whole job on `SQLITE_BUSY`, backing off with
105//! `tokio::time::sleep` rather than parking a thread. A write that is still
106//! contended after that surfaces as [`KnlError::Busy`], which is the one class
107//! that tells the caller another try is worth making.
108//!
109//! `append_if` gets the busy timeout and the retryable error, not the backoff
110//! loop — its decision is a `FnOnce`, so an attempt consumes it. What it no
111//! longer needs is the channel round trip the borrowed-closure form required:
112//! the decision is owned and `Send`, so it travels *with* the job and runs on
113//! the isle's own thread, inside the transaction, with nothing on either side
114//! waiting for the other.
115//!
116//! That is what makes the SPI's promise true here: appends to one stream are
117//! *serialized* — two handles both write and the log interleaves in arrival
118//! order — a batch is one transaction, so it lands whole or not at all, and a
119//! decision taken by `append_if` runs against the stream inside the same
120//! transaction that records its answer, so no concurrent writer can slip
121//! between the two.
122//!
123//! [`MemEventStore`]: super::event_store::MemEventStore
124//! [`AsyncIsle::call_retry`]: rusqlite_isle::AsyncIsle::call_retry
125//! [`AsyncIsle::call_timeout`]: rusqlite_isle::AsyncIsle::call_timeout
126
127use std::path::{Path, PathBuf};
128use std::sync::{Arc, Mutex, PoisonError};
129use std::time::Duration;
130
131use async_trait::async_trait;
132use rusqlite::types::{Value as SqlValue, ValueRef};
133use rusqlite::{params, params_from_iter, Connection, OpenFlags, TransactionBehavior};
134use rusqlite_isle::{AsyncIsle, AsyncIsleDriver, IsleError, RetryPolicy};
135use serde_json::{Map, Value};
136use tokio::sync::OnceCell;
137
138use super::event::{
139 stamp, validate_event, FIELD_BEAT, FIELD_DATA, FIELD_EPOCH_MS, FIELD_KIND, FIELD_META,
140 FIELD_SEQ,
141};
142use super::event_store::{
143 stamp_schema_version, ChildScan, ChildrenDecision, Committed, Decision, EventStore, Split,
144 SplitDecision, CURRENT_SCHEMA_VERSION, SCHEMA_VERSION_FIELD,
145};
146use super::query::{session_slot, QueryParams, QueryPlan, QueryRows, STREAM_PARAM};
147use super::{now_ms, KnlError, KnlResult};
148
149/// How long a contended write waits for the lock before erroring.
150const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
151
152/// The lifecycle owners of the connection threads a session's log lives on.
153///
154/// [`rusqlite_isle::AsyncIsle`] hands back a cloneable handle and a driver
155/// that is not clonable: the driver owns the thread's join handle and is the
156/// only thing that can drain and stop it. A session cannot hold its own —
157/// the whole point of the drop backstop is that a handle nobody closed can
158/// still hand its `session_closed` to the isle *after* the handle is gone, and
159/// a thread its own store had already stopped could not take it.
160///
161/// So the drivers are parked here instead: one collection per host run, shut
162/// down once at the end of it, exactly as the `std.ts` connection thread is.
163/// Cheap to clone (an `Arc`), because every site that opens a store needs to
164/// reach it.
165///
166/// The lock is a plain [`Mutex`] and is never held across an `.await`:
167/// [`IsleDrivers::shutdown`] takes the whole list out under the lock and
168/// releases it before it starts waiting on the first thread.
169#[derive(Clone, Default)]
170pub struct IsleDrivers {
171 parked: Arc<Mutex<Vec<AsyncIsleDriver>>>,
172}
173
174impl std::fmt::Debug for IsleDrivers {
175 /// The drivers themselves have nothing worth printing; the count is what
176 /// a caller debugging a leak wants.
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("IsleDrivers")
179 .field("parked", &self.len())
180 .finish()
181 }
182}
183
184impl IsleDrivers {
185 /// A fresh, empty collection.
186 pub fn new() -> Self {
187 Self::default()
188 }
189
190 /// Take ownership of `driver` for the rest of the run.
191 ///
192 /// A poisoned lock is stepped over rather than raised on: this runs while
193 /// a store is being opened, the data behind the lock is a plain `Vec` that
194 /// no half-finished write can corrupt, and refusing to keep the driver
195 /// would leak the thread outright.
196 fn park(&self, driver: AsyncIsleDriver) {
197 self.parked
198 .lock()
199 .unwrap_or_else(PoisonError::into_inner)
200 .push(driver);
201 }
202
203 /// How many connection threads are still owned here.
204 pub fn len(&self) -> usize {
205 self.parked
206 .lock()
207 .unwrap_or_else(PoisonError::into_inner)
208 .len()
209 }
210
211 /// Whether no connection thread has been opened (or all were drained).
212 pub fn is_empty(&self) -> bool {
213 self.len() == 0
214 }
215
216 /// Drain every thread: queued jobs run to completion, then each thread
217 /// stops and is joined.
218 ///
219 /// The queued jobs matter — the drop backstop submits its `session_closed`
220 /// without waiting for it, so this is where those land. Failures are
221 /// collected rather than raised on the first one: a thread that panicked
222 /// is no reason to leave the rest running.
223 ///
224 /// Idempotent: a second call finds nothing parked and returns an empty
225 /// list.
226 pub async fn shutdown(&self) -> Vec<IsleError> {
227 // The guard is released here, before the first `.await` below.
228 let drivers: Vec<AsyncIsleDriver> =
229 std::mem::take(&mut *self.parked.lock().unwrap_or_else(PoisonError::into_inner));
230 let mut failures = Vec::new();
231 for driver in drivers {
232 if let Err(e) = driver.shutdown().await {
233 failures.push(e);
234 }
235 }
236 failures
237 }
238}
239
240/// The table the log lives in — published as the read contract
241/// ([`events_schema`]).
242pub const EVENTS_TABLE: &str = "events";
243
244/// The DDL for [`EVENTS_TABLE`] and its three indexes.
245///
246/// `IF NOT EXISTS` throughout, so opening a fresh database and reopening one
247/// an earlier build wrote take the same path. The `(stream, kind, seq)`
248/// index is what makes a kind-filtered read cost the size of the fold rather
249/// than the size of the stream, and it keeps the rows in `seq` order within a
250/// kind, so the read needs no sort; `(stream, beat, seq)` does the same for
251/// the events of one beat.
252///
253/// `events_session_opened_parent` does it for the close-time scan
254/// ([`open_children_in`]), which asks across streams rather than within one:
255/// which openings name *this* stream as their parent. It is a *partial
256/// expression* index and both halves are load-bearing. The expression is
257/// written exactly as the scan writes it, because SQLite matches an indexed
258/// expression against a query's by form — a path bound as a parameter would
259/// never match one written as a literal, which is why
260/// [`child_scan_sql`] spells its words out. The `WHERE` keeps the index to
261/// the openings: `parent` lives on `session_opened` and nowhere else, so
262/// indexing every row would be storing a NULL per event to find the handful
263/// that are not.
264///
265/// That is the kernel's vocabulary sitting in the store's schema, which the
266/// rest of this backend avoids ([`ChildScan`] is an argument, not a constant).
267/// The price of the index is that those words are settled at DDL time; what it
268/// buys is that a close on a large log looks the openings up instead of walking
269/// the table. A scan under some other vocabulary still reads correctly — it
270/// just reads without the index.
271///
272/// **An index is not a stored shape**, so adding one does not touch
273/// [`super::event_store::CURRENT_SCHEMA_VERSION`]: the rows say exactly what
274/// they said before, and a database an earlier build wrote picks the index up
275/// on the next open, the same `IF NOT EXISTS` path a fresh one takes. What
276/// obliges a version bump and an upcaster is a change to what an event *is* —
277/// see the [`super::event_store`] module docs.
278///
279/// `beat` is the one nullable column: it is the caller's to declare and most
280/// events do not belong to a beat. `meta` and `data` are `NOT NULL` because
281/// they are filled in with `{}` on the way in ([`stamp`]), so a reader never
282/// has to tell an empty object from a missing one.
283const SCHEMA_DDL: &str = "CREATE TABLE IF NOT EXISTS events ( \
284 stream TEXT NOT NULL, \
285 seq INTEGER NOT NULL, \
286 epoch_ms INTEGER NOT NULL, \
287 kind TEXT NOT NULL, \
288 schema_version INTEGER NOT NULL, \
289 beat TEXT NULL, \
290 meta TEXT NOT NULL, \
291 data TEXT NOT NULL, \
292 PRIMARY KEY (stream, seq) \
293 ); \
294 CREATE INDEX IF NOT EXISTS events_stream_kind_seq \
295 ON events (stream, kind, seq); \
296 CREATE INDEX IF NOT EXISTS events_stream_beat_seq \
297 ON events (stream, beat, seq); \
298 CREATE INDEX IF NOT EXISTS events_session_opened_parent \
299 ON events (json_extract(data, '$.parent')) \
300 WHERE kind = 'session_opened';";
301
302/// One column of [`EVENTS_TABLE`], as SQLite itself reports it.
303///
304/// Published to the shell so a caller writing SQL against the log reads the
305/// column names and types from the database rather than from a list somebody
306/// retyped — and so a test can hold the shell's declaration of the schema
307/// against the table that actually exists.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct SchemaColumn {
310 /// The column name.
311 pub name: String,
312 /// Its declared type, as written in the DDL.
313 pub declared_type: String,
314 /// Whether it is part of the primary key.
315 pub pk: bool,
316}
317
318/// What the shared-cache URI of an in-memory kernel database starts with.
319///
320/// One constant for the two directions: [`Db::memory_uri`] builds the address,
321/// and [`is_memory_database`] reads one back.
322const MEMORY_URI_PREFIX: &str = "file:knl-";
323
324/// Whether `database` — an [`EventStore::database`] identity — names an
325/// in-memory database rather than a file.
326///
327/// The identity is documented as a thing to pass along and not to take apart,
328/// and this is the one question about it that is the store's to answer rather
329/// than a caller's to parse: the URI form is minted here, so the reading of it
330/// belongs here too. The caller that asks is the bridge, deciding whether a
331/// session can be a parent — a tree writes to one database, and the in-memory
332/// one locks per table under its shared cache.
333pub fn is_memory_database(database: &str) -> bool {
334 database.starts_with(MEMORY_URI_PREFIX)
335}
336
337/// Where a store's database lives.
338///
339/// The store keeps this so it can open a *second* connection to the same
340/// database for reads. For a file that is the same path; for an in-memory
341/// database it is the shared-cache URI, which is the only way a second
342/// connection can reach one.
343#[derive(Debug, Clone)]
344enum Db {
345 /// A file on disk.
346 File(PathBuf),
347 /// An in-memory database, addressed by its shared-cache URI.
348 Memory(String),
349}
350
351impl Db {
352 /// The identity of this database, as [`EventStore::database`] reports it.
353 ///
354 /// The same string the connection is opened by — a path for a file, the
355 /// shared-cache URI for an in-memory database — because that is exactly
356 /// what "the same database" means here: two stores opened by the same
357 /// target reach the same rows, and the `(stream, seq)` key keeps their
358 /// streams apart inside it.
359 fn id(&self) -> String {
360 self.target().to_string_lossy().into_owned()
361 }
362
363 /// The URI an in-memory database for `stream` is addressed by.
364 ///
365 /// Derived from the stream id, so reopening the same stream in the same
366 /// process finds the same database — which is what makes an in-memory
367 /// session resumable while it is still alive.
368 fn memory_uri(stream: &str) -> String {
369 format!("{MEMORY_URI_PREFIX}{stream}?mode=memory&cache=shared")
370 }
371
372 /// What SQLite is asked to open: a path for a file, the shared-cache URI
373 /// for an in-memory database.
374 ///
375 /// The URI goes through the same argument the path does, which is why
376 /// `SQLITE_OPEN_URI` is in both flag sets below: it is what makes the
377 /// `file:` form a URI rather than a relative path called "file:…".
378 fn target(&self) -> PathBuf {
379 match self {
380 Self::File(path) => path.clone(),
381 Self::Memory(uri) => PathBuf::from(uri),
382 }
383 }
384
385 /// The flags the writing connection is opened with.
386 fn write_flags() -> OpenFlags {
387 OpenFlags::default() | OpenFlags::SQLITE_OPEN_URI
388 }
389
390 /// The flags a read-only connection is opened with.
391 fn read_only_flags() -> OpenFlags {
392 OpenFlags::SQLITE_OPEN_READ_ONLY
393 | OpenFlags::SQLITE_OPEN_NO_MUTEX
394 | OpenFlags::SQLITE_OPEN_URI
395 }
396
397 /// Start the writing isle: the thread that owns the connection every
398 /// append goes through, with the `events` table ensured before it takes
399 /// its first job.
400 ///
401 /// The thread is created by `std::thread::Builder` inside the isle and
402 /// needs no runtime of its own; what this call awaits is the oneshot that
403 /// says the connection opened and the DDL ran. So the caller yields
404 /// rather than blocking, which is what lets `knl.open` be reached from
405 /// inside the Lua VM at all.
406 async fn spawn_writer(&self, drivers: &IsleDrivers) -> KnlResult<AsyncIsle> {
407 let (isle, driver) = AsyncIsle::builder()
408 .thread_name("knl-events")
409 .open_flags(Self::write_flags())
410 .wal(BUSY_TIMEOUT)
411 .spawn(self.target(), |conn| conn.execute_batch(SCHEMA_DDL))
412 .await
413 .map_err(KnlError::from)?;
414 drivers.park(driver);
415 Ok(isle)
416 }
417
418 /// Start the reading isle: a second thread, a second connection, and no
419 /// write capability on it at all.
420 async fn spawn_reader(&self, drivers: &IsleDrivers) -> KnlResult<AsyncIsle> {
421 let (isle, driver) = AsyncIsle::builder()
422 .thread_name("knl-events-read")
423 .open_flags(Self::read_only_flags())
424 .busy_timeout(BUSY_TIMEOUT)
425 .spawn(self.target(), |conn| {
426 conn.execute_batch("PRAGMA query_only = 1;")
427 })
428 .await
429 .map_err(KnlError::from)?;
430 drivers.park(driver);
431 Ok(isle)
432 }
433}
434
435/// How a busy write is retried: the isle re-submits the whole job on
436/// `SQLITE_BUSY`, backing off between attempts.
437///
438/// The defaults (3 retries from 50 ms, doubling) are the isle's, and so is the
439/// decision of what counts as busy — this store no longer classifies lock
440/// contention for the purpose of retrying it.
441fn retry_policy() -> RetryPolicy {
442 RetryPolicy::default()
443}
444
445/// A job's failure, split by who should see it.
446///
447/// [`Sqlite`](Self::Sqlite) is handed back to the isle, which is what lets it
448/// recognise a contended write and try again; a
449/// [`Terminal`](Self::Terminal) kernel error (a rejected event, a corrupt row,
450/// an encode failure) is carried out through the job's *value* instead, so no
451/// retry is spent on something no retry can fix.
452enum JobError {
453 /// A rusqlite fault, returned to the isle.
454 Sqlite(rusqlite::Error),
455 /// A terminal kernel error — never retried.
456 Terminal(KnlError),
457}
458
459/// Hand a job's outcome to the isle in the shape it expects: SQLite's errors
460/// as errors (retryable), the kernel's as a value (terminal).
461fn finish<T>(outcome: Result<T, JobError>) -> Result<KnlResult<T>, rusqlite::Error> {
462 match outcome {
463 Ok(value) => Ok(Ok(value)),
464 Err(JobError::Sqlite(error)) => Err(error),
465 Err(JobError::Terminal(error)) => Ok(Err(error)),
466 }
467}
468
469/// Whether a rusqlite error is a retryable lock contention (matched on the
470/// SQLite error *code*, never the message text).
471fn is_retryable(error: &rusqlite::Error) -> bool {
472 matches!(
473 error,
474 rusqlite::Error::SqliteFailure(inner, _)
475 if matches!(
476 inner.code,
477 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
478 )
479 )
480}
481
482/// A durable [`EventStore`] backed by SQLite, scoped to one `stream`.
483///
484/// The session *is* the stream: one instance serves one session's log.
485/// Several instances may point at the same DB file with different streams.
486pub struct SqliteEventStore {
487 /// The handle on the thread that owns the writing connection.
488 ///
489 /// Held for the store's whole life, which for an in-memory database is
490 /// not merely convenient: a shared-cache in-memory database exists only
491 /// while a connection to it is open, so this isle *is* the database.
492 writer: AsyncIsle,
493 /// Where the database is, so a second connection can be opened to it.
494 db: Db,
495 /// The read-only isle, started on the first query and reused.
496 ///
497 /// Lazy because most sessions never run one: a store that only appends
498 /// and folds pays nothing — not even a thread — for the read side
499 /// existing. A [`tokio::sync::OnceCell`] rather than the `std` one
500 /// because opening it is now an `await`, and because the cell has to stay
501 /// `Sync` for the store's `&self` reads to be `Send` futures.
502 reader: OnceCell<AsyncIsle>,
503 /// Where the drivers of both threads went, so the reader can park its own
504 /// when it is opened.
505 drivers: IsleDrivers,
506 /// The stream this store is scoped to — the session id.
507 stream: String,
508 /// The identity of the database, computed once at open ([`Db::id`]) so
509 /// [`EventStore::database`] can hand back a borrow of it.
510 db_id: String,
511}
512
513impl SqliteEventStore {
514 /// Open (creating if absent) the DB at `path`, scoped to `stream`.
515 ///
516 /// The `events` table is created if it does not exist, so opening a fresh
517 /// file and reopening an existing one take the same path.
518 ///
519 /// `drivers` takes ownership of the connection thread this starts (and of
520 /// the read thread, if a query ever opens one): see [`IsleDrivers`].
521 pub async fn open(
522 path: &Path,
523 stream: impl Into<String>,
524 drivers: &IsleDrivers,
525 ) -> KnlResult<Self> {
526 Self::init(Db::File(path.to_path_buf()), stream.into(), drivers).await
527 }
528
529 /// Open an in-memory database for `stream`.
530 ///
531 /// The database is named after the stream and opened in shared-cache
532 /// mode, so the read connection reaches the same rows the writer wrote —
533 /// and so reopening the same stream id in the same process finds the same
534 /// log. It lives as long as a connection to it is open, which is until
535 /// `drivers` is shut down.
536 pub async fn open_memory(stream: impl Into<String>, drivers: &IsleDrivers) -> KnlResult<Self> {
537 let stream = stream.into();
538 Self::init(Db::Memory(Db::memory_uri(&stream)), stream, drivers).await
539 }
540
541 /// Start the writing isle — which sets the busy timeout, applies the WAL
542 /// preset and ensures the table and its indexes before it takes a job.
543 async fn init(db: Db, stream: String, drivers: &IsleDrivers) -> KnlResult<Self> {
544 let writer = db.spawn_writer(drivers).await?;
545 let db_id = db.id();
546 Ok(Self {
547 writer,
548 db,
549 reader: OnceCell::new(),
550 drivers: drivers.clone(),
551 stream,
552 db_id,
553 })
554 }
555
556 /// The read-only isle, started on first use.
557 ///
558 /// A *second* connection to the same database, on a thread of its own and
559 /// with no write capability: `SQLITE_OPEN_READ_ONLY` is what SQLite was
560 /// asked for, and `query_only` is the same answer said again inside the
561 /// connection, so a statement that slipped past the checks on the text
562 /// still has nothing to write with.
563 ///
564 /// A failed open is not remembered: the cell stays empty, so the next
565 /// query tries again rather than reporting the first failure forever.
566 async fn reader(&self) -> KnlResult<&AsyncIsle> {
567 self.reader
568 .get_or_try_init(|| self.db.spawn_reader(&self.drivers))
569 .await
570 }
571
572 /// The columns of the `events` table, as SQLite reports them.
573 ///
574 /// Read through the *reader*, because this is the read contract: what a
575 /// caller's SQL may name. `PRAGMA table_info` rather than a list written
576 /// out here, so the published schema cannot drift from the table.
577 pub async fn schema(&self) -> KnlResult<Vec<SchemaColumn>> {
578 self.reader()
579 .await?
580 .call(schema_of)
581 .await
582 .map_err(KnlError::from)
583 }
584}
585
586/// `PRAGMA table_info(events)`, as [`SchemaColumn`]s.
587///
588/// One reader for both callers: a live store's [`SqliteEventStore::schema`],
589/// which runs it on the reading isle, and [`events_schema`], which runs it on
590/// a connection of its own.
591fn schema_of(conn: &mut Connection) -> rusqlite::Result<Vec<SchemaColumn>> {
592 let mut stmt = conn.prepare(&format!("PRAGMA table_info({EVENTS_TABLE})"))?;
593 let rows = stmt.query_map([], |row| {
594 Ok(SchemaColumn {
595 name: row.get::<_, String>("name")?,
596 declared_type: row.get::<_, String>("type")?,
597 pk: row.get::<_, i64>("pk")? > 0,
598 })
599 })?;
600 rows.collect::<rusqlite::Result<Vec<_>>>()
601}
602
603/// The columns of the `events` table, without a session to ask.
604///
605/// The schema is a property of the kernel, not of any one log, so this creates
606/// the table in a private in-memory database and reads it straight back — the
607/// same `PRAGMA table_info` a caller's own store would answer with. It is
608/// what `knl.api()` publishes.
609///
610/// Deliberately **not** async, and deliberately not an isle. It opens a
611/// nameless in-memory database, runs `CREATE TABLE IF NOT EXISTS` and one
612/// pragma against it, and drops it: no file is touched, no lock can be
613/// contended, and no thread is started, so there is nothing here for the
614/// caller to wait on. That is what keeps `knl.api()` a synchronous call —
615/// a declaration of the surface should not have to be awaited — while the
616/// rule that the VM thread never waits on the OS still holds, because this
617/// never reaches the OS.
618pub fn events_schema() -> KnlResult<Vec<SchemaColumn>> {
619 let mut conn = Connection::open_in_memory().map_err(KnlError::from)?;
620 conn.execute_batch(SCHEMA_DDL).map_err(KnlError::from)?;
621 schema_of(&mut conn).map_err(KnlError::from)
622}
623
624/// The kinds a read was asked for, owned, so the selection can be sent to the
625/// isle's thread along with the closure that uses it.
626fn owned_kinds(kinds: Option<&[&str]>) -> Option<Vec<String>> {
627 kinds.map(|kinds| kinds.iter().map(|kind| (*kind).to_string()).collect())
628}
629
630#[async_trait]
631impl EventStore for SqliteEventStore {
632 async fn append(&mut self, mut event: Map<String, Value>) -> KnlResult<Committed> {
633 // Reject before touching the stream: a rejected event burns no seq.
634 validate_event(&event)?;
635 // Stamp the schema version once, before the job is submitted; the
636 // kernel-owned seq / epoch_ms are stamped per attempt inside the
637 // transaction, recomputed from the live head each time.
638 stamp_schema_version(&mut event);
639 let stream = self.stream.clone();
640 self.writer
641 .call_retry(retry_policy(), move |conn| {
642 finish(append_in(conn, &stream, &event))
643 })
644 .await
645 .map_err(KnlError::from)?
646 }
647
648 async fn append_many(&mut self, events: Vec<Map<String, Value>>) -> KnlResult<Vec<Committed>> {
649 if events.is_empty() {
650 return Ok(Vec::new());
651 }
652 // Validate before the transaction is opened: a batch with a malformed
653 // event in it never takes the write lock at all.
654 for event in &events {
655 validate_event(event)?;
656 }
657 // One IMMEDIATE transaction for the whole batch, so the facts that
658 // belong together land together. A contended attempt is retried
659 // whole; nothing outside the transaction has been changed by a failed
660 // one, so re-running it is the correct thing to do.
661 let stream = self.stream.clone();
662 self.writer
663 .call_retry(retry_policy(), move |conn| {
664 finish(append_many_in(conn, &stream, &events))
665 })
666 .await
667 .map_err(KnlError::from)?
668 }
669
670 async fn append_if(
671 &mut self,
672 kinds: Option<&[&str]>,
673 decide: Decision,
674 ) -> KnlResult<Option<Committed>> {
675 // The read, the decision and the insert share one IMMEDIATE
676 // transaction, so the invariant `decide` checks holds at the instant
677 // the event lands — and all three now run in one job, on the isle's
678 // own thread, because the decision is an owned `Send` closure that
679 // travels with it. The channel round trip the borrowed form needed
680 // (job asks, caller answers, both waiting on each other with the write
681 // lock held) is gone with it.
682 let stream = self.stream.clone();
683 let kinds = owned_kinds(kinds);
684 self.writer
685 .call(move |conn| finish(append_if_in(conn, &stream, kinds.as_deref(), decide)))
686 .await
687 .map_err(KnlError::from)?
688 }
689
690 async fn append_if_many(
691 &mut self,
692 other: &str,
693 kinds: Option<&[&str]>,
694 decide: SplitDecision,
695 ) -> KnlResult<Option<Split<Committed>>> {
696 // One IMMEDIATE transaction over both streams, exactly as
697 // `append_if` takes one over this stream: they are rows of the same
698 // table on the same connection, so "two streams" costs the write
699 // nothing beyond a second `MAX(seq)`. Not retried, for the reason
700 // `append_if` is not — the decision is a `FnOnce` and an attempt
701 // consumes it.
702 let stream = self.stream.clone();
703 let other = other.to_string();
704 let kinds = owned_kinds(kinds);
705 self.writer
706 .call(move |conn| {
707 finish(append_if_many_in(
708 conn,
709 &stream,
710 &other,
711 kinds.as_deref(),
712 decide,
713 ))
714 })
715 .await
716 .map_err(KnlError::from)?
717 }
718
719 async fn append_with_open_children(
720 &mut self,
721 scan: &ChildScan,
722 decide: ChildrenDecision,
723 ) -> KnlResult<Committed> {
724 // The scan reads other streams and the insert writes this one, so
725 // they share the IMMEDIATE transaction: what the boundary records is
726 // what was true at the instant it landed, not a moment before it.
727 let stream = self.stream.clone();
728 let scan = scan.clone();
729 self.writer
730 .call(move |conn| finish(append_with_open_children_in(conn, &stream, &scan, decide)))
731 .await
732 .map_err(KnlError::from)?
733 }
734
735 fn database(&self) -> Option<&str> {
736 Some(&self.db_id)
737 }
738
739 async fn read_kinds(
740 &self,
741 kinds: Option<&[&str]>,
742 from_seq: u64,
743 limit: usize,
744 ) -> KnlResult<Vec<Value>> {
745 // An empty selection selects nothing — and `kind IN ()` is not SQL,
746 // so it is answered here rather than built into a statement.
747 if kinds.is_some_and(<[&str]>::is_empty) {
748 return Ok(Vec::new());
749 }
750 // `usize::MAX` (an unbounded read) caps at i64::MAX, which SQLite
751 // treats as "no limit"; `0` reads nothing.
752 let capped = i64::try_from(limit).unwrap_or(i64::MAX);
753 let kinds = owned_kinds(kinds);
754 let stream = self.stream.clone();
755 self.writer
756 .call(move |conn| finish(read_in(conn, &stream, kinds.as_deref(), from_seq, capped)))
757 .await
758 .map_err(KnlError::from)?
759 }
760
761 async fn read_last(&self, n: usize) -> KnlResult<Vec<Value>> {
762 // `usize::MAX` caps at i64::MAX, which SQLite treats as "no limit";
763 // `0` reads nothing. Same convention as `read_kinds` above.
764 let capped = i64::try_from(n).unwrap_or(i64::MAX);
765 let stream = self.stream.clone();
766 self.writer
767 .call(move |conn| finish(read_last_in(conn, &stream, capped)))
768 .await
769 .map_err(KnlError::from)?
770 }
771
772 async fn head(&self) -> KnlResult<Option<u64>> {
773 // A transient busy read must surface, not read as "empty": a caller
774 // deciding open-vs-resume (or a CAS) on a swallowed error would
775 // treat a populated stream as fresh. Same discipline as read().
776 let stream = self.stream.clone();
777 self.writer
778 .call(move |conn| head_in(conn, &stream))
779 .await
780 .map_err(KnlError::from)
781 }
782
783 async fn len(&self) -> KnlResult<usize> {
784 let stream = self.stream.clone();
785 self.writer
786 .call(move |conn| {
787 conn.query_row(
788 "SELECT COUNT(*) FROM events WHERE stream = ?1",
789 params![stream],
790 |row| row.get::<_, i64>(0),
791 )
792 })
793 .await
794 .map(|n| n as usize)
795 .map_err(KnlError::from)
796 }
797
798 async fn query(&self, plan: &QueryPlan) -> KnlResult<QueryRows> {
799 // The deadline is the isle's: it interrupts the statement when the
800 // time is up and reports `Timeout`, so there is no watchdog thread
801 // here to outlive the query it was watching.
802 let timeout = plan.timeout;
803 let plan = plan.clone();
804 self.reader()
805 .await?
806 .call_timeout(timeout, move |conn| Ok(run_query(conn, &plan)))
807 .await
808 .map_err(KnlError::from)?
809 }
810
811 fn detach_append(&self, mut event: Map<String, Value>) {
812 // The drop backstop's path, and the one write nobody awaits. A
813 // handle that was collected has no caller left to raise to and no
814 // task left to wait in, so the job is handed to the isle and let go
815 // of: the thread runs it because its driver outlives every session
816 // ([`IsleDrivers`]), and the boundary lands before the host drains
817 // that thread at shutdown.
818 if let Err(e) = validate_event(&event) {
819 tracing::warn!(error = %e, "knl: a detached append was refused before it was submitted");
820 return;
821 }
822 stamp_schema_version(&mut event);
823 let stream = self.stream.clone();
824 // `detach`, not a dropped task: dropping an `AsyncTask` cancels the
825 // job it stands for, which would throw away the very event this
826 // exists to record.
827 self.writer
828 .spawn_call(move |conn| finish(append_in(conn, &stream, &event)))
829 .detach();
830 }
831}
832
833/// A query's own translation of a rusqlite failure.
834///
835/// The write path's [`From<rusqlite::Error>`] answers a different question —
836/// "can this write be retried" — and has no reason to know about deadlines.
837/// Here there are two more outcomes a caller can act on: a statement the
838/// watchdog cut short is [`KnlError::Timeout`] (the query was too slow, not
839/// the store too busy), and a value that came back and would not read as what
840/// it is declared to be is [`KnlError::Corruption`] — the IO worked, so what
841/// is wrong is the data. Matched on the error's shape, never on message text.
842fn query_error(error: rusqlite::Error) -> KnlError {
843 if let rusqlite::Error::SqliteFailure(inner, _) = &error {
844 if inner.code == rusqlite::ErrorCode::OperationInterrupted {
845 return KnlError::Timeout(format!("query interrupted: {error}"));
846 }
847 }
848 match error {
849 rusqlite::Error::Utf8Error(_)
850 | rusqlite::Error::FromSqlConversionFailure(..)
851 | rusqlite::Error::IntegralValueOutOfRange(..) => {
852 KnlError::Corruption(format!("sqlite: query: {error}"))
853 }
854 other => KnlError::from(other),
855 }
856}
857
858/// Prepare, check, bind and run one query.
859///
860/// Runs on the reading isle's thread, under the deadline that thread was given
861/// ([`EventStore::query`]): SQLite has no per-statement timeout — `busy_timeout`
862/// bounds waiting for a *lock*, which is a different thing from a statement
863/// that is simply expensive — so the isle interrupts the connection when the
864/// time is up. The interrupt reaches this function as `SQLITE_INTERRUPT` on
865/// whichever step was running, and [`query_error`] names it a timeout.
866fn run_query(conn: &Connection, plan: &QueryPlan) -> KnlResult<QueryRows> {
867 // A statement that will not compile is the caller's SQL, not the store
868 // failing: report it as the refusal it is, unless the database was too
869 // busy to answer at all.
870 let mut stmt = conn.prepare(&plan.sql).map_err(|error| {
871 if is_retryable(&error) {
872 KnlError::from(error)
873 } else {
874 KnlError::Validation(format!("sql: {error}"))
875 }
876 })?;
877 // The second of the three guards (the text was checked before this, the
878 // connection has no write capability at all): SQLite's own answer to
879 // "does this statement change the database".
880 if !stmt.readonly() {
881 return Err(KnlError::Validation(
882 "a query may not write; only SELECT / WITH statements are run".to_string(),
883 ));
884 }
885 bind(&mut stmt, plan)?;
886
887 // Taken before the rows borrow the statement, and owned, so the columns
888 // outlive the borrow.
889 let columns: Vec<String> = stmt
890 .column_names()
891 .into_iter()
892 .map(str::to_string)
893 .collect();
894
895 let mut rows = stmt.raw_query();
896 let mut out = Vec::new();
897 while out.len() < plan.limit {
898 let Some(row) = rows.next().map_err(query_error)? else {
899 // The whole result set fitted.
900 return Ok(QueryRows {
901 rows: out,
902 truncated: false,
903 });
904 };
905 let mut record = Map::new();
906 for (index, column) in columns.iter().enumerate() {
907 // A NULL is an absent key rather than a null value: the Lua side
908 // reads it as `nil`, which is what a missing column means there.
909 if let Some(value) = read_value(row.get_ref(index).map_err(query_error)?)? {
910 record.insert(column.clone(), value);
911 }
912 }
913 out.push(record);
914 }
915 // The cap was reached: whether anything was actually cut off is one more
916 // step, so a result that happens to be exactly `limit` long is not
917 // reported as truncated.
918 let truncated = rows.next().map_err(query_error)?.is_some();
919 Ok(QueryRows {
920 rows: out,
921 truncated,
922 })
923}
924
925/// Bind every parameter the statement declares.
926///
927/// Driven by the *statement*, not by the caller's table: SQLite is asked what
928/// parameters it compiled and each one is answered, so a value that matches
929/// nothing and a parameter that nothing matches are both errors instead of a
930/// silent NULL. The reserved names ([`STREAM_PARAM`] and the
931/// `:knl_sessions_*` slots [`super::query`] wrote) are the kernel's;
932/// everything else is looked up in what the caller passed.
933fn bind(stmt: &mut rusqlite::Statement<'_>, plan: &QueryPlan) -> KnlResult<()> {
934 const NO_VALUES: &[Value] = &[];
935
936 let slots: Vec<String> = (0..plan.sessions.len()).map(session_slot).collect();
937 let given: &[Value] = match &plan.params {
938 QueryParams::Positional(values) => values,
939 _ => NO_VALUES,
940 };
941 let mut taken = 0;
942
943 for index in 1..=stmt.parameter_count() {
944 // Read out as an owned name first: the borrow of the statement ends
945 // here, so the binding below can take it mutably.
946 let name = stmt.parameter_name(index).map(str::to_string);
947 let Some(name) = name else {
948 // An anonymous `?`: the caller's, in the order they were given.
949 // Every parameter the kernel wrote is named, so there is nothing
950 // of ours here to confuse them with.
951 let value = given.get(taken).ok_or_else(|| {
952 KnlError::Validation(format!(
953 "the query has more `?` parameters than the {} value(s) given",
954 given.len()
955 ))
956 })?;
957 taken += 1;
958 stmt.raw_bind_parameter(index, SqlParam(value.clone()))
959 .map_err(query_error)?;
960 continue;
961 };
962
963 if name == STREAM_PARAM {
964 stmt.raw_bind_parameter(index, plan.stream.clone())
965 .map_err(query_error)?;
966 continue;
967 }
968 if let Some(slot) = slots.iter().position(|slot| *slot == name) {
969 stmt.raw_bind_parameter(index, plan.sessions[slot].clone())
970 .map_err(query_error)?;
971 continue;
972 }
973
974 let QueryParams::Named(named) = &plan.params else {
975 return Err(KnlError::Validation(format!(
976 "the query names the parameter {name:?}, so params must be a table of names \
977 to values"
978 )));
979 };
980 // The prefix character is SQLite's, not the caller's: `:kind` is
981 // answered by `kind`. The full spelling is accepted too, for a
982 // caller that writes what it sees.
983 let value = named
984 .get(&name[1..])
985 .or_else(|| named.get(&name))
986 .ok_or_else(|| {
987 KnlError::Validation(format!("no value was given for the parameter {name:?}"))
988 })?;
989 stmt.raw_bind_parameter(index, SqlParam(value.clone()))
990 .map_err(query_error)?;
991 }
992
993 if given.len() > taken {
994 return Err(KnlError::Validation(format!(
995 "{} value(s) were given for {taken} `?` parameter(s)",
996 given.len()
997 )));
998 }
999 Ok(())
1000}
1001
1002/// A caller's JSON value on its way into a bound parameter.
1003///
1004/// The four SQLite types a JSON value maps onto without inventing anything:
1005/// null, integer, real, text. A composite — an array or an object — is not a
1006/// SQLite value, and encoding one as its JSON text would be the kernel
1007/// guessing what the caller meant, so it is refused.
1008struct SqlParam(Value);
1009
1010impl rusqlite::ToSql for SqlParam {
1011 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
1012 use rusqlite::types::ToSqlOutput;
1013 let value = match &self.0 {
1014 Value::Null => SqlValue::Null,
1015 Value::Bool(b) => SqlValue::Integer(i64::from(*b)),
1016 Value::Number(n) => {
1017 if let Some(i) = n.as_i64() {
1018 SqlValue::Integer(i)
1019 } else if let Some(f) = n.as_f64() {
1020 SqlValue::Real(f)
1021 } else {
1022 return Err(rusqlite::Error::ToSqlConversionFailure(
1023 format!("{n} is not a SQLite number").into(),
1024 ));
1025 }
1026 }
1027 Value::String(s) => SqlValue::Text(s.clone()),
1028 other => {
1029 return Err(rusqlite::Error::ToSqlConversionFailure(
1030 format!("a {} is not a SQLite value", type_name_of(other)).into(),
1031 ));
1032 }
1033 };
1034 Ok(ToSqlOutput::Owned(value))
1035 }
1036}
1037
1038/// What kind of JSON value this is, for a refusal message.
1039fn type_name_of(value: &Value) -> &'static str {
1040 match value {
1041 Value::Null => "null",
1042 Value::Bool(_) => "boolean",
1043 Value::Number(_) => "number",
1044 Value::String(_) => "string",
1045 Value::Array(_) => "list",
1046 Value::Object(_) => "table",
1047 }
1048}
1049
1050/// One column of one row, as JSON — or `None` for `NULL`.
1051///
1052/// INTEGER and REAL come back as numbers, TEXT as a string. A BLOB comes
1053/// back as a string too, lossily: the boundary above this one is Lua, whose
1054/// strings are byte strings, and refusing the row would make a column nobody
1055/// selected on purpose fatal. A TEXT column that is not UTF-8 is a different
1056/// matter — it was declared to be text and it is not — so that is corruption.
1057/// A REAL that is NaN or infinite has no representation on the other side of
1058/// this boundary, and dropping it would hand back a row with a column
1059/// silently missing, so it is raised instead.
1060fn read_value(value: ValueRef<'_>) -> KnlResult<Option<Value>> {
1061 Ok(match value {
1062 ValueRef::Null => None,
1063 ValueRef::Integer(i) => Some(Value::from(i)),
1064 ValueRef::Real(f) => {
1065 let number = serde_json::Number::from_f64(f).ok_or_else(|| {
1066 KnlError::Storage(format!(
1067 "sqlite: a REAL column is {f}, which has no value on the other side of the \
1068 bridge"
1069 ))
1070 })?;
1071 Some(Value::Number(number))
1072 }
1073 ValueRef::Text(bytes) => {
1074 let text = std::str::from_utf8(bytes).map_err(|e| {
1075 KnlError::Corruption(format!("sqlite: a TEXT column is not valid UTF-8: {e}"))
1076 })?;
1077 Some(Value::from(text))
1078 }
1079 ValueRef::Blob(bytes) => Some(Value::from(String::from_utf8_lossy(bytes).into_owned())),
1080 })
1081}
1082
1083/// One `IMMEDIATE` append: take the reserved lock up front, compute the next
1084/// `seq` from the live head, stamp and insert, then commit.
1085///
1086/// Runs on the writing isle's thread, and may run more than once: a contended
1087/// `BEGIN` is a [`JobError::Sqlite`], which is what the isle's retry keys on,
1088/// and nothing outside the transaction was changed by an attempt that failed.
1089fn append_in(
1090 conn: &mut Connection,
1091 stream: &str,
1092 event: &Map<String, Value>,
1093) -> Result<Committed, JobError> {
1094 let tx = conn
1095 .transaction_with_behavior(TransactionBehavior::Immediate)
1096 .map_err(JobError::Sqlite)?;
1097 let seq = next_seq(&tx, stream).map_err(JobError::Sqlite)?;
1098 let epoch_ms = now_ms();
1099 let mut row = event.clone();
1100 stamp(&mut row, seq, epoch_ms);
1101 insert_row(&tx, stream, seq, epoch_ms, &row)?;
1102 tx.commit().map_err(JobError::Sqlite)?;
1103 Ok(Committed { seq, epoch_ms })
1104}
1105
1106/// One `IMMEDIATE` batch append: take the reserved lock up front, number the
1107/// events on from the live head, and insert them all before committing.
1108///
1109/// All or nothing: an event that will not encode, or a contended insert
1110/// part-way through, drops the transaction and leaves the stream exactly as
1111/// it was — which is what lets a caller write two facts that are one fact.
1112fn append_many_in(
1113 conn: &mut Connection,
1114 stream: &str,
1115 events: &[Map<String, Value>],
1116) -> Result<Vec<Committed>, JobError> {
1117 let tx = conn
1118 .transaction_with_behavior(TransactionBehavior::Immediate)
1119 .map_err(JobError::Sqlite)?;
1120 let committed = insert_batch(&tx, stream, events)?;
1121 tx.commit().map_err(JobError::Sqlite)?;
1122 Ok(committed)
1123}
1124
1125/// Number `events` on from `stream`'s live head, stamp them and insert them,
1126/// inside a transaction the caller opened and commits.
1127///
1128/// The one numbering rule for every batch a transaction writes — a plain
1129/// [`append_many_in`], and each side of an allocation
1130/// ([`append_if_many_in`]) — so the second stream of a two-stream write is
1131/// numbered exactly as the first is: from its own head, which is what makes
1132/// `seq` per-stream rather than per-transaction.
1133///
1134/// It validates, because an event that reaches here has not always been
1135/// checked: a decision's events are the decision's, and one it built wrong
1136/// must not be the first thing a stream carries. A [`JobError::Terminal`]
1137/// drops the transaction, so a batch that fails part-way writes nothing.
1138fn insert_batch(
1139 tx: &Connection,
1140 stream: &str,
1141 events: &[Map<String, Value>],
1142) -> Result<Vec<Committed>, JobError> {
1143 let mut seq = next_seq(tx, stream).map_err(JobError::Sqlite)?;
1144 let mut committed = Vec::with_capacity(events.len());
1145 for event in events {
1146 validate_event(event).map_err(JobError::Terminal)?;
1147 let epoch_ms = now_ms();
1148 let mut row = event.clone();
1149 stamp_schema_version(&mut row);
1150 stamp(&mut row, seq, epoch_ms);
1151 insert_row(tx, stream, seq, epoch_ms, &row)?;
1152 committed.push(Committed { seq, epoch_ms });
1153 seq = seq.saturating_add(1);
1154 }
1155 Ok(committed)
1156}
1157
1158/// One `IMMEDIATE` decide-then-append over two streams: read this stream,
1159/// ask the decision what to record where, and insert both sides before
1160/// committing.
1161///
1162/// The two-stream twin of [`append_if_in`], and the reason it exists is the
1163/// atomicity rather than the convenience: an allocation is a move between two
1164/// ledgers, and a reader that met one side without the other would be reading
1165/// units that had left one balance without arriving in the other. Both
1166/// streams are rows of the same table on this one connection, so the same
1167/// transaction covers them.
1168///
1169/// A `None` decision commits nothing. A [`Split`] with an empty `other`
1170/// writes only this stream, which is how a refusal is recorded: the fact that
1171/// the allocation was asked for and turned down, with no child opened.
1172///
1173/// Both streams are *read* as well, and the second one for a single question:
1174/// whether it carries anything at all. One row settles it, so the decision is
1175/// shown at most the other stream's first event — inside this transaction,
1176/// which is the whole point: a command whose invariant is "the target is
1177/// empty" cannot ask before taking the write lock, or a second one would
1178/// answer the same and both would write.
1179fn append_if_many_in(
1180 conn: &mut Connection,
1181 stream: &str,
1182 other: &str,
1183 kinds: Option<&[String]>,
1184 decide: SplitDecision,
1185) -> Result<Option<Split<Committed>>, JobError> {
1186 let tx = conn
1187 .transaction_with_behavior(TransactionBehavior::Immediate)
1188 .map_err(JobError::Sqlite)?;
1189 let seen = Split {
1190 own: read_in(&tx, stream, kinds, 0, i64::MAX)?,
1191 // Unfiltered and capped at one: the question is "is there an event",
1192 // not "which", so a kind filter could only make an occupied stream
1193 // look empty.
1194 other: read_in(&tx, other, None, 0, 1)?,
1195 };
1196 let Some(split) = decide(seen) else {
1197 // Nothing to write: the transaction is rolled back on drop.
1198 return Ok(None);
1199 };
1200 let own = insert_batch(&tx, stream, &split.own)?;
1201 let other = insert_batch(&tx, other, &split.other)?;
1202 tx.commit().map_err(JobError::Sqlite)?;
1203 Ok(Some(Split { own, other }))
1204}
1205
1206/// One `IMMEDIATE` scan-then-append: find the streams this one is the parent
1207/// of that have not ended, hand them to the decision, and insert the event it
1208/// builds.
1209///
1210/// The scan is inside the transaction on purpose. Asked before the write, it
1211/// would answer about a moment the boundary does not land in — a child could
1212/// end, or a new one open, in between — and a `session_closed` that named a
1213/// child which had already closed would be a record of something that never
1214/// happened.
1215fn append_with_open_children_in(
1216 conn: &mut Connection,
1217 stream: &str,
1218 scan: &ChildScan,
1219 decide: ChildrenDecision,
1220) -> Result<Committed, JobError> {
1221 let tx = conn
1222 .transaction_with_behavior(TransactionBehavior::Immediate)
1223 .map_err(JobError::Sqlite)?;
1224 let children = open_children_in(&tx, stream, scan).map_err(JobError::Sqlite)?;
1225 let committed = insert_batch(&tx, stream, &[decide(children)])?;
1226 tx.commit().map_err(JobError::Sqlite)?;
1227 // One event in, one out: `insert_batch` numbers what it is given, and it
1228 // was given exactly one.
1229 committed
1230 .into_iter()
1231 .next()
1232 .ok_or_else(|| JobError::Terminal(KnlError::Storage("the close wrote nothing".to_string())))
1233}
1234
1235/// `text` as an SQL string literal, with any quote in it doubled.
1236///
1237/// For the two places a *word* rather than a value has to go into a statement
1238/// ([`child_scan_sql`]): `json_extract`'s path argument is not a value SQLite
1239/// will take a parameter for, and a term the planner has to compare against a
1240/// partial index's `WHERE` cannot be one either. Doubling is the whole of
1241/// SQLite's escaping rule for a single-quoted literal, so this closes the hole
1242/// that interpolating text otherwise opens.
1243fn sql_literal(text: &str) -> String {
1244 format!("'{}'", text.replace('\'', "''"))
1245}
1246
1247/// The statement [`open_children_in`] runs, with the scan's two words written
1248/// into it as literals.
1249///
1250/// The kind and the JSON path are literals rather than parameters *so that the
1251/// planner can see them*: `events_session_opened_parent` ([`SCHEMA_DDL`]) is a
1252/// partial index on an expression, and both halves are matched by form — a
1253/// `kind = ?` term proves nothing about `WHERE kind = 'session_opened'`, and a
1254/// bound path never matches an indexed one. The parent being looked for stays
1255/// a parameter, because it is a value. A test holds the query plan against
1256/// the index name, so this cannot quietly become a table scan again.
1257fn child_scan_sql(scan: &ChildScan) -> String {
1258 let opened = sql_literal(&scan.opened);
1259 let closed = sql_literal(&scan.closed);
1260 let path = sql_literal(&format!("$.{}", scan.parent_field));
1261 format!(
1262 "SELECT opened.stream \
1263 FROM events AS opened \
1264 WHERE opened.kind = {opened} \
1265 AND json_extract(opened.data, {path}) = ?1 \
1266 AND NOT EXISTS ( \
1267 SELECT 1 FROM events AS ending \
1268 WHERE ending.stream = opened.stream AND ending.kind = {closed} \
1269 ) \
1270 ORDER BY opened.epoch_ms, opened.stream"
1271 )
1272}
1273
1274/// The streams that name `stream` as their parent and carry no ending.
1275///
1276/// The vocabulary is the caller's ([`ChildScan`]): which kind opens a stream,
1277/// which kind ends one, and where in the opening's `data` the parent is
1278/// named. Those three words are written into the statement
1279/// ([`child_scan_sql`]) rather than bound, which is what lets the scan read by
1280/// `events_session_opened_parent` instead of walking every event in the
1281/// database.
1282///
1283/// Ordered by when each child opened, so a close records its children in the
1284/// order they were started rather than in whatever order the rows came back.
1285fn open_children_in(
1286 conn: &Connection,
1287 stream: &str,
1288 scan: &ChildScan,
1289) -> rusqlite::Result<Vec<String>> {
1290 let mut stmt = conn.prepare(&child_scan_sql(scan))?;
1291 let rows = stmt.query_map(params![stream], |row| row.get::<_, String>(0))?;
1292 rows.collect()
1293}
1294
1295/// One `IMMEDIATE` decide-then-append: read the stream, ask the caller's
1296/// closure what to record, and insert its answer in the same transaction.
1297///
1298/// The decision travels *with* the job — it is owned and `Send` — so it runs
1299/// here, on the isle's thread, between the read and the insert, with the write
1300/// lock held throughout. Nothing waits on anything else: the caller's task is
1301/// suspended on the job's own oneshot and there is no second channel for the
1302/// two sides to deadlock across.
1303///
1304/// `kinds` narrows what the decision is shown, not where its answer lands:
1305/// the new event's `seq` comes from the stream's live head, so a filtered
1306/// decision numbers its write against everything, exactly as an ordinary
1307/// append does.
1308///
1309/// A `None` decision commits nothing — the transaction is dropped, so the
1310/// stream is exactly as it was — and reports `Ok(None)`.
1311fn append_if_in(
1312 conn: &mut Connection,
1313 stream: &str,
1314 kinds: Option<&[String]>,
1315 decide: Decision,
1316) -> Result<Option<Committed>, JobError> {
1317 let tx = conn
1318 .transaction_with_behavior(TransactionBehavior::Immediate)
1319 .map_err(JobError::Sqlite)?;
1320 let events = read_in(&tx, stream, kinds, 0, i64::MAX)?;
1321 let Some(event) = decide(events) else {
1322 // Nothing to write: the transaction is rolled back on drop.
1323 return Ok(None);
1324 };
1325 // The decision's event is validated like any other: a malformed one is
1326 // refused and the transaction goes no further.
1327 validate_event(&event).map_err(JobError::Terminal)?;
1328 // The head of the whole stream, not of the events the decision was shown:
1329 // a filtered read says nothing about where the next event goes.
1330 let seq = next_seq(&tx, stream).map_err(JobError::Sqlite)?;
1331 let epoch_ms = now_ms();
1332 let mut row = event.clone();
1333 stamp_schema_version(&mut row);
1334 stamp(&mut row, seq, epoch_ms);
1335 insert_row(&tx, stream, seq, epoch_ms, &row)?;
1336 tx.commit().map_err(JobError::Sqlite)?;
1337 Ok(Some(Committed { seq, epoch_ms }))
1338}
1339
1340/// The columns a read selects, in the order [`read_row`] takes them.
1341const READ_COLUMNS: &str = "seq, epoch_ms, kind, schema_version, beat, meta, data";
1342
1343/// One stored row, as its columns come back from SQLite.
1344///
1345/// The raw values, before the two JSON columns are decoded: reading and
1346/// decoding are separate so a fault on the read (retryable) and a value that
1347/// will not decode (corruption) stay two different answers.
1348struct StoredRow {
1349 /// The store-assigned sequence number.
1350 seq: i64,
1351 /// The wall-clock append time the store stamped.
1352 epoch_ms: i64,
1353 /// The event's kind.
1354 kind: String,
1355 /// The shape the event was written under.
1356 schema_version: i64,
1357 /// The beat the caller declared, if it declared one.
1358 beat: Option<String>,
1359 /// The shallow `meta` object, as stored text.
1360 meta: String,
1361 /// The kind's own `data` object, as stored text.
1362 data: String,
1363}
1364
1365/// Take one row's columns, in [`READ_COLUMNS`] order.
1366fn read_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredRow> {
1367 Ok(StoredRow {
1368 seq: row.get(0)?,
1369 epoch_ms: row.get(1)?,
1370 kind: row.get(2)?,
1371 schema_version: row.get(3)?,
1372 beat: row.get(4)?,
1373 meta: row.get(5)?,
1374 data: row.get(6)?,
1375 })
1376}
1377
1378/// Rebuild the event object a row was written from.
1379///
1380/// The inverse of [`insert_row`], and exactly that: the same keys in the same
1381/// envelope, so a caller reading a durable log sees what it wrote. An absent
1382/// `beat` is an absent key rather than a null — the kernel's rule is that a
1383/// beat is a string when it is there at all.
1384fn event_of(row: StoredRow) -> KnlResult<Value> {
1385 let meta = decode_object(&row.meta, FIELD_META)?;
1386 let data = decode_object(&row.data, FIELD_DATA)?;
1387
1388 let mut event = Map::new();
1389 event.insert(FIELD_KIND.to_string(), Value::from(row.kind));
1390 if let Some(beat) = row.beat {
1391 event.insert(FIELD_BEAT.to_string(), Value::from(beat));
1392 }
1393 event.insert(FIELD_META.to_string(), meta);
1394 event.insert(FIELD_DATA.to_string(), data);
1395 event.insert(FIELD_SEQ.to_string(), Value::from(row.seq as u64));
1396 event.insert(FIELD_EPOCH_MS.to_string(), Value::from(row.epoch_ms as u64));
1397 event.insert(
1398 SCHEMA_VERSION_FIELD.to_string(),
1399 Value::from(row.schema_version as u64),
1400 );
1401 Ok(Value::Object(event))
1402}
1403
1404/// Decode a stored JSON column, which must be an object.
1405///
1406/// Corruption rather than storage: the IO worked and the bytes came back, so
1407/// what is wrong is the data, and no retry changes it. A value that is not
1408/// an object is the same fault as one that will not parse — the store's own
1409/// writes are objects, so a scalar here came from the bytes.
1410fn decode_object(text: &str, column: &str) -> KnlResult<Value> {
1411 let value = serde_json::from_str::<Value>(text)
1412 .map_err(|e| KnlError::Corruption(format!("sqlite: corrupt event {column}: {e}")))?;
1413 if !value.is_object() {
1414 return Err(KnlError::Corruption(format!(
1415 "sqlite: corrupt event {column}: stored as {}, not a table",
1416 super::event::json_type_name(&value)
1417 )));
1418 }
1419 Ok(value)
1420}
1421
1422/// The read statement for a stream, with its bound arguments.
1423///
1424/// One builder for both read paths — the plain one and the in-transaction
1425/// twin — so a filtered read and the input a decision is shown select the
1426/// same rows by the same rule. The kinds are bound as parameters rather than
1427/// written into the SQL, so a kind is data here as it is everywhere else.
1428fn read_query(
1429 stream: &str,
1430 kinds: Option<&[String]>,
1431 from_seq: u64,
1432 limit: i64,
1433) -> (String, Vec<SqlValue>) {
1434 let mut sql = format!("SELECT {READ_COLUMNS} FROM events WHERE stream = ? AND seq >= ?");
1435 let mut args = vec![
1436 SqlValue::Text(stream.to_string()),
1437 SqlValue::Integer(from_seq as i64),
1438 ];
1439 if let Some(kinds) = kinds {
1440 let placeholders = vec!["?"; kinds.len()].join(", ");
1441 sql.push_str(&format!(" AND kind IN ({placeholders})"));
1442 args.extend(kinds.iter().map(|kind| SqlValue::Text(kind.clone())));
1443 }
1444 sql.push_str(" ORDER BY seq ASC LIMIT ?");
1445 args.push(SqlValue::Integer(limit));
1446 (sql, args)
1447}
1448
1449/// The statement for the *last* `n` events of a stream, with its bound
1450/// arguments.
1451///
1452/// `ORDER BY seq DESC LIMIT ?` — the index on `(stream, seq)` walks backwards
1453/// and stops at `n`, so a `tail` of five over a log of a million reads five
1454/// rows. The rows come back newest-first and are reversed by the caller
1455/// ([`read_last_in`]), because the SPI hands events over in `seq` order
1456/// whichever end they were read from.
1457fn read_last_query(stream: &str, n: i64) -> (String, Vec<SqlValue>) {
1458 (
1459 format!("SELECT {READ_COLUMNS} FROM events WHERE stream = ? ORDER BY seq DESC LIMIT ?"),
1460 vec![SqlValue::Text(stream.to_string()), SqlValue::Integer(n)],
1461 )
1462}
1463
1464/// The last `n` events of `stream`, in `seq` order, read on the isle's thread.
1465///
1466/// Decodes exactly as [`read_in`] does — a row that will not decode is
1467/// corruption and terminal — and reverses what SQLite handed back, so the
1468/// caller sees the same ordering a range read gives.
1469fn read_last_in(conn: &Connection, stream: &str, n: i64) -> Result<Vec<Value>, JobError> {
1470 let (sql, args) = read_last_query(stream, n);
1471 let mut stmt = conn.prepare(&sql).map_err(JobError::Sqlite)?;
1472 let rows = stmt
1473 .query_map(params_from_iter(args.iter()), read_row)
1474 .map_err(JobError::Sqlite)?;
1475 let mut events = Vec::new();
1476 for row in rows {
1477 let row = row.map_err(JobError::Sqlite)?;
1478 events.push(event_of(row).map_err(JobError::Terminal)?);
1479 }
1480 events.reverse();
1481 Ok(events)
1482}
1483
1484/// The events of `stream`, in `seq` order, read on the isle's thread.
1485///
1486/// The one read both paths take — a plain [`EventStore::read_kinds`] and the
1487/// input a decision is shown from inside its transaction — so they select the
1488/// same rows by the same rule. A fault on the read itself is SQLite's (and so
1489/// retryable); a row whose stored objects do not decode is corruption and
1490/// terminal, and it surfaces as an error rather than being silently dropped,
1491/// so a caller (resume) never re-folds a truncated log into the wrong state.
1492fn read_in(
1493 conn: &Connection,
1494 stream: &str,
1495 kinds: Option<&[String]>,
1496 from_seq: u64,
1497 limit: i64,
1498) -> Result<Vec<Value>, JobError> {
1499 // An empty selection selects nothing, and `kind IN ()` is not SQL.
1500 if kinds.is_some_and(<[String]>::is_empty) {
1501 return Ok(Vec::new());
1502 }
1503 let (sql, args) = read_query(stream, kinds, from_seq, limit);
1504 let mut stmt = conn.prepare(&sql).map_err(JobError::Sqlite)?;
1505 let rows = stmt
1506 .query_map(params_from_iter(args.iter()), read_row)
1507 .map_err(JobError::Sqlite)?;
1508 let mut events = Vec::new();
1509 for row in rows {
1510 let row = row.map_err(JobError::Sqlite)?;
1511 events.push(event_of(row).map_err(JobError::Terminal)?);
1512 }
1513 Ok(events)
1514}
1515
1516/// The next `seq` for `stream`: `MAX(seq) + 1`, or `1` for an empty stream.
1517///
1518/// Returns the raw rusqlite error so the retry driver can key on its code.
1519fn next_seq(conn: &Connection, stream: &str) -> Result<u64, rusqlite::Error> {
1520 conn.query_row(
1521 "SELECT COALESCE(MAX(seq), 0) + 1 FROM events WHERE stream = ?1",
1522 params![stream],
1523 |row| row.get::<_, i64>(0),
1524 )
1525 .map(|n| n as u64)
1526}
1527
1528/// The current head of `stream`: `MAX(seq)`, or `None` when empty.
1529///
1530/// Returns the raw rusqlite error so the retry driver can key on its code.
1531fn head_in(conn: &Connection, stream: &str) -> Result<Option<u64>, rusqlite::Error> {
1532 let max: Option<i64> = conn.query_row(
1533 "SELECT MAX(seq) FROM events WHERE stream = ?1",
1534 params![stream],
1535 |row| row.get::<_, Option<i64>>(0),
1536 )?;
1537 Ok(max.map(|n| n as u64))
1538}
1539
1540/// Insert the fully-stamped event, one column per envelope key and one each
1541/// for the two objects it carries, so a read rebuilds the exact same `Value`
1542/// the caller wrote ([`event_of`]).
1543///
1544/// An encode failure is terminal; a contended insert is retryable.
1545fn insert_row(
1546 conn: &Connection,
1547 stream: &str,
1548 seq: u64,
1549 epoch_ms: u64,
1550 event: &Map<String, Value>,
1551) -> Result<(), JobError> {
1552 let kind = event.get(FIELD_KIND).and_then(Value::as_str).unwrap_or("");
1553 let schema_version = event
1554 .get(SCHEMA_VERSION_FIELD)
1555 .and_then(Value::as_u64)
1556 .unwrap_or(CURRENT_SCHEMA_VERSION);
1557 // The beat is the caller's and most events have none: an undeclared one
1558 // is a NULL in its column, which is what the read gives back as an
1559 // absent key.
1560 let beat = event.get(FIELD_BEAT).and_then(Value::as_str);
1561 let meta = encode_object(event.get(FIELD_META), FIELD_META)?;
1562 let data = encode_object(event.get(FIELD_DATA), FIELD_DATA)?;
1563 conn.execute(
1564 "INSERT INTO events (stream, seq, epoch_ms, kind, schema_version, beat, meta, data) \
1565 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1566 params![
1567 stream,
1568 seq as i64,
1569 epoch_ms as i64,
1570 kind,
1571 schema_version as i64,
1572 beat,
1573 meta,
1574 data
1575 ],
1576 )
1577 .map_err(JobError::Sqlite)?;
1578 Ok(())
1579}
1580
1581/// Encode `meta` / `data` for its column: the object as text, `{}` when the
1582/// event carries none.
1583///
1584/// Both are filled in on the way through [`stamp`], so the default is a
1585/// belt-and-braces answer rather than the usual path — and it is the empty
1586/// object either way, which is what makes the column `NOT NULL`.
1587///
1588/// An event that will not encode never reaches the disk, so a failure here is
1589/// the store failing to do the work rather than data that came back wrong —
1590/// `Storage`, not `Corruption`.
1591fn encode_object(value: Option<&Value>, column: &str) -> Result<String, JobError> {
1592 let Some(value) = value else {
1593 return Ok("{}".to_string());
1594 };
1595 serde_json::to_string(value).map_err(|e| {
1596 JobError::Terminal(KnlError::Storage(format!(
1597 "sqlite: encode event {column}: {e}"
1598 )))
1599 })
1600}
1601
1602/// Classify a rusqlite error into the kernel's vocabulary.
1603///
1604/// This is the one place the backend's error language is translated, and the
1605/// split is the one the caller can act on: a contended lock is
1606/// [`KnlError::Busy`] — the same call may succeed if it is made again — and
1607/// everything else is [`KnlError::Storage`], a fault the kernel cannot promise
1608/// anything about. Matched on the SQLite error *code*, never the message
1609/// text, so the classification does not drift with a library's wording.
1610///
1611/// This is a wider net than the isle's own retry uses: the isle re-submits on
1612/// `SQLITE_BUSY` alone, because that is contention with another connection and
1613/// clears on its own, while `SQLITE_LOCKED` within one connection does not.
1614/// What the *caller* is told is the coarser question — "is another attempt
1615/// worth making at all" — and for that both are worth a try.
1616///
1617/// Corruption is not produced here: a row that comes back and will not decode
1618/// is a fault of the data rather than of the store, so it is raised where the
1619/// decode happens.
1620impl From<rusqlite::Error> for KnlError {
1621 fn from(error: rusqlite::Error) -> Self {
1622 if is_retryable(&error) {
1623 return KnlError::Busy(format!("sqlite: busy/locked: {error}"));
1624 }
1625 KnlError::Storage(format!("sqlite: {error}"))
1626 }
1627}
1628
1629/// Translate an isle-level failure into the kernel's vocabulary.
1630///
1631/// The isle answers two kinds of question, and they map onto two kinds of
1632/// kernel error. A SQL fault is passed straight through to the translation
1633/// above, so a contended write still reads as [`KnlError::Busy`] however it
1634/// arrived. The isle's own conditions are about the *thread*, and they split
1635/// on whether waiting could help:
1636///
1637/// - `QueueFull` is backpressure — the connection thread is alive and behind,
1638/// so this is [`KnlError::Busy`], the one class that says "ask again";
1639/// - `Timeout` and `Cancelled` both mean a job was cut short rather than
1640/// answered, which is [`KnlError::Timeout`]: the deadline was the caller's,
1641/// and another identical attempt buys nothing;
1642/// - `Closed` (the thread is gone) and `Panicked` are [`KnlError::Storage`] —
1643/// the store could not do the work, and no retry changes that.
1644impl From<IsleError> for KnlError {
1645 fn from(error: IsleError) -> Self {
1646 match error {
1647 IsleError::Sqlite(error) => KnlError::from(error),
1648 IsleError::QueueFull => {
1649 KnlError::Busy("sqlite: the connection thread is at capacity".to_string())
1650 }
1651 IsleError::Timeout => KnlError::Timeout("sqlite: the deadline elapsed".to_string()),
1652 IsleError::Cancelled => KnlError::Timeout("sqlite: the job was cancelled".to_string()),
1653 // `IsleError` is `#[non_exhaustive]`: anything not named above is
1654 // the store failing to do the work, which is what `Storage` is.
1655 other => KnlError::Storage(format!("sqlite: {other}")),
1656 }
1657 }
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663 use crate::knl::event::{kind_of, seq_of};
1664 use crate::knl::query::QueryOpts;
1665 use serde_json::json;
1666
1667 /// Object map for an event literal.
1668 fn obj(value: Value) -> Map<String, Value> {
1669 match value {
1670 Value::Object(map) => map,
1671 other => panic!("test fixture must be an object, got {other}"),
1672 }
1673 }
1674
1675 /// An event of a caller's own kind, named `e{i}`.
1676 fn ev(i: usize) -> Map<String, Value> {
1677 obj(json!({ "kind": format!("e{i}") }))
1678 }
1679
1680 /// A `budget_*` event of `amount`, as the kernel writes one.
1681 fn budget(kind: &str, amount: i64) -> Map<String, Value> {
1682 obj(json!({ "kind": kind, "data": { "amount": amount } }))
1683 }
1684
1685 /// A store on an in-memory database of its very own, with the collection
1686 /// that owns its connection thread.
1687 ///
1688 /// The name matters: an in-memory database is shared by *name*, which is
1689 /// what lets the reader see the writer's rows — and would equally let two
1690 /// tests running in parallel see each other's. A fresh id per store keeps
1691 /// each test's log to itself.
1692 ///
1693 /// The [`IsleDrivers`] comes back with the store because the caller has to
1694 /// hold it: it owns the connection thread, and a test that dropped it
1695 /// early would be pulling the database out from under its own assertions.
1696 async fn mem_store() -> (SqliteEventStore, IsleDrivers) {
1697 let drivers = IsleDrivers::new();
1698 let store = SqliteEventStore::open_memory(uuid::Uuid::new_v4().to_string(), &drivers)
1699 .await
1700 .expect("open");
1701 (store, drivers)
1702 }
1703
1704 /// A decision as [`EventStore::append_if`] takes one: owned, and handed
1705 /// its input by value.
1706 fn decide(
1707 f: impl FnOnce(Vec<Value>) -> Option<Map<String, Value>> + Send + 'static,
1708 ) -> Decision {
1709 Box::new(f)
1710 }
1711
1712 #[tokio::test]
1713 async fn append_assigns_gap_free_monotonic_seq_from_one() {
1714 let (mut store, _drivers) = mem_store().await;
1715 assert!(store.is_empty().await.expect("is_empty"));
1716 assert_eq!(store.len().await.expect("len"), 0);
1717
1718 let a = store.append(ev(1)).await.expect("append e1");
1719 let b = store.append(ev(2)).await.expect("append e2");
1720 let c = store.append(ev(3)).await.expect("append e3");
1721
1722 assert_eq!((a.seq, b.seq, c.seq), (1, 2, 3));
1723 assert_eq!(store.len().await.expect("len"), 3);
1724 assert!(!store.is_empty().await.expect("is_empty"));
1725
1726 // The stamped epoch is what is stored.
1727 let stored = store.read(0, usize::MAX).await.expect("read");
1728 let stored_epoch = stored[0]
1729 .get("epoch_ms")
1730 .and_then(Value::as_u64)
1731 .expect("epoch is on the stored event");
1732 assert_eq!(stored_epoch, a.epoch_ms);
1733 }
1734
1735 #[tokio::test]
1736 async fn a_rejected_append_records_nothing_and_burns_no_seq() {
1737 let (mut store, _drivers) = mem_store().await;
1738 store
1739 .append(obj(json!({ "text": "no kind" })))
1740 .await
1741 .expect_err("kind is required");
1742 assert_eq!(store.len().await.expect("len"), 0);
1743 assert_eq!(store.append(ev(1)).await.expect("append").seq, 1);
1744 }
1745
1746 /// `append_if` decides on the stream inside its transaction: the events
1747 /// it is handed are the durable ones, a `Some` lands at the next seq, and
1748 /// a `None` commits nothing.
1749 #[tokio::test]
1750 async fn append_if_decides_inside_the_transaction_and_writes_only_a_some() {
1751 let (mut store, _drivers) = mem_store().await;
1752 store.append(ev(1)).await.expect("seed");
1753
1754 // The decision runs on the connection's own thread now, so what it
1755 // saw comes back through a shared cell rather than a borrow.
1756 let seen_kinds: Arc<Mutex<Vec<String>>> = Arc::default();
1757 let recorded = Arc::clone(&seen_kinds);
1758 let committed = store
1759 .append_if(
1760 None,
1761 decide(move |events| {
1762 *recorded.lock().expect("not poisoned") =
1763 events.iter().map(|e| kind_of(e).to_string()).collect();
1764 Some(ev(2))
1765 }),
1766 )
1767 .await
1768 .expect("append_if");
1769 assert_eq!(
1770 *seen_kinds.lock().expect("not poisoned"),
1771 ["e1"],
1772 "decide saw the durable stream"
1773 );
1774 assert_eq!(committed.map(|c| c.seq), Some(2));
1775
1776 let nothing = store
1777 .append_if(None, decide(|_| None))
1778 .await
1779 .expect("append_if");
1780 assert_eq!(nothing, None);
1781 assert_eq!(store.len().await.expect("len"), 2, "a None commits nothing");
1782 assert_eq!(store.append(ev(3)).await.expect("append").seq, 3);
1783 }
1784
1785 /// A malformed decision is refused and leaves the stream alone.
1786 #[tokio::test]
1787 async fn append_if_validates_the_event_the_decision_returns() {
1788 let (mut store, _drivers) = mem_store().await;
1789 store
1790 .append_if(None, decide(|_| Some(obj(json!({ "text": "no kind" })))))
1791 .await
1792 .expect_err("kind is required");
1793 assert_eq!(store.len().await.expect("len"), 0);
1794 }
1795
1796 /// A batch is one transaction: the events land together, numbered on from
1797 /// the live head — and a batch that fails part-way leaves the stream
1798 /// exactly as it was, which is the whole reason it is one call.
1799 #[tokio::test]
1800 async fn append_many_is_one_transaction_that_lands_whole_or_not_at_all() {
1801 let (mut store, _drivers) = mem_store().await;
1802 store.append(ev(1)).await.expect("seed");
1803
1804 let committed = store
1805 .append_many(vec![ev(2), ev(3)])
1806 .await
1807 .expect("the batch");
1808 assert_eq!(
1809 committed.iter().map(|c| c.seq).collect::<Vec<_>>(),
1810 [2, 3],
1811 "numbered on from the head that was there"
1812 );
1813 let stored = store.read(0, usize::MAX).await.expect("read");
1814 let kinds: Vec<&str> = stored.iter().map(kind_of).collect();
1815 assert_eq!(kinds, ["e1", "e2", "e3"]);
1816
1817 // A malformed event refuses the whole batch, and the one before it in
1818 // the same call is not in the log either.
1819 store
1820 .append_many(vec![ev(4), obj(json!({ "text": "no kind" }))])
1821 .await
1822 .expect_err("kind is required");
1823 assert_eq!(
1824 store.len().await.expect("len"),
1825 3,
1826 "a failed batch wrote nothing"
1827 );
1828 assert_eq!(
1829 store.append(ev(5)).await.expect("append").seq,
1830 4,
1831 "no seq burnt"
1832 );
1833
1834 // An empty batch is nothing to write, not an empty transaction.
1835 assert!(store
1836 .append_many(Vec::new())
1837 .await
1838 .expect("empty")
1839 .is_empty());
1840 assert_eq!(store.len().await.expect("len"), 4);
1841 }
1842
1843 /// A two-stream write is one transaction: each side is numbered from its
1844 /// own head, both land together, and a `None` decision — or a malformed
1845 /// event on either side — leaves both streams exactly as they were.
1846 #[tokio::test]
1847 async fn append_if_many_writes_both_streams_or_neither() {
1848 let dir = tempfile::tempdir().expect("tempdir");
1849 let path = dir.path().join("events.db");
1850 let drivers = IsleDrivers::new();
1851
1852 let mut parent = SqliteEventStore::open(&path, "p", &drivers)
1853 .await
1854 .expect("open the parent");
1855 let child = SqliteEventStore::open(&path, "c", &drivers)
1856 .await
1857 .expect("open the child");
1858 parent.append(ev(1)).await.expect("seed");
1859
1860 let committed = parent
1861 .append_if_many(
1862 "c",
1863 None,
1864 Box::new(|events| {
1865 assert_eq!(events.own.len(), 1, "the decision reads its own stream");
1866 assert!(
1867 events.other.is_empty(),
1868 "and is shown that the other one is empty"
1869 );
1870 Some(Split {
1871 own: vec![ev(2)],
1872 other: vec![ev(3), ev(4)],
1873 })
1874 }),
1875 )
1876 .await
1877 .expect("both sides")
1878 .expect("the decision wrote");
1879 assert_eq!(
1880 committed.own.iter().map(|c| c.seq).collect::<Vec<_>>(),
1881 [2],
1882 "this stream numbers on from its own head"
1883 );
1884 assert_eq!(
1885 committed.other.iter().map(|c| c.seq).collect::<Vec<_>>(),
1886 [1, 2],
1887 "and the other from its own, which was empty"
1888 );
1889 assert_eq!(child.len().await.expect("len"), 2, "the other side landed");
1890
1891 // A `None` is a decision too: neither stream is touched.
1892 assert_eq!(
1893 parent
1894 .append_if_many("c", None, Box::new(|_| None))
1895 .await
1896 .expect("append_if_many"),
1897 None
1898 );
1899 assert_eq!(parent.len().await.expect("len"), 2);
1900 assert_eq!(child.len().await.expect("len"), 2);
1901
1902 // One side may be empty — a refusal writes only this stream.
1903 parent
1904 .append_if_many("c", None, Box::new(|_| Some(Split::own(vec![ev(5)]))))
1905 .await
1906 .expect("append_if_many")
1907 .expect("the decision wrote");
1908 assert_eq!(parent.len().await.expect("len"), 3);
1909 assert_eq!(child.len().await.expect("len"), 2, "and nothing else");
1910
1911 // A malformed event on the far side takes the whole transaction with
1912 // it, including the well-formed one on this side.
1913 parent
1914 .append_if_many(
1915 "c",
1916 None,
1917 Box::new(|_| {
1918 Some(Split {
1919 own: vec![ev(6)],
1920 other: vec![obj(json!({ "text": "no kind" }))],
1921 })
1922 }),
1923 )
1924 .await
1925 .expect_err("kind is required");
1926 assert_eq!(parent.len().await.expect("len"), 3, "nothing was written");
1927 assert_eq!(child.len().await.expect("len"), 2);
1928 }
1929
1930 /// The close-time child scan reads by `events_session_opened_parent`
1931 /// instead of walking every event in the database.
1932 ///
1933 /// The plan is the assertion because the alternative is silent: a bound
1934 /// `kind` proves nothing about the index's `WHERE kind = 'session_opened'`
1935 /// and a bound path never matches an indexed expression, so getting either
1936 /// wrong still answers correctly — it just answers by reading the whole
1937 /// table, on the one query that is not scoped to a stream. The rows are
1938 /// checked too, so the literals that buy the index cannot buy it by
1939 /// asking a different question.
1940 ///
1941 /// What the plan reads today: `SEARCH opened USING INDEX
1942 /// events_session_opened_parent (<expr>=?)`, with the ending's `NOT
1943 /// EXISTS` served by `events_stream_kind_seq`.
1944 #[test]
1945 fn the_child_scan_reads_by_the_parent_index() {
1946 let conn = Connection::open_in_memory().expect("an in-memory database");
1947 conn.execute_batch(SCHEMA_DDL).expect("the schema");
1948
1949 let insert = |stream: &str, seq: i64, kind: &str, data: &str| {
1950 conn.execute(
1951 "INSERT INTO events \
1952 (stream, seq, epoch_ms, kind, schema_version, beat, meta, data) \
1953 VALUES (?1, ?2, ?3, ?4, 1, NULL, '{}', ?5)",
1954 params![stream, seq, seq * 10, kind, data],
1955 )
1956 .expect("insert");
1957 };
1958 insert("c1", 1, "session_opened", r#"{"parent":"p"}"#);
1959 insert("c2", 1, "session_opened", r#"{"parent":"p"}"#);
1960 insert("c2", 2, "session_closed", "{}");
1961 insert("c3", 1, "session_opened", r#"{"parent":"elsewhere"}"#);
1962 insert("p", 1, "session_opened", "{}");
1963
1964 // The kernel's own vocabulary, which is the one the index is cut for.
1965 let scan = ChildScan {
1966 opened: "session_opened".to_string(),
1967 closed: "session_closed".to_string(),
1968 parent_field: "parent".to_string(),
1969 };
1970 assert_eq!(
1971 open_children_in(&conn, "p", &scan).expect("scan"),
1972 vec!["c1".to_string()],
1973 "the ended child and the other parent's are not this stream's open children"
1974 );
1975
1976 let sql = format!("EXPLAIN QUERY PLAN {}", child_scan_sql(&scan));
1977 let mut stmt = conn.prepare(&sql).expect("prepare the plan");
1978 let plan: Vec<String> = stmt
1979 .query_map(params!["p"], |row| row.get::<_, String>(3))
1980 .expect("the plan's rows")
1981 .collect::<rusqlite::Result<Vec<_>>>()
1982 .expect("the plan's rows");
1983
1984 assert!(
1985 plan.iter()
1986 .any(|step| step.contains("events_session_opened_parent")),
1987 "the openings must be looked up by the index: {plan:?}"
1988 );
1989 assert!(
1990 !plan
1991 .iter()
1992 .any(|step| step.starts_with("SCAN events AS opened")),
1993 "and not found by walking the table: {plan:?}"
1994 );
1995 }
1996
1997 /// The scan's words go into the statement as literals, so a quote in one
1998 /// of them is doubled rather than closing the string early.
1999 ///
2000 /// They are the kernel's own constants today, which is why writing them
2001 /// in is safe *and* why nothing would notice if they stopped being: the
2002 /// vocabulary is an argument ([`ChildScan`]), and a word that ended the
2003 /// literal would turn the rest of the statement into SQL somebody else
2004 /// wrote.
2005 #[test]
2006 fn a_word_written_into_the_scan_stays_one_word() {
2007 assert_eq!(sql_literal("parent"), "'parent'");
2008 assert_eq!(sql_literal("a'b"), "'a''b'");
2009
2010 let conn = Connection::open_in_memory().expect("an in-memory database");
2011 conn.execute_batch(SCHEMA_DDL).expect("the schema");
2012 conn.execute(
2013 "INSERT INTO events \
2014 (stream, seq, epoch_ms, kind, schema_version, beat, meta, data) \
2015 VALUES ('c', 1, 10, 'it''s open', 1, NULL, '{}', ?1)",
2016 params![r#"{"pa'rent":"p"}"#],
2017 )
2018 .expect("insert");
2019
2020 let scan = ChildScan {
2021 opened: "it's open".to_string(),
2022 closed: "it's over".to_string(),
2023 parent_field: "pa'rent".to_string(),
2024 };
2025 assert_eq!(
2026 open_children_in(&conn, "p", &scan).expect("the statement parses and runs"),
2027 vec!["c".to_string()],
2028 "the quoted words are still the words being matched"
2029 );
2030 }
2031
2032 /// `database` names the database, not the stream: two stores on one file
2033 /// answer with the same string and a store on another file does not.
2034 /// That is the whole of what the identity is for — deciding whether one
2035 /// transaction can cover both.
2036 #[tokio::test]
2037 async fn database_is_the_same_for_two_streams_of_one_database() {
2038 let dir = tempfile::tempdir().expect("tempdir");
2039 let path = dir.path().join("events.db");
2040 let elsewhere = dir.path().join("other.db");
2041 let drivers = IsleDrivers::new();
2042
2043 let a = SqliteEventStore::open(&path, "a", &drivers)
2044 .await
2045 .expect("open a");
2046 let b = SqliteEventStore::open(&path, "b", &drivers)
2047 .await
2048 .expect("open b");
2049 let far = SqliteEventStore::open(&elsewhere, "a", &drivers)
2050 .await
2051 .expect("open far");
2052
2053 assert_eq!(a.database(), b.database(), "two streams, one database");
2054 assert_ne!(a.database(), far.database(), "two databases");
2055 assert_eq!(
2056 a.database(),
2057 Some(path.to_string_lossy().as_ref()),
2058 "the target it was opened by"
2059 );
2060
2061 // An in-memory database has an identity too, and it is the URI a
2062 // second connection reaches it by.
2063 let (mem, _mem_drivers) = mem_store().await;
2064 let uri = mem.database().expect("a database").to_string();
2065 assert!(uri.contains("mode=memory"), "{uri}");
2066 let beside = SqliteEventStore::open(std::path::Path::new(&uri), "beside", &drivers)
2067 .await
2068 .expect("open beside");
2069 assert_eq!(
2070 beside.database(),
2071 Some(uri.as_str()),
2072 "opening that target reaches the same database"
2073 );
2074 }
2075
2076 /// The child scan finds the streams that name this one as their parent
2077 /// and carry no ending — and nobody else's children, and not the ones
2078 /// that already closed.
2079 #[tokio::test]
2080 async fn open_children_are_the_unended_streams_that_name_this_one() {
2081 let dir = tempfile::tempdir().expect("tempdir");
2082 let path = dir.path().join("events.db");
2083 let drivers = IsleDrivers::new();
2084
2085 /// A `session_opened` naming `parent`.
2086 fn opened(parent: &str) -> Map<String, Value> {
2087 obj(json!({
2088 "kind": "session_opened",
2089 "data": { "scope_id": "sc", "owner": "anon", "parent": parent }
2090 }))
2091 }
2092 let ended = obj(json!({ "kind": "session_closed", "data": { "reason": "done" } }));
2093
2094 let mut parent = SqliteEventStore::open(&path, "p", &drivers)
2095 .await
2096 .expect("open p");
2097 // Still running.
2098 let mut running = SqliteEventStore::open(&path, "kid-a", &drivers)
2099 .await
2100 .expect("open kid-a");
2101 running.append(opened("p")).await.expect("opened");
2102 // Opened from p and already over.
2103 let mut over = SqliteEventStore::open(&path, "kid-b", &drivers)
2104 .await
2105 .expect("open kid-b");
2106 over.append(opened("p")).await.expect("opened");
2107 over.append(ended.clone()).await.expect("closed");
2108 // Somebody else's child, still running.
2109 let mut theirs = SqliteEventStore::open(&path, "kid-c", &drivers)
2110 .await
2111 .expect("open kid-c");
2112 theirs.append(opened("q")).await.expect("opened");
2113 // A stream with no parent at all.
2114 let mut root = SqliteEventStore::open(&path, "r", &drivers)
2115 .await
2116 .expect("open r");
2117 root.append(obj(
2118 json!({ "kind": "session_opened", "data": { "scope_id": "sc", "owner": "anon" } }),
2119 ))
2120 .await
2121 .expect("opened");
2122
2123 let scan = ChildScan {
2124 opened: "session_opened".to_string(),
2125 closed: "session_closed".to_string(),
2126 parent_field: "parent".to_string(),
2127 };
2128 let seen: Arc<Mutex<Vec<String>>> = Arc::default();
2129 let recorded = Arc::clone(&seen);
2130 let committed = parent
2131 .append_with_open_children(
2132 &scan,
2133 Box::new(move |children| {
2134 *recorded.lock().expect("not poisoned") = children;
2135 ended.clone()
2136 }),
2137 )
2138 .await
2139 .expect("the close lands");
2140
2141 assert_eq!(
2142 *seen.lock().expect("not poisoned"),
2143 ["kid-a"],
2144 "only the unended streams that named this one"
2145 );
2146 assert_eq!(committed.seq, 1, "and the event it built was appended");
2147 assert_eq!(parent.len().await.expect("len"), 1);
2148 }
2149
2150 /// A kind-filtered read is answered off the index: only the kinds asked
2151 /// for come back, in `seq` order, still carrying the `seq` the stream gave
2152 /// them. `None` is the whole stream, an empty selection is nothing.
2153 #[tokio::test]
2154 async fn read_kinds_selects_by_kind_and_keeps_the_streams_order() {
2155 let (mut store, _drivers) = mem_store().await;
2156 store
2157 .append(budget("budget_granted", 100))
2158 .await
2159 .expect("grant");
2160 store.append(ev(1)).await.expect("noise");
2161 store
2162 .append(budget("budget_spent", 10))
2163 .await
2164 .expect("spend");
2165 store.append(ev(2)).await.expect("more noise");
2166
2167 let ledger = store
2168 .read_kinds(Some(&["budget_granted", "budget_spent"]), 0, usize::MAX)
2169 .await
2170 .expect("read_kinds");
2171 let kinds: Vec<&str> = ledger.iter().map(kind_of).collect();
2172 assert_eq!(kinds, ["budget_granted", "budget_spent"]);
2173 assert_eq!(seq_of(&ledger[0]), 1);
2174 assert_eq!(seq_of(&ledger[1]), 3, "the seq is the stream's");
2175
2176 // from_seq and limit still apply to the filtered set.
2177 assert_eq!(
2178 store
2179 .read_kinds(Some(&["budget_granted"]), 2, usize::MAX)
2180 .await
2181 .expect("read_kinds")
2182 .len(),
2183 0
2184 );
2185 assert_eq!(
2186 store
2187 .read_kinds(Some(&["budget_granted", "budget_spent"]), 0, 1)
2188 .await
2189 .expect("read_kinds")
2190 .len(),
2191 1
2192 );
2193
2194 assert!(store
2195 .read_kinds(Some(&[]), 0, usize::MAX)
2196 .await
2197 .expect("read_kinds")
2198 .is_empty());
2199 assert_eq!(
2200 store
2201 .read_kinds(None, 0, usize::MAX)
2202 .await
2203 .expect("read_kinds")
2204 .len(),
2205 4
2206 );
2207 }
2208
2209 /// A decision that names its kinds is shown those and nothing else, and
2210 /// its write is still numbered against the whole stream — the filter is
2211 /// what the decision *reads*, not where its answer goes.
2212 #[tokio::test]
2213 async fn append_if_filters_the_decisions_input_and_numbers_against_the_stream() {
2214 let (mut store, _drivers) = mem_store().await;
2215 store
2216 .append(budget("budget_granted", 100))
2217 .await
2218 .expect("grant");
2219 store.append(ev(1)).await.expect("noise");
2220 store.append(ev(2)).await.expect("more noise");
2221
2222 let seen: Arc<Mutex<Vec<String>>> = Arc::default();
2223 let recorded = Arc::clone(&seen);
2224 let committed = store
2225 .append_if(
2226 Some(&["budget_granted"]),
2227 decide(move |events| {
2228 *recorded.lock().expect("not poisoned") =
2229 events.iter().map(|e| kind_of(e).to_string()).collect();
2230 Some(budget("budget_spent", 10))
2231 }),
2232 )
2233 .await
2234 .expect("append_if");
2235 assert_eq!(
2236 *seen.lock().expect("not poisoned"),
2237 ["budget_granted"],
2238 "only the kinds asked for"
2239 );
2240 assert_eq!(
2241 committed.map(|c| c.seq),
2242 Some(4),
2243 "the write lands after everything, not after the filtered read"
2244 );
2245 assert_eq!(store.len().await.expect("len"), 4);
2246 }
2247
2248 /// Two handles on one stream, one invariant: each decides inside its own
2249 /// transaction, so the second sees what the first wrote and exactly one
2250 /// of them may write. This is the property a compare-and-swap against a
2251 /// cached head could only detect after the fact.
2252 #[tokio::test]
2253 async fn append_if_across_two_handles_decides_on_the_other_handles_write() {
2254 let dir = tempfile::tempdir().expect("tempdir");
2255 let path = dir.path().join("events.db");
2256 let drivers = IsleDrivers::new();
2257
2258 let mut a = SqliteEventStore::open(&path, "s", &drivers)
2259 .await
2260 .expect("open a");
2261 let mut b = SqliteEventStore::open(&path, "s", &drivers)
2262 .await
2263 .expect("open b");
2264
2265 // "Write the marker, but only if nobody has written one yet."
2266 let only_once = || {
2267 decide(|events: Vec<Value>| {
2268 (!events.iter().any(|e| kind_of(e) == "marker"))
2269 .then(|| obj(json!({ "kind": "marker" })))
2270 })
2271 };
2272
2273 let first = a.append_if(None, only_once()).await.expect("a decides");
2274 assert_eq!(first.map(|c| c.seq), Some(1), "a wrote the marker");
2275
2276 let second = b.append_if(None, only_once()).await.expect("b decides");
2277 assert_eq!(second, None, "b saw a's marker and wrote nothing");
2278 assert_eq!(b.len().await.expect("len"), 1, "exactly one marker");
2279 }
2280
2281 #[tokio::test]
2282 async fn read_pages_by_from_seq_and_limit() {
2283 let (mut store, _drivers) = mem_store().await;
2284 for i in 1..=5 {
2285 store.append(ev(i)).await.expect("append");
2286 }
2287
2288 assert_eq!(store.read(0, usize::MAX).await.expect("read").len(), 5);
2289 assert_eq!(store.read(1, usize::MAX).await.expect("read").len(), 5);
2290 assert_eq!(store.read(3, usize::MAX).await.expect("read").len(), 3);
2291 assert_eq!(store.read(6, usize::MAX).await.expect("read").len(), 0);
2292
2293 let page = store.read(2, 2).await.expect("read");
2294 assert_eq!(page.len(), 2);
2295 assert_eq!(kind_of(&page[0]), "e2");
2296 assert_eq!(kind_of(&page[1]), "e3");
2297
2298 // A zero limit returns nothing even when events match.
2299 assert!(store.read(0, 0).await.expect("read").is_empty());
2300 }
2301
2302 /// The last `n` come back in `seq` order, and the statement that fetched
2303 /// them asked SQLite for `n` rows rather than for the stream.
2304 ///
2305 /// The query plan is the half that matters: `ORDER BY seq DESC LIMIT ?`
2306 /// walks the `(stream, seq)` index backwards and stops, so the cost of a
2307 /// `tail` is `n` and not the length of the log. Reading the SQL here is
2308 /// how that is held — the row count alone would pass just as well for a
2309 /// backend that read everything and threw most of it away.
2310 #[tokio::test]
2311 async fn read_last_takes_the_end_of_the_stream_in_seq_order() {
2312 let (sql, args) = read_last_query("s-1", 5);
2313 assert!(
2314 sql.contains("ORDER BY seq DESC LIMIT ?"),
2315 "the read must stop at n rows: {sql}"
2316 );
2317 assert_eq!(args.len(), 2, "the stream and the cap are bound: {sql}");
2318
2319 let (mut store, _drivers) = mem_store().await;
2320 for i in 1..=200 {
2321 store.append(ev(i)).await.expect("append");
2322 }
2323
2324 let tail = store.read_last(5).await.expect("read_last");
2325 assert_eq!(tail.len(), 5);
2326 assert_eq!(kind_of(&tail[0]), "e196", "oldest of the five first");
2327 assert_eq!(kind_of(&tail[4]), "e200", "the head last");
2328
2329 // The two edges: more than there is, and none at all.
2330 assert_eq!(store.read_last(usize::MAX).await.expect("all").len(), 200);
2331 assert!(store.read_last(0).await.expect("none").is_empty());
2332 }
2333
2334 #[tokio::test]
2335 async fn head_is_none_when_empty_then_tracks_the_max() {
2336 let (mut store, _drivers) = mem_store().await;
2337 assert_eq!(store.head().await.expect("head"), None);
2338
2339 store.append(ev(1)).await.expect("append");
2340 assert_eq!(store.head().await.expect("head"), Some(1));
2341 store.append(ev(2)).await.expect("append");
2342 assert_eq!(store.head().await.expect("head"), Some(2));
2343
2344 // A rejected append does not move the head.
2345 store
2346 .append(obj(json!({ "text": "no kind" })))
2347 .await
2348 .expect_err("kind is required");
2349 assert_eq!(store.head().await.expect("head"), Some(2));
2350 }
2351
2352 /// A read rebuilds the object that was written: the envelope out of its
2353 /// columns, `meta` and `data` out of theirs, and the beat back as an
2354 /// absent key when there was none.
2355 #[tokio::test]
2356 async fn read_reconstructs_the_written_event_out_of_its_columns() {
2357 let (mut store, _drivers) = mem_store().await;
2358 store
2359 .append(obj(json!({
2360 "kind": "note",
2361 "beat": "b1",
2362 "meta": { "label": "a", "attempt": 2, "retried": true },
2363 "data": { "text": "hi", "nested": { "deep": [1, 2] } }
2364 })))
2365 .await
2366 .expect("append");
2367 store
2368 .append(obj(json!({ "kind": "note" })))
2369 .await
2370 .expect("append a bare one");
2371
2372 let stored = store.read(0, usize::MAX).await.expect("read");
2373 assert_eq!(kind_of(&stored[0]), "note");
2374 assert_eq!(stored[0]["beat"], json!("b1"));
2375 assert_eq!(
2376 stored[0]["meta"],
2377 json!({ "label": "a", "attempt": 2, "retried": true })
2378 );
2379 assert_eq!(
2380 stored[0]["data"],
2381 json!({ "text": "hi", "nested": { "deep": [1, 2] } }),
2382 "data comes back at any depth"
2383 );
2384 assert_eq!(seq_of(&stored[0]), 1);
2385 assert!(stored[0].get("epoch_ms").is_some(), "{}", stored[0]);
2386 assert_eq!(
2387 stored[0].get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
2388 Some(CURRENT_SCHEMA_VERSION)
2389 );
2390
2391 // An event with nothing declared: no beat key at all, and the two
2392 // objects empty rather than missing.
2393 assert_eq!(stored[1].get("beat"), None, "{}", stored[1]);
2394 assert_eq!(stored[1]["meta"], json!({}));
2395 assert_eq!(stored[1]["data"], json!({}));
2396 }
2397
2398 /// The beat lands in its own column — a plain `SELECT beat` sees it —
2399 /// and the index that makes a by-beat read a range is on the table.
2400 #[tokio::test]
2401 async fn the_beat_is_a_column_of_its_own_with_an_index() {
2402 let (mut store, _drivers) = mem_store().await;
2403 store
2404 .append(obj(json!({ "kind": "e1", "beat": "b1" })))
2405 .await
2406 .expect("append");
2407 store.append(ev(2)).await.expect("append with no beat");
2408
2409 let rows = ask(
2410 &store,
2411 "SELECT seq, beat FROM events WHERE stream = $stream ORDER BY seq",
2412 )
2413 .await
2414 .expect("query");
2415 assert_eq!(rows.rows[0]["beat"], Value::from("b1"));
2416 assert!(
2417 !rows.rows[1].contains_key("beat"),
2418 "an undeclared beat is NULL: {:?}",
2419 rows.rows[1]
2420 );
2421
2422 // Grouping a run by beat is a range of an index, not a scan.
2423 let indexes = store
2424 .writer
2425 .call(|conn| {
2426 let mut stmt = conn.prepare("PRAGMA index_list(events)")?;
2427 let names = stmt
2428 .query_map([], |row| row.get::<_, String>("name"))?
2429 .collect::<rusqlite::Result<Vec<_>>>();
2430 names
2431 })
2432 .await
2433 .expect("index_list");
2434 assert!(
2435 indexes.iter().any(|name| name == "events_stream_beat_seq"),
2436 "the (stream, beat, seq) index must exist: {indexes:?}"
2437 );
2438 assert!(
2439 indexes.iter().any(|name| name == "events_stream_kind_seq"),
2440 "…beside the by-kind one: {indexes:?}"
2441 );
2442 }
2443
2444 #[tokio::test]
2445 async fn events_persist_across_a_reopen_of_the_same_path_and_stream() {
2446 let dir = tempfile::tempdir().expect("tempdir");
2447 let path = dir.path().join("events.db");
2448
2449 {
2450 // Its own collection, shut down at the end of the block, so the
2451 // first connection is drained and joined before the reopen below
2452 // — the same "the store is gone" the sync version got from Drop.
2453 let drivers = IsleDrivers::new();
2454 let mut store = SqliteEventStore::open(&path, "s", &drivers)
2455 .await
2456 .expect("open");
2457 store
2458 .append(obj(
2459 json!({ "kind": "note", "data": { "text": "durable" } }),
2460 ))
2461 .await
2462 .expect("append note");
2463 store.append(ev(2)).await.expect("append e2");
2464 drop(store);
2465 assert!(drivers.shutdown().await.is_empty(), "the writer joined");
2466 }
2467
2468 // Reopening the same file and stream reads the same events back: the
2469 // durability payoff.
2470 let drivers = IsleDrivers::new();
2471 let store = SqliteEventStore::open(&path, "s", &drivers)
2472 .await
2473 .expect("reopen");
2474 let events = store.read(0, usize::MAX).await.expect("read");
2475 assert_eq!(events.len(), 2);
2476 assert_eq!(kind_of(&events[0]), "note");
2477 assert_eq!(events[0]["data"], json!({ "text": "durable" }));
2478 assert_eq!(seq_of(&events[0]), 1);
2479 assert_eq!(
2480 events[0].get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
2481 Some(CURRENT_SCHEMA_VERSION),
2482 "the schema version survives the round-trip too"
2483 );
2484 assert_eq!(store.head().await.expect("head"), Some(2));
2485 }
2486
2487 #[tokio::test]
2488 async fn two_streams_in_one_db_file_do_not_see_each_others_events() {
2489 let dir = tempfile::tempdir().expect("tempdir");
2490 let path = dir.path().join("events.db");
2491 let drivers = IsleDrivers::new();
2492
2493 let mut a = SqliteEventStore::open(&path, "stream-a", &drivers)
2494 .await
2495 .expect("open a");
2496 let mut b = SqliteEventStore::open(&path, "stream-b", &drivers)
2497 .await
2498 .expect("open b");
2499
2500 a.append(obj(json!({ "kind": "only_a" })))
2501 .await
2502 .expect("append a");
2503 b.append(obj(json!({ "kind": "only_b1" })))
2504 .await
2505 .expect("append b1");
2506 b.append(obj(json!({ "kind": "only_b2" })))
2507 .await
2508 .expect("append b2");
2509
2510 assert_eq!(a.len().await.expect("len"), 1);
2511 assert_eq!(b.len().await.expect("len"), 2);
2512 // Each stream numbers its own seq from 1, independent of the other.
2513 assert_eq!(a.head().await.expect("head"), Some(1));
2514 assert_eq!(b.head().await.expect("head"), Some(2));
2515
2516 assert_eq!(
2517 kind_of(&a.read(0, usize::MAX).await.expect("read")[0]),
2518 "only_a"
2519 );
2520 let b_events = b.read(0, usize::MAX).await.expect("read");
2521 let b_kinds: Vec<&str> = b_events.iter().map(kind_of).collect();
2522 assert_eq!(b_kinds, ["only_b1", "only_b2"]);
2523 }
2524
2525 /// (Fix 2) A row whose stored objects will not decode is corruption:
2526 /// `read` surfaces it as an error rather than silently dropping the row
2527 /// (which would let a resume re-fold a truncated log into the wrong
2528 /// state). Both JSON columns are checked, and a scalar where an object
2529 /// was written is the same fault as text that will not parse.
2530 #[tokio::test]
2531 async fn read_errors_on_a_corrupt_row_instead_of_dropping_it() {
2532 for (seq, meta, data, column) in [
2533 (2_i64, "{}", "{not valid json", "data"),
2534 (3_i64, "not valid json either", "{}", "meta"),
2535 (4_i64, "{}", "7", "data"),
2536 ] {
2537 let (mut store, _drivers) = mem_store().await;
2538 store.append(ev(1)).await.expect("append");
2539
2540 // Sneak in a row the store itself could not have written.
2541 let stream = store.stream.clone();
2542 let (meta, data) = (meta.to_string(), data.to_string());
2543 store
2544 .writer
2545 .call(move |conn| {
2546 conn.execute(
2547 "INSERT INTO events \
2548 (stream, seq, epoch_ms, kind, schema_version, beat, meta, data) \
2549 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
2550 params![
2551 stream,
2552 seq,
2553 0_i64,
2554 "note",
2555 1_i64,
2556 None::<String>,
2557 meta,
2558 data
2559 ],
2560 )
2561 })
2562 .await
2563 .expect("insert corrupt row");
2564
2565 let err = store
2566 .read(0, usize::MAX)
2567 .await
2568 .expect_err("a corrupt row must surface, not be dropped");
2569 assert!(
2570 err.reason().contains(&format!("corrupt event {column}")),
2571 "{}",
2572 err.reason()
2573 );
2574 // Corruption, not storage: the IO worked and the bytes came back,
2575 // so what is wrong is the data — no retry and no reconnect
2576 // changes it.
2577 assert_eq!(err.kind(), KnlError::CORRUPTION);
2578 assert!(!err.is_retryable());
2579 }
2580 }
2581
2582 /// The backend's error language is translated in exactly one place, and
2583 /// the split is the one a caller can act on: a contended lock says "ask
2584 /// again", every other fault says nothing of the kind.
2585 #[test]
2586 fn a_contended_lock_is_busy_and_every_other_fault_is_storage() {
2587 /// A `rusqlite` failure carrying `code`.
2588 fn failure(code: rusqlite::ErrorCode) -> rusqlite::Error {
2589 rusqlite::Error::SqliteFailure(
2590 rusqlite::ffi::Error {
2591 code,
2592 extended_code: 0,
2593 },
2594 Some("under test".to_string()),
2595 )
2596 }
2597
2598 for code in [
2599 rusqlite::ErrorCode::DatabaseBusy,
2600 rusqlite::ErrorCode::DatabaseLocked,
2601 ] {
2602 let error = KnlError::from(failure(code));
2603 assert_eq!(error.kind(), KnlError::BUSY, "{code:?}: {error}");
2604 assert!(error.is_retryable(), "{code:?}: {error}");
2605 }
2606
2607 for code in [
2608 rusqlite::ErrorCode::DatabaseCorrupt,
2609 rusqlite::ErrorCode::ReadOnly,
2610 rusqlite::ErrorCode::DiskFull,
2611 ] {
2612 let error = KnlError::from(failure(code));
2613 assert_eq!(error.kind(), KnlError::STORAGE, "{code:?}: {error}");
2614 assert!(
2615 !error.is_retryable(),
2616 "the kernel does not promise a retry it cannot back: {error}"
2617 );
2618 }
2619
2620 // A non-SQLite rusqlite fault is storage too — it is the store
2621 // failing to do the work, whatever the shape of the failure.
2622 let error = KnlError::from(rusqlite::Error::QueryReturnedNoRows);
2623 assert_eq!(error.kind(), KnlError::STORAGE, "{error}");
2624 }
2625
2626 /// The busy classification is what a real contended write surfaces as,
2627 /// not only what the translation function returns in isolation: a second
2628 /// connection holds the write lock, so the retries are exhausted and the
2629 /// error the caller gets says "ask again".
2630 #[tokio::test]
2631 async fn a_write_that_stays_contended_surfaces_as_busy() {
2632 let dir = tempfile::tempdir().expect("tempdir");
2633 let path = dir.path().join("events.db");
2634 let drivers = IsleDrivers::new();
2635
2636 let mut store = SqliteEventStore::open(&path, "s", &drivers)
2637 .await
2638 .expect("open");
2639 store.append(ev(1)).await.expect("seed");
2640
2641 // A blocker holding an EXCLUSIVE transaction: every attempt this
2642 // store makes finds the database locked, and the busy_timeout is cut
2643 // to nothing so the test does not wait it out five times over.
2644 let blocker = Connection::open(&path).expect("open blocker");
2645 blocker
2646 .execute_batch("BEGIN EXCLUSIVE")
2647 .expect("take the write lock");
2648 store
2649 .writer
2650 .call(|conn| conn.busy_timeout(Duration::from_millis(0)))
2651 .await
2652 .expect("no waiting");
2653
2654 let err = store
2655 .append(ev(2))
2656 .await
2657 .expect_err("a write against a held lock must not succeed");
2658 assert_eq!(err.kind(), KnlError::BUSY, "{err}");
2659 assert!(err.is_retryable(), "{err}");
2660 }
2661
2662 /// Two handles on one stream both write: an append records a fact, so it
2663 /// is serialized and assigned the next seq rather than refused for the
2664 /// head one of them last saw.
2665 #[tokio::test]
2666 async fn two_handles_on_one_stream_both_append_in_arrival_order() {
2667 let dir = tempfile::tempdir().expect("tempdir");
2668 let path = dir.path().join("events.db");
2669 let drivers = IsleDrivers::new();
2670
2671 let mut a = SqliteEventStore::open(&path, "s", &drivers)
2672 .await
2673 .expect("open a");
2674 let mut b = SqliteEventStore::open(&path, "s", &drivers)
2675 .await
2676 .expect("open b");
2677
2678 a.append(ev(1)).await.expect("seed"); // both handles now see head 1
2679
2680 // A writes, then B writes — neither is refused, and the log holds
2681 // them in the order they arrived.
2682 assert_eq!(a.append(ev(2)).await.expect("a appends").seq, 2);
2683 assert_eq!(b.append(ev(3)).await.expect("b appends").seq, 3);
2684
2685 let events = b.read(0, usize::MAX).await.expect("read");
2686 let kinds: Vec<&str> = events.iter().map(kind_of).collect();
2687 assert_eq!(kinds, ["e1", "e2", "e3"]);
2688 assert_eq!(b.head().await.expect("head"), Some(3));
2689 }
2690
2691 /// (Fix 3) Under the IMMEDIATE transaction + `busy_timeout`, interleaved
2692 /// single-threaded appends across two handles on one stream serialize and
2693 /// round-trip cleanly.
2694 #[tokio::test]
2695 async fn immediate_tx_appends_round_trip_across_two_handles() {
2696 let dir = tempfile::tempdir().expect("tempdir");
2697 let path = dir.path().join("events.db");
2698 let drivers = IsleDrivers::new();
2699
2700 let mut a = SqliteEventStore::open(&path, "s", &drivers)
2701 .await
2702 .expect("open a");
2703 let mut b = SqliteEventStore::open(&path, "s", &drivers)
2704 .await
2705 .expect("open b");
2706
2707 assert_eq!(a.append(ev(1)).await.expect("a1").seq, 1);
2708 assert_eq!(b.append(ev(2)).await.expect("b2").seq, 2);
2709 assert_eq!(a.append(ev(3)).await.expect("a3").seq, 3);
2710
2711 assert_eq!(b.head().await.expect("head"), Some(3));
2712 assert_eq!(b.read(0, usize::MAX).await.expect("read").len(), 3);
2713 }
2714
2715 // -- the read side -----------------------------------------------------
2716
2717 /// Ask `store` for `sql` with everything default.
2718 async fn ask(store: &SqliteEventStore, sql: &str) -> KnlResult<QueryRows> {
2719 ask_with(store, sql, QueryParams::None, &QueryOpts::default()).await
2720 }
2721
2722 /// Ask `store` for `sql`, saying how.
2723 async fn ask_with(
2724 store: &SqliteEventStore,
2725 sql: &str,
2726 params: QueryParams,
2727 opts: &QueryOpts,
2728 ) -> KnlResult<QueryRows> {
2729 let plan = crate::knl::query::plan(sql, params, opts, &store.stream)?;
2730 store.query(&plan).await
2731 }
2732
2733 /// The `kind` column of every row, in order.
2734 fn kinds_of(rows: &QueryRows) -> Vec<&str> {
2735 rows.rows
2736 .iter()
2737 .map(|row| row["kind"].as_str().expect("kind is a string"))
2738 .collect()
2739 }
2740
2741 /// The reader sees what the writer wrote — on an in-memory database as
2742 /// much as on a file, which is the whole reason the memory one is opened
2743 /// under a shared-cache URI rather than as a private `:memory:`.
2744 #[tokio::test]
2745 async fn the_reader_sees_the_writers_rows_in_memory() {
2746 let (mut store, _drivers) = mem_store().await;
2747 store.append(ev(1)).await.expect("append e1");
2748 store.append(ev(2)).await.expect("append e2");
2749
2750 let rows = ask(
2751 &store,
2752 "SELECT seq, kind FROM events WHERE stream = $stream ORDER BY seq",
2753 )
2754 .await
2755 .expect("query");
2756 assert_eq!(kinds_of(&rows), ["e1", "e2"]);
2757 assert_eq!(rows.rows[0]["seq"], Value::from(1));
2758 assert!(!rows.truncated);
2759
2760 // A write after the first query is visible to the next one: the
2761 // reader is a live connection, not a snapshot taken when it opened.
2762 store.append(ev(3)).await.expect("append e3");
2763 let again = ask(&store, "SELECT kind FROM events ORDER BY seq")
2764 .await
2765 .expect("query");
2766 assert_eq!(kinds_of(&again), ["e1", "e2", "e3"]);
2767 }
2768
2769 /// `$stream` is this store's own stream and nothing else: a second stream
2770 /// in the same database is not selected by it.
2771 #[tokio::test]
2772 async fn stream_binds_to_this_stores_own_stream() {
2773 let dir = tempfile::tempdir().expect("tempdir");
2774 let path = dir.path().join("events.db");
2775 let drivers = IsleDrivers::new();
2776
2777 let mut a = SqliteEventStore::open(&path, "stream-a", &drivers)
2778 .await
2779 .expect("open a");
2780 let mut b = SqliteEventStore::open(&path, "stream-b", &drivers)
2781 .await
2782 .expect("open b");
2783 a.append(obj(json!({ "kind": "only_a" }))).await.expect("a");
2784 b.append(obj(json!({ "kind": "only_b" }))).await.expect("b");
2785
2786 let rows = ask(&a, "SELECT kind FROM events WHERE stream = $stream")
2787 .await
2788 .expect("query");
2789 assert_eq!(kinds_of(&rows), ["only_a"]);
2790 let rows = ask(&b, "SELECT kind FROM events WHERE stream = $stream")
2791 .await
2792 .expect("query");
2793 assert_eq!(kinds_of(&rows), ["only_b"]);
2794 }
2795
2796 /// `$sessions` reads across a set: two streams in one database, one
2797 /// statement, and the ids are bound rather than pasted in.
2798 #[tokio::test]
2799 async fn sessions_reads_across_the_set_it_was_given() {
2800 let dir = tempfile::tempdir().expect("tempdir");
2801 let path = dir.path().join("events.db");
2802 let drivers = IsleDrivers::new();
2803
2804 let mut a = SqliteEventStore::open(&path, "stream-a", &drivers)
2805 .await
2806 .expect("open a");
2807 let mut b = SqliteEventStore::open(&path, "stream-b", &drivers)
2808 .await
2809 .expect("open b");
2810 a.append(obj(json!({ "kind": "from_a" }))).await.expect("a");
2811 b.append(obj(json!({ "kind": "from_b1" })))
2812 .await
2813 .expect("b1");
2814 b.append(obj(json!({ "kind": "from_b2" })))
2815 .await
2816 .expect("b2");
2817
2818 let opts = QueryOpts {
2819 sessions: Some(vec!["stream-a".to_string(), "stream-b".to_string()]),
2820 ..QueryOpts::default()
2821 };
2822 let rows = ask_with(
2823 &a,
2824 "SELECT stream, kind FROM events WHERE stream IN $sessions ORDER BY stream, seq",
2825 QueryParams::None,
2826 &opts,
2827 )
2828 .await
2829 .expect("query");
2830 assert_eq!(kinds_of(&rows), ["from_a", "from_b1", "from_b2"]);
2831
2832 // Left out, the set is the asking store's own stream.
2833 let rows = ask(&a, "SELECT kind FROM events WHERE stream IN $sessions")
2834 .await
2835 .expect("query");
2836 assert_eq!(kinds_of(&rows), ["from_a"]);
2837 }
2838
2839 /// A value is bound, never pasted: a quote inside it is a character in a
2840 /// string, not the end of one.
2841 #[tokio::test]
2842 async fn a_bound_value_with_a_quote_in_it_is_a_value() {
2843 let (mut store, _drivers) = mem_store().await;
2844 store
2845 .append(obj(json!({ "kind": "it's a kind" })))
2846 .await
2847 .expect("append");
2848 store.append(ev(1)).await.expect("append e1");
2849
2850 let rows = ask_with(
2851 &store,
2852 "SELECT kind FROM events WHERE kind = ?",
2853 QueryParams::Positional(vec![json!("it's a kind")]),
2854 &QueryOpts::default(),
2855 )
2856 .await
2857 .expect("query");
2858 assert_eq!(kinds_of(&rows), ["it's a kind"]);
2859
2860 // The same by name, and a value that would be SQL if it were pasted
2861 // in matches nothing rather than doing anything.
2862 let named = QueryParams::Named(
2863 json!({ "kind": "x' OR 1=1 --" })
2864 .as_object()
2865 .expect("an object")
2866 .clone(),
2867 );
2868 let rows = ask_with(
2869 &store,
2870 "SELECT kind FROM events WHERE kind = :kind",
2871 named,
2872 &QueryOpts::default(),
2873 )
2874 .await
2875 .expect("query");
2876 assert!(rows.rows.is_empty(), "{:?}", rows.rows);
2877 }
2878
2879 /// The cap is reported, not silently applied — and a result that happens
2880 /// to be exactly `limit` long is not called truncated.
2881 #[tokio::test]
2882 async fn the_row_cap_is_reported_when_it_cuts() {
2883 let (mut store, _drivers) = mem_store().await;
2884 for i in 1..=5 {
2885 store.append(ev(i)).await.expect("append");
2886 }
2887
2888 let capped = QueryOpts {
2889 limit: 2,
2890 ..QueryOpts::default()
2891 };
2892 let rows = ask_with(
2893 &store,
2894 "SELECT kind FROM events ORDER BY seq",
2895 QueryParams::None,
2896 &capped,
2897 )
2898 .await
2899 .expect("query");
2900 assert_eq!(kinds_of(&rows), ["e1", "e2"]);
2901 assert!(rows.truncated, "the cap cut three rows off");
2902
2903 let exact = QueryOpts {
2904 limit: 5,
2905 ..QueryOpts::default()
2906 };
2907 let rows = ask_with(
2908 &store,
2909 "SELECT kind FROM events ORDER BY seq",
2910 QueryParams::None,
2911 &exact,
2912 )
2913 .await
2914 .expect("query");
2915 assert_eq!(rows.rows.len(), 5);
2916 assert!(!rows.truncated, "nothing was cut off");
2917 }
2918
2919 /// A query that will not finish is cut short, and says so in its own
2920 /// class: nothing was contended, so "ask again" would be the wrong advice.
2921 #[tokio::test]
2922 async fn a_query_that_runs_too_long_is_a_timeout() {
2923 let (store, _drivers) = mem_store().await;
2924 let hurried = QueryOpts {
2925 timeout_ms: 50,
2926 ..QueryOpts::default()
2927 };
2928 let err = ask_with(
2929 &store,
2930 // Unbounded on purpose: it ends when the deadline ends it.
2931 "WITH RECURSIVE forever(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM forever) \
2932 SELECT COUNT(*) FROM forever",
2933 QueryParams::None,
2934 &hurried,
2935 )
2936 .await
2937 .expect_err("an endless query must be cut short");
2938 assert_eq!(err.kind(), KnlError::TIMEOUT, "{err}");
2939 assert!(!err.is_retryable(), "a slow query is not a retry: {err}");
2940
2941 // The connection is usable afterwards: the interrupt ended a
2942 // statement, not the reader.
2943 assert!(ask(&store, "SELECT 1 AS one").await.is_ok());
2944 }
2945
2946 /// The reader cannot write. The statement checks run on the text, but
2947 /// they are not the only thing standing between a caller and the log:
2948 /// the connection a query runs on has no write capability at all.
2949 #[tokio::test]
2950 async fn the_reader_connection_refuses_a_write() {
2951 let (mut store, _drivers) = mem_store().await;
2952 store.append(ev(1)).await.expect("append");
2953 let reader = store.reader().await.expect("open the reader");
2954
2955 let err = reader
2956 .call(|conn| {
2957 conn.execute(
2958 "INSERT INTO events \
2959 (stream, seq, epoch_ms, kind, schema_version, beat, meta, data) \
2960 VALUES ('x', 1, 0, 'note', 1, NULL, '{}', '{}')",
2961 [],
2962 )
2963 })
2964 .await
2965 .expect_err("the reader must not be able to write");
2966 assert!(
2967 matches!(KnlError::from(err), KnlError::Storage(_)),
2968 "a write through the reader is refused by SQLite itself"
2969 );
2970
2971 // …and the log is as it was.
2972 assert_eq!(store.len().await.expect("len"), 1);
2973 }
2974
2975 /// A statement that is not a read never reaches the connection, and a
2976 /// second statement is refused whole. (The rules are
2977 /// [`super::super::query`]'s; this is the path through the store.)
2978 #[tokio::test]
2979 async fn a_write_or_a_second_statement_is_refused_before_the_connection() {
2980 let (store, _drivers) = mem_store().await;
2981 for sql in [
2982 "INSERT INTO events (stream) VALUES ('x')",
2983 "UPDATE events SET kind = 'x'",
2984 "PRAGMA table_info(events)",
2985 "ATTACH DATABASE '/tmp/other.db' AS other",
2986 "SELECT 1; DROP TABLE events",
2987 ] {
2988 let err = ask(&store, sql).await.expect_err("must be refused");
2989 assert_eq!(err.kind(), KnlError::VALIDATION, "{sql:?}: {err}");
2990 }
2991 }
2992
2993 /// A parameter nobody answered, and a value nobody asked for, are both
2994 /// errors: a silent NULL is how a query quietly stops meaning what it
2995 /// says.
2996 #[tokio::test]
2997 async fn every_parameter_is_answered_and_every_value_is_used() {
2998 let (store, _drivers) = mem_store().await;
2999
3000 let err = ask(&store, "SELECT * FROM events WHERE kind = :kind")
3001 .await
3002 .expect_err("an unanswered parameter must be refused");
3003 assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
3004 assert!(err.reason().contains(":kind"), "{}", err.reason());
3005
3006 let err = ask_with(
3007 &store,
3008 "SELECT * FROM events WHERE kind = ?",
3009 QueryParams::Positional(vec![json!("a"), json!("b")]),
3010 &QueryOpts::default(),
3011 )
3012 .await
3013 .expect_err("a value with no parameter must be refused");
3014 assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
3015 }
3016
3017 /// Every SQLite type comes back as itself, and a NULL comes back as an
3018 /// absent column rather than a present nothing.
3019 #[tokio::test]
3020 async fn the_sqlite_types_map_onto_values_and_null_is_absence() {
3021 let (store, _drivers) = mem_store().await;
3022 let rows = ask(
3023 &store,
3024 // `absent`, not `nothing`: NOTHING is a SQLite keyword.
3025 "SELECT 1 AS whole, 1.5 AS fraction, 'text' AS words, NULL AS absent, \
3026 CAST('bytes' AS BLOB) AS raw",
3027 )
3028 .await
3029 .expect("query");
3030 let row = &rows.rows[0];
3031 assert_eq!(row["whole"], Value::from(1));
3032 assert_eq!(row["fraction"], Value::from(1.5));
3033 assert_eq!(row["words"], Value::from("text"));
3034 assert_eq!(row["raw"], Value::from("bytes"));
3035 assert!(
3036 !row.contains_key("absent"),
3037 "a NULL column is absent, so it reads as nil: {row:?}"
3038 );
3039 }
3040
3041 /// The published schema is the table: read back off SQLite rather than
3042 /// written out, with the two columns that make a stream a stream as its
3043 /// primary key.
3044 #[tokio::test]
3045 async fn the_published_schema_is_the_events_table() {
3046 let columns = events_schema().expect("schema");
3047 let names: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();
3048 assert_eq!(
3049 names,
3050 [
3051 "stream",
3052 "seq",
3053 "epoch_ms",
3054 "kind",
3055 "schema_version",
3056 "beat",
3057 "meta",
3058 "data"
3059 ]
3060 );
3061
3062 let pk: Vec<&str> = columns
3063 .iter()
3064 .filter(|c| c.pk)
3065 .map(|c| c.name.as_str())
3066 .collect();
3067 assert_eq!(pk, ["stream", "seq"], "the log is keyed by (stream, seq)");
3068
3069 let declared: Vec<&str> = columns.iter().map(|c| c.declared_type.as_str()).collect();
3070 assert_eq!(
3071 declared,
3072 ["TEXT", "INTEGER", "INTEGER", "TEXT", "INTEGER", "TEXT", "TEXT", "TEXT"]
3073 );
3074
3075 // And a query may name every one of them.
3076 let (store, _drivers) = mem_store().await;
3077 let sql = format!("SELECT {} FROM {EVENTS_TABLE}", names.join(", "));
3078 ask(&store, &sql)
3079 .await
3080 .expect("the published columns are the real ones");
3081 }
3082
3083 /// The published schema is also the *live* one: a store's own reader
3084 /// reports the same columns the schema-only path does, which is what
3085 /// makes reading it off a throwaway connection sound.
3086 #[tokio::test]
3087 async fn a_live_store_reports_the_published_schema() {
3088 let (store, _drivers) = mem_store().await;
3089 assert_eq!(
3090 store.schema().await.expect("schema"),
3091 events_schema().expect("published schema")
3092 );
3093 }
3094}