Skip to main content

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}