Skip to main content

agent_block_core/knl/
sqlite_store.rs

1//! The durable [`EventStore`]: one stream of an eventsdb log.
2//!
3//! [`SqliteEventStore`] is an *adapter*.  The kernel's [`EventStore`] is the
4//! kernel's SPI and does not move; underneath it is
5//! [`eventsdb_sqlite`] — a SQLite event log with a writer thread of its own, a
6//! pool of read-only connections beside it, a migration ladder for the table's
7//! shape and a transaction hatch for the two writes the kernel cannot express
8//! any other way.  What is left here is the translation: the kernel's
9//! vocabulary in, eventsdb's out, and back.
10//!
11//! ```text
12//!   knl::Logs ──▶ SqliteEventLog ──stream_handle(id)──▶ eventsdb SqliteEventStore
13//!                       │                                        ▲
14//!                       │ with_transaction(TxnContext)           │ delegate
15//!                       ▼                                        │
16//!            append_if_many / append_with_open_children     append / append_many
17//!            (two streams, and the child scan)              append_if / reads
18//! ```
19//!
20//! # What the adapter owns, and what it hands over
21//!
22//! Two things are the kernel's and stay here:
23//!
24//! - **[`validate_event`]**, the kernel's own rules — the envelope, and the
25//!   `data` of the six kinds the kernel writes ([`super::event`]).  eventsdb
26//!   checks the envelope too and knows nothing of a kind's shape, so the
27//!   kernel's check runs first, on every write path including the ones a
28//!   decision produces;
29//! - **the schema version.**  eventsdb takes the version from the event's
30//!   author and only fills in a default for an author who did not say
31//!   ([`eventsdb_core::event::stamp`]), so every append here stamps
32//!   [`CURRENT_SCHEMA_VERSION`] on the way past.  `seq` and `epoch_ms` are
33//!   removed for the same reason in reverse: they are the store's to assign,
34//!   so a caller-supplied one is dropped rather than trusted.
35//!
36//! Everything else is eventsdb's: the `IMMEDIATE` transaction every write
37//! takes, the busy retry, the per-stream `seq` counter, the global `position`,
38//! the upcaster chain, and the read-only connections a query runs on.
39//!
40//! # The chain runs once, and it runs down there
41//!
42//! [`kernel_upcasters`] is registered on the *log* ([`super::Logs`]), because
43//! eventsdb applies it to everything it reads — `read_kinds`, `read_last`, and
44//! the events a decision is shown inside its transaction.  So the seam above
45//! this ([`super::CurrentStore`]) carries an **empty** chain: its job here is
46//! the type, not the transform.  It still checks what it is handed
47//! ([`super::Current`]), which is what keeps "only upcasted events reach the
48//! domain" a property rather than a convention.
49//!
50//! # The two writes that go through the hatch
51//!
52//! [`EventStore::append_if_many`] and [`EventStore::append_with_open_children`]
53//! are the two operations that are not about one stream, and both are one
54//! transaction by necessity rather than for convenience: an allocation moves
55//! units between two ledgers, and a close records the children that had not
56//! ended *as of the write that records it*.  `log.with_transaction` hands over
57//! a [`TxnContext`] — the log's own stamped `append` / `append_many` / `read`,
58//! and a raw [`rusqlite::Transaction`] underneath for the child scan's
59//! `SELECT`.  Raw writes to `events` are refused there by SQLite's own
60//! authorizer, which is the point: an append cannot skip validation or
61//! sequencing by going round the side.
62//!
63//! # The read side
64//!
65//! [`EventStore::query`] is [`SqliteEventLog::query_timeout`]: the caller's
66//! statement, positional values ([`super::query`] resolved them), a deadline,
67//! and a read-only connection that is not the writer.  The row cap is the
68//! kernel's and is applied by *wrapping* the statement — `SELECT * FROM (…)
69//! LIMIT n + 1` — so one more row than the caller allowed is read and the
70//! extra one is what says the answer was cut ([`QueryRows::truncated`]).
71//!
72//! A `NULL` column comes back from eventsdb as a JSON null and is dropped from
73//! the row here, so the Lua side reads an absent key as `nil`, which is what a
74//! missing column means there.  That is the one conversion this adapter makes.
75//! The rest are eventsdb's, and it is **strict** about the three cells JSON has
76//! no value for — a `BLOB`, a non-finite `REAL`, and `TEXT` that is not UTF-8
77//! — refusing each as [`KnlError::Unsupported`] with the column named and the
78//! SQL that gets the value through (`hex(col)`, `CAST(col AS TEXT)`).  The
79//! store can be asked to substitute instead; it is not asked to, because a
80//! substitute is a value the caller cannot tell from one that was really in
81//! the row, and the log has no column that produces any of the three.
82//!
83//! A statement that does not compile is the caller's, not the store's, and is
84//! answered as [`KnlError::Validation`] even though eventsdb classes it with
85//! the disk faults ([`SQLITE_STATEMENT_ERRORS`]).
86//!
87//! [`TxnContext`]: eventsdb_sqlite::TxnContext
88
89use std::path::Path;
90use std::sync::{Arc, Mutex, PoisonError};
91
92use async_trait::async_trait;
93use eventsdb_core::store::EventStore as EventsdbStore;
94use eventsdb_core::upcast::Current as Upcasted;
95use eventsdb_sqlite::SqliteEventLog;
96use serde_json::{Map, Value};
97
98use super::event::{validate_event, FIELD_EPOCH_MS, FIELD_SEQ};
99use super::event_store::{
100    stamp_schema_version, ChildScan, ChildrenDecision, Committed, Decision, EventStore, Split,
101    SplitDecision,
102};
103use super::logs::Logs;
104use super::query::{QueryPlan, QueryRows};
105use super::{KnlError, KnlResult};
106
107/// The table the log lives in — published as the read contract
108/// ([`events_schema`]).
109pub const EVENTS_TABLE: &str = "events";
110
111/// The index the close-time child scan reads by.
112///
113/// Created once per log open ([`super::Logs`]) rather than declared in a DDL,
114/// because the table's shape is eventsdb's and this index is the kernel's:
115/// which openings name *this* stream as their parent is a question about the
116/// whole database, and without an index it is answered by walking every event
117/// in it.
118///
119/// It is a *partial expression* index and both halves are load-bearing.  The
120/// expression is written exactly as the scan writes it, because SQLite matches
121/// an indexed expression against a query's by form — a path bound as a
122/// parameter would never match one written as a literal, which is why
123/// [`child_scan_sql`] spells its words out.  The `WHERE` keeps the index to
124/// the openings: `parent` lives on `session_opened` and nowhere else, so
125/// indexing every row would be storing a NULL per event to find the handful
126/// that are not.
127///
128/// That is the kernel's vocabulary sitting in the store's schema, which the
129/// rest of this backend avoids ([`ChildScan`] is an argument, not a constant).
130/// The price is that those words are settled at open time; what it buys is
131/// that a close on a large log looks the openings up instead of walking the
132/// table.  A scan under some other vocabulary still reads correctly — it just
133/// reads without the index.
134pub(super) const CHILD_INDEX_DDL: &str = "CREATE INDEX IF NOT EXISTS \
135     events_session_opened_parent \
136         ON events (json_extract(data, '$.parent')) \
137      WHERE kind = 'session_opened';";
138
139/// One column of [`EVENTS_TABLE`].
140///
141/// Published to the shell so a caller writing SQL against the log reads the
142/// column names and types from the kernel rather than from a list somebody
143/// retyped — and so a test can hold the shell's declaration of the schema
144/// against the table that actually exists.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct SchemaColumn {
147    /// The column name.
148    pub name: String,
149    /// Its declared type, as written in the DDL.
150    pub declared_type: String,
151    /// Whether it is part of the primary key.
152    pub pk: bool,
153}
154
155/// The columns of the `events` table, in the order SQLite reports them.
156///
157/// A constant rather than a `PRAGMA table_info` against a throwaway database,
158/// for two reasons: the table is the store's and its DDL runs inside a
159/// migration ladder that is `async` — while `knl.api()` is a declaration of
160/// the surface, which should not have to be awaited — and a pragma is one of
161/// the things the store's hatch refuses, since setting one is how the ladder's
162/// own marker would be changed underneath it.  What keeps the constant honest
163/// is a test: it opens a real log and holds this list against what SQLite says
164/// the table has, so the two cannot drift apart unnoticed.
165///
166/// `position` is the key — the global order, dense and gap-free as read —
167/// and `(stream, seq)` is a unique constraint beside it rather than the
168/// primary key it used to be.  There is no `beat` column: the beat is a label
169/// of `meta` ([`super::event`]) and a read reaches it with
170/// `json_extract(meta, '$.beat')`, which the log has an index for.
171const EVENTS_COLUMNS: [(&str, &str, bool); 8] = [
172    ("position", "INTEGER", true),
173    ("stream", "TEXT", false),
174    ("seq", "INTEGER", false),
175    ("epoch_ms", "INTEGER", false),
176    ("kind", "TEXT", false),
177    ("schema_version", "INTEGER", false),
178    ("meta", "TEXT", false),
179    ("data", "TEXT", false),
180];
181
182/// The columns of the `events` table, without a session to ask.
183///
184/// The read contract, as data: what a caller's SQL may name.  It is what
185/// `knl.api()` publishes, and it is fallible only because it always was —
186/// there is nothing here that can fail now, and the shape is kept so a later
187/// backend that has to open something to answer can.
188pub fn events_schema() -> KnlResult<Vec<SchemaColumn>> {
189    Ok(EVENTS_COLUMNS
190        .iter()
191        .map(|(name, declared_type, pk)| SchemaColumn {
192            name: (*name).to_string(),
193            declared_type: (*declared_type).to_string(),
194            pk: *pk,
195        })
196        .collect())
197}
198
199/// A durable [`EventStore`] backed by an eventsdb log, scoped to one `stream`.
200///
201/// The session *is* the stream: one instance serves one session's log.  Several
202/// instances may point at the same log with different streams, and that is
203/// what a session tree is.
204pub struct SqliteEventStore {
205    /// The log the stream lives in.  Held so the database-level calls — the
206    /// transaction hatch, the query, the detached append — are reachable, and
207    /// so the log outlives every handle it issued.
208    log: Arc<SqliteEventLog>,
209    /// eventsdb's own handle on this stream: the per-stream calls delegate
210    /// straight to it.
211    handle: eventsdb_sqlite::SqliteEventStore,
212    /// The stream this store is scoped to — the session id.
213    stream: String,
214}
215
216impl SqliteEventStore {
217    /// Open (creating if absent) the log at `path`, scoped to `stream`.
218    ///
219    /// `logs` is where the open log is kept: a file is opened once per process
220    /// and shared from then on, so two sessions on one file are two streams of
221    /// one log rather than two logs racing for one file ([`Logs`]).
222    pub async fn open(path: &Path, stream: impl Into<String>, logs: &Logs) -> KnlResult<Self> {
223        Ok(Self::on(logs.file(path).await?, stream))
224    }
225
226    /// Open on the in-memory log, scoped to `stream`.
227    ///
228    /// One database per [`Logs`], not per stream: an ephemeral session is a
229    /// stream in it like any other, so it can have children and can be resumed
230    /// by name for as long as the host lives.  What it cannot do is survive
231    /// the process, and it does not pretend to.
232    pub async fn open_memory(stream: impl Into<String>, logs: &Logs) -> KnlResult<Self> {
233        Ok(Self::on(logs.memory().await?, stream))
234    }
235
236    /// A store on a log that is already open.
237    ///
238    /// The form a child takes: it is opened on its parent's log, which the
239    /// caller already has ([`Logs::database`]), and issuing a handle on it
240    /// waits for nothing.
241    pub fn on(log: Arc<SqliteEventLog>, stream: impl Into<String>) -> Self {
242        let stream = stream.into();
243        let handle = log.stream_handle(&stream);
244        Self {
245            log,
246            handle,
247            stream,
248        }
249    }
250}
251
252/// The kinds a read was asked for, owned, so the selection can travel into a
253/// closure that outlives the caller's slice.
254fn owned_kinds(kinds: Option<&[&str]>) -> Option<Vec<String>> {
255    kinds.map(|kinds| kinds.iter().map(|kind| (*kind).to_string()).collect())
256}
257
258/// Borrow an owned kind list back into the shape the read takes.
259fn borrowed_kinds(kinds: &Option<Vec<String>>) -> Option<Vec<&str>> {
260    kinds
261        .as_ref()
262        .map(|kinds| kinds.iter().map(String::as_str).collect())
263}
264
265/// An event on its way to the store: the kernel's coordinates removed, and
266/// the kernel's schema version stamped.
267///
268/// `seq` and `epoch_ms` are the store's to assign, so an event that carries
269/// either has it dropped rather than trusted — eventsdb refuses a stored
270/// coordinate on a new write, and silently accepting one would be a caller
271/// choosing where its event lands.  The version goes the other way: eventsdb
272/// takes it from the author and only defaults it, and the kernel *is* the
273/// author.
274fn prepared(mut event: Map<String, Value>) -> Map<String, Value> {
275    event.remove(FIELD_SEQ);
276    event.remove(FIELD_EPOCH_MS);
277    stamp_schema_version(&mut event);
278    event
279}
280
281/// eventsdb's coordinates as the kernel's.
282///
283/// The global `position` is dropped: the kernel's SPI is scoped to one stream,
284/// and `seq` is the coordinate inside it.
285fn committed_of(committed: eventsdb_core::position::Committed) -> Committed {
286    Committed {
287        seq: committed.seq,
288        epoch_ms: committed.epoch_ms,
289    }
290}
291
292/// Upcasted events as the raw [`Value`]s the kernel's SPI deals in.
293///
294/// eventsdb has already run the chain, so what comes back is the current
295/// shape; the seam above turns these back into [`super::Current`]s, which is
296/// where the version is checked.
297fn values_of(events: Vec<Upcasted>) -> Vec<Value> {
298    events
299        .into_iter()
300        .map(|event| Value::Object(event.into_inner()))
301        .collect()
302}
303
304/// The same, for a decision's input, which arrives borrowed.
305fn values_of_ref(events: &[Upcasted]) -> Vec<Value> {
306    events
307        .iter()
308        .map(|event| Value::Object((**event).clone()))
309        .collect()
310}
311
312/// Where a kernel error goes when it happens inside a closure that has no way
313/// to report one.
314///
315/// eventsdb's decisions answer with an event or with nothing, and its hatch
316/// answers in eventsdb's own error language.  A kernel refusal — an event a
317/// decision built wrong — is neither, so it is parked in a cell both sides can
318/// reach and raised by the caller: nothing is written, and the caller is told
319/// what was wrong rather than being handed the `Ok(None)` that would read as
320/// "the invariant said no".
321type Parked = Arc<Mutex<Option<KnlError>>>;
322
323/// Take whatever was parked, if anything.
324fn taken(parked: &Parked) -> Option<KnlError> {
325    parked.lock().unwrap_or_else(PoisonError::into_inner).take()
326}
327
328/// Park `error` for the caller to raise.
329fn park(parked: &Parked, error: KnlError) {
330    *parked.lock().unwrap_or_else(PoisonError::into_inner) = Some(error);
331}
332
333/// The refusal handed to eventsdb when a kernel error was parked: it rolls the
334/// transaction back, and the caller replaces it with the parked one.
335fn rolled_back() -> eventsdb_core::Error {
336    eventsdb_core::Error::validation("the kernel refused an event this write was to record")
337}
338
339#[async_trait]
340impl EventStore for SqliteEventStore {
341    async fn append(&mut self, event: Map<String, Value>) -> KnlResult<Committed> {
342        // Reject before touching the stream: a rejected event burns no seq.
343        validate_event(&event)?;
344        self.handle
345            .append(prepared(event))
346            .await
347            .map(committed_of)
348            .map_err(KnlError::from)
349    }
350
351    async fn append_many(&mut self, events: Vec<Map<String, Value>>) -> KnlResult<Vec<Committed>> {
352        // Validate before the transaction is opened: a batch with a malformed
353        // event in it never takes the write lock at all.
354        for event in &events {
355            validate_event(event)?;
356        }
357        let events: Vec<Map<String, Value>> = events.into_iter().map(prepared).collect();
358        self.handle
359            .append_many(events)
360            .await
361            .map(|committed| committed.into_iter().map(committed_of).collect())
362            .map_err(KnlError::from)
363    }
364
365    async fn append_if(
366        &mut self,
367        kinds: Option<&[&str]>,
368        decide: Decision,
369    ) -> KnlResult<Option<Committed>> {
370        // The read, the decision and the insert share one IMMEDIATE
371        // transaction on the log's own thread, so the invariant `decide`
372        // checks holds at the instant the event lands.  The decision travels
373        // with the job — it is owned and `Send` — so nothing waits on
374        // anything else with the write lock held.
375        let parked: Parked = Arc::default();
376        let sink = Arc::clone(&parked);
377        let answer: eventsdb_core::store::Decision = Box::new(move |seen: &[Upcasted]| {
378            let event = decide(values_of_ref(seen))?;
379            // The decision's event is the kernel's to check: eventsdb checks
380            // the envelope and knows nothing of a kernel kind's `data`.  A
381            // refusal parks and writes nothing, rather than reading as a
382            // decision that said no.
383            match validate_event(&event) {
384                Ok(()) => Some(prepared(event)),
385                Err(refusal) => {
386                    park(&sink, refusal);
387                    None
388                }
389            }
390        });
391        let committed = self.handle.append_if(kinds, answer).await;
392        match taken(&parked) {
393            Some(refusal) => Err(refusal),
394            None => committed
395                .map(|committed| committed.map(committed_of))
396                .map_err(KnlError::from),
397        }
398    }
399
400    async fn append_if_many(
401        &mut self,
402        other: &str,
403        kinds: Option<&[&str]>,
404        decide: SplitDecision,
405    ) -> KnlResult<Option<Split<Committed>>> {
406        // One transaction over both streams: they are rows of one table on one
407        // connection, so "two streams" costs the write nothing beyond a second
408        // counter read.  Not retried, because the decision is a `FnOnce` and
409        // an attempt consumes it.
410        let stream = self.stream.clone();
411        let other = other.to_string();
412        let kinds = owned_kinds(kinds);
413        let parked: Parked = Arc::default();
414        let sink = Arc::clone(&parked);
415
416        let committed = self
417            .log
418            .with_transaction(move |tx| {
419                let selection = borrowed_kinds(&kinds);
420                let seen = Split {
421                    own: values_of(tx.read(&stream, selection.as_deref(), 0, usize::MAX)?),
422                    // Unfiltered and capped at one: the question is "is there
423                    // an event", not "which", so a kind filter could only make
424                    // an occupied stream look empty.
425                    other: values_of(tx.read(&other, None, 0, 1)?),
426                };
427                let Some(split) = decide(seen) else {
428                    // Nothing to write: the transaction is rolled back.
429                    return Ok(None);
430                };
431                for event in split.own.iter().chain(split.other.iter()) {
432                    if let Err(refusal) = validate_event(event) {
433                        park(&sink, refusal);
434                        return Err(rolled_back());
435                    }
436                }
437                let own = tx.append_many(
438                    &stream,
439                    split.own.into_iter().map(prepared).collect::<Vec<_>>(),
440                )?;
441                let elsewhere = tx.append_many(
442                    &other,
443                    split.other.into_iter().map(prepared).collect::<Vec<_>>(),
444                )?;
445                Ok(Some(Split {
446                    own: own.into_iter().map(committed_of).collect(),
447                    other: elsewhere.into_iter().map(committed_of).collect(),
448                }))
449            })
450            .await;
451
452        match taken(&parked) {
453            Some(refusal) => Err(refusal),
454            None => committed.map_err(KnlError::from),
455        }
456    }
457
458    async fn append_with_open_children(
459        &mut self,
460        scan: &ChildScan,
461        decide: ChildrenDecision,
462    ) -> KnlResult<Committed> {
463        // The scan reads other streams and the insert writes this one, so they
464        // share the transaction: what the boundary records is what was true at
465        // the instant it landed, not a moment before it.
466        let stream = self.stream.clone();
467        let scan = scan.clone();
468        let parked: Parked = Arc::default();
469        let sink = Arc::clone(&parked);
470
471        let committed = self
472            .log
473            .with_transaction(move |tx| {
474                // The raw transaction underneath the context: the scan is a
475                // `SELECT` over `events`, which the hatch allows and has no
476                // stamped equivalent of.
477                let conn: &rusqlite::Connection = tx;
478                let children = open_children_in(conn, &stream, &scan)
479                    .map_err(|e| eventsdb_core::Error::storage(e.to_string()))?;
480                let event = decide(children);
481                if let Err(refusal) = validate_event(&event) {
482                    park(&sink, refusal);
483                    return Err(rolled_back());
484                }
485                tx.append(&stream, prepared(event)).map(committed_of)
486            })
487            .await;
488
489        match taken(&parked) {
490            Some(refusal) => Err(refusal),
491            None => committed.map_err(KnlError::from),
492        }
493    }
494
495    fn database(&self) -> Option<&str> {
496        Some(self.log.database())
497    }
498
499    async fn read_kinds(
500        &self,
501        kinds: Option<&[&str]>,
502        from_seq: u64,
503        limit: usize,
504    ) -> KnlResult<Vec<Value>> {
505        self.handle
506            .read_kinds(kinds, from_seq, limit)
507            .await
508            .map(values_of)
509            .map_err(KnlError::from)
510    }
511
512    async fn read_last(&self, n: usize) -> KnlResult<Vec<Value>> {
513        self.handle
514            .read_last(n)
515            .await
516            .map(values_of)
517            .map_err(KnlError::from)
518    }
519
520    async fn head(&self) -> KnlResult<Option<u64>> {
521        self.handle.head().await.map_err(KnlError::from)
522    }
523
524    async fn len(&self) -> KnlResult<usize> {
525        self.handle.len().await.map_err(KnlError::from)
526    }
527
528    /// The caller's statement, on a read-only connection that is not the
529    /// writer.
530    ///
531    /// A `NULL` column is dropped rather than carried as a null, so it reads
532    /// as `nil` where a missing column does.  A cell JSON has no value for —
533    /// a `BLOB`, a non-finite `REAL`, `TEXT` that is not UTF-8 — is not
534    /// carried either, but it is *refused*, as [`KnlError::Unsupported`]
535    /// naming the column and the SQL that gets it through.  Those are the two
536    /// answers a row can end in, and they are different on purpose: absence is
537    /// something the caller can read, while a stand-in for a value the bridge
538    /// could not carry is something they would read as a value.
539    async fn query(&self, plan: &QueryPlan) -> KnlResult<QueryRows> {
540        // The cap is the kernel's, and the statement is the caller's, so the
541        // one is put around the other: `limit + 1` rows are asked for and the
542        // extra one is what says the answer was cut.  `plan.sql` is one
543        // statement with no trailing `;` ([`super::query`]), which is what
544        // makes it a subquery rather than a splice.
545        let cap = i64::try_from(plan.limit)
546            .unwrap_or(i64::MAX)
547            .saturating_add(1);
548        let sql = format!("SELECT * FROM ({}) LIMIT {cap}", plan.sql);
549        let rows = self
550            .log
551            .query_timeout(&sql, plan.values.clone(), plan.timeout)
552            .await
553            .map_err(query_error)?;
554
555        let truncated = rows.len() > plan.limit;
556        Ok(QueryRows {
557            rows: rows
558                .into_iter()
559                .take(plan.limit)
560                // A NULL is an absent key rather than a null value: the Lua
561                // side reads it as `nil`, which is what a missing column means
562                // there.
563                .map(|row| {
564                    row.into_iter()
565                        .filter(|(_, value)| !value.is_null())
566                        .collect()
567                })
568                .collect(),
569            truncated,
570        })
571    }
572
573    fn detach_append(&self, event: Map<String, Value>) {
574        // The drop backstop's path, and the one write nobody awaits.  A handle
575        // that was collected has no caller left to raise to and no task left to
576        // wait in, so the job is handed to the log's own queue and let go of:
577        // it lands before the host drains that queue at shutdown
578        // ([`Logs::shutdown`]).
579        if let Err(e) = validate_event(&event) {
580            tracing::warn!(error = %e, "knl: a detached append was refused before it was submitted");
581            return;
582        }
583        if let Err(e) = self.log.detach_append(&self.stream, prepared(event)) {
584            tracing::warn!(error = %e, "knl: a detached append was not accepted by the log");
585        }
586    }
587}
588
589/// `text` as an SQL string literal, with any quote in it doubled.
590///
591/// For the two places a *word* rather than a value has to go into a statement
592/// ([`child_scan_sql`]): `json_extract`'s path argument is not a value SQLite
593/// will take a parameter for, and a term the planner has to compare against a
594/// partial index's `WHERE` cannot be one either.  Doubling is the whole of
595/// SQLite's escaping rule for a single-quoted literal, so this closes the hole
596/// that interpolating text otherwise opens.
597fn sql_literal(text: &str) -> String {
598    format!("'{}'", text.replace('\'', "''"))
599}
600
601/// The statement [`open_children_in`] runs, with the scan's two words written
602/// into it as literals.
603///
604/// The kind and the JSON path are literals rather than parameters *so that the
605/// planner can see them*: [`CHILD_INDEX_DDL`] is a partial index on an
606/// expression, and both halves are matched by form — a `kind = ?` term proves
607/// nothing about `WHERE kind = 'session_opened'`, and a bound path never
608/// matches an indexed one.  The parent being looked for stays a parameter,
609/// because it is a value.  A test holds the query plan against the index name,
610/// so this cannot quietly become a table scan again.
611fn child_scan_sql(scan: &ChildScan) -> String {
612    let opened = sql_literal(&scan.opened);
613    let closed = sql_literal(&scan.closed);
614    let path = sql_literal(&format!("$.{}", scan.parent_field));
615    format!(
616        "SELECT opened.stream \
617           FROM events AS opened \
618          WHERE opened.kind = {opened} \
619            AND json_extract(opened.data, {path}) = ?1 \
620            AND NOT EXISTS ( \
621                SELECT 1 FROM events AS ending \
622                 WHERE ending.stream = opened.stream AND ending.kind = {closed} \
623            ) \
624          ORDER BY opened.epoch_ms, opened.stream"
625    )
626}
627
628/// The streams that name `stream` as their parent and carry no ending.
629///
630/// The vocabulary is the caller's ([`ChildScan`]): which kind opens a stream,
631/// which kind ends one, and where in the opening's `data` the parent is named.
632/// Those three words are written into the statement ([`child_scan_sql`])
633/// rather than bound, which is what lets the scan read by the parent index
634/// instead of walking every event in the database.
635///
636/// Ordered by when each child opened, so a close records its children in the
637/// order they were started rather than in whatever order the rows came back.
638fn open_children_in(
639    conn: &rusqlite::Connection,
640    stream: &str,
641    scan: &ChildScan,
642) -> rusqlite::Result<Vec<String>> {
643    let mut stmt = conn.prepare(&child_scan_sql(scan))?;
644    let rows = stmt.query_map(rusqlite::params![stream], |row| row.get::<_, String>(0))?;
645    rows.collect()
646}
647
648/// Whether a rusqlite error is a retryable lock contention (matched on the
649/// SQLite error *code*, never the message text).
650fn is_retryable(error: &rusqlite::Error) -> bool {
651    matches!(
652        error,
653        rusqlite::Error::SqliteFailure(inner, _)
654            if matches!(
655                inner.code,
656                rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
657            )
658    )
659}
660
661/// Classify a rusqlite error into the kernel's vocabulary.
662///
663/// The store's own SQL goes through eventsdb, which has its own classification
664/// ([`From<eventsdb_core::Error>`]); what is left on this path is the SQL the
665/// kernel runs itself — the legacy migration's plain connection
666/// ([`super::logs`]).  The split is the one the caller can act on: a contended
667/// lock is [`KnlError::Busy`] — the same call may succeed if it is made again
668/// — and everything else is [`KnlError::Storage`], a fault the kernel cannot
669/// promise anything about.  Matched on the SQLite error *code*, never the
670/// message text, so the classification does not drift with a library's
671/// wording.
672impl From<rusqlite::Error> for KnlError {
673    fn from(error: rusqlite::Error) -> Self {
674        if is_retryable(&error) {
675            return KnlError::Busy(format!("sqlite: busy/locked: {error}"));
676        }
677        KnlError::Storage(format!("sqlite: {error}"))
678    }
679}
680
681/// Translate the store's failure into the kernel's vocabulary.
682///
683/// One class each, because the two vocabularies were drawn along the same line
684/// — what a caller can *do* about it — and the six that exist on both sides
685/// mean the same thing on both sides.  The message travels as it was written:
686/// it is the reason, and the kernel renders the class itself.
687///
688/// The rest go to [`KnlError::Storage`] with their text.  `Truncated`,
689/// `HeadMismatch`, and the two retention refusals are answers to calls this
690/// kernel does not make — nothing here removes history, and no append names
691/// the head it expects — so a caller meeting one is meeting the store failing
692/// to do the work, which is what `Storage` says.  The enum is
693/// `#[non_exhaustive]`, so a class added later lands there too rather than
694/// failing to compile.
695impl From<eventsdb_core::Error> for KnlError {
696    fn from(error: eventsdb_core::Error) -> Self {
697        use eventsdb_core::Error as Failure;
698        match error {
699            Failure::Validation(reason) => KnlError::Validation(reason),
700            Failure::Busy(reason) => KnlError::Busy(reason),
701            Failure::Timeout(reason) => KnlError::Timeout(reason),
702            Failure::Storage(reason) => KnlError::Storage(reason),
703            Failure::Corruption(reason) => KnlError::Corruption(reason),
704            Failure::Unsupported(reason) => KnlError::Unsupported(reason),
705            other => KnlError::Storage(other.to_string()),
706        }
707    }
708}
709
710/// SQLite's own words for a statement that did not compile.
711///
712/// A stand-in for a class the store does not have yet.  eventsdb sorts a
713/// statement that fails to *prepare* — a misspelled column, a syntax error —
714/// into `Storage` along with a disk fault, because its classification has no
715/// statement class to put it in.  The two are not the same answer: `storage`
716/// says the store is not well, `validation` says the caller's statement is,
717/// and a reader branches on which.  So the query path — and only the query
718/// path, since it is the only one that runs a statement the caller wrote —
719/// reads the message back for these phrases and returns the statement errors
720/// to the class they belong to.
721///
722/// The ask for a statement class upstream is filed separately; when it lands,
723/// this table and [`query_error`] go with it.  It has not landed: eventsdb
724/// 0.5.0 moved a *parameter* fault — a name the statement does not declare, a
725/// positional count that does not match — from `Storage` to `Validation`, and
726/// that is a fault rusqlite raises before SQLite compiles anything.  A
727/// statement that does not compile is still `Storage`, so every phrase below
728/// is still load-bearing and none of them was about a parameter.  (A parameter
729/// mismatch cannot reach here anyway: [`super::query`] resolves every
730/// parameter to a `?` and a value in the same order at plan time, so the count
731/// is right by construction and there are no names left to miss.)
732///
733/// Until a statement class exists, matching the text is what is left, and it
734/// is kept to the phrases SQLite itself produces: rusqlite renders them as
735/// `… : <sqlite message>`, so the test is `contains`, not a prefix of the
736/// whole string.
737const SQLITE_STATEMENT_ERRORS: [&str; 7] = [
738    "no such column",
739    "no such table",
740    "no such function",
741    "syntax error",
742    "near \"",
743    "wrong number of arguments",
744    "ambiguous column name",
745];
746
747/// Whether `message` is SQLite refusing to compile the caller's statement.
748fn is_statement_error(message: &str) -> bool {
749    SQLITE_STATEMENT_ERRORS
750        .iter()
751        .any(|phrase| message.contains(phrase))
752}
753
754/// [`From<eventsdb_core::Error>`] for the query path: a `Storage` failure that
755/// is really the caller's statement comes back as [`KnlError::Validation`],
756/// prefixed `sql:` so the reason names which half of the request was wrong.
757///
758/// Everything else travels unchanged — a busy read is still `busy`, a
759/// deadline is still `timeout`, and a fault that is the store's is still
760/// `storage`.
761fn query_error(error: eventsdb_core::Error) -> KnlError {
762    match KnlError::from(error) {
763        KnlError::Storage(reason) if is_statement_error(&reason) => {
764            KnlError::Validation(format!("sql: {reason}"))
765        }
766        other => other,
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773    use crate::knl::event::{kind_of, seq_of, FIELD_DATA, FIELD_META};
774    use crate::knl::query::{self, QueryOpts, QueryParams};
775    use crate::knl::CURRENT_SCHEMA_VERSION;
776    use serde_json::json;
777
778    /// Object map for an event literal.
779    fn obj(value: Value) -> Map<String, Value> {
780        match value {
781            Value::Object(map) => map,
782            other => panic!("test fixture must be an object, got {other}"),
783        }
784    }
785
786    /// An event of a caller's own kind, named `e{i}`.
787    fn ev(i: usize) -> Map<String, Value> {
788        obj(json!({ "kind": format!("e{i}") }))
789    }
790
791    /// A `budget_*` event of `amount`, as the kernel writes one.
792    fn budget(kind: &str, amount: i64) -> Map<String, Value> {
793        obj(json!({ "kind": kind, "data": { "amount": amount } }))
794    }
795
796    /// A store on an in-memory log of its very own.
797    ///
798    /// The [`Logs`] comes back with the store because the caller has to hold
799    /// it: it owns the log, and a test that dropped it early would be pulling
800    /// the database out from under its own assertions.  One `Logs` per test is
801    /// also what keeps two tests running in parallel out of each other's log.
802    async fn mem_store() -> (SqliteEventStore, Logs) {
803        let logs = Logs::new();
804        let store = SqliteEventStore::open_memory(uuid::Uuid::new_v4().to_string(), &logs)
805            .await
806            .expect("open");
807        (store, logs)
808    }
809
810    /// A decision as [`EventStore::append_if`] takes one: owned, and handed
811    /// its input by value.
812    fn decide(
813        f: impl FnOnce(Vec<Value>) -> Option<Map<String, Value>> + Send + 'static,
814    ) -> Decision {
815        Box::new(f)
816    }
817
818    #[tokio::test]
819    async fn append_assigns_gap_free_monotonic_seq_from_one() {
820        let (mut store, _logs) = mem_store().await;
821        assert!(store.is_empty().await.expect("is_empty"));
822        assert_eq!(store.len().await.expect("len"), 0);
823
824        let a = store.append(ev(1)).await.expect("append e1");
825        let b = store.append(ev(2)).await.expect("append e2");
826        let c = store.append(ev(3)).await.expect("append e3");
827
828        assert_eq!((a.seq, b.seq, c.seq), (1, 2, 3));
829        assert_eq!(store.len().await.expect("len"), 3);
830        assert!(!store.is_empty().await.expect("is_empty"));
831
832        // The stamped epoch is what is stored.
833        let stored = store.read(0, usize::MAX).await.expect("read");
834        let stored_epoch = stored[0]
835            .get("epoch_ms")
836            .and_then(Value::as_u64)
837            .expect("epoch is on the stored event");
838        assert_eq!(stored_epoch, a.epoch_ms);
839    }
840
841    #[tokio::test]
842    async fn a_rejected_append_records_nothing_and_burns_no_seq() {
843        let (mut store, _logs) = mem_store().await;
844        store
845            .append(obj(json!({ "text": "no kind" })))
846            .await
847            .expect_err("kind is required");
848        assert_eq!(store.len().await.expect("len"), 0);
849        assert_eq!(store.append(ev(1)).await.expect("append").seq, 1);
850    }
851
852    /// The coordinates are the store's: an event that arrives carrying `seq`
853    /// or `epoch_ms` has them replaced rather than honoured, so a caller
854    /// cannot choose where its event lands or when it says it happened.
855    #[tokio::test]
856    async fn a_caller_supplied_coordinate_is_overwritten() {
857        let (mut store, _logs) = mem_store().await;
858        store.append(ev(1)).await.expect("seed");
859
860        let committed = store
861            .append(obj(json!({ "kind": "e2", "seq": 99, "epoch_ms": 7 })))
862            .await
863            .expect("append");
864        assert_eq!(committed.seq, 2, "the store numbers the stream");
865        assert_ne!(committed.epoch_ms, 7, "the store reads the clock");
866
867        let stored = store.read(0, usize::MAX).await.expect("read");
868        assert_eq!(seq_of(&stored[1]), 2);
869        assert_eq!(
870            stored[1].get("_schema_version").and_then(Value::as_u64),
871            Some(CURRENT_SCHEMA_VERSION),
872            "every append is stamped with the kernel's version"
873        );
874    }
875
876    /// `append_if` decides on the stream inside its transaction: the events
877    /// it is handed are the durable ones, a `Some` lands at the next seq, and
878    /// a `None` commits nothing.
879    #[tokio::test]
880    async fn append_if_decides_inside_the_transaction_and_writes_only_a_some() {
881        let (mut store, _logs) = mem_store().await;
882        store.append(ev(1)).await.expect("seed");
883
884        // The decision runs on the connection's own thread, so what it saw
885        // comes back through a shared cell rather than a borrow.
886        let seen_kinds: Arc<Mutex<Vec<String>>> = Arc::default();
887        let recorded = Arc::clone(&seen_kinds);
888        let committed = store
889            .append_if(
890                None,
891                decide(move |events| {
892                    *recorded.lock().expect("not poisoned") =
893                        events.iter().map(|e| kind_of(e).to_string()).collect();
894                    Some(ev(2))
895                }),
896            )
897            .await
898            .expect("append_if");
899        assert_eq!(
900            *seen_kinds.lock().expect("not poisoned"),
901            ["e1"],
902            "decide saw the durable stream"
903        );
904        assert_eq!(committed.map(|c| c.seq), Some(2));
905
906        let nothing = store
907            .append_if(None, decide(|_| None))
908            .await
909            .expect("append_if");
910        assert_eq!(nothing, None);
911        assert_eq!(store.len().await.expect("len"), 2, "a None commits nothing");
912        assert_eq!(store.append(ev(3)).await.expect("append").seq, 3);
913    }
914
915    /// A malformed decision is refused and leaves the stream alone — and the
916    /// caller is told, rather than being handed the `None` that would read as
917    /// a decision that said no.
918    #[tokio::test]
919    async fn append_if_validates_the_event_the_decision_returns() {
920        let (mut store, _logs) = mem_store().await;
921        let err = store
922            .append_if(None, decide(|_| Some(obj(json!({ "text": "no kind" })))))
923            .await
924            .expect_err("kind is required");
925        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
926        assert_eq!(store.len().await.expect("len"), 0);
927    }
928
929    /// The kernel's own rules reach a decision's event too: a kernel kind
930    /// whose `data` is missing a required field is refused, which eventsdb
931    /// (which knows no kind) would have accepted.
932    #[tokio::test]
933    async fn append_if_holds_a_kernel_kind_to_its_data() {
934        let (mut store, _logs) = mem_store().await;
935        let err = store
936            .append_if(
937                None,
938                decide(|_| Some(obj(json!({ "kind": "budget_spent", "data": {} })))),
939            )
940            .await
941            .expect_err("a kernel kind needs its data");
942        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
943        assert!(err.reason().contains("amount"), "{}", err.reason());
944        assert_eq!(store.len().await.expect("len"), 0);
945    }
946
947    /// A batch is one transaction: the events land together, numbered on from
948    /// the live head — and a batch that fails part-way leaves the stream
949    /// exactly as it was, which is the whole reason it is one call.
950    #[tokio::test]
951    async fn append_many_is_one_transaction_that_lands_whole_or_not_at_all() {
952        let (mut store, _logs) = mem_store().await;
953        store.append(ev(1)).await.expect("seed");
954
955        let committed = store
956            .append_many(vec![ev(2), ev(3)])
957            .await
958            .expect("the batch");
959        assert_eq!(
960            committed.iter().map(|c| c.seq).collect::<Vec<_>>(),
961            [2, 3],
962            "numbered on from the head that was there"
963        );
964        let stored = store.read(0, usize::MAX).await.expect("read");
965        let kinds: Vec<&str> = stored.iter().map(kind_of).collect();
966        assert_eq!(kinds, ["e1", "e2", "e3"]);
967
968        // A malformed event refuses the whole batch, and the one before it in
969        // the same call is not in the log either.
970        store
971            .append_many(vec![ev(4), obj(json!({ "text": "no kind" }))])
972            .await
973            .expect_err("kind is required");
974        assert_eq!(
975            store.len().await.expect("len"),
976            3,
977            "a batch that fails lands nothing"
978        );
979    }
980
981    /// A two-stream write is one transaction: each side is numbered from its
982    /// own head, both land together, and a `None` decision — or a malformed
983    /// event on either side — leaves both streams exactly as they were.
984    #[tokio::test]
985    async fn append_if_many_writes_both_streams_or_neither() {
986        let logs = Logs::new();
987        let parent = uuid::Uuid::new_v4().to_string();
988        let child = uuid::Uuid::new_v4().to_string();
989        let log = logs.memory().await.expect("the log");
990        let mut ledger = SqliteEventStore::on(Arc::clone(&log), parent.clone());
991        let opened = SqliteEventStore::on(Arc::clone(&log), child.clone());
992        assert_eq!(
993            ledger.database(),
994            opened.database(),
995            "both streams are in one database"
996        );
997
998        ledger
999            .append(budget("budget_granted", 100))
1000            .await
1001            .expect("the grant");
1002
1003        // The decision is shown its own stream, filtered, and the other
1004        // stream's first event — which is nothing, since it is empty.
1005        let seen: Arc<Mutex<(usize, usize)>> = Arc::default();
1006        let recorded = Arc::clone(&seen);
1007        let child_stream = child.clone();
1008        let committed = ledger
1009            .append_if_many(
1010                &child,
1011                Some(&["budget_granted"]),
1012                Box::new(move |split: Split<Value>| {
1013                    *recorded.lock().expect("not poisoned") = (split.own.len(), split.other.len());
1014                    Some(Split {
1015                        own: vec![obj(json!({
1016                            "kind": "budget_reserved",
1017                            "data": { "amount": 10, "child": child_stream },
1018                        }))],
1019                        other: vec![
1020                            obj(json!({
1021                                "kind": "session_opened",
1022                                "data": { "scope_id": "sc-1", "owner": "o", "parent": "p" },
1023                            })),
1024                            budget("budget_granted", 10),
1025                        ],
1026                    })
1027                }),
1028            )
1029            .await
1030            .expect("append_if_many")
1031            .expect("a Some writes");
1032        assert_eq!(
1033            *seen.lock().expect("not poisoned"),
1034            (1, 0),
1035            "its own kinds, and an empty other stream"
1036        );
1037        assert_eq!(committed.own.iter().map(|c| c.seq).collect::<Vec<_>>(), [2]);
1038        assert_eq!(
1039            committed.other.iter().map(|c| c.seq).collect::<Vec<_>>(),
1040            [1, 2],
1041            "the other stream is numbered from its own head"
1042        );
1043
1044        // A None writes nothing at all.
1045        let nothing = ledger
1046            .append_if_many(&child, None, Box::new(|_| None))
1047            .await
1048            .expect("append_if_many");
1049        assert_eq!(nothing, None);
1050        assert_eq!(ledger.len().await.expect("len"), 2);
1051        assert_eq!(opened.len().await.expect("len"), 2);
1052
1053        // A malformed event on the far side leaves both streams as they were.
1054        let err = ledger
1055            .append_if_many(
1056                &child,
1057                None,
1058                Box::new(|_| {
1059                    Some(Split {
1060                        own: vec![budget("budget_spent", 1)],
1061                        other: vec![obj(json!({ "text": "no kind" }))],
1062                    })
1063                }),
1064            )
1065            .await
1066            .expect_err("kind is required");
1067        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
1068        assert_eq!(ledger.len().await.expect("len"), 2);
1069        assert_eq!(opened.len().await.expect("len"), 2);
1070    }
1071
1072    /// The close-time child scan reads by the parent index instead of walking
1073    /// every event in the database.
1074    ///
1075    /// The plan is the assertion because the alternative is silent: a bound
1076    /// `kind` proves nothing about the index's `WHERE kind = 'session_opened'`
1077    /// and a bound path never matches an indexed expression, so getting either
1078    /// wrong still answers correctly — it just answers by reading the whole
1079    /// table, on the one query that is not scoped to a stream.
1080    #[tokio::test]
1081    async fn the_child_scan_reads_by_the_parent_index() {
1082        let logs = Logs::new();
1083        let log = logs.memory().await.expect("the log");
1084        let scan = ChildScan {
1085            opened: "session_opened".to_string(),
1086            closed: "session_closed".to_string(),
1087            parent_field: "parent".to_string(),
1088        };
1089        let rows = log
1090            .query(
1091                &format!("EXPLAIN QUERY PLAN {}", child_scan_sql(&scan)),
1092                vec![Value::from("p-1")],
1093            )
1094            .await
1095            .expect("the plan");
1096        let plan: String = rows
1097            .iter()
1098            .filter_map(|row| row.get("detail").and_then(Value::as_str))
1099            .collect::<Vec<_>>()
1100            .join(" | ");
1101        assert!(
1102            plan.contains("events_session_opened_parent"),
1103            "the scan must read by the parent index: {plan}"
1104        );
1105    }
1106
1107    /// The scan's words go into the statement as literals, so a quote in one
1108    /// of them is doubled rather than closing the string early.
1109    ///
1110    /// They are the kernel's own constants today, which is why writing them
1111    /// in is safe *and* why nothing would notice if they stopped being: the
1112    /// vocabulary is an argument ([`ChildScan`]), and a word that ended the
1113    /// literal would turn the rest of the statement into SQL somebody else
1114    /// wrote.
1115    #[test]
1116    fn a_word_written_into_the_scan_stays_one_word() {
1117        let sql = child_scan_sql(&ChildScan {
1118            opened: "it's opened".to_string(),
1119            closed: "it's closed".to_string(),
1120            parent_field: "it's parent".to_string(),
1121        });
1122        assert!(sql.contains("'it''s opened'"), "{sql}");
1123        assert!(sql.contains("'it''s closed'"), "{sql}");
1124        assert!(sql.contains("'$.it''s parent'"), "{sql}");
1125        assert_eq!(
1126            sql.matches('\'').count() % 2,
1127            0,
1128            "every literal is closed: {sql}"
1129        );
1130    }
1131
1132    /// `database` names the database, not the stream: two stores on one file
1133    /// answer with the same string and a store on another file does not.
1134    /// That is the whole of what the identity is for — deciding whether one
1135    /// transaction can cover both.
1136    #[tokio::test]
1137    async fn database_is_the_same_for_two_streams_of_one_database() {
1138        let logs = Logs::new();
1139        let dir = tempfile::tempdir().expect("tempdir");
1140        let here = dir.path().join("knl.db");
1141        let there = dir.path().join("other.db");
1142
1143        let a = SqliteEventStore::open(&here, "s-1", &logs)
1144            .await
1145            .expect("open a");
1146        let b = SqliteEventStore::open(&here, "s-2", &logs)
1147            .await
1148            .expect("open b");
1149        let elsewhere = SqliteEventStore::open(&there, "s-1", &logs)
1150            .await
1151            .expect("open elsewhere");
1152
1153        assert_eq!(a.database(), b.database());
1154        assert_ne!(a.database(), elsewhere.database());
1155
1156        // The in-memory log is a database of its own, and says so.
1157        let (mem, _mem_logs) = mem_store().await;
1158        assert_ne!(mem.database(), a.database());
1159        assert!(mem.database().is_some());
1160    }
1161
1162    /// The child scan finds the streams that name this one as their parent
1163    /// and carry no ending — and nobody else's children, and not the ones
1164    /// that already closed.
1165    #[tokio::test]
1166    async fn open_children_are_the_unended_streams_that_name_this_one() {
1167        let logs = Logs::new();
1168        let log = logs.memory().await.expect("the log");
1169        let scan = ChildScan {
1170            opened: "session_opened".to_string(),
1171            closed: "session_closed".to_string(),
1172            parent_field: "parent".to_string(),
1173        };
1174
1175        /// A stream that opened, naming `parent`.
1176        async fn opened(log: &Arc<SqliteEventLog>, id: &str, parent: &str) -> SqliteEventStore {
1177            let mut store = SqliteEventStore::on(Arc::clone(log), id);
1178            store
1179                .append(obj(json!({
1180                    "kind": "session_opened",
1181                    "data": { "scope_id": "sc", "owner": "o", "parent": parent },
1182                })))
1183                .await
1184                .expect("the opening");
1185            store
1186        }
1187
1188        let mut parent = SqliteEventStore::on(Arc::clone(&log), "parent");
1189        let _open_child = opened(&log, "child-open", "parent").await;
1190        let mut ended = opened(&log, "child-ended", "parent").await;
1191        let _elsewhere = opened(&log, "child-of-other", "another").await;
1192        ended
1193            .append(obj(json!({
1194                "kind": "session_closed",
1195                "data": { "reason": "done" },
1196            })))
1197            .await
1198            .expect("the ending");
1199
1200        let recorded: Arc<Mutex<Vec<String>>> = Arc::default();
1201        let seen = Arc::clone(&recorded);
1202        let committed = parent
1203            .append_with_open_children(
1204                &scan,
1205                Box::new(move |children| {
1206                    *seen.lock().expect("not poisoned") = children.clone();
1207                    obj(json!({
1208                        "kind": "session_closed",
1209                        "data": { "reason": "done", "open_children": children },
1210                    }))
1211                }),
1212            )
1213            .await
1214            .expect("the close");
1215        assert_eq!(committed.seq, 1, "the close is the parent's first event");
1216        assert_eq!(
1217            *recorded.lock().expect("not poisoned"),
1218            ["child-open"],
1219            "only this stream's children, and only the open ones"
1220        );
1221    }
1222
1223    /// A kind-filtered read is answered off the index: only the kinds asked
1224    /// for come back, in `seq` order, still carrying the `seq` the stream gave
1225    /// them.  `None` is the whole stream, an empty selection is nothing.
1226    #[tokio::test]
1227    async fn read_kinds_selects_by_kind_and_keeps_the_streams_order() {
1228        let (mut store, _logs) = mem_store().await;
1229        store
1230            .append(budget("budget_granted", 100))
1231            .await
1232            .expect("grant");
1233        store.append(ev(1)).await.expect("noise");
1234        store
1235            .append(budget("budget_spent", 10))
1236            .await
1237            .expect("spend");
1238
1239        let all = store.read(0, usize::MAX).await.expect("read");
1240        assert_eq!(
1241            all.iter().map(kind_of).collect::<Vec<_>>(),
1242            ["budget_granted", "e1", "budget_spent"]
1243        );
1244
1245        let ledger = store
1246            .read_kinds(Some(&["budget_granted", "budget_spent"]), 0, usize::MAX)
1247            .await
1248            .expect("read_kinds");
1249        assert_eq!(
1250            ledger.iter().map(seq_of).collect::<Vec<_>>(),
1251            [1, 3],
1252            "the seq the stream gave them, not a fresh numbering"
1253        );
1254
1255        let nothing = store
1256            .read_kinds(Some(&[]), 0, usize::MAX)
1257            .await
1258            .expect("read_kinds");
1259        assert!(nothing.is_empty(), "an empty selection selects nothing");
1260    }
1261
1262    /// A decision that names its kinds is shown those and nothing else, and
1263    /// its write is still numbered against the whole stream — the filter is
1264    /// what the decision *reads*, not where its answer goes.
1265    #[tokio::test]
1266    async fn append_if_filters_the_decisions_input_and_numbers_against_the_stream() {
1267        let (mut store, _logs) = mem_store().await;
1268        store
1269            .append(budget("budget_granted", 100))
1270            .await
1271            .expect("grant");
1272        store.append(ev(1)).await.expect("noise");
1273
1274        let seen: Arc<Mutex<Vec<String>>> = Arc::default();
1275        let recorded = Arc::clone(&seen);
1276        let committed = store
1277            .append_if(
1278                Some(&["budget_granted", "budget_spent"]),
1279                decide(move |events| {
1280                    *recorded.lock().expect("not poisoned") =
1281                        events.iter().map(|e| kind_of(e).to_string()).collect();
1282                    Some(budget("budget_spent", 10))
1283                }),
1284            )
1285            .await
1286            .expect("append_if");
1287        assert_eq!(
1288            *seen.lock().expect("not poisoned"),
1289            ["budget_granted"],
1290            "only the kinds asked for"
1291        );
1292        assert_eq!(
1293            committed.map(|c| c.seq),
1294            Some(3),
1295            "numbered against the whole stream"
1296        );
1297    }
1298
1299    /// Two handles on one stream, one invariant: each decides inside its own
1300    /// transaction, so the second sees what the first wrote and exactly one
1301    /// of them may write.
1302    #[tokio::test]
1303    async fn append_if_across_two_handles_decides_on_the_other_handles_write() {
1304        let logs = Logs::new();
1305        let log = logs.memory().await.expect("the log");
1306        let stream = uuid::Uuid::new_v4().to_string();
1307        let mut a = SqliteEventStore::on(Arc::clone(&log), stream.clone());
1308        let mut b = SqliteEventStore::on(Arc::clone(&log), stream);
1309
1310        // "Write the claim, but only if nobody has."
1311        fn claim() -> Decision {
1312            decide(|events: Vec<Value>| {
1313                if events.is_empty() {
1314                    Some(obj(json!({ "kind": "claim" })))
1315                } else {
1316                    None
1317                }
1318            })
1319        }
1320        assert!(a
1321            .append_if(Some(&["claim"]), claim())
1322            .await
1323            .expect("a")
1324            .is_some());
1325        assert!(
1326            b.append_if(Some(&["claim"]), claim())
1327                .await
1328                .expect("b")
1329                .is_none(),
1330            "the second handle decided against what the first wrote"
1331        );
1332        assert_eq!(a.len().await.expect("len"), 1);
1333    }
1334
1335    #[tokio::test]
1336    async fn read_pages_by_from_seq_and_limit() {
1337        let (mut store, _logs) = mem_store().await;
1338        for i in 1..=5 {
1339            store.append(ev(i)).await.expect("append");
1340        }
1341        let page = store.read(2, 2).await.expect("read");
1342        assert_eq!(page.iter().map(seq_of).collect::<Vec<_>>(), [2, 3]);
1343        let rest = store.read(4, usize::MAX).await.expect("read");
1344        assert_eq!(rest.iter().map(seq_of).collect::<Vec<_>>(), [4, 5]);
1345        assert!(store.read(6, 10).await.expect("read").is_empty());
1346    }
1347
1348    /// The last `n` come back in `seq` order.
1349    #[tokio::test]
1350    async fn read_last_takes_the_end_of_the_stream_in_seq_order() {
1351        let (mut store, _logs) = mem_store().await;
1352        for i in 1..=5 {
1353            store.append(ev(i)).await.expect("append");
1354        }
1355        let tail = store.read_last(2).await.expect("read_last");
1356        assert_eq!(tail.iter().map(seq_of).collect::<Vec<_>>(), [4, 5]);
1357        assert!(store.read_last(0).await.expect("read_last").is_empty());
1358        assert_eq!(store.read_last(50).await.expect("read_last").len(), 5);
1359    }
1360
1361    #[tokio::test]
1362    async fn head_is_none_when_empty_then_tracks_the_max() {
1363        let (mut store, _logs) = mem_store().await;
1364        assert_eq!(store.head().await.expect("head"), None);
1365        store.append(ev(1)).await.expect("append");
1366        assert_eq!(store.head().await.expect("head"), Some(1));
1367        store.append(ev(2)).await.expect("append");
1368        assert_eq!(store.head().await.expect("head"), Some(2));
1369    }
1370
1371    /// A read rebuilds the object that was written: the envelope out of its
1372    /// columns, `meta` and `data` out of theirs, and the beat inside the
1373    /// `meta` it was written in.
1374    #[tokio::test]
1375    async fn read_reconstructs_the_written_event_out_of_its_columns() {
1376        let (mut store, _logs) = mem_store().await;
1377        let committed = store
1378            .append(obj(json!({
1379                "kind": "llm_response",
1380                "meta": { "beat": "b-1", "attempt": 2, "final": true },
1381                "data": { "content": { "text": "hi" }, "usage": { "input_tokens": 3 } },
1382            })))
1383            .await
1384            .expect("append");
1385
1386        let stored = store.read(0, usize::MAX).await.expect("read");
1387        let event = &stored[0];
1388        assert_eq!(kind_of(event), "llm_response");
1389        assert_eq!(seq_of(event), committed.seq);
1390        assert_eq!(
1391            event.get(FIELD_META),
1392            Some(&json!({ "beat": "b-1", "attempt": 2, "final": true }))
1393        );
1394        assert_eq!(
1395            event.get(FIELD_DATA),
1396            Some(&json!({ "content": { "text": "hi" }, "usage": { "input_tokens": 3 } }))
1397        );
1398        assert_eq!(
1399            event.get("_schema_version").and_then(Value::as_u64),
1400            Some(CURRENT_SCHEMA_VERSION)
1401        );
1402    }
1403
1404    /// The beat is a label of `meta` and a read reaches it there — and the
1405    /// log carries an index on exactly that expression, so a by-beat read is
1406    /// a range rather than a scan.
1407    #[tokio::test]
1408    async fn the_beat_is_a_meta_label_with_an_index() {
1409        let (mut store, _logs) = mem_store().await;
1410        store
1411            .append(obj(json!({ "kind": "e1", "meta": { "beat": "b-1" } })))
1412            .await
1413            .expect("append");
1414        store.append(ev(2)).await.expect("append");
1415
1416        let rows = ask(
1417            &store,
1418            "SELECT json_extract(meta, '$.beat') AS beat FROM events \
1419              WHERE stream = $stream ORDER BY seq",
1420        )
1421        .await
1422        .expect("query");
1423        assert_eq!(rows.rows[0].get("beat"), Some(&json!("b-1")));
1424        assert!(
1425            !rows.rows[1].contains_key("beat"),
1426            "an undeclared beat reads as nil: {:?}",
1427            rows.rows[1]
1428        );
1429
1430        let indexes = ask(
1431            &store,
1432            "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'events'",
1433        )
1434        .await
1435        .expect("query");
1436        let names: Vec<&str> = indexes
1437            .rows
1438            .iter()
1439            .filter_map(|row| row.get("name").and_then(Value::as_str))
1440            .collect();
1441        assert!(
1442            names.contains(&"events_meta_beat"),
1443            "the beat label is indexed: {names:?}"
1444        );
1445        assert!(
1446            !names.contains(&"events_stream_beat_seq"),
1447            "and the column's old index is gone: {names:?}"
1448        );
1449    }
1450
1451    #[tokio::test]
1452    async fn events_persist_across_a_reopen_of_the_same_path_and_stream() {
1453        let dir = tempfile::tempdir().expect("tempdir");
1454        let path = dir.path().join("knl.db");
1455        let stream = "s-1";
1456
1457        {
1458            let logs = Logs::new();
1459            let mut store = SqliteEventStore::open(&path, stream, &logs)
1460                .await
1461                .expect("open");
1462            store.append(ev(1)).await.expect("append");
1463            store.append(ev(2)).await.expect("append");
1464            assert!(logs.shutdown().await.is_empty(), "the log closed cleanly");
1465        }
1466
1467        let logs = Logs::new();
1468        let reopened = SqliteEventStore::open(&path, stream, &logs)
1469            .await
1470            .expect("reopen");
1471        let stored = reopened.read(0, usize::MAX).await.expect("read");
1472        assert_eq!(stored.iter().map(kind_of).collect::<Vec<_>>(), ["e1", "e2"]);
1473        assert_eq!(reopened.head().await.expect("head"), Some(2));
1474    }
1475
1476    #[tokio::test]
1477    async fn two_streams_in_one_db_file_do_not_see_each_others_events() {
1478        let logs = Logs::new();
1479        let dir = tempfile::tempdir().expect("tempdir");
1480        let path = dir.path().join("knl.db");
1481
1482        let mut a = SqliteEventStore::open(&path, "s-a", &logs)
1483            .await
1484            .expect("open a");
1485        let mut b = SqliteEventStore::open(&path, "s-b", &logs)
1486            .await
1487            .expect("open b");
1488        a.append(ev(1)).await.expect("a");
1489        b.append(ev(2)).await.expect("b");
1490
1491        assert_eq!(
1492            a.read(0, usize::MAX)
1493                .await
1494                .expect("read a")
1495                .iter()
1496                .map(kind_of)
1497                .collect::<Vec<_>>(),
1498            ["e1"]
1499        );
1500        assert_eq!(
1501            b.read(0, usize::MAX)
1502                .await
1503                .expect("read b")
1504                .iter()
1505                .map(kind_of)
1506                .collect::<Vec<_>>(),
1507            ["e2"]
1508        );
1509        assert_eq!(a.head().await.expect("head"), Some(1));
1510        assert_eq!(b.head().await.expect("head"), Some(1));
1511    }
1512
1513    /// A row whose stored objects will not decode is corruption: a read
1514    /// surfaces it as an error rather than silently dropping the row (which
1515    /// would let a resume re-fold a truncated log into the wrong state).
1516    ///
1517    /// The bad row is written from outside the store, which is the only place
1518    /// it can come from: the hatch's authorizer refuses a raw `INSERT` into
1519    /// `events`, and the schema's own trigger refuses an `UPDATE` to every
1520    /// connection there is.
1521    #[tokio::test]
1522    async fn read_errors_on_a_corrupt_row_instead_of_dropping_it() {
1523        let dir = tempfile::tempdir().expect("tempdir");
1524        let path = dir.path().join("knl.db");
1525
1526        {
1527            let logs = Logs::new();
1528            let mut store = SqliteEventStore::open(&path, "s-1", &logs)
1529                .await
1530                .expect("open");
1531            store.append(ev(1)).await.expect("append");
1532            assert!(logs.shutdown().await.is_empty(), "the log closed cleanly");
1533        }
1534
1535        let conn = rusqlite::Connection::open(&path).expect("open the file");
1536        conn.execute(
1537            "INSERT INTO events (stream, seq, epoch_ms, kind, schema_version, meta, data) \
1538             VALUES ('s-1', 2, 0, 'e2', 2, '{}', 'not json')",
1539            [],
1540        )
1541        .expect("write a bad row");
1542        drop(conn);
1543
1544        let logs = Logs::new();
1545        let store = SqliteEventStore::open(&path, "s-1", &logs)
1546            .await
1547            .expect("reopen");
1548        let err = store
1549            .read(0, usize::MAX)
1550            .await
1551            .expect_err("a row that will not decode must surface");
1552        assert_eq!(err.kind(), KnlError::CORRUPTION, "{err}");
1553    }
1554
1555    /// The backend's error language is translated in exactly one place, and
1556    /// the split is the one a caller can act on.
1557    #[test]
1558    fn every_store_error_has_a_kernel_class() {
1559        use eventsdb_core::Error as Failure;
1560        let cases = [
1561            (Failure::Validation("v".into()), KnlError::VALIDATION),
1562            (Failure::Busy("b".into()), KnlError::BUSY),
1563            (Failure::Timeout("t".into()), KnlError::TIMEOUT),
1564            (Failure::Storage("s".into()), KnlError::STORAGE),
1565            (Failure::Corruption("c".into()), KnlError::CORRUPTION),
1566            (Failure::Unsupported("u".into()), KnlError::UNSUPPORTED),
1567            // Not a call this kernel makes, so it is the store failing to do
1568            // the work — and it has to land somewhere, since the enum is
1569            // `#[non_exhaustive]`.
1570            (
1571                Failure::Truncated {
1572                    requested: 1,
1573                    removed_up_to: 2,
1574                },
1575                KnlError::STORAGE,
1576            ),
1577        ];
1578        for (failure, expected) in cases {
1579            let translated = KnlError::from(failure);
1580            assert_eq!(translated.kind(), expected, "{translated}");
1581        }
1582        assert!(
1583            !KnlError::from(Failure::Timeout("t".into())).is_retryable(),
1584            "a deadline is not contention"
1585        );
1586        assert!(KnlError::from(Failure::Busy("b".into())).is_retryable());
1587    }
1588
1589    /// A contended lock is what a real contended write surfaces as: a second
1590    /// connection holds the write lock, so the retries are exhausted and the
1591    /// error the caller gets says "ask again".
1592    ///
1593    /// The log is opened by hand with a short busy timeout, because the point
1594    /// is the *class* of the failure and the default timeout would spend five
1595    /// seconds per attempt reaching it.
1596    #[tokio::test]
1597    async fn a_write_that_stays_contended_surfaces_as_busy() {
1598        let dir = tempfile::tempdir().expect("tempdir");
1599        let path = dir.path().join("knl.db");
1600        let log = SqliteEventLog::open_with(
1601            &path,
1602            eventsdb_sqlite::OpenOptions::default()
1603                .busy_timeout(std::time::Duration::from_millis(50))
1604                .upcasters(crate::knl::kernel_upcasters()),
1605        )
1606        .await
1607        .expect("open");
1608        let mut store = SqliteEventStore::on(Arc::new(log), "s-1");
1609        store.append(ev(1)).await.expect("the first append");
1610
1611        // A second connection takes the write lock and keeps it.
1612        let blocker = rusqlite::Connection::open(&path).expect("open the file");
1613        blocker
1614            .execute_batch("BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS held (x)")
1615            .expect("hold the lock");
1616
1617        let err = store
1618            .append(ev(2))
1619            .await
1620            .expect_err("a write that stays contended must surface");
1621        assert_eq!(err.kind(), KnlError::BUSY, "{err}");
1622        assert!(err.is_retryable(), "busy is the class that says ask again");
1623
1624        blocker.execute_batch("ROLLBACK").expect("release");
1625        store.append(ev(3)).await.expect("the lock is free again");
1626    }
1627
1628    /// Two handles on one stream both write: an append records a fact, so it
1629    /// is serialized and assigned the next seq rather than refused for the
1630    /// head one of them last saw.
1631    #[tokio::test]
1632    async fn two_handles_on_one_stream_both_append_in_arrival_order() {
1633        let logs = Logs::new();
1634        let dir = tempfile::tempdir().expect("tempdir");
1635        let path = dir.path().join("knl.db");
1636        let mut a = SqliteEventStore::open(&path, "s-1", &logs)
1637            .await
1638            .expect("open a");
1639        let mut b = SqliteEventStore::open(&path, "s-1", &logs)
1640            .await
1641            .expect("open b");
1642
1643        assert_eq!(a.append(ev(1)).await.expect("a").seq, 1);
1644        assert_eq!(b.append(ev(2)).await.expect("b").seq, 2);
1645        assert_eq!(a.append(ev(3)).await.expect("a").seq, 3);
1646
1647        let stored = b.read(0, usize::MAX).await.expect("read");
1648        assert_eq!(
1649            stored.iter().map(kind_of).collect::<Vec<_>>(),
1650            ["e1", "e2", "e3"]
1651        );
1652    }
1653
1654    // -- the read side ------------------------------------------------------
1655
1656    /// Ask `store` for `sql` with everything default.
1657    async fn ask(store: &SqliteEventStore, sql: &str) -> KnlResult<QueryRows> {
1658        ask_with(store, sql, QueryParams::None, &QueryOpts::default()).await
1659    }
1660
1661    /// Ask `store` for `sql`, saying how.
1662    async fn ask_with(
1663        store: &SqliteEventStore,
1664        sql: &str,
1665        params: QueryParams,
1666        opts: &QueryOpts,
1667    ) -> KnlResult<QueryRows> {
1668        let plan = query::plan(sql, params, opts, &store.stream)?;
1669        store.query(&plan).await
1670    }
1671
1672    /// The `kind` column of every row, in order.
1673    fn kinds_of(rows: &QueryRows) -> Vec<&str> {
1674        rows.rows
1675            .iter()
1676            .filter_map(|row| row.get("kind").and_then(Value::as_str))
1677            .collect()
1678    }
1679
1680    /// A query reads what the writer wrote — on the in-memory log as much as
1681    /// on a file.
1682    #[tokio::test]
1683    async fn a_query_reads_what_the_writer_wrote() {
1684        let (mut store, _logs) = mem_store().await;
1685        store.append(ev(1)).await.expect("append");
1686        store.append(ev(2)).await.expect("append");
1687
1688        let rows = ask(
1689            &store,
1690            "SELECT kind, seq FROM events WHERE stream = $stream ORDER BY seq",
1691        )
1692        .await
1693        .expect("query");
1694        assert_eq!(kinds_of(&rows), ["e1", "e2"]);
1695        assert!(!rows.truncated);
1696        assert_eq!(rows.rows[0].get("seq"), Some(&json!(1)));
1697    }
1698
1699    /// `$stream` is this store's own stream and nothing else: a second stream
1700    /// in the same database is not selected by it.
1701    #[tokio::test]
1702    async fn stream_binds_to_this_stores_own_stream() {
1703        let logs = Logs::new();
1704        let log = logs.memory().await.expect("the log");
1705        let mut mine = SqliteEventStore::on(Arc::clone(&log), "s-mine");
1706        let mut theirs = SqliteEventStore::on(Arc::clone(&log), "s-theirs");
1707        mine.append(ev(1)).await.expect("mine");
1708        theirs.append(ev(2)).await.expect("theirs");
1709
1710        let rows = ask(&mine, "SELECT kind FROM events WHERE stream = $stream")
1711            .await
1712            .expect("query");
1713        assert_eq!(kinds_of(&rows), ["e1"]);
1714    }
1715
1716    /// `$sessions` reads across a set: two streams in one database, one
1717    /// statement, and the ids are bound rather than pasted in.
1718    #[tokio::test]
1719    async fn sessions_reads_across_the_set_it_was_given() {
1720        let logs = Logs::new();
1721        let log = logs.memory().await.expect("the log");
1722        let mut one = SqliteEventStore::on(Arc::clone(&log), "s-one");
1723        let mut two = SqliteEventStore::on(Arc::clone(&log), "s-two");
1724        let mut three = SqliteEventStore::on(Arc::clone(&log), "s-three");
1725        one.append(ev(1)).await.expect("one");
1726        two.append(ev(2)).await.expect("two");
1727        three.append(ev(3)).await.expect("three");
1728
1729        let opts = QueryOpts {
1730            sessions: Some(vec!["s-one".to_string(), "s-two".to_string()]),
1731            ..QueryOpts::default()
1732        };
1733        let rows = ask_with(
1734            &one,
1735            "SELECT kind FROM events WHERE stream IN $sessions ORDER BY position",
1736            QueryParams::None,
1737            &opts,
1738        )
1739        .await
1740        .expect("query");
1741        assert_eq!(kinds_of(&rows), ["e1", "e2"]);
1742    }
1743
1744    /// A value is bound, never pasted: a quote inside it is a character in a
1745    /// string, not the end of one.
1746    #[tokio::test]
1747    async fn a_bound_value_with_a_quote_in_it_is_a_value() {
1748        let (mut store, _logs) = mem_store().await;
1749        store
1750            .append(obj(json!({ "kind": "it's fine" })))
1751            .await
1752            .expect("append");
1753
1754        let rows = ask_with(
1755            &store,
1756            "SELECT kind FROM events WHERE stream = $stream AND kind = :kind",
1757            QueryParams::Named(obj(json!({ "kind": "it's fine" }))),
1758            &QueryOpts::default(),
1759        )
1760        .await
1761        .expect("query");
1762        assert_eq!(kinds_of(&rows), ["it's fine"]);
1763    }
1764
1765    /// The cap is reported, not silently applied — and a result that happens
1766    /// to be exactly `limit` long is not called truncated.
1767    #[tokio::test]
1768    async fn the_row_cap_is_reported_when_it_cuts() {
1769        let (mut store, _logs) = mem_store().await;
1770        for i in 1..=5 {
1771            store.append(ev(i)).await.expect("append");
1772        }
1773
1774        let capped = QueryOpts {
1775            limit: 2,
1776            ..QueryOpts::default()
1777        };
1778        let rows = ask_with(
1779            &store,
1780            "SELECT kind FROM events WHERE stream = $stream ORDER BY seq",
1781            QueryParams::None,
1782            &capped,
1783        )
1784        .await
1785        .expect("query");
1786        assert_eq!(kinds_of(&rows), ["e1", "e2"]);
1787        assert!(rows.truncated, "the answer was cut");
1788
1789        let exact = QueryOpts {
1790            limit: 5,
1791            ..QueryOpts::default()
1792        };
1793        let rows = ask_with(
1794            &store,
1795            "SELECT kind FROM events WHERE stream = $stream ORDER BY seq",
1796            QueryParams::None,
1797            &exact,
1798        )
1799        .await
1800        .expect("query");
1801        assert_eq!(rows.rows.len(), 5);
1802        assert!(!rows.truncated, "nothing was cut off");
1803    }
1804
1805    /// A query that will not finish is cut short, and says so in its own
1806    /// class: nothing was contended, so "ask again" would be the wrong advice.
1807    #[tokio::test]
1808    async fn a_query_that_runs_too_long_is_a_timeout() {
1809        let (store, _logs) = mem_store().await;
1810        let hurried = QueryOpts {
1811            timeout_ms: 50,
1812            ..QueryOpts::default()
1813        };
1814        let err = ask_with(
1815            &store,
1816            // Unbounded on purpose: it ends when the deadline ends it.
1817            "WITH RECURSIVE forever(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM forever) \
1818             SELECT COUNT(*) FROM forever",
1819            QueryParams::None,
1820            &hurried,
1821        )
1822        .await
1823        .expect_err("an endless query must be cut short");
1824        assert_eq!(err.kind(), KnlError::TIMEOUT, "{err}");
1825        assert!(!err.is_retryable(), "a slow query is not a retry: {err}");
1826
1827        // The connection is usable afterwards: the interrupt ended a
1828        // statement, not the reader.
1829        assert!(ask(&store, "SELECT 1 AS one").await.is_ok());
1830    }
1831
1832    /// A statement that is not a read never reaches the connection, and a
1833    /// second statement is refused whole.  (The rules are
1834    /// [`super::super::query`]'s; this is the path through the store.)
1835    #[tokio::test]
1836    async fn a_write_or_a_second_statement_is_refused_before_the_connection() {
1837        let (store, _logs) = mem_store().await;
1838        for sql in [
1839            "INSERT INTO events (stream) VALUES ('x')",
1840            "UPDATE events SET kind = 'x'",
1841            "PRAGMA table_info(events)",
1842            "ATTACH DATABASE '/tmp/other.db' AS other",
1843            "SELECT 1; DROP TABLE events",
1844        ] {
1845            let err = ask(&store, sql).await.expect_err("must be refused");
1846            assert_eq!(err.kind(), KnlError::VALIDATION, "{sql:?}: {err}");
1847        }
1848    }
1849
1850    /// A statement that does not compile is the caller's mistake, and says so
1851    /// in the class a reader branches on.
1852    ///
1853    /// The store underneath has no statement class and files these with the
1854    /// disk faults ([`SQLITE_STATEMENT_ERRORS`]); the query path puts them
1855    /// back. One statement per phrase, run against a real log, so the list is
1856    /// held to what SQLite actually says rather than to what it said once.
1857    #[tokio::test]
1858    async fn a_statement_that_does_not_compile_is_the_callers() {
1859        let (store, _logs) = mem_store().await;
1860        for (sql, phrase) in [
1861            // The one the change is about: `beat` stopped being a column.
1862            ("SELECT beat FROM events", "no such column"),
1863            ("SELECT * FROM chronicle", "no such table"),
1864            ("SELECT nonesuch(1) AS x", "no such function"),
1865            ("SELECT 1 + FROM events", "syntax error"),
1866            ("SELECT * FROM events ORDER seq", "near \""),
1867            ("SELECT abs(1, 2) AS x", "wrong number of arguments"),
1868            (
1869                "SELECT seq FROM events AS a, events AS b",
1870                "ambiguous column name",
1871            ),
1872        ] {
1873            let err = ask(&store, sql).await.expect_err("must not compile");
1874            assert_eq!(err.kind(), KnlError::VALIDATION, "{sql:?}: {err}");
1875            assert!(
1876                err.reason().contains(phrase),
1877                "{sql:?}: expected SQLite to say {phrase:?}, got {err}"
1878            );
1879            assert!(
1880                err.reason().starts_with("sql: "),
1881                "{sql:?}: the reason names which half was wrong: {err}"
1882            );
1883        }
1884    }
1885
1886    /// And a fault that really is the store's stays `storage`: the phrases are
1887    /// a filter for the caller's mistakes, not a reclassification of the
1888    /// failures underneath.
1889    #[tokio::test]
1890    async fn a_real_store_fault_is_still_storage() {
1891        let (store, _logs) = mem_store().await;
1892        // Compiles, then fails at run time against SQLite's own length limit
1893        // — the store failing to produce the row, which is what `storage`
1894        // says. (Nothing is allocated: the limit is checked first.)
1895        let err = ask(&store, "SELECT zeroblob(1000000001) AS huge")
1896            .await
1897            .expect_err("over SQLITE_MAX_LENGTH");
1898        assert_eq!(err.kind(), KnlError::STORAGE, "{err}");
1899    }
1900
1901    /// Every SQLite type comes back as itself, and a NULL comes back as an
1902    /// absent column rather than a present nothing.
1903    #[tokio::test]
1904    async fn the_sqlite_types_map_onto_values_and_null_is_absence() {
1905        let (store, _logs) = mem_store().await;
1906        let rows = ask(
1907            &store,
1908            // `absent`, not `nothing`: NOTHING is a SQLite keyword.
1909            "SELECT 1 AS whole, 1.5 AS fraction, 'text' AS words, NULL AS absent",
1910        )
1911        .await
1912        .expect("query");
1913        let row = &rows.rows[0];
1914        assert_eq!(row["whole"], Value::from(1));
1915        assert_eq!(row["fraction"], Value::from(1.5));
1916        assert_eq!(row["words"], Value::from("text"));
1917        assert!(
1918            !row.contains_key("absent"),
1919            "a NULL column is absent, so it reads as nil: {row:?}"
1920        );
1921    }
1922
1923    /// A cell JSON has no value for is refused, by name.
1924    ///
1925    /// The three are a `BLOB`, a non-finite `REAL` and `TEXT` that is not
1926    /// UTF-8.  A substitute — the string `"<blob>"`, or the absent key a null
1927    /// would have become — is a value the caller cannot tell from one that was
1928    /// really there, which is the whole reason this is an error and not a
1929    /// reading.  The refusal names the column and the SQL that gets the value
1930    /// through, so the answer is one edit to the statement away.
1931    #[tokio::test]
1932    async fn a_cell_with_no_json_value_is_refused_and_the_refusal_names_it() {
1933        let (store, _logs) = mem_store().await;
1934        for (sql, column, advice) in [
1935            ("SELECT CAST('bytes' AS BLOB) AS raw", "raw", "hex(raw)"),
1936            // A literal SQLite keeps as `real` infinity. NaN is not one of
1937            // these: SQLite stores it as NULL, which is absence and reads as
1938            // nil.
1939            ("SELECT 9e999 AS boundless", "boundless", "CAST(boundless"),
1940            ("SELECT CAST(x'ff' AS TEXT) AS garbled", "garbled", "UTF-8"),
1941        ] {
1942            let err = ask(&store, sql).await.expect_err("must be refused");
1943            assert_eq!(err.kind(), KnlError::UNSUPPORTED, "{sql:?}: {err}");
1944            assert!(
1945                err.reason().contains(column),
1946                "{sql:?}: the refusal names the column: {err}"
1947            );
1948            assert!(
1949                err.reason().contains(advice),
1950                "{sql:?}: expected {advice:?} in the refusal: {err}"
1951            );
1952        }
1953    }
1954
1955    /// The published schema is the table: the constant `knl.api()` hands out,
1956    /// held against the columns SQLite actually reports.
1957    #[tokio::test]
1958    async fn the_published_schema_is_the_events_table() {
1959        let columns = events_schema().expect("schema");
1960        let names: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();
1961        assert_eq!(
1962            names,
1963            [
1964                "position",
1965                "stream",
1966                "seq",
1967                "epoch_ms",
1968                "kind",
1969                "schema_version",
1970                "meta",
1971                "data"
1972            ]
1973        );
1974
1975        let pk: Vec<&str> = columns
1976            .iter()
1977            .filter(|c| c.pk)
1978            .map(|c| c.name.as_str())
1979            .collect();
1980        assert_eq!(pk, ["position"], "the log is keyed by its global order");
1981
1982        // And a query may name every one of them.
1983        let (store, _logs) = mem_store().await;
1984        let sql = format!("SELECT {} FROM {EVENTS_TABLE}", names.join(", "));
1985        ask(&store, &sql)
1986            .await
1987            .expect("the published columns are the real ones");
1988    }
1989
1990    /// The published schema is also the *live* one.
1991    ///
1992    /// This is what keeps [`events_schema`] a reading of the table rather than
1993    /// a claim about it: a real log is opened and asked what its `events`
1994    /// table has, through the store's own hatch.  `PRAGMA table_info` is one
1995    /// of the introspection pragmas the hatch allows — *setting* a pragma is
1996    /// what it refuses, since that is how the migration ladder's marker or the
1997    /// journal mode would be changed underneath it — so the reading needs no
1998    /// connection of its own.  [`events_schema`] stays a constant: `knl.api()`
1999    /// is synchronous, and this is what keeps the constant honest.
2000    #[tokio::test]
2001    async fn the_published_schema_is_the_live_one() {
2002        let dir = tempfile::tempdir().expect("tempdir");
2003        let path = dir.path().join("knl.db");
2004        let logs = Logs::new();
2005        let log = logs.file(&path).await.expect("open");
2006
2007        let rows = log
2008            .query(&format!("PRAGMA table_info({EVENTS_TABLE})"), vec![])
2009            .await
2010            .expect("table_info");
2011        let live: Vec<SchemaColumn> = rows
2012            .iter()
2013            .map(|row| SchemaColumn {
2014                name: row["name"].as_str().expect("a column name").to_string(),
2015                declared_type: row["type"].as_str().expect("a declared type").to_string(),
2016                pk: row["pk"].as_i64().expect("a pk flag") > 0,
2017            })
2018            .collect();
2019
2020        assert_eq!(live, events_schema().expect("published schema"));
2021        drop(log);
2022        assert!(logs.shutdown().await.is_empty(), "the log closed cleanly");
2023    }
2024}