1use 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
26fn 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
60pub(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; 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
92fn 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
106fn 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
116enum PreparedBatchStatement<'conn> {
122 Ready(rusqlite::Statement<'conn>),
123 PrepareAtExecution,
124}
125
126fn 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
143fn 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
164const TRANSACTION_CONTROL_KEYWORDS: [&str; 7] = [
175 "BEGIN",
176 "START",
177 "COMMIT",
178 "END",
179 "ROLLBACK",
180 "SAVEPOINT",
181 "RELEASE",
182];
183
184fn 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
230fn 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
278fn 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
299fn 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 BeginDeferred,
308 Finish(&'static str),
310 Unsupported(&'static str),
313}
314
315fn cached_read_transaction_control(sql: &str) -> Option<CachedReadTransactionControl> {
323 let (keyword, tail) = transaction_control_parts(sql)?;
324 match keyword {
325 "BEGIN" => {
326 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
380fn 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
403struct 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
416fn 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 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 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 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#[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 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 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
631fn 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
654fn statement_is_cancellable_read(stmt: &rusqlite::Statement<'_>, sql: &str) -> bool {
659 stmt.readonly() && transaction_control_head(sql).is_none()
660}
661
662const 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
677const 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
697fn 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("e) {
728 idx += 1; } 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
762fn 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("e) {
778 idx += 1; } 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
799fn 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 let next = skip_sqlite_identifier(rest)?;
816 rest = skip_sqlite_empty_prefix(next);
817 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 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 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
851pub(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 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 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
981fn admit_reader_capability_sql(
986 statement: &SqlStatement,
987 transaction_control: Option<CachedReadTransactionControl>,
988 operation: &'static str,
989) -> khive_storage::types::StorageResult<()> {
990 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
1121fn map_rusqlite_err(e: rusqlite::Error, op: &'static str) -> StorageError {
1123 StorageError::driver(StorageCapability::Sql, op, e)
1124}
1125
1126#[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
1163pub(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
1203fn 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
1228async 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
1248async 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
1264async 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
1273const CACHED_READ_TRANSACTION_LABEL: &str = "sql_bridge_cached_read_transaction";
1278
1279struct CachedReadTransaction {
1289 _slot: OwnedSemaphorePermit,
1290 _tx_handle: khive_storage::tx_registry::TxHandle,
1291 opened_at: Instant,
1297}
1298
1299struct StandaloneHandle {
1300 conn: rusqlite::Connection,
1301 _retained_slot: Option<OwnedSemaphorePermit>,
1306 read_transaction_slot: Option<CachedReadTransaction>,
1312}
1313
1314impl StandaloneHandle {
1315 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 handle: Option<StandaloneHandle>,
1331 pool: Arc<ConnectionPool>,
1332 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 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 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
1418async 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 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 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 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 restore_handle = false;
1602 }
1603
1604 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 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 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 drop(owned_handle);
1762 None
1763 };
1764 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
1945struct SqliteWriter {
1950 handle: Option<StandaloneHandle>,
1959 writer_task: Option<crate::writer_task::WriterTaskHandle>,
1966 origin: khive_storage::tx_registry::TxOrigin,
1969 db: String,
1973 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 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 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 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 let script = maintenance.as_sql();
2417 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 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
2458async 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 let mut guard = pool.resolve_reader_checkout(
2484 StorageCapability::Sql,
2485 operation,
2486 pool.reader_until(|| scope.should_stop()),
2487 )?;
2488 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 transaction: Option<SharedReaderTransactionGuard>,
2539}
2540
2541fn 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 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
2585async 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#[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 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 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 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
3005struct InlineWriter {
3031 conn: *const rusqlite::Connection,
3032}
3033
3034unsafe impl Send for InlineWriter {}
3041
3042impl InlineWriter {
3043 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 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 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 self.conn()
3135 .execute_batch(&script)
3136 .map_err(|e| map_rusqlite_err(e, "inline.execute_script"))
3137 }
3138}
3139
3140fn 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 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
3193async 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
3234pub struct SqlBridge {
3247 pool: Arc<ConnectionPool>,
3248 is_file_backed: bool,
3249}
3250
3251impl SqlBridge {
3252 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 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 crate::timeout_sink::emit_direct_route_violation(
3330 &db,
3331 crate::timeout_sink::Site::DirectRouteSqlBridgeWriter,
3332 );
3333 }
3334 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 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 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 return writer_task
3427 .send_bounded(move |conn| {
3428 let mut inline = InlineWriter {
3429 conn: conn as *const rusqlite::Connection,
3430 };
3431 match block_on_sync(op(&mut inline)) {
3441 Ok(inner) => inner,
3442 Err(e) => Err(e),
3443 }
3444 })
3445 .await;
3446 }
3447 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 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 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 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 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.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 = ¬ify_on_drop;
3863 if !callback_blocked_once.swap(true, std::sync::atomic::Ordering::SeqCst) {
3864 callback_entered.notify_one();
3865 callback_release.wait();
3866 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 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 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 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 #[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 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 #[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 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 #[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 #[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 "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 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 #[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 }
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 #[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 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 ("BEGIN TRANSACTION IMMEDIATE", "BEGIN"),
5641 ("BEGIN TRANSACTION EXCLUSIVE", "BEGIN"),
5642 ("BEGIN DEFERRED TRANSACTION trailing", "BEGIN"),
5643 ("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 #[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 #[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 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 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_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 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 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 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 #[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 #[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 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 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 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 #[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 ("/* /* 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 (
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 ("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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 #[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 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 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 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 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 #[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 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}