Skip to main content

khive_db/
sql_bridge.rs

1//! SqlAccess bridge: connects `ConnectionPool` to `khive_storage::SqlAccess`.
2//!
3//! Two modes:
4//! - **File-backed**: ordinary reads check out pooled readers per operation.
5//!   The only standalone-reader exception is an explicitly admitted multi-call
6//!   deferred read transaction; standalone writer handles remain capped at one.
7//!   Cross-statement write atomicity goes through `atomic_unit`, which drives a
8//!   single registered raw transaction span rather than a caller-held per-tx
9//!   connection.
10//! - **Memory**: Uses pool-backed approach (acquire pool connection per-query inside `spawn_blocking`).
11
12use std::any::Any;
13use std::sync::Arc;
14use std::time::Instant;
15
16use async_trait::async_trait;
17
18use khive_storage::error::StorageError;
19use khive_storage::types::{PageRequest, SqlColumn, SqlRow, SqlStatement, SqlValue};
20use khive_storage::{AtomicUnitOp, StorageCapability, TopLevelMaintenance};
21use tokio::sync::{OwnedSemaphorePermit, Semaphore};
22
23use crate::error::SqliteError;
24use crate::pool::{ConnectionPool, SharedReaderTransactionGuard, StandaloneReaderPurpose};
25
26// =============================================================================
27// Shared helpers
28// =============================================================================
29
30/// Convert a rusqlite `Row` into an owned `SqlRow`.
31fn row_to_sql_row(row: &rusqlite::Row<'_>, col_count: usize, col_names: &[String]) -> SqlRow {
32    #[cfg(test)]
33    ROW_CONVERSIONS.with(|count| count.set(count.get() + 1));
34
35    let mut columns = Vec::with_capacity(col_count);
36    for i in 0..col_count {
37        let value = match row.get_ref(i) {
38            Ok(rusqlite::types::ValueRef::Null) => SqlValue::Null,
39            Ok(rusqlite::types::ValueRef::Integer(v)) => SqlValue::Integer(v),
40            Ok(rusqlite::types::ValueRef::Real(v)) => SqlValue::Float(v),
41            Ok(rusqlite::types::ValueRef::Text(bytes)) => {
42                SqlValue::Text(String::from_utf8_lossy(bytes).into_owned())
43            }
44            Ok(rusqlite::types::ValueRef::Blob(bytes)) => SqlValue::Blob(bytes.to_vec()),
45            Err(_) => SqlValue::Null,
46        };
47        columns.push(SqlColumn {
48            name: col_names.get(i).cloned().unwrap_or_default(),
49            value,
50        });
51    }
52    SqlRow { columns }
53}
54
55#[cfg(test)]
56thread_local! {
57    static ROW_CONVERSIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
58}
59
60/// Bind `SqlValue` parameters to a rusqlite statement.
61///
62/// `pub(crate)` (ADR-099 B3 r6 structural cut): reused by the pure
63/// `*_statement` builders in `stores::{entity,note,graph,text,vectors}` so
64/// that every store's async execution path and the ADR-099 `--atomic`
65/// prepare path bind params identically — one implementation, not two.
66pub(crate) fn bind_params(
67    stmt: &mut rusqlite::Statement<'_>,
68    params: &[SqlValue],
69) -> Result<(), rusqlite::Error> {
70    for (i, param) in params.iter().enumerate() {
71        let idx = i + 1; // rusqlite uses 1-based indexing
72        match param {
73            SqlValue::Null => stmt.raw_bind_parameter(idx, rusqlite::types::Null)?,
74            SqlValue::Bool(v) => stmt.raw_bind_parameter(idx, *v as i64)?,
75            SqlValue::Integer(v) => stmt.raw_bind_parameter(idx, *v)?,
76            SqlValue::Float(v) => stmt.raw_bind_parameter(idx, *v)?,
77            SqlValue::Text(v) => stmt.raw_bind_parameter(idx, v.as_str())?,
78            SqlValue::Blob(v) => stmt.raw_bind_parameter(idx, v.as_slice())?,
79            SqlValue::Json(v) => {
80                let s = serde_json::to_string(v).unwrap_or_default();
81                stmt.raw_bind_parameter(idx, s.as_str())?;
82            }
83            SqlValue::Uuid(v) => stmt.raw_bind_parameter(idx, v.to_string().as_str())?,
84            SqlValue::Timestamp(v) => {
85                stmt.raw_bind_parameter(idx, v.timestamp_micros())?;
86            }
87        }
88    }
89    Ok(())
90}
91
92/// Prepare exactly one [`SqlStatement`] SQL string.
93///
94/// rusqlite's `Connection::prepare` checks SQLite's returned tail and returns
95/// `rusqlite::Error::MultipleStatement` when the tail contains another
96/// executable statement (tail comments remain valid). Keeping this wrapper at
97/// the bridge boundary makes the single-statement `SqlStatement` contract
98/// explicit for queries and writes alike.
99fn prepare_sql_statement<'conn>(
100    conn: &'conn rusqlite::Connection,
101    sql: &str,
102) -> Result<rusqlite::Statement<'conn>, rusqlite::Error> {
103    conn.prepare(sql)
104}
105
106/// Prepare one [`SqlStatement`] through rusqlite's per-connection LRU cache
107/// while retaining the same single-statement tail validation as
108/// [`prepare_sql_statement`].
109fn prepare_cached_sql_statement<'conn>(
110    conn: &'conn rusqlite::Connection,
111    sql: &str,
112) -> Result<rusqlite::CachedStatement<'conn>, rusqlite::Error> {
113    conn.prepare_cached(sql)
114}
115
116/// A batch statement prepared once before execution. Ordinary SQL errors are
117/// retried in the execution phase so they still exercise the owning
118/// transaction's rollback path and can observe schema changes made by an
119/// earlier statement in the same batch; only `MultipleStatement` aborts
120/// preflight.
121enum PreparedBatchStatement<'conn> {
122    Ready(rusqlite::Statement<'conn>),
123    PrepareAtExecution,
124}
125
126/// Prepare each batch statement exactly once while rejecting an executable
127/// tail before any statement runs.
128fn prepare_batch_statements<'conn>(
129    conn: &'conn rusqlite::Connection,
130    statements: &[SqlStatement],
131) -> Result<Vec<PreparedBatchStatement<'conn>>, rusqlite::Error> {
132    let mut prepared = Vec::with_capacity(statements.len());
133    for statement in statements {
134        match prepare_sql_statement(conn, &statement.sql) {
135            Ok(statement) => prepared.push(PreparedBatchStatement::Ready(statement)),
136            Err(error @ rusqlite::Error::MultipleStatement) => return Err(error),
137            Err(_) => prepared.push(PreparedBatchStatement::PrepareAtExecution),
138        }
139    }
140    Ok(prepared)
141}
142
143/// Bind and execute handles returned by [`prepare_batch_statements`].
144fn execute_prepared_batch<'conn>(
145    conn: &'conn rusqlite::Connection,
146    prepared: Vec<PreparedBatchStatement<'conn>>,
147    statements: &[SqlStatement],
148) -> Result<u64, rusqlite::Error> {
149    debug_assert_eq!(prepared.len(), statements.len());
150    let mut total = 0u64;
151    for (prepared, statement) in prepared.into_iter().zip(statements) {
152        let mut prepared = match prepared {
153            PreparedBatchStatement::Ready(prepared) => prepared,
154            PreparedBatchStatement::PrepareAtExecution => {
155                prepare_sql_statement(conn, &statement.sql)?
156            }
157        };
158        bind_params(&mut prepared, &statement.params)?;
159        total += prepared.raw_execute()? as u64;
160    }
161    Ok(total)
162}
163
164/// SQL statement heads that are transaction control. `execute_batch` owns the
165/// `BEGIN`/`COMMIT` boundary for the whole batch (the standalone path wraps
166/// the list in its own `BEGIN IMMEDIATE`, and the queue-backed path runs
167/// inside the writer task's per-request transaction), so a caller-supplied
168/// statement that itself starts, ends, or branches a transaction can commit
169/// or roll back early and break the batch's all-or-nothing contract. Cached
170/// read-only handles use the same lexical classification to drive their
171/// separately admitted single-level read-transaction state machine below.
172/// `START` is classified as the alternate transaction-opening spelling so
173/// callers get a typed boundary error; `END` is SQLite's `COMMIT` spelling.
174const TRANSACTION_CONTROL_KEYWORDS: [&str; 7] = [
175    "BEGIN",
176    "START",
177    "COMMIT",
178    "END",
179    "ROLLBACK",
180    "SAVEPOINT",
181    "RELEASE",
182];
183
184/// Skip the same leading whitespace, UTF-8 BOMs, empty statements (`;`), and
185/// line/block comments SQLite accepts before an executable statement.
186fn skip_sqlite_empty_prefix(mut rest: &[u8]) -> &[u8] {
187    loop {
188        let mut idx = 0;
189        while idx < rest.len() && rest[idx].is_ascii_whitespace() {
190            idx += 1;
191        }
192        rest = &rest[idx..];
193        if let Some(tail) = rest.strip_prefix(b"\xEF\xBB\xBF") {
194            rest = tail;
195            continue;
196        }
197        if let Some(tail) = rest.strip_prefix(b";") {
198            rest = tail;
199            continue;
200        }
201        if let Some(tail) = rest.strip_prefix(b"--") {
202            let mut idx = 0;
203            while idx < tail.len() && tail[idx] != b'\n' {
204                idx += 1;
205            }
206            rest = if idx < tail.len() {
207                &tail[idx + 1..]
208            } else {
209                &[]
210            };
211            continue;
212        }
213        if let Some(tail) = rest.strip_prefix(b"/*") {
214            let mut idx = 0;
215            while idx + 1 < tail.len() && !(tail[idx] == b'*' && tail[idx + 1] == b'/') {
216                idx += 1;
217            }
218            rest = if idx + 1 < tail.len() {
219                &tail[idx + 2..]
220            } else {
221                &[]
222            };
223            continue;
224        }
225        break;
226    }
227    rest
228}
229
230/// Return one ASCII SQL token after SQLite whitespace/comments/BOM trivia.
231/// Empty-statement separators are deliberately not trivia here: callers use
232/// this only after the executable statement head has already been consumed.
233fn next_sqlite_token(mut rest: &[u8]) -> Option<(&[u8], &[u8])> {
234    loop {
235        let mut idx = 0;
236        while idx < rest.len() && rest[idx].is_ascii_whitespace() {
237            idx += 1;
238        }
239        rest = &rest[idx..];
240        if let Some(tail) = rest.strip_prefix(b"\xEF\xBB\xBF") {
241            rest = tail;
242            continue;
243        }
244        if let Some(tail) = rest.strip_prefix(b"--") {
245            let mut idx = 0;
246            while idx < tail.len() && tail[idx] != b'\n' {
247                idx += 1;
248            }
249            rest = if idx < tail.len() {
250                &tail[idx + 1..]
251            } else {
252                &[]
253            };
254            continue;
255        }
256        if let Some(tail) = rest.strip_prefix(b"/*") {
257            let mut idx = 0;
258            while idx + 1 < tail.len() && !(tail[idx] == b'*' && tail[idx + 1] == b'/') {
259                idx += 1;
260            }
261            rest = if idx + 1 < tail.len() {
262                &tail[idx + 2..]
263            } else {
264                &[]
265            };
266            continue;
267        }
268        break;
269    }
270
271    let len = rest
272        .iter()
273        .take_while(|byte| byte.is_ascii_alphanumeric() || **byte == b'_')
274        .count();
275    (len != 0).then_some((&rest[..len], &rest[len..]))
276}
277
278/// Return a transaction-control keyword and the bytes following it, if any.
279/// Matching is case-insensitive and requires a word boundary, so an identifier
280/// that merely starts with `begin` or `commit` never matches.
281fn transaction_control_parts(sql: &str) -> Option<(&'static str, &[u8])> {
282    let rest = skip_sqlite_empty_prefix(sql.as_bytes());
283    TRANSACTION_CONTROL_KEYWORDS
284        .iter()
285        .copied()
286        .find_map(|keyword| {
287            let kw = keyword.as_bytes();
288            if rest.len() < kw.len() || !rest[..kw.len()].eq_ignore_ascii_case(kw) {
289                return None;
290            }
291            let boundary = match rest.get(kw.len()) {
292                Some(next) => !(next.is_ascii_alphanumeric() || *next == b'_'),
293                None => true,
294            };
295            boundary.then_some((keyword, &rest[kw.len()..]))
296        })
297}
298
299/// Return the transaction-control keyword heading `sql`, if any.
300fn transaction_control_head(sql: &str) -> Option<&'static str> {
301    transaction_control_parts(sql).map(|(keyword, _)| keyword)
302}
303
304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
305enum CachedReadTransactionControl {
306    /// `BEGIN`, `BEGIN TRANSACTION`, or their explicitly `DEFERRED` form.
307    BeginDeferred,
308    /// A transaction-ending control statement and its diagnostic keyword.
309    Finish(&'static str),
310    /// Transaction control that cannot be represented by the single-level
311    /// admitted read-transaction state machine.
312    Unsupported(&'static str),
313}
314
315/// Classify transaction control for a cached read-only connection.
316///
317/// A cached reader may own exactly one top-level deferred read transaction.
318/// Immediate/exclusive starts could reserve write-side locks, while nested
319/// savepoint/rollback-to controls would require a second lifecycle level, so
320/// both remain rejected. Batch/write paths continue using
321/// [`transaction_control_head`] and reject every variant without exception.
322fn cached_read_transaction_control(sql: &str) -> Option<CachedReadTransactionControl> {
323    let (keyword, tail) = transaction_control_parts(sql)?;
324    match keyword {
325        "BEGIN" => {
326            // Accept exactly `BEGIN`, `BEGIN TRANSACTION`, `BEGIN DEFERRED`,
327            // or `BEGIN DEFERRED TRANSACTION`, with no trailing tokens.
328            // SQLite's grammar also admits `BEGIN TRANSACTION <name>` (the
329            // name parses as an identifier and is ignored), so a mode keyword
330            // in that trailing position — `BEGIN TRANSACTION IMMEDIATE` —
331            // still parses, and classifying it by its first token alone would
332            // launder what reads as a write-reserving start into a deferred
333            // one. Every trailing token is therefore Unsupported — and the
334            // check cannot stop at `next_sqlite_token` returning `None`,
335            // because that tokenizer returns `None` for any non-identifier
336            // byte, not only end-of-input: a quoted or bracketed tail
337            // (`BEGIN TRANSACTION "IMMEDIATE"`, `[IMMEDIATE]`) would fall
338            // out of the loop and read as the end of an accepted form. After
339            // the accepted keywords, the remainder must reduce to nothing
340            // under the same trivia/empty-statement skipping SQLite applies
341            // (whitespace, comments, `;`), or the statement is Unsupported.
342            let mut rest = tail;
343            let mut saw_deferred = false;
344            let mut saw_transaction = false;
345            while let Some((token, next)) = next_sqlite_token(rest) {
346                if !saw_deferred && !saw_transaction && token.eq_ignore_ascii_case(b"DEFERRED") {
347                    saw_deferred = true;
348                } else if !saw_transaction && token.eq_ignore_ascii_case(b"TRANSACTION") {
349                    saw_transaction = true;
350                } else {
351                    return Some(CachedReadTransactionControl::Unsupported(keyword));
352                }
353                rest = next;
354            }
355            if !skip_sqlite_empty_prefix(rest).is_empty() {
356                return Some(CachedReadTransactionControl::Unsupported(keyword));
357            }
358            Some(CachedReadTransactionControl::BeginDeferred)
359        }
360        "COMMIT" | "END" => Some(CachedReadTransactionControl::Finish(keyword)),
361        "ROLLBACK" => {
362            let first = next_sqlite_token(tail);
363            let rollback_target = match first {
364                Some((token, rest)) if token.eq_ignore_ascii_case(b"TRANSACTION") => {
365                    next_sqlite_token(rest).map(|(token, _)| token)
366                }
367                Some((token, _)) => Some(token),
368                None => None,
369            };
370            if rollback_target.is_some_and(|token| token.eq_ignore_ascii_case(b"TO")) {
371                Some(CachedReadTransactionControl::Unsupported(keyword))
372            } else {
373                Some(CachedReadTransactionControl::Finish(keyword))
374            }
375        }
376        _ => Some(CachedReadTransactionControl::Unsupported(keyword)),
377    }
378}
379
380/// Reject transaction-control statements in `statements` with a typed
381/// [`StorageError::InvalidInput`] BEFORE anything executes, preserving the
382/// batch's all-or-nothing contract (see [`TRANSACTION_CONTROL_KEYWORDS`]).
383fn reject_transaction_control_statements(
384    statements: &[SqlStatement],
385    operation: &'static str,
386) -> khive_storage::types::StorageResult<()> {
387    for (index, statement) in statements.iter().enumerate() {
388        if let Some(keyword) = transaction_control_head(&statement.sql) {
389            return Err(StorageError::InvalidInput {
390                capability: StorageCapability::Sql,
391                operation: operation.into(),
392                message: format!(
393                    "statement at index {index} is transaction control ({keyword}); \
394                     execute_batch owns the BEGIN/COMMIT boundary for the whole \
395                     batch — remove transaction-control statements from the batch"
396                ),
397            });
398        }
399    }
400    Ok(())
401}
402
403/// One standalone-`execute_batch` failure, paired with the reason the handle
404/// was poisoned (dropped instead of restored), if it was.
405struct BatchFailure {
406    error: rusqlite::Error,
407    poison_reason: Option<BatchPoisonReason>,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411enum BatchHandleDisposition {
412    Retain,
413    Poison,
414}
415
416/// Execute one standalone batch while every prepared statement remains scoped
417/// to the borrowed connection. The owned [`StandaloneHandle`] stays outside
418/// this helper, so it can be restored or dropped only after all statement
419/// borrows have ended.
420fn execute_standalone_batch(
421    conn: &rusqlite::Connection,
422    statements: &[SqlStatement],
423    origin: khive_storage::tx_registry::TxOrigin,
424) -> (BatchHandleDisposition, Result<u64, BatchFailure>) {
425    let prepared = match prepare_batch_statements(conn, statements) {
426        Ok(prepared) => prepared,
427        Err(error) => {
428            return (
429                BatchHandleDisposition::Retain,
430                Err(BatchFailure {
431                    error,
432                    poison_reason: None,
433                }),
434            );
435        }
436    };
437    if let Err(begin_error) = conn.execute_batch("BEGIN IMMEDIATE") {
438        // Busy/locked is transient contention (another writer held SQLite's
439        // write lock past `busy_timeout`); the connection itself is untouched,
440        // so the handle remains reusable. Any other failure leaves transaction
441        // state suspect and poisons the handle.
442        drop(prepared);
443        let (disposition, poison_reason) = if crate::timeout_sink::is_busy_or_locked(&begin_error) {
444            (BatchHandleDisposition::Retain, None)
445        } else {
446            tracing::warn!(
447                %begin_error,
448                "execute_batch: BEGIN IMMEDIATE failed non-transiently; \
449                 poisoning the standalone connection — the handle is \
450                 dropped and must be re-acquired"
451            );
452            (
453                BatchHandleDisposition::Poison,
454                Some(BatchPoisonReason::BeginFailed),
455            )
456        };
457        return (
458            disposition,
459            Err(BatchFailure {
460                error: begin_error,
461                poison_reason,
462            }),
463        );
464    }
465
466    // Registered only after BEGIN succeeds, and retained through COMMIT or
467    // ROLLBACK so the registry never reports a transaction as finished early.
468    let _tx_handle =
469        khive_storage::tx_registry::register_scoped(Some("execute_batch".to_string()), origin);
470    let result = (|| -> Result<u64, rusqlite::Error> {
471        let total = execute_prepared_batch(conn, prepared, statements)?;
472        conn.execute_batch("COMMIT")?;
473        Ok(total)
474    })();
475
476    let mut disposition = BatchHandleDisposition::Retain;
477    let mut poison_reason = None;
478    if let Err(error) = &result {
479        if let Err(rollback_error) = conn.execute_batch("ROLLBACK") {
480            // A failed ROLLBACK leaves the connection in an unknown
481            // transaction state. Preserve the original statement error while
482            // making the poison cause explicit to the caller.
483            tracing::warn!(
484                %error,
485                %rollback_error,
486                "execute_batch: ROLLBACK after statement failure failed; \
487                 poisoning the standalone connection — the handle is \
488                 dropped and must be re-acquired"
489            );
490            disposition = BatchHandleDisposition::Poison;
491            poison_reason = Some(BatchPoisonReason::RollbackFailed(rollback_error));
492        }
493    }
494
495    (
496        disposition,
497        result.map_err(|error| BatchFailure {
498            error,
499            poison_reason,
500        }),
501    )
502}
503
504#[derive(Debug)]
505enum BatchPoisonReason {
506    BeginFailed,
507    RollbackFailed(rusqlite::Error),
508}
509
510impl std::fmt::Display for BatchPoisonReason {
511    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512        match self {
513            Self::BeginFailed => f.write_str(
514                "BEGIN IMMEDIATE failed non-transiently; connection transaction state is suspect",
515            ),
516            Self::RollbackFailed(error) => {
517                write!(f, "ROLLBACK after statement failure failed: {error}")
518            }
519        }
520    }
521}
522
523/// A `rusqlite::Error` whose display carries the poison context, so a
524/// poisoned handle is visible to the caller in the returned error instead of
525/// being discoverable only through later calls' generic "connection already
526/// consumed" failures.
527#[derive(Debug)]
528struct PoisonedBatchError {
529    original: rusqlite::Error,
530    poison_reason: BatchPoisonReason,
531}
532
533impl std::fmt::Display for PoisonedBatchError {
534    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535        write!(
536            f,
537            "{}; original error: {}",
538            self.poison_reason, self.original
539        )
540    }
541}
542
543impl std::error::Error for PoisonedBatchError {
544    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
545        Some(&self.original)
546    }
547}
548
549fn prepare_bound_statement<'conn>(
550    conn: &'conn rusqlite::Connection,
551    statement: &SqlStatement,
552) -> Result<rusqlite::Statement<'conn>, rusqlite::Error> {
553    let mut stmt = prepare_sql_statement(conn, &statement.sql)?;
554    bind_params(&mut stmt, &statement.params)?;
555    Ok(stmt)
556}
557
558fn execute_prepared_query(
559    mut stmt: rusqlite::Statement<'_>,
560) -> Result<Vec<SqlRow>, rusqlite::Error> {
561    let col_count = stmt.column_count();
562    let col_names: Vec<String> = (0..col_count)
563        .map(|i| stmt.column_name(i).unwrap_or("").to_string())
564        .collect();
565
566    let mut rows = Vec::new();
567    let mut raw_rows = stmt.raw_query();
568    while let Some(row) = raw_rows.next()? {
569        rows.push(row_to_sql_row(row, col_count, &col_names));
570    }
571    Ok(rows)
572}
573
574fn execute_prepared_query_row(
575    mut stmt: rusqlite::Statement<'_>,
576) -> Result<Option<SqlRow>, rusqlite::Error> {
577    let col_count = stmt.column_count();
578    let col_names: Vec<String> = (0..col_count)
579        .map(|i| stmt.column_name(i).unwrap_or("").to_string())
580        .collect();
581
582    let mut raw_rows = stmt.raw_query();
583    Ok(raw_rows
584        .next()?
585        .map(|row| row_to_sql_row(row, col_count, &col_names)))
586}
587
588fn execute_prepared_query_page(
589    mut stmt: rusqlite::Statement<'_>,
590    page: &PageRequest,
591) -> Result<Vec<SqlRow>, rusqlite::Error> {
592    // A zero-limit page still prepares and binds the statement, so invalid
593    // SQL fails identically across every limit; it skips the row cursor
594    // entirely and returns no rows.
595    if page.limit == 0 {
596        return Ok(Vec::new());
597    }
598
599    let col_count = stmt.column_count();
600    let col_names: Vec<String> = (0..col_count)
601        .map(|i| stmt.column_name(i).unwrap_or("").to_string())
602        .collect();
603
604    let mut rows = Vec::new();
605    let mut offset = page.offset;
606    let mut remaining = u64::from(page.limit);
607    let mut raw_rows = stmt.raw_query();
608    // The bound covers owned Rust rows only — this function advances past
609    // `offset`, owns at most the caller-supplied `page.limit` rows, and drops
610    // the statement cursor immediately afterward (ADR-005's bounded-
611    // materialization amendment). Callers own choosing a sane limit.
612    // Engine work is the query plan's own cost, not O(offset + limit):
613    // SQLite still produces and discards `offset` rows, and an unindexed
614    // ORDER BY can force a full sort of the result set before the first row
615    // is stepped. Callers deep-paging a large result set should prefer
616    // keyset pagination over growing offsets.
617    while remaining > 0 {
618        let Some(row) = raw_rows.next()? else {
619            break;
620        };
621        if offset > 0 {
622            offset -= 1;
623            continue;
624        }
625        rows.push(row_to_sql_row(row, col_count, &col_names));
626        remaining -= 1;
627    }
628    Ok(rows)
629}
630
631/// Execute a query on a `rusqlite::Connection` and return owned rows.
632fn execute_query(
633    conn: &rusqlite::Connection,
634    statement: &SqlStatement,
635) -> Result<Vec<SqlRow>, rusqlite::Error> {
636    execute_prepared_query(prepare_bound_statement(conn, statement)?)
637}
638
639fn execute_query_row(
640    conn: &rusqlite::Connection,
641    statement: &SqlStatement,
642) -> Result<Option<SqlRow>, rusqlite::Error> {
643    execute_prepared_query_row(prepare_bound_statement(conn, statement)?)
644}
645
646fn execute_query_page(
647    conn: &rusqlite::Connection,
648    statement: &SqlStatement,
649    page: &PageRequest,
650) -> Result<Vec<SqlRow>, rusqlite::Error> {
651    execute_prepared_query_page(prepare_bound_statement(conn, statement)?, page)
652}
653
654/// SQLite's prepared-statement classifier is authoritative for the safety
655/// boundary. Row-producing DML (`UPDATE ... RETURNING`) and transaction
656/// control may be called through `SqlReader`, but they are admitted writes and
657/// must never register an interrupt target.
658fn statement_is_cancellable_read(stmt: &rusqlite::Statement<'_>, sql: &str) -> bool {
659    stmt.readonly() && transaction_control_head(sql).is_none()
660}
661
662/// Introspection `PRAGMA`s admitted through the pooled reader capability with
663/// an optional single positional argument (`PRAGMA name` or `PRAGMA
664/// name(arg)`) — none of these has a set/assignment form in SQLite, so an
665/// argument is always a read-side filter, never a mutation.
666const READER_STRUCTURAL_PRAGMAS: [&str; 8] = [
667    "table_info",
668    "table_xinfo",
669    "table_list",
670    "index_list",
671    "index_info",
672    "index_xinfo",
673    "foreign_key_list",
674    "integrity_check",
675];
676
677/// Introspection `PRAGMA`s admitted through the pooled reader capability only
678/// in their bare, argument-less read form. Every one of these also has a
679/// `PRAGMA name = value` or `PRAGMA name(value)` assignment form in SQLite —
680/// an assigning form changes connection-local state that a pristine-state
681/// scan run only on dirty checkouts must never let back into the pool
682/// unnoticed (`reader_connection_state_is_pristine`,
683/// `reader_connection_settings_match_baseline`).
684const READER_SETTING_PRAGMAS: [&str; 10] = [
685    "database_list",
686    "collation_list",
687    "function_list",
688    "compile_options",
689    "page_count",
690    "freelist_count",
691    "user_version",
692    "schema_version",
693    "journal_mode",
694    "page_size",
695];
696
697/// Skip a fully parenthesized region starting at `rest[0] == b'('`,
698/// respecting SQL string/identifier quoting (`'...'` with `''` escapes,
699/// `"..."`/`` `...` `` with doubled-quote escapes, and `[...]` bracket
700/// identifiers) and line/block comments, so that a `)`, `(`, or `--` that
701/// merely appears inside a literal or comment is never mistaken for syntax.
702/// Returns the bytes after the matching `)`, or `None` if the region never
703/// closes (malformed or truncated SQL) — callers must fail closed on `None`.
704fn skip_balanced_parens(rest: &[u8]) -> Option<&[u8]> {
705    debug_assert_eq!(rest.first(), Some(&b'('));
706    let mut depth: u32 = 0;
707    let mut idx = 0;
708    loop {
709        match *rest.get(idx)? {
710            b'(' => {
711                depth += 1;
712                idx += 1;
713            }
714            b')' => {
715                depth -= 1;
716                idx += 1;
717                if depth == 0 {
718                    return Some(&rest[idx..]);
719                }
720            }
721            quote @ (b'\'' | b'"' | b'`') => {
722                idx += 1;
723                loop {
724                    match *rest.get(idx)? {
725                        byte if byte == quote => {
726                            idx += 1;
727                            if rest.get(idx) == Some(&quote) {
728                                idx += 1; // doubled-quote escape inside the literal
729                            } else {
730                                break;
731                            }
732                        }
733                        _ => idx += 1,
734                    }
735                }
736            }
737            b'[' => {
738                idx += 1;
739                while *rest.get(idx)? != b']' {
740                    idx += 1;
741                }
742                idx += 1;
743            }
744            b'-' if rest.get(idx + 1) == Some(&b'-') => {
745                idx += 2;
746                while idx < rest.len() && rest[idx] != b'\n' {
747                    idx += 1;
748                }
749            }
750            b'/' if rest.get(idx + 1) == Some(&b'*') => {
751                idx += 2;
752                while idx + 1 < rest.len() && !(rest[idx] == b'*' && rest[idx + 1] == b'/') {
753                    idx += 1;
754                }
755                idx = (idx + 2).min(rest.len());
756            }
757            _ => idx += 1,
758        }
759    }
760}
761
762/// Skip one SQLite identifier — unquoted (`[A-Za-z0-9_]+`, as
763/// [`next_sqlite_token`] already recognizes), double-quoted or
764/// backtick-quoted (both honoring the doubled-quote-character escape), or
765/// bracket-quoted (no escape; SQLite's bracket form ends at the first `]`) —
766/// and return the bytes after it. A quoted CTE name may contain any byte,
767/// including `(` and `)`, so the CTE-list walk must skip the identifier
768/// itself rather than tokenizing it the way an unquoted keyword is.
769fn skip_sqlite_identifier(rest: &[u8]) -> Option<&[u8]> {
770    match *rest.first()? {
771        quote @ (b'"' | b'`') => {
772            let mut idx = 1;
773            loop {
774                match *rest.get(idx)? {
775                    byte if byte == quote => {
776                        idx += 1;
777                        if rest.get(idx) == Some(&quote) {
778                            idx += 1; // doubled-quote escape inside the identifier
779                        } else {
780                            break;
781                        }
782                    }
783                    _ => idx += 1,
784                }
785            }
786            Some(&rest[idx..])
787        }
788        b'[' => {
789            let mut idx = 1;
790            while *rest.get(idx)? != b']' {
791                idx += 1;
792            }
793            Some(&rest[idx + 1..])
794        }
795        _ => next_sqlite_token(rest).map(|(_, next)| next),
796    }
797}
798
799/// Walk past the common-table-expression list following `WITH [RECURSIVE]`
800/// and return the bytes starting at the main statement's own head keyword.
801/// Each CTE body is skipped as a balanced parenthesized region
802/// ([`skip_balanced_parens`]), so nested parens, string literals, and
803/// comments inside a CTE body never confuse the walk. Returns `None` if the
804/// CTE list is not well-formed enough to walk past safely — the caller must
805/// fail closed (refuse admission) rather than guess.
806fn skip_common_table_expressions(tail: &[u8]) -> Option<&[u8]> {
807    let mut rest = skip_sqlite_empty_prefix(tail);
808    if let Some((word, next)) = next_sqlite_token(rest) {
809        if word.eq_ignore_ascii_case(b"RECURSIVE") {
810            rest = skip_sqlite_empty_prefix(next);
811        }
812    }
813    loop {
814        // CTE name — unquoted or SQLite-quoted (`"..."`, `` `...` ``, `[...]`).
815        let next = skip_sqlite_identifier(rest)?;
816        rest = skip_sqlite_empty_prefix(next);
817        // Optional column-name list.
818        if rest.first() == Some(&b'(') {
819            rest = skip_sqlite_empty_prefix(skip_balanced_parens(rest)?);
820        }
821        let (as_keyword, next) = next_sqlite_token(rest)?;
822        if !as_keyword.eq_ignore_ascii_case(b"AS") {
823            return None;
824        }
825        rest = skip_sqlite_empty_prefix(next);
826        // Optional `[NOT] MATERIALIZED` hint (SQLite 3.35+).
827        if let Some((word, next)) = next_sqlite_token(rest) {
828            if word.eq_ignore_ascii_case(b"MATERIALIZED") {
829                rest = skip_sqlite_empty_prefix(next);
830            } else if word.eq_ignore_ascii_case(b"NOT") {
831                let (materialized, next) = next_sqlite_token(skip_sqlite_empty_prefix(next))?;
832                if !materialized.eq_ignore_ascii_case(b"MATERIALIZED") {
833                    return None;
834                }
835                rest = skip_sqlite_empty_prefix(next);
836            }
837        }
838        // CTE body.
839        if rest.first() != Some(&b'(') {
840            return None;
841        }
842        rest = skip_sqlite_empty_prefix(skip_balanced_parens(rest)?);
843        if rest.first() == Some(&b',') {
844            rest = skip_sqlite_empty_prefix(&rest[1..]);
845            continue;
846        }
847        return Some(rest);
848    }
849}
850
851/// Reject any raw SQL that is not one of a small allow-listed set of
852/// read-only statement shapes before it ever reaches a pooled reader
853/// connection.
854///
855/// `stmt.readonly()` (SQLite's own classifier, used by
856/// [`statement_is_cancellable_read`] to decide interrupt eligibility) is not
857/// a sufficient admission predicate on its own: by SQLite's own definition it
858/// also returns `true` for `ATTACH`/`DETACH`, `CREATE TEMP TABLE`, and
859/// configuration `PRAGMA`s like `writable_schema`, `busy_timeout`, or
860/// `cache_size` — none of which write the main database file, but all of
861/// which leave connection-local state that persists across pooled checkouts.
862/// Classification here instead runs on the statement head, admitting only
863/// `SELECT`, `WITH ... SELECT`, `VALUES`, `EXPLAIN [QUERY PLAN] <admitted>`,
864/// and the fixed `PRAGMA` allow-lists above. A `WITH` clause is not itself a
865/// read shape — SQLite also allows `WITH ... INSERT/UPDATE/DELETE`, with or
866/// without `RETURNING` — so a `WITH` head is admitted only after walking past
867/// its CTE list ([`skip_common_table_expressions`]) and confirming the main
868/// statement underneath is itself `SELECT` or `VALUES`.
869pub(crate) fn reader_capability_admits(sql: &str) -> Result<(), String> {
870    let rest = skip_sqlite_empty_prefix(sql.as_bytes());
871    let Some((head, tail)) = next_sqlite_token(rest) else {
872        // No head token (empty/comment-only statement): let SQLite's own
873        // prepare step surface that error rather than duplicating it here.
874        return Ok(());
875    };
876    if head.eq_ignore_ascii_case(b"SELECT") || head.eq_ignore_ascii_case(b"VALUES") {
877        return Ok(());
878    }
879    if head.eq_ignore_ascii_case(b"WITH") {
880        let Some(after_ctes) = skip_common_table_expressions(tail) else {
881            return Err(
882                "WITH statement's common-table-expression list could not be parsed; refusing \
883                 to admit it through the reader capability"
884                    .into(),
885            );
886        };
887        return match next_sqlite_token(after_ctes) {
888            Some((main_head, _))
889                if main_head.eq_ignore_ascii_case(b"SELECT")
890                    || main_head.eq_ignore_ascii_case(b"VALUES") =>
891            {
892                Ok(())
893            }
894            other => Err(format!(
895                "WITH ... {:?} is not admitted through the reader capability; only a \
896                 read-only SELECT/VALUES body after the CTE list may run against a pooled \
897                 reader connection",
898                other.map_or_else(
899                    || "<none>".to_string(),
900                    |(main_head, _)| String::from_utf8_lossy(main_head).into_owned()
901                )
902            )),
903        };
904    }
905    if head.eq_ignore_ascii_case(b"EXPLAIN") {
906        let mut rest = skip_sqlite_empty_prefix(tail);
907        if let Some((query, next)) = next_sqlite_token(rest) {
908            if query.eq_ignore_ascii_case(b"QUERY") {
909                let after_query = skip_sqlite_empty_prefix(next);
910                match next_sqlite_token(after_query) {
911                    Some((plan, next2)) if plan.eq_ignore_ascii_case(b"PLAN") => {
912                        rest = skip_sqlite_empty_prefix(next2);
913                    }
914                    _ => {
915                        return Err(
916                            "EXPLAIN QUERY must be followed by PLAN through the reader capability"
917                                .into(),
918                        );
919                    }
920                }
921            }
922        }
923        return reader_capability_admits(&String::from_utf8_lossy(rest));
924    }
925    if head.eq_ignore_ascii_case(b"PRAGMA") {
926        return reader_capability_admits_pragma(tail);
927    }
928    Err(format!(
929        "statement head {:?} is not admitted through the reader capability; only \
930         SELECT/WITH/VALUES/EXPLAIN and an allow-listed set of read-only PRAGMA forms \
931         may run against a pooled reader connection",
932        String::from_utf8_lossy(head)
933    ))
934}
935
936fn reader_capability_admits_pragma(tail: &[u8]) -> Result<(), String> {
937    let rest = skip_sqlite_empty_prefix(tail);
938    let Some((mut name, mut after_name)) = next_sqlite_token(rest) else {
939        return Err("PRAGMA with no name is not admitted through the reader capability".into());
940    };
941    // `schema.pragma_name` — the schema qualifier changes which attached
942    // database the pragma targets, not which pragma runs.
943    if after_name.first() == Some(&b'.') {
944        let (qualified_name, qualified_after) =
945            next_sqlite_token(&after_name[1..]).ok_or_else(|| {
946                "PRAGMA schema-qualifier with no pragma name is not admitted through the \
947                 reader capability"
948                    .to_string()
949            })?;
950        name = qualified_name;
951        after_name = qualified_after;
952    }
953    let after = skip_sqlite_empty_prefix(after_name);
954    let is_structural = READER_STRUCTURAL_PRAGMAS
955        .iter()
956        .any(|allowed| name.eq_ignore_ascii_case(allowed.as_bytes()));
957    let is_setting = READER_SETTING_PRAGMAS
958        .iter()
959        .any(|allowed| name.eq_ignore_ascii_case(allowed.as_bytes()));
960    if !is_structural && !is_setting {
961        return Err(format!(
962            "PRAGMA {:?} is not admitted through the reader capability",
963            String::from_utf8_lossy(name)
964        ));
965    }
966    if after.first() == Some(&b'=') {
967        return Err(format!(
968            "PRAGMA {:?} may not be assigned through the reader capability",
969            String::from_utf8_lossy(name)
970        ));
971    }
972    if after.first() == Some(&b'(') && !is_structural {
973        return Err(format!(
974            "PRAGMA {:?} may not carry an argument through the reader capability",
975            String::from_utf8_lossy(name)
976        ));
977    }
978    Ok(())
979}
980
981/// Refuse a statement bound for the reader capability that is neither
982/// admitted transaction control (handled by the caller's cached
983/// read-transaction state machine) nor an admitted read shape
984/// ([`reader_capability_admits`]).
985fn admit_reader_capability_sql(
986    statement: &SqlStatement,
987    transaction_control: Option<CachedReadTransactionControl>,
988    operation: &'static str,
989) -> khive_storage::types::StorageResult<()> {
990    // Only the admitted single-level deferred-read span (`BEGIN`/`BEGIN
991    // DEFERRED` and its `COMMIT`/`END`/`ROLLBACK` counterpart) bypasses the
992    // read-shape check below; `Unsupported` forms (`BEGIN IMMEDIATE`,
993    // `SAVEPOINT`, `ROLLBACK TO ...`) fall through and are refused there like
994    // any other non-admitted statement head.
995    if matches!(
996        transaction_control,
997        Some(CachedReadTransactionControl::BeginDeferred)
998            | Some(CachedReadTransactionControl::Finish(_))
999    ) {
1000        return Ok(());
1001    }
1002    reader_capability_admits(&statement.sql).map_err(|message| StorageError::InvalidInput {
1003        capability: StorageCapability::Sql,
1004        operation: operation.into(),
1005        message,
1006    })
1007}
1008
1009fn execute_query_interruptibly(
1010    scope: &crate::read_cancellation::InterruptibleReadScope,
1011    conn: &rusqlite::Connection,
1012    statement: &SqlStatement,
1013    operation: &'static str,
1014    rollback_interrupted_transaction: bool,
1015    interruptible: bool,
1016) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
1017    let stmt = prepare_bound_statement(conn, statement)
1018        .map_err(|error| map_rusqlite_err(error, operation))?;
1019    if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
1020        scope.run_with_interrupted_cleanup(
1021            conn,
1022            move || {
1023                execute_prepared_query(stmt).map_err(|error| map_rusqlite_err(error, operation))
1024            },
1025            || {
1026                rollback_interrupted_read_transaction(
1027                    conn,
1028                    operation,
1029                    rollback_interrupted_transaction,
1030                )
1031            },
1032        )
1033    } else {
1034        scope.mark_write_committed()?;
1035        execute_prepared_query(stmt).map_err(|error| map_rusqlite_err(error, operation))
1036    }
1037}
1038
1039fn execute_query_row_interruptibly(
1040    scope: &crate::read_cancellation::InterruptibleReadScope,
1041    conn: &rusqlite::Connection,
1042    statement: &SqlStatement,
1043    operation: &'static str,
1044    rollback_interrupted_transaction: bool,
1045    interruptible: bool,
1046) -> khive_storage::types::StorageResult<Option<SqlRow>> {
1047    let stmt = prepare_bound_statement(conn, statement)
1048        .map_err(|error| map_rusqlite_err(error, operation))?;
1049    if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
1050        scope.run_with_interrupted_cleanup(
1051            conn,
1052            move || {
1053                execute_prepared_query_row(stmt).map_err(|error| map_rusqlite_err(error, operation))
1054            },
1055            || {
1056                rollback_interrupted_read_transaction(
1057                    conn,
1058                    operation,
1059                    rollback_interrupted_transaction,
1060                )
1061            },
1062        )
1063    } else {
1064        scope.mark_write_committed()?;
1065        execute_prepared_query_row(stmt).map_err(|error| map_rusqlite_err(error, operation))
1066    }
1067}
1068
1069fn execute_query_page_interruptibly(
1070    scope: &crate::read_cancellation::InterruptibleReadScope,
1071    conn: &rusqlite::Connection,
1072    statement: &SqlStatement,
1073    page: &PageRequest,
1074    operation: &'static str,
1075    rollback_interrupted_transaction: bool,
1076    interruptible: bool,
1077) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
1078    let stmt = prepare_bound_statement(conn, statement)
1079        .map_err(|error| map_rusqlite_err(error, operation))?;
1080    if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
1081        scope.run_with_interrupted_cleanup(
1082            conn,
1083            move || {
1084                execute_prepared_query_page(stmt, page)
1085                    .map_err(|error| map_rusqlite_err(error, operation))
1086            },
1087            || {
1088                rollback_interrupted_read_transaction(
1089                    conn,
1090                    operation,
1091                    rollback_interrupted_transaction,
1092                )
1093            },
1094        )
1095    } else {
1096        scope.mark_write_committed()?;
1097        execute_prepared_query_page(stmt, page).map_err(|error| map_rusqlite_err(error, operation))
1098    }
1099}
1100
1101fn rollback_interrupted_read_transaction(
1102    conn: &rusqlite::Connection,
1103    operation: &'static str,
1104    enabled: bool,
1105) -> khive_storage::types::StorageResult<()> {
1106    if !enabled || conn.is_autocommit() {
1107        return Ok(());
1108    }
1109    conn.execute_batch("ROLLBACK")
1110        .map_err(|error| map_rusqlite_err(error, operation))?;
1111    if conn.is_autocommit() {
1112        Ok(())
1113    } else {
1114        Err(StorageError::Transaction {
1115            operation: operation.into(),
1116            message: "interrupted read transaction rollback did not restore autocommit".into(),
1117        })
1118    }
1119}
1120
1121/// Map a rusqlite error to `StorageError`.
1122fn map_rusqlite_err(e: rusqlite::Error, op: &'static str) -> StorageError {
1123    StorageError::driver(StorageCapability::Sql, op, e)
1124}
1125
1126/// How an elapsed handle-slot deadline is classified. ADR-005 pins the closed
1127/// raw-SQL standalone exception (`sql_bridge.reader_open`) and reads on a
1128/// standalone writer to `StorageError::Timeout`; ordinary reader-pool
1129/// saturation reports the typed `AdmissionTimeout` through
1130/// `ConnectionPool::resolve_reader_checkout`.
1131///
1132/// Classification is decided by this enum and never by the operation label.
1133/// The label names the caller's own read (`query_row`, `writer.query_all`, and
1134/// so on) so that a recorded maximum reader hold can be attributed to the read
1135/// that caused it.
1136#[derive(Clone, Copy)]
1137enum SlotTimeoutClass {
1138    Admission,
1139    ReaderContract,
1140}
1141
1142async fn acquire_reader_handle_slot(
1143    pool: &ConnectionPool,
1144    operation: &'static str,
1145    class: SlotTimeoutClass,
1146) -> Result<OwnedSemaphorePermit, StorageError> {
1147    let result = acquire_handle_slot(
1148        pool.sql_bridge_reader_slots(),
1149        pool.config().checkout_timeout,
1150        operation,
1151        class,
1152    )
1153    .await;
1154    if matches!(
1155        &result,
1156        Err(StorageError::Timeout { .. } | StorageError::AdmissionTimeout { .. })
1157    ) {
1158        pool.record_reader_admission_timeout();
1159    }
1160    result
1161}
1162
1163/// Serialize whole units on the one shared in-memory connection. Event-store
1164/// transactions participate in the same budget as raw SQL atomic units.
1165/// The permit must outlive the unit's final COMMIT or ROLLBACK.
1166pub(crate) async fn acquire_in_memory_write_unit(
1167    pool: &ConnectionPool,
1168    operation: &'static str,
1169) -> Result<OwnedSemaphorePermit, StorageError> {
1170    acquire_handle_slot(
1171        pool.sql_bridge_writer_slots(),
1172        pool.config().checkout_timeout,
1173        operation,
1174        SlotTimeoutClass::Admission,
1175    )
1176    .await
1177}
1178
1179async fn acquire_handle_slot(
1180    slots: Arc<Semaphore>,
1181    timeout: std::time::Duration,
1182    operation: &'static str,
1183    class: SlotTimeoutClass,
1184) -> Result<OwnedSemaphorePermit, StorageError> {
1185    tokio::time::timeout(timeout, slots.acquire_owned())
1186        .await
1187        .map_err(|_| match class {
1188            SlotTimeoutClass::Admission => StorageError::AdmissionTimeout {
1189                operation: operation.into(),
1190                timeout_ms: u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX),
1191                pool_identity: None,
1192            },
1193            SlotTimeoutClass::ReaderContract => StorageError::Timeout {
1194                operation: operation.into(),
1195            },
1196        })?
1197        .map_err(|error| StorageError::Pool {
1198            operation: operation.into(),
1199            message: error.to_string(),
1200        })
1201}
1202
1203// =============================================================================
1204// Standalone connection readers/writers (file-backed databases)
1205// =============================================================================
1206
1207fn open_standalone_reader(pool: &ConnectionPool) -> Result<rusqlite::Connection, StorageError> {
1208    pool.open_standalone_reader(StandaloneReaderPurpose::ExplicitSqlReadTransaction)
1209        .map_err(|error| StorageError::driver(StorageCapability::Sql, "open_reader", error))
1210}
1211
1212fn open_standalone_writer(pool: &ConnectionPool) -> Result<rusqlite::Connection, StorageError> {
1213    let config = pool.config();
1214    let conn = pool
1215        .open_standalone_writer()
1216        .map_err(|e| StorageError::driver(StorageCapability::Sql, "open_writer", e))?;
1217
1218    conn.busy_timeout(config.busy_timeout)
1219        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
1220    conn.pragma_update(None, "cache_size", "-65536")
1221        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
1222    conn.pragma_update(None, "mmap_size", "1073741824")
1223        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
1224
1225    Ok(conn)
1226}
1227
1228/// Lift a standalone open onto the blocking thread pool while carrying the
1229/// connection-cap permit into the same closure.
1230///
1231/// If the awaiting future is cancelled, the detached blocking closure owns
1232/// both the connection result and the permit until it finishes, so the cap
1233/// cannot be released while an open is still running.
1234async fn open_standalone_on_blocking<F>(
1235    pool: Arc<ConnectionPool>,
1236    slot: OwnedSemaphorePermit,
1237    operation: &'static str,
1238    open: F,
1239) -> khive_storage::types::StorageResult<(rusqlite::Connection, OwnedSemaphorePermit)>
1240where
1241    F: FnOnce(&ConnectionPool) -> Result<rusqlite::Connection, StorageError> + Send + 'static,
1242{
1243    tokio::task::spawn_blocking(move || open(&pool).map(|conn| (conn, slot)))
1244        .await
1245        .map_err(|e| StorageError::driver(StorageCapability::Sql, operation, e))?
1246}
1247
1248/// [`open_standalone_reader`] lifted onto the blocking thread pool.
1249///
1250/// Opening a SQLite connection is filesystem I/O (open the file, read the
1251/// database header) followed by pragmas executed through SQLite. No
1252/// database lock is acquired at open itself — locks are taken on the first
1253/// statement — but filesystem latency is unbounded, and this module already
1254/// runs every other rusqlite call under `spawn_blocking`, so the open gets
1255/// the same treatment instead of blocking an async worker thread. The reader
1256/// permit is supplied to the helper and returned with the connection.
1257async fn open_standalone_reader_on_blocking(
1258    pool: Arc<ConnectionPool>,
1259    slot: OwnedSemaphorePermit,
1260) -> khive_storage::types::StorageResult<(rusqlite::Connection, OwnedSemaphorePermit)> {
1261    open_standalone_on_blocking(pool, slot, "open_reader", open_standalone_reader).await
1262}
1263
1264/// [`open_standalone_writer`] lifted onto the blocking thread pool; see
1265/// [`open_standalone_reader_on_blocking`] for the blocking rationale.
1266async fn open_standalone_writer_on_blocking(
1267    pool: Arc<ConnectionPool>,
1268    slot: OwnedSemaphorePermit,
1269) -> khive_storage::types::StorageResult<(rusqlite::Connection, OwnedSemaphorePermit)> {
1270    open_standalone_on_blocking(pool, slot, "open_writer", open_standalone_writer).await
1271}
1272
1273// =============================================================================
1274// File-backed: pooled SqliteReader with an explicit transaction exception
1275// =============================================================================
1276
1277const CACHED_READ_TRANSACTION_LABEL: &str = "sql_bridge_cached_read_transaction";
1278
1279/// Admission and observability guards for one explicit cached-reader
1280/// transaction. Both guards are installed only after SQLite accepts `BEGIN`
1281/// and are retained together until SQLite reports autocommit again or the
1282/// owning connection is closed.
1283///
1284/// Field order is deliberate: after [`StandaloneHandle::conn`] closes, the
1285/// reader permit is returned before the registry evidence disappears. There
1286/// is therefore no interval in which SQLite can still own the snapshot while
1287/// the transaction is absent from `tx_registry`.
1288struct CachedReadTransaction {
1289    _slot: OwnedSemaphorePermit,
1290    _tx_handle: khive_storage::tx_registry::TxHandle,
1291    /// When this explicit `BEGIN` was admitted. Read on every subsequent
1292    /// reuse of the owning cached-reader handle (#1846): a transaction whose
1293    /// age has crossed `read_tx_max_age` is rolled back instead of being
1294    /// extended by another call, bounding how long any one reader can pin
1295    /// the WAL snapshot regardless of how many further requests it makes.
1296    opened_at: Instant,
1297}
1298
1299struct StandaloneHandle {
1300    conn: rusqlite::Connection,
1301    /// Present only for a standalone read-write handle, whose one-permit
1302    /// connection budget remains handle-scoped. Read-only connections exist
1303    /// only for explicit deferred transactions and retain reader admission in
1304    /// `read_transaction_slot` until terminal control.
1305    _retained_slot: Option<OwnedSemaphorePermit>,
1306    /// Present only while a cached read-only connection owns one explicit
1307    /// multi-call read transaction. Field order is load-bearing: Rust drops
1308    /// `conn` before these guards, so cancellation or handle drop closes the
1309    /// SQLite transaction before returning reader admission or deregistering
1310    /// the transaction span.
1311    read_transaction_slot: Option<CachedReadTransaction>,
1312}
1313
1314impl StandaloneHandle {
1315    /// Whether this is an idle-cacheable read-only connection rather than a
1316    /// writer connection covered by the handle-scoped writer permit.
1317    fn is_cached_reader(&self) -> bool {
1318        self._retained_slot.is_none()
1319    }
1320
1321    fn has_read_transaction(&self) -> bool {
1322        self.read_transaction_slot.is_some()
1323    }
1324}
1325
1326struct SqliteReader {
1327    /// Present only for the ADR-005/ADR-091 explicitly admitted multi-call
1328    /// deferred transaction. Ordinary reads never populate this field and
1329    /// route through `ConnectionPool::reader` for each operation.
1330    handle: Option<StandaloneHandle>,
1331    pool: Arc<ConnectionPool>,
1332    /// Fail-loud compatibility state after a transaction connection could not
1333    /// be restored safely. `None` normally means "pooled ordinary route";
1334    /// this bit distinguishes that from "exceptional connection consumed".
1335    poisoned: bool,
1336}
1337
1338async fn open_explicit_read_transaction_handle(
1339    pool: Arc<ConnectionPool>,
1340) -> khive_storage::types::StorageResult<StandaloneHandle> {
1341    let open_slot = crate::await_request_read_phase(
1342        "sql_bridge.reader_open",
1343        acquire_reader_handle_slot(
1344            &pool,
1345            "sql_bridge.reader_open",
1346            SlotTimeoutClass::ReaderContract,
1347        ),
1348    )
1349    .await??;
1350    let (conn, open_slot) = crate::await_request_read_phase(
1351        "sql_bridge.reader_open",
1352        open_standalone_reader_on_blocking(pool, open_slot),
1353    )
1354    .await??;
1355    drop(open_slot);
1356    Ok(StandaloneHandle {
1357        conn,
1358        _retained_slot: None,
1359        read_transaction_slot: None,
1360    })
1361}
1362
1363impl SqliteReader {
1364    /// Select the standalone path only for a live or newly requested explicit
1365    /// deferred transaction. `false` means the caller must execute this
1366    /// operation through the bounded reader pool.
1367    async fn use_explicit_transaction_handle(
1368        &mut self,
1369        transaction_control: Option<CachedReadTransactionControl>,
1370        operation: &'static str,
1371    ) -> khive_storage::types::StorageResult<bool> {
1372        if self.poisoned {
1373            return Err(StorageError::Pool {
1374                operation: operation.into(),
1375                message: "connection already consumed".into(),
1376            });
1377        }
1378        if self.handle.is_some() {
1379            return Ok(true);
1380        }
1381        match transaction_control {
1382            None => Ok(false),
1383            Some(CachedReadTransactionControl::BeginDeferred) => {
1384                self.handle =
1385                    Some(open_explicit_read_transaction_handle(Arc::clone(&self.pool)).await?);
1386                Ok(true)
1387            }
1388            Some(CachedReadTransactionControl::Finish(keyword))
1389            | Some(CachedReadTransactionControl::Unsupported(keyword)) => {
1390                Err(StorageError::InvalidInput {
1391                    capability: StorageCapability::Sql,
1392                    operation: operation.into(),
1393                    message: format!(
1394                        "cached read-only handle has no admitted transaction for transaction \
1395                         control ({keyword})"
1396                    ),
1397                })
1398            }
1399        }
1400    }
1401
1402    /// A standalone connection exists only for one explicit transaction.
1403    /// Close it immediately after a failed BEGIN, COMMIT/ROLLBACK, age
1404    /// eviction, or cancellation cleanup restores autocommit. A later
1405    /// ordinary query must return to pooled routing instead of silently
1406    /// retaining a standalone cache.
1407    fn close_inactive_transaction_handle(&mut self) {
1408        if self
1409            .handle
1410            .as_ref()
1411            .is_some_and(|handle| handle.is_cached_reader() && !handle.has_read_transaction())
1412        {
1413            drop(self.handle.take());
1414        }
1415    }
1416}
1417
1418/// Run one file-backed read while coupling its connection state to the active
1419/// reader permit.
1420///
1421/// This path serves only the explicit deferred read-transaction exception and
1422/// reader-supertrait calls on a standalone read-write handle. Ordinary
1423/// read-only traffic uses [`run_pool_reader_query`]. A successful top-level
1424/// deferred `BEGIN` moves its operation permit into the handle; subsequent
1425/// reads reuse it until `COMMIT`/`END`/`ROLLBACK` restores autocommit. The
1426/// connection is declared before that retained permit, so dropping or
1427/// cancelling the handle closes SQLite first and releases admission second.
1428/// A standalone writer's reader calls still take active-reader admission; once
1429/// its connection is outside autocommit, the acquisition and SELECT are
1430/// completion-preserving rather than request-cancellable.
1431async fn execute_standalone_read<R, F>(
1432    handle: &mut Option<StandaloneHandle>,
1433    pool: Arc<ConnectionPool>,
1434    operation: &'static str,
1435    transaction_control: Option<CachedReadTransactionControl>,
1436    read: F,
1437) -> khive_storage::types::StorageResult<R>
1438where
1439    R: Send + 'static,
1440    F: FnOnce(
1441            &crate::read_cancellation::InterruptibleReadScope,
1442            &rusqlite::Connection,
1443            bool,
1444            bool,
1445        ) -> khive_storage::types::StorageResult<R>
1446        + Send
1447        + 'static,
1448{
1449    if handle.is_none() {
1450        return Err(StorageError::Pool {
1451            operation: operation.into(),
1452            message: "connection already consumed".into(),
1453        });
1454    }
1455    let active_read_transaction = handle
1456        .as_ref()
1457        .is_some_and(|handle| handle.is_cached_reader() && handle.has_read_transaction());
1458    let completion_preserving_writer_transaction = handle
1459        .as_ref()
1460        .is_some_and(|handle| !handle.is_cached_reader() && !handle.conn.is_autocommit());
1461    let mut operation_slot = if active_read_transaction {
1462        None
1463    } else if completion_preserving_writer_transaction {
1464        // A read inside an admitted write transaction still counts against the
1465        // reader budget, but request cancellation cannot skip that admission
1466        // and strand the transaction between statements.
1467        Some(acquire_reader_handle_slot(&pool, operation, SlotTimeoutClass::ReaderContract).await?)
1468    } else {
1469        Some(
1470            crate::await_request_read_phase(
1471                operation,
1472                acquire_reader_handle_slot(&pool, operation, SlotTimeoutClass::ReaderContract),
1473            )
1474            .await??,
1475        )
1476    };
1477    let Some(owned_handle) = handle.take() else {
1478        return Err(StorageError::Pool {
1479            operation: operation.into(),
1480            message: "connection already consumed".into(),
1481        });
1482    };
1483    let origin = pool.origin();
1484    let read_tx_max_age = pool.config().read_tx_max_age;
1485    let (owned_handle, result) = crate::read_cancellation::run_interruptible_read(
1486        StorageCapability::Sql,
1487        operation,
1488        move |scope| {
1489            let mut owned_handle = owned_handle;
1490            let cached_reader = owned_handle.is_cached_reader();
1491            let entered_with_transaction = owned_handle.has_read_transaction();
1492            let entered_autocommit = owned_handle.conn.is_autocommit();
1493            let mut restore_handle = true;
1494            let mut result = if cached_reader && entered_with_transaction && entered_autocommit {
1495                // A connection cannot pin a snapshot in autocommit. Repair the
1496                // admission state before returning the invariant failure.
1497                drop(owned_handle.read_transaction_slot.take());
1498                Err(StorageError::InvalidInput {
1499                    capability: StorageCapability::Sql,
1500                    operation: operation.into(),
1501                    message: "cached read-only handle retained transaction admission after SQLite \
1502                          had already returned to autocommit; the stale permit was released"
1503                        .into(),
1504                })
1505            } else if cached_reader && !entered_with_transaction && !entered_autocommit {
1506                Err(StorageError::InvalidInput {
1507                    capability: StorageCapability::Sql,
1508                    operation: operation.into(),
1509                    message: "cached read-only handle entered the operation outside autocommit; \
1510                          its transaction was rolled back before releasing the reader permit"
1511                        .into(),
1512                })
1513            } else if cached_reader
1514                && entered_with_transaction
1515                && owned_handle
1516                    .read_transaction_slot
1517                    .as_ref()
1518                    .is_some_and(|tx| tx.opened_at.elapsed() >= read_tx_max_age)
1519            {
1520                // #1846: this handle's admitted read transaction has pinned a
1521                // WAL snapshot for at least `read_tx_max_age` — reject the
1522                // continuation and roll it back instead of extending the pin
1523                // for another call, regardless of what the caller asked for.
1524                crate::checkpoint::note_read_tx_max_age_eviction();
1525                match owned_handle.conn.execute_batch("ROLLBACK") {
1526                    Ok(()) if owned_handle.conn.is_autocommit() => {
1527                        drop(owned_handle.read_transaction_slot.take());
1528                        Err(StorageError::ReadTransactionAgeEvicted {
1529                            operation: operation.into(),
1530                            max_age_secs: read_tx_max_age.as_secs(),
1531                        })
1532                    }
1533                    Ok(()) => {
1534                        restore_handle = false;
1535                        Err(StorageError::ReadTransactionAgeEvictionCleanupFailed {
1536                            operation: operation.into(),
1537                            max_age_secs: read_tx_max_age.as_secs(),
1538                            message: "rollback did not restore autocommit".into(),
1539                        })
1540                    }
1541                    Err(error) => {
1542                        restore_handle = false;
1543                        Err(StorageError::ReadTransactionAgeEvictionCleanupFailed {
1544                            operation: operation.into(),
1545                            max_age_secs: read_tx_max_age.as_secs(),
1546                            message: format!("rollback failed: {error}"),
1547                        })
1548                    }
1549                }
1550            } else if cached_reader && entered_with_transaction {
1551                match transaction_control {
1552                    None | Some(CachedReadTransactionControl::Finish(_)) => {
1553                        read(scope, &owned_handle.conn, true, true)
1554                    }
1555                    Some(CachedReadTransactionControl::BeginDeferred) => {
1556                        Err(StorageError::InvalidInput {
1557                            capability: StorageCapability::Sql,
1558                            operation: operation.into(),
1559                            message: "cached read-only handle already owns an admitted read \
1560                                  transaction; nested BEGIN is not supported"
1561                                .into(),
1562                        })
1563                    }
1564                    Some(CachedReadTransactionControl::Unsupported(keyword)) => {
1565                        Err(StorageError::InvalidInput {
1566                            capability: StorageCapability::Sql,
1567                            operation: operation.into(),
1568                            message: format!(
1569                                "cached read-only transaction does not support nested or \
1570                             write-locking transaction control ({keyword})"
1571                            ),
1572                        })
1573                    }
1574                }
1575            } else if cached_reader {
1576                match transaction_control {
1577                    None | Some(CachedReadTransactionControl::BeginDeferred) => {
1578                        read(scope, &owned_handle.conn, false, true)
1579                    }
1580                    Some(CachedReadTransactionControl::Finish(keyword))
1581                    | Some(CachedReadTransactionControl::Unsupported(keyword)) => {
1582                        Err(StorageError::InvalidInput {
1583                            capability: StorageCapability::Sql,
1584                            operation: operation.into(),
1585                            message: format!(
1586                                "cached read-only handle has no admitted transaction for \
1587                             transaction control ({keyword})"
1588                            ),
1589                        })
1590                    }
1591                }
1592            } else {
1593                read(scope, &owned_handle.conn, false, entered_autocommit)
1594            };
1595
1596            if scope.cleanup_failed() {
1597                // A connection-global callback that could not be removed may
1598                // fire for an unrelated future borrower. Closing this handle
1599                // is the only safe recovery; its transaction, if any, ends
1600                // before reader admission is released below.
1601                restore_handle = false;
1602            }
1603
1604            // An interrupted explicit read transaction must never be restored to
1605            // the cached handle: it may still own a WAL snapshot and SQLite's
1606            // interrupted flag applies to the transaction as a whole. Roll back
1607            // before releasing its retained admission; if rollback cannot prove
1608            // autocommit, discard the connection.
1609            if cached_reader
1610                && matches!(result, Err(StorageError::Timeout { .. }))
1611                && !owned_handle.conn.is_autocommit()
1612            {
1613                match owned_handle.conn.execute_batch("ROLLBACK") {
1614                    Ok(()) if owned_handle.conn.is_autocommit() => {
1615                        drop(owned_handle.read_transaction_slot.take());
1616                    }
1617                    Ok(()) => {
1618                        restore_handle = false;
1619                        result = Err(StorageError::Transaction {
1620                            operation: operation.into(),
1621                            message:
1622                                "interrupted read transaction rollback did not restore autocommit; \
1623                                  the connection was discarded"
1624                                    .into(),
1625                        });
1626                    }
1627                    Err(error) => {
1628                        restore_handle = false;
1629                        result = Err(StorageError::Transaction {
1630                            operation: operation.into(),
1631                            message: format!(
1632                                "failed to roll back interrupted read transaction ({error}); \
1633                             the connection was discarded"
1634                            ),
1635                        });
1636                    }
1637                }
1638            }
1639
1640            if cached_reader && entered_with_transaction {
1641                if owned_handle.conn.is_autocommit() {
1642                    // SQLite has ended the snapshot; release only after observing
1643                    // that terminal state. This also fails closed if an ordinary
1644                    // statement unexpectedly ended the transaction.
1645                    drop(owned_handle.read_transaction_slot.take());
1646                    if result.is_ok()
1647                        && !matches!(
1648                            transaction_control,
1649                            Some(CachedReadTransactionControl::Finish(_))
1650                        )
1651                    {
1652                        result = Err(StorageError::InvalidInput {
1653                            capability: StorageCapability::Sql,
1654                            operation: operation.into(),
1655                            message: "cached read-only operation unexpectedly ended its admitted \
1656                                  transaction; reader admission was released after autocommit"
1657                                .into(),
1658                        });
1659                    }
1660                } else if result.is_ok()
1661                    && matches!(
1662                        transaction_control,
1663                        Some(CachedReadTransactionControl::Finish(_))
1664                    )
1665                {
1666                    result = Err(StorageError::InvalidInput {
1667                        capability: StorageCapability::Sql,
1668                        operation: operation.into(),
1669                        message: "transaction-ending control completed but the cached reader \
1670                              remained outside autocommit; its reader permit remains retained"
1671                            .into(),
1672                    });
1673                }
1674            } else if cached_reader
1675                && entered_autocommit
1676                && matches!(
1677                    transaction_control,
1678                    Some(CachedReadTransactionControl::BeginDeferred)
1679                )
1680                && result.is_ok()
1681            {
1682                if owned_handle.conn.is_autocommit() {
1683                    result = Err(StorageError::InvalidInput {
1684                        capability: StorageCapability::Sql,
1685                        operation: operation.into(),
1686                        message: "deferred BEGIN completed without opening a read transaction"
1687                            .into(),
1688                    });
1689                } else {
1690                    match operation_slot.take() {
1691                        Some(slot) => {
1692                            let tx_handle = khive_storage::tx_registry::register_scoped(
1693                                Some(CACHED_READ_TRANSACTION_LABEL.to_string()),
1694                                origin.clone(),
1695                            );
1696                            owned_handle.read_transaction_slot = Some(CachedReadTransaction {
1697                                _slot: slot,
1698                                _tx_handle: tx_handle,
1699                                opened_at: Instant::now(),
1700                            });
1701                        }
1702                        None => {
1703                            result = Err(StorageError::Pool {
1704                                operation: operation.into(),
1705                                message: "successful cached-reader BEGIN had no operation permit; \
1706                                      its transaction was rolled back before returning"
1707                                    .into(),
1708                            });
1709                        }
1710                    }
1711                }
1712            }
1713
1714            // Any non-autocommit state without its retained admission is stale or
1715            // was opened by a statement the transaction classifier did not admit.
1716            if cached_reader
1717                && owned_handle.read_transaction_slot.is_none()
1718                && !owned_handle.conn.is_autocommit()
1719            {
1720                match owned_handle.conn.execute_batch("ROLLBACK") {
1721                    Ok(()) if owned_handle.conn.is_autocommit() => {
1722                        if result.is_ok() {
1723                            result = Err(StorageError::InvalidInput {
1724                                capability: StorageCapability::Sql,
1725                                operation: operation.into(),
1726                                message: "cached read-only operation left the connection outside \
1727                                      autocommit; its transaction was rolled back before \
1728                                      releasing the reader permit"
1729                                    .into(),
1730                            });
1731                        }
1732                    }
1733                    Ok(()) => {
1734                        restore_handle = false;
1735                        result = Err(StorageError::Transaction {
1736                            operation: operation.into(),
1737                            message: "ROLLBACK completed but the cached reader remained outside \
1738                                  autocommit; the connection was discarded before releasing \
1739                                  the reader permit"
1740                                .into(),
1741                        });
1742                    }
1743                    Err(error) => {
1744                        restore_handle = false;
1745                        result = Err(StorageError::Transaction {
1746                            operation: operation.into(),
1747                            message: format!(
1748                            "failed to roll back a cached reader outside autocommit ({error}); \
1749                             the connection was discarded before releasing the reader permit"
1750                        ),
1751                        });
1752                    }
1753                }
1754            }
1755
1756            let owned_handle = if restore_handle {
1757                Some(owned_handle)
1758            } else {
1759                // Closing the poisoned connection ends any remaining transaction.
1760                // This must precede the active-reader permit release below.
1761                drop(owned_handle);
1762                None
1763            };
1764            // For ordinary reads and rejected controls this is the operation
1765            // permit. A successful BEGIN moved it into `owned_handle`; poisoned
1766            // handles were closed above before this remaining permit is released.
1767            drop(operation_slot);
1768            Ok((owned_handle, result))
1769        },
1770    )
1771    .await?;
1772    *handle = owned_handle;
1773    result
1774}
1775
1776#[async_trait]
1777impl khive_storage::SqlReader for SqliteReader {
1778    async fn query_row(
1779        &mut self,
1780        statement: SqlStatement,
1781    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
1782        let transaction_control = cached_read_transaction_control(&statement.sql);
1783        admit_reader_capability_sql(&statement, transaction_control, "query_row")?;
1784        if !self
1785            .use_explicit_transaction_handle(transaction_control, "query_row")
1786            .await?
1787        {
1788            return run_pool_reader_query(
1789                Arc::clone(&self.pool),
1790                "query_row",
1791                move |scope, conn| {
1792                    execute_query_row_interruptibly(
1793                        scope,
1794                        conn,
1795                        &statement,
1796                        "query_row",
1797                        false,
1798                        true,
1799                    )
1800                },
1801            )
1802            .await;
1803        }
1804        let result = execute_standalone_read(
1805            &mut self.handle,
1806            Arc::clone(&self.pool),
1807            "query_row",
1808            transaction_control,
1809            move |scope, conn, rollback, interruptible| {
1810                execute_query_row_interruptibly(
1811                    scope,
1812                    conn,
1813                    &statement,
1814                    "query_row",
1815                    rollback,
1816                    interruptible,
1817                )
1818            },
1819        )
1820        .await;
1821        if self.handle.is_none() {
1822            self.poisoned = true;
1823        }
1824        self.close_inactive_transaction_handle();
1825        result
1826    }
1827
1828    async fn query_all(
1829        &mut self,
1830        statement: SqlStatement,
1831    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
1832        let transaction_control = cached_read_transaction_control(&statement.sql);
1833        admit_reader_capability_sql(&statement, transaction_control, "query_all")?;
1834        if !self
1835            .use_explicit_transaction_handle(transaction_control, "query_all")
1836            .await?
1837        {
1838            return run_pool_reader_query(
1839                Arc::clone(&self.pool),
1840                "query_all",
1841                move |scope, conn| {
1842                    execute_query_interruptibly(scope, conn, &statement, "query_all", false, true)
1843                },
1844            )
1845            .await;
1846        }
1847        let result = execute_standalone_read(
1848            &mut self.handle,
1849            Arc::clone(&self.pool),
1850            "query_all",
1851            transaction_control,
1852            move |scope, conn, rollback, interruptible| {
1853                execute_query_interruptibly(
1854                    scope,
1855                    conn,
1856                    &statement,
1857                    "query_all",
1858                    rollback,
1859                    interruptible,
1860                )
1861            },
1862        )
1863        .await;
1864        if self.handle.is_none() {
1865            self.poisoned = true;
1866        }
1867        self.close_inactive_transaction_handle();
1868        result
1869    }
1870
1871    async fn query_page(
1872        &mut self,
1873        statement: SqlStatement,
1874        page: PageRequest,
1875    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
1876        let transaction_control = cached_read_transaction_control(&statement.sql);
1877        admit_reader_capability_sql(&statement, transaction_control, "query_page")?;
1878        if !self
1879            .use_explicit_transaction_handle(transaction_control, "query_page")
1880            .await?
1881        {
1882            return run_pool_reader_query(
1883                Arc::clone(&self.pool),
1884                "query_page",
1885                move |scope, conn| {
1886                    execute_query_page_interruptibly(
1887                        scope,
1888                        conn,
1889                        &statement,
1890                        &page,
1891                        "query_page",
1892                        false,
1893                        true,
1894                    )
1895                },
1896            )
1897            .await;
1898        }
1899        let result = execute_standalone_read(
1900            &mut self.handle,
1901            Arc::clone(&self.pool),
1902            "query_page",
1903            transaction_control,
1904            move |scope, conn, rollback, interruptible| {
1905                execute_query_page_interruptibly(
1906                    scope,
1907                    conn,
1908                    &statement,
1909                    &page,
1910                    "query_page",
1911                    rollback,
1912                    interruptible,
1913                )
1914            },
1915        )
1916        .await;
1917        if self.handle.is_none() {
1918            self.poisoned = true;
1919        }
1920        self.close_inactive_transaction_handle();
1921        result
1922    }
1923
1924    async fn query_scalar(
1925        &mut self,
1926        statement: SqlStatement,
1927    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
1928        let row = self.query_row(statement).await?;
1929        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
1930    }
1931
1932    async fn explain(
1933        &mut self,
1934        statement: SqlStatement,
1935    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
1936        let explain_stmt = SqlStatement {
1937            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
1938            params: statement.params,
1939            label: statement.label,
1940        };
1941        self.query_all(explain_stmt).await
1942    }
1943}
1944
1945// =============================================================================
1946// File-backed: SqliteWriter (standalone connection)
1947// =============================================================================
1948
1949struct SqliteWriter {
1950    /// `None` at construction when a `WriterTaskHandle` was obtained (ADR-136
1951    /// D1 gate 1: queue-first `writer()` skips the standalone open in that
1952    /// case). Ordinary `SqlReader` supertrait calls then use pooled readers;
1953    /// this slot is populated only while such a queue-backed handle owns an
1954    /// explicit deferred read transaction. An eagerly opened read-write
1955    /// connection (the no-writer-task branch) retains its one-permit writer
1956    /// budget for the handle's lifetime and must serve its reads on that same
1957    /// connection to preserve manual-transaction visibility.
1958    handle: Option<StandaloneHandle>,
1959    /// ADR-067 Component A: when the write queue is enabled, `execute_batch`
1960    /// routes the whole caller-supplied statement list through the
1961    /// single-writer task instead of opening its own `BEGIN IMMEDIATE` on
1962    /// the standalone connection. `None` when the flag is off or no writer
1963    /// task is available
1964    /// (best-effort — degrades to the standalone-connection path below).
1965    writer_task: Option<crate::writer_task::WriterTaskHandle>,
1966    /// The origin (ADR-091 backend-scoped attribution) of the pool this
1967    /// standalone connection was opened against.
1968    origin: khive_storage::tx_registry::TxOrigin,
1969    /// This connection's pool's writer-timeout sink identity (`db_label`),
1970    /// captured at construction so the standalone-path busy/locked mapping
1971    /// below doesn't need a `&ConnectionPool` reference to report against.
1972    db: String,
1973    /// Reader-pool owner and explicit-transaction exception source.
1974    pool: Arc<ConnectionPool>,
1975}
1976
1977impl SqliteWriter {
1978    async fn use_queue_read_transaction_handle(
1979        &mut self,
1980        transaction_control: Option<CachedReadTransactionControl>,
1981        operation: &'static str,
1982    ) -> khive_storage::types::StorageResult<bool> {
1983        if self.handle.is_some() {
1984            return Ok(true);
1985        }
1986        match transaction_control {
1987            None => Ok(false),
1988            Some(CachedReadTransactionControl::BeginDeferred) => {
1989                self.handle =
1990                    Some(open_explicit_read_transaction_handle(Arc::clone(&self.pool)).await?);
1991                Ok(true)
1992            }
1993            Some(CachedReadTransactionControl::Finish(keyword))
1994            | Some(CachedReadTransactionControl::Unsupported(keyword)) => {
1995                Err(StorageError::InvalidInput {
1996                    capability: StorageCapability::Sql,
1997                    operation: operation.into(),
1998                    message: format!(
1999                        "cached read-only handle has no admitted transaction for transaction \
2000                         control ({keyword})"
2001                    ),
2002                })
2003            }
2004        }
2005    }
2006
2007    fn close_inactive_queue_read_transaction_handle(&mut self) {
2008        if self
2009            .handle
2010            .as_ref()
2011            .is_some_and(|handle| handle.is_cached_reader() && !handle.has_read_transaction())
2012        {
2013            drop(self.handle.take());
2014        }
2015    }
2016}
2017
2018#[async_trait]
2019impl khive_storage::SqlReader for SqliteWriter {
2020    async fn query_row(
2021        &mut self,
2022        statement: SqlStatement,
2023    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
2024        if self.writer_task.is_some() {
2025            let transaction_control = cached_read_transaction_control(&statement.sql);
2026            if !self
2027                .use_queue_read_transaction_handle(transaction_control, "writer.query_row")
2028                .await?
2029            {
2030                admit_reader_capability_sql(&statement, transaction_control, "writer.query_row")?;
2031                return run_pool_reader_query(
2032                    Arc::clone(&self.pool),
2033                    "writer.query_row",
2034                    move |scope, conn| {
2035                        execute_query_row_interruptibly(
2036                            scope,
2037                            conn,
2038                            &statement,
2039                            "writer.query_row",
2040                            false,
2041                            true,
2042                        )
2043                    },
2044                )
2045                .await;
2046            }
2047            let result = execute_standalone_read(
2048                &mut self.handle,
2049                Arc::clone(&self.pool),
2050                "writer.query_row",
2051                transaction_control,
2052                move |scope, conn, rollback, interruptible| {
2053                    execute_query_row_interruptibly(
2054                        scope,
2055                        conn,
2056                        &statement,
2057                        "writer.query_row",
2058                        rollback,
2059                        interruptible,
2060                    )
2061                },
2062            )
2063            .await;
2064            self.close_inactive_queue_read_transaction_handle();
2065            return result;
2066        }
2067        let transaction_control = cached_read_transaction_control(&statement.sql);
2068        execute_standalone_read(
2069            &mut self.handle,
2070            Arc::clone(&self.pool),
2071            "writer.query_row",
2072            transaction_control,
2073            move |scope, conn, rollback, interruptible| {
2074                execute_query_row_interruptibly(
2075                    scope,
2076                    conn,
2077                    &statement,
2078                    "writer.query_row",
2079                    rollback,
2080                    interruptible,
2081                )
2082            },
2083        )
2084        .await
2085    }
2086
2087    async fn query_all(
2088        &mut self,
2089        statement: SqlStatement,
2090    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2091        if self.writer_task.is_some() {
2092            let transaction_control = cached_read_transaction_control(&statement.sql);
2093            if !self
2094                .use_queue_read_transaction_handle(transaction_control, "writer.query_all")
2095                .await?
2096            {
2097                admit_reader_capability_sql(&statement, transaction_control, "writer.query_all")?;
2098                return run_pool_reader_query(
2099                    Arc::clone(&self.pool),
2100                    "writer.query_all",
2101                    move |scope, conn| {
2102                        execute_query_interruptibly(
2103                            scope,
2104                            conn,
2105                            &statement,
2106                            "writer.query_all",
2107                            false,
2108                            true,
2109                        )
2110                    },
2111                )
2112                .await;
2113            }
2114            let result = execute_standalone_read(
2115                &mut self.handle,
2116                Arc::clone(&self.pool),
2117                "writer.query_all",
2118                transaction_control,
2119                move |scope, conn, rollback, interruptible| {
2120                    execute_query_interruptibly(
2121                        scope,
2122                        conn,
2123                        &statement,
2124                        "writer.query_all",
2125                        rollback,
2126                        interruptible,
2127                    )
2128                },
2129            )
2130            .await;
2131            self.close_inactive_queue_read_transaction_handle();
2132            return result;
2133        }
2134        let transaction_control = cached_read_transaction_control(&statement.sql);
2135        execute_standalone_read(
2136            &mut self.handle,
2137            Arc::clone(&self.pool),
2138            "writer.query_all",
2139            transaction_control,
2140            move |scope, conn, rollback, interruptible| {
2141                execute_query_interruptibly(
2142                    scope,
2143                    conn,
2144                    &statement,
2145                    "writer.query_all",
2146                    rollback,
2147                    interruptible,
2148                )
2149            },
2150        )
2151        .await
2152    }
2153
2154    async fn query_page(
2155        &mut self,
2156        statement: SqlStatement,
2157        page: PageRequest,
2158    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2159        if self.writer_task.is_some() {
2160            let transaction_control = cached_read_transaction_control(&statement.sql);
2161            if !self
2162                .use_queue_read_transaction_handle(transaction_control, "writer.query_page")
2163                .await?
2164            {
2165                admit_reader_capability_sql(&statement, transaction_control, "writer.query_page")?;
2166                return run_pool_reader_query(
2167                    Arc::clone(&self.pool),
2168                    "writer.query_page",
2169                    move |scope, conn| {
2170                        execute_query_page_interruptibly(
2171                            scope,
2172                            conn,
2173                            &statement,
2174                            &page,
2175                            "writer.query_page",
2176                            false,
2177                            true,
2178                        )
2179                    },
2180                )
2181                .await;
2182            }
2183            let result = execute_standalone_read(
2184                &mut self.handle,
2185                Arc::clone(&self.pool),
2186                "writer.query_page",
2187                transaction_control,
2188                move |scope, conn, rollback, interruptible| {
2189                    execute_query_page_interruptibly(
2190                        scope,
2191                        conn,
2192                        &statement,
2193                        &page,
2194                        "writer.query_page",
2195                        rollback,
2196                        interruptible,
2197                    )
2198                },
2199            )
2200            .await;
2201            self.close_inactive_queue_read_transaction_handle();
2202            return result;
2203        }
2204        let transaction_control = cached_read_transaction_control(&statement.sql);
2205        execute_standalone_read(
2206            &mut self.handle,
2207            Arc::clone(&self.pool),
2208            "writer.query_page",
2209            transaction_control,
2210            move |scope, conn, rollback, interruptible| {
2211                execute_query_page_interruptibly(
2212                    scope,
2213                    conn,
2214                    &statement,
2215                    &page,
2216                    "writer.query_page",
2217                    rollback,
2218                    interruptible,
2219                )
2220            },
2221        )
2222        .await
2223    }
2224
2225    async fn query_scalar(
2226        &mut self,
2227        statement: SqlStatement,
2228    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
2229        let row = khive_storage::SqlReader::query_row(self, statement).await?;
2230        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
2231    }
2232
2233    async fn explain(
2234        &mut self,
2235        statement: SqlStatement,
2236    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2237        let explain_stmt = SqlStatement {
2238            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
2239            params: statement.params,
2240            label: statement.label,
2241        };
2242        khive_storage::SqlReader::query_all(self, explain_stmt).await
2243    }
2244}
2245
2246#[async_trait]
2247impl khive_storage::SqlWriter for SqliteWriter {
2248    async fn execute(
2249        &mut self,
2250        statement: SqlStatement,
2251    ) -> khive_storage::types::StorageResult<u64> {
2252        // ADR-067 Component A (Fork C slice 2): a single statement is
2253        // self-contained, just like `execute_batch`'s full statement list —
2254        // transaction-control rejection remains an `execute_batch` contract;
2255        // this primitive is also used by internal atomic transaction owners.
2256        // route it through the writer task when available. `self.handle` is
2257        // left untouched so a subsequent `execute`/`execute_script` call on
2258        // this same handle still works over the standalone connection.
2259        if let Some(writer_task) = self.writer_task.clone() {
2260            return writer_task
2261                .send_bounded(move |conn| {
2262                    let mut stmt = prepare_cached_sql_statement(conn, &statement.sql)
2263                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
2264                    bind_params(&mut stmt, &statement.params)
2265                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
2266                    let affected = stmt
2267                        .raw_execute()
2268                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
2269                    Ok(affected as u64)
2270                })
2271                .await;
2272        }
2273
2274        let handle = self.handle.take().ok_or_else(|| StorageError::Pool {
2275            operation: "execute".into(),
2276            message: "connection already consumed".into(),
2277        })?;
2278        let (handle, result) = tokio::task::spawn_blocking(move || {
2279            let res = (|| -> Result<usize, rusqlite::Error> {
2280                let mut stmt = prepare_cached_sql_statement(&handle.conn, &statement.sql)?;
2281                bind_params(&mut stmt, &statement.params)?;
2282                stmt.raw_execute()
2283            })();
2284            (handle, res)
2285        })
2286        .await
2287        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute", e))?;
2288        self.handle = Some(handle);
2289        let affected = result.map_err(|e| {
2290            crate::timeout_sink::maybe_emit_busy(
2291                &self.db,
2292                crate::timeout_sink::Site::StandaloneSqlBridge,
2293                &e,
2294            );
2295            map_rusqlite_err(e, "execute")
2296        })?;
2297        Ok(affected as u64)
2298    }
2299
2300    async fn execute_batch(
2301        &mut self,
2302        statements: Vec<SqlStatement>,
2303    ) -> khive_storage::types::StorageResult<u64> {
2304        // ADR-067 Component A: this call is self-contained (the full statement
2305        // list is supplied up front and the whole thing commits or rolls back
2306        // as one unit) — unlike `writer()`'s live incrementally-driven handle,
2307        // it maps cleanly onto a single `WriteRequest`. Route it through the
2308        // writer task when available; `self.handle` is left untouched so a
2309        // subsequent `execute`/`execute_script` call on this same handle still
2310        // works over the standalone connection (that dispatch is unmigrated —
2311        // see `SqlBridge::writer()`).
2312        //
2313        // Both paths reject transaction-control statements BEFORE executing
2314        // anything: the queue-backed branch runs inside the writer task's own
2315        // `BEGIN IMMEDIATE` (a caller `COMMIT` there would close the task's
2316        // transaction and terminate the writer task), and the standalone
2317        // branch below wraps the list in its own `BEGIN IMMEDIATE` (a caller
2318        // `COMMIT` would commit early and break all-or-nothing).
2319        reject_transaction_control_statements(&statements, "execute_batch")?;
2320        if let Some(writer_task) = self.writer_task.clone() {
2321            return writer_task
2322                .send_bounded(move |conn| {
2323                    let prepared = prepare_batch_statements(conn, &statements)
2324                        .map_err(|e| map_rusqlite_err(e, "execute_batch"))?;
2325                    execute_prepared_batch(conn, prepared, &statements)
2326                        .map_err(|e| map_rusqlite_err(e, "execute_batch"))
2327                })
2328                .await;
2329        }
2330
2331        let handle = self.handle.take().ok_or_else(|| StorageError::Pool {
2332            operation: "execute_batch".into(),
2333            message: "connection already consumed".into(),
2334        })?;
2335        let origin = self.origin.clone();
2336        let (handle, result) = tokio::task::spawn_blocking(move || {
2337            let (disposition, result) = execute_standalone_batch(&handle.conn, &statements, origin);
2338            let retained = match disposition {
2339                BatchHandleDisposition::Retain => Some(handle),
2340                BatchHandleDisposition::Poison => None,
2341            };
2342            (retained, result)
2343        })
2344        .await
2345        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_batch", e))?;
2346        self.handle = handle;
2347        result.map_err(|failure| {
2348            crate::timeout_sink::maybe_emit_busy(
2349                &self.db,
2350                crate::timeout_sink::Site::StandaloneSqlBridge,
2351                &failure.error,
2352            );
2353            match failure.poison_reason {
2354                Some(poison_reason) => StorageError::driver(
2355                    StorageCapability::Sql,
2356                    "execute_batch",
2357                    PoisonedBatchError {
2358                        original: failure.error,
2359                        poison_reason,
2360                    },
2361                ),
2362                None => map_rusqlite_err(failure.error, "execute_batch"),
2363            }
2364        })
2365    }
2366
2367    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
2368        // ADR-067 Component A (Fork C slice 2): the script text is
2369        // self-contained (supplied up front, runs as one unit), just like
2370        // `execute_batch` — route it through the writer task when
2371        // available. `self.handle` is left untouched so a subsequent
2372        // `execute`/`execute_script` call on this same handle still works
2373        // over the standalone connection. Callers must supply a DML-only
2374        // script (no bare `BEGIN`/`COMMIT`/`ROLLBACK`) on the flag-on path,
2375        // since it runs inside the writer task's own transaction — same
2376        // Boundary: transaction-control rejection is an `execute_batch`
2377        // contract; this raw script path is internal/migration-only. The
2378        // queue-backed branch still requires a DML-only script because it
2379        // runs inside the writer task's transaction.
2380        if let Some(writer_task) = self.writer_task.clone() {
2381            return writer_task
2382                .send_bounded(move |conn| {
2383                    conn.execute_batch(&script)
2384                        .map_err(|e| map_rusqlite_err(e, "execute_script"))
2385                })
2386                .await;
2387        }
2388
2389        let handle = self.handle.take().ok_or_else(|| StorageError::Pool {
2390            operation: "execute_script".into(),
2391            message: "connection already consumed".into(),
2392        })?;
2393        let (handle, result) = tokio::task::spawn_blocking(move || {
2394            let res = handle.conn.execute_batch(&script);
2395            (handle, res)
2396        })
2397        .await
2398        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_script", e))?;
2399        self.handle = Some(handle);
2400        result.map_err(|e| {
2401            crate::timeout_sink::maybe_emit_busy(
2402                &self.db,
2403                crate::timeout_sink::Site::StandaloneSqlBridge,
2404                &e,
2405            );
2406            map_rusqlite_err(e, "execute_script")
2407        })
2408    }
2409
2410    async fn execute_script_top_level(
2411        &mut self,
2412        maintenance: TopLevelMaintenance,
2413    ) -> khive_storage::types::StorageResult<()> {
2414        // Only the closed maintenance enum can supply unbound SQL here.
2415        // This is not the separate raw migration-script interface.
2416        let script = maintenance.as_sql();
2417        // ADR-067 Component A: unlike
2418        // `execute_script`, this must NOT run inside the writer task's
2419        // per-request `BEGIN IMMEDIATE` — statements such as VACUUM are
2420        // rejected by SQLite inside any open transaction. Route through
2421        // `WriterTaskHandle::send_top_level`, which still serializes this
2422        // call through the single writer owner but skips the transaction
2423        // wrap entirely.
2424        if let Some(writer_task) = self.writer_task.clone() {
2425            return writer_task
2426                .send_top_level_bounded(move |conn| {
2427                    conn.execute_batch(script)
2428                        .map_err(|e| map_rusqlite_err(e, "execute_script_top_level"))
2429                })
2430                .await;
2431        }
2432
2433        // Flag off / no writer task: identical to `execute_script`'s own
2434        // flag-off path — a bare `execute_batch` on the standalone
2435        // connection, already transaction-free.
2436        let handle = self.handle.take().ok_or_else(|| StorageError::Pool {
2437            operation: "execute_script_top_level".into(),
2438            message: "connection already consumed".into(),
2439        })?;
2440        let (handle, result) = tokio::task::spawn_blocking(move || {
2441            let res = handle.conn.execute_batch(script);
2442            (handle, res)
2443        })
2444        .await
2445        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_script_top_level", e))?;
2446        self.handle = Some(handle);
2447        result.map_err(|e| {
2448            crate::timeout_sink::maybe_emit_busy(
2449                &self.db,
2450                crate::timeout_sink::Site::StandaloneSqlBridge,
2451                &e,
2452            );
2453            map_rusqlite_err(e, "execute_script_top_level")
2454        })
2455    }
2456}
2457
2458// =============================================================================
2459// Pool-backed reader/writer (in-memory databases)
2460// =============================================================================
2461
2462async fn run_pool_reader_query<T, F>(
2463    pool: Arc<ConnectionPool>,
2464    operation: &'static str,
2465    query: F,
2466) -> khive_storage::types::StorageResult<T>
2467where
2468    T: Send + 'static,
2469    F: FnOnce(
2470            &crate::read_cancellation::InterruptibleReadScope,
2471            &rusqlite::Connection,
2472        ) -> khive_storage::types::StorageResult<T>
2473        + Send
2474        + 'static,
2475{
2476    crate::read_cancellation::run_interruptible_read(
2477        StorageCapability::Sql,
2478        operation,
2479        move |scope| {
2480            // Checkout tri-state (cancelled -> Timeout, admission expiry ->
2481            // retryable AdmissionTimeout, other -> Driver) lives in ONE place:
2482            // `ConnectionPool::resolve_reader_checkout`.
2483            let mut guard = pool.resolve_reader_checkout(
2484                StorageCapability::Sql,
2485                operation,
2486                pool.reader_until(|| scope.should_stop()),
2487            )?;
2488            // Every caller of `run_pool_reader_query` runs a `SqlStatement`
2489            // (raw SQL) rather than a typed store's fixed query, so the
2490            // checkout pays the pristine-state scan on return regardless of
2491            // which `SqlReader` wrapper (reader or writer capability) drew it.
2492            guard.mark_dirty();
2493            scope.with_pooled_reader(&mut guard, |conn| query(scope, conn))
2494        },
2495    )
2496    .await
2497}
2498
2499async fn run_pool_writer_query<T, F>(
2500    pool: Arc<ConnectionPool>,
2501    operation: &'static str,
2502    query: F,
2503) -> khive_storage::types::StorageResult<T>
2504where
2505    T: Send + 'static,
2506    F: FnOnce(
2507            &crate::read_cancellation::InterruptibleReadScope,
2508            &rusqlite::Connection,
2509            bool,
2510        ) -> khive_storage::types::StorageResult<T>
2511        + Send
2512        + 'static,
2513{
2514    crate::read_cancellation::run_interruptible_read(
2515        StorageCapability::Sql,
2516        operation,
2517        move |scope| {
2518            let guard = pool.try_writer().map_err(|error: SqliteError| {
2519                StorageError::driver(StorageCapability::Sql, operation, error)
2520            })?;
2521            scope.with_pooled_writer(&pool, &guard, |conn| {
2522                let interruptible = conn.is_autocommit();
2523                query(scope, conn, interruptible)
2524            })
2525        },
2526    )
2527    .await
2528}
2529
2530struct PoolBackedReader {
2531    pool: Arc<ConnectionPool>,
2532    /// Present only while an admitted explicit deferred read transaction
2533    /// (ADR-005/ADR-091) is open on the in-memory backend's single shared
2534    /// connection. Retaining it here — instead of drawing a fresh checkout
2535    /// per call, the way an ordinary read does — is what gives the span
2536    /// real connection ownership: see [`SharedReaderTransactionGuard`] and
2537    /// [`run_pool_backed_reader_query`].
2538    transaction: Option<SharedReaderTransactionGuard>,
2539}
2540
2541/// Decide what happened to a [`SharedReaderTransactionGuard`] after one
2542/// statement ran against it and either retain it in `*transaction` (still
2543/// mid-span) or let it drop naturally (span finished cleanly).
2544///
2545/// `expect_open_after` is the caller's terminal-state expectation: `true`
2546/// for an ordinary read inside an already-open span (the transaction must
2547/// still be open afterward), `false` for the statement that opens or closes
2548/// the span. A mismatch poisons the guard — it is never handed back for
2549/// reuse — and is folded into the returned error so a broken span is never
2550/// reported as a successful read.
2551fn finish_pool_backed_reader_step<T>(
2552    transaction: &mut Option<SharedReaderTransactionGuard>,
2553    guard: SharedReaderTransactionGuard,
2554    expect_open_after: bool,
2555    operation: &'static str,
2556    result: khive_storage::types::StorageResult<T>,
2557) -> khive_storage::types::StorageResult<T> {
2558    let still_open = !guard.conn().is_autocommit();
2559    if still_open == expect_open_after {
2560        if still_open {
2561            *transaction = Some(guard);
2562        }
2563        // Otherwise the span finished cleanly (or never opened); let `guard`
2564        // drop here, returning the connection to the pool.
2565        return result;
2566    }
2567    guard.poison();
2568    let message = if expect_open_after {
2569        "a read inside the pool-backed reader's admitted transaction unexpectedly ended it; \
2570         the connection was discarded"
2571    } else {
2572        "transaction-ending control completed but the pool-backed reader's connection \
2573         remained outside autocommit; the connection was discarded"
2574    };
2575    match result {
2576        Err(error) => Err(error),
2577        Ok(_) => Err(StorageError::InvalidInput {
2578            capability: StorageCapability::Sql,
2579            operation: operation.into(),
2580            message: message.into(),
2581        }),
2582    }
2583}
2584
2585/// Open the explicit deferred read-transaction span on the in-memory
2586/// backend's shared connection and run the admitted `BEGIN DEFERRED`
2587/// statement against it.
2588async fn open_pool_backed_reader_transaction<T, F>(
2589    transaction: &mut Option<SharedReaderTransactionGuard>,
2590    pool: Arc<ConnectionPool>,
2591    operation: &'static str,
2592    query: F,
2593) -> khive_storage::types::StorageResult<T>
2594where
2595    T: Send + 'static,
2596    F: FnOnce(
2597            &crate::read_cancellation::InterruptibleReadScope,
2598            &rusqlite::Connection,
2599            bool,
2600            bool,
2601        ) -> khive_storage::types::StorageResult<T>
2602        + Send
2603        + 'static,
2604{
2605    let (guard, result) = crate::read_cancellation::run_interruptible_read(
2606        StorageCapability::Sql,
2607        operation,
2608        move |scope| {
2609            let Some(guard) = pool
2610                .checkout_shared_reader_transaction(|| scope.should_stop())
2611                .map_err(|error| StorageError::driver(StorageCapability::Sql, operation, error))?
2612            else {
2613                return Err(StorageError::Timeout {
2614                    operation: operation.into(),
2615                });
2616            };
2617            let result = query(scope, guard.conn(), false, true);
2618            if scope.cleanup_failed() {
2619                guard.poison();
2620            }
2621            Ok((guard, result))
2622        },
2623    )
2624    .await?;
2625    finish_pool_backed_reader_step(transaction, guard, true, operation, result)
2626}
2627
2628/// Route one raw-SQL call through the in-memory backend's `PoolBackedReader`.
2629/// An ordinary read with no open span falls through to the regular per-call
2630/// pooled checkout ([`run_pool_reader_query`]); every other combination
2631/// (opening, continuing, or closing the explicit deferred read-transaction
2632/// span) is handled here so the span retains one connection end to end.
2633#[allow(clippy::too_many_lines)]
2634async fn run_pool_backed_reader_query<T, F>(
2635    transaction: &mut Option<SharedReaderTransactionGuard>,
2636    pool: Arc<ConnectionPool>,
2637    operation: &'static str,
2638    transaction_control: Option<CachedReadTransactionControl>,
2639    query: F,
2640) -> khive_storage::types::StorageResult<T>
2641where
2642    T: Send + 'static,
2643    F: FnOnce(
2644            &crate::read_cancellation::InterruptibleReadScope,
2645            &rusqlite::Connection,
2646            bool,
2647            bool,
2648        ) -> khive_storage::types::StorageResult<T>
2649        + Send
2650        + 'static,
2651{
2652    if transaction.is_none() {
2653        return match transaction_control {
2654            None => {
2655                run_pool_reader_query(pool, operation, move |scope, conn| {
2656                    query(scope, conn, false, true)
2657                })
2658                .await
2659            }
2660            Some(CachedReadTransactionControl::Finish(keyword))
2661            | Some(CachedReadTransactionControl::Unsupported(keyword)) => {
2662                Err(StorageError::InvalidInput {
2663                    capability: StorageCapability::Sql,
2664                    operation: operation.into(),
2665                    message: format!(
2666                        "pool-backed reader has no admitted transaction for transaction \
2667                         control ({keyword})"
2668                    ),
2669                })
2670            }
2671            Some(CachedReadTransactionControl::BeginDeferred) => {
2672                open_pool_backed_reader_transaction(transaction, pool, operation, query).await
2673            }
2674        };
2675    }
2676
2677    match transaction_control {
2678        Some(CachedReadTransactionControl::BeginDeferred) => {
2679            return Err(StorageError::InvalidInput {
2680                capability: StorageCapability::Sql,
2681                operation: operation.into(),
2682                message: "pool-backed reader already owns an admitted read transaction; \
2683                          nested BEGIN is not supported"
2684                    .into(),
2685            });
2686        }
2687        Some(CachedReadTransactionControl::Unsupported(keyword)) => {
2688            return Err(StorageError::InvalidInput {
2689                capability: StorageCapability::Sql,
2690                operation: operation.into(),
2691                message: format!(
2692                    "pool-backed reader's admitted read transaction does not support nested \
2693                     or write-locking transaction control ({keyword})"
2694                ),
2695            });
2696        }
2697        None | Some(CachedReadTransactionControl::Finish(_)) => {}
2698    }
2699
2700    let expect_open_after = transaction_control.is_none();
2701    let guard = transaction.take().expect("checked Some above");
2702    let (guard, result) = crate::read_cancellation::run_interruptible_read(
2703        StorageCapability::Sql,
2704        operation,
2705        move |scope| {
2706            let result = query(scope, guard.conn(), true, true);
2707            if scope.cleanup_failed() {
2708                guard.poison();
2709            }
2710            Ok((guard, result))
2711        },
2712    )
2713    .await?;
2714    finish_pool_backed_reader_step(transaction, guard, expect_open_after, operation, result)
2715}
2716
2717#[async_trait]
2718impl khive_storage::SqlReader for PoolBackedReader {
2719    async fn query_row(
2720        &mut self,
2721        statement: SqlStatement,
2722    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
2723        let transaction_control = cached_read_transaction_control(&statement.sql);
2724        admit_reader_capability_sql(&statement, transaction_control, "pool_reader.query_row")?;
2725        let pool = Arc::clone(&self.pool);
2726        run_pool_backed_reader_query(
2727            &mut self.transaction,
2728            pool,
2729            "pool_reader.query_row",
2730            transaction_control,
2731            move |scope, conn, rollback, interruptible| {
2732                execute_query_row_interruptibly(
2733                    scope,
2734                    conn,
2735                    &statement,
2736                    "pool_reader.query_row",
2737                    rollback,
2738                    interruptible,
2739                )
2740            },
2741        )
2742        .await
2743    }
2744
2745    async fn query_all(
2746        &mut self,
2747        statement: SqlStatement,
2748    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2749        let transaction_control = cached_read_transaction_control(&statement.sql);
2750        admit_reader_capability_sql(&statement, transaction_control, "pool_reader.query_all")?;
2751        let pool = Arc::clone(&self.pool);
2752        run_pool_backed_reader_query(
2753            &mut self.transaction,
2754            pool,
2755            "pool_reader.query_all",
2756            transaction_control,
2757            move |scope, conn, rollback, interruptible| {
2758                execute_query_interruptibly(
2759                    scope,
2760                    conn,
2761                    &statement,
2762                    "pool_reader.query_all",
2763                    rollback,
2764                    interruptible,
2765                )
2766            },
2767        )
2768        .await
2769    }
2770
2771    async fn query_page(
2772        &mut self,
2773        statement: SqlStatement,
2774        page: PageRequest,
2775    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2776        let transaction_control = cached_read_transaction_control(&statement.sql);
2777        admit_reader_capability_sql(&statement, transaction_control, "pool_reader.query_page")?;
2778        let pool = Arc::clone(&self.pool);
2779        run_pool_backed_reader_query(
2780            &mut self.transaction,
2781            pool,
2782            "pool_reader.query_page",
2783            transaction_control,
2784            move |scope, conn, rollback, interruptible| {
2785                execute_query_page_interruptibly(
2786                    scope,
2787                    conn,
2788                    &statement,
2789                    &page,
2790                    "pool_reader.query_page",
2791                    rollback,
2792                    interruptible,
2793                )
2794            },
2795        )
2796        .await
2797    }
2798
2799    async fn query_scalar(
2800        &mut self,
2801        statement: SqlStatement,
2802    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
2803        let row = self.query_row(statement).await?;
2804        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
2805    }
2806
2807    async fn explain(
2808        &mut self,
2809        statement: SqlStatement,
2810    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2811        let explain_stmt = SqlStatement {
2812            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
2813            params: statement.params,
2814            label: statement.label,
2815        };
2816        self.query_all(explain_stmt).await
2817    }
2818}
2819
2820struct PoolBackedWriter {
2821    pool: Arc<ConnectionPool>,
2822}
2823
2824#[async_trait]
2825impl khive_storage::SqlReader for PoolBackedWriter {
2826    async fn query_row(
2827        &mut self,
2828        statement: SqlStatement,
2829    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
2830        let pool = Arc::clone(&self.pool);
2831        run_pool_writer_query(
2832            pool,
2833            "pool_writer.query_row",
2834            move |scope, conn, interruptible| {
2835                execute_query_row_interruptibly(
2836                    scope,
2837                    conn,
2838                    &statement,
2839                    "pool_writer.query_row",
2840                    false,
2841                    interruptible,
2842                )
2843            },
2844        )
2845        .await
2846    }
2847
2848    async fn query_all(
2849        &mut self,
2850        statement: SqlStatement,
2851    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2852        let pool = Arc::clone(&self.pool);
2853        run_pool_writer_query(
2854            pool,
2855            "pool_writer.query_all",
2856            move |scope, conn, interruptible| {
2857                execute_query_interruptibly(
2858                    scope,
2859                    conn,
2860                    &statement,
2861                    "pool_writer.query_all",
2862                    false,
2863                    interruptible,
2864                )
2865            },
2866        )
2867        .await
2868    }
2869
2870    async fn query_page(
2871        &mut self,
2872        statement: SqlStatement,
2873        page: PageRequest,
2874    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2875        let pool = Arc::clone(&self.pool);
2876        run_pool_writer_query(
2877            pool,
2878            "pool_writer.query_page",
2879            move |scope, conn, interruptible| {
2880                execute_query_page_interruptibly(
2881                    scope,
2882                    conn,
2883                    &statement,
2884                    &page,
2885                    "pool_writer.query_page",
2886                    false,
2887                    interruptible,
2888                )
2889            },
2890        )
2891        .await
2892    }
2893
2894    async fn query_scalar(
2895        &mut self,
2896        statement: SqlStatement,
2897    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
2898        let row = khive_storage::SqlReader::query_row(self, statement).await?;
2899        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
2900    }
2901
2902    async fn explain(
2903        &mut self,
2904        statement: SqlStatement,
2905    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
2906        let explain_stmt = SqlStatement {
2907            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
2908            params: statement.params,
2909            label: statement.label,
2910        };
2911        khive_storage::SqlReader::query_all(self, explain_stmt).await
2912    }
2913}
2914
2915#[async_trait]
2916impl khive_storage::SqlWriter for PoolBackedWriter {
2917    async fn execute(
2918        &mut self,
2919        statement: SqlStatement,
2920    ) -> khive_storage::types::StorageResult<u64> {
2921        // Boundary: `execute_batch` owns transaction-control rejection;
2922        // this one-statement primitive is used by internal DML/transaction
2923        // owners and is still guarded by the SqlStatement single-statement
2924        // prepare contract.
2925        let pool = Arc::clone(&self.pool);
2926        tokio::task::spawn_blocking(move || {
2927            let guard = pool.try_writer().map_err(|e: SqliteError| {
2928                StorageError::driver(StorageCapability::Sql, "pool_writer.execute", e)
2929            })?;
2930            let mut stmt = prepare_cached_sql_statement(&guard, &statement.sql)
2931                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
2932            bind_params(&mut stmt, &statement.params)
2933                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
2934            let rows = stmt
2935                .raw_execute()
2936                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
2937            Ok(rows as u64)
2938        })
2939        .await
2940        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.execute", e))?
2941    }
2942
2943    async fn execute_batch(
2944        &mut self,
2945        statements: Vec<SqlStatement>,
2946    ) -> khive_storage::types::StorageResult<u64> {
2947        // Same all-or-nothing contract as the file-backed path: this batch
2948        // wraps its list in its own `BEGIN IMMEDIATE`, so reject caller
2949        // transaction-control statements before executing anything.
2950        reject_transaction_control_statements(&statements, "pool_writer.execute_batch")?;
2951        let pool = Arc::clone(&self.pool);
2952        tokio::task::spawn_blocking(move || {
2953            let guard = pool.try_writer().map_err(|e: SqliteError| {
2954                StorageError::driver(StorageCapability::Sql, "pool_writer.execute_batch", e)
2955            })?;
2956            let prepared = prepare_batch_statements(&guard, &statements)
2957                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?;
2958            guard
2959                .execute_batch("BEGIN IMMEDIATE")
2960                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?;
2961            let _tx_handle = khive_storage::tx_registry::register_scoped(
2962                Some("pool_writer.execute_batch".to_string()),
2963                pool.origin(),
2964            );
2965            let result = execute_prepared_batch(&guard, prepared, &statements)
2966                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"));
2967            match result {
2968                Ok(total) => {
2969                    if let Err(e) = guard.execute_batch("COMMIT") {
2970                        let _ = guard.execute_batch("ROLLBACK");
2971                        Err(map_rusqlite_err(e, "pool_writer.execute_batch"))
2972                    } else {
2973                        Ok(total)
2974                    }
2975                }
2976                Err(e) => {
2977                    let _ = guard.execute_batch("ROLLBACK");
2978                    Err(e)
2979                }
2980            }
2981        })
2982        .await
2983        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.execute_batch", e))?
2984    }
2985
2986    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
2987        // Boundary: raw scripts are internal/migration-only and do not inherit
2988        // `execute_batch`'s transaction-control rejection.
2989        let pool = Arc::clone(&self.pool);
2990        tokio::task::spawn_blocking(move || {
2991            let guard = pool.try_writer().map_err(|e: SqliteError| {
2992                StorageError::driver(StorageCapability::Sql, "pool_writer.execute_script", e)
2993            })?;
2994            guard
2995                .execute_batch(&script)
2996                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_script"))
2997        })
2998        .await
2999        .map_err(|e| {
3000            StorageError::driver(StorageCapability::Sql, "pool_writer.execute_script", e)
3001        })?
3002    }
3003}
3004
3005// =============================================================================
3006// atomic_unit (ADR-067 Component A, Fork C slice 2)
3007// =============================================================================
3008
3009/// A purely-synchronous `SqlReader`/`SqlWriter` over a borrowed connection,
3010/// used to drive an [`AtomicUnitOp`] on the queued or in-memory path, where the
3011/// closure body runs inside a `spawn_blocking` (synchronous
3012/// `FnOnce(&rusqlite::Connection) -> ...`) rather than a real async context.
3013///
3014/// Every method here does plain, non-suspending rusqlite work — there is no
3015/// real `.await` point anywhere in this impl — so [`block_on_sync`] driving
3016/// the resulting future to completion with a single poll is sound, not a
3017/// hack: the future can never actually be `Pending`.
3018///
3019/// `SqlReader`/`SqlWriter` both carry a `'static` supertrait bound (they are
3020/// used as `Box<dyn ...>` elsewhere in this module), so this type cannot
3021/// hold a real `&'c Connection` borrow — it would tie `InlineWriter` to a
3022/// non-`'static` lifetime and, independently, `&Connection` is not `Send`
3023/// (`Connection` is `!Sync`), which the `#[async_trait]`-generated futures
3024/// require. A raw pointer sidesteps both: `*const Connection` is `Send` and
3025/// `'static` on its face, and the safety burden (the pointee outliving
3026/// every dereference) is upheld by construction — see `atomic_unit`, the
3027/// only call site: it builds an `InlineWriter` from `conn: &Connection`,
3028/// drives `op` to completion via `block_on_sync` synchronously, and drops
3029/// the `InlineWriter` before that borrow ends, all within one stack frame.
3030struct InlineWriter {
3031    conn: *const rusqlite::Connection,
3032}
3033
3034// SAFETY: `InlineWriter` is never actually shared across a real thread
3035// boundary — it is constructed, driven to completion synchronously via
3036// `block_on_sync`, and dropped within a single call frame inside the
3037// `spawn_blocking` closure (see `atomic_unit`). The `Send`
3038// bound `async_trait` imposes on the futures below is a static
3039// over-approximation for this restricted, single-threaded usage pattern.
3040unsafe impl Send for InlineWriter {}
3041
3042impl InlineWriter {
3043    /// SAFETY: valid for the lifetime of the enclosing synchronous scope in
3044    /// `atomic_unit` (see the struct doc comment above) — the pointee is
3045    /// never dereferenced after that scope ends.
3046    fn conn(&self) -> &rusqlite::Connection {
3047        unsafe { &*self.conn }
3048    }
3049}
3050
3051#[async_trait]
3052impl khive_storage::SqlReader for InlineWriter {
3053    async fn query_row(
3054        &mut self,
3055        statement: SqlStatement,
3056    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
3057        execute_query_row(self.conn(), &statement)
3058            .map_err(|e| map_rusqlite_err(e, "inline.query_row"))
3059    }
3060
3061    async fn query_all(
3062        &mut self,
3063        statement: SqlStatement,
3064    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
3065        execute_query(self.conn(), &statement).map_err(|e| map_rusqlite_err(e, "inline.query_all"))
3066    }
3067
3068    async fn query_page(
3069        &mut self,
3070        statement: SqlStatement,
3071        page: PageRequest,
3072    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
3073        execute_query_page(self.conn(), &statement, &page)
3074            .map_err(|e| map_rusqlite_err(e, "inline.query_page"))
3075    }
3076
3077    async fn query_scalar(
3078        &mut self,
3079        statement: SqlStatement,
3080    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
3081        let row = khive_storage::SqlReader::query_row(self, statement).await?;
3082        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
3083    }
3084
3085    async fn explain(
3086        &mut self,
3087        statement: SqlStatement,
3088    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
3089        let explain_stmt = SqlStatement {
3090            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
3091            params: statement.params,
3092            label: statement.label,
3093        };
3094        khive_storage::SqlReader::query_all(self, explain_stmt).await
3095    }
3096}
3097
3098#[async_trait]
3099impl khive_storage::SqlWriter for InlineWriter {
3100    async fn execute(
3101        &mut self,
3102        statement: SqlStatement,
3103    ) -> khive_storage::types::StorageResult<u64> {
3104        // Boundary: `execute_batch` owns transaction-control rejection;
3105        // `atomic_unit` uses this one-statement primitive for its own boundary.
3106        let mut stmt = prepare_cached_sql_statement(self.conn(), &statement.sql)
3107            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
3108        bind_params(&mut stmt, &statement.params)
3109            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
3110        let affected = stmt
3111            .raw_execute()
3112            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
3113        Ok(affected as u64)
3114    }
3115
3116    async fn execute_batch(
3117        &mut self,
3118        statements: Vec<SqlStatement>,
3119    ) -> khive_storage::types::StorageResult<u64> {
3120        // Runs inside the writer task's per-request `BEGIN IMMEDIATE`
3121        // (atomic_unit flag-on path), so a caller `COMMIT` would close the
3122        // task's transaction — reject transaction-control statements up
3123        // front, same contract as every other `execute_batch`.
3124        reject_transaction_control_statements(&statements, "inline.execute_batch")?;
3125        let prepared = prepare_batch_statements(self.conn(), &statements)
3126            .map_err(|e| map_rusqlite_err(e, "inline.execute_batch"))?;
3127        execute_prepared_batch(self.conn(), prepared, &statements)
3128            .map_err(|e| map_rusqlite_err(e, "inline.execute_batch"))
3129    }
3130
3131    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
3132        // Boundary: this raw script path is internal maintenance only and is
3133        // outside the `execute_batch` transaction-control contract.
3134        self.conn()
3135            .execute_batch(&script)
3136            .map_err(|e| map_rusqlite_err(e, "inline.execute_script"))
3137    }
3138}
3139
3140/// Poll `fut` exactly once with a no-op waker and return its output.
3141///
3142/// Only sound for futures that never actually suspend — every caller in
3143/// this module drives an [`InlineWriter`], whose methods are pure
3144/// synchronous rusqlite calls with no real `.await` point.
3145///
3146/// ADR-067 Component A: this used to
3147/// `unreachable!()`-panic on `Poll::Pending`, and a panicking closure
3148/// running inside the writer task's `spawn_blocking` (see
3149/// `SqlBridge::atomic_unit`'s flag-on branch) would surface as a
3150/// `JoinError` in `run_writer_task`, which is treated as fatal — the writer
3151/// task exits and every subsequent `WriterTaskHandle::send` on this pool
3152/// fails for the rest of the process. A future `atomic_unit` caller whose
3153/// closure ever gains a real suspend point (this file's own contract
3154/// already forbids it, but the invariant is enforced by convention, not the
3155/// type system) would take down the writer task for the whole daemon.
3156/// Returning `Err` instead lets `Pending` flow through the SAME error path
3157/// as any other `atomic_unit` op failure: `WriteRequest::execute_and_reply`
3158/// treats it as an ordinary `Err`, issues `ROLLBACK` on the writer task's
3159/// held transaction, replies the error to the caller, and the writer task's
3160/// `spawn_blocking` closure returns normally (not via panic) — so the task
3161/// keeps draining subsequent requests instead of dying with the whole pool.
3162fn block_on_sync<F: std::future::Future>(fut: F) -> Result<F::Output, StorageError> {
3163    use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
3164
3165    fn no_op(_: *const ()) {}
3166    fn clone_waker(_: *const ()) -> RawWaker {
3167        RawWaker::new(std::ptr::null(), &VTABLE)
3168    }
3169    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_waker, no_op, no_op, no_op);
3170
3171    // SAFETY: every `RawWakerVTable` function is a no-op that never
3172    // dereferences the data pointer, so a null data pointer is sound.
3173    let raw_waker = RawWaker::new(std::ptr::null(), &VTABLE);
3174    let waker = unsafe { Waker::from_raw(raw_waker) };
3175    let mut cx = Context::from_waker(&waker);
3176
3177    let mut fut = std::pin::pin!(fut);
3178    match fut.as_mut().poll(&mut cx) {
3179        Poll::Ready(v) => Ok(v),
3180        Poll::Pending => {
3181            tracing::error!(
3182                "block_on_sync: atomic_unit future suspended on its first poll — \
3183                 the closure passed to SqlAccess::atomic_unit must be non-blocking \
3184                 (synchronous InlineWriter calls only, no real .await point)"
3185            );
3186            Err(StorageError::Internal(
3187                "atomic_unit future suspended — closure must be non-blocking".to_string(),
3188            ))
3189        }
3190    }
3191}
3192
3193/// Run `op` under a manual `BEGIN IMMEDIATE`/`COMMIT`/`ROLLBACK` on `writer`
3194/// — the pre-ADR-067 shape, used by [`SqlBridge::atomic_unit`] whenever no
3195/// writer task applies to a file-backed pool.
3196async fn run_manual_atomic_unit(
3197    writer: &mut dyn khive_storage::SqlWriter,
3198    op: AtomicUnitOp,
3199    origin: khive_storage::tx_registry::TxOrigin,
3200) -> khive_storage::types::StorageResult<Box<dyn Any + Send>> {
3201    fn tx_stmt(sql: &str, label: &str) -> SqlStatement {
3202        SqlStatement {
3203            sql: sql.to_string(),
3204            params: vec![],
3205            label: Some(label.to_string()),
3206        }
3207    }
3208    khive_storage::SqlWriter::execute(writer, tx_stmt("BEGIN IMMEDIATE", "begin")).await?;
3209    let _tx_handle =
3210        khive_storage::tx_registry::register_scoped(Some("atomic_unit".to_string()), origin);
3211
3212    let result = op(writer).await;
3213
3214    match result {
3215        Ok(value) => {
3216            match khive_storage::SqlWriter::execute(writer, tx_stmt("COMMIT", "commit")).await {
3217                Ok(_) => Ok(value),
3218                Err(e) => {
3219                    let _ =
3220                        khive_storage::SqlWriter::execute(writer, tx_stmt("ROLLBACK", "rollback"))
3221                            .await;
3222                    Err(e)
3223                }
3224            }
3225        }
3226        Err(e) => {
3227            let _ =
3228                khive_storage::SqlWriter::execute(writer, tx_stmt("ROLLBACK", "rollback")).await;
3229            Err(e)
3230        }
3231    }
3232}
3233
3234// =============================================================================
3235// SqlBridge: the SqlAccess implementor
3236// =============================================================================
3237
3238/// Bridges `ConnectionPool` to `khive_storage::SqlAccess`.
3239///
3240/// Dispatches based on whether the pool is file-backed or in-memory:
3241/// - File-backed: pooled ordinary reader operations and a lazy standalone
3242///   connection only for explicitly admitted multi-call read transactions,
3243///   plus standalone writer connections capped at one live handle; atomic
3244///   units drive a single registered raw transaction span.
3245/// - In-memory: pool-backed connections per query (single shared connection).
3246pub struct SqlBridge {
3247    pool: Arc<ConnectionPool>,
3248    is_file_backed: bool,
3249}
3250
3251impl SqlBridge {
3252    /// Create a new bridge wrapping the given pool.
3253    pub fn new(pool: Arc<ConnectionPool>, is_file_backed: bool) -> Self {
3254        Self {
3255            pool,
3256            is_file_backed,
3257        }
3258    }
3259}
3260
3261#[async_trait]
3262impl khive_storage::SqlAccess for SqlBridge {
3263    fn database_path(&self) -> Option<std::path::PathBuf> {
3264        self.pool.canonical_path().map(std::path::Path::to_path_buf)
3265    }
3266
3267    async fn reader(
3268        &self,
3269    ) -> khive_storage::types::StorageResult<Box<dyn khive_storage::SqlReader>> {
3270        if self.is_file_backed {
3271            Ok(Box::new(SqliteReader {
3272                handle: None,
3273                pool: Arc::clone(&self.pool),
3274                poisoned: false,
3275            }))
3276        } else {
3277            Ok(Box::new(PoolBackedReader {
3278                pool: Arc::clone(&self.pool),
3279                transaction: None,
3280            }))
3281        }
3282    }
3283
3284    async fn writer(
3285        &self,
3286    ) -> khive_storage::types::StorageResult<Box<dyn khive_storage::SqlWriter>> {
3287        if self.is_file_backed {
3288            if self.pool.config().read_only {
3289                return Err(StorageError::Pool {
3290                    operation: "writer".into(),
3291                    message: "backend is read-only".into(),
3292                });
3293            }
3294            let db = crate::timeout_sink::db_label(&self.pool);
3295            // ADR-136 D1 gate 1: queue-first. The handle lookup runs BEFORE
3296            // any standalone connection is opened, and a lookup failure is
3297            // propagated (never silently degraded) when strict routing is
3298            // on. Only the flag-off/degraded case still opens a standalone
3299            // connection.
3300            let writer_task = match self.pool.writer_task_handle() {
3301                Ok(handle) => handle,
3302                Err(e) => {
3303                    if self.pool.config().write_routing_strict {
3304                        return Err(e);
3305                    }
3306                    tracing::warn!(
3307                        error = %e,
3308                        "KHIVE_WRITE_ROUTING is not strict; writer() degrades to the \
3309                         standalone-connection path"
3310                    );
3311                    None
3312                }
3313            };
3314            if writer_task.is_none() && self.pool.config().write_routing_strict {
3315                return Err(StorageError::Pool {
3316                    operation: "writer".into(),
3317                    message: "KHIVE_WRITE_ROUTING=strict but no writer-task handle is \
3318                              available; refusing to fall back to a direct connection"
3319                        .into(),
3320                });
3321            }
3322            if writer_task.is_none() && self.pool.write_queue_active() {
3323                // The queue is enabled but this call didn't get a handle
3324                // (spawn/runtime degrade) — a direct-route violation in the
3325                // making once this writer's execute*/query* methods run.
3326                // In-memory pools are excluded: they never spawn a writer
3327                // task by documented design (explicit `Some(true)` degrades),
3328                // so a violation row there would be noise, not signal.
3329                crate::timeout_sink::emit_direct_route_violation(
3330                    &db,
3331                    crate::timeout_sink::Site::DirectRouteSqlBridgeWriter,
3332                );
3333            }
3334            // A standalone read-write connection is opened only when there is
3335            // no queue handle to route writes through — `SqliteWriter`'s
3336            // `SqlReader` methods (`query_row`/`query_all`/`query_page`)
3337            // use pooled readers in the queue-backed case and lazily open the
3338            // closed standalone exception only for an explicit deferred read
3339            // transaction. Production callers do read through a `writer()`
3340            // handle, so both routes are live.
3341            // The standalone open acquires the pool-wide one-permit writer
3342            // budget first, and the permit travels in the handle for the
3343            // handle's whole lifetime — a queue-backed handle holds no
3344            // writer permit (its writes route through the writer task), so
3345            // this budget caps exactly the standalone read-write
3346            // connections.
3347            let handle = if writer_task.is_none() {
3348                let handle_slot = acquire_handle_slot(
3349                    self.pool.sql_bridge_writer_slots(),
3350                    self.pool.config().checkout_timeout,
3351                    "sql_bridge.writer_handle",
3352                    SlotTimeoutClass::Admission,
3353                )
3354                .await?;
3355                let (conn, handle_slot) =
3356                    open_standalone_writer_on_blocking(Arc::clone(&self.pool), handle_slot).await?;
3357                Some(StandaloneHandle {
3358                    conn,
3359                    _retained_slot: Some(handle_slot),
3360                    read_transaction_slot: None,
3361                })
3362            } else {
3363                None
3364            };
3365            Ok(Box::new(SqliteWriter {
3366                handle,
3367                writer_task,
3368                origin: self.pool.origin(),
3369                db,
3370                pool: Arc::clone(&self.pool),
3371            }))
3372        } else {
3373            Ok(Box::new(PoolBackedWriter {
3374                pool: Arc::clone(&self.pool),
3375            }))
3376        }
3377    }
3378
3379    /// Implements the trait's atomic-unit suspend-free invariant
3380    /// (`SqlAccess::atomic_unit`'s doc comment): on the queued and in-memory branches,
3381    /// `op` is driven through `block_on_sync` on an `InlineWriter` — a
3382    /// single-poll driver that returns `Err` the instant `op`'s future is
3383    /// `Pending` instead of ever actually suspending. `op` must therefore
3384    /// issue only synchronous DML; see `InlineWriter`'s and
3385    /// `block_on_sync`'s doc comments for the full mechanics and why this
3386    /// restriction is load-bearing (a suspended poll inside the writer
3387    /// task's `spawn_blocking` would otherwise block that task on external
3388    /// async work while holding the single write connection).
3389    async fn atomic_unit(
3390        &self,
3391        op: AtomicUnitOp,
3392    ) -> khive_storage::types::StorageResult<Box<dyn Any + Send>> {
3393        if self.is_file_backed {
3394            if self.pool.config().read_only {
3395                return Err(StorageError::Pool {
3396                    operation: "atomic_unit".into(),
3397                    message: "backend is read-only".into(),
3398                });
3399            }
3400            // Best-effort, same guard `writer()` uses: `Ok(None)` on flag-off;
3401            // `Err(WriterTaskNoRuntime)` propagates loud rather than silently
3402            // falling back to a competing connection from a sync caller. ADR-136
3403            // D1 gate 3: `Ok(None)` under strict routing is ALSO a fail-closed
3404            // error (queue was requested but unavailable), not just a degrade.
3405            let handle = self.pool.writer_task_handle()?;
3406            if handle.is_none() && self.pool.config().write_routing_strict {
3407                return Err(StorageError::Pool {
3408                    operation: "atomic_unit".into(),
3409                    message: "KHIVE_WRITE_ROUTING=strict but no writer-task handle is \
3410                              available; refusing to fall back to a direct connection"
3411                        .into(),
3412                });
3413            }
3414            if handle.is_none() && self.pool.write_queue_active() {
3415                crate::timeout_sink::emit_direct_route_violation(
3416                    &crate::timeout_sink::db_label(&self.pool),
3417                    crate::timeout_sink::Site::DirectRouteAtomicUnit,
3418                );
3419            }
3420            if let Some(writer_task) = handle {
3421                // Flag-on: ONE queued WriteRequest. `run_writer_task` already
3422                // has an open `BEGIN IMMEDIATE` on its dedicated connection
3423                // before this closure runs and issues `COMMIT`/`ROLLBACK`
3424                // after it returns — `op` must not (and, via `InlineWriter`,
3425                // does not) issue its own transaction control.
3426                return writer_task
3427                    .send_bounded(move |conn| {
3428                        let mut inline = InlineWriter {
3429                            conn: conn as *const rusqlite::Connection,
3430                        };
3431                        // Flatten: `block_on_sync` now returns `Result<F::Output,
3432                        // StorageError>` (outer = "did the future actually
3433                        // resolve on first poll", inner = the op's own
3434                        // `StorageResult`) instead of panicking on `Pending`
3435                        // (ADR-067 Component A). Either
3436                        // error flows through this closure's ordinary `Err`
3437                        // return, which `WriteRequest::execute_and_reply`
3438                        // already turns into a normal ROLLBACK + error reply —
3439                        // no panic, so the writer task survives.
3440                        match block_on_sync(op(&mut inline)) {
3441                            Ok(inner) => inner,
3442                            Err(e) => Err(e),
3443                        }
3444                    })
3445                    .await;
3446            }
3447            // Flag-off (or no writer task available): manual
3448            // BEGIN IMMEDIATE/COMMIT/ROLLBACK on a standalone writer —
3449            // byte-for-byte the pre-ADR-067 shape.
3450            //
3451            // Contract: this acquire waits on the pool-wide one-permit
3452            // writer-handle budget — the same permit a live `writer()` handle
3453            // holds for its lifetime — so it times out with
3454            // `StorageError::AdmissionTimeout` after `checkout_timeout` while a writer
3455            // handle is checked out (and a `writer()` call times out while
3456            // this unit runs). Callers must not hold a boxed writer handle
3457            // across an `atomic_unit()` call on the same pool; drop the
3458            // handle first. The `writer_task` branch above never touches this
3459            // budget.
3460            let handle_slot = acquire_handle_slot(
3461                self.pool.sql_bridge_writer_slots(),
3462                self.pool.config().checkout_timeout,
3463                "sql_bridge.atomic_unit_handle",
3464                SlotTimeoutClass::Admission,
3465            )
3466            .await?;
3467            let (conn, handle_slot) =
3468                open_standalone_writer_on_blocking(Arc::clone(&self.pool), handle_slot).await?;
3469            let mut writer = SqliteWriter {
3470                handle: Some(StandaloneHandle {
3471                    conn,
3472                    _retained_slot: Some(handle_slot),
3473                    read_transaction_slot: None,
3474                }),
3475                writer_task: None,
3476                origin: self.pool.origin(),
3477                db: crate::timeout_sink::db_label(&self.pool),
3478                pool: Arc::clone(&self.pool),
3479            };
3480            run_manual_atomic_unit(&mut writer, op, self.pool.origin()).await
3481        } else {
3482            // Every statement shares one connection. Keep its guard through
3483            // commit/rollback so other units and ordinary writes cannot join it.
3484            let pool = Arc::clone(&self.pool);
3485            tokio::task::spawn_blocking(move || {
3486                let guard = pool.try_writer().map_err(|error: SqliteError| {
3487                    StorageError::driver(StorageCapability::Sql, "atomic_unit", error)
3488                })?;
3489                let conn = guard.conn();
3490                if !conn.is_autocommit() {
3491                    pool.retire_pooled_writer(conn);
3492                    return Err(StorageError::WriterTaskTerminated {
3493                        request_state: khive_storage::WriterTaskRequestState::SideEffectsUnknown,
3494                    });
3495                }
3496                if let Err(error) = conn.execute_batch("BEGIN IMMEDIATE") {
3497                    if !conn.is_autocommit() {
3498                        pool.retire_pooled_writer(conn);
3499                        return Err(StorageError::WriterTaskTerminated {
3500                            request_state:
3501                                khive_storage::WriterTaskRequestState::SideEffectsUnknown,
3502                        });
3503                    }
3504                    return Err(map_rusqlite_err(error, "atomic_unit.begin"));
3505                }
3506                let _tx_handle = khive_storage::tx_registry::register_scoped(
3507                    Some("atomic_unit".to_string()),
3508                    pool.origin(),
3509                );
3510                let (result, terminal_state) = crate::writer_task::execute_wrapped_transaction(
3511                    conn,
3512                    "atomic_unit.commit",
3513                    |conn| {
3514                        let mut inline = InlineWriter {
3515                            conn: conn as *const rusqlite::Connection,
3516                        };
3517                        block_on_sync(op(&mut inline)).and_then(|result| result)
3518                    },
3519                );
3520                if terminal_state.is_some() {
3521                    pool.retire_pooled_writer(conn);
3522                }
3523                result
3524            })
3525            .await
3526            .map_err(|error| StorageError::driver(StorageCapability::Sql, "atomic_unit", error))?
3527        }
3528    }
3529}
3530
3531#[cfg(test)]
3532mod tests {
3533    use super::*;
3534    use crate::pool::PoolConfig;
3535    use khive_storage::types::{SqlStatement, SqlValue};
3536    use khive_storage::{SqlAccess as _, SqlReader as _};
3537
3538    #[tokio::test]
3539    async fn in_memory_atomic_unit_pending_future_rolls_back_and_remains_usable() {
3540        let pool = Arc::new(
3541            ConnectionPool::new(PoolConfig {
3542                path: None,
3543                write_queue_enabled: Some(false),
3544                ..PoolConfig::default()
3545            })
3546            .unwrap(),
3547        );
3548        pool.writer()
3549            .unwrap()
3550            .conn()
3551            .execute_batch("CREATE TABLE atomic_pending (id INTEGER PRIMARY KEY)")
3552            .unwrap();
3553        let bridge = SqlBridge::new(Arc::clone(&pool), false);
3554        let inserted = Arc::new(std::sync::atomic::AtomicBool::new(false));
3555        let op_inserted = Arc::clone(&inserted);
3556        let op: AtomicUnitOp = Box::new(move |writer| {
3557            Box::pin(async move {
3558                writer
3559                    .execute(SqlStatement {
3560                        sql: "INSERT INTO atomic_pending VALUES (1)".into(),
3561                        params: vec![],
3562                        label: None,
3563                    })
3564                    .await?;
3565                op_inserted.store(true, std::sync::atomic::Ordering::SeqCst);
3566                std::future::pending::<khive_storage::types::StorageResult<Box<dyn Any + Send>>>()
3567                    .await
3568            })
3569        });
3570        let result =
3571            tokio::time::timeout(std::time::Duration::from_secs(10), bridge.atomic_unit(op)).await;
3572        assert!(inserted.load(std::sync::atomic::Ordering::SeqCst));
3573        let error = result
3574            .expect("in-memory atomic_unit must reject Pending promptly, not await it forever")
3575            .expect_err("a suspending atomic unit must fail");
3576        assert!(error.to_string().contains("future suspended"), "{error}");
3577        {
3578            let guard = pool.writer().unwrap();
3579            assert!(guard.conn().is_autocommit());
3580            let count: i64 = guard
3581                .conn()
3582                .query_row("SELECT COUNT(*) FROM atomic_pending", [], |row| row.get(0))
3583                .unwrap();
3584            assert_eq!(count, 0, "SQL before Pending must roll back");
3585        }
3586        let next: AtomicUnitOp = Box::new(|writer| {
3587            Box::pin(async move {
3588                writer
3589                    .execute(SqlStatement {
3590                        sql: "INSERT INTO atomic_pending VALUES (2)".into(),
3591                        params: vec![],
3592                        label: None,
3593                    })
3594                    .await?;
3595                Ok(Box::new(()) as Box<dyn Any + Send>)
3596            })
3597        });
3598        tokio::time::timeout(std::time::Duration::from_secs(10), bridge.atomic_unit(next))
3599            .await
3600            .expect("unit admission must remain usable")
3601            .unwrap();
3602        let sum: i64 = pool
3603            .writer()
3604            .unwrap()
3605            .conn()
3606            .query_row("SELECT SUM(id) FROM atomic_pending", [], |row| row.get(0))
3607            .unwrap();
3608        assert_eq!(sum, 2);
3609    }
3610
3611    // Pause SQLite itself during the first INSERT preparation so tests can
3612    // arrange contention without putting an async wait in an AtomicUnitOp.
3613    fn pause_first_insert(
3614        pool: &ConnectionPool,
3615        table: &'static str,
3616    ) -> (
3617        tokio::sync::oneshot::Receiver<()>,
3618        std::sync::mpsc::Sender<()>,
3619    ) {
3620        use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
3621
3622        let (entered, in_statement) = tokio::sync::oneshot::channel();
3623        let (release, released) = std::sync::mpsc::channel();
3624        let mut entered = Some(entered);
3625        pool.writer()
3626            .unwrap()
3627            .conn()
3628            .authorizer(Some(move |ctx: AuthContext<'_>| {
3629                if matches!(ctx.action, AuthAction::Insert { table_name } if table_name == table) {
3630                    if let Some(entered) = entered.take() {
3631                        entered.send(()).unwrap();
3632                        released
3633                            .recv_timeout(std::time::Duration::from_secs(5))
3634                            .expect("test must release the SQLite statement");
3635                    }
3636                }
3637                Authorization::Allow
3638            }))
3639            .unwrap();
3640        (in_statement, release)
3641    }
3642
3643    #[tokio::test]
3644    async fn in_memory_atomic_units_serialize_across_bridges() {
3645        let pool = Arc::new(
3646            ConnectionPool::new(PoolConfig {
3647                path: None,
3648                write_queue_enabled: Some(false),
3649                ..PoolConfig::default()
3650            })
3651            .unwrap(),
3652        );
3653        pool.writer()
3654            .unwrap()
3655            .conn()
3656            .execute_batch("CREATE TABLE atomic_in_memory (id INTEGER PRIMARY KEY)")
3657            .unwrap();
3658
3659        let (in_statement, release) = pause_first_insert(&pool, "atomic_in_memory");
3660        let ready = Arc::new(tokio::sync::Barrier::new(8));
3661        let mut jobs = Vec::new();
3662        for unit in 0..8_i64 {
3663            let pool = Arc::clone(&pool);
3664            let ready = Arc::clone(&ready);
3665            jobs.push(tokio::spawn(async move {
3666                // Separate bridges must share the pool's transaction budget.
3667                let bridge = SqlBridge::new(pool, false);
3668                ready.wait().await;
3669                let op: AtomicUnitOp = Box::new(move |writer| {
3670                    Box::pin(async move {
3671                        for row in 0..2 {
3672                            writer
3673                                .execute(SqlStatement {
3674                                    sql: "INSERT INTO atomic_in_memory (id) VALUES (?1)".into(),
3675                                    params: vec![SqlValue::Integer(unit * 2 + row)],
3676                                    label: None,
3677                                })
3678                                .await?;
3679                        }
3680                        Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
3681                    })
3682                });
3683                bridge.atomic_unit(op).await.map(|_| ())
3684            }));
3685        }
3686        let entered = tokio::time::timeout(std::time::Duration::from_secs(10), in_statement).await;
3687        if !matches!(&entered, Ok(Ok(()))) {
3688            drop(release);
3689            for job in jobs {
3690                let _ = job.await;
3691            }
3692            panic!("atomic unit did not reach its first INSERT: {entered:?}");
3693        }
3694        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
3695        release.send(()).unwrap();
3696        let mut errors = Vec::new();
3697        for job in jobs {
3698            if let Err(error) = job.await.unwrap() {
3699                errors.push(error.to_string());
3700            }
3701        }
3702        assert!(
3703            errors.is_empty(),
3704            "atomic units failed: {}",
3705            errors.join("; ")
3706        );
3707        let count: i64 = pool
3708            .writer()
3709            .unwrap()
3710            .conn()
3711            .query_row("SELECT COUNT(*) FROM atomic_in_memory", [], |row| {
3712                row.get(0)
3713            })
3714            .unwrap();
3715        assert_eq!(count, 16, "all eight two-row units must commit");
3716    }
3717
3718    #[tokio::test]
3719    async fn in_memory_atomic_unit_serializes_with_event_writes() {
3720        use khive_storage::EventStore as _;
3721
3722        // Each ordinary event entry point opens a transaction through with_writer.
3723        for mode in 0..3 {
3724            let pool = Arc::new(
3725                ConnectionPool::new(PoolConfig {
3726                    path: None,
3727                    write_queue_enabled: Some(false),
3728                    ..PoolConfig::default()
3729                })
3730                .unwrap(),
3731            );
3732            {
3733                let writer = pool.writer().unwrap();
3734                crate::stores::event::ensure_events_schema(writer.conn()).unwrap();
3735                writer
3736                    .conn()
3737                    .execute_batch("CREATE TABLE atomic_event_overlap (id INTEGER PRIMARY KEY)")
3738                    .unwrap();
3739            }
3740            let (in_unit, release) = pause_first_insert(&pool, "atomic_event_overlap");
3741            let bridge = SqlBridge::new(Arc::clone(&pool), false);
3742            let unit = tokio::spawn(async move {
3743                let op: AtomicUnitOp = Box::new(move |writer| {
3744                    Box::pin(async move {
3745                        writer
3746                            .execute(SqlStatement {
3747                                sql: "INSERT INTO atomic_event_overlap VALUES (1)".into(),
3748                                params: vec![],
3749                                label: None,
3750                            })
3751                            .await?;
3752                        writer
3753                            .execute(SqlStatement {
3754                                sql: "INSERT INTO atomic_event_overlap VALUES (2)".into(),
3755                                params: vec![],
3756                                label: None,
3757                            })
3758                            .await?;
3759                        Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
3760                    })
3761                });
3762                bridge.atomic_unit(op).await.map(|_| ())
3763            });
3764            let entered = tokio::time::timeout(std::time::Duration::from_secs(10), in_unit).await;
3765            if !matches!(&entered, Ok(Ok(()))) {
3766                drop(release);
3767                let result = unit.await;
3768                panic!("atomic unit did not reach its first INSERT: {entered:?}; {result:?}");
3769            }
3770            let store = Arc::new(crate::stores::event::SqlEventStore::new_scoped(
3771                Arc::clone(&pool),
3772                false,
3773                "atomic-event",
3774            ));
3775            let event = khive_storage::event::Event::new(
3776                "atomic-event",
3777                "search",
3778                khive_types::EventKind::SearchExecuted,
3779                khive_types::SubstrateKind::Note,
3780                "agent:test",
3781            )
3782            .with_payload(serde_json::json!({"result_kind": "note"}));
3783            let event_id = event.id;
3784            let event_store = Arc::clone(&store);
3785            let mut event_job = tokio::spawn(async move {
3786                match mode {
3787                    0 => event_store.append_event(event).await,
3788                    1 => event_store.append_events(vec![event]).await.map(|_| ()),
3789                    _ => event_store
3790                        .append_events_idempotent(vec![event])
3791                        .await
3792                        .map(|_| ()),
3793                }
3794            });
3795            let early =
3796                tokio::time::timeout(std::time::Duration::from_millis(20), &mut event_job).await;
3797            let finished_inside_unit = early.is_ok();
3798            // Release and join both tasks before asserting, including in the RED control.
3799            release.send(()).unwrap();
3800            unit.await.unwrap().expect("atomic unit commits both rows");
3801            let event_result = match early {
3802                Ok(joined) => joined,
3803                Err(_) => event_job.await,
3804            }
3805            .expect("event task joins");
3806            assert!(
3807                event_result.is_ok(),
3808                "event write mode {mode} overlapped the atomic transaction: {event_result:?}"
3809            );
3810            assert!(
3811                !finished_inside_unit,
3812                "event write mode {mode} must wait until the atomic unit ends"
3813            );
3814            assert!(store.get_event(event_id).await.unwrap().is_some());
3815            let rows: i64 = pool
3816                .writer()
3817                .unwrap()
3818                .conn()
3819                .query_row("SELECT COUNT(*) FROM atomic_event_overlap", [], |row| {
3820                    row.get(0)
3821                })
3822                .unwrap();
3823            assert_eq!(rows, 2);
3824        }
3825    }
3826
3827    fn database_tx_view(pool: &ConnectionPool) -> khive_storage::tx_registry::TxOriginFilter {
3828        match pool.origin() {
3829            khive_storage::tx_registry::TxOrigin::Database(identity) => {
3830                khive_storage::tx_registry::TxOriginFilter::Secondary(identity)
3831            }
3832            other => panic!("expected a file-backed database origin, got {other:?}"),
3833        }
3834    }
3835
3836    struct NotifyOnDrop(Arc<tokio::sync::Notify>);
3837
3838    impl Drop for NotifyOnDrop {
3839        fn drop(&mut self) {
3840            self.0.notify_one();
3841        }
3842    }
3843
3844    fn blocking_non_interrupting_progress_gate(
3845        conn: &rusqlite::Connection,
3846    ) -> (
3847        Arc<tokio::sync::Notify>,
3848        Arc<std::sync::Barrier>,
3849        Arc<tokio::sync::Notify>,
3850    ) {
3851        let entered = Arc::new(tokio::sync::Notify::new());
3852        let callback_entered = Arc::clone(&entered);
3853        let release = Arc::new(std::sync::Barrier::new(2));
3854        let callback_release = Arc::clone(&release);
3855        let completed = Arc::new(tokio::sync::Notify::new());
3856        let notify_on_drop = NotifyOnDrop(Arc::clone(&completed));
3857        let blocked_once = Arc::new(std::sync::atomic::AtomicBool::new(false));
3858        let callback_blocked_once = Arc::clone(&blocked_once);
3859        conn.progress_handler(
3860            1_000,
3861            Some(move || {
3862                let _keep_until_connection_drop = &notify_on_drop;
3863                if !callback_blocked_once.swap(true, std::sync::atomic::Ordering::SeqCst) {
3864                    callback_entered.notify_one();
3865                    callback_release.wait();
3866                    // The gate deliberately stalls but never asks SQLite to
3867                    // abort. It proves completion-preserving SQLite work ignores
3868                    // request-read cancellation and finishes normally.
3869                    return false;
3870                }
3871                false
3872            }),
3873        )
3874        .unwrap();
3875        (entered, release, completed)
3876    }
3877
3878    fn progress_gate_statement() -> SqlStatement {
3879        SqlStatement {
3880            sql: "WITH RECURSIVE rows(value) AS (\
3881                  SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 999\
3882                  ) SELECT SUM(value) FROM rows"
3883                .into(),
3884            params: vec![],
3885            label: None,
3886        }
3887    }
3888
3889    fn slow_insert_statement() -> SqlStatement {
3890        SqlStatement {
3891            sql: "INSERT INTO cancellation_write_probe(value) \
3892                  WITH RECURSIVE rows(value) AS (\
3893                  SELECT 1 UNION ALL SELECT value + 1 FROM rows WHERE value < 10000\
3894                  ) SELECT value FROM rows"
3895                .into(),
3896            params: vec![],
3897            label: Some("non-interruptible-write-probe".into()),
3898        }
3899    }
3900
3901    fn passive_checkpoint(conn: &rusqlite::Connection) -> (i64, i64, i64) {
3902        conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |row| {
3903            Ok((row.get(0)?, row.get(1)?, row.get(2)?))
3904        })
3905        .unwrap()
3906    }
3907
3908    fn deliberately_slow_read_statement() -> SqlStatement {
3909        SqlStatement {
3910            sql: "WITH RECURSIVE numbers(value) AS (\
3911                  SELECT 1 UNION ALL SELECT value + 1 FROM numbers WHERE value < 1000\
3912                  ) SELECT SUM(a.value * b.value * c.value) \
3913                  FROM numbers AS a CROSS JOIN numbers AS b CROSS JOIN numbers AS c"
3914                .into(),
3915            params: vec![],
3916            label: Some("read-cancellation-progress-probe".into()),
3917        }
3918    }
3919
3920    async fn wait_for_progress(probe: &std::sync::atomic::AtomicUsize) {
3921        tokio::time::timeout(std::time::Duration::from_secs(1), async {
3922            while probe.load(std::sync::atomic::Ordering::SeqCst) == 0 {
3923                tokio::task::yield_now().await;
3924            }
3925        })
3926        .await
3927        .expect("slow SQLite statement never reached its progress callback");
3928    }
3929
3930    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3931    async fn pooled_stats_count_cancellation_stops_scan_and_releases_reader() {
3932        let dir = tempfile::tempdir().unwrap();
3933        let pool = Arc::new(
3934            ConnectionPool::new(PoolConfig {
3935                path: Some(dir.path().join("stats-count-cancel.db")),
3936                max_readers: 1,
3937                ..PoolConfig::default()
3938            })
3939            .unwrap(),
3940        );
3941        // Expand a small file-backed fixture into a long scan, preserving the
3942        // handler's exact outer COUNT rather than substituting a SUM query.
3943        pool.writer()
3944            .unwrap()
3945            .conn()
3946            .execute_batch(
3947                "CREATE TABLE count_fixture(n INTEGER PRIMARY KEY); \
3948             WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<1000) \
3949             INSERT INTO count_fixture SELECT x FROM n; \
3950             CREATE VIEW events AS SELECT 'local' AS namespace, 'knowledge.learn' AS verb \
3951             FROM count_fixture a CROSS JOIN count_fixture b CROSS JOIN count_fixture c;",
3952            )
3953            .unwrap();
3954        let bridge = SqlBridge::new(Arc::clone(&pool), true);
3955        let mut reader = bridge.reader().await.unwrap();
3956        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3957        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
3958        let query = tokio::spawn(crate::scope_test_read_progress(
3959            Arc::clone(&progress),
3960            crate::scope_request_read_cancellation(cancel_rx, async move {
3961                let result = reader.query_scalar(SqlStatement {
3962                    sql: "SELECT COUNT(*) FROM events WHERE namespace = ?1 AND verb LIKE 'knowledge.%'".into(),
3963                    params: vec![SqlValue::Text("local".into())],
3964                    label: Some("knowledge.stats.event_count".into()),
3965                }).await;
3966                (reader, result)
3967            }),
3968        ));
3969        wait_for_progress(progress.as_ref()).await;
3970        assert!(
3971            !query.is_finished(),
3972            "COUNT must still be scanning before cancellation"
3973        );
3974        let started = std::time::Instant::now();
3975        let grace = crate::read_cancellation::sqlite_interrupt_grace_from_env();
3976        cancel_tx.send(true).unwrap();
3977        let (mut reader, result) = tokio::time::timeout(grace, query)
3978            .await
3979            .expect("COUNT did not settle within the interrupt grace")
3980            .unwrap();
3981        let elapsed = started.elapsed();
3982        assert!(
3983            matches!(result, Err(StorageError::Timeout { .. })),
3984            "{result:?}"
3985        );
3986        assert_eq!(
3987            pool.available_readers(),
3988            1,
3989            "COUNT retained the sole pooled reader"
3990        );
3991        let stopped = progress.load(std::sync::atomic::Ordering::SeqCst);
3992        let next = reader
3993            .query_scalar(SqlStatement {
3994                sql: "SELECT COUNT(*) FROM count_fixture".into(),
3995                params: vec![],
3996                label: None,
3997            })
3998            .await
3999            .unwrap();
4000        assert!(matches!(next, Some(SqlValue::Integer(1000))));
4001        assert_eq!(
4002            progress.load(std::sync::atomic::Ordering::SeqCst),
4003            stopped,
4004            "cancelled callback leaked into the next borrower"
4005        );
4006        eprintln!(
4007            "stats_count_cancel_ms={} grace_ms={}",
4008            elapsed.as_secs_f64() * 1000.0,
4009            grace.as_millis()
4010        );
4011    }
4012
4013    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4014    async fn cancellation_before_reader_checkout_is_prompt_and_executes_no_statement() {
4015        let dir = tempfile::tempdir().unwrap();
4016        let config = PoolConfig {
4017            path: Some(dir.path().join("sql_bridge_cancel_before_checkout.db")),
4018            max_readers: 1,
4019            checkout_timeout: std::time::Duration::from_secs(5),
4020            ..PoolConfig::default()
4021        };
4022        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4023        pool.writer()
4024            .unwrap()
4025            .conn()
4026            .execute_batch(
4027                "CREATE TABLE checkout_cancel_probe(value INTEGER NOT NULL); \
4028                 INSERT INTO checkout_cancel_probe VALUES (0);",
4029            )
4030            .unwrap();
4031        let held_reader = pool.reader().expect("hold the sole pooled reader");
4032        // Production-shaped file-backed raw-SQL reads must use the same pool
4033        // admission as typed stores. Before ADR-165 Slice 2, `reader()` opened
4034        // a standalone connection and this held pooled reader was invisible.
4035        // A plain SELECT (not a DML/RETURNING statement) is used here: the
4036        // reader capability's admission gate now refuses non-read statement
4037        // shapes before checkout is even attempted, so a DML probe would
4038        // never reach the cancellation-race path this test exercises.
4039        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4040        let mut waiting_reader = bridge.reader().await.unwrap();
4041        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
4042        let waiting = tokio::spawn(crate::scope_request_read_cancellation(
4043            cancel_rx,
4044            async move {
4045                waiting_reader
4046                    .query_row(SqlStatement {
4047                        sql: "SELECT value FROM checkout_cancel_probe".into(),
4048                        params: vec![],
4049                        label: Some("must-not-run-after-cancelled-checkout".into()),
4050                    })
4051                    .await
4052            },
4053        ));
4054
4055        tokio::task::yield_now().await;
4056        cancel_tx.send(true).unwrap();
4057        let result = tokio::time::timeout(std::time::Duration::from_millis(100), waiting)
4058            .await
4059            .expect("cancelled reader checkout waited for the five-second pool timeout")
4060            .expect("checkout task panicked");
4061        // Cancellation is NOT an admission wait: it must stay the non-admission
4062        // Timeout, never the retryable AdmissionTimeout, so a cancelled request
4063        // does not signal clients to retry into a saturated pool.
4064        assert!(matches!(result, Err(StorageError::Timeout { .. })));
4065
4066        drop(held_reader);
4067        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
4068        let value: i64 = pool
4069            .reader()
4070            .unwrap()
4071            .conn()
4072            .query_row("SELECT value FROM checkout_cancel_probe", [], |row| {
4073                row.get(0)
4074            })
4075            .unwrap();
4076        assert_eq!(
4077            value, 0,
4078            "the probe row must be untouched: nothing else in this test writes to it"
4079        );
4080        assert_eq!(
4081            pool.available_readers(),
4082            1,
4083            "reader checkout leaked a permit"
4084        );
4085    }
4086
4087    /// Reproduction probe for the reader-capability admission gap: `sqlite3_stmt_readonly`
4088    /// (the only check the pre-fix bridge applied to raw SQL reaching the
4089    /// pooled reader) returns `true` for `ATTACH`, configuration `PRAGMA`s,
4090    /// and `CREATE TEMP TABLE` — none of which write the main database file,
4091    /// but all of which leave connection-local state that `reset_reader_connection`
4092    /// did not catch. Each probe below is submitted through the reader
4093    /// capability exactly as an untrusted caller would reach it
4094    /// (`SqlBridge::reader` -> `SqlReader::query_all`).
4095    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4096    async fn probe_reader_capability_admission_of_state_mutating_statements() {
4097        let dir = tempfile::tempdir().unwrap();
4098        let config = PoolConfig {
4099            path: Some(dir.path().join("sql_bridge_reader_admission_probe.db")),
4100            max_readers: 2,
4101            ..PoolConfig::default()
4102        };
4103        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4104        pool.writer()
4105            .unwrap()
4106            .conn()
4107            .execute_batch("CREATE TABLE reader_admission_probe(value INTEGER NOT NULL);")
4108            .unwrap();
4109        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4110
4111        let probes: [&str; 5] = [
4112            "ATTACH DATABASE ':memory:' AS x",
4113            "PRAGMA writable_schema=ON",
4114            "PRAGMA busy_timeout=1",
4115            "PRAGMA cache_size(-64)",
4116            "CREATE TEMP TABLE t(x)",
4117        ];
4118        let mut admitted = Vec::new();
4119        for probe in probes {
4120            let mut reader = bridge.reader().await.unwrap();
4121            let result = reader
4122                .query_all(SqlStatement {
4123                    sql: probe.into(),
4124                    params: vec![],
4125                    label: Some("reader-admission-probe".into()),
4126                })
4127                .await;
4128            admitted.push((probe, result.is_ok()));
4129        }
4130        eprintln!("reader capability admission per probe: {admitted:#?}");
4131        for (probe, was_admitted) in &admitted {
4132            assert!(
4133                !was_admitted,
4134                "reader capability must refuse {probe:?}; the pre-fix bridge wrongly admitted it"
4135            );
4136        }
4137
4138        // Control: an ordinary SELECT and an allow-listed introspection
4139        // PRAGMA must still be admitted through the same reader capability.
4140        let controls: [&str; 2] = [
4141            "SELECT value FROM reader_admission_probe",
4142            "PRAGMA table_info(reader_admission_probe)",
4143        ];
4144        for control in controls {
4145            let mut reader = bridge.reader().await.unwrap();
4146            let result = reader
4147                .query_all(SqlStatement {
4148                    sql: control.into(),
4149                    params: vec![],
4150                    label: Some("reader-admission-control".into()),
4151                })
4152                .await;
4153            assert!(
4154                result.is_ok(),
4155                "reader capability must still admit {control:?}: {result:?}"
4156            );
4157        }
4158    }
4159
4160    /// A `WITH` clause is not by itself a read shape: SQLite accepts
4161    /// `WITH ... INSERT/UPDATE/DELETE`, including with `RETURNING`, and the
4162    /// admission classifier must walk past the common-table-expression list
4163    /// to the main statement's own head keyword rather than admitting every
4164    /// statement that merely starts with `WITH`.
4165    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4166    async fn probe_reader_capability_admission_of_with_cte_dml_statements() {
4167        let dir = tempfile::tempdir().unwrap();
4168        let config = PoolConfig {
4169            path: Some(dir.path().join("sql_bridge_cte_dml_admission_probe.db")),
4170            max_readers: 2,
4171            ..PoolConfig::default()
4172        };
4173        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4174        pool.writer()
4175            .unwrap()
4176            .conn()
4177            .execute_batch(
4178                "CREATE TABLE cte_dml_admission_probe(id INTEGER PRIMARY KEY, value INTEGER NOT NULL);",
4179            )
4180            .unwrap();
4181        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4182
4183        let probes: [&str; 4] = [
4184            "WITH x(v) AS (SELECT 1) \
4185             INSERT INTO cte_dml_admission_probe(id, value) SELECT 1, v FROM x",
4186            "WITH x(v) AS (SELECT 1) \
4187             INSERT INTO cte_dml_admission_probe(id, value) SELECT 1, v FROM x RETURNING id",
4188            "WITH x(v) AS (SELECT 0) \
4189             UPDATE cte_dml_admission_probe SET value = value + (SELECT v FROM x) WHERE id = 1",
4190            "WITH x(v) AS (SELECT 1) \
4191             DELETE FROM cte_dml_admission_probe WHERE id = (SELECT v FROM x)",
4192        ];
4193        let mut admitted = Vec::new();
4194        for probe in probes {
4195            let mut reader = bridge.reader().await.unwrap();
4196            let result = reader
4197                .query_all(SqlStatement {
4198                    sql: probe.into(),
4199                    params: vec![],
4200                    label: Some("cte-dml-admission-probe".into()),
4201                })
4202                .await;
4203            eprintln!("{probe:?} -> {result:?}");
4204            admitted.push((probe, result.is_ok()));
4205        }
4206        eprintln!("WITH-DML reader capability admission per probe: {admitted:#?}");
4207        for (probe, was_admitted) in &admitted {
4208            assert!(
4209                !was_admitted,
4210                "reader capability must refuse {probe:?}; it is a WITH-prefixed write, not a read"
4211            );
4212        }
4213
4214        // Controls: ordinary and recursive read-only CTEs, including a
4215        // multi-CTE chain and a CTE body containing string literals with
4216        // commas (the shape produced by the graph traversal compiler), must
4217        // still be admitted.
4218        let controls: [&str; 3] = [
4219            "WITH x(v) AS (SELECT 1) SELECT v FROM x",
4220            "WITH RECURSIVE n(v) AS (VALUES(0) UNION ALL SELECT v + 1 FROM n WHERE v < 3) \
4221             SELECT v FROM n",
4222            "WITH a(v) AS (SELECT 1), b(v) AS (SELECT v FROM a WHERE ',' || 'x' NOT LIKE '%,%') \
4223             SELECT v FROM b",
4224        ];
4225        for control in controls {
4226            let mut reader = bridge.reader().await.unwrap();
4227            let result = reader
4228                .query_all(SqlStatement {
4229                    sql: control.into(),
4230                    params: vec![],
4231                    label: Some("cte-dml-admission-control".into()),
4232                })
4233                .await;
4234            assert!(
4235                result.is_ok(),
4236                "reader capability must still admit {control:?}: {result:?}"
4237            );
4238        }
4239    }
4240
4241    /// A CTE name may be a SQLite-quoted identifier — double-quoted,
4242    /// backtick-quoted, or bracket-quoted — and is not restricted to the
4243    /// unquoted `[A-Za-z0-9_]+` shape `skip_common_table_expressions` uses to
4244    /// recognize an ordinary keyword. A quoted name containing parentheses
4245    /// must still be admitted when the statement underneath the CTE list is
4246    /// a read, and refused when it is a write, exactly like an unquoted name.
4247    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4248    async fn probe_reader_capability_admission_of_quoted_cte_names() {
4249        let dir = tempfile::tempdir().unwrap();
4250        let config = PoolConfig {
4251            path: Some(dir.path().join("sql_bridge_quoted_cte_admission_probe.db")),
4252            max_readers: 2,
4253            ..PoolConfig::default()
4254        };
4255        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4256        pool.writer()
4257            .unwrap()
4258            .conn()
4259            .execute_batch(
4260                "CREATE TABLE quoted_cte_admission_probe(id INTEGER PRIMARY KEY, value INTEGER NOT NULL);",
4261            )
4262            .unwrap();
4263        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4264
4265        let controls: [&str; 3] = [
4266            "WITH \"my (cte)\"(v) AS (SELECT 1) SELECT v FROM \"my (cte)\"",
4267            "WITH `my (cte)`(v) AS (SELECT 1) SELECT v FROM `my (cte)`",
4268            "WITH [my (cte)](v) AS (SELECT 1) SELECT v FROM [my (cte)]",
4269        ];
4270        for control in controls {
4271            let mut reader = bridge.reader().await.unwrap();
4272            let result = reader
4273                .query_all(SqlStatement {
4274                    sql: control.into(),
4275                    params: vec![],
4276                    label: Some("quoted-cte-admission-control".into()),
4277                })
4278                .await;
4279            assert!(
4280                result.is_ok(),
4281                "reader capability must admit a quoted CTE name in {control:?}: {result:?}"
4282            );
4283        }
4284
4285        let write_probe = "WITH \"my (cte)\"(v) AS (SELECT 1) \
4286             INSERT INTO quoted_cte_admission_probe(id, value) SELECT 1, v FROM \"my (cte)\"";
4287        let mut reader = bridge.reader().await.unwrap();
4288        let result = reader
4289            .query_all(SqlStatement {
4290                sql: write_probe.into(),
4291                params: vec![],
4292                label: Some("quoted-cte-admission-probe".into()),
4293            })
4294            .await;
4295        assert!(
4296            result.is_err(),
4297            "reader capability must refuse a write statement under a quoted CTE name: {result:?}"
4298        );
4299    }
4300
4301    /// Regression for the in-memory reader path: `PoolBackedReader` (the
4302    /// `SqlBridge::reader` implementation used whenever `is_file_backed` is
4303    /// `false`) computed its admission classification differently from
4304    /// `SqliteReader`, always passing `None` for the cached transaction-control
4305    /// read instead of classifying the statement. That made the deferred-read
4306    /// snapshot pair (`BEGIN DEFERRED` ... `COMMIT`) a multi-call reader
4307    /// already relies on to hold one consistent view across several pooled
4308    /// checkouts — see `khive-pack-memory`'s fresh-tail leg — indistinguishable
4309    /// from an unrecognized statement head and refused outright. Mutating
4310    /// statements must still be refused through this path exactly as they are
4311    /// through the file-backed one.
4312    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4313    async fn pool_backed_reader_admits_deferred_read_transaction_control() {
4314        let config = PoolConfig {
4315            path: None,
4316            ..PoolConfig::default()
4317        };
4318        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4319        pool.writer()
4320            .unwrap()
4321            .conn()
4322            .execute_batch(
4323                "CREATE TABLE pool_backed_reader_probe(value INTEGER NOT NULL); \
4324                 INSERT INTO pool_backed_reader_probe VALUES (1);",
4325            )
4326            .unwrap();
4327        let bridge = SqlBridge::new(Arc::clone(&pool), false);
4328        let mut reader = bridge.reader().await.unwrap();
4329
4330        reader
4331            .query_all(SqlStatement {
4332                sql: "BEGIN DEFERRED".into(),
4333                params: vec![],
4334                label: Some("pool-backed-reader-snapshot-begin".into()),
4335            })
4336            .await
4337            .expect("BEGIN DEFERRED must be admitted through the pool-backed reader");
4338        let rows = reader
4339            .query_all(SqlStatement {
4340                sql: "SELECT value FROM pool_backed_reader_probe".into(),
4341                params: vec![],
4342                label: Some("pool-backed-reader-snapshot-read".into()),
4343            })
4344            .await
4345            .expect("a read inside the admitted snapshot must succeed");
4346        assert_eq!(rows.len(), 1);
4347        reader
4348            .query_all(SqlStatement {
4349                sql: "COMMIT".into(),
4350                params: vec![],
4351                label: Some("pool-backed-reader-snapshot-commit".into()),
4352            })
4353            .await
4354            .expect("COMMIT must be admitted through the pool-backed reader");
4355
4356        let integrity = reader
4357            .query_scalar(SqlStatement {
4358                sql: "PRAGMA integrity_check".into(),
4359                params: vec![],
4360                label: Some("pool-backed-reader-integrity-check".into()),
4361            })
4362            .await
4363            .expect("PRAGMA integrity_check must be admitted through the pool-backed reader");
4364        assert!(matches!(integrity, Some(SqlValue::Text(ref s)) if s.eq_ignore_ascii_case("ok")));
4365
4366        for probe in [
4367            "ATTACH DATABASE ':memory:' AS x",
4368            "PRAGMA writable_schema=ON",
4369            "CREATE TEMP TABLE t(x)",
4370            "SAVEPOINT nested_snapshot",
4371            // A `WITH` head is not itself a read shape: SQLite also allows
4372            // `WITH ... INSERT/UPDATE/DELETE`, with or without `RETURNING`,
4373            // and the pool-backed route must refuse it exactly like the
4374            // file-backed one does.
4375            "WITH x(v) AS (SELECT 99) INSERT INTO pool_backed_reader_probe(value) SELECT v FROM x",
4376            "WITH x(v) AS (SELECT 99) \
4377             INSERT INTO pool_backed_reader_probe(value) SELECT v FROM x RETURNING value",
4378            "WITH x(v) AS (SELECT 0) \
4379             UPDATE pool_backed_reader_probe SET value = value + (SELECT v FROM x)",
4380            "WITH x(v) AS (SELECT 1) \
4381             DELETE FROM pool_backed_reader_probe WHERE value = (SELECT v FROM x)",
4382        ] {
4383            let mut reader = bridge.reader().await.unwrap();
4384            let result = reader
4385                .query_all(SqlStatement {
4386                    sql: probe.into(),
4387                    params: vec![],
4388                    label: Some("pool-backed-reader-admission-probe".into()),
4389                })
4390                .await;
4391            assert!(
4392                result.is_err(),
4393                "pool-backed reader capability must refuse {probe:?}; got {result:?}"
4394            );
4395        }
4396
4397        // The refused probes above must not have left a stray write behind:
4398        // exactly the one row inserted before the probe loop is still there.
4399        let rows = bridge
4400            .reader()
4401            .await
4402            .unwrap()
4403            .query_all(SqlStatement {
4404                sql: "SELECT value FROM pool_backed_reader_probe".into(),
4405                params: vec![],
4406                label: Some("pool-backed-reader-post-probe-read".into()),
4407            })
4408            .await
4409            .expect("a plain read must still work after every probe above was refused");
4410        assert_eq!(
4411            rows.len(),
4412            1,
4413            "a refused WITH-DML probe must not have committed a row"
4414        );
4415    }
4416
4417    /// An explicit deferred read-transaction span
4418    /// (`begin_read_snapshot`/`end_read_snapshot` in `khive-pack-memory`)
4419    /// that never reaches its own `COMMIT` — because the caller hit an error
4420    /// mid-span and gave up on the read, or was cancelled — must still leave
4421    /// the pool's connection rolled back to autocommit before it can be
4422    /// reused. Before the span retained a real owned connection guard across
4423    /// `.await` points, every `PoolBackedReader` call drew its own fresh
4424    /// checkout: `BEGIN` and `COMMIT` could land on different checkouts, and
4425    /// an abandoned span between them returned the connection to the pool
4426    /// while still inside an open transaction.
4427    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4428    async fn abandoned_deferred_read_transaction_span_is_rolled_back_before_reuse() {
4429        let config = PoolConfig {
4430            path: None,
4431            ..PoolConfig::default()
4432        };
4433        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4434        pool.writer()
4435            .unwrap()
4436            .conn()
4437            .execute_batch(
4438                "CREATE TABLE abandoned_span_probe(value INTEGER NOT NULL); \
4439                 INSERT INTO abandoned_span_probe VALUES (1);",
4440            )
4441            .unwrap();
4442        let bridge = SqlBridge::new(Arc::clone(&pool), false);
4443
4444        {
4445            let mut reader = bridge.reader().await.unwrap();
4446            reader
4447                .query_all(SqlStatement {
4448                    sql: "BEGIN DEFERRED".into(),
4449                    params: vec![],
4450                    label: Some("abandoned-span-begin".into()),
4451                })
4452                .await
4453                .expect("the span must open");
4454            let failed = reader
4455                .query_all(SqlStatement {
4456                    sql: "SELECT value FROM abandoned_span_probe_missing_table".into(),
4457                    params: vec![],
4458                    label: Some("abandoned-span-failing-read".into()),
4459                })
4460                .await;
4461            assert!(
4462                failed.is_err(),
4463                "the probe read against a nonexistent table must fail"
4464            );
4465            // `reader`, and the still-open transaction guard it owns, is
4466            // dropped here without ever reaching `COMMIT`/`ROLLBACK`.
4467        }
4468
4469        let writer = pool.writer().unwrap();
4470        assert!(
4471            writer.conn().is_autocommit(),
4472            "an abandoned deferred-read span must be rolled back before its connection \
4473             returns to service"
4474        );
4475        drop(writer);
4476
4477        let mut reader = bridge.reader().await.unwrap();
4478        let rows = reader
4479            .query_all(SqlStatement {
4480                sql: "SELECT value FROM abandoned_span_probe".into(),
4481                params: vec![],
4482                label: Some("abandoned-span-post-recovery-read".into()),
4483            })
4484            .await
4485            .expect("a fresh checkout must read normally after the abandoned span");
4486        assert_eq!(
4487            rows.len(),
4488            1,
4489            "the original row must be intact; the abandoned span must not have committed \
4490             anything"
4491        );
4492    }
4493
4494    /// A pooled-reader checkout that exhausts `checkout_timeout` WITHOUT any
4495    /// cancellation is a genuine admission wait and must surface as the
4496    /// retryable AdmissionTimeout. Before the fix, `reader_until`'s
4497    /// pool-exhausted error was mapped to `StorageError::Driver`, so a
4498    /// saturated pooled read stayed a non-retryable driver failure and the new
4499    /// AdmissionTimeout branch (reachable only for `Ok(None)` cancellation) was
4500    /// dead for real timeouts.
4501    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4502    async fn pooled_reader_checkout_timeout_is_a_retryable_admission_timeout() {
4503        let dir = tempfile::tempdir().unwrap();
4504        let config = PoolConfig {
4505            path: Some(dir.path().join("sql_bridge_reader_admission_timeout.db")),
4506            max_readers: 1,
4507            checkout_timeout: std::time::Duration::from_millis(200),
4508            ..PoolConfig::default()
4509        };
4510        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4511        pool.writer()
4512            .unwrap()
4513            .conn()
4514            .execute_batch(
4515                "CREATE TABLE reader_admission_probe(value INTEGER NOT NULL); \
4516                 INSERT INTO reader_admission_probe VALUES (0);",
4517            )
4518            .unwrap();
4519        // Hold the sole pooled reader so the contending checkout cannot succeed
4520        // and must run `checkout_timeout` to exhaustion — no cancellation.
4521        let held_reader = pool.reader().expect("hold the sole pooled reader");
4522
4523        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4524        let mut contender = bridge.reader().await.unwrap();
4525        let blocked = contender
4526            .query_row(SqlStatement {
4527                sql: "SELECT value FROM reader_admission_probe".into(),
4528                params: vec![],
4529                label: Some("reader-admission-timeout-probe".into()),
4530            })
4531            .await;
4532        assert!(
4533            matches!(blocked, Err(StorageError::AdmissionTimeout { .. })),
4534            "an exhausted pooled-reader checkout must be a retryable AdmissionTimeout; got {blocked:?}"
4535        );
4536
4537        drop(held_reader);
4538        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
4539        assert_eq!(
4540            pool.available_readers(),
4541            1,
4542            "reader checkout leaked a permit"
4543        );
4544    }
4545
4546    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4547    async fn abandoned_read_interrupts_sqlite_releases_permit_and_stops_work() {
4548        let dir = tempfile::tempdir().unwrap();
4549        let config = PoolConfig {
4550            path: Some(dir.path().join("sql_bridge_abandoned_read.db")),
4551            max_readers: 1,
4552            checkout_timeout: std::time::Duration::from_millis(500),
4553            ..PoolConfig::default()
4554        };
4555        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4556        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4557        let mut reader = SqliteReader {
4558            handle: Some(
4559                open_explicit_read_transaction_handle(Arc::clone(&pool))
4560                    .await
4561                    .unwrap(),
4562            ),
4563            pool: Arc::clone(&pool),
4564            poisoned: false,
4565        };
4566        let mut contender = bridge.reader().await.unwrap();
4567        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4568        let progress_in_scope = Arc::clone(&progress);
4569
4570        let query = tokio::spawn(crate::scope_test_read_progress(
4571            progress_in_scope,
4572            async move { reader.query_all(deliberately_slow_read_statement()).await },
4573        ));
4574        wait_for_progress(progress.as_ref()).await;
4575        query.abort();
4576        assert!(matches!(query.await, Err(error) if error.is_cancelled()));
4577
4578        tokio::time::timeout(
4579            std::time::Duration::from_millis(500),
4580            contender.query_row(SqlStatement {
4581                sql: "SELECT 1".into(),
4582                params: vec![],
4583                label: None,
4584            }),
4585        )
4586        .await
4587        .expect("abandoned SQLite statement did not return the sole reader promptly")
4588        .expect("reader probe failed after cancellation");
4589
4590        let stopped_at = progress.load(std::sync::atomic::Ordering::SeqCst);
4591        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4592        assert_eq!(
4593            progress.load(std::sync::atomic::Ordering::SeqCst),
4594            stopped_at,
4595            "SQLite progress kept advancing after the abandoned request returned its reader"
4596        );
4597    }
4598
4599    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4600    async fn request_deadline_interrupts_statement_without_outer_timeout() {
4601        let dir = tempfile::tempdir().unwrap();
4602        let config = PoolConfig {
4603            path: Some(dir.path().join("sql_bridge_request_deadline.db")),
4604            max_readers: 1,
4605            checkout_timeout: std::time::Duration::from_millis(500),
4606            ..PoolConfig::default()
4607        };
4608        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4609        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4610        let mut reader = bridge.reader().await.unwrap();
4611        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4612
4613        let result = crate::scope_test_read_progress(
4614            Arc::clone(&progress),
4615            crate::scope_request_read_deadline(std::time::Duration::from_millis(25), async move {
4616                reader.query_all(deliberately_slow_read_statement()).await
4617            }),
4618        )
4619        .await;
4620        assert!(
4621            matches!(result, Err(StorageError::Timeout { .. })),
4622            "deadline must surface as a typed timeout, got {result:?}"
4623        );
4624
4625        let stopped_at = progress.load(std::sync::atomic::Ordering::SeqCst);
4626        assert!(stopped_at > 0, "deadline test never exercised SQLite work");
4627        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4628        assert_eq!(
4629            progress.load(std::sync::atomic::Ordering::SeqCst),
4630            stopped_at,
4631            "deadline returned while SQLite kept consuming work"
4632        );
4633    }
4634
4635    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4636    async fn progress_handler_cleanup_failure_discards_pooled_connection() {
4637        let dir = tempfile::tempdir().unwrap();
4638        let config = PoolConfig {
4639            path: Some(dir.path().join("sql_bridge_cleanup_failure.db")),
4640            max_readers: 1,
4641            ..PoolConfig::default()
4642        };
4643        let pool = Arc::new(ConnectionPool::new(config).unwrap());
4644        let pool_for_read = Arc::clone(&pool);
4645        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4646        let result = crate::read_cancellation::scope_test_read_cleanup_failure(
4647            crate::scope_test_read_progress(
4648                Arc::clone(&progress),
4649                crate::read_cancellation::run_interruptible_read(
4650                    StorageCapability::Sql,
4651                    "cleanup_failure_probe",
4652                    move |scope| {
4653                        let mut guard = pool_for_read.reader().map_err(|error| {
4654                            StorageError::driver(
4655                                StorageCapability::Sql,
4656                                "cleanup_failure_probe",
4657                                error,
4658                            )
4659                        })?;
4660                        scope.run_pooled_reader(&mut guard, |conn| {
4661                            conn.query_row("SELECT 1", [], |row| row.get::<_, i64>(0))
4662                                .map_err(|error| map_rusqlite_err(error, "cleanup_failure_probe"))
4663                        })
4664                    },
4665                ),
4666            ),
4667        )
4668        .await;
4669        assert!(
4670            matches!(result, Err(StorageError::Internal(ref message)) if message.contains("clear failure")),
4671            "injected cleanup failure must be surfaced; got {result:?}"
4672        );
4673        assert_eq!(
4674            pool.available_readers(),
4675            1,
4676            "discard must install a replacement"
4677        );
4678
4679        let calls_after_failed_read = progress.load(std::sync::atomic::Ordering::SeqCst);
4680        let guard = pool.reader().unwrap();
4681        let sum: i64 = guard
4682            .conn()
4683            .query_row(
4684                "WITH RECURSIVE n(x) AS (VALUES(0) UNION ALL SELECT x + 1 FROM n WHERE x < 10000) \
4685                 SELECT sum(x) FROM n",
4686                [],
4687                |row| row.get(0),
4688            )
4689            .unwrap();
4690        assert_eq!(sum, 50_005_000);
4691        assert_eq!(
4692            progress.load(std::sync::atomic::Ordering::SeqCst),
4693            calls_after_failed_read,
4694            "a connection whose handler could not be cleared was reused"
4695        );
4696    }
4697
4698    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4699    async fn raw_pooled_reader_quarantines_cleanup_failure_during_unwind() {
4700        let dir = tempfile::tempdir().unwrap();
4701        let pool = Arc::new(
4702            ConnectionPool::new(PoolConfig {
4703                path: Some(dir.path().join("raw_reader_unwind_cleanup.db")),
4704                max_readers: 1,
4705                ..PoolConfig::default()
4706            })
4707            .unwrap(),
4708        );
4709        let worker_pool = Arc::clone(&pool);
4710        let result = crate::read_cancellation::scope_test_read_cleanup_failure(
4711            crate::read_cancellation::run_interruptible_read(
4712                StorageCapability::Sql,
4713                "raw_reader_unwind_cleanup",
4714                move |scope| {
4715                    let mut guard = worker_pool.reader().map_err(|error| {
4716                        StorageError::driver(
4717                            StorageCapability::Sql,
4718                            "raw_reader_unwind_cleanup",
4719                            error,
4720                        )
4721                    })?;
4722                    scope.with_pooled_reader(&mut guard, |conn| {
4723                        scope.run(conn, || -> khive_storage::types::StorageResult<()> {
4724                            panic!("injected raw reader panic after progress registration")
4725                        })
4726                    })
4727                },
4728            ),
4729        )
4730        .await;
4731        assert!(
4732            result.is_err(),
4733            "blocking panic must surface as a join error"
4734        );
4735        assert_eq!(
4736            pool.available_readers(),
4737            pool.max_readers(),
4738            "unwind cleanup failure must close and replace the raw pooled reader"
4739        );
4740    }
4741
4742    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4743    async fn raw_pooled_writer_retires_cleanup_failure_during_unwind() {
4744        let pool = Arc::new(ConnectionPool::new(PoolConfig::default()).unwrap());
4745        let worker_pool = Arc::clone(&pool);
4746        let result = crate::read_cancellation::scope_test_read_cleanup_failure(
4747            crate::read_cancellation::run_interruptible_read(
4748                StorageCapability::Sql,
4749                "raw_writer_unwind_cleanup",
4750                move |scope| {
4751                    let guard = worker_pool.try_writer().map_err(|error| {
4752                        StorageError::driver(
4753                            StorageCapability::Sql,
4754                            "raw_writer_unwind_cleanup",
4755                            error,
4756                        )
4757                    })?;
4758                    scope.with_pooled_writer(&worker_pool, &guard, |conn| {
4759                        scope.run(conn, || -> khive_storage::types::StorageResult<()> {
4760                            panic!("injected raw writer panic after progress registration")
4761                        })
4762                    })
4763                },
4764            ),
4765        )
4766        .await;
4767        assert!(
4768            result.is_err(),
4769            "blocking panic must surface as a join error"
4770        );
4771        assert!(
4772            pool.try_writer().is_err(),
4773            "unwind cleanup failure must retire the raw pooled writer"
4774        );
4775    }
4776
4777    #[test]
4778    fn query_row_converts_only_the_first_matching_row() {
4779        let conn = rusqlite::Connection::open_in_memory().unwrap();
4780        let statement = SqlStatement {
4781            sql: "WITH RECURSIVE rows(value) AS (\
4782                  SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 99\
4783                  ) SELECT value FROM rows ORDER BY value"
4784                .into(),
4785            params: vec![],
4786            label: None,
4787        };
4788
4789        ROW_CONVERSIONS.with(|count| count.set(0));
4790        let row = execute_query_row(&conn, &statement).unwrap().unwrap();
4791
4792        assert!(matches!(row.get("value"), Some(SqlValue::Integer(0))));
4793        ROW_CONVERSIONS.with(|count| assert_eq!(count.get(), 1));
4794    }
4795
4796    #[test]
4797    fn query_page_bounds_owned_rows_before_full_materialization() {
4798        let conn = rusqlite::Connection::open_in_memory().unwrap();
4799        let statement = SqlStatement {
4800            sql: "WITH RECURSIVE rows(value) AS (\
4801                  SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 99\
4802                  ) SELECT value FROM rows ORDER BY value"
4803                .into(),
4804            params: vec![],
4805            label: None,
4806        };
4807
4808        ROW_CONVERSIONS.with(|count| count.set(0));
4809        let rows = execute_query_page(
4810            &conn,
4811            &statement,
4812            &PageRequest {
4813                offset: 40,
4814                limit: 3,
4815            },
4816        )
4817        .unwrap();
4818
4819        assert_eq!(rows.len(), 3);
4820        assert!(matches!(rows[0].get("value"), Some(SqlValue::Integer(40))));
4821        assert!(matches!(rows[2].get("value"), Some(SqlValue::Integer(42))));
4822        ROW_CONVERSIONS.with(|count| assert_eq!(count.get(), 3));
4823    }
4824
4825    #[test]
4826    fn query_page_zero_limit_converts_no_rows_but_still_validates_sql() {
4827        let conn = rusqlite::Connection::open_in_memory().unwrap();
4828        let statement = SqlStatement {
4829            sql: "WITH RECURSIVE rows(value) AS (\
4830                  SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 99\
4831                  ) SELECT value FROM rows ORDER BY value"
4832                .into(),
4833            params: vec![],
4834            label: None,
4835        };
4836
4837        ROW_CONVERSIONS.with(|count| count.set(0));
4838        let rows = execute_query_page(
4839            &conn,
4840            &statement,
4841            &PageRequest {
4842                offset: 0,
4843                limit: 0,
4844            },
4845        )
4846        .unwrap();
4847
4848        assert!(rows.is_empty());
4849        ROW_CONVERSIONS.with(|count| assert_eq!(count.get(), 0));
4850
4851        let invalid = SqlStatement {
4852            sql: "SELECT FROM WHERE".into(),
4853            params: vec![],
4854            label: None,
4855        };
4856        assert!(
4857            execute_query_page(
4858                &conn,
4859                &invalid,
4860                &PageRequest {
4861                    offset: 0,
4862                    limit: 0
4863                }
4864            )
4865            .is_err(),
4866            "a zero-limit page must still fail on invalid SQL at prepare time"
4867        );
4868    }
4869
4870    #[test]
4871    fn cached_writer_prepare_preserves_the_single_statement_boundary() {
4872        let conn = rusqlite::Connection::open_in_memory().unwrap();
4873        assert!(matches!(
4874            prepare_cached_sql_statement(&conn, "SELECT 1; SELECT 2"),
4875            Err(rusqlite::Error::MultipleStatement)
4876        ));
4877    }
4878
4879    #[tokio::test]
4880    async fn queue_backed_execute_reuses_the_persistent_connection_statement_cache() {
4881        use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
4882        use std::sync::atomic::{AtomicUsize, Ordering};
4883
4884        let dir = tempfile::tempdir().unwrap();
4885        let pool = Arc::new(
4886            ConnectionPool::new(PoolConfig {
4887                path: Some(dir.path().join("sql_bridge_writer_cache.db")),
4888                write_queue_enabled: Some(true),
4889                write_routing_strict: true,
4890                ..PoolConfig::for_test()
4891            })
4892            .unwrap(),
4893        );
4894        pool.writer()
4895            .unwrap()
4896            .conn()
4897            .execute_batch(
4898                "CREATE TABLE writer_cache_test (id INTEGER PRIMARY KEY, value TEXT NOT NULL)",
4899            )
4900            .unwrap();
4901
4902        let writer_task = pool
4903            .writer_task_handle()
4904            .unwrap()
4905            .expect("file-backed queue-enabled pool must expose its writer task");
4906        let prepare_count = Arc::new(AtomicUsize::new(0));
4907        let hook_count = Arc::clone(&prepare_count);
4908        writer_task
4909            .send_top_level(move |conn| {
4910                conn.authorizer(Some(move |context: AuthContext<'_>| {
4911                    if matches!(
4912                        context.action,
4913                        AuthAction::Insert { table_name } if table_name == "writer_cache_test"
4914                    ) {
4915                        hook_count.fetch_add(1, Ordering::SeqCst);
4916                    }
4917                    Authorization::Allow
4918                }))
4919                .map_err(|error| map_rusqlite_err(error, "test.install_authorizer"))
4920            })
4921            .await
4922            .unwrap();
4923
4924        let bridge = SqlBridge::new(Arc::clone(&pool), true);
4925        let mut writer = bridge.writer().await.unwrap();
4926        for id in [1, 2] {
4927            khive_storage::SqlWriter::execute(
4928                &mut *writer,
4929                SqlStatement {
4930                    sql: "INSERT INTO writer_cache_test (id, value) VALUES (?1, ?2)".into(),
4931                    params: vec![SqlValue::Integer(id), SqlValue::Text(format!("value-{id}"))],
4932                    label: None,
4933                },
4934            )
4935            .await
4936            .unwrap();
4937        }
4938
4939        assert_eq!(
4940            prepare_count.load(Ordering::SeqCst),
4941            1,
4942            "the second identical execute on the writer task's persistent connection must reuse \
4943             the cached SQLite statement instead of compiling it again"
4944        );
4945        writer_task
4946            .send_top_level(|conn| {
4947                conn.authorizer(None::<fn(AuthContext<'_>) -> Authorization>)
4948                    .map_err(|error| map_rusqlite_err(error, "test.remove_authorizer"))
4949            })
4950            .await
4951            .unwrap();
4952    }
4953
4954    #[test]
4955    fn inline_execute_batch_prepares_each_statement_once() {
4956        use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
4957        use std::sync::atomic::{AtomicUsize, Ordering};
4958
4959        let conn = rusqlite::Connection::open_in_memory().unwrap();
4960        conn.execute_batch(
4961            "CREATE TABLE single_prepare_test (id INTEGER PRIMARY KEY, value TEXT NOT NULL)",
4962        )
4963        .unwrap();
4964        let prepare_count = Arc::new(AtomicUsize::new(0));
4965        let hook_count = Arc::clone(&prepare_count);
4966        conn.authorizer(Some(move |context: AuthContext<'_>| {
4967            if matches!(
4968                context.action,
4969                AuthAction::Insert { table_name } if table_name == "single_prepare_test"
4970            ) {
4971                hook_count.fetch_add(1, Ordering::SeqCst);
4972            }
4973            Authorization::Allow
4974        }))
4975        .unwrap();
4976
4977        let mut writer = InlineWriter {
4978            conn: &conn as *const rusqlite::Connection,
4979        };
4980        let affected = block_on_sync(khive_storage::SqlWriter::execute_batch(
4981            &mut writer,
4982            vec![SqlStatement {
4983                sql: "INSERT INTO single_prepare_test (id, value) VALUES (?1, ?2)".into(),
4984                params: vec![SqlValue::Integer(1), SqlValue::Text("once".into())],
4985                label: None,
4986            }],
4987        ))
4988        .expect("InlineWriter operations must resolve on their first poll")
4989        .expect("valid batch must execute");
4990
4991        assert_eq!(affected, 1);
4992        assert_eq!(
4993            prepare_count.load(Ordering::SeqCst),
4994            1,
4995            "classification and execution must share one prepared statement handle"
4996        );
4997        conn.authorizer(None::<fn(AuthContext<'_>) -> Authorization>)
4998            .unwrap();
4999    }
5000
5001    #[test]
5002    fn inline_execute_batch_preserves_schema_dependencies_between_statements() {
5003        let conn = rusqlite::Connection::open_in_memory().unwrap();
5004        let mut writer = InlineWriter {
5005            conn: &conn as *const rusqlite::Connection,
5006        };
5007
5008        let affected = block_on_sync(khive_storage::SqlWriter::execute_batch(
5009            &mut writer,
5010            vec![
5011                SqlStatement {
5012                    sql: "CREATE TABLE dependent_prepare_test (id INTEGER PRIMARY KEY)".into(),
5013                    params: vec![],
5014                    label: None,
5015                },
5016                SqlStatement {
5017                    sql: "INSERT INTO dependent_prepare_test (id) VALUES (1)".into(),
5018                    params: vec![],
5019                    label: None,
5020                },
5021            ],
5022        ))
5023        .expect("InlineWriter operations must resolve on their first poll")
5024        .expect("a later statement must be prepared after its prerequisite schema change");
5025
5026        assert_eq!(affected, 1);
5027        let count: i64 = conn
5028            .query_row("SELECT COUNT(*) FROM dependent_prepare_test", [], |row| {
5029                row.get(0)
5030            })
5031            .unwrap();
5032        assert_eq!(count, 1);
5033    }
5034
5035    #[tokio::test]
5036    async fn pool_backed_query_page_beyond_result_set_returns_empty() {
5037        let config = PoolConfig {
5038            path: None,
5039            ..PoolConfig::default()
5040        };
5041        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5042        {
5043            let writer = pool.writer().unwrap();
5044            writer
5045                .conn()
5046                .execute_batch(
5047                    "CREATE TABLE page_test (id INTEGER PRIMARY KEY, val TEXT NOT NULL);\
5048                     INSERT INTO page_test (id, val) VALUES (1, 'a'), (2, 'b'), (3, 'c');",
5049                )
5050                .unwrap();
5051        }
5052        let bridge = SqlBridge::new(Arc::clone(&pool), false);
5053
5054        let statement = || SqlStatement {
5055            sql: "SELECT val FROM page_test ORDER BY id".into(),
5056            params: vec![],
5057            label: None,
5058        };
5059
5060        let mut reader = bridge.reader().await.unwrap();
5061        let page = reader
5062            .query_page(
5063                statement(),
5064                PageRequest {
5065                    offset: 1,
5066                    limit: 2,
5067                },
5068            )
5069            .await
5070            .unwrap();
5071        assert_eq!(page.len(), 2);
5072        assert!(matches!(page[0].get("val"), Some(SqlValue::Text(v)) if v == "b"));
5073        assert!(matches!(page[1].get("val"), Some(SqlValue::Text(v)) if v == "c"));
5074
5075        let empty = reader
5076            .query_page(
5077                statement(),
5078                PageRequest {
5079                    offset: 99,
5080                    limit: 10,
5081                },
5082            )
5083            .await
5084            .unwrap();
5085        assert!(
5086            empty.is_empty(),
5087            "offset past the last row must return an empty page, got {empty:?}"
5088        );
5089        drop(reader);
5090
5091        let mut writer = bridge.writer().await.unwrap();
5092        let empty = writer
5093            .query_page(
5094                statement(),
5095                PageRequest {
5096                    offset: 99,
5097                    limit: 10,
5098                },
5099            )
5100            .await
5101            .unwrap();
5102        assert!(
5103            empty.is_empty(),
5104            "offset past the last row must return an empty page, got {empty:?}"
5105        );
5106    }
5107
5108    #[tokio::test]
5109    async fn file_bridge_scopes_reader_permits_to_operations_and_caps_writer_handles() {
5110        let dir = tempfile::tempdir().unwrap();
5111        let config = PoolConfig {
5112            path: Some(dir.path().join("sql_bridge_handle_cap.db")),
5113            write_queue_enabled: Some(false),
5114            max_readers: 2,
5115            checkout_timeout: std::time::Duration::from_millis(20),
5116            ..PoolConfig::default()
5117        };
5118        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5119        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5120        let second_bridge = SqlBridge::new(Arc::clone(&pool), true);
5121
5122        let mut retained_readers = Vec::new();
5123        for expected in 0..3 {
5124            let mut reader = second_bridge.reader().await.unwrap();
5125            let value = reader
5126                .query_scalar(SqlStatement {
5127                    sql: format!("SELECT {expected}"),
5128                    params: vec![],
5129                    label: None,
5130                })
5131                .await
5132                .unwrap();
5133            assert!(matches!(value, Some(SqlValue::Integer(value)) if value == expected));
5134            retained_readers.push(reader);
5135        }
5136        assert_eq!(retained_readers.len(), 3);
5137
5138        let mut additional_reader = bridge.reader().await.unwrap();
5139        let page = additional_reader
5140            .query_page(
5141                SqlStatement {
5142                    sql: "WITH RECURSIVE rows(value) AS (\
5143                          SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 9\
5144                          ) SELECT value FROM rows ORDER BY value"
5145                        .into(),
5146                    params: vec![],
5147                    label: None,
5148                },
5149                PageRequest {
5150                    offset: 7,
5151                    limit: 2,
5152                },
5153            )
5154            .await
5155            .unwrap();
5156        assert_eq!(page.len(), 2);
5157        assert!(matches!(page[0].get("value"), Some(SqlValue::Integer(7))));
5158        assert!(matches!(page[1].get("value"), Some(SqlValue::Integer(8))));
5159        drop((additional_reader, retained_readers));
5160
5161        let writer = bridge.writer().await.unwrap();
5162        let writer_error = match second_bridge.writer().await {
5163            Ok(_) => panic!("a second live writer handle exceeded the one-handle cap"),
5164            Err(error) => error,
5165        };
5166        assert!(matches!(
5167            writer_error,
5168            StorageError::AdmissionTimeout { ref operation, .. }
5169                if operation.as_ref() == "sql_bridge.writer_handle"
5170        ));
5171        drop(writer);
5172        let writer_after_release = bridge.writer().await.unwrap();
5173        drop(writer_after_release);
5174    }
5175
5176    #[tokio::test]
5177    #[serial_test::serial(tx_registry)]
5178    async fn file_bridge_attributes_ordinary_pool_reads_and_explicit_transaction_exception() {
5179        let dir = tempfile::tempdir().unwrap();
5180        let pool = Arc::new(
5181            ConnectionPool::new(PoolConfig {
5182                path: Some(dir.path().join("sql_bridge_reader_routes.db")),
5183                max_readers: 1,
5184                ..PoolConfig::default()
5185            })
5186            .unwrap(),
5187        );
5188        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5189        let mut reader = bridge.reader().await.unwrap();
5190
5191        assert_eq!(
5192            pool.reader_acquisition_snapshot(),
5193            crate::pool::ReaderAcquisitionSnapshot {
5194                reader_admission_capacity: 1,
5195                available_reader_admission_slots: 1,
5196                ..crate::pool::ReaderAcquisitionSnapshot::default()
5197            },
5198            "constructing or retaining an idle raw-SQL reader must open nothing"
5199        );
5200
5201        let value = reader
5202            .query_scalar(SqlStatement {
5203                sql: "SELECT 1".into(),
5204                params: vec![],
5205                label: None,
5206            })
5207            .await
5208            .unwrap();
5209        assert!(matches!(value, Some(SqlValue::Integer(1))));
5210        let ordinary = pool.reader_acquisition_snapshot();
5211        assert_eq!(ordinary.pooled_checkouts, 1);
5212        assert_eq!(ordinary.completed_pooled_checkouts, 1);
5213        assert_eq!(ordinary.standalone_opens, 0);
5214
5215        reader
5216            .query_all(SqlStatement {
5217                sql: "BEGIN DEFERRED".into(),
5218                params: vec![],
5219                label: None,
5220            })
5221            .await
5222            .expect("the documented explicit transaction exception opens");
5223        let begun = pool.reader_acquisition_snapshot();
5224        assert_eq!(begun.pooled_checkouts, 1);
5225        assert_eq!(begun.standalone_opens, 1);
5226
5227        reader
5228            .query_scalar(SqlStatement {
5229                sql: "SELECT 2".into(),
5230                params: vec![],
5231                label: None,
5232            })
5233            .await
5234            .expect("transaction query reuses its one exceptional connection");
5235        reader
5236            .query_all(SqlStatement {
5237                sql: "COMMIT".into(),
5238                params: vec![],
5239                label: None,
5240            })
5241            .await
5242            .expect("COMMIT closes the explicit transaction exception");
5243        let committed = pool.reader_acquisition_snapshot();
5244        assert_eq!(committed.reader_admission_capacity, 1);
5245        assert_eq!(committed.available_reader_admission_slots, 1);
5246        assert_eq!(committed.acquisitions, begun.acquisitions);
5247        assert_eq!(committed.pooled_checkouts, begun.pooled_checkouts);
5248        assert_eq!(committed.standalone_opens, begun.standalone_opens);
5249        assert_eq!(
5250            committed.completed_pooled_checkouts, begun.completed_pooled_checkouts,
5251            "queries and COMMIT inside one explicit transaction must not acquire another reader"
5252        );
5253
5254        reader
5255            .query_scalar(SqlStatement {
5256                sql: "SELECT 3".into(),
5257                params: vec![],
5258                label: None,
5259            })
5260            .await
5261            .expect("ordinary traffic returns to the reader pool after COMMIT");
5262        let after = pool.reader_acquisition_snapshot();
5263        assert_eq!(after.pooled_checkouts, 2);
5264        assert_eq!(after.completed_pooled_checkouts, 2);
5265        assert_eq!(after.standalone_opens, 1);
5266        assert_eq!(after.active_pooled_checkouts, 0);
5267    }
5268
5269    #[tokio::test]
5270    async fn explicit_reader_open_timeout_is_visible_without_a_standalone_fallback() {
5271        let dir = tempfile::tempdir().unwrap();
5272        let pool = Arc::new(
5273            ConnectionPool::new(PoolConfig {
5274                path: Some(dir.path().join("sql_bridge_reader_open_timeout.db")),
5275                max_readers: 1,
5276                checkout_timeout: std::time::Duration::from_millis(20),
5277                ..PoolConfig::default()
5278            })
5279            .unwrap(),
5280        );
5281        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5282        let mut logical_reader = bridge.reader().await.unwrap();
5283        let held = pool.reader().expect("hold the shared reader budget");
5284
5285        let blocked = logical_reader
5286            .query_all(SqlStatement {
5287                sql: "BEGIN DEFERRED".into(),
5288                params: vec![],
5289                label: None,
5290            })
5291            .await;
5292        assert!(
5293            matches!(
5294                &blocked,
5295                Err(StorageError::Timeout { operation })
5296                    if operation.as_ref() == "sql_bridge.reader_open"
5297            ),
5298            "the compatible explicit-transaction open phase must stay visible; got {blocked:?}"
5299        );
5300
5301        let snapshot = pool.reader_acquisition_snapshot();
5302        assert_eq!(snapshot.checkout_timeouts, 1);
5303        assert_eq!(snapshot.pooled_checkouts, 1);
5304        assert_eq!(snapshot.standalone_opens, 0);
5305        assert_eq!(snapshot.active_pooled_checkouts, 1);
5306        assert_eq!(snapshot.available_reader_admission_slots, 0);
5307        drop(held);
5308    }
5309
5310    #[tokio::test]
5311    #[serial_test::serial(tx_registry)]
5312    async fn cached_read_transaction_retains_one_permit_until_commit_or_rollback() {
5313        let dir = tempfile::tempdir().unwrap();
5314        let config = PoolConfig {
5315            path: Some(dir.path().join("sql_bridge_reader_tx_control.db")),
5316            write_queue_enabled: Some(true),
5317            max_readers: 1,
5318            checkout_timeout: std::time::Duration::from_millis(20),
5319            ..PoolConfig::default()
5320        };
5321        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5322        let origin = pool.origin();
5323        let origin_view = database_tx_view(&pool);
5324        let unrelated_view = khive_storage::tx_registry::TxOriginFilter::Secondary(
5325            khive_storage::tx_registry::DbIdentity::new("unrelated-sql-bridge.db"),
5326        );
5327        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5328        let mut reader = bridge.reader().await.unwrap();
5329        let mut contender = bridge.reader().await.unwrap();
5330
5331        assert!(
5332            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5333            "an idle cached reader must not register a transaction"
5334        );
5335
5336        reader
5337            .query_all(SqlStatement {
5338                sql: "BEGIN DEFERRED".into(),
5339                params: vec![],
5340                label: None,
5341            })
5342            .await
5343            .expect("BEGIN DEFERRED must open an admitted cached-reader snapshot");
5344        let opened = khive_storage::tx_registry::oldest_for(&origin_view)
5345            .expect("successful BEGIN must register the cached-reader transaction");
5346        assert_eq!(opened.label.as_deref(), Some(CACHED_READ_TRANSACTION_LABEL));
5347        assert_eq!(opened.origin, origin);
5348        assert!(
5349            khive_storage::tx_registry::oldest_for(&unrelated_view).is_none(),
5350            "the read transaction must be attributed only to its own backend"
5351        );
5352        assert_eq!(
5353            pool.sql_bridge_reader_slots().available_permits(),
5354            0,
5355            "the successful BEGIN must retain its operation permit"
5356        );
5357
5358        let value = reader
5359            .query_scalar(SqlStatement {
5360                sql: "SELECT 7".into(),
5361                params: vec![],
5362                label: None,
5363            })
5364            .await
5365            .expect("a query inside the admitted transaction must reuse its retained permit");
5366        assert!(matches!(value, Some(SqlValue::Integer(7))));
5367        assert_eq!(
5368            khive_storage::tx_registry::oldest_for(&origin_view)
5369                .expect("queries must retain the transaction registration")
5370                .id,
5371            opened.id,
5372            "queries inside the transaction must retain the original span"
5373        );
5374
5375        let blocked = contender
5376            .query_scalar(SqlStatement {
5377                sql: "SELECT 8".into(),
5378                params: vec![],
5379                label: None,
5380            })
5381            .await;
5382        assert!(
5383            matches!(
5384                &blocked,
5385                Err(StorageError::AdmissionTimeout { operation, .. })
5386                    if operation.as_ref() == "query_row"
5387            ),
5388            "a second logical read must contend with the admitted transaction \
5389             and fail at the bounded pooled-admission stage; got {blocked:?}"
5390        );
5391
5392        reader
5393            .query_all(SqlStatement {
5394                sql: "COMMIT".into(),
5395                params: vec![],
5396                label: None,
5397            })
5398            .await
5399            .expect("COMMIT must close the admitted cached-reader snapshot");
5400        assert!(
5401            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5402            "COMMIT must deregister after SQLite returns to autocommit"
5403        );
5404        assert_eq!(
5405            pool.sql_bridge_reader_slots().available_permits(),
5406            1,
5407            "COMMIT may release the permit only after autocommit is restored"
5408        );
5409        let value = contender
5410            .query_scalar(SqlStatement {
5411                sql: "SELECT 8".into(),
5412                params: vec![],
5413                label: None,
5414            })
5415            .await
5416            .expect("the contender must run after COMMIT releases admission");
5417        assert!(matches!(value, Some(SqlValue::Integer(8))));
5418
5419        reader
5420            .query_all(SqlStatement {
5421                sql: "BEGIN TRANSACTION".into(),
5422                params: vec![],
5423                label: None,
5424            })
5425            .await
5426            .expect("plain deferred BEGIN TRANSACTION must also be admitted");
5427        let reopened = khive_storage::tx_registry::oldest_for(&origin_view)
5428            .expect("the second successful BEGIN must register a fresh span");
5429        assert_ne!(reopened.id, opened.id);
5430        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 0);
5431        let nested = reader
5432            .query_all(SqlStatement {
5433                sql: "ROLLBACK TO stale_snapshot".into(),
5434                params: vec![],
5435                label: None,
5436            })
5437            .await;
5438        assert!(
5439            matches!(&nested, Err(StorageError::InvalidInput { .. })),
5440            "ROLLBACK TO requires unsupported nested state; got {nested:?}"
5441        );
5442        assert_eq!(
5443            pool.sql_bridge_reader_slots().available_permits(),
5444            0,
5445            "rejected nested control must not release the still-live transaction admission"
5446        );
5447        assert_eq!(
5448            khive_storage::tx_registry::oldest_for(&origin_view)
5449                .expect("ROLLBACK TO rejection must retain the live span")
5450                .id,
5451            reopened.id
5452        );
5453        let savepoint = reader
5454            .query_all(SqlStatement {
5455                sql: "SAVEPOINT nested_snapshot".into(),
5456                params: vec![],
5457                label: None,
5458            })
5459            .await;
5460        assert!(
5461            matches!(&savepoint, Err(StorageError::InvalidInput { .. })),
5462            "SAVEPOINT must be rejected inside the admitted transaction; got {savepoint:?}"
5463        );
5464        assert_eq!(
5465            khive_storage::tx_registry::oldest_for(&origin_view)
5466                .expect("SAVEPOINT rejection must retain the live span")
5467                .id,
5468            reopened.id
5469        );
5470        reader
5471            .query_all(SqlStatement {
5472                sql: "ROLLBACK".into(),
5473                params: vec![],
5474                label: None,
5475            })
5476            .await
5477            .expect("ROLLBACK must close the admitted cached-reader snapshot");
5478        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
5479        assert!(
5480            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5481            "full ROLLBACK must deregister after SQLite returns to autocommit"
5482        );
5483    }
5484
5485    #[tokio::test]
5486    async fn failed_cached_reader_begin_does_not_register_a_transaction() {
5487        use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
5488
5489        fn deny_begin(ctx: AuthContext<'_>) -> Authorization {
5490            match ctx.action {
5491                AuthAction::Transaction {
5492                    operation: TransactionOperation::Begin,
5493                } => Authorization::Deny,
5494                _ => Authorization::Allow,
5495            }
5496        }
5497
5498        let dir = tempfile::tempdir().unwrap();
5499        let config = PoolConfig {
5500            path: Some(dir.path().join("sql_bridge_reader_failed_begin.db")),
5501            max_readers: 1,
5502            ..PoolConfig::default()
5503        };
5504        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5505        let origin_view = database_tx_view(&pool);
5506        let conn = open_standalone_reader(&pool).unwrap();
5507        conn.authorizer(Some(deny_begin)).unwrap();
5508        let mut reader = SqliteReader {
5509            handle: Some(StandaloneHandle {
5510                conn,
5511                _retained_slot: None,
5512                read_transaction_slot: None,
5513            }),
5514            pool: Arc::clone(&pool),
5515            poisoned: false,
5516        };
5517
5518        let begin = reader
5519            .query_all(SqlStatement {
5520                sql: "BEGIN DEFERRED".into(),
5521                params: vec![],
5522                label: None,
5523            })
5524            .await;
5525        assert!(begin.is_err(), "the authorizer must reject BEGIN");
5526        assert!(
5527            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5528            "a failed BEGIN must never enter the transaction registry"
5529        );
5530        assert_eq!(
5531            pool.sql_bridge_reader_slots().available_permits(),
5532            1,
5533            "a failed BEGIN must return the operation permit"
5534        );
5535    }
5536
5537    #[tokio::test]
5538    #[serial_test::serial(tx_registry)]
5539    async fn failed_cached_reader_rollback_deregisters_only_when_connection_is_discarded() {
5540        use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
5541
5542        fn deny_rollback(ctx: AuthContext<'_>) -> Authorization {
5543            match ctx.action {
5544                AuthAction::Transaction {
5545                    operation: TransactionOperation::Rollback,
5546                } => Authorization::Deny,
5547                _ => Authorization::Allow,
5548            }
5549        }
5550
5551        let dir = tempfile::tempdir().unwrap();
5552        let config = PoolConfig {
5553            path: Some(dir.path().join("sql_bridge_reader_failed_rollback.db")),
5554            max_readers: 1,
5555            ..PoolConfig::default()
5556        };
5557        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5558        let origin_view = database_tx_view(&pool);
5559        let conn = open_standalone_reader(&pool).unwrap();
5560        let mut reader = SqliteReader {
5561            handle: Some(StandaloneHandle {
5562                conn,
5563                _retained_slot: None,
5564                read_transaction_slot: None,
5565            }),
5566            pool: Arc::clone(&pool),
5567            poisoned: false,
5568        };
5569
5570        reader
5571            .query_all(SqlStatement {
5572                sql: "BEGIN DEFERRED".into(),
5573                params: vec![],
5574                label: None,
5575            })
5576            .await
5577            .expect("BEGIN must establish the registered transaction");
5578        let opened = khive_storage::tx_registry::oldest_for(&origin_view)
5579            .expect("the admitted transaction must be registered");
5580        reader
5581            .handle
5582            .as_ref()
5583            .expect("reader must retain its connection")
5584            .conn
5585            .authorizer(Some(deny_rollback))
5586            .unwrap();
5587
5588        let rollback = reader
5589            .query_all(SqlStatement {
5590                sql: "ROLLBACK".into(),
5591                params: vec![],
5592                label: None,
5593            })
5594            .await;
5595        assert!(rollback.is_err(), "the authorizer must reject ROLLBACK");
5596        assert_eq!(
5597            khive_storage::tx_registry::oldest_for(&origin_view)
5598                .expect("failed ROLLBACK must retain registry evidence")
5599                .id,
5600            opened.id
5601        );
5602        assert_eq!(
5603            pool.sql_bridge_reader_slots().available_permits(),
5604            0,
5605            "failed ROLLBACK must retain reader admission"
5606        );
5607
5608        drop(reader);
5609        assert!(
5610            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5611            "discarding the connection must not leak its registry entry"
5612        );
5613        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
5614    }
5615
5616    #[tokio::test]
5617    #[serial_test::serial(tx_registry)]
5618    async fn cached_read_only_handles_reject_unsupported_transaction_control_without_consumption() {
5619        let dir = tempfile::tempdir().unwrap();
5620        let config = PoolConfig {
5621            path: Some(
5622                dir.path()
5623                    .join("sql_bridge_reader_unsupported_tx_control.db"),
5624            ),
5625            write_queue_enabled: Some(true),
5626            max_readers: 1,
5627            ..PoolConfig::default()
5628        };
5629        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5630        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5631
5632        let mut reader = bridge.reader().await.unwrap();
5633        for (sql, keyword) in [
5634            ("BEGIN IMMEDIATE", "BEGIN"),
5635            ("BEGIN EXCLUSIVE", "BEGIN"),
5636            // Trailing-mode spellings parse in SQLite as a NAMED deferred
5637            // transaction, but the mode keyword in name position reads as
5638            // lock intent; the classifier must refuse rather than launder
5639            // them into a deferred start the cached reader would then hold.
5640            ("BEGIN TRANSACTION IMMEDIATE", "BEGIN"),
5641            ("BEGIN TRANSACTION EXCLUSIVE", "BEGIN"),
5642            ("BEGIN DEFERRED TRANSACTION trailing", "BEGIN"),
5643            // Quoted/bracketed tails tokenize as no identifier at all, so a
5644            // classifier that stops at the tokenizer's `None` reads them as
5645            // an accepted form's end. They must be refused exactly like the
5646            // bare-word spellings.
5647            ("BEGIN TRANSACTION \"IMMEDIATE\"", "BEGIN"),
5648            ("BEGIN TRANSACTION [IMMEDIATE]", "BEGIN"),
5649            ("BEGIN TRANSACTION `IMMEDIATE`", "BEGIN"),
5650            ("BEGIN TRANSACTION 'IMMEDIATE'", "BEGIN"),
5651            ("BEGIN \"DEFERRED\"", "BEGIN"),
5652            ("BEGIN; COMMIT", "BEGIN"),
5653            ("START TRANSACTION", "START"),
5654            ("COMMIT", "COMMIT"),
5655        ] {
5656            let rejected = reader
5657                .query_all(SqlStatement {
5658                    sql: sql.into(),
5659                    params: vec![],
5660                    label: None,
5661                })
5662                .await;
5663            assert!(
5664                matches!(
5665                    &rejected,
5666                    Err(StorageError::InvalidInput { message, .. })
5667                        if message.contains(keyword)
5668                ),
5669                "unsupported cached-reader control {sql:?} must fail closed; got {rejected:?}"
5670            );
5671            assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
5672        }
5673
5674        let mut queue_backed_writer = bridge.writer().await.unwrap();
5675        let rejected = queue_backed_writer
5676            .query_all(SqlStatement {
5677                sql: "SAVEPOINT stale_snapshot".into(),
5678                params: vec![],
5679                label: None,
5680            })
5681            .await;
5682        assert!(
5683            matches!(
5684                &rejected,
5685                Err(StorageError::InvalidInput {
5686                    operation,
5687                    message,
5688                    ..
5689                }) if operation.as_ref() == "writer.query_all"
5690                    && message.contains("transaction control")
5691                    && message.contains("SAVEPOINT")
5692            ),
5693            "a queue-backed writer without an explicit read transaction must reject nested \
5694             transaction control; got {rejected:?}"
5695        );
5696        assert_eq!(
5697            pool.sql_bridge_reader_slots().available_permits(),
5698            1,
5699            "queue-backed rejection must leave the operation permit available"
5700        );
5701        let value = queue_backed_writer
5702            .query_scalar(SqlStatement {
5703                sql: "SELECT 8".into(),
5704                params: vec![],
5705                label: None,
5706            })
5707            .await
5708            .expect("transaction-control rejection must not consume the queue-backed handle");
5709        assert!(matches!(value, Some(SqlValue::Integer(8))));
5710
5711        queue_backed_writer
5712            .query_all(SqlStatement {
5713                sql: "BEGIN DEFERRED".into(),
5714                params: vec![],
5715                label: None,
5716            })
5717            .await
5718            .expect("queue-backed cached reader must share explicit read-transaction admission");
5719        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 0);
5720        queue_backed_writer
5721            .query_all(SqlStatement {
5722                sql: "END".into(),
5723                params: vec![],
5724                label: None,
5725            })
5726            .await
5727            .expect("END must release queue-backed cached-reader admission");
5728        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
5729    }
5730
5731    #[tokio::test]
5732    #[serial_test::serial(tx_registry)]
5733    async fn dropping_cached_reader_transaction_closes_snapshot_before_releasing_permit() {
5734        let dir = tempfile::tempdir().unwrap();
5735        let config = PoolConfig {
5736            path: Some(dir.path().join("sql_bridge_reader_tx_drop.db")),
5737            max_readers: 1,
5738            checkout_timeout: std::time::Duration::from_millis(20),
5739            ..PoolConfig::default()
5740        };
5741        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5742        let origin_view = database_tx_view(&pool);
5743        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5744        let mut reader = bridge.reader().await.unwrap();
5745        let mut contender = bridge.reader().await.unwrap();
5746
5747        reader
5748            .query_all(SqlStatement {
5749                sql: "BEGIN DEFERRED".into(),
5750                params: vec![],
5751                label: None,
5752            })
5753            .await
5754            .expect("begin admitted transaction");
5755        reader
5756            .query_all(SqlStatement {
5757                sql: "SELECT * FROM sqlite_schema".into(),
5758                params: vec![],
5759                label: None,
5760            })
5761            .await
5762            .expect("materialize read snapshot");
5763        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 0);
5764        assert!(
5765            khive_storage::tx_registry::oldest_for(&origin_view).is_some(),
5766            "the live snapshot must remain registered until handle drop"
5767        );
5768
5769        drop(reader);
5770        assert!(
5771            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5772            "handle drop must close SQLite before deregistering the snapshot"
5773        );
5774        assert_eq!(
5775            pool.sql_bridge_reader_slots().available_permits(),
5776            1,
5777            "dropping the handle must close its transaction before returning admission"
5778        );
5779        contender
5780            .query_all(SqlStatement {
5781                sql: "SELECT * FROM sqlite_schema".into(),
5782                params: vec![],
5783                label: None,
5784            })
5785            .await
5786            .expect("a new operation must run after the transactional handle drops");
5787    }
5788
5789    /// #1846 regression: a cached-reader explicit read transaction that is
5790    /// never explicitly finished (a stuck/leaked caller that keeps reusing
5791    /// the handle without COMMIT/ROLLBACK) would otherwise pin the WAL
5792    /// snapshot open for as long as the caller kept calling in. Without the
5793    /// age check this reddens: the second `query_all` would return the row
5794    /// materialized inside the still-open transaction instead of an error,
5795    /// and `tx_registry::oldest_for` would keep reporting the same span
5796    /// open past `read_tx_max_age`.
5797    #[tokio::test]
5798    #[serial_test::serial(tx_registry)]
5799    async fn expired_cached_reader_transaction_is_rolled_back_on_reuse() {
5800        let dir = tempfile::tempdir().unwrap();
5801        let config = PoolConfig {
5802            path: Some(dir.path().join("sql_bridge_reader_tx_max_age.db")),
5803            max_readers: 1,
5804            checkout_timeout: std::time::Duration::from_millis(20),
5805            read_tx_max_age: std::time::Duration::from_millis(20),
5806            ..PoolConfig::default()
5807        };
5808        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5809        let origin_view = database_tx_view(&pool);
5810        let bridge = SqlBridge::new(Arc::clone(&pool), true);
5811        let mut reader = bridge.reader().await.unwrap();
5812
5813        reader
5814            .query_all(SqlStatement {
5815                sql: "BEGIN DEFERRED".into(),
5816                params: vec![],
5817                label: None,
5818            })
5819            .await
5820            .expect("begin admitted transaction");
5821        reader
5822            .query_all(SqlStatement {
5823                sql: "SELECT * FROM sqlite_schema".into(),
5824                params: vec![],
5825                label: None,
5826            })
5827            .await
5828            .expect("materialize read snapshot");
5829        assert!(
5830            khive_storage::tx_registry::oldest_for(&origin_view).is_some(),
5831            "the open transaction must be registered before it ages out"
5832        );
5833
5834        tokio::time::sleep(std::time::Duration::from_millis(40)).await;
5835
5836        let evictions_before = crate::checkpoint::read_tx_max_age_evictions();
5837        let error = reader
5838            .query_all(SqlStatement {
5839                sql: "SELECT * FROM sqlite_schema".into(),
5840                params: vec![],
5841                label: None,
5842            })
5843            .await
5844            .expect_err("reusing a transaction past read_tx_max_age must be refused");
5845        assert!(
5846            error.is_retryable(),
5847            "an evicted-transaction error must be retryable so the caller can open a fresh \
5848             snapshot: {error}"
5849        );
5850        match &error {
5851            StorageError::ReadTransactionAgeEvicted {
5852                operation,
5853                max_age_secs,
5854            } => {
5855                assert_eq!(operation.as_ref(), "query_all");
5856                assert_eq!(
5857                    *max_age_secs, 0,
5858                    "a 20ms read_tx_max_age truncates to 0 whole seconds"
5859                );
5860            }
5861            other => panic!(
5862                "a clean age-triggered rollback must surface the dedicated \
5863                 ReadTransactionAgeEvicted variant, not a generic classification: {other:?}"
5864            ),
5865        }
5866        assert_eq!(
5867            crate::checkpoint::read_tx_max_age_evictions(),
5868            evictions_before + 1,
5869            "the eviction must be counted in the #1846 diagnostics gauge"
5870        );
5871        assert!(
5872            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
5873            "the expired transaction must be rolled back and deregistered rather than \
5874             continuing to pin the WAL snapshot"
5875        );
5876
5877        reader
5878            .query_all(SqlStatement {
5879                sql: "SELECT * FROM sqlite_schema".into(),
5880                params: vec![],
5881                label: None,
5882            })
5883            .await
5884            .expect("the handle must remain usable for a fresh autocommit read after eviction");
5885    }
5886
5887    /// #1846 follow-up: the age-eviction branch
5888    /// has a rollback-failure path distinct from
5889    /// `failed_cached_reader_rollback_deregisters_only_when_connection_is_discarded`
5890    /// above (which covers an explicit caller-issued `ROLLBACK`, not the
5891    /// age-triggered cleanup rollback). When SQLite denies the age-triggered
5892    /// `ROLLBACK`, the branch must still discard the poisoned connection,
5893    /// deregister the expired transaction span, and release the reader
5894    /// admission permit rather than leaking either.
5895    #[tokio::test]
5896    #[serial_test::serial(tx_registry)]
5897    async fn expired_cached_reader_transaction_rollback_denial_discards_connection_and_releases_admission(
5898    ) {
5899        use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
5900
5901        fn deny_rollback(ctx: AuthContext<'_>) -> Authorization {
5902            match ctx.action {
5903                AuthAction::Transaction {
5904                    operation: TransactionOperation::Rollback,
5905                } => Authorization::Deny,
5906                _ => Authorization::Allow,
5907            }
5908        }
5909
5910        let dir = tempfile::tempdir().unwrap();
5911        let config = PoolConfig {
5912            path: Some(
5913                dir.path()
5914                    .join("sql_bridge_reader_tx_max_age_rollback_denied.db"),
5915            ),
5916            max_readers: 1,
5917            checkout_timeout: std::time::Duration::from_millis(20),
5918            read_tx_max_age: std::time::Duration::from_millis(20),
5919            ..PoolConfig::default()
5920        };
5921        let pool = Arc::new(ConnectionPool::new(config).unwrap());
5922        let origin_view = database_tx_view(&pool);
5923        let conn = open_standalone_reader(&pool).unwrap();
5924        let mut reader = SqliteReader {
5925            handle: Some(StandaloneHandle {
5926                conn,
5927                _retained_slot: None,
5928                read_transaction_slot: None,
5929            }),
5930            pool: Arc::clone(&pool),
5931            poisoned: false,
5932        };
5933
5934        reader
5935            .query_all(SqlStatement {
5936                sql: "BEGIN DEFERRED".into(),
5937                params: vec![],
5938                label: None,
5939            })
5940            .await
5941            .expect("begin admitted transaction");
5942        reader
5943            .query_all(SqlStatement {
5944                sql: "SELECT * FROM sqlite_schema".into(),
5945                params: vec![],
5946                label: None,
5947            })
5948            .await
5949            .expect("materialize read snapshot");
5950        assert!(
5951            khive_storage::tx_registry::oldest_for(&origin_view).is_some(),
5952            "the open transaction must be registered before it ages out"
5953        );
5954
5955        reader
5956            .handle
5957            .as_ref()
5958            .expect("reader must retain its connection")
5959            .conn
5960            .authorizer(Some(deny_rollback))
5961            .unwrap();
5962
5963        tokio::time::sleep(std::time::Duration::from_millis(40)).await;
5964
5965        let evictions_before = crate::checkpoint::read_tx_max_age_evictions();
5966        let error = reader
5967            .query_all(SqlStatement {
5968                sql: "SELECT * FROM sqlite_schema".into(),
5969                params: vec![],
5970                label: None,
5971            })
5972            .await
5973            .expect_err("a denied rollback on an expired transaction must surface an error");
5974        assert!(
5975            error.is_retryable(),
5976            "even a failed cleanup rollback must remain classified retryable so callers open a \
5977             fresh handle: {error}"
5978        );
5979        match &error {
5980            StorageError::ReadTransactionAgeEvictionCleanupFailed {
5981                operation,
5982                max_age_secs,
5983                message,
5984            } => {
5985                assert_eq!(operation.as_ref(), "query_all");
5986                assert_eq!(
5987                    *max_age_secs, 0,
5988                    "a 20ms read_tx_max_age truncates to 0 whole seconds"
5989                );
5990                assert!(
5991                    message.contains("rollback failed"),
5992                    "the failure must be attributable to the denied ROLLBACK, not silent \
5993                     success: {message}"
5994                );
5995            }
5996            other => panic!(
5997                "a denied cleanup rollback must surface the dedicated \
5998                 ReadTransactionAgeEvictionCleanupFailed variant, not a generic Transaction \
5999                 error the caller cannot machine-detect: {other:?}"
6000            ),
6001        }
6002        assert_eq!(
6003            crate::checkpoint::read_tx_max_age_evictions(),
6004            evictions_before + 1,
6005            "the eviction attempt must still be counted even though cleanup failed"
6006        );
6007        assert!(
6008            khive_storage::tx_registry::oldest_for(&origin_view).is_none(),
6009            "a denied rollback must discard the connection and deregister the expired \
6010             transaction span rather than leaking it"
6011        );
6012        assert_eq!(
6013            pool.sql_bridge_reader_slots().available_permits(),
6014            1,
6015            "discarding the poisoned connection must release the reader admission slot"
6016        );
6017
6018        let reuse = reader
6019            .query_all(SqlStatement {
6020                sql: "SELECT * FROM sqlite_schema".into(),
6021                params: vec![],
6022                label: None,
6023            })
6024            .await;
6025        let message = match reuse {
6026            Err(StorageError::Pool { message, .. }) => message,
6027            other => panic!(
6028                "reusing this discarded reader must fail loudly with 'connection already \
6029                 consumed' rather than silently reopening; got {other:?}"
6030            ),
6031        };
6032        assert!(
6033            message.contains("connection already consumed"),
6034            "expected the discarded reader's reuse error to name the pinned failure; got \
6035             {message:?}"
6036        );
6037    }
6038
6039    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6040    #[serial_test::serial(tx_registry)]
6041    async fn cancelled_cached_reader_transaction_releases_guards_after_connection_closes() {
6042        let dir = tempfile::tempdir().unwrap();
6043        let config = PoolConfig {
6044            path: Some(dir.path().join("sql_bridge_reader_tx_drop_cancel.db")),
6045            max_readers: 1,
6046            checkout_timeout: std::time::Duration::from_millis(50),
6047            ..PoolConfig::default()
6048        };
6049        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6050        let origin_view = database_tx_view(&pool);
6051        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6052        let mut reader = SqliteReader {
6053            handle: Some(
6054                open_explicit_read_transaction_handle(Arc::clone(&pool))
6055                    .await
6056                    .unwrap(),
6057            ),
6058            pool: Arc::clone(&pool),
6059            poisoned: false,
6060        };
6061        let mut contender = bridge.reader().await.unwrap();
6062
6063        reader
6064            .query_all(SqlStatement {
6065                sql: "BEGIN DEFERRED".into(),
6066                params: vec![],
6067                label: None,
6068            })
6069            .await
6070            .expect("begin admitted transaction");
6071        assert!(
6072            khive_storage::tx_registry::oldest_for(&origin_view).is_some(),
6073            "the explicit transaction must be registered before cancellation"
6074        );
6075        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 0);
6076
6077        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6078        let query = tokio::spawn(crate::scope_test_read_progress(
6079            Arc::clone(&progress),
6080            async move { reader.query_all(deliberately_slow_read_statement()).await },
6081        ));
6082        wait_for_progress(progress.as_ref()).await;
6083        query.abort();
6084        assert!(matches!(query.await, Err(error) if error.is_cancelled()));
6085        tokio::time::timeout(std::time::Duration::from_secs(1), async {
6086            while khive_storage::tx_registry::oldest_for(&origin_view).is_some()
6087                || pool.sql_bridge_reader_slots().available_permits() != 1
6088            {
6089                tokio::task::yield_now().await;
6090            }
6091        })
6092        .await
6093        .expect("connection cleanup leaked transaction evidence or reader admission");
6094        contender
6095            .query_all(SqlStatement {
6096                sql: "SELECT 1".into(),
6097                params: vec![],
6098                label: None,
6099            })
6100            .await
6101            .expect("admission must recover after the cancelled connection closes");
6102    }
6103
6104    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6105    #[serial_test::serial(tx_registry)]
6106    async fn cancelled_cached_reader_rolls_back_releases_wal_and_clears_handler() {
6107        let dir = tempfile::tempdir().unwrap();
6108        let config = PoolConfig {
6109            path: Some(dir.path().join("sql_bridge_reader_tx_request_cancel.db")),
6110            max_readers: 1,
6111            checkout_timeout: std::time::Duration::from_millis(500),
6112            ..PoolConfig::default()
6113        };
6114        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6115        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6116        let writer = open_standalone_writer(&pool).unwrap();
6117        writer
6118            .execute_batch(
6119                "CREATE TABLE snapshot_probe(id INTEGER PRIMARY KEY, value TEXT NOT NULL); \
6120                 INSERT INTO snapshot_probe(value) VALUES ('seed');",
6121            )
6122            .unwrap();
6123        let mut reader = bridge.reader().await.unwrap();
6124        let mut contender = bridge.reader().await.unwrap();
6125
6126        reader
6127            .query_all(SqlStatement {
6128                sql: "BEGIN DEFERRED".into(),
6129                params: vec![],
6130                label: None,
6131            })
6132            .await
6133            .expect("begin admitted transaction");
6134        reader
6135            .query_all(SqlStatement {
6136                sql: "SELECT * FROM snapshot_probe".into(),
6137                params: vec![],
6138                label: None,
6139            })
6140            .await
6141            .expect("materialize a real WAL snapshot");
6142        writer
6143            .execute_batch(
6144                "WITH RECURSIVE rows(value) AS (\
6145                 SELECT 1 UNION ALL SELECT value + 1 FROM rows WHERE value < 100\
6146                 ) INSERT INTO snapshot_probe(value) SELECT printf('row-%d', value) FROM rows;",
6147            )
6148            .unwrap();
6149        let (_, log_before, checkpointed_before) = passive_checkpoint(&writer);
6150        assert!(
6151            log_before > checkpointed_before,
6152            "the explicit reader snapshot must pin WAL frames before cancellation"
6153        );
6154
6155        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6156        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
6157        let query = tokio::spawn(crate::scope_test_read_progress(
6158            Arc::clone(&progress),
6159            crate::scope_request_read_cancellation(cancel_rx, async move {
6160                let result = reader.query_all(deliberately_slow_read_statement()).await;
6161                (reader, result)
6162            }),
6163        ));
6164        wait_for_progress(progress.as_ref()).await;
6165        cancel_tx.send(true).unwrap();
6166        let (mut reader, result) = tokio::time::timeout(std::time::Duration::from_secs(1), query)
6167            .await
6168            .expect("interrupted explicit read transaction did not stop promptly")
6169            .unwrap();
6170        assert!(
6171            matches!(result, Err(StorageError::Timeout { .. })),
6172            "request cancellation must surface as a typed timeout; got {result:?}"
6173        );
6174        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
6175
6176        let (_, log_after, checkpointed_after) = passive_checkpoint(&writer);
6177        assert_eq!(
6178            log_after, checkpointed_after,
6179            "cancellation must release the explicit reader's WAL snapshot"
6180        );
6181        contender
6182            .query_all(SqlStatement {
6183                sql: "SELECT 1".into(),
6184                params: vec![],
6185                label: None,
6186            })
6187            .await
6188            .expect("the sole reader permit must be reusable after rollback");
6189
6190        let stopped_at = progress.load(std::sync::atomic::Ordering::SeqCst);
6191        reader
6192            .query_all(SqlStatement {
6193                sql: "WITH RECURSIVE rows(value) AS (\
6194                      SELECT 0 UNION ALL SELECT value + 1 FROM rows WHERE value < 10000\
6195                      ) SELECT SUM(value) FROM rows"
6196                    .into(),
6197                params: vec![],
6198                label: None,
6199            })
6200            .await
6201            .expect("same connection must remain usable after handler teardown");
6202        assert_eq!(
6203            progress.load(std::sync::atomic::Ordering::SeqCst),
6204            stopped_at,
6205            "the cancelled request's progress callback bled into the next borrower"
6206        );
6207    }
6208
6209    /// A statement that blocks inside a single SQLite VM step for longer
6210    /// than [`crate::read_cancellation::DEFAULT_SQLITE_INTERRUPT_GRACE_MS`].
6211    /// Unlike a recursive CTE (interrupt-checked every 1,000 VM
6212    /// instructions, so it stops promptly), a UDF call is one opcode: SQLite
6213    /// cannot observe the interrupt flag until the call returns. This is the
6214    /// only way to deterministically force a worker past the grace window
6215    /// rather than merely past `wait_for_progress`.
6216    fn register_khive_test_slow_udf(
6217        conn: &rusqlite::Connection,
6218        sleep_ms: u64,
6219        started: Arc<std::sync::atomic::AtomicBool>,
6220    ) {
6221        conn.create_scalar_function(
6222            "khive_test_slow_udf",
6223            0,
6224            rusqlite::functions::FunctionFlags::SQLITE_UTF8,
6225            move |_| {
6226                started.store(true, std::sync::atomic::Ordering::Release);
6227                std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
6228                Ok(0i64)
6229            },
6230        )
6231        .unwrap();
6232    }
6233
6234    async fn wait_for_flag(flag: &std::sync::atomic::AtomicBool) {
6235        tokio::time::timeout(std::time::Duration::from_secs(1), async {
6236            while !flag.load(std::sync::atomic::Ordering::Acquire) {
6237                tokio::task::yield_now().await;
6238            }
6239        })
6240        .await
6241        .expect("slow UDF never started");
6242    }
6243
6244    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6245    async fn abandoned_read_past_grace_recovers_admission_after_bounded_join() {
6246        // Regression for the PR #1897 review blocker: a `spawn_blocking`
6247        // read worker that outlives `KHIVE_SQLITE_INTERRUPT_GRACE_MS` must
6248        // not be treated as reaped just because the async side detached
6249        // from it. This forces the worker past grace with a slow UDF (so
6250        // the interrupt genuinely cannot be observed mid-call) and asserts
6251        // that admission and the WAL snapshot are only reported recovered
6252        // once the real worker has actually joined.
6253        let dir = tempfile::tempdir().unwrap();
6254        let config = PoolConfig {
6255            path: Some(dir.path().join("sql_bridge_grace_exceeded.db")),
6256            max_readers: 1,
6257            checkout_timeout: std::time::Duration::from_millis(2_000),
6258            ..PoolConfig::default()
6259        };
6260        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6261        let writer = open_standalone_writer(&pool).unwrap();
6262        writer
6263            .execute_batch(
6264                "CREATE TABLE grace_probe(id INTEGER PRIMARY KEY, value TEXT NOT NULL); \
6265                 INSERT INTO grace_probe(value) VALUES ('seed');",
6266            )
6267            .unwrap();
6268
6269        let mut reader = SqliteReader {
6270            handle: Some(
6271                open_explicit_read_transaction_handle(Arc::clone(&pool))
6272                    .await
6273                    .unwrap(),
6274            ),
6275            pool: Arc::clone(&pool),
6276            poisoned: false,
6277        };
6278        let mut contender = SqliteReader {
6279            handle: Some(
6280                open_explicit_read_transaction_handle(Arc::clone(&pool))
6281                    .await
6282                    .unwrap(),
6283            ),
6284            pool: Arc::clone(&pool),
6285            poisoned: false,
6286        };
6287        let udf_started = Arc::new(std::sync::atomic::AtomicBool::new(false));
6288        register_khive_test_slow_udf(
6289            &reader.handle.as_ref().unwrap().conn,
6290            900,
6291            Arc::clone(&udf_started),
6292        );
6293
6294        reader
6295            .query_all(SqlStatement {
6296                sql: "BEGIN DEFERRED".into(),
6297                params: vec![],
6298                label: None,
6299            })
6300            .await
6301            .expect("begin admitted transaction");
6302        reader
6303            .query_all(SqlStatement {
6304                sql: "SELECT * FROM grace_probe".into(),
6305                params: vec![],
6306                label: None,
6307            })
6308            .await
6309            .expect("materialize a real WAL snapshot");
6310        writer
6311            .execute_batch(
6312                "WITH RECURSIVE rows(value) AS (\
6313                 SELECT 1 UNION ALL SELECT value + 1 FROM rows WHERE value < 100\
6314                 ) INSERT INTO grace_probe(value) SELECT printf('row-%d', value) FROM rows;",
6315            )
6316            .unwrap();
6317        let (_, log_before, checkpointed_before) = passive_checkpoint(&writer);
6318        assert!(
6319            log_before > checkpointed_before,
6320            "the explicit reader snapshot must pin WAL frames before cancellation"
6321        );
6322
6323        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
6324        let query = tokio::spawn(crate::scope_request_read_cancellation(
6325            cancel_rx,
6326            async move {
6327                let result = reader
6328                    .query_all(SqlStatement {
6329                        sql: "SELECT khive_test_slow_udf()".into(),
6330                        params: vec![],
6331                        label: None,
6332                    })
6333                    .await;
6334                (reader, result)
6335            },
6336        ));
6337        // Wait for the UDF to actually be running (not just scheduled) so
6338        // registration has completed and the worker is provably blocked
6339        // inside SQLite before cancelling — the same proof `wait_for_progress`
6340        // gives the recursive-CTE tests, since a progress callback (fired
6341        // between opcodes) never runs during the UDF's own blocking call.
6342        wait_for_flag(udf_started.as_ref()).await;
6343        cancel_tx.send(true).unwrap();
6344
6345        let (mut reader, result) = tokio::time::timeout(std::time::Duration::from_secs(3), query)
6346            .await
6347            .expect(
6348                "a worker that settles within the grace+hard-cap bound must not hang the caller",
6349            )
6350            .unwrap();
6351        assert!(
6352            matches!(result, Err(StorageError::Timeout { .. })),
6353            "request cancellation must still surface as a typed timeout even after grace \
6354             was exceeded; got {result:?}"
6355        );
6356
6357        // The response was only returned above because the real worker
6358        // joined — expected-fail arm: this assertion would fail (permit
6359        // still 0) under the pre-fix behavior, which detached and returned
6360        // Timeout at the grace boundary while the worker (and its permit)
6361        // were still live.
6362        assert_eq!(
6363            pool.sql_bridge_reader_slots().available_permits(),
6364            1,
6365            "the sole reader permit must be visible again once the bounded join completes"
6366        );
6367
6368        let (_, log_after, checkpointed_after) = passive_checkpoint(&writer);
6369        assert_eq!(
6370            log_after, checkpointed_after,
6371            "the abandoned explicit read transaction must release its WAL snapshot by the \
6372             time the caller observes the timeout"
6373        );
6374
6375        contender
6376            .query_all(SqlStatement {
6377                sql: "SELECT 1".into(),
6378                params: vec![],
6379                label: None,
6380            })
6381            .await
6382            .expect("a fresh reader must be admitted once the zombie worker has settled");
6383
6384        // The settled connection itself must also be reusable (not
6385        // quarantined) and must not still be carrying the slow UDF's
6386        // progress callback into a later borrower.
6387        reader
6388            .query_all(SqlStatement {
6389                sql: "SELECT 1".into(),
6390                params: vec![],
6391                label: None,
6392            })
6393            .await
6394            .expect("the interrupted connection must remain usable after settling");
6395    }
6396
6397    #[tokio::test]
6398    #[serial_test::serial(tx_registry)]
6399    async fn cached_reader_transaction_lifecycle_survives_sqlite_empty_prefixes() {
6400        let dir = tempfile::tempdir().unwrap();
6401        let config = PoolConfig {
6402            path: Some(dir.path().join("sql_bridge_reader_prefixed_tx_control.db")),
6403            write_queue_enabled: Some(true),
6404            max_readers: 1,
6405            ..PoolConfig::default()
6406        };
6407        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6408        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6409        let mut reader = bridge.reader().await.unwrap();
6410
6411        reader
6412            .query_all(SqlStatement {
6413                sql: " ; -- empty statement\n /* leading comment */ \u{feff} BEGIN DEFERRED".into(),
6414                params: vec![],
6415                label: None,
6416            })
6417            .await
6418            .expect("prefixed BEGIN must enter the admitted transaction state");
6419        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 0);
6420        reader
6421            .query_all(SqlStatement {
6422                sql: " /* leading comment */ \u{feff} ; COMMIT".into(),
6423                params: vec![],
6424                label: None,
6425            })
6426            .await
6427            .expect("prefixed COMMIT must end the admitted transaction state");
6428        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
6429
6430        let rejected = reader
6431            .query_all(SqlStatement {
6432                sql: " ; /* no active transaction */ \u{feff} COMMIT".into(),
6433                params: vec![],
6434                label: None,
6435            })
6436            .await;
6437        assert!(
6438            matches!(
6439                &rejected,
6440                Err(StorageError::InvalidInput {
6441                    operation,
6442                    message,
6443                    ..
6444                }) if operation.as_ref() == "query_all"
6445                    && message.contains("transaction control")
6446                    && message.contains("COMMIT")
6447            ),
6448            "a prefixed COMMIT without an admitted transaction must still fail closed; \
6449             got {rejected:?}"
6450        );
6451
6452        let mut queue_backed_writer = bridge.writer().await.unwrap();
6453        let rejected = queue_backed_writer
6454            .query_all(SqlStatement {
6455                sql: "-- leading comment\n \u{feff} ; /* empty */ SAVEPOINT pinned".into(),
6456                params: vec![],
6457                label: None,
6458            })
6459            .await;
6460        assert!(
6461            matches!(
6462                &rejected,
6463                Err(StorageError::InvalidInput {
6464                    operation,
6465                    message,
6466                    ..
6467                }) if operation.as_ref() == "writer.query_all"
6468                    && message.contains("transaction control")
6469                    && message.contains("SAVEPOINT")
6470            ),
6471            "a queue-backed cached reader must classify transaction control through \
6472             comments, BOMs, and empty statements; got {rejected:?}"
6473        );
6474
6475        let value = reader
6476            .query_scalar(SqlStatement {
6477                sql: "SELECT 10".into(),
6478                params: vec![],
6479                label: None,
6480            })
6481            .await
6482            .expect("prefixed transaction lifecycle must preserve the cached reader");
6483        assert!(matches!(value, Some(SqlValue::Integer(10))));
6484    }
6485
6486    #[tokio::test]
6487    async fn cached_reader_restores_autocommit_before_releasing_its_operation_permit() {
6488        let dir = tempfile::tempdir().unwrap();
6489        let config = PoolConfig {
6490            path: Some(dir.path().join("sql_bridge_reader_autocommit.db")),
6491            max_readers: 1,
6492            ..PoolConfig::default()
6493        };
6494        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6495        let conn = open_standalone_reader(&pool).unwrap();
6496        conn.execute_batch("BEGIN DEFERRED; SELECT * FROM sqlite_schema")
6497            .unwrap();
6498        assert!(
6499            !conn.is_autocommit(),
6500            "the regression precondition needs a live read transaction"
6501        );
6502        let mut reader = SqliteReader {
6503            handle: Some(StandaloneHandle {
6504                conn,
6505                _retained_slot: None,
6506                read_transaction_slot: None,
6507            }),
6508            pool: Arc::clone(&pool),
6509            poisoned: false,
6510        };
6511
6512        let rejected = reader
6513            .query_all(SqlStatement {
6514                // The stale-state cleanup must take precedence over the
6515                // ordinary transaction-control rejection. Otherwise an idle
6516                // snapshot could survive every rejected ROLLBACK attempt.
6517                sql: "ROLLBACK".into(),
6518                params: vec![],
6519                label: None,
6520            })
6521            .await;
6522        assert!(
6523            matches!(
6524                &rejected,
6525                Err(StorageError::InvalidInput {
6526                    operation,
6527                    message,
6528                    ..
6529                }) if operation.as_ref() == "query_all"
6530                    && message.contains("outside autocommit")
6531            ),
6532            "a cached reader that reaches the boundary outside autocommit must fail closed; \
6533             got {rejected:?}"
6534        );
6535        assert_eq!(
6536            pool.sql_bridge_reader_slots().available_permits(),
6537            1,
6538            "the permit may be released only after the stale transaction is gone"
6539        );
6540        assert!(
6541            reader.handle.is_none(),
6542            "the restored connection must close instead of surviving as an idle standalone cache"
6543        );
6544
6545        let value = reader
6546            .query_scalar(SqlStatement {
6547                sql: "SELECT 9".into(),
6548                params: vec![],
6549                label: None,
6550            })
6551            .await
6552            .expect("the cleaned reader must remain usable through the pooled route");
6553        assert!(matches!(value, Some(SqlValue::Integer(9))));
6554    }
6555
6556    #[tokio::test]
6557    async fn standalone_writer_read_preserves_manual_atomic_transaction() {
6558        let dir = tempfile::tempdir().unwrap();
6559        let config = PoolConfig {
6560            path: Some(dir.path().join("sql_bridge_writer_atomic_read.db")),
6561            write_queue_enabled: Some(false),
6562            max_readers: 1,
6563            ..PoolConfig::default()
6564        };
6565        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6566        {
6567            let writer = pool.writer().unwrap();
6568            writer
6569                .conn()
6570                .execute_batch(
6571                    "CREATE TABLE atomic_read_test \
6572                     (id INTEGER PRIMARY KEY, value TEXT NOT NULL)",
6573                )
6574                .unwrap();
6575        }
6576        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6577
6578        let observed = bridge
6579            .atomic_unit(Box::new(|writer| {
6580                Box::pin(async move {
6581                    writer
6582                        .execute(SqlStatement {
6583                            sql: "INSERT INTO atomic_read_test (id, value) VALUES (1, 'pending')"
6584                                .into(),
6585                            params: vec![],
6586                            label: None,
6587                        })
6588                        .await?;
6589                    let count = writer
6590                        .query_scalar(SqlStatement {
6591                            sql: "SELECT COUNT(*) FROM atomic_read_test".into(),
6592                            params: vec![],
6593                            label: None,
6594                        })
6595                        .await?;
6596                    Ok(Box::new(count) as Box<dyn std::any::Any + Send>)
6597                })
6598            }))
6599            .await
6600            .expect("manual atomic read must not be mistaken for an idle reader snapshot");
6601        let observed = match observed.downcast::<Option<SqlValue>>() {
6602            Ok(observed) => observed,
6603            Err(_) => panic!("unexpected atomic result type"),
6604        };
6605        assert!(matches!(*observed, Some(SqlValue::Integer(1))));
6606
6607        let mut reader = bridge.reader().await.unwrap();
6608        let committed = reader
6609            .query_scalar(SqlStatement {
6610                sql: "SELECT COUNT(*) FROM atomic_read_test".into(),
6611                params: vec![],
6612                label: None,
6613            })
6614            .await
6615            .unwrap();
6616        assert!(matches!(committed, Some(SqlValue::Integer(1))));
6617    }
6618
6619    #[tokio::test]
6620    async fn request_cancellation_preserves_file_backed_manual_atomic_read_and_commit() {
6621        let dir = tempfile::tempdir().unwrap();
6622        let pool = Arc::new(
6623            ConnectionPool::new(PoolConfig {
6624                path: Some(dir.path().join("sql_bridge_writer_tx_cancel.db")),
6625                write_queue_enabled: Some(false),
6626                ..PoolConfig::for_test()
6627            })
6628            .unwrap(),
6629        );
6630        pool.writer()
6631            .unwrap()
6632            .conn()
6633            .execute_batch(
6634                "CREATE TABLE writer_tx_cancel_probe(\
6635                 id INTEGER PRIMARY KEY, value TEXT NOT NULL)",
6636            )
6637            .unwrap();
6638        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6639        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
6640
6641        let observed = crate::scope_request_read_cancellation(
6642            cancel_rx,
6643            bridge.atomic_unit(Box::new(move |writer| {
6644                Box::pin(async move {
6645                    writer
6646                        .execute(SqlStatement {
6647                            sql: "INSERT INTO writer_tx_cancel_probe VALUES (1, 'before')".into(),
6648                            params: vec![],
6649                            label: None,
6650                        })
6651                        .await?;
6652                    cancel_tx.send(true).unwrap();
6653                    let count = writer
6654                        .query_scalar(SqlStatement {
6655                            sql: "SELECT COUNT(*) FROM writer_tx_cancel_probe".into(),
6656                            params: vec![],
6657                            label: None,
6658                        })
6659                        .await?;
6660                    writer
6661                        .execute(SqlStatement {
6662                            sql: "INSERT INTO writer_tx_cancel_probe VALUES (2, 'after')".into(),
6663                            params: vec![],
6664                            label: None,
6665                        })
6666                        .await?;
6667                    Ok(Box::new(count) as Box<dyn std::any::Any + Send>)
6668                })
6669            })),
6670        )
6671        .await
6672        .expect("request cancellation must not interrupt an admitted manual write transaction");
6673        let observed = match observed.downcast::<Option<SqlValue>>() {
6674            Ok(observed) => observed,
6675            Err(_) => panic!("unexpected atomic result type"),
6676        };
6677        assert!(matches!(*observed, Some(SqlValue::Integer(1))));
6678
6679        let reader = pool.reader().unwrap();
6680        let rows: i64 = reader
6681            .conn()
6682            .query_row("SELECT COUNT(*) FROM writer_tx_cancel_probe", [], |row| {
6683                row.get(0)
6684            })
6685            .unwrap();
6686        assert_eq!(rows, 2, "both writes around the SELECT must commit");
6687    }
6688
6689    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6690    async fn cancelled_standalone_writer_transaction_retains_active_reader_admission() {
6691        let dir = tempfile::tempdir().unwrap();
6692        let pool = Arc::new(
6693            ConnectionPool::new(PoolConfig {
6694                path: Some(dir.path().join("sql_bridge_writer_tx_admission.db")),
6695                write_queue_enabled: Some(false),
6696                max_readers: 1,
6697                checkout_timeout: std::time::Duration::from_millis(250),
6698                ..PoolConfig::default()
6699            })
6700            .unwrap(),
6701        );
6702        let writer_slot = pool
6703            .sql_bridge_writer_slots()
6704            .acquire_owned()
6705            .await
6706            .unwrap();
6707        let conn = open_standalone_writer(&pool).unwrap();
6708        let (entered, release, _completed) = blocking_non_interrupting_progress_gate(&conn);
6709        let mut writer = SqliteWriter {
6710            handle: Some(StandaloneHandle {
6711                conn,
6712                _retained_slot: Some(writer_slot),
6713                read_transaction_slot: None,
6714            }),
6715            writer_task: None,
6716            origin: pool.origin(),
6717            db: crate::timeout_sink::db_label(&pool),
6718            pool: Arc::clone(&pool),
6719        };
6720        khive_storage::SqlWriter::execute(
6721            &mut writer,
6722            SqlStatement {
6723                sql: "BEGIN IMMEDIATE".into(),
6724                params: vec![],
6725                label: None,
6726            },
6727        )
6728        .await
6729        .unwrap();
6730        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
6731        cancel_tx.send(true).unwrap();
6732
6733        let query = tokio::spawn(crate::scope_request_read_cancellation(
6734            cancel_rx,
6735            async move {
6736                let result =
6737                    khive_storage::SqlReader::query_all(&mut writer, progress_gate_statement())
6738                        .await;
6739                let rollback = khive_storage::SqlWriter::execute(
6740                    &mut writer,
6741                    SqlStatement {
6742                        sql: "ROLLBACK".into(),
6743                        params: vec![],
6744                        label: None,
6745                    },
6746                )
6747                .await;
6748                (result, rollback)
6749            },
6750        ));
6751        tokio::time::timeout(std::time::Duration::from_secs(1), entered.notified())
6752            .await
6753            .expect("cancelled writer-transaction SELECT never reached SQLite");
6754        assert_eq!(
6755            pool.sql_bridge_reader_slots().available_permits(),
6756            0,
6757            "a writer-supertrait SELECT must retain ordinary active-reader admission"
6758        );
6759
6760        tokio::task::spawn_blocking(move || release.wait())
6761            .await
6762            .unwrap();
6763        let (rows, rollback) = tokio::time::timeout(std::time::Duration::from_secs(2), query)
6764            .await
6765            .expect("writer-transaction SELECT did not finish after its gate opened")
6766            .unwrap();
6767        assert_eq!(
6768            rows.expect("request cancellation interrupted the admitted writer transaction")
6769                .len(),
6770            1
6771        );
6772        rollback.expect("writer transaction did not return to autocommit");
6773        assert_eq!(pool.sql_bridge_reader_slots().available_permits(), 1);
6774    }
6775
6776    #[tokio::test]
6777    async fn expired_deadline_preserves_pool_backed_manual_atomic_read_and_commit() {
6778        let pool = Arc::new(ConnectionPool::new(PoolConfig::default()).unwrap());
6779        pool.writer()
6780            .unwrap()
6781            .conn()
6782            .execute_batch(
6783                "CREATE TABLE pool_writer_tx_deadline_probe(\
6784                 id INTEGER PRIMARY KEY, value TEXT NOT NULL)",
6785            )
6786            .unwrap();
6787        let bridge = SqlBridge::new(Arc::clone(&pool), false);
6788
6789        let observed = crate::scope_request_read_deadline(
6790            std::time::Duration::ZERO,
6791            bridge.atomic_unit(Box::new(|writer| {
6792                Box::pin(async move {
6793                    writer
6794                        .execute(SqlStatement {
6795                            sql: "INSERT INTO pool_writer_tx_deadline_probe VALUES (1, 'before')"
6796                                .into(),
6797                            params: vec![],
6798                            label: None,
6799                        })
6800                        .await?;
6801                    let count = writer
6802                        .query_scalar(SqlStatement {
6803                            sql: "SELECT COUNT(*) FROM pool_writer_tx_deadline_probe".into(),
6804                            params: vec![],
6805                            label: None,
6806                        })
6807                        .await?;
6808                    writer
6809                        .execute(SqlStatement {
6810                            sql: "INSERT INTO pool_writer_tx_deadline_probe VALUES (2, 'after')"
6811                                .into(),
6812                            params: vec![],
6813                            label: None,
6814                        })
6815                        .await?;
6816                    Ok(Box::new(count) as Box<dyn std::any::Any + Send>)
6817                })
6818            })),
6819        )
6820        .await
6821        .expect("an expired read deadline must not interrupt an admitted manual write transaction");
6822        let observed = match observed.downcast::<Option<SqlValue>>() {
6823            Ok(observed) => observed,
6824            Err(_) => panic!("unexpected atomic result type"),
6825        };
6826        assert!(matches!(*observed, Some(SqlValue::Integer(1))));
6827
6828        let reader = pool.reader().unwrap();
6829        let rows: i64 = reader
6830            .conn()
6831            .query_row(
6832                "SELECT COUNT(*) FROM pool_writer_tx_deadline_probe",
6833                [],
6834                |row| row.get(0),
6835            )
6836            .unwrap();
6837        assert_eq!(rows, 2, "both writes around the SELECT must commit");
6838    }
6839
6840    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6841    async fn cancelled_standalone_open_retains_slot_until_open_finishes() {
6842        let dir = tempfile::tempdir().unwrap();
6843        let config = PoolConfig {
6844            path: Some(dir.path().join("sql_bridge_cancelled_open.db")),
6845            max_readers: 1,
6846            checkout_timeout: std::time::Duration::from_millis(250),
6847            ..PoolConfig::default()
6848        };
6849        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6850        let slots = pool.sql_bridge_reader_slots();
6851        let slot = Arc::clone(&slots).acquire_owned().await.unwrap();
6852        assert_eq!(slots.available_permits(), 0);
6853
6854        let (entered_tx, entered_rx) = std::sync::mpsc::channel();
6855        let (release_tx, release_rx) = std::sync::mpsc::channel();
6856        let open = tokio::spawn(open_standalone_on_blocking(
6857            Arc::clone(&pool),
6858            slot,
6859            "test_open_reader",
6860            move |pool| {
6861                entered_tx.send(()).unwrap();
6862                release_rx.recv().unwrap();
6863                open_standalone_reader(pool)
6864            },
6865        ));
6866        tokio::task::spawn_blocking(move || entered_rx.recv())
6867            .await
6868            .unwrap()
6869            .unwrap();
6870
6871        open.abort();
6872        assert!(matches!(open.await, Err(error) if error.is_cancelled()));
6873        assert_eq!(
6874            slots.available_permits(),
6875            0,
6876            "the permit must remain in the detached open closure"
6877        );
6878        let contender = tokio::time::timeout(
6879            std::time::Duration::from_millis(50),
6880            Arc::clone(&slots).acquire_owned(),
6881        )
6882        .await;
6883        assert!(contender.is_err(), "an in-flight open must retain the cap");
6884
6885        release_tx.send(()).unwrap();
6886        let recovered = tokio::time::timeout(
6887            std::time::Duration::from_secs(1),
6888            Arc::clone(&slots).acquire_owned(),
6889        )
6890        .await
6891        .expect("the detached open did not release its permit")
6892        .unwrap();
6893        assert_eq!(slots.available_permits(), 0);
6894        drop(recovered);
6895        assert_eq!(slots.available_permits(), 1);
6896    }
6897
6898    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6899    async fn abandoned_writer_read_interrupts_and_releases_writer_handle() {
6900        let dir = tempfile::tempdir().unwrap();
6901        let config = PoolConfig {
6902            path: Some(dir.path().join("sql_bridge_cancelled_writer.db")),
6903            write_queue_enabled: Some(false),
6904            checkout_timeout: std::time::Duration::from_millis(250),
6905            ..PoolConfig::for_test()
6906        };
6907        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6908        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6909
6910        let handle_slot = pool
6911            .sql_bridge_writer_slots()
6912            .acquire_owned()
6913            .await
6914            .unwrap();
6915        let conn = open_standalone_writer(&pool).unwrap();
6916        let mut writer = SqliteWriter {
6917            handle: Some(StandaloneHandle {
6918                conn,
6919                _retained_slot: Some(handle_slot),
6920                read_transaction_slot: None,
6921            }),
6922            writer_task: None,
6923            origin: pool.origin(),
6924            db: crate::timeout_sink::db_label(&pool),
6925            pool: Arc::clone(&pool),
6926        };
6927        let progress = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6928        let query = tokio::spawn(crate::scope_test_read_progress(
6929            Arc::clone(&progress),
6930            async move {
6931                khive_storage::SqlReader::query_all(&mut writer, deliberately_slow_read_statement())
6932                    .await
6933            },
6934        ));
6935
6936        wait_for_progress(progress.as_ref()).await;
6937        query.abort();
6938        assert!(matches!(query.await, Err(error) if error.is_cancelled()));
6939        let writer_after =
6940            tokio::time::timeout(std::time::Duration::from_millis(500), bridge.writer())
6941                .await
6942                .expect("abandoned SQLite read did not release the writer handle promptly")
6943                .expect("writer handle remained unavailable after read interruption");
6944        drop(writer_after);
6945        let stopped_at = progress.load(std::sync::atomic::Ordering::SeqCst);
6946        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6947        assert_eq!(
6948            progress.load(std::sync::atomic::Ordering::SeqCst),
6949            stopped_at,
6950            "writer-backed SQLite read kept consuming work after cancellation"
6951        );
6952    }
6953
6954    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6955    async fn request_cancellation_never_interrupts_admitted_execute_batch() {
6956        let dir = tempfile::tempdir().unwrap();
6957        let config = PoolConfig {
6958            path: Some(dir.path().join("sql_bridge_cancelled_writer_batch.db")),
6959            write_queue_enabled: Some(false),
6960            checkout_timeout: std::time::Duration::from_millis(250),
6961            ..PoolConfig::for_test()
6962        };
6963        let pool = Arc::new(ConnectionPool::new(config).unwrap());
6964        let bridge = SqlBridge::new(Arc::clone(&pool), true);
6965        {
6966            let guard = pool.writer().unwrap();
6967            guard
6968                .conn()
6969                .execute_batch(
6970                    "CREATE TABLE cancellation_write_probe(\
6971                     id INTEGER PRIMARY KEY, value INTEGER NOT NULL)",
6972                )
6973                .unwrap();
6974        }
6975
6976        let handle_slot = pool
6977            .sql_bridge_writer_slots()
6978            .acquire_owned()
6979            .await
6980            .unwrap();
6981        let conn = open_standalone_writer(&pool).unwrap();
6982        let (entered, release, completed) = blocking_non_interrupting_progress_gate(&conn);
6983        let mut writer = SqliteWriter {
6984            handle: Some(StandaloneHandle {
6985                conn,
6986                _retained_slot: Some(handle_slot),
6987                read_transaction_slot: None,
6988            }),
6989            writer_task: None,
6990            origin: pool.origin(),
6991            db: crate::timeout_sink::db_label(&pool),
6992            pool: Arc::clone(&pool),
6993        };
6994        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
6995        let query = tokio::spawn(crate::scope_request_read_cancellation(
6996            cancel_rx,
6997            async move {
6998                khive_storage::SqlWriter::execute_batch(&mut writer, vec![slow_insert_statement()])
6999                    .await
7000            },
7001        ));
7002
7003        tokio::time::timeout(std::time::Duration::from_secs(1), entered.notified())
7004            .await
7005            .expect("mutating execute_batch never reached SQLite VM work");
7006        cancel_tx.send(true).unwrap();
7007        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
7008        assert!(
7009            !query.is_finished(),
7010            "request-read cancellation must not interrupt an admitted batch"
7011        );
7012
7013        let contender = bridge.writer().await;
7014        let retained_slot = matches!(
7015            &contender,
7016            Err(StorageError::AdmissionTimeout { operation, .. })
7017                if operation.as_ref() == "sql_bridge.writer_handle"
7018        );
7019        drop(contender);
7020
7021        tokio::task::spawn_blocking(move || release.wait())
7022            .await
7023            .unwrap();
7024        let affected = tokio::time::timeout(std::time::Duration::from_secs(2), query)
7025            .await
7026            .expect("admitted batch did not finish after its gate was released")
7027            .unwrap()
7028            .expect("request cancellation must preserve the batch result");
7029        assert_eq!(affected, 10_000);
7030        tokio::time::timeout(std::time::Duration::from_secs(1), completed.notified())
7031            .await
7032            .expect("completed batch did not release its connection");
7033        assert!(
7034            retained_slot,
7035            "request cancellation released the writer slot before the admitted batch stopped"
7036        );
7037        let reader = pool.reader().unwrap();
7038        let count: i64 = reader
7039            .conn()
7040            .query_row("SELECT COUNT(*) FROM cancellation_write_probe", [], |row| {
7041                row.get(0)
7042            })
7043            .unwrap();
7044        assert_eq!(count, 10_000, "the admitted batch must commit every row");
7045    }
7046
7047    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7048    async fn request_cancellation_never_interrupts_dml_returning_via_sql_reader() {
7049        let dir = tempfile::tempdir().unwrap();
7050        let config = PoolConfig {
7051            path: Some(dir.path().join("sql_bridge_dml_returning_cancel.db")),
7052            write_queue_enabled: Some(false),
7053            checkout_timeout: std::time::Duration::from_millis(250),
7054            ..PoolConfig::for_test()
7055        };
7056        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7057        {
7058            let guard = pool.writer().unwrap();
7059            guard
7060                .conn()
7061                .execute_batch(
7062                    "CREATE TABLE returning_write_probe(\
7063                     id INTEGER PRIMARY KEY, value INTEGER NOT NULL)",
7064                )
7065                .unwrap();
7066        }
7067
7068        let handle_slot = pool
7069            .sql_bridge_writer_slots()
7070            .acquire_owned()
7071            .await
7072            .unwrap();
7073        let conn = open_standalone_writer(&pool).unwrap();
7074        let (entered, release, completed) = blocking_non_interrupting_progress_gate(&conn);
7075        let mut writer = SqliteWriter {
7076            handle: Some(StandaloneHandle {
7077                conn,
7078                _retained_slot: Some(handle_slot),
7079                read_transaction_slot: None,
7080            }),
7081            writer_task: None,
7082            origin: pool.origin(),
7083            db: crate::timeout_sink::db_label(&pool),
7084            pool: Arc::clone(&pool),
7085        };
7086        let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
7087        let query = tokio::spawn(crate::scope_request_read_cancellation(
7088            cancel_rx,
7089            async move {
7090                khive_storage::SqlReader::query_all(
7091                    &mut writer,
7092                    SqlStatement {
7093                        sql: "INSERT INTO returning_write_probe(value) \
7094                          WITH RECURSIVE rows(value) AS (\
7095                          SELECT 1 UNION ALL SELECT value + 1 FROM rows WHERE value < 10000\
7096                          ) SELECT value FROM rows RETURNING id"
7097                            .into(),
7098                        params: vec![],
7099                        label: Some("non-interruptible-returning-probe".into()),
7100                    },
7101                )
7102                .await
7103            },
7104        ));
7105
7106        tokio::time::timeout(std::time::Duration::from_secs(1), entered.notified())
7107            .await
7108            .expect("DML RETURNING never reached admitted SQLite work");
7109        cancel_tx.send(true).unwrap();
7110        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
7111        assert!(
7112            !query.is_finished(),
7113            "request-read cancellation interrupted DML RETURNING"
7114        );
7115
7116        tokio::task::spawn_blocking(move || release.wait())
7117            .await
7118            .unwrap();
7119        let rows = tokio::time::timeout(std::time::Duration::from_secs(2), query)
7120            .await
7121            .expect("DML RETURNING did not finish after its gate was released")
7122            .unwrap()
7123            .expect("request cancellation must preserve DML RETURNING's result");
7124        assert_eq!(rows.len(), 10_000);
7125        tokio::time::timeout(std::time::Duration::from_secs(1), completed.notified())
7126            .await
7127            .expect("completed DML RETURNING did not release its connection");
7128
7129        let reader = pool.reader().unwrap();
7130        let count: i64 = reader
7131            .conn()
7132            .query_row("SELECT COUNT(*) FROM returning_write_probe", [], |row| {
7133                row.get(0)
7134            })
7135            .unwrap();
7136        assert_eq!(count, 10_000, "DML RETURNING must commit every row");
7137    }
7138
7139    /// Cancelling an in-flight call permanently invalidates the boxed handle:
7140    /// the call took the handle's connection into the detached blocking task,
7141    /// so every subsequent call on the SAME handle fails loudly with
7142    /// "connection already consumed" instead of silently operating on a
7143    /// connection that may still be running the cancelled statement.
7144    /// Callers that cancel or time out a bridge call must drop the handle and
7145    /// acquire a fresh one.
7146    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7147    async fn cancelled_call_invalidates_handle_reuse_fails_loud() {
7148        let dir = tempfile::tempdir().unwrap();
7149        let config = PoolConfig {
7150            path: Some(dir.path().join("sql_bridge_cancelled_reuse.db")),
7151            checkout_timeout: std::time::Duration::from_millis(250),
7152            ..PoolConfig::for_test()
7153        };
7154        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7155
7156        let handle_slot = acquire_handle_slot(
7157            pool.sql_bridge_writer_slots(),
7158            pool.config().checkout_timeout,
7159            "sql_bridge.writer_handle",
7160            SlotTimeoutClass::Admission,
7161        )
7162        .await
7163        .unwrap();
7164        let conn = open_standalone_writer(&pool).unwrap();
7165        let (entered, release, completed) = blocking_non_interrupting_progress_gate(&conn);
7166        let writer = Arc::new(tokio::sync::Mutex::new(SqliteWriter {
7167            handle: Some(StandaloneHandle {
7168                conn,
7169                _retained_slot: Some(handle_slot),
7170                read_transaction_slot: None,
7171            }),
7172            writer_task: None,
7173            origin: pool.origin(),
7174            db: crate::timeout_sink::db_label(&pool),
7175            pool: Arc::clone(&pool),
7176        }));
7177        let writer_clone = Arc::clone(&writer);
7178        let query = tokio::spawn(async move {
7179            khive_storage::SqlWriter::execute_batch(
7180                &mut *writer_clone.lock().await,
7181                vec![progress_gate_statement()],
7182            )
7183            .await
7184        });
7185
7186        entered.notified().await;
7187        query.abort();
7188        let cancelled = matches!(query.await, Err(error) if error.is_cancelled());
7189
7190        let reuse = khive_storage::SqlWriter::execute(
7191            &mut *writer.lock().await,
7192            SqlStatement {
7193                sql: "CREATE TABLE cancelled_reuse_probe (id INTEGER PRIMARY KEY)".into(),
7194                params: vec![],
7195                label: None,
7196            },
7197        )
7198        .await;
7199        let message = match reuse {
7200            Err(StorageError::Pool { message, .. }) => message,
7201            other => panic!(
7202                "reusing a cancelled writer handle must fail loudly with \
7203                 'connection already consumed'; got {other:?}"
7204            ),
7205        };
7206        assert!(
7207            message.contains("connection already consumed"),
7208            "expected the cancelled handle's reuse error to name the pinned \
7209             failure; got {message:?}"
7210        );
7211
7212        tokio::task::spawn_blocking(move || release.wait())
7213            .await
7214            .unwrap();
7215        tokio::time::timeout(std::time::Duration::from_secs(1), completed.notified())
7216            .await
7217            .expect("cancelled writer's detached SQLite call did not finish");
7218        assert!(cancelled, "writer batch task did not report cancellation");
7219    }
7220
7221    /// Transaction-control statements (`BEGIN`/`START`/`COMMIT`/`END`/
7222    /// `ROLLBACK`/`SAVEPOINT`/`RELEASE`) in `execute_batch` input are rejected with a
7223    /// typed invalid-input error BEFORE anything executes, on the standalone
7224    /// path: a caller `COMMIT` inside the batch's own `BEGIN IMMEDIATE`
7225    /// would commit early and break the all-or-nothing contract. The
7226    /// rejection must leave the handle untouched and fully reusable, and no
7227    /// statement (not even the valid ones before the offending one) may
7228    /// have run.
7229    #[tokio::test]
7230    async fn execute_batch_rejects_transaction_control_before_executing_anything() {
7231        let dir = tempfile::tempdir().unwrap();
7232        let config = PoolConfig {
7233            path: Some(dir.path().join("sql_bridge_tx_control_reject.db")),
7234            checkout_timeout: std::time::Duration::from_millis(250),
7235            write_queue_enabled: Some(false),
7236            ..PoolConfig::for_test()
7237        };
7238        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7239        {
7240            let guard = pool.writer().unwrap();
7241            guard
7242                .conn()
7243                .execute_batch(
7244                    "CREATE TABLE tx_reject_test (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
7245                )
7246                .unwrap();
7247        }
7248
7249        let handle_slot = acquire_handle_slot(
7250            pool.sql_bridge_writer_slots(),
7251            pool.config().checkout_timeout,
7252            "sql_bridge.writer_handle",
7253            SlotTimeoutClass::Admission,
7254        )
7255        .await
7256        .unwrap();
7257        let conn = open_standalone_writer(&pool).unwrap();
7258        let mut writer = SqliteWriter {
7259            handle: Some(StandaloneHandle {
7260                conn,
7261                _retained_slot: Some(handle_slot),
7262                read_transaction_slot: None,
7263            }),
7264            writer_task: None,
7265            origin: pool.origin(),
7266            db: crate::timeout_sink::db_label(&pool),
7267            pool: Arc::clone(&pool),
7268        };
7269
7270        for tail in ["COMMIT", "BEGIN"] {
7271            let multi = khive_storage::SqlWriter::execute_batch(
7272                &mut writer,
7273                vec![SqlStatement {
7274                    sql: format!(
7275                        "INSERT INTO tx_reject_test (id, val) VALUES (10, 'tail'); {tail}"
7276                    ),
7277                    params: vec![],
7278                    label: None,
7279                }],
7280            )
7281            .await;
7282            let message = multi
7283                .as_ref()
7284                .err()
7285                .map(ToString::to_string)
7286                .unwrap_or_default();
7287            assert!(
7288                message.contains("Multiple statements"),
7289                "a SqlStatement with trailing {tail} must be rejected before execution; got {message}"
7290            );
7291        }
7292
7293        // A valid INSERT first, then a bare COMMIT: the whole batch must be
7294        // rejected and the INSERT must NOT have run.
7295        let batch = khive_storage::SqlWriter::execute_batch(
7296            &mut writer,
7297            vec![
7298                SqlStatement {
7299                    sql: "INSERT INTO tx_reject_test (id, val) VALUES (1, 'a')".into(),
7300                    params: vec![],
7301                    label: None,
7302                },
7303                SqlStatement {
7304                    sql: "COMMIT".into(),
7305                    params: vec![],
7306                    label: None,
7307                },
7308            ],
7309        )
7310        .await;
7311        match &batch {
7312            Err(StorageError::InvalidInput {
7313                operation, message, ..
7314            }) => {
7315                assert_eq!(operation.as_ref(), "execute_batch");
7316                assert!(
7317                    message.contains("transaction control") && message.contains("COMMIT"),
7318                    "the rejection must name the offending statement head; got {message:?}"
7319                );
7320            }
7321            other => {
7322                panic!("a batch containing a bare COMMIT must be rejected up front; got {other:?}")
7323            }
7324        }
7325
7326        // Every transaction-control head is rejected, case-insensitively and
7327        // through leading whitespace and `--`/`/* */` comments.
7328        for sql in [
7329            "BEGIN IMMEDIATE",
7330            "START TRANSACTION",
7331            "commit",
7332            "End transaction",
7333            "ROLLBACK",
7334            "SAVEPOINT sp1",
7335            "RELEASE sp1",
7336            "  -- leading comment\nCOMMIT",
7337            "/* block */ rollback to savepoint sp1",
7338        ] {
7339            let rejected = khive_storage::SqlWriter::execute_batch(
7340                &mut writer,
7341                vec![SqlStatement {
7342                    sql: sql.into(),
7343                    params: vec![],
7344                    label: None,
7345                }],
7346            )
7347            .await;
7348            assert!(
7349                matches!(&rejected, Err(StorageError::InvalidInput { .. })),
7350                "transaction-control head {sql:?} must be rejected; got {rejected:?}"
7351            );
7352        }
7353
7354        // The rejection ran before the handle was taken: no statement
7355        // executed (the INSERT above did not land), and the handle is still
7356        // fully reusable.
7357        let count: i64 = {
7358            let guard = pool.reader().unwrap();
7359            guard
7360                .conn()
7361                .query_row("SELECT COUNT(*) FROM tx_reject_test", [], |r| r.get(0))
7362                .unwrap()
7363        };
7364        assert_eq!(count, 0, "a rejected batch must not have executed anything");
7365
7366        let affected = khive_storage::SqlWriter::execute(
7367            &mut writer,
7368            SqlStatement {
7369                sql: "INSERT INTO tx_reject_test (id, val) VALUES (2, 'b')".into(),
7370                params: vec![],
7371                label: None,
7372            },
7373        )
7374        .await
7375        .expect("the handle must survive a rejected batch untouched");
7376        assert_eq!(affected, 1);
7377    }
7378
7379    #[tokio::test]
7380    async fn standalone_execute_batch_rejects_prefixed_commit_before_any_write() {
7381        let dir = tempfile::tempdir().unwrap();
7382        let config = PoolConfig {
7383            path: Some(dir.path().join("sql_bridge_prefixed_commit_standalone.db")),
7384            write_queue_enabled: Some(false),
7385            ..PoolConfig::for_test()
7386        };
7387        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7388        {
7389            let guard = pool.writer().unwrap();
7390            guard
7391                .conn()
7392                .execute_batch("CREATE TABLE prefixed_commit (id INTEGER PRIMARY KEY)")
7393                .unwrap();
7394        }
7395        let bridge = SqlBridge::new(Arc::clone(&pool), true);
7396        let mut writer = bridge.writer().await.unwrap();
7397
7398        let rejected = writer
7399            .execute_batch(vec![
7400                SqlStatement {
7401                    sql: "INSERT INTO prefixed_commit (id) VALUES (1)".into(),
7402                    params: vec![],
7403                    label: None,
7404                },
7405                SqlStatement {
7406                    sql: " ; -- empty statement\n /* leading comment */ \u{feff} ; COMMIT".into(),
7407                    params: vec![],
7408                    label: None,
7409                },
7410            ])
7411            .await;
7412        assert!(
7413            matches!(
7414                &rejected,
7415                Err(StorageError::InvalidInput {
7416                    operation,
7417                    message,
7418                    ..
7419                }) if operation.as_ref() == "execute_batch"
7420                    && message.contains("transaction control")
7421                    && message.contains("COMMIT")
7422            ),
7423            "standalone execute_batch must reject a prefixed COMMIT before the INSERT; \
7424             got {rejected:?}"
7425        );
7426
7427        let mut reader = bridge.reader().await.unwrap();
7428        let count = reader
7429            .query_scalar(SqlStatement {
7430                sql: "SELECT COUNT(*) FROM prefixed_commit".into(),
7431                params: vec![],
7432                label: None,
7433            })
7434            .await
7435            .unwrap();
7436        assert!(
7437            matches!(count, Some(SqlValue::Integer(0))),
7438            "prefixed COMMIT rejection must happen before the earlier INSERT; got {count:?}"
7439        );
7440
7441        let affected = writer
7442            .execute(SqlStatement {
7443                sql: "INSERT INTO prefixed_commit (id) VALUES (2)".into(),
7444                params: vec![],
7445                label: None,
7446            })
7447            .await
7448            .expect("prefixed COMMIT rejection must leave the standalone handle reusable");
7449        assert_eq!(affected, 1);
7450    }
7451
7452    #[tokio::test]
7453    async fn execute_batch_rejects_multi_statement_on_pool_backed_path() {
7454        let pool = Arc::new(ConnectionPool::new(PoolConfig::default()).unwrap());
7455        pool.writer()
7456            .unwrap()
7457            .conn()
7458            .execute_batch(
7459                "CREATE TABLE multi_statement_pool_test (id INTEGER PRIMARY KEY, val TEXT)",
7460            )
7461            .unwrap();
7462        let bridge = SqlBridge::new(Arc::clone(&pool), false);
7463        let mut writer = bridge.writer().await.unwrap();
7464
7465        let result = khive_storage::SqlWriter::execute_batch(
7466            &mut *writer,
7467            vec![SqlStatement {
7468                sql: "INSERT INTO multi_statement_pool_test (id, val) VALUES (1, 'x'); COMMIT"
7469                    .into(),
7470                params: vec![],
7471                label: None,
7472            }],
7473        )
7474        .await;
7475        let message = result
7476            .as_ref()
7477            .err()
7478            .map(ToString::to_string)
7479            .unwrap_or_default();
7480        assert!(
7481            message.contains("Multiple statements"),
7482            "pool-backed execute_batch must reject a trailing COMMIT; got {message}"
7483        );
7484        let count: i64 = pool
7485            .reader()
7486            .unwrap()
7487            .conn()
7488            .query_row(
7489                "SELECT COUNT(*) FROM multi_statement_pool_test",
7490                [],
7491                |row| row.get(0),
7492            )
7493            .unwrap();
7494        assert_eq!(count, 0);
7495    }
7496
7497    #[tokio::test]
7498    async fn inline_execute_batch_rejects_multi_statement_sql() {
7499        let dir = tempfile::tempdir().unwrap();
7500        let pool = Arc::new(
7501            ConnectionPool::new(PoolConfig {
7502                path: Some(dir.path().join("sql_bridge_multi_statement_inline.db")),
7503                write_queue_enabled: Some(true),
7504                write_routing_strict: true,
7505                ..PoolConfig::for_test()
7506            })
7507            .unwrap(),
7508        );
7509        pool.writer()
7510            .unwrap()
7511            .conn()
7512            .execute_batch(
7513                "CREATE TABLE multi_statement_inline_test (id INTEGER PRIMARY KEY, val TEXT)",
7514            )
7515            .unwrap();
7516        let bridge = SqlBridge::new(Arc::clone(&pool), true);
7517
7518        let result = bridge
7519            .atomic_unit(Box::new(|writer| {
7520                Box::pin(async move {
7521                    writer
7522                        .execute_batch(vec![SqlStatement {
7523                            sql: "INSERT INTO multi_statement_inline_test (id, val) VALUES (1, 'x'); BEGIN"
7524                                .into(),
7525                            params: vec![],
7526                            label: None,
7527                        }])
7528                        .await
7529                        .map(|_| Box::new(()) as Box<dyn Any + Send>)
7530                })
7531            }))
7532            .await;
7533        let message = result
7534            .as_ref()
7535            .err()
7536            .map(ToString::to_string)
7537            .unwrap_or_default();
7538        assert!(
7539            message.contains("Multiple statements"),
7540            "InlineWriter must reject a trailing BEGIN; got {message}"
7541        );
7542        let count: i64 = pool
7543            .reader()
7544            .unwrap()
7545            .conn()
7546            .query_row(
7547                "SELECT COUNT(*) FROM multi_statement_inline_test",
7548                [],
7549                |row| row.get(0),
7550            )
7551            .unwrap();
7552        assert_eq!(count, 0);
7553    }
7554
7555    /// Unit matrix for [`transaction_control_head`]: statement heads are
7556    /// classified case-insensitively through leading whitespace and
7557    /// comments; non-transaction-control heads (including identifiers that
7558    /// merely START with a keyword) never match.
7559    #[test]
7560    fn transaction_control_head_classification_matrix() {
7561        for (sql, expected) in [
7562            ("BEGIN", Some("BEGIN")),
7563            ("begin immediate", Some("BEGIN")),
7564            ("START TRANSACTION", Some("START")),
7565            ("start transaction", Some("START")),
7566            ("COMMIT", Some("COMMIT")),
7567            ("commit;", Some("COMMIT")),
7568            ("END", Some("END")),
7569            ("end transaction", Some("END")),
7570            ("ROLLBACK", Some("ROLLBACK")),
7571            ("rollback to savepoint sp1", Some("ROLLBACK")),
7572            ("SAVEPOINT sp1", Some("SAVEPOINT")),
7573            ("RELEASE sp1", Some("RELEASE")),
7574            ("release savepoint sp1", Some("RELEASE")),
7575            ("   \t COMMIT", Some("COMMIT")),
7576            ("\u{feff}BEGIN", Some("BEGIN")),
7577            (" ; BEGIN", Some("BEGIN")),
7578            (" ; ; -- empty\n /* comment */ COMMIT", Some("COMMIT")),
7579            ("  \u{feff} SAVEPOINT sp1", Some("SAVEPOINT")),
7580            ("/* comment */ \u{feff} ; RELEASE sp1", Some("RELEASE")),
7581            ("\u{feff} ; \u{feff} -- empty\n ROLLBACK", Some("ROLLBACK")),
7582            ("-- a comment\nCOMMIT", Some("COMMIT")),
7583            // SQLite does not nest block comments: the comment ends at the
7584            // first `*/`, leaving `*/ COMMIT`, which is not a statement head.
7585            ("/* /* nested? no */ */ COMMIT", None),
7586            ("-- one\n-- two\n  /* x */ begin", Some("BEGIN")),
7587            ("INSERT INTO t VALUES (1)", None),
7588            ("UPDATE t SET x = 1", None),
7589            ("DELETE FROM t", None),
7590            ("SELECT * FROM commit_log", None),
7591            ("CREATE TABLE rollback_audit (id INTEGER)", None),
7592            ("/* comment only */", None),
7593            (" ; /* empty statements only */ ; ", None),
7594            (" ; SELECT 1", None),
7595            ("", None),
7596        ] {
7597            assert_eq!(
7598                transaction_control_head(sql),
7599                expected,
7600                "classification mismatch for {sql:?}"
7601            );
7602        }
7603    }
7604
7605    #[test]
7606    fn cached_read_transaction_control_classification_matrix() {
7607        use CachedReadTransactionControl::{BeginDeferred, Finish, Unsupported};
7608
7609        for (sql, expected) in [
7610            ("BEGIN", Some(BeginDeferred)),
7611            ("begin transaction", Some(BeginDeferred)),
7612            (
7613                "/* p */ \u{feff} ; BEGIN /* mode */ DEFERRED",
7614                Some(BeginDeferred),
7615            ),
7616            ("BEGIN DEFERRED TRANSACTION", Some(BeginDeferred)),
7617            ("BEGIN IMMEDIATE", Some(Unsupported("BEGIN"))),
7618            ("BEGIN /* lock */ EXCLUSIVE", Some(Unsupported("BEGIN"))),
7619            ("BEGIN TRANSACTION IMMEDIATE", Some(Unsupported("BEGIN"))),
7620            ("begin transaction exclusive", Some(Unsupported("BEGIN"))),
7621            ("BEGIN TRANSACTION DEFERRED", Some(Unsupported("BEGIN"))),
7622            ("BEGIN IMMEDIATE TRANSACTION", Some(Unsupported("BEGIN"))),
7623            ("BEGIN TRANSACTION named_txn", Some(Unsupported("BEGIN"))),
7624            (
7625                "BEGIN DEFERRED TRANSACTION trailing",
7626                Some(Unsupported("BEGIN")),
7627            ),
7628            ("BEGIN DEFERRED DEFERRED", Some(Unsupported("BEGIN"))),
7629            // Non-identifier tails: the tokenizer yields no token for a
7630            // quoted, bracketed, or backticked tail, which must read as a
7631            // refused remainder, never as end-of-statement.
7632            (
7633                "BEGIN TRANSACTION \"IMMEDIATE\"",
7634                Some(Unsupported("BEGIN")),
7635            ),
7636            ("BEGIN TRANSACTION [IMMEDIATE]", Some(Unsupported("BEGIN"))),
7637            ("BEGIN TRANSACTION `IMMEDIATE`", Some(Unsupported("BEGIN"))),
7638            ("BEGIN TRANSACTION 'IMMEDIATE'", Some(Unsupported("BEGIN"))),
7639            ("BEGIN \"DEFERRED\"", Some(Unsupported("BEGIN"))),
7640            ("BEGIN; COMMIT", Some(Unsupported("BEGIN"))),
7641            // Trailing empty statements and trivia remain an accepted end.
7642            ("BEGIN;", Some(BeginDeferred)),
7643            ("BEGIN DEFERRED ; -- done", Some(BeginDeferred)),
7644            ("BEGIN TRANSACTION /* t */ ;;", Some(BeginDeferred)),
7645            ("START TRANSACTION", Some(Unsupported("START"))),
7646            ("COMMIT", Some(Finish("COMMIT"))),
7647            ("END TRANSACTION", Some(Finish("END"))),
7648            ("ROLLBACK", Some(Finish("ROLLBACK"))),
7649            ("ROLLBACK TRANSACTION", Some(Finish("ROLLBACK"))),
7650            ("ROLLBACK TO sp", Some(Unsupported("ROLLBACK"))),
7651            (
7652                "ROLLBACK /* nested */ TRANSACTION /* target */ TO sp",
7653                Some(Unsupported("ROLLBACK")),
7654            ),
7655            ("SAVEPOINT sp", Some(Unsupported("SAVEPOINT"))),
7656            ("SELECT 1", None),
7657        ] {
7658            assert_eq!(
7659                cached_read_transaction_control(sql),
7660                expected,
7661                "cached-reader transaction classification mismatch for {sql:?}"
7662            );
7663        }
7664    }
7665
7666    #[test]
7667    fn sqlite_accepts_utf8_bom_before_transaction_control() {
7668        let conn = rusqlite::Connection::open_in_memory().unwrap();
7669        conn.execute_batch("CREATE TABLE bom_transaction_test (id INTEGER)")
7670            .unwrap();
7671        conn.execute_batch("\u{feff}BEGIN IMMEDIATE").unwrap();
7672        conn.execute_batch("ROLLBACK").unwrap();
7673    }
7674
7675    /// The queue-backed `execute_batch` path rejects transaction-control
7676    /// statements too, and the rejection protects the writer task: a caller
7677    /// `COMMIT` that reached the task would close its per-request `BEGIN
7678    /// IMMEDIATE` and terminate the task permanently. After the typed
7679    /// rejection, a legitimate batch must still succeed through the SAME
7680    /// writer task (it was never touched).
7681    #[tokio::test]
7682    async fn execute_batch_rejects_transaction_control_on_queue_backed_path() {
7683        let dir = tempfile::tempdir().unwrap();
7684        let config = PoolConfig {
7685            path: Some(dir.path().join("sql_bridge_tx_reject_queue.db")),
7686            checkout_timeout: std::time::Duration::from_millis(250),
7687            write_queue_enabled: Some(true),
7688            write_routing_strict: true,
7689            ..PoolConfig::for_test()
7690        };
7691        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7692        {
7693            let guard = pool.writer().unwrap();
7694            guard
7695                .conn()
7696                .execute_batch(
7697                    "CREATE TABLE tx_reject_queue_test (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
7698                )
7699                .unwrap();
7700        }
7701        let bridge = SqlBridge::new(Arc::clone(&pool), true);
7702        let mut writer = bridge.writer().await.unwrap();
7703
7704        let rejected = khive_storage::SqlWriter::execute_batch(
7705            &mut *writer,
7706            vec![
7707                SqlStatement {
7708                    sql: "INSERT INTO tx_reject_queue_test (id, val) VALUES (1, 'a')".into(),
7709                    params: vec![],
7710                    label: None,
7711                },
7712                SqlStatement {
7713                    sql: "COMMIT".into(),
7714                    params: vec![],
7715                    label: None,
7716                },
7717            ],
7718        )
7719        .await;
7720        assert!(
7721            matches!(&rejected, Err(StorageError::InvalidInput { .. })),
7722            "a bare COMMIT in a queue-backed batch must be rejected up front; got {rejected:?}"
7723        );
7724
7725        let prefixed = khive_storage::SqlWriter::execute_batch(
7726            &mut *writer,
7727            vec![
7728                SqlStatement {
7729                    sql: "INSERT INTO tx_reject_queue_test (id, val) VALUES (3, 'prefixed')".into(),
7730                    params: vec![],
7731                    label: None,
7732                },
7733                SqlStatement {
7734                    sql: "/* leading */ \u{feff} ; -- empty\n ; COMMIT".into(),
7735                    params: vec![],
7736                    label: None,
7737                },
7738            ],
7739        )
7740        .await;
7741        assert!(
7742            matches!(
7743                &prefixed,
7744                Err(StorageError::InvalidInput {
7745                    operation,
7746                    message,
7747                    ..
7748                }) if operation.as_ref() == "execute_batch"
7749                    && message.contains("transaction control")
7750                    && message.contains("COMMIT")
7751            ),
7752            "a prefixed COMMIT must be rejected before touching the writer task; got {prefixed:?}"
7753        );
7754
7755        let affected = khive_storage::SqlWriter::execute_batch(
7756            &mut *writer,
7757            vec![SqlStatement {
7758                sql: "INSERT INTO tx_reject_queue_test (id, val) VALUES (2, 'b')".into(),
7759                params: vec![],
7760                label: None,
7761            }],
7762        )
7763        .await
7764        .expect("the writer task must survive the rejected batch");
7765        assert_eq!(affected, 1);
7766
7767        let count: i64 = {
7768            let guard = pool.reader().unwrap();
7769            guard
7770                .conn()
7771                .query_row("SELECT COUNT(*) FROM tx_reject_queue_test", [], |r| {
7772                    r.get(0)
7773                })
7774                .unwrap()
7775        };
7776        assert_eq!(
7777            count, 1,
7778            "exactly the post-rejection batch's row may have landed"
7779        );
7780    }
7781
7782    /// A failed ROLLBACK after a statement failure poisons the handle: the
7783    /// connection may be in an unknown transaction state, so it is dropped
7784    /// instead of restored, and every subsequent call on the same handle
7785    /// fails loudly with "connection already consumed". The caller sees the
7786    /// ORIGINAL statement error with the poison context attached (the
7787    /// rollback failure is never hidden, but never replaces the original).
7788    ///
7789    /// Forcing the arm legitimately (the pre-round-2 version smuggled a
7790    /// bare `COMMIT` into the batch, which `execute_batch` now rejects up
7791    /// front): a connection authorizer denies the `ROLLBACK` transaction
7792    /// operation, so the error path's `ROLLBACK` genuinely fails while the
7793    /// batch's own `BEGIN IMMEDIATE` and the statements run normally.
7794    #[tokio::test]
7795    async fn failed_rollback_poisons_handle_reuse_fails_loud() {
7796        use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
7797
7798        fn deny_rollback(ctx: AuthContext<'_>) -> Authorization {
7799            match ctx.action {
7800                AuthAction::Transaction {
7801                    operation: TransactionOperation::Rollback,
7802                } => Authorization::Deny,
7803                _ => Authorization::Allow,
7804            }
7805        }
7806
7807        let dir = tempfile::tempdir().unwrap();
7808        let config = PoolConfig {
7809            path: Some(dir.path().join("sql_bridge_rollback_poison.db")),
7810            checkout_timeout: std::time::Duration::from_millis(250),
7811            ..PoolConfig::for_test()
7812        };
7813        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7814        {
7815            let guard = pool.writer().unwrap();
7816            guard
7817                .conn()
7818                .execute_batch(
7819                    "CREATE TABLE rollback_poison_test (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
7820                )
7821                .unwrap();
7822        }
7823
7824        let handle_slot = acquire_handle_slot(
7825            pool.sql_bridge_writer_slots(),
7826            pool.config().checkout_timeout,
7827            "sql_bridge.writer_handle",
7828            SlotTimeoutClass::Admission,
7829        )
7830        .await
7831        .unwrap();
7832        let conn = open_standalone_writer(&pool).unwrap();
7833        conn.authorizer(Some(deny_rollback)).unwrap();
7834        let mut writer = SqliteWriter {
7835            handle: Some(StandaloneHandle {
7836                conn,
7837                _retained_slot: Some(handle_slot),
7838                read_transaction_slot: None,
7839            }),
7840            writer_task: None,
7841            origin: pool.origin(),
7842            db: crate::timeout_sink::db_label(&pool),
7843            pool: Arc::clone(&pool),
7844        };
7845
7846        let batch = khive_storage::SqlWriter::execute_batch(
7847            &mut writer,
7848            vec![
7849                SqlStatement {
7850                    sql: "INSERT INTO rollback_poison_test (id, val) VALUES (1, 'a')".into(),
7851                    params: vec![],
7852                    label: None,
7853                },
7854                SqlStatement {
7855                    sql: "SELECT FROM WHERE".into(),
7856                    params: vec![],
7857                    label: None,
7858                },
7859            ],
7860        )
7861        .await;
7862        let batch_error = batch.expect_err("invalid second statement must fail the batch");
7863        let poison = match &batch_error {
7864            StorageError::Driver { source, .. } => source
7865                .downcast_ref::<PoisonedBatchError>()
7866                .expect("failed rollback must retain its typed poison wrapper"),
7867            other => panic!("failed rollback must return a driver error; got {other:?}"),
7868        };
7869        assert!(
7870            matches!(&poison.poison_reason, BatchPoisonReason::RollbackFailed(_)),
7871            "the poison cause must be compiler-checked as RollbackFailed; got {poison:?}"
7872        );
7873        let batch_message = batch_error.to_string();
7874        assert!(
7875            batch_message.contains("ROLLBACK after statement failure failed"),
7876            "the caller must see the poison context naming the failed \
7877             rollback; got {batch_message:?}"
7878        );
7879        assert!(
7880            batch_message.contains("original error"),
7881            "the original statement error must stay visible alongside the \
7882             poison context; got {batch_message:?}"
7883        );
7884
7885        let reuse = khive_storage::SqlWriter::execute(
7886            &mut writer,
7887            SqlStatement {
7888                sql: "CREATE TABLE rollback_poison_probe (id INTEGER PRIMARY KEY)".into(),
7889                params: vec![],
7890                label: None,
7891            },
7892        )
7893        .await;
7894        let message = match reuse {
7895            Err(StorageError::Pool { message, .. }) => message,
7896            other => panic!(
7897                "reusing a poisoned writer handle must fail loudly with \
7898                 'connection already consumed'; got {other:?}"
7899            ),
7900        };
7901        assert!(
7902            message.contains("connection already consumed"),
7903            "expected the poisoned handle's reuse error to name the pinned \
7904             failure; got {message:?}"
7905        );
7906    }
7907
7908    /// A NON-TRANSIENT `BEGIN IMMEDIATE` failure poisons the handle instead
7909    /// of restoring it: the connection's transaction state is suspect (here
7910    /// a caller-driven transaction is already open on the same connection,
7911    /// so SQLite answers "cannot start a transaction within a transaction"),
7912    /// and the returned error carries the poison context.
7913    #[tokio::test]
7914    async fn non_transient_begin_failure_poisons_handle() {
7915        let dir = tempfile::tempdir().unwrap();
7916        let config = PoolConfig {
7917            path: Some(dir.path().join("sql_bridge_begin_poison.db")),
7918            checkout_timeout: std::time::Duration::from_millis(250),
7919            ..PoolConfig::for_test()
7920        };
7921        let pool = Arc::new(ConnectionPool::new(config).unwrap());
7922
7923        let handle_slot = acquire_handle_slot(
7924            pool.sql_bridge_writer_slots(),
7925            pool.config().checkout_timeout,
7926            "sql_bridge.writer_handle",
7927            SlotTimeoutClass::Admission,
7928        )
7929        .await
7930        .unwrap();
7931        let conn = open_standalone_writer(&pool).unwrap();
7932        // A caller-driven open transaction on the same connection: the
7933        // batch's own `BEGIN IMMEDIATE` fails non-transiently.
7934        conn.execute_batch("BEGIN IMMEDIATE").unwrap();
7935        let mut writer = SqliteWriter {
7936            handle: Some(StandaloneHandle {
7937                conn,
7938                _retained_slot: Some(handle_slot),
7939                read_transaction_slot: None,
7940            }),
7941            writer_task: None,
7942            origin: pool.origin(),
7943            db: crate::timeout_sink::db_label(&pool),
7944            pool: Arc::clone(&pool),
7945        };
7946
7947        let batch = khive_storage::SqlWriter::execute_batch(
7948            &mut writer,
7949            vec![SqlStatement {
7950                sql: "SELECT 1".into(),
7951                params: vec![],
7952                label: None,
7953            }],
7954        )
7955        .await;
7956        let batch_error = batch.expect_err("BEGIN inside an open transaction must fail");
7957        let poison = match &batch_error {
7958            StorageError::Driver { source, .. } => source
7959                .downcast_ref::<PoisonedBatchError>()
7960                .expect("failed BEGIN must retain its typed poison wrapper"),
7961            other => panic!("failed BEGIN must return a driver error; got {other:?}"),
7962        };
7963        assert!(
7964            matches!(&poison.poison_reason, BatchPoisonReason::BeginFailed),
7965            "the poison cause must be compiler-checked as BeginFailed; got {poison:?}"
7966        );
7967        let batch_message = batch_error.to_string();
7968        assert!(
7969            batch_message.contains("BEGIN IMMEDIATE failed non-transiently"),
7970            "a non-transient BEGIN failure must surface the poison context; \
7971             got {batch_message:?}"
7972        );
7973        assert!(
7974            batch_message.contains("cannot start a transaction within a transaction"),
7975            "the original BEGIN error must stay visible; got {batch_message:?}"
7976        );
7977
7978        let reuse = khive_storage::SqlWriter::execute(
7979            &mut writer,
7980            SqlStatement {
7981                sql: "CREATE TABLE begin_poison_probe (id INTEGER PRIMARY KEY)".into(),
7982                params: vec![],
7983                label: None,
7984            },
7985        )
7986        .await;
7987        assert!(
7988            matches!(
7989                &reuse,
7990                Err(StorageError::Pool { message, .. })
7991                    if message.contains("connection already consumed")
7992            ),
7993            "a handle poisoned by a non-transient BEGIN failure must be \
7994             dropped, not restored; got {reuse:?}"
7995        );
7996    }
7997
7998    /// A BUSY/LOCKED `BEGIN IMMEDIATE` failure is transient contention: the
7999    /// connection itself is untouched, so the handle is restored as
8000    /// reusable, and the next call succeeds once the contending lock is
8001    /// released.
8002    #[tokio::test]
8003    async fn busy_begin_failure_restores_handle_reusable() {
8004        let dir = tempfile::tempdir().unwrap();
8005        let config = PoolConfig {
8006            path: Some(dir.path().join("sql_bridge_begin_busy.db")),
8007            checkout_timeout: std::time::Duration::from_millis(250),
8008            busy_timeout: std::time::Duration::from_millis(100),
8009            ..PoolConfig::for_test()
8010        };
8011        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8012        {
8013            let guard = pool.writer().unwrap();
8014            guard
8015                .conn()
8016                .execute_batch("CREATE TABLE begin_busy_test (id INTEGER PRIMARY KEY)")
8017                .unwrap();
8018        }
8019
8020        // Hold SQLite's write lock from a separate connection so the batch's
8021        // `BEGIN IMMEDIATE` genuinely fails with SQLITE_BUSY after the short
8022        // busy timeout.
8023        let lock_conn = pool.open_standalone_writer().unwrap();
8024        lock_conn.execute_batch("BEGIN IMMEDIATE").unwrap();
8025
8026        let handle_slot = acquire_handle_slot(
8027            pool.sql_bridge_writer_slots(),
8028            pool.config().checkout_timeout,
8029            "sql_bridge.writer_handle",
8030            SlotTimeoutClass::Admission,
8031        )
8032        .await
8033        .unwrap();
8034        let conn = open_standalone_writer(&pool).unwrap();
8035        let mut writer = SqliteWriter {
8036            handle: Some(StandaloneHandle {
8037                conn,
8038                _retained_slot: Some(handle_slot),
8039                read_transaction_slot: None,
8040            }),
8041            writer_task: None,
8042            origin: pool.origin(),
8043            db: crate::timeout_sink::db_label(&pool),
8044            pool: Arc::clone(&pool),
8045        };
8046
8047        let batch = khive_storage::SqlWriter::execute_batch(
8048            &mut writer,
8049            vec![SqlStatement {
8050                sql: "INSERT INTO begin_busy_test (id) VALUES (1)".into(),
8051                params: vec![],
8052                label: None,
8053            }],
8054        )
8055        .await;
8056        let batch_error = batch.expect_err("BEGIN IMMEDIATE under a held write lock must fail");
8057        assert!(
8058            batch_error.to_string().contains("database is locked"),
8059            "the busy BEGIN failure must surface SQLite's busy error; got {batch_error:?}"
8060        );
8061
8062        lock_conn.execute_batch("ROLLBACK").unwrap();
8063        drop(lock_conn);
8064
8065        let affected = khive_storage::SqlWriter::execute(
8066            &mut writer,
8067            SqlStatement {
8068                sql: "INSERT INTO begin_busy_test (id) VALUES (2)".into(),
8069                params: vec![],
8070                label: None,
8071            },
8072        )
8073        .await
8074        .expect("a busy BEGIN failure must restore the handle as reusable");
8075        assert_eq!(affected, 1);
8076    }
8077
8078    /// The manual `atomic_unit` path (write queue off) shares the pool's
8079    /// one-permit writer-handle budget with `writer()`: while a boxed writer
8080    /// handle is live, `atomic_unit` times out; after the handle drops, the
8081    /// next `atomic_unit` succeeds on the same pool.
8082    #[tokio::test]
8083    async fn manual_atomic_unit_shares_writer_permit_budget_with_writer_handle() {
8084        let dir = tempfile::tempdir().unwrap();
8085        let config = PoolConfig {
8086            path: Some(dir.path().join("sql_bridge_atomic_unit_budget.db")),
8087            checkout_timeout: std::time::Duration::from_millis(50),
8088            write_queue_enabled: Some(false),
8089            ..PoolConfig::for_test()
8090        };
8091        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8092        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8093        {
8094            let guard = pool.writer().unwrap();
8095            guard
8096                .conn()
8097                .execute_batch(
8098                    "CREATE TABLE IF NOT EXISTS atomic_unit_budget_test \
8099                     (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)",
8100                )
8101                .unwrap();
8102        }
8103
8104        fn insert_op(id: i64) -> AtomicUnitOp {
8105            Box::new(move |writer| {
8106                Box::pin(async move {
8107                    writer
8108                        .execute(SqlStatement {
8109                            sql: "INSERT INTO atomic_unit_budget_test (id, val) VALUES (?1, ?2)"
8110                                .into(),
8111                            params: vec![SqlValue::Integer(id), SqlValue::Integer(id)],
8112                            label: None,
8113                        })
8114                        .await
8115                        .map_err(|e| {
8116                            khive_storage::StorageError::driver(
8117                                StorageCapability::Sql,
8118                                "atomic_unit_budget_test_insert",
8119                                e,
8120                            )
8121                        })?;
8122                    Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
8123                })
8124            })
8125        }
8126
8127        let writer_handle = bridge.writer().await.unwrap();
8128        let blocked = bridge.atomic_unit(insert_op(1)).await;
8129        assert!(
8130            matches!(
8131                &blocked,
8132                Err(StorageError::AdmissionTimeout { operation, .. })
8133                    if operation.as_ref() == "sql_bridge.atomic_unit_handle"
8134            ),
8135            "atomic_unit must time out on the shared writer permit while a \
8136             writer handle is live; got {blocked:?}"
8137        );
8138
8139        drop(writer_handle);
8140        let unblocked = bridge.atomic_unit(insert_op(2)).await;
8141        assert!(
8142            unblocked.is_ok(),
8143            "atomic_unit must succeed once the writer handle releases the \
8144             shared writer permit; got {unblocked:?}"
8145        );
8146
8147        let mut reader = bridge.reader().await.unwrap();
8148        let count = reader
8149            .query_scalar(SqlStatement {
8150                sql: "SELECT COUNT(*) FROM atomic_unit_budget_test".into(),
8151                params: vec![],
8152                label: None,
8153            })
8154            .await
8155            .unwrap();
8156        assert!(
8157            matches!(count, Some(SqlValue::Integer(1))),
8158            "only the post-drop atomic_unit call may have committed; got {count:?}"
8159        );
8160    }
8161
8162    /// ADR-067 Component A entry 10: with `KHIVE_WRITE_QUEUE=1`,
8163    /// `SqliteWriter::execute_batch` (reached via `SqlBridge::writer()`)
8164    /// routes the whole statement list through the WriterTask channel
8165    /// instead of opening its own `BEGIN IMMEDIATE` on the standalone
8166    /// connection, and the row is actually committed and readable back.
8167    #[tokio::test]
8168    async fn execute_batch_routes_through_writer_task_when_flag_enabled() {
8169        let dir = tempfile::tempdir().unwrap();
8170        let path = dir.path().join("write_queue_execute_batch.db");
8171        let config = PoolConfig {
8172            path: Some(path.clone()),
8173            write_queue_enabled: Some(true),
8174            ..PoolConfig::for_test()
8175        };
8176        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8177        {
8178            let guard = pool.writer().unwrap();
8179            guard
8180                .conn()
8181                .execute_batch(
8182                    "CREATE TABLE IF NOT EXISTS write_queue_batch_test \
8183                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
8184                )
8185                .unwrap();
8186        }
8187
8188        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8189
8190        let mut writer = bridge.writer().await.unwrap();
8191        let affected = writer
8192            .execute_batch(vec![
8193                SqlStatement {
8194                    sql: "INSERT INTO write_queue_batch_test (id, val) VALUES (?1, ?2)".into(),
8195                    params: vec![SqlValue::Integer(1), SqlValue::Text("a".into())],
8196                    label: None,
8197                },
8198                SqlStatement {
8199                    sql: "INSERT INTO write_queue_batch_test (id, val) VALUES (?1, ?2)".into(),
8200                    params: vec![SqlValue::Integer(2), SqlValue::Text("b".into())],
8201                    label: None,
8202                },
8203            ])
8204            .await
8205            .unwrap();
8206        assert_eq!(affected, 2);
8207
8208        let mut reader = bridge.reader().await.unwrap();
8209        let count = reader
8210            .query_scalar(SqlStatement {
8211                sql: "SELECT COUNT(*) FROM write_queue_batch_test".into(),
8212                params: vec![],
8213                label: None,
8214            })
8215            .await
8216            .unwrap();
8217        assert!(
8218            matches!(count, Some(SqlValue::Integer(2))),
8219            "expected 2 rows, got {count:?}"
8220        );
8221        assert_eq!(
8222            pool.writer_task_spawn_count(),
8223            1,
8224            "the flag-ON path must actually spawn and use the writer task"
8225        );
8226    }
8227
8228    /// ADR-067 Component A entry 10, atomicity: a batch whose second
8229    /// statement fails (duplicate primary key) must roll back the WHOLE
8230    /// request — including the first statement's otherwise-successful
8231    /// INSERT — because the WriterTask commits or rolls back one
8232    /// `WriteRequest` as a single unit (ADR-067 Component A). Zero rows must
8233    /// land, not one.
8234    #[tokio::test]
8235    async fn execute_batch_rolls_back_atomically_on_mid_sequence_failure() {
8236        let dir = tempfile::tempdir().unwrap();
8237        let path = dir.path().join("write_queue_execute_batch_rollback.db");
8238        let config = PoolConfig {
8239            path: Some(path.clone()),
8240            write_queue_enabled: Some(true),
8241            ..PoolConfig::for_test()
8242        };
8243        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8244        {
8245            let guard = pool.writer().unwrap();
8246            guard
8247                .conn()
8248                .execute_batch(
8249                    "CREATE TABLE IF NOT EXISTS write_queue_rollback_test \
8250                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
8251                )
8252                .unwrap();
8253        }
8254
8255        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8256
8257        let mut writer = bridge.writer().await.unwrap();
8258        let result = writer
8259            .execute_batch(vec![
8260                // Statement 1: succeeds on its own.
8261                SqlStatement {
8262                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
8263                    params: vec![SqlValue::Integer(1), SqlValue::Text("first".into())],
8264                    label: None,
8265                },
8266                // Statement 2: duplicate primary key — fails mid-sequence.
8267                SqlStatement {
8268                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
8269                    params: vec![SqlValue::Integer(1), SqlValue::Text("duplicate".into())],
8270                    label: None,
8271                },
8272                // Statement 3: never reached.
8273                SqlStatement {
8274                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
8275                    params: vec![SqlValue::Integer(2), SqlValue::Text("third".into())],
8276                    label: None,
8277                },
8278            ])
8279            .await;
8280        assert!(
8281            result.is_err(),
8282            "a batch with a mid-sequence PK conflict must return an error"
8283        );
8284
8285        let mut reader = bridge.reader().await.unwrap();
8286        let count = reader
8287            .query_scalar(SqlStatement {
8288                sql: "SELECT COUNT(*) FROM write_queue_rollback_test".into(),
8289                params: vec![],
8290                label: None,
8291            })
8292            .await
8293            .unwrap();
8294        assert!(
8295            matches!(count, Some(SqlValue::Integer(0))),
8296            "the whole request must roll back — including statement 1's \
8297             otherwise-successful INSERT — not just the failing statement; \
8298             got {count:?}"
8299        );
8300    }
8301
8302    #[tokio::test]
8303    async fn in_memory_atomic_unit_terminal_fault_retires_writer() {
8304        use khive_storage::WriterTaskRequestState;
8305        use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
8306
8307        // Exercise operation, commit, and panic failures, including a clean
8308        // panic rollback. Terminal connections must never reenter the pool.
8309        for (mode, deny_rollback, expected) in [
8310            ("error", true, WriterTaskRequestState::SideEffectsUnknown),
8311            ("commit", true, WriterTaskRequestState::SideEffectsUnknown),
8312            ("panic", true, WriterTaskRequestState::SideEffectsUnknown),
8313            (
8314                "panic",
8315                false,
8316                WriterTaskRequestState::TransactionRolledBack,
8317            ),
8318        ] {
8319            let pool = Arc::new(
8320                ConnectionPool::new(PoolConfig {
8321                    path: None,
8322                    ..PoolConfig::default()
8323                })
8324                .unwrap(),
8325            );
8326            {
8327                let guard = pool.writer().unwrap();
8328                guard
8329                    .execute_batch("CREATE TABLE atomic_terminal_probe (id INTEGER PRIMARY KEY)")
8330                    .unwrap();
8331                guard
8332                    .authorizer(Some(move |ctx: AuthContext<'_>| match ctx.action {
8333                        AuthAction::Transaction {
8334                            operation: TransactionOperation::Rollback,
8335                        } if deny_rollback => Authorization::Deny,
8336                        AuthAction::Transaction {
8337                            operation: TransactionOperation::Unknown,
8338                        } if mode == "commit" => Authorization::Deny,
8339                        _ => Authorization::Allow,
8340                    }))
8341                    .unwrap();
8342            }
8343            let bridge = SqlBridge::new(Arc::clone(&pool), false);
8344            let result = bridge
8345                .atomic_unit(Box::new(move |writer| {
8346                    Box::pin(async move {
8347                        writer
8348                            .execute(SqlStatement {
8349                                sql: "INSERT INTO atomic_terminal_probe VALUES (1)".into(),
8350                                params: vec![],
8351                                label: None,
8352                            })
8353                            .await?;
8354                        match mode {
8355                            "error" => Err(StorageError::Internal("terminal probe".into())),
8356                            "panic" => panic!("terminal probe"),
8357                            _ => Ok(Box::new(()) as Box<dyn Any + Send>),
8358                        }
8359                    })
8360                }))
8361                .await;
8362            assert!(
8363                matches!(result, Err(StorageError::WriterTaskTerminated { request_state })
8364                if request_state == expected),
8365                "{mode}: {result:?}"
8366            );
8367            assert!(
8368                pool.try_writer_nowait().is_err(),
8369                "{mode}: writer was not retired"
8370            );
8371            let mut writer = bridge.writer().await.unwrap();
8372            assert!(
8373                writer
8374                    .execute(SqlStatement {
8375                        sql: "INSERT INTO atomic_terminal_probe VALUES (2)".into(),
8376                        params: vec![],
8377                        label: None,
8378                    })
8379                    .await
8380                    .is_err(),
8381                "{mode}: ordinary write reused a terminal connection"
8382            );
8383        }
8384    }
8385
8386    #[tokio::test]
8387    async fn in_memory_atomic_unit_holds_writer_guard_through_rollback() {
8388        use std::sync::atomic::{AtomicBool, Ordering};
8389
8390        let pool = Arc::new(
8391            ConnectionPool::new(PoolConfig {
8392                path: None,
8393                ..PoolConfig::default()
8394            })
8395            .unwrap(),
8396        );
8397        pool.writer()
8398            .unwrap()
8399            .execute_batch("CREATE TABLE atomic_guard_probe (id INTEGER PRIMARY KEY)")
8400            .unwrap();
8401        let bridge = SqlBridge::new(Arc::clone(&pool), false);
8402        let excluded = Arc::new(AtomicBool::new(false));
8403        let observed = Arc::clone(&excluded);
8404        let probe_pool = Arc::clone(&pool);
8405        let result = bridge
8406            .atomic_unit(Box::new(move |writer| {
8407                Box::pin(async move {
8408                    writer
8409                        .execute(SqlStatement {
8410                            sql: "INSERT INTO atomic_guard_probe VALUES (1)".into(),
8411                            params: vec![],
8412                            label: None,
8413                        })
8414                        .await?;
8415                    observed.store(probe_pool.try_writer_nowait().is_err(), Ordering::SeqCst);
8416                    Err(StorageError::Internal("rollback guard probe".into()))
8417                })
8418            }))
8419            .await;
8420        assert!(
8421            matches!(result, Err(StorageError::WriterTaskRequestFailed {
8422                request_state: khive_storage::WriterTaskRequestState::TransactionRolledBack,
8423                ref source,
8424            }) if matches!(source.as_ref(), StorageError::Internal(message)
8425                if message == "rollback guard probe")),
8426            "{result:?}"
8427        );
8428        assert!(
8429            excluded.load(Ordering::SeqCst),
8430            "atomic unit released its writer guard"
8431        );
8432        let mut writer = bridge.writer().await.unwrap();
8433        writer
8434            .execute(SqlStatement {
8435                sql: "INSERT INTO atomic_guard_probe VALUES (2)".into(),
8436                params: vec![],
8437                label: None,
8438            })
8439            .await
8440            .unwrap();
8441        let rows = writer
8442            .query_all(SqlStatement {
8443                sql: "SELECT id FROM atomic_guard_probe ORDER BY id".into(),
8444                params: vec![],
8445                label: None,
8446            })
8447            .await
8448            .unwrap();
8449        assert_eq!(rows.len(), 1, "{rows:?}");
8450        assert!(matches!(rows[0].columns[0].value, SqlValue::Integer(2)));
8451    }
8452
8453    #[tokio::test]
8454    async fn in_memory_atomic_unit_pending_op_rolls_back_and_releases_guard() {
8455        let pool = Arc::new(
8456            ConnectionPool::new(PoolConfig {
8457                path: None,
8458                ..PoolConfig::default()
8459            })
8460            .unwrap(),
8461        );
8462        pool.writer()
8463            .unwrap()
8464            .execute_batch("CREATE TABLE atomic_pending_probe (id INTEGER PRIMARY KEY)")
8465            .unwrap();
8466        let bridge = SqlBridge::new(Arc::clone(&pool), false);
8467        let result = tokio::time::timeout(
8468            std::time::Duration::from_secs(10),
8469            bridge.atomic_unit(Box::new(|writer| {
8470                Box::pin(async move {
8471                    writer
8472                        .execute(SqlStatement {
8473                            sql: "INSERT INTO atomic_pending_probe VALUES (1)".into(),
8474                            params: vec![],
8475                            label: None,
8476                        })
8477                        .await?;
8478                    std::future::pending::<
8479                            khive_storage::types::StorageResult<Box<dyn Any + Send>>,
8480                        >()
8481                        .await
8482                })
8483            })),
8484        )
8485        .await
8486        .expect("suspending atomic unit must return promptly");
8487        assert!(
8488            matches!(result, Err(StorageError::WriterTaskRequestFailed {
8489                request_state: khive_storage::WriterTaskRequestState::TransactionRolledBack,
8490                ref source,
8491            }) if matches!(source.as_ref(), StorageError::Internal(message)
8492                if message.contains("future suspended"))),
8493            "{result:?}"
8494        );
8495        let mut writer = bridge.writer().await.unwrap();
8496        writer
8497            .execute(SqlStatement {
8498                sql: "INSERT INTO atomic_pending_probe VALUES (2)".into(),
8499                params: vec![],
8500                label: None,
8501            })
8502            .await
8503            .unwrap();
8504        let sum = writer
8505            .query_scalar(SqlStatement {
8506                sql: "SELECT SUM(id) FROM atomic_pending_probe".into(),
8507                params: vec![],
8508                label: None,
8509            })
8510            .await
8511            .unwrap();
8512        assert!(matches!(sum, Some(SqlValue::Integer(2))), "{sum:?}");
8513        assert!(pool.try_writer_nowait().unwrap().is_autocommit());
8514    }
8515
8516    /// ADR-067 Component A: before
8517    /// this fix, `block_on_sync` (this file) `unreachable!()`-panicked if
8518    /// an `atomic_unit` closure's future was `Pending` on its first poll.
8519    /// That panic ran inside the writer task's own `spawn_blocking` frame
8520    /// (see `atomic_unit`'s flag-on branch), and `run_writer_task` treats
8521    /// any `spawn_blocking` `JoinError` as fatal — the whole writer task
8522    /// exits, taking down every subsequent write for this pool. Proves the
8523    /// fix: an `atomic_unit` op built to suspend on first poll (via
8524    /// `std::future::pending`, never actually resolving) now returns a
8525    /// clean `Err` from `atomic_unit` — no panic — AND the writer task
8526    /// survives to serve a completely unrelated, well-behaved `atomic_unit`
8527    /// call immediately afterward.
8528    ///
8529    /// Not `#[serial]` / no env var: builds the pool directly with
8530    /// `write_queue_enabled: Some(true)` in the `PoolConfig` literal, same
8531    /// technique as this round's other new routing tests.
8532    #[tokio::test]
8533    async fn atomic_unit_pending_future_errors_without_killing_writer_task() {
8534        let dir = tempfile::tempdir().unwrap();
8535        let path = dir.path().join("atomic_unit_pending_future.db");
8536        let config = PoolConfig {
8537            path: Some(path.clone()),
8538            write_queue_enabled: Some(true),
8539            ..PoolConfig::for_test()
8540        };
8541        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8542        {
8543            let guard = pool.writer().unwrap();
8544            guard
8545                .conn()
8546                .execute_batch(
8547                    "CREATE TABLE IF NOT EXISTS atomic_unit_pending_test \
8548                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
8549                )
8550                .unwrap();
8551        }
8552        assert!(
8553            pool.writer_task_handle().unwrap().is_some(),
8554            "writer task must be spawned with the flag on for a file-backed pool"
8555        );
8556
8557        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8558
8559        // A closure whose future never resolves on first poll — the exact
8560        // misuse `block_on_sync` must reject instead of panicking on.
8561        let pending_op: AtomicUnitOp = Box::new(|_writer| {
8562            Box::pin(std::future::pending::<
8563                khive_storage::types::StorageResult<Box<dyn std::any::Any + Send>>,
8564            >())
8565        });
8566
8567        let pending_result = bridge.atomic_unit(pending_op).await;
8568        assert!(
8569            pending_result.is_err(),
8570            "a Pending-on-first-poll atomic_unit closure must return Err, \
8571             not panic; got {pending_result:?}"
8572        );
8573
8574        // If the panic had instead killed the writer task, every subsequent
8575        // write on this pool (including a completely unrelated, correctly
8576        // non-blocking atomic_unit call) would now fail with a channel-closed
8577        // error. Prove the task is still alive and serving requests.
8578        let ok_op: AtomicUnitOp = Box::new(|writer| {
8579            Box::pin(async move {
8580                writer
8581                    .execute(SqlStatement {
8582                        sql: "INSERT INTO atomic_unit_pending_test (id, val) VALUES (?1, ?2)"
8583                            .into(),
8584                        params: vec![SqlValue::Integer(1), SqlValue::Text("survived".into())],
8585                        label: None,
8586                    })
8587                    .await
8588                    .map_err(|e| {
8589                        khive_storage::StorageError::driver(
8590                            StorageCapability::Sql,
8591                            "atomic_unit_pending_future_test_insert",
8592                            e,
8593                        )
8594                    })?;
8595                Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
8596            })
8597        });
8598        let ok_result = bridge.atomic_unit(ok_op).await;
8599        assert!(
8600            ok_result.is_ok(),
8601            "writer task must survive a Pending misuse and keep serving \
8602             subsequent well-behaved atomic_unit requests; got {ok_result:?}"
8603        );
8604
8605        let mut reader = bridge.reader().await.unwrap();
8606        let count = reader
8607            .query_scalar(SqlStatement {
8608                sql: "SELECT COUNT(*) FROM atomic_unit_pending_test".into(),
8609                params: vec![],
8610                label: None,
8611            })
8612            .await
8613            .unwrap();
8614        assert!(
8615            matches!(count, Some(SqlValue::Integer(1))),
8616            "the well-behaved atomic_unit call after the Pending misuse must \
8617             have actually committed its write; got {count:?}"
8618        );
8619    }
8620
8621    /// ADR-136 D1 gate 1/3: with `KHIVE_WRITE_ROUTING=strict` and no writer
8622    /// task available, `SqlBridge::writer()` must error instead of silently
8623    /// degrading to a standalone connection — even when the reason no handle
8624    /// exists is simply that the queue itself was never enabled. Strict
8625    /// routing without an enabled queue is a caller misconfiguration this
8626    /// gate refuses rather than silently no-ops: an operator who set
8627    /// `KHIVE_WRITE_ROUTING=strict` believing every write is single-admission
8628    /// must be told loudly if `KHIVE_WRITE_QUEUE` was never turned on, not
8629    /// left thinking strict routing is in effect when it is not.
8630    #[tokio::test]
8631    async fn writer_strict_routing_fails_closed_without_writer_task() {
8632        let dir = tempfile::tempdir().unwrap();
8633        let path = dir.path().join("strict_writer.db");
8634        let config = PoolConfig {
8635            path: Some(path),
8636            write_queue_enabled: Some(false),
8637            write_routing_strict: true,
8638            ..PoolConfig::for_test()
8639        };
8640        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8641        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8642
8643        let result = bridge.writer().await;
8644        let err = match result {
8645            Ok(_) => panic!(
8646                "KHIVE_WRITE_ROUTING=strict with no writer task must fail closed, not \
8647                 silently degrade to a standalone connection"
8648            ),
8649            Err(e) => e,
8650        };
8651        assert!(
8652            err.to_string().contains("strict"),
8653            "error must name strict routing, got: {err}"
8654        );
8655    }
8656
8657    /// ADR-136 D1 gate 3/4: with `KHIVE_WRITE_ROUTING=strict` and no writer
8658    /// task available, `SqlBridge::atomic_unit` must error instead of
8659    /// silently falling back to a manual `BEGIN IMMEDIATE` on a standalone
8660    /// connection.
8661    #[tokio::test]
8662    async fn atomic_unit_strict_routing_fails_closed_without_writer_task() {
8663        let dir = tempfile::tempdir().unwrap();
8664        let path = dir.path().join("strict_atomic_unit.db");
8665        let config = PoolConfig {
8666            path: Some(path),
8667            write_queue_enabled: Some(false),
8668            write_routing_strict: true,
8669            ..PoolConfig::for_test()
8670        };
8671        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8672        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8673
8674        let op: AtomicUnitOp = Box::new(|_writer| {
8675            Box::pin(async move { Ok(Box::new(()) as Box<dyn std::any::Any + Send>) })
8676        });
8677        let result = bridge.atomic_unit(op).await;
8678        assert!(
8679            result.is_err(),
8680            "KHIVE_WRITE_ROUTING=strict but the queue is off (no writer task handle) must \
8681             fail closed instead of falling back to a manual BEGIN IMMEDIATE; got {result:?}"
8682        );
8683        let msg = result.unwrap_err().to_string();
8684        assert!(
8685            msg.contains("strict"),
8686            "error must name strict routing, got: {msg}"
8687        );
8688    }
8689
8690    /// ADR-136 D1 gate 3 amendment: production DOES read through a
8691    /// queue-backed `writer()` handle — `khive-pack-comm`'s handlers obtain
8692    /// a writer then call `w.query_row(...)` cursor-style, and
8693    /// `khive-pack-gtd`'s bootstrap calls `w.query_all("PRAGMA
8694    /// table_info...")` on one. Exercise that exact shape under a strict,
8695    /// queue-enabled pool: write through the handle, then read the same row
8696    /// back through it before it is dropped.
8697    #[tokio::test]
8698    async fn writer_handle_supports_read_after_write_under_strict_queue() {
8699        let dir = tempfile::tempdir().unwrap();
8700        let path = dir.path().join("writer_read_after_write.db");
8701        let config = PoolConfig {
8702            path: Some(path),
8703            write_queue_enabled: Some(true),
8704            write_routing_strict: true,
8705            ..PoolConfig::for_test()
8706        };
8707        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8708        {
8709            let guard = pool.writer().unwrap();
8710            guard
8711                .conn()
8712                .execute_batch(
8713                    "CREATE TABLE IF NOT EXISTS writer_cursor_test \
8714                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
8715                )
8716                .unwrap();
8717        }
8718
8719        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8720
8721        let mut w = bridge.writer().await.unwrap();
8722        w.execute(SqlStatement {
8723            sql: "INSERT INTO writer_cursor_test (id, val) VALUES (?1, ?2)".into(),
8724            params: vec![SqlValue::Integer(1), SqlValue::Text("via-writer".into())],
8725            label: None,
8726        })
8727        .await
8728        .unwrap();
8729
8730        let row = w
8731            .query_row(SqlStatement {
8732                sql: "SELECT val FROM writer_cursor_test WHERE id = ?1".into(),
8733                params: vec![SqlValue::Integer(1)],
8734                label: None,
8735            })
8736            .await
8737            .unwrap()
8738            .expect("row inserted through the same writer handle must be visible to it");
8739        assert!(
8740            matches!(&row.columns[0].value, SqlValue::Text(v) if v == "via-writer"),
8741            "query_row through a queue-backed writer handle must see its own \
8742             committed write; got {:?}",
8743            row.columns[0].value
8744        );
8745    }
8746
8747    /// Queue-backed ordinary reads use the pooled reader budget rather than a
8748    /// cached standalone connection. Arm 1 saturates the one reader permit and
8749    /// proves the failure is a visible pooled-admission timeout. Arm 2 proves
8750    /// a handle with no exceptional transaction connection remains reusable
8751    /// through the pool.
8752    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
8753    async fn queue_backed_read_uses_pool_budget_and_remains_reusable_after_saturation() {
8754        let dir = tempfile::tempdir().unwrap();
8755        let path = dir.path().join("queue_backed_reader_budget.db");
8756        let config = PoolConfig {
8757            path: Some(path),
8758            write_queue_enabled: Some(true),
8759            write_routing_strict: true,
8760            max_readers: 1,
8761            checkout_timeout: std::time::Duration::from_millis(250),
8762            ..PoolConfig::default()
8763        };
8764        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8765        {
8766            let guard = pool.writer().unwrap();
8767            guard
8768                .conn()
8769                .execute_batch(
8770                    "CREATE TABLE IF NOT EXISTS reopen_test \
8771                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
8772                )
8773                .unwrap();
8774        }
8775        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8776
8777        let mut w = bridge.writer().await.unwrap();
8778        w.execute(SqlStatement {
8779            sql: "INSERT INTO reopen_test (id, val) VALUES (1, 'seed')".into(),
8780            params: vec![],
8781            label: None,
8782        })
8783        .await
8784        .unwrap();
8785
8786        // Arm 1: with the sole reader permit held, the queue-backed read
8787        // must time out on the reader budget.
8788        let held = pool
8789            .sql_bridge_reader_slots()
8790            .acquire_owned()
8791            .await
8792            .unwrap();
8793        let starved = w
8794            .query_row(SqlStatement {
8795                sql: "SELECT val FROM reopen_test WHERE id = 1".into(),
8796                params: vec![],
8797                label: None,
8798            })
8799            .await;
8800        assert!(
8801            matches!(
8802                &starved,
8803                Err(StorageError::AdmissionTimeout { operation, .. })
8804                    if operation.as_ref() == "writer.query_row"
8805            ),
8806            "queue-backed read with reader permits saturated must time out \
8807             at the shared pooled-reader admission stage; \
8808             got {starved:?}"
8809        );
8810        let saturated = pool.reader_acquisition_snapshot();
8811        assert_eq!(saturated.checkout_timeouts, 1);
8812        assert_eq!(saturated.standalone_opens, 0);
8813        drop(held);
8814
8815        // Arm 2: a queue-backed handle in the exact post-cancelled-read
8816        // state — `handle: None` — must serve the next ordinary read through
8817        // the pool, never a hard "connection already consumed" failure (that
8818        // contract is specific to a poisoned explicit-transaction reader).
8819        // Constructed directly so the state is deterministic.
8820        let writer_task = pool
8821            .writer_task_handle()
8822            .expect("queue-enabled file pool must offer a writer task")
8823            .expect("writer task present under write_queue_enabled");
8824        let mut post_cancel = SqliteWriter {
8825            handle: None,
8826            writer_task: Some(writer_task),
8827            origin: pool.origin(),
8828            db: crate::timeout_sink::db_label(&pool),
8829            pool: Arc::clone(&pool),
8830        };
8831        let row = post_cancel
8832            .query_row(SqlStatement {
8833                sql: "SELECT val FROM reopen_test WHERE id = 1".into(),
8834                params: vec![],
8835                label: None,
8836            })
8837            .await
8838            .expect("read on a queue-backed handle with no transaction connection must pool")
8839            .expect("seeded row must be visible");
8840        assert!(
8841            matches!(&row.columns[0].value, SqlValue::Text(v) if v == "seed"),
8842            "pooled read must return the seeded row; got {:?}",
8843            row.columns[0].value
8844        );
8845    }
8846
8847    /// ADR-136 D1 gate 3 amendment: `SqlWriter::query_row`/`query_all` carry
8848    /// no read-only restriction at the trait level — a caller could hand a
8849    /// DML-with-RETURNING statement to `query_row`. A queue-backed handle now
8850    /// sends ordinary reads through a pooled read-only connection, so SQLite
8851    /// rejects the statement instead of mutating on an untracked writer.
8852    #[tokio::test]
8853    async fn writer_query_row_rejects_dml_with_returning_on_queue_backed_handle() {
8854        let dir = tempfile::tempdir().unwrap();
8855        let path = dir.path().join("writer_readonly_returning.db");
8856        let config = PoolConfig {
8857            path: Some(path),
8858            write_queue_enabled: Some(true),
8859            write_routing_strict: true,
8860            ..PoolConfig::for_test()
8861        };
8862        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8863        {
8864            let guard = pool.writer().unwrap();
8865            guard
8866                .conn()
8867                .execute_batch(
8868                    "CREATE TABLE IF NOT EXISTS writer_returning_test \
8869                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL);
8870                     INSERT INTO writer_returning_test (id, val) VALUES (1, 'original');",
8871                )
8872                .unwrap();
8873        }
8874
8875        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8876
8877        let mut w = bridge.writer().await.unwrap();
8878        let result = w
8879            .query_row(SqlStatement {
8880                sql: "UPDATE writer_returning_test SET val = 'mutated' \
8881                      WHERE id = ?1 RETURNING val"
8882                    .into(),
8883                params: vec![SqlValue::Integer(1)],
8884                label: None,
8885            })
8886            .await;
8887        assert!(
8888            result.is_err(),
8889            "a DML-with-RETURNING statement through query_row on a \
8890             queue-backed writer handle must be rejected, not executed on \
8891             an untracked read-write connection; got {result:?}"
8892        );
8893
8894        let mut reader = bridge.reader().await.unwrap();
8895        let val = reader
8896            .query_scalar(SqlStatement {
8897                sql: "SELECT val FROM writer_returning_test WHERE id = ?1".into(),
8898                params: vec![SqlValue::Integer(1)],
8899                label: None,
8900            })
8901            .await
8902            .unwrap();
8903        assert!(
8904            matches!(&val, Some(SqlValue::Text(v)) if v == "original"),
8905            "the rejected UPDATE...RETURNING must not have altered the row; got {val:?}"
8906        );
8907    }
8908
8909    /// The three queue-backed `SqlReader` methods on `SqliteWriter` classify
8910    /// transaction control, but a queue-backed handle with no cached
8911    /// transaction and no transaction-control statement falls through to the
8912    /// same pooled reader route `PoolBackedReader`/`SqliteReader` use
8913    /// (`run_pool_reader_query`) — without first running the statement
8914    /// through `admit_reader_capability_sql`. A setting `PRAGMA` (unlike a
8915    /// DML write) does not touch the main database file, so SQLite's own
8916    /// read-only connection flag does not refuse it; without the admission
8917    /// gate it would silently change connection-local state on a connection
8918    /// this same pool later hands back out.
8919    #[tokio::test]
8920    async fn writer_query_row_rejects_setting_pragma_on_queue_backed_reader_route() {
8921        let dir = tempfile::tempdir().unwrap();
8922        let path = dir.path().join("writer_reader_route_pragma_admission.db");
8923        let config = PoolConfig {
8924            path: Some(path),
8925            write_queue_enabled: Some(true),
8926            write_routing_strict: true,
8927            ..PoolConfig::for_test()
8928        };
8929        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8930        let bridge = SqlBridge::new(Arc::clone(&pool), true);
8931
8932        let mut w = bridge.writer().await.unwrap();
8933        let result = w
8934            .query_row(SqlStatement {
8935                sql: "PRAGMA cache_size = -999".into(),
8936                params: vec![],
8937                label: None,
8938            })
8939            .await;
8940        assert!(
8941            result.is_err(),
8942            "a setting PRAGMA through the queue-backed writer's reader route must be \
8943             refused, exactly like it is through PoolBackedReader/SqliteReader; \
8944             got {result:?}"
8945        );
8946    }
8947
8948    /// ADR-136 D1 acceptance arm: a 5-op batch shaped like `[send, mark,
8949    /// mark, mark, mark]` at the storage layer — every "mark" op is a real
8950    /// `UPDATE` against its own pre-seeded row, so (like `send`) it routes
8951    /// through the writer task rather than bypassing it as a `SELECT` would
8952    /// — issued while 3 concurrent writers contend the write path, must
8953    /// complete every op — no checkout timeout — once routing is strict and
8954    /// the queue is on. An occupier holds the writer task's single drain
8955    /// slot until all 8 requests (3 contenders + send + 4 marks) are
8956    /// provably enqueued behind it (`queue_depth() >= 8`, the same
8957    /// occupier/`queue_depth()` discriminator the migrated-call-site tests
8958    /// use), so this proves genuine contention instead of a scheduler that
8959    /// happens to drain the tiny writes before the others even enqueue.
8960    /// Mirrors the measured production failure ADR-136's Context section
8961    /// documents (middle ops of a batch starving while a sibling write wins
8962    /// under the legacy fixed-deadline pool mutex). Red-proofed: reverting
8963    /// the marks back to `bridge.reader()` `SELECT`s (the pre-fix shape)
8964    /// makes the `queue_depth() >= 8` wait time out and fail, since a read
8965    /// never reaches the writer task's channel — confirming this version
8966    /// actually requires all four marks to be real writes.
8967    #[tokio::test]
8968    async fn acceptance_five_op_batch_completes_under_concurrent_write_contention() {
8969        let dir = tempfile::tempdir().unwrap();
8970        let path = dir.path().join("acceptance_batch.db");
8971        let config = PoolConfig {
8972            path: Some(path),
8973            write_queue_enabled: Some(true),
8974            write_routing_strict: true,
8975            ..PoolConfig::for_test()
8976        };
8977        let pool = Arc::new(ConnectionPool::new(config).unwrap());
8978        {
8979            let guard = pool.writer().unwrap();
8980            guard
8981                .conn()
8982                .execute_batch(
8983                    "CREATE TABLE IF NOT EXISTS acceptance_batch \
8984                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL);
8985                     INSERT INTO acceptance_batch (id, val) VALUES \
8986                     (200, 'seed-0'), (201, 'seed-1'), (202, 'seed-2'), (203, 'seed-3');",
8987                )
8988                .unwrap();
8989        }
8990
8991        let bridge = Arc::new(SqlBridge::new(Arc::clone(&pool), true));
8992
8993        let writer_task = pool
8994            .writer_task_handle()
8995            .unwrap()
8996            .expect("writer task must be spawned for a file-backed pool with the flag on");
8997
8998        // Occupier: holds the single writer-task drain slot until released,
8999        // so every op below is provably queued behind it rather than racing
9000        // to finish before the others even enqueue (same technique as
9001        // `rename_namespace_routes_through_writer_task_when_flag_enabled` in
9002        // `stores::text_tests`).
9003        let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
9004        let (release_tx, release_rx) = tokio::sync::oneshot::channel::<()>();
9005        let occupier = {
9006            let writer_task = writer_task.clone();
9007            tokio::spawn(async move {
9008                writer_task
9009                    .send(move |_conn| {
9010                        let _ = started_tx.send(());
9011                        let _ = release_rx.blocking_recv();
9012                        Ok::<(), StorageError>(())
9013                    })
9014                    .await
9015            })
9016        };
9017        started_rx
9018            .await
9019            .expect("occupier must signal it has started running inside the writer task");
9020        assert_eq!(
9021            writer_task.queue_depth(),
9022            0,
9023            "channel must start empty once the occupier has been dequeued and is running"
9024        );
9025
9026        // 3 concurrent writers contending the write path — each a
9027        // self-contained `execute()` through `SqlBridge::writer()`, matching
9028        // the "several short acquisitions" shape ADR-136's Context section
9029        // measures (a logical write is many short holds, not one long one).
9030        let contenders: Vec<_> = (0..3)
9031            .map(|i| {
9032                let bridge = Arc::clone(&bridge);
9033                tokio::spawn(async move {
9034                    let mut writer = bridge.writer().await?;
9035                    writer
9036                        .execute(SqlStatement {
9037                            sql: "INSERT INTO acceptance_batch (id, val) VALUES (?1, ?2)".into(),
9038                            params: vec![
9039                                SqlValue::Integer(100 + i),
9040                                SqlValue::Text(format!("contender-{i}")),
9041                            ],
9042                            label: None,
9043                        })
9044                        .await
9045                })
9046            })
9047            .collect();
9048
9049        // The 5-op batch: [send, mark, mark, mark, mark].
9050        let send = {
9051            let bridge = Arc::clone(&bridge);
9052            tokio::spawn(async move {
9053                let mut writer = bridge.writer().await?;
9054                writer
9055                    .execute(SqlStatement {
9056                        sql: "INSERT INTO acceptance_batch (id, val) VALUES (?1, ?2)".into(),
9057                        params: vec![SqlValue::Integer(1), SqlValue::Text("send".into())],
9058                        label: None,
9059                    })
9060                    .await
9061            })
9062        };
9063        let marks: Vec<_> = (0..4)
9064            .map(|i| {
9065                let bridge = Arc::clone(&bridge);
9066                tokio::spawn(async move {
9067                    let mut writer = bridge.writer().await?;
9068                    writer
9069                        .execute(SqlStatement {
9070                            sql: "UPDATE acceptance_batch SET val = ?2 WHERE id = ?1".into(),
9071                            params: vec![
9072                                SqlValue::Integer(200 + i),
9073                                SqlValue::Text(format!("marked-{i}")),
9074                            ],
9075                            label: None,
9076                        })
9077                        .await
9078                })
9079            })
9080            .collect();
9081
9082        // All 8 requests must actually reach the writer task's channel
9083        // while the occupier still holds the single drain slot.
9084        let mut saw_all_enqueued = false;
9085        for _ in 0..200 {
9086            if writer_task.queue_depth() >= 8 {
9087                saw_all_enqueued = true;
9088                break;
9089            }
9090            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
9091        }
9092        assert!(
9093            saw_all_enqueued,
9094            "not all 8 contending writes (3 contenders + send + 4 marks) reached \
9095             the writer task's channel while the occupier held the single drain \
9096             slot — got depth {}",
9097            writer_task.queue_depth()
9098        );
9099
9100        release_tx
9101            .send(())
9102            .expect("occupier must still be waiting on the release signal");
9103        occupier
9104            .await
9105            .expect("occupier task must not panic")
9106            .expect("occupier write must succeed");
9107
9108        for c in contenders {
9109            c.await
9110                .expect("contender task must not panic")
9111                .expect("contender write must complete without a checkout timeout");
9112        }
9113        send.await
9114            .expect("send task must not panic")
9115            .expect("send op must complete without a checkout timeout");
9116        for (i, m) in marks.into_iter().enumerate() {
9117            let affected = m
9118                .await
9119                .expect("mark task must not panic")
9120                .expect("mark op must complete without a checkout timeout — no starvation");
9121            assert_eq!(
9122                affected, 1,
9123                "mark {i} must have updated exactly its own row"
9124            );
9125        }
9126
9127        let mut reader = bridge.reader().await.unwrap();
9128        let count = reader
9129            .query_scalar(SqlStatement {
9130                sql: "SELECT COUNT(*) FROM acceptance_batch".into(),
9131                params: vec![],
9132                label: None,
9133            })
9134            .await
9135            .unwrap();
9136        assert!(
9137            matches!(count, Some(SqlValue::Integer(8))),
9138            "the 4 seeded mark rows plus 3 contenders plus the batch's own send \
9139             must all be present; got {count:?}"
9140        );
9141
9142        for i in 0..4i64 {
9143            let mut reader = bridge.reader().await.unwrap();
9144            let val = reader
9145                .query_scalar(SqlStatement {
9146                    sql: "SELECT val FROM acceptance_batch WHERE id = ?1".into(),
9147                    params: vec![SqlValue::Integer(200 + i)],
9148                    label: None,
9149                })
9150                .await
9151                .unwrap();
9152            assert!(
9153                matches!(&val, Some(SqlValue::Text(v)) if *v == format!("marked-{i}")),
9154                "mark row {i} must reflect the persisted UPDATE after release; got {val:?}"
9155            );
9156        }
9157    }
9158
9159    #[tokio::test]
9160    async fn file_backed_bridge_counts_writer_and_flag_off_atomic_unit_acquisitions() {
9161        let dir = tempfile::tempdir().unwrap();
9162        let config = PoolConfig {
9163            path: Some(dir.path().join("bridge_writer_acquisitions.db")),
9164            write_queue_enabled: Some(false),
9165            ..PoolConfig::for_test()
9166        };
9167        let pool = Arc::new(ConnectionPool::new(config).unwrap());
9168        let bridge = SqlBridge::new(Arc::clone(&pool), true);
9169
9170        let before = pool.writer_acquisition_snapshot();
9171
9172        drop(bridge.writer().await.unwrap());
9173        let after_writer = pool.writer_acquisition_snapshot();
9174        assert_eq!(
9175            after_writer.standalone_acquisitions,
9176            before.standalone_acquisitions + 1
9177        );
9178        assert_eq!(after_writer.acquisitions, before.acquisitions + 1);
9179        assert_eq!(after_writer.pooled_acquisitions, before.pooled_acquisitions);
9180        assert_eq!(
9181            after_writer.writer_task_acquisitions,
9182            before.writer_task_acquisitions
9183        );
9184
9185        let op: AtomicUnitOp = Box::new(|_writer| {
9186            Box::pin(async { Ok(Box::new(()) as Box<dyn std::any::Any + Send>) })
9187        });
9188        bridge.atomic_unit(op).await.unwrap();
9189
9190        let after_atomic_unit = pool.writer_acquisition_snapshot();
9191        assert_eq!(
9192            after_atomic_unit.standalone_acquisitions,
9193            before.standalone_acquisitions + 2
9194        );
9195        assert_eq!(after_atomic_unit.acquisitions, before.acquisitions + 2);
9196        assert_eq!(
9197            after_atomic_unit.pooled_acquisitions,
9198            before.pooled_acquisitions
9199        );
9200        assert_eq!(
9201            after_atomic_unit.writer_task_acquisitions,
9202            before.writer_task_acquisitions
9203        );
9204    }
9205
9206    /// `max_completed_hold_operation` must name the caller's own read, so a
9207    /// recorded maximum hold can be attributed to the query that caused it.
9208    ///
9209    /// The pool's attribution machinery was already correct and covered by
9210    /// `longest_completed_hold_names_its_operation` in `pool.rs`. What defeated
9211    /// it was this bridge: every pooled read handed the slot one shared
9212    /// constant, so the field was two-valued across the whole product while
9213    /// still returning a plausible-looking name (#2793).
9214    ///
9215    /// The discriminating assertion is the last one, that the two arms DIFFER.
9216    /// A single constant satisfies any per-arm expectation you write; only a
9217    /// pair of distinct labels shows that the caller's name reached the slot.
9218    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
9219    async fn max_completed_hold_operation_names_the_caller_read_not_a_bridge_constant() {
9220        async fn recorded_hold_operation(through_writer: bool) -> (Option<&'static str>, u64) {
9221            let dir = tempfile::tempdir().unwrap();
9222            let path = dir.path().join("hold_attribution.db");
9223            let pool = Arc::new(
9224                ConnectionPool::new(PoolConfig {
9225                    path: Some(path),
9226                    write_queue_enabled: Some(true),
9227                    write_routing_strict: true,
9228                    ..PoolConfig::for_test()
9229                })
9230                .unwrap(),
9231            );
9232            {
9233                let guard = pool.writer().unwrap();
9234                guard
9235                    .conn()
9236                    .execute_batch("CREATE TABLE IF NOT EXISTS hold_attr (id INTEGER PRIMARY KEY)")
9237                    .unwrap();
9238            }
9239            let bridge = SqlBridge::new(Arc::clone(&pool), true);
9240            let statement = SqlStatement {
9241                sql: "SELECT id FROM hold_attr".into(),
9242                params: vec![],
9243                label: None,
9244            };
9245            if through_writer {
9246                let mut w = bridge.writer().await.unwrap();
9247                w.query_row(statement).await.unwrap();
9248            } else {
9249                let mut r = bridge.reader().await.unwrap();
9250                r.query_all(statement).await.unwrap();
9251            }
9252            let snapshot = pool.reader_acquisition_snapshot();
9253            (
9254                snapshot.max_completed_hold_operation,
9255                snapshot.completed_pooled_checkouts,
9256            )
9257        }
9258
9259        let (read_side, read_completed) = recorded_hold_operation(false).await;
9260        let (write_side, write_completed) = recorded_hold_operation(true).await;
9261
9262        assert!(
9263            read_completed >= 1 && write_completed >= 1,
9264            "both arms must complete a pooled checkout, or the attribution \
9265             below is reading an empty population; got {read_completed} and \
9266             {write_completed}"
9267        );
9268        // The discriminating assertion runs FIRST, deliberately. Under the
9269        // shared-constant behaviour both arms collapse to one value, and if a
9270        // per-arm equality ran ahead of this it would fire instead, so the
9271        // assertion that actually separates "attributed" from "constant" would
9272        // never execute in the case it exists for.
9273        assert_ne!(
9274            read_side, write_side,
9275            "the recorded operation must tell two different reads apart; a \
9276             shared bridge constant makes these equal while still looking \
9277             like an answer"
9278        );
9279        assert_eq!(
9280            read_side,
9281            Some("query_all"),
9282            "a pooled read drawn through the reader must record its own operation"
9283        );
9284        assert_eq!(
9285            write_side,
9286            Some("writer.query_row"),
9287            "a pooled read drawn through the writer must record its own operation"
9288        );
9289    }
9290}