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**: Opens standalone connections per reader/writer call (high concurrency).
5//!   Cross-statement atomicity goes through `atomic_unit`, which drives a single
6//!   registered raw transaction span rather than a caller-held per-tx connection.
7//! - **Memory**: Uses pool-backed approach (acquire pool connection per-query inside `spawn_blocking`).
8
9use std::any::Any;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13
14use khive_storage::error::StorageError;
15use khive_storage::types::{SqlColumn, SqlRow, SqlStatement, SqlValue};
16use khive_storage::{AtomicUnitOp, StorageCapability};
17
18use crate::error::SqliteError;
19use crate::pool::ConnectionPool;
20
21// =============================================================================
22// Shared helpers
23// =============================================================================
24
25/// Convert a rusqlite `Row` into an owned `SqlRow`.
26fn row_to_sql_row(row: &rusqlite::Row<'_>, col_count: usize, col_names: &[String]) -> SqlRow {
27    let mut columns = Vec::with_capacity(col_count);
28    for i in 0..col_count {
29        let value = match row.get_ref(i) {
30            Ok(rusqlite::types::ValueRef::Null) => SqlValue::Null,
31            Ok(rusqlite::types::ValueRef::Integer(v)) => SqlValue::Integer(v),
32            Ok(rusqlite::types::ValueRef::Real(v)) => SqlValue::Float(v),
33            Ok(rusqlite::types::ValueRef::Text(bytes)) => {
34                SqlValue::Text(String::from_utf8_lossy(bytes).into_owned())
35            }
36            Ok(rusqlite::types::ValueRef::Blob(bytes)) => SqlValue::Blob(bytes.to_vec()),
37            Err(_) => SqlValue::Null,
38        };
39        columns.push(SqlColumn {
40            name: col_names.get(i).cloned().unwrap_or_default(),
41            value,
42        });
43    }
44    SqlRow { columns }
45}
46
47/// Bind `SqlValue` parameters to a rusqlite statement.
48///
49/// `pub(crate)` (ADR-099 B3 r6 structural cut): reused by the pure
50/// `*_statement` builders in `stores::{entity,note,graph,text,vectors}` so
51/// that every store's async execution path and the ADR-099 `--atomic`
52/// prepare path bind params identically — one implementation, not two.
53pub(crate) fn bind_params(
54    stmt: &mut rusqlite::Statement<'_>,
55    params: &[SqlValue],
56) -> Result<(), rusqlite::Error> {
57    for (i, param) in params.iter().enumerate() {
58        let idx = i + 1; // rusqlite uses 1-based indexing
59        match param {
60            SqlValue::Null => stmt.raw_bind_parameter(idx, rusqlite::types::Null)?,
61            SqlValue::Bool(v) => stmt.raw_bind_parameter(idx, *v as i64)?,
62            SqlValue::Integer(v) => stmt.raw_bind_parameter(idx, *v)?,
63            SqlValue::Float(v) => stmt.raw_bind_parameter(idx, *v)?,
64            SqlValue::Text(v) => stmt.raw_bind_parameter(idx, v.as_str())?,
65            SqlValue::Blob(v) => stmt.raw_bind_parameter(idx, v.as_slice())?,
66            SqlValue::Json(v) => {
67                let s = serde_json::to_string(v).unwrap_or_default();
68                stmt.raw_bind_parameter(idx, s.as_str())?;
69            }
70            SqlValue::Uuid(v) => stmt.raw_bind_parameter(idx, v.to_string().as_str())?,
71            SqlValue::Timestamp(v) => {
72                stmt.raw_bind_parameter(idx, v.timestamp_micros())?;
73            }
74        }
75    }
76    Ok(())
77}
78
79/// Execute a query on a `rusqlite::Connection` and return owned rows.
80fn execute_query(
81    conn: &rusqlite::Connection,
82    statement: &SqlStatement,
83) -> Result<Vec<SqlRow>, rusqlite::Error> {
84    let mut stmt = conn.prepare(&statement.sql)?;
85    bind_params(&mut stmt, &statement.params)?;
86
87    let col_count = stmt.column_count();
88    let col_names: Vec<String> = (0..col_count)
89        .map(|i| stmt.column_name(i).unwrap_or("").to_string())
90        .collect();
91
92    let mut rows = Vec::new();
93    let mut raw_rows = stmt.raw_query();
94    while let Some(row) = raw_rows.next()? {
95        rows.push(row_to_sql_row(row, col_count, &col_names));
96    }
97    Ok(rows)
98}
99
100/// Map a rusqlite error to `StorageError`.
101fn map_rusqlite_err(e: rusqlite::Error, op: &'static str) -> StorageError {
102    StorageError::driver(StorageCapability::Sql, op, e)
103}
104
105// =============================================================================
106// Standalone connection readers/writers (file-backed databases)
107// =============================================================================
108
109fn open_standalone_reader(pool: &ConnectionPool) -> Result<rusqlite::Connection, StorageError> {
110    let config = pool.config();
111    let path = config.path.as_ref().ok_or_else(|| StorageError::Pool {
112        operation: "reader".into(),
113        message: "in-memory databases do not support standalone readers; use pool-backed".into(),
114    })?;
115
116    let conn = rusqlite::Connection::open_with_flags(
117        path,
118        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
119            | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX
120            | rusqlite::OpenFlags::SQLITE_OPEN_URI,
121    )
122    .map_err(|e| map_rusqlite_err(e, "open_reader"))?;
123
124    conn.busy_timeout(config.busy_timeout)
125        .map_err(|e| map_rusqlite_err(e, "open_reader"))?;
126    conn.pragma_update(None, "cache_size", "-65536")
127        .map_err(|e| map_rusqlite_err(e, "open_reader"))?;
128    conn.pragma_update(None, "mmap_size", "1073741824")
129        .map_err(|e| map_rusqlite_err(e, "open_reader"))?;
130
131    Ok(conn)
132}
133
134fn open_standalone_writer(pool: &ConnectionPool) -> Result<rusqlite::Connection, StorageError> {
135    let config = pool.config();
136    let path = config.path.as_ref().ok_or_else(|| StorageError::Pool {
137        operation: "writer".into(),
138        message: "in-memory databases do not support standalone writer; use pool-backed".into(),
139    })?;
140
141    let conn = rusqlite::Connection::open_with_flags(
142        path,
143        rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
144            | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX
145            | rusqlite::OpenFlags::SQLITE_OPEN_URI,
146    )
147    .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
148
149    conn.busy_timeout(config.busy_timeout)
150        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
151    conn.pragma_update(None, "cache_size", "-65536")
152        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
153    conn.pragma_update(None, "mmap_size", "1073741824")
154        .map_err(|e| map_rusqlite_err(e, "open_writer"))?;
155
156    Ok(conn)
157}
158
159// =============================================================================
160// File-backed: SqliteReader (standalone connection)
161// =============================================================================
162
163struct SqliteReader {
164    conn: Option<rusqlite::Connection>,
165}
166
167#[async_trait]
168impl khive_storage::SqlReader for SqliteReader {
169    async fn query_row(
170        &mut self,
171        statement: SqlStatement,
172    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
173        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
174            operation: "query_row".into(),
175            message: "connection already consumed".into(),
176        })?;
177        let (conn, result) = tokio::task::spawn_blocking(move || {
178            let res = execute_query(&conn, &statement);
179            (conn, res)
180        })
181        .await
182        .map_err(|e| StorageError::driver(StorageCapability::Sql, "query_row", e))?;
183        self.conn = Some(conn);
184        let rows = result.map_err(|e| map_rusqlite_err(e, "query_row"))?;
185        Ok(rows.into_iter().next())
186    }
187
188    async fn query_all(
189        &mut self,
190        statement: SqlStatement,
191    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
192        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
193            operation: "query_all".into(),
194            message: "connection already consumed".into(),
195        })?;
196        let (conn, result) = tokio::task::spawn_blocking(move || {
197            let res = execute_query(&conn, &statement);
198            (conn, res)
199        })
200        .await
201        .map_err(|e| StorageError::driver(StorageCapability::Sql, "query_all", e))?;
202        self.conn = Some(conn);
203        result.map_err(|e| map_rusqlite_err(e, "query_all"))
204    }
205
206    async fn query_scalar(
207        &mut self,
208        statement: SqlStatement,
209    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
210        let row = self.query_row(statement).await?;
211        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
212    }
213
214    async fn explain(
215        &mut self,
216        statement: SqlStatement,
217    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
218        let explain_stmt = SqlStatement {
219            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
220            params: statement.params,
221            label: statement.label,
222        };
223        self.query_all(explain_stmt).await
224    }
225}
226
227// =============================================================================
228// File-backed: SqliteWriter (standalone connection)
229// =============================================================================
230
231struct SqliteWriter {
232    conn: Option<rusqlite::Connection>,
233    /// ADR-067 Component A: when the write queue is enabled, `execute_batch`
234    /// routes the whole caller-supplied statement list through the
235    /// single-writer task instead of opening its own `BEGIN IMMEDIATE` on
236    /// `conn`. `None` when the flag is off or no writer task is available
237    /// (best-effort — degrades to the standalone-connection path below).
238    writer_task: Option<crate::writer_task::WriterTaskHandle>,
239    /// The origin (ADR-091 backend-scoped attribution) of the pool this
240    /// standalone connection was opened against.
241    origin: khive_storage::tx_registry::TxOrigin,
242}
243
244#[async_trait]
245impl khive_storage::SqlReader for SqliteWriter {
246    async fn query_row(
247        &mut self,
248        statement: SqlStatement,
249    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
250        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
251            operation: "writer.query_row".into(),
252            message: "connection already consumed".into(),
253        })?;
254        let (conn, result) = tokio::task::spawn_blocking(move || {
255            let res = execute_query(&conn, &statement);
256            (conn, res)
257        })
258        .await
259        .map_err(|e| StorageError::driver(StorageCapability::Sql, "writer.query_row", e))?;
260        self.conn = Some(conn);
261        let rows = result.map_err(|e| map_rusqlite_err(e, "writer.query_row"))?;
262        Ok(rows.into_iter().next())
263    }
264
265    async fn query_all(
266        &mut self,
267        statement: SqlStatement,
268    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
269        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
270            operation: "writer.query_all".into(),
271            message: "connection already consumed".into(),
272        })?;
273        let (conn, result) = tokio::task::spawn_blocking(move || {
274            let res = execute_query(&conn, &statement);
275            (conn, res)
276        })
277        .await
278        .map_err(|e| StorageError::driver(StorageCapability::Sql, "writer.query_all", e))?;
279        self.conn = Some(conn);
280        result.map_err(|e| map_rusqlite_err(e, "writer.query_all"))
281    }
282
283    async fn query_scalar(
284        &mut self,
285        statement: SqlStatement,
286    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
287        let row = khive_storage::SqlReader::query_row(self, statement).await?;
288        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
289    }
290
291    async fn explain(
292        &mut self,
293        statement: SqlStatement,
294    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
295        let explain_stmt = SqlStatement {
296            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
297            params: statement.params,
298            label: statement.label,
299        };
300        khive_storage::SqlReader::query_all(self, explain_stmt).await
301    }
302}
303
304#[async_trait]
305impl khive_storage::SqlWriter for SqliteWriter {
306    async fn execute(
307        &mut self,
308        statement: SqlStatement,
309    ) -> khive_storage::types::StorageResult<u64> {
310        // ADR-067 Component A (Fork C slice 2): a single statement is
311        // self-contained, just like `execute_batch`'s full statement list —
312        // route it through the writer task when available. `self.conn` is
313        // left untouched so a subsequent `execute`/`execute_script` call on
314        // this same handle still works over the standalone connection.
315        if let Some(writer_task) = self.writer_task.clone() {
316            return writer_task
317                .send(move |conn| {
318                    let mut stmt = conn
319                        .prepare(&statement.sql)
320                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
321                    bind_params(&mut stmt, &statement.params)
322                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
323                    let affected = stmt
324                        .raw_execute()
325                        .map_err(|e| map_rusqlite_err(e, "execute"))?;
326                    Ok(affected as u64)
327                })
328                .await;
329        }
330
331        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
332            operation: "execute".into(),
333            message: "connection already consumed".into(),
334        })?;
335        let (conn, result) = tokio::task::spawn_blocking(move || {
336            let res = (|| -> Result<usize, rusqlite::Error> {
337                let mut stmt = conn.prepare(&statement.sql)?;
338                bind_params(&mut stmt, &statement.params)?;
339                stmt.raw_execute()
340            })();
341            (conn, res)
342        })
343        .await
344        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute", e))?;
345        self.conn = Some(conn);
346        let affected = result.map_err(|e| map_rusqlite_err(e, "execute"))?;
347        Ok(affected as u64)
348    }
349
350    async fn execute_batch(
351        &mut self,
352        statements: Vec<SqlStatement>,
353    ) -> khive_storage::types::StorageResult<u64> {
354        // ADR-067 Component A: this call is self-contained (the full statement
355        // list is supplied up front and the whole thing commits or rolls back
356        // as one unit) — unlike `writer()`'s live incrementally-driven handle,
357        // it maps cleanly onto a single `WriteRequest`. Route it through the
358        // writer task when available; `self.conn` is left untouched so a
359        // subsequent `execute`/`execute_script` call on this same handle still
360        // works over the standalone connection (that dispatch is unmigrated —
361        // see `SqlBridge::writer()`).
362        if let Some(writer_task) = self.writer_task.clone() {
363            return writer_task
364                .send(move |conn| {
365                    let mut total: u64 = 0;
366                    for statement in &statements {
367                        let mut stmt = conn
368                            .prepare(&statement.sql)
369                            .map_err(|e| map_rusqlite_err(e, "execute_batch"))?;
370                        bind_params(&mut stmt, &statement.params)
371                            .map_err(|e| map_rusqlite_err(e, "execute_batch"))?;
372                        total += stmt
373                            .raw_execute()
374                            .map_err(|e| map_rusqlite_err(e, "execute_batch"))?
375                            as u64;
376                    }
377                    Ok(total)
378                })
379                .await;
380        }
381
382        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
383            operation: "execute_batch".into(),
384            message: "connection already consumed".into(),
385        })?;
386        let origin = self.origin.clone();
387        let (conn, result) = tokio::task::spawn_blocking(move || {
388            if let Err(e) = conn.execute_batch("BEGIN IMMEDIATE") {
389                return (conn, Err(e));
390            }
391            // Registered only after BEGIN succeeds, so an unopened transaction is
392            // never counted. The handle is declared here — enclosing both the
393            // statement-execution closure below AND the ROLLBACK path — so it
394            // stays registered until the transaction is actually finished
395            // (COMMIT or ROLLBACK), not just until the inner closure returns.
396            let _tx_handle = khive_storage::tx_registry::register_scoped(
397                Some("execute_batch".to_string()),
398                origin,
399            );
400            let result = (|| -> Result<u64, rusqlite::Error> {
401                let mut total: u64 = 0;
402                for statement in &statements {
403                    let mut stmt = conn.prepare(&statement.sql)?;
404                    bind_params(&mut stmt, &statement.params)?;
405                    total += stmt.raw_execute()? as u64;
406                }
407                conn.execute_batch("COMMIT")?;
408                Ok(total)
409            })();
410            if result.is_err() {
411                let _ = conn.execute_batch("ROLLBACK");
412            }
413            // `_tx_handle` drops here, after ROLLBACK (or COMMIT) has already run.
414            (conn, result)
415        })
416        .await
417        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_batch", e))?;
418        self.conn = Some(conn);
419        result.map_err(|e| map_rusqlite_err(e, "execute_batch"))
420    }
421
422    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
423        // ADR-067 Component A (Fork C slice 2): the script text is
424        // self-contained (supplied up front, runs as one unit), just like
425        // `execute_batch` — route it through the writer task when
426        // available. `self.conn` is left untouched so a subsequent
427        // `execute`/`execute_script` call on this same handle still works
428        // over the standalone connection. Callers must supply a DML-only
429        // script (no bare `BEGIN`/`COMMIT`/`ROLLBACK`) on the flag-on path,
430        // since it runs inside the writer task's own transaction — same
431        // contract as `execute_batch`'s statement list.
432        if let Some(writer_task) = self.writer_task.clone() {
433            return writer_task
434                .send(move |conn| {
435                    conn.execute_batch(&script)
436                        .map_err(|e| map_rusqlite_err(e, "execute_script"))
437                })
438                .await;
439        }
440
441        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
442            operation: "execute_script".into(),
443            message: "connection already consumed".into(),
444        })?;
445        let (conn, result) = tokio::task::spawn_blocking(move || {
446            let res = conn.execute_batch(&script);
447            (conn, res)
448        })
449        .await
450        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_script", e))?;
451        self.conn = Some(conn);
452        result.map_err(|e| map_rusqlite_err(e, "execute_script"))
453    }
454
455    async fn execute_script_top_level(
456        &mut self,
457        script: String,
458    ) -> khive_storage::types::StorageResult<()> {
459        // ADR-067 Component A: unlike
460        // `execute_script`, this must NOT run inside the writer task's
461        // per-request `BEGIN IMMEDIATE` — statements such as VACUUM are
462        // rejected by SQLite inside any open transaction. Route through
463        // `WriterTaskHandle::send_top_level`, which still serializes this
464        // call through the single writer owner but skips the transaction
465        // wrap entirely.
466        if let Some(writer_task) = self.writer_task.clone() {
467            return writer_task
468                .send_top_level(move |conn| {
469                    conn.execute_batch(&script)
470                        .map_err(|e| map_rusqlite_err(e, "execute_script_top_level"))
471                })
472                .await;
473        }
474
475        // Flag off / no writer task: identical to `execute_script`'s own
476        // flag-off path — a bare `execute_batch` on the standalone
477        // connection, already transaction-free.
478        let conn = self.conn.take().ok_or_else(|| StorageError::Pool {
479            operation: "execute_script_top_level".into(),
480            message: "connection already consumed".into(),
481        })?;
482        let (conn, result) = tokio::task::spawn_blocking(move || {
483            let res = conn.execute_batch(&script);
484            (conn, res)
485        })
486        .await
487        .map_err(|e| StorageError::driver(StorageCapability::Sql, "execute_script_top_level", e))?;
488        self.conn = Some(conn);
489        result.map_err(|e| map_rusqlite_err(e, "execute_script_top_level"))
490    }
491}
492
493// =============================================================================
494// Pool-backed reader/writer (in-memory databases)
495// =============================================================================
496
497struct PoolBackedReader {
498    pool: Arc<ConnectionPool>,
499}
500
501#[async_trait]
502impl khive_storage::SqlReader for PoolBackedReader {
503    async fn query_row(
504        &mut self,
505        statement: SqlStatement,
506    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
507        let pool = Arc::clone(&self.pool);
508        tokio::task::spawn_blocking(move || {
509            let guard = pool
510                .reader()
511                .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_reader", e))?;
512            let rows = execute_query(&guard, &statement)
513                .map_err(|e| map_rusqlite_err(e, "pool_reader.query_row"))?;
514            Ok(rows.into_iter().next())
515        })
516        .await
517        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_reader.query_row", e))?
518    }
519
520    async fn query_all(
521        &mut self,
522        statement: SqlStatement,
523    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
524        let pool = Arc::clone(&self.pool);
525        tokio::task::spawn_blocking(move || {
526            let guard = pool
527                .reader()
528                .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_reader", e))?;
529            execute_query(&guard, &statement)
530                .map_err(|e| map_rusqlite_err(e, "pool_reader.query_all"))
531        })
532        .await
533        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_reader.query_all", e))?
534    }
535
536    async fn query_scalar(
537        &mut self,
538        statement: SqlStatement,
539    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
540        let row = self.query_row(statement).await?;
541        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
542    }
543
544    async fn explain(
545        &mut self,
546        statement: SqlStatement,
547    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
548        let explain_stmt = SqlStatement {
549            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
550            params: statement.params,
551            label: statement.label,
552        };
553        self.query_all(explain_stmt).await
554    }
555}
556
557struct PoolBackedWriter {
558    pool: Arc<ConnectionPool>,
559}
560
561#[async_trait]
562impl khive_storage::SqlReader for PoolBackedWriter {
563    async fn query_row(
564        &mut self,
565        statement: SqlStatement,
566    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
567        let pool = Arc::clone(&self.pool);
568        tokio::task::spawn_blocking(move || {
569            let guard = pool.try_writer().map_err(|e: SqliteError| {
570                StorageError::driver(StorageCapability::Sql, "pool_writer.query_row", e)
571            })?;
572            let rows = execute_query(&guard, &statement)
573                .map_err(|e| map_rusqlite_err(e, "pool_writer.query_row"))?;
574            Ok(rows.into_iter().next())
575        })
576        .await
577        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.query_row", e))?
578    }
579
580    async fn query_all(
581        &mut self,
582        statement: SqlStatement,
583    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
584        let pool = Arc::clone(&self.pool);
585        tokio::task::spawn_blocking(move || {
586            let guard = pool.try_writer().map_err(|e: SqliteError| {
587                StorageError::driver(StorageCapability::Sql, "pool_writer.query_all", e)
588            })?;
589            execute_query(&guard, &statement)
590                .map_err(|e| map_rusqlite_err(e, "pool_writer.query_all"))
591        })
592        .await
593        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.query_all", e))?
594    }
595
596    async fn query_scalar(
597        &mut self,
598        statement: SqlStatement,
599    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
600        let row = khive_storage::SqlReader::query_row(self, statement).await?;
601        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
602    }
603
604    async fn explain(
605        &mut self,
606        statement: SqlStatement,
607    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
608        let explain_stmt = SqlStatement {
609            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
610            params: statement.params,
611            label: statement.label,
612        };
613        khive_storage::SqlReader::query_all(self, explain_stmt).await
614    }
615}
616
617#[async_trait]
618impl khive_storage::SqlWriter for PoolBackedWriter {
619    async fn execute(
620        &mut self,
621        statement: SqlStatement,
622    ) -> khive_storage::types::StorageResult<u64> {
623        let pool = Arc::clone(&self.pool);
624        tokio::task::spawn_blocking(move || {
625            let guard = pool.try_writer().map_err(|e: SqliteError| {
626                StorageError::driver(StorageCapability::Sql, "pool_writer.execute", e)
627            })?;
628            let mut stmt = guard
629                .prepare(&statement.sql)
630                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
631            bind_params(&mut stmt, &statement.params)
632                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
633            let rows = stmt
634                .raw_execute()
635                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute"))?;
636            Ok(rows as u64)
637        })
638        .await
639        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.execute", e))?
640    }
641
642    async fn execute_batch(
643        &mut self,
644        statements: Vec<SqlStatement>,
645    ) -> khive_storage::types::StorageResult<u64> {
646        let pool = Arc::clone(&self.pool);
647        tokio::task::spawn_blocking(move || {
648            let guard = pool.try_writer().map_err(|e: SqliteError| {
649                StorageError::driver(StorageCapability::Sql, "pool_writer.execute_batch", e)
650            })?;
651            guard
652                .execute_batch("BEGIN IMMEDIATE")
653                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?;
654            let _tx_handle = khive_storage::tx_registry::register_scoped(
655                Some("pool_writer.execute_batch".to_string()),
656                pool.origin(),
657            );
658            let result = (|| -> Result<u64, StorageError> {
659                let mut total = 0u64;
660                for statement in &statements {
661                    let mut stmt = guard
662                        .prepare(&statement.sql)
663                        .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?;
664                    bind_params(&mut stmt, &statement.params)
665                        .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?;
666                    total += stmt
667                        .raw_execute()
668                        .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_batch"))?
669                        as u64;
670                }
671                Ok(total)
672            })();
673            match result {
674                Ok(total) => {
675                    if let Err(e) = guard.execute_batch("COMMIT") {
676                        let _ = guard.execute_batch("ROLLBACK");
677                        Err(map_rusqlite_err(e, "pool_writer.execute_batch"))
678                    } else {
679                        Ok(total)
680                    }
681                }
682                Err(e) => {
683                    let _ = guard.execute_batch("ROLLBACK");
684                    Err(e)
685                }
686            }
687        })
688        .await
689        .map_err(|e| StorageError::driver(StorageCapability::Sql, "pool_writer.execute_batch", e))?
690    }
691
692    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
693        let pool = Arc::clone(&self.pool);
694        tokio::task::spawn_blocking(move || {
695            let guard = pool.try_writer().map_err(|e: SqliteError| {
696                StorageError::driver(StorageCapability::Sql, "pool_writer.execute_script", e)
697            })?;
698            guard
699                .execute_batch(&script)
700                .map_err(|e| map_rusqlite_err(e, "pool_writer.execute_script"))
701        })
702        .await
703        .map_err(|e| {
704            StorageError::driver(StorageCapability::Sql, "pool_writer.execute_script", e)
705        })?
706    }
707}
708
709// =============================================================================
710// atomic_unit (ADR-067 Component A, Fork C slice 2)
711// =============================================================================
712
713/// A purely-synchronous `SqlReader`/`SqlWriter` over a borrowed connection,
714/// used ONLY to drive an [`AtomicUnitOp`] on the flag-on path, where the
715/// closure body runs inside the writer task's `spawn_blocking` (synchronous
716/// `FnOnce(&rusqlite::Connection) -> ...`) rather than a real async context.
717///
718/// Every method here does plain, non-suspending rusqlite work — there is no
719/// real `.await` point anywhere in this impl — so [`block_on_sync`] driving
720/// the resulting future to completion with a single poll is sound, not a
721/// hack: the future can never actually be `Pending`.
722///
723/// `SqlReader`/`SqlWriter` both carry a `'static` supertrait bound (they are
724/// used as `Box<dyn ...>` elsewhere in this module), so this type cannot
725/// hold a real `&'c Connection` borrow — it would tie `InlineWriter` to a
726/// non-`'static` lifetime and, independently, `&Connection` is not `Send`
727/// (`Connection` is `!Sync`), which the `#[async_trait]`-generated futures
728/// require. A raw pointer sidesteps both: `*const Connection` is `Send` and
729/// `'static` on its face, and the safety burden (the pointee outliving
730/// every dereference) is upheld by construction — see `atomic_unit`, the
731/// only call site: it builds an `InlineWriter` from `conn: &Connection`,
732/// drives `op` to completion via `block_on_sync` synchronously, and drops
733/// the `InlineWriter` before that borrow ends, all within one stack frame.
734struct InlineWriter {
735    conn: *const rusqlite::Connection,
736}
737
738// SAFETY: `InlineWriter` is never actually shared across a real thread
739// boundary — it is constructed, driven to completion synchronously via
740// `block_on_sync`, and dropped within a single call frame inside the
741// writer task's `spawn_blocking` closure (see `atomic_unit`). The `Send`
742// bound `async_trait` imposes on the futures below is a static
743// over-approximation for this restricted, single-threaded usage pattern.
744unsafe impl Send for InlineWriter {}
745
746impl InlineWriter {
747    /// SAFETY: valid for the lifetime of the enclosing synchronous scope in
748    /// `atomic_unit` (see the struct doc comment above) — the pointee is
749    /// never dereferenced after that scope ends.
750    fn conn(&self) -> &rusqlite::Connection {
751        unsafe { &*self.conn }
752    }
753}
754
755#[async_trait]
756impl khive_storage::SqlReader for InlineWriter {
757    async fn query_row(
758        &mut self,
759        statement: SqlStatement,
760    ) -> khive_storage::types::StorageResult<Option<SqlRow>> {
761        let rows = execute_query(self.conn(), &statement)
762            .map_err(|e| map_rusqlite_err(e, "inline.query_row"))?;
763        Ok(rows.into_iter().next())
764    }
765
766    async fn query_all(
767        &mut self,
768        statement: SqlStatement,
769    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
770        execute_query(self.conn(), &statement).map_err(|e| map_rusqlite_err(e, "inline.query_all"))
771    }
772
773    async fn query_scalar(
774        &mut self,
775        statement: SqlStatement,
776    ) -> khive_storage::types::StorageResult<Option<SqlValue>> {
777        let row = khive_storage::SqlReader::query_row(self, statement).await?;
778        Ok(row.and_then(|r| r.columns.into_iter().next().map(|c| c.value)))
779    }
780
781    async fn explain(
782        &mut self,
783        statement: SqlStatement,
784    ) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
785        let explain_stmt = SqlStatement {
786            sql: format!("EXPLAIN QUERY PLAN {}", statement.sql),
787            params: statement.params,
788            label: statement.label,
789        };
790        khive_storage::SqlReader::query_all(self, explain_stmt).await
791    }
792}
793
794#[async_trait]
795impl khive_storage::SqlWriter for InlineWriter {
796    async fn execute(
797        &mut self,
798        statement: SqlStatement,
799    ) -> khive_storage::types::StorageResult<u64> {
800        let mut stmt = self
801            .conn()
802            .prepare(&statement.sql)
803            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
804        bind_params(&mut stmt, &statement.params)
805            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
806        let affected = stmt
807            .raw_execute()
808            .map_err(|e| map_rusqlite_err(e, "inline.execute"))?;
809        Ok(affected as u64)
810    }
811
812    async fn execute_batch(
813        &mut self,
814        statements: Vec<SqlStatement>,
815    ) -> khive_storage::types::StorageResult<u64> {
816        let mut total: u64 = 0;
817        for statement in &statements {
818            let mut stmt = self
819                .conn()
820                .prepare(&statement.sql)
821                .map_err(|e| map_rusqlite_err(e, "inline.execute_batch"))?;
822            bind_params(&mut stmt, &statement.params)
823                .map_err(|e| map_rusqlite_err(e, "inline.execute_batch"))?;
824            total += stmt
825                .raw_execute()
826                .map_err(|e| map_rusqlite_err(e, "inline.execute_batch"))?
827                as u64;
828        }
829        Ok(total)
830    }
831
832    async fn execute_script(&mut self, script: String) -> khive_storage::types::StorageResult<()> {
833        self.conn()
834            .execute_batch(&script)
835            .map_err(|e| map_rusqlite_err(e, "inline.execute_script"))
836    }
837}
838
839/// Poll `fut` exactly once with a no-op waker and return its output.
840///
841/// Only sound for futures that never actually suspend — every caller in
842/// this module drives an [`InlineWriter`], whose methods are pure
843/// synchronous rusqlite calls with no real `.await` point.
844///
845/// ADR-067 Component A: this used to
846/// `unreachable!()`-panic on `Poll::Pending`, and a panicking closure
847/// running inside the writer task's `spawn_blocking` (see
848/// `SqlBridge::atomic_unit`'s flag-on branch) would surface as a
849/// `JoinError` in `run_writer_task`, which is treated as fatal — the writer
850/// task exits and every subsequent `WriterTaskHandle::send` on this pool
851/// fails for the rest of the process. A future `atomic_unit` caller whose
852/// closure ever gains a real suspend point (this file's own contract
853/// already forbids it, but the invariant is enforced by convention, not the
854/// type system) would take down the writer task for the whole daemon.
855/// Returning `Err` instead lets `Pending` flow through the SAME error path
856/// as any other `atomic_unit` op failure: `WriteRequest::execute_and_reply`
857/// treats it as an ordinary `Err`, issues `ROLLBACK` on the writer task's
858/// held transaction, replies the error to the caller, and the writer task's
859/// `spawn_blocking` closure returns normally (not via panic) — so the task
860/// keeps draining subsequent requests instead of dying with the whole pool.
861fn block_on_sync<F: std::future::Future>(fut: F) -> Result<F::Output, StorageError> {
862    use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
863
864    fn no_op(_: *const ()) {}
865    fn clone_waker(_: *const ()) -> RawWaker {
866        RawWaker::new(std::ptr::null(), &VTABLE)
867    }
868    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_waker, no_op, no_op, no_op);
869
870    // SAFETY: every `RawWakerVTable` function is a no-op that never
871    // dereferences the data pointer, so a null data pointer is sound.
872    let raw_waker = RawWaker::new(std::ptr::null(), &VTABLE);
873    let waker = unsafe { Waker::from_raw(raw_waker) };
874    let mut cx = Context::from_waker(&waker);
875
876    let mut fut = std::pin::pin!(fut);
877    match fut.as_mut().poll(&mut cx) {
878        Poll::Ready(v) => Ok(v),
879        Poll::Pending => {
880            tracing::error!(
881                "block_on_sync: atomic_unit future suspended on its first poll — \
882                 the closure passed to SqlAccess::atomic_unit must be non-blocking \
883                 (synchronous InlineWriter calls only, no real .await point)"
884            );
885            Err(StorageError::Internal(
886                "atomic_unit future suspended — closure must be non-blocking".to_string(),
887            ))
888        }
889    }
890}
891
892/// Run `op` under a manual `BEGIN IMMEDIATE`/`COMMIT`/`ROLLBACK` on `writer`
893/// — the pre-ADR-067 shape, used by [`SqlBridge::atomic_unit`] whenever no
894/// writer task applies (flag off, no runtime, or an in-memory pool),
895/// preserving that path byte-for-byte.
896async fn run_manual_atomic_unit(
897    writer: &mut dyn khive_storage::SqlWriter,
898    op: AtomicUnitOp,
899    origin: khive_storage::tx_registry::TxOrigin,
900) -> khive_storage::types::StorageResult<Box<dyn Any + Send>> {
901    fn tx_stmt(sql: &str, label: &str) -> SqlStatement {
902        SqlStatement {
903            sql: sql.to_string(),
904            params: vec![],
905            label: Some(label.to_string()),
906        }
907    }
908    khive_storage::SqlWriter::execute(writer, tx_stmt("BEGIN IMMEDIATE", "begin")).await?;
909    let _tx_handle =
910        khive_storage::tx_registry::register_scoped(Some("atomic_unit".to_string()), origin);
911
912    let result = op(writer).await;
913
914    match result {
915        Ok(value) => {
916            match khive_storage::SqlWriter::execute(writer, tx_stmt("COMMIT", "commit")).await {
917                Ok(_) => Ok(value),
918                Err(e) => {
919                    let _ =
920                        khive_storage::SqlWriter::execute(writer, tx_stmt("ROLLBACK", "rollback"))
921                            .await;
922                    Err(e)
923                }
924            }
925        }
926        Err(e) => {
927            let _ =
928                khive_storage::SqlWriter::execute(writer, tx_stmt("ROLLBACK", "rollback")).await;
929            Err(e)
930        }
931    }
932}
933
934// =============================================================================
935// SqlBridge: the SqlAccess implementor
936// =============================================================================
937
938/// Bridges `ConnectionPool` to `khive_storage::SqlAccess`.
939///
940/// Dispatches based on whether the pool is file-backed or in-memory:
941/// - File-backed: standalone connections per reader/writer call (high
942///   concurrency); atomic units drive a single registered raw transaction
943///   span instead of a caller-held per-tx connection.
944/// - In-memory: pool-backed connections per query (single shared connection).
945pub struct SqlBridge {
946    pool: Arc<ConnectionPool>,
947    is_file_backed: bool,
948}
949
950impl SqlBridge {
951    /// Create a new bridge wrapping the given pool.
952    pub fn new(pool: Arc<ConnectionPool>, is_file_backed: bool) -> Self {
953        Self {
954            pool,
955            is_file_backed,
956        }
957    }
958}
959
960#[async_trait]
961impl khive_storage::SqlAccess for SqlBridge {
962    async fn reader(
963        &self,
964    ) -> khive_storage::types::StorageResult<Box<dyn khive_storage::SqlReader>> {
965        if self.is_file_backed {
966            let conn = open_standalone_reader(&self.pool)?;
967            Ok(Box::new(SqliteReader { conn: Some(conn) }))
968        } else {
969            Ok(Box::new(PoolBackedReader {
970                pool: Arc::clone(&self.pool),
971            }))
972        }
973    }
974
975    async fn writer(
976        &self,
977    ) -> khive_storage::types::StorageResult<Box<dyn khive_storage::SqlWriter>> {
978        if self.is_file_backed {
979            if self.pool.config().read_only {
980                return Err(StorageError::Pool {
981                    operation: "writer".into(),
982                    message: "backend is read-only".into(),
983                });
984            }
985            let conn = open_standalone_writer(&self.pool)?;
986            // Best-effort: a lookup failure (no runtime context) degrades to the
987            // standalone-connection path in `execute_batch` rather than failing
988            // handle construction (mirrors every other migrated store).
989            let writer_task = self.pool.writer_task_handle().ok().flatten();
990            Ok(Box::new(SqliteWriter {
991                conn: Some(conn),
992                writer_task,
993                origin: self.pool.origin(),
994            }))
995        } else {
996            Ok(Box::new(PoolBackedWriter {
997                pool: Arc::clone(&self.pool),
998            }))
999        }
1000    }
1001
1002    /// Implements the trait's atomic-unit suspend-free invariant
1003    /// (`SqlAccess::atomic_unit`'s doc comment): on the flag-on branch below,
1004    /// `op` is driven through `block_on_sync` on an `InlineWriter` — a
1005    /// single-poll driver that returns `Err` the instant `op`'s future is
1006    /// `Pending` instead of ever actually suspending. `op` must therefore
1007    /// issue only synchronous DML; see `InlineWriter`'s and
1008    /// `block_on_sync`'s doc comments for the full mechanics and why this
1009    /// restriction is load-bearing (a suspended poll inside the writer
1010    /// task's `spawn_blocking` would otherwise block that task on external
1011    /// async work while holding the single write connection).
1012    async fn atomic_unit(
1013        &self,
1014        op: AtomicUnitOp,
1015    ) -> khive_storage::types::StorageResult<Box<dyn Any + Send>> {
1016        if self.is_file_backed {
1017            if self.pool.config().read_only {
1018                return Err(StorageError::Pool {
1019                    operation: "atomic_unit".into(),
1020                    message: "backend is read-only".into(),
1021                });
1022            }
1023            // Best-effort, same guard `writer()` uses: `Ok(None)` on flag-off;
1024            // `Err(WriterTaskNoRuntime)` propagates loud rather than silently
1025            // falling back to a competing connection from a sync caller.
1026            if let Some(writer_task) = self.pool.writer_task_handle()? {
1027                // Flag-on: ONE queued WriteRequest. `run_writer_task` already
1028                // has an open `BEGIN IMMEDIATE` on its dedicated connection
1029                // before this closure runs and issues `COMMIT`/`ROLLBACK`
1030                // after it returns — `op` must not (and, via `InlineWriter`,
1031                // does not) issue its own transaction control.
1032                return writer_task
1033                    .send(move |conn| {
1034                        let mut inline = InlineWriter {
1035                            conn: conn as *const rusqlite::Connection,
1036                        };
1037                        // Flatten: `block_on_sync` now returns `Result<F::Output,
1038                        // StorageError>` (outer = "did the future actually
1039                        // resolve on first poll", inner = the op's own
1040                        // `StorageResult`) instead of panicking on `Pending`
1041                        // (ADR-067 Component A). Either
1042                        // error flows through this closure's ordinary `Err`
1043                        // return, which `WriteRequest::execute_and_reply`
1044                        // already turns into a normal ROLLBACK + error reply —
1045                        // no panic, so the writer task survives.
1046                        match block_on_sync(op(&mut inline)) {
1047                            Ok(inner) => inner,
1048                            Err(e) => Err(e),
1049                        }
1050                    })
1051                    .await;
1052            }
1053            // Flag-off (or no writer task available): manual
1054            // BEGIN IMMEDIATE/COMMIT/ROLLBACK on a standalone writer —
1055            // byte-for-byte the pre-ADR-067 shape.
1056            let conn = open_standalone_writer(&self.pool)?;
1057            let mut writer = SqliteWriter {
1058                conn: Some(conn),
1059                writer_task: None,
1060                origin: self.pool.origin(),
1061            };
1062            run_manual_atomic_unit(&mut writer, op, self.pool.origin()).await
1063        } else {
1064            // In-memory pools are exempt (not accept-loop reachable, per the
1065            // rework spec's "Out of scope") — preserve the existing
1066            // pool-backed manual-transaction behavior.
1067            let mut writer = PoolBackedWriter {
1068                pool: Arc::clone(&self.pool),
1069            };
1070            run_manual_atomic_unit(&mut writer, op, self.pool.origin()).await
1071        }
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078    use crate::pool::PoolConfig;
1079    use khive_storage::types::{SqlStatement, SqlValue};
1080    use khive_storage::SqlAccess as _;
1081
1082    /// ADR-067 Component A entry 10: with `KHIVE_WRITE_QUEUE=1`,
1083    /// `SqliteWriter::execute_batch` (reached via `SqlBridge::writer()`)
1084    /// routes the whole statement list through the WriterTask channel
1085    /// instead of opening its own `BEGIN IMMEDIATE` on the standalone
1086    /// connection, and the row is actually committed and readable back.
1087    #[tokio::test]
1088    async fn execute_batch_routes_through_writer_task_when_flag_enabled() {
1089        let dir = tempfile::tempdir().unwrap();
1090        let path = dir.path().join("write_queue_execute_batch.db");
1091        let config = PoolConfig {
1092            path: Some(path.clone()),
1093            write_queue_enabled: true,
1094            ..PoolConfig::default()
1095        };
1096        let pool = Arc::new(ConnectionPool::new(config).unwrap());
1097        {
1098            let guard = pool.writer().unwrap();
1099            guard
1100                .conn()
1101                .execute_batch(
1102                    "CREATE TABLE IF NOT EXISTS write_queue_batch_test \
1103                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
1104                )
1105                .unwrap();
1106        }
1107
1108        let bridge = SqlBridge::new(Arc::clone(&pool), true);
1109
1110        let mut writer = bridge.writer().await.unwrap();
1111        let affected = writer
1112            .execute_batch(vec![
1113                SqlStatement {
1114                    sql: "INSERT INTO write_queue_batch_test (id, val) VALUES (?1, ?2)".into(),
1115                    params: vec![SqlValue::Integer(1), SqlValue::Text("a".into())],
1116                    label: None,
1117                },
1118                SqlStatement {
1119                    sql: "INSERT INTO write_queue_batch_test (id, val) VALUES (?1, ?2)".into(),
1120                    params: vec![SqlValue::Integer(2), SqlValue::Text("b".into())],
1121                    label: None,
1122                },
1123            ])
1124            .await
1125            .unwrap();
1126        assert_eq!(affected, 2);
1127
1128        let mut reader = bridge.reader().await.unwrap();
1129        let count = reader
1130            .query_scalar(SqlStatement {
1131                sql: "SELECT COUNT(*) FROM write_queue_batch_test".into(),
1132                params: vec![],
1133                label: None,
1134            })
1135            .await
1136            .unwrap();
1137        assert!(
1138            matches!(count, Some(SqlValue::Integer(2))),
1139            "expected 2 rows, got {count:?}"
1140        );
1141        assert_eq!(
1142            pool.writer_task_spawn_count(),
1143            1,
1144            "the flag-ON path must actually spawn and use the writer task"
1145        );
1146    }
1147
1148    /// ADR-067 Component A entry 10, atomicity: a batch whose second
1149    /// statement fails (duplicate primary key) must roll back the WHOLE
1150    /// request — including the first statement's otherwise-successful
1151    /// INSERT — because the WriterTask commits or rolls back one
1152    /// `WriteRequest` as a single unit (ADR-067 Component A). Zero rows must
1153    /// land, not one.
1154    #[tokio::test]
1155    async fn execute_batch_rolls_back_atomically_on_mid_sequence_failure() {
1156        let dir = tempfile::tempdir().unwrap();
1157        let path = dir.path().join("write_queue_execute_batch_rollback.db");
1158        let config = PoolConfig {
1159            path: Some(path.clone()),
1160            write_queue_enabled: true,
1161            ..PoolConfig::default()
1162        };
1163        let pool = Arc::new(ConnectionPool::new(config).unwrap());
1164        {
1165            let guard = pool.writer().unwrap();
1166            guard
1167                .conn()
1168                .execute_batch(
1169                    "CREATE TABLE IF NOT EXISTS write_queue_rollback_test \
1170                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
1171                )
1172                .unwrap();
1173        }
1174
1175        let bridge = SqlBridge::new(Arc::clone(&pool), true);
1176
1177        let mut writer = bridge.writer().await.unwrap();
1178        let result = writer
1179            .execute_batch(vec![
1180                // Statement 1: succeeds on its own.
1181                SqlStatement {
1182                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
1183                    params: vec![SqlValue::Integer(1), SqlValue::Text("first".into())],
1184                    label: None,
1185                },
1186                // Statement 2: duplicate primary key — fails mid-sequence.
1187                SqlStatement {
1188                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
1189                    params: vec![SqlValue::Integer(1), SqlValue::Text("duplicate".into())],
1190                    label: None,
1191                },
1192                // Statement 3: never reached.
1193                SqlStatement {
1194                    sql: "INSERT INTO write_queue_rollback_test (id, val) VALUES (?1, ?2)".into(),
1195                    params: vec![SqlValue::Integer(2), SqlValue::Text("third".into())],
1196                    label: None,
1197                },
1198            ])
1199            .await;
1200        assert!(
1201            result.is_err(),
1202            "a batch with a mid-sequence PK conflict must return an error"
1203        );
1204
1205        let mut reader = bridge.reader().await.unwrap();
1206        let count = reader
1207            .query_scalar(SqlStatement {
1208                sql: "SELECT COUNT(*) FROM write_queue_rollback_test".into(),
1209                params: vec![],
1210                label: None,
1211            })
1212            .await
1213            .unwrap();
1214        assert!(
1215            matches!(count, Some(SqlValue::Integer(0))),
1216            "the whole request must roll back — including statement 1's \
1217             otherwise-successful INSERT — not just the failing statement; \
1218             got {count:?}"
1219        );
1220    }
1221
1222    /// ADR-067 Component A: before
1223    /// this fix, `block_on_sync` (this file) `unreachable!()`-panicked if
1224    /// an `atomic_unit` closure's future was `Pending` on its first poll.
1225    /// That panic ran inside the writer task's own `spawn_blocking` frame
1226    /// (see `atomic_unit`'s flag-on branch), and `run_writer_task` treats
1227    /// any `spawn_blocking` `JoinError` as fatal — the whole writer task
1228    /// exits, taking down every subsequent write for this pool. Proves the
1229    /// fix: an `atomic_unit` op built to suspend on first poll (via
1230    /// `std::future::pending`, never actually resolving) now returns a
1231    /// clean `Err` from `atomic_unit` — no panic — AND the writer task
1232    /// survives to serve a completely unrelated, well-behaved `atomic_unit`
1233    /// call immediately afterward.
1234    ///
1235    /// Not `#[serial]` / no env var: builds the pool directly with
1236    /// `write_queue_enabled: true` in the `PoolConfig` literal, same
1237    /// technique as this round's other new routing tests.
1238    #[tokio::test]
1239    async fn atomic_unit_pending_future_errors_without_killing_writer_task() {
1240        let dir = tempfile::tempdir().unwrap();
1241        let path = dir.path().join("atomic_unit_pending_future.db");
1242        let config = PoolConfig {
1243            path: Some(path.clone()),
1244            write_queue_enabled: true,
1245            ..PoolConfig::default()
1246        };
1247        let pool = Arc::new(ConnectionPool::new(config).unwrap());
1248        {
1249            let guard = pool.writer().unwrap();
1250            guard
1251                .conn()
1252                .execute_batch(
1253                    "CREATE TABLE IF NOT EXISTS atomic_unit_pending_test \
1254                     (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
1255                )
1256                .unwrap();
1257        }
1258        assert!(
1259            pool.writer_task_handle().unwrap().is_some(),
1260            "writer task must be spawned with the flag on for a file-backed pool"
1261        );
1262
1263        let bridge = SqlBridge::new(Arc::clone(&pool), true);
1264
1265        // A closure whose future never resolves on first poll — the exact
1266        // misuse `block_on_sync` must reject instead of panicking on.
1267        let pending_op: AtomicUnitOp = Box::new(|_writer| {
1268            Box::pin(std::future::pending::<
1269                khive_storage::types::StorageResult<Box<dyn std::any::Any + Send>>,
1270            >())
1271        });
1272
1273        let pending_result = bridge.atomic_unit(pending_op).await;
1274        assert!(
1275            pending_result.is_err(),
1276            "a Pending-on-first-poll atomic_unit closure must return Err, \
1277             not panic; got {pending_result:?}"
1278        );
1279
1280        // If the panic had instead killed the writer task, every subsequent
1281        // write on this pool (including a completely unrelated, correctly
1282        // non-blocking atomic_unit call) would now fail with a channel-closed
1283        // error. Prove the task is still alive and serving requests.
1284        let ok_op: AtomicUnitOp = Box::new(|writer| {
1285            Box::pin(async move {
1286                writer
1287                    .execute(SqlStatement {
1288                        sql: "INSERT INTO atomic_unit_pending_test (id, val) VALUES (?1, ?2)"
1289                            .into(),
1290                        params: vec![SqlValue::Integer(1), SqlValue::Text("survived".into())],
1291                        label: None,
1292                    })
1293                    .await
1294                    .map_err(|e| {
1295                        khive_storage::StorageError::driver(
1296                            StorageCapability::Sql,
1297                            "atomic_unit_pending_future_test_insert",
1298                            e,
1299                        )
1300                    })?;
1301                Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
1302            })
1303        });
1304        let ok_result = bridge.atomic_unit(ok_op).await;
1305        assert!(
1306            ok_result.is_ok(),
1307            "writer task must survive a Pending misuse and keep serving \
1308             subsequent well-behaved atomic_unit requests; got {ok_result:?}"
1309        );
1310
1311        let mut reader = bridge.reader().await.unwrap();
1312        let count = reader
1313            .query_scalar(SqlStatement {
1314                sql: "SELECT COUNT(*) FROM atomic_unit_pending_test".into(),
1315                params: vec![],
1316                label: None,
1317            })
1318            .await
1319            .unwrap();
1320        assert!(
1321            matches!(count, Some(SqlValue::Integer(1))),
1322            "the well-behaved atomic_unit call after the Pending misuse must \
1323             have actually committed its write; got {count:?}"
1324        );
1325    }
1326}