use crate::channel::mpsc;
use crate::cx::Cx;
use crate::database::transaction::trace_database_transaction;
use crate::obligation::graded::{ObligationToken, TransactionKind};
use crate::runtime::blocking_pool::{BlockingPool, BlockingPoolHandle};
use crate::time::{sleep, wall_now};
use crate::types::{CancelReason, Outcome};
use parking_lot::Mutex;
use std::collections::BTreeMap;
use std::fmt;
use std::future::poll_fn;
use std::marker::PhantomData;
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use std::task::Poll;
use std::time::Duration;
static SQLITE_POOL: OnceLock<BlockingPool> = OnceLock::new();
const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_millis(250);
const DEFAULT_STATEMENT_CACHE_CAPACITY: usize = 64;
const SQLITE_ROW_STREAM_CHANNEL_CAPACITY: usize = 1;
const SQLITE_ROW_STREAM_FULL_BACKOFF: Duration = Duration::from_millis(1);
fn sqlite_cancelled_reason(cx: &Cx) -> CancelReason {
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled"))
}
fn sqlite_error_is_interrupt(err: &SqliteError) -> bool {
match err {
SqliteError::Sqlite(msg) => {
let msg = msg.trim().to_ascii_lowercase();
msg == "interrupted" || msg == "error code 9: interrupted"
}
_ => false,
}
}
async fn sqlite_wait_retry_delay(cx: &Cx, delay: Duration) -> Result<(), CancelReason> {
if delay.is_zero() {
cx.checkpoint().map_err(|_| sqlite_cancelled_reason(cx))?;
crate::runtime::yield_now().await;
return cx.checkpoint().map_err(|_| sqlite_cancelled_reason(cx));
}
let now = cx
.timer_driver()
.map_or_else(wall_now, |driver| driver.now());
let mut sleeper = sleep(now, delay);
poll_fn(|task_cx| {
if cx.checkpoint().is_err() {
return Poll::Ready(Err(sqlite_cancelled_reason(cx)));
}
Pin::new(&mut sleeper).poll(task_cx).map(Ok)
})
.await
}
fn wal_checkpoint_i64(row: &SqliteRow, column: &str) -> Result<i64, SqliteError> {
row.get_i64(column).map_err(|err| {
SqliteError::WalCheckpointFailed(format!(
"WAL checkpoint status column {column:?} was missing or non-integer: {err}"
))
})
}
fn get_sqlite_pool() -> BlockingPoolHandle {
SQLITE_POOL.get_or_init(|| BlockingPool::new(1, 4)).handle()
}
fn configure_connection_defaults(
conn: &rusqlite::Connection,
enable_wal: bool,
) -> Result<(), SqliteError> {
configure_connection_defaults_with(conn, enable_wal, |_, error| {
SqliteError::Sqlite(error.to_string())
})
}
fn configure_connection_defaults_with<E, F>(
conn: &rusqlite::Connection,
enable_wal: bool,
mut map_error: F,
) -> Result<(), E>
where
F: FnMut(SqliteOperation, rusqlite::Error) -> E,
{
conn.busy_timeout(DEFAULT_BUSY_TIMEOUT)
.map_err(|error| map_error(SqliteOperation::Configure, error))?;
conn.pragma_update(None, "foreign_keys", "ON")
.map_err(|error| map_error(SqliteOperation::Configure, error))?;
if enable_wal {
conn.pragma_update(None, "journal_mode", "WAL")
.map_err(|error| map_error(SqliteOperation::Configure, error))?;
}
conn.set_prepared_statement_cache_capacity(DEFAULT_STATEMENT_CACHE_CAPACITY);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransactionState {
Autocommit,
InTransaction,
NeedsRollback,
RollingBack, }
#[derive(Debug, Default)]
struct BeginLifecycle {
abandoned: bool,
opened: bool,
generation: Option<u64>,
}
#[derive(Clone)]
struct BeginAttempt {
lifecycle: Arc<Mutex<BeginLifecycle>>,
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
}
impl BeginAttempt {
fn new(
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
) -> Self {
Self {
lifecycle: Arc::new(Mutex::new(BeginLifecycle::default())),
transaction_state,
transaction_generation,
}
}
fn abandon(&self) {
let mut lifecycle = self.lifecycle.lock();
lifecycle.abandoned = true;
let mut state = self.transaction_state.lock();
let owns_current_generation = lifecycle.generation.is_some_and(|generation| {
self.transaction_generation.load(Ordering::Acquire) == generation
});
if owns_current_generation || (!lifecycle.opened && *state == TransactionState::Autocommit)
{
*state = TransactionState::NeedsRollback;
}
}
fn finish_worker(
&self,
conn: &rusqlite::Connection,
result: Result<u64, SqliteError>,
) -> Result<u64, SqliteError> {
if result.is_ok() {
let mut lifecycle = self.lifecycle.lock();
lifecycle.opened = true;
let Some(generation) =
advance_transaction_generation(self.transaction_generation.as_ref())
else {
drop(lifecycle);
rollback_abandoned_begin_mutex_guarded(
conn,
self.transaction_state.as_ref(),
self.transaction_generation.as_ref(),
)?;
return Err(SqliteError::Sqlite(
"managed SQLite transaction generation exhausted".to_string(),
));
};
lifecycle.generation = Some(generation);
if lifecycle.abandoned {
drop(lifecycle);
rollback_abandoned_begin_mutex_guarded(
conn,
self.transaction_state.as_ref(),
self.transaction_generation.as_ref(),
)?;
} else {
*self.transaction_state.lock() = TransactionState::InTransaction;
}
}
result
}
fn finish_worker_diagnosed(
&self,
conn: &rusqlite::Connection,
operation: SqliteOperation,
result: Result<u64, SqliteOperationError>,
) -> Result<u64, SqliteOperationError> {
if result.is_ok() {
let mut lifecycle = self.lifecycle.lock();
lifecycle.opened = true;
let Some(generation) =
advance_transaction_generation(self.transaction_generation.as_ref())
else {
drop(lifecycle);
rollback_abandoned_begin_mutex_guarded(
conn,
self.transaction_state.as_ref(),
self.transaction_generation.as_ref(),
)
.map_err(|error| SqliteOperationError::from_legacy(operation, error))?;
return Err(SqliteOperationError::from_legacy(
operation,
SqliteError::Sqlite(
"managed SQLite transaction generation exhausted".to_string(),
),
));
};
lifecycle.generation = Some(generation);
if lifecycle.abandoned {
drop(lifecycle);
rollback_abandoned_begin_mutex_guarded(
conn,
self.transaction_state.as_ref(),
self.transaction_generation.as_ref(),
)
.map_err(|error| SqliteOperationError::from_legacy(operation, error))?;
} else {
*self.transaction_state.lock() = TransactionState::InTransaction;
}
}
result
}
}
enum TransactionWorkerEffect {
Begin(BeginAttempt),
Finish(TransactionFinishEffect),
}
impl TransactionWorkerEffect {
fn execute_worker(self, conn: &rusqlite::Connection, sql: &str) -> Result<u64, SqliteError> {
match self {
Self::Begin(attempt) => {
if attempt.transaction_generation.load(Ordering::Acquire) >= u64::MAX - 1 {
return Err(SqliteError::Sqlite(
"managed SQLite transaction generation exhausted".to_string(),
));
}
let result = conn
.execute(sql, [])
.map(|rows| rows as u64)
.map_err(|error| SqliteError::Sqlite(error.to_string()));
attempt.finish_worker(conn, result)
}
Self::Finish(mut effect) => effect.execute_worker(conn, sql),
}
}
fn execute_worker_diagnosed(
self,
conn: &rusqlite::Connection,
sql: &str,
operation: SqliteOperation,
) -> Result<u64, SqliteOperationError> {
match self {
Self::Begin(attempt) => {
if attempt.transaction_generation.load(Ordering::Acquire) >= u64::MAX - 1 {
return Err(SqliteOperationError::from_legacy(
operation,
SqliteError::Sqlite(
"managed SQLite transaction generation exhausted".to_string(),
),
));
}
let result = conn
.execute(sql, [])
.map(|rows| rows as u64)
.map_err(|error| SqliteOperationError::from_rusqlite(operation, error));
attempt.finish_worker_diagnosed(conn, operation, result)
}
Self::Finish(mut effect) => effect.execute_worker_diagnosed(conn, sql, operation),
}
}
}
#[derive(Clone, Copy)]
enum TransactionFinishKind {
Commit,
Rollback,
}
struct TransactionFinishEffect {
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
expected_generation: u64,
kind: TransactionFinishKind,
obligation: Option<ObligationToken<TransactionKind>>,
}
impl TransactionFinishEffect {
fn new(
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
expected_generation: u64,
kind: TransactionFinishKind,
obligation: Option<ObligationToken<TransactionKind>>,
) -> Self {
Self {
transaction_state,
transaction_generation,
expected_generation,
kind,
obligation,
}
}
fn execute_worker(
&mut self,
conn: &rusqlite::Connection,
sql: &str,
) -> Result<u64, SqliteError> {
if self.transaction_generation.load(Ordering::Acquire) != self.expected_generation {
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
return Err(SqliteError::TransactionFinished);
}
if conn.is_autocommit() {
let _ = advance_transaction_generation(self.transaction_generation.as_ref());
*self.transaction_state.lock() = TransactionState::Autocommit;
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
return Err(SqliteError::TransactionFinished);
}
let result = conn
.execute(sql, [])
.map(|rows| rows as u64)
.map_err(|error| SqliteError::Sqlite(error.to_string()));
if result.is_ok() {
let _ = advance_transaction_generation(self.transaction_generation.as_ref());
*self.transaction_state.lock() = TransactionState::Autocommit;
if let Some(token) = self.obligation.take() {
match self.kind {
TransactionFinishKind::Commit => {
let _ = token.commit();
}
TransactionFinishKind::Rollback => {
let _ = token.abort();
}
}
}
} else if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
result
}
fn execute_worker_diagnosed(
&mut self,
conn: &rusqlite::Connection,
sql: &str,
operation: SqliteOperation,
) -> Result<u64, SqliteOperationError> {
if self.transaction_generation.load(Ordering::Acquire) != self.expected_generation {
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
return Err(SqliteOperationError::from_legacy(
operation,
SqliteError::TransactionFinished,
));
}
if conn.is_autocommit() {
let _ = advance_transaction_generation(self.transaction_generation.as_ref());
*self.transaction_state.lock() = TransactionState::Autocommit;
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
return Err(SqliteOperationError::from_legacy(
operation,
SqliteError::TransactionFinished,
));
}
let result = conn
.execute(sql, [])
.map(|rows| rows as u64)
.map_err(|error| SqliteOperationError::from_rusqlite(operation, error));
if result.is_ok() {
let _ = advance_transaction_generation(self.transaction_generation.as_ref());
*self.transaction_state.lock() = TransactionState::Autocommit;
if let Some(token) = self.obligation.take() {
match self.kind {
TransactionFinishKind::Commit => {
let _ = token.commit();
}
TransactionFinishKind::Rollback => {
let _ = token.abort();
}
}
}
} else if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
result
}
}
impl Drop for TransactionFinishEffect {
fn drop(&mut self) {
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
}
}
struct BeginDropGuard {
attempt: BeginAttempt,
armed: bool,
}
impl BeginDropGuard {
fn new(
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
) -> Self {
Self {
attempt: BeginAttempt::new(transaction_state, transaction_generation),
armed: true,
}
}
fn attempt(&self) -> BeginAttempt {
self.attempt.clone()
}
fn disarm(&mut self) {
self.armed = false;
}
fn opened_generation(&self) -> Option<u64> {
self.attempt.lifecycle.lock().generation
}
fn abandon(&mut self) {
if std::mem::replace(&mut self.armed, false) {
self.attempt.abandon();
}
}
}
impl Drop for BeginDropGuard {
fn drop(&mut self) {
self.abandon();
}
}
fn rollback_abandoned_begin_mutex_guarded(
conn: &rusqlite::Connection,
transaction_state: &Mutex<TransactionState>,
transaction_generation: &AtomicU64,
) -> Result<(), SqliteError> {
let mut state = transaction_state.lock();
*state = TransactionState::RollingBack;
if conn.is_autocommit() {
let _ = advance_transaction_generation(transaction_generation);
*state = TransactionState::Autocommit;
return Ok(());
}
match conn.execute_batch("ROLLBACK") {
Ok(()) => {
let _ = advance_transaction_generation(transaction_generation);
*state = TransactionState::Autocommit;
Ok(())
}
Err(_) if conn.is_autocommit() => {
let _ = advance_transaction_generation(transaction_generation);
*state = TransactionState::Autocommit;
Ok(())
}
Err(error) => {
*state = TransactionState::NeedsRollback;
Err(SqliteError::Sqlite(error.to_string()))
}
}
}
#[allow(deprecated)]
fn advance_transaction_generation(transaction_generation: &AtomicU64) -> Option<u64> {
transaction_generation
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |generation| {
generation.checked_add(1)
})
.ok()
.and_then(|generation| generation.checked_add(1))
}
fn ensure_managed_transaction_open(
conn: &rusqlite::Connection,
transaction_state: &Mutex<TransactionState>,
transaction_generation: &AtomicU64,
expected_generation: Option<u64>,
) -> Result<(), SqliteError> {
let mut state = transaction_state.lock();
if expected_generation
.is_some_and(|expected| transaction_generation.load(Ordering::Acquire) != expected)
{
return Err(SqliteError::TransactionFinished);
}
if *state == TransactionState::InTransaction && conn.is_autocommit() {
let _ = advance_transaction_generation(transaction_generation);
*state = TransactionState::Autocommit;
return Err(SqliteError::TransactionFinished);
}
Ok(())
}
fn execute_legacy_statement(
conn: &rusqlite::Connection,
sql: &str,
params: &[SqliteValue],
) -> Result<u64, SqliteError> {
let params_refs: Vec<&dyn rusqlite::ToSql> = params
.iter()
.map(|value| value as &dyn rusqlite::ToSql)
.collect();
conn.execute(sql, params_refs.as_slice())
.map(|rows| rows as u64)
.map_err(|error| SqliteError::Sqlite(error.to_string()))
}
fn rollback_orphaned_transaction_generation_guarded(
conn: &rusqlite::Connection,
transaction_state: &Mutex<TransactionState>,
transaction_generation: &AtomicU64,
) -> Result<(), SqliteError> {
if *transaction_state.lock() != TransactionState::NeedsRollback {
return Ok(());
}
rollback_orphaned_transaction_mutex_guarded(conn, transaction_state)?;
let _ = advance_transaction_generation(transaction_generation);
*transaction_state.lock() = TransactionState::Autocommit;
Ok(())
}
fn rollback_orphaned_transaction_mutex_guarded(
conn: &rusqlite::Connection,
transaction_state: &Mutex<TransactionState>,
) -> Result<(), SqliteError> {
let mut state_guard = transaction_state.lock();
if *state_guard != TransactionState::NeedsRollback {
return Ok(());
}
*state_guard = TransactionState::RollingBack;
drop(state_guard);
let final_state = if conn.is_autocommit() {
TransactionState::Autocommit
} else {
match conn.execute_batch("ROLLBACK") {
Ok(()) => TransactionState::Autocommit,
Err(e) => {
if conn.is_autocommit() {
TransactionState::Autocommit
} else {
let mut state_guard = transaction_state.lock();
*state_guard = TransactionState::NeedsRollback;
return Err(SqliteError::Sqlite(e.to_string()));
}
}
}
};
let mut state_guard = transaction_state.lock();
*state_guard = final_state;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SqlSurfaceViolation {
Pragma,
TransactionControl,
AttachDetach,
Vacuum,
ExtensionLoading,
ParserRejected,
StatementCount,
ResourceLimit,
}
impl SqlSurfaceViolation {
fn checked_surface_message(self) -> &'static str {
match self {
Self::Pragma => "PRAGMA statements require the explicit *_unchecked SQLite APIs",
Self::TransactionControl => {
"transaction or connection control statements require the explicit *_unchecked SQLite APIs"
}
Self::AttachDetach => "ATTACH and DETACH are disabled on the checked SQLite APIs",
Self::Vacuum => {
"VACUUM requires the explicit *_unchecked SQLite APIs because VACUUM INTO can write an arbitrary filesystem path"
}
Self::ExtensionLoading => {
"SQLite extension loading is disabled on the checked SQLite APIs"
}
Self::ParserRejected => {
"checked SQLite SQL must be accepted by the bounded policy parser; audited engine-specific SQL requires an explicit *_unchecked API"
}
Self::StatementCount => "this checked SQLite API requires exactly one SQL statement",
Self::ResourceLimit => "SQLite SQL exceeds the checked-surface parser resource limits",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CheckedSqlCardinality {
ExactlyOne,
Batch,
}
const MAX_CHECKED_SQL_BYTES: usize = 1024 * 1024;
const MAX_CHECKED_SQL_RECURSION: usize = 128;
#[cfg(test)]
fn classify_sql_surface_violation(sql: &str) -> Option<SqlSurfaceViolation> {
match parse_checked_sql(sql) {
Ok(statements) => check_parsed_statements(&statements),
Err(violation) => Some(violation),
}
}
fn contains_extension_loading_call(sql: &str) -> Result<bool, SqlSurfaceViolation> {
use sqlparser::dialect::SQLiteDialect;
use sqlparser::tokenizer::{Token, Tokenizer};
let dialect = SQLiteDialect {};
let tokens = Tokenizer::new(&dialect, sql)
.tokenize()
.map_err(|_| SqlSurfaceViolation::ParserRejected)?;
let mut significant = tokens
.iter()
.filter(|token| !matches!(token, Token::Whitespace(_)))
.peekable();
while let Some(token) = significant.next() {
let Token::Word(word) = token else {
continue;
};
if word.value.eq_ignore_ascii_case("load_extension")
&& significant
.peek()
.is_some_and(|next| matches!(next, Token::LParen))
{
return Ok(true);
}
}
Ok(false)
}
fn unchecked_sql_contains_attach_detach(sql: &str) -> bool {
use sqlparser::ast::Statement;
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
let dialect = SQLiteDialect {};
match Parser::new(&dialect)
.with_recursion_limit(MAX_CHECKED_SQL_RECURSION)
.try_with_sql(sql)
.and_then(|mut parser| parser.parse_statements())
{
Ok(statements) => statements.iter().any(|statement| {
matches!(
statement,
Statement::AttachDatabase { .. }
| Statement::AttachDuckDBDatabase { .. }
| Statement::DetachDuckDBDatabase { .. }
)
}),
Err(_) => remove_sql_comments(sql).split(';').any(|statement| {
let statement = statement.trim().to_ascii_uppercase();
starts_with_sql_keyword(&statement, "ATTACH")
|| starts_with_sql_keyword(&statement, "DETACH")
}),
}
}
fn check_parsed_statements(
statements: &[sqlparser::ast::Statement],
) -> Option<SqlSurfaceViolation> {
use sqlparser::ast::Statement;
for statement in statements {
match statement {
Statement::Pragma { .. } => {
return Some(SqlSurfaceViolation::Pragma);
}
Statement::Set(_) if is_pragma_statement(statement) => {
return Some(SqlSurfaceViolation::Pragma);
}
Statement::AttachDatabase { .. }
| Statement::AttachDuckDBDatabase { .. }
| Statement::DetachDuckDBDatabase { .. } => {
return Some(SqlSurfaceViolation::AttachDetach);
}
Statement::Vacuum(_) => {
return Some(SqlSurfaceViolation::Vacuum);
}
Statement::StartTransaction { .. }
| Statement::Commit { .. }
| Statement::Rollback { .. }
| Statement::Savepoint { .. }
| Statement::ReleaseSavepoint { .. } => {
return Some(SqlSurfaceViolation::TransactionControl);
}
Statement::CreateTrigger { .. } => {
}
_ => {}
}
}
None
}
fn is_pragma_statement(statement: &sqlparser::ast::Statement) -> bool {
use sqlparser::ast::{ObjectName, Set, Statement};
fn name_is_pragma(name: &ObjectName) -> bool {
name.to_string().to_uppercase().starts_with("PRAGMA")
}
let Statement::Set(set) = statement else {
return false;
};
match set {
Set::SingleAssignment { variable, .. } => name_is_pragma(variable),
Set::ParenthesizedAssignments { variables, .. } => variables.iter().any(name_is_pragma),
Set::MultipleAssignments { assignments } => {
assignments.iter().any(|a| name_is_pragma(&a.name))
}
_ => false,
}
}
fn check_sql_keywords_fallback(sql: &str) -> Option<SqlSurfaceViolation> {
let sql_upper = sql.to_uppercase();
let sql_clean = remove_sql_comments(&sql_upper);
let statements: Vec<&str> = sql_clean.split(';').map(|s| s.trim()).collect();
for stmt in statements {
if stmt.is_empty() {
continue;
}
if starts_with_sql_keyword(stmt, "PRAGMA") {
return Some(SqlSurfaceViolation::Pragma);
}
if starts_with_sql_keyword(stmt, "ATTACH") || starts_with_sql_keyword(stmt, "DETACH") {
return Some(SqlSurfaceViolation::AttachDetach);
}
if starts_with_sql_keyword(stmt, "VACUUM") {
return Some(SqlSurfaceViolation::Vacuum);
}
if !stmt.contains(" TRIGGER ") {
if starts_with_sql_keyword(stmt, "BEGIN")
|| starts_with_sql_keyword(stmt, "COMMIT")
|| starts_with_sql_keyword(stmt, "ROLLBACK")
|| starts_with_sql_keyword(stmt, "SAVEPOINT")
|| starts_with_sql_keyword(stmt, "RELEASE")
|| starts_with_sql_keyword(stmt, "END")
{
return Some(SqlSurfaceViolation::TransactionControl);
}
}
}
None
}
fn starts_with_sql_keyword(statement: &str, keyword: &str) -> bool {
statement.strip_prefix(keyword).is_some_and(|suffix| {
suffix
.chars()
.next()
.is_none_or(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')))
})
}
fn remove_sql_comments(sql: &str) -> String {
let mut result = String::with_capacity(sql.len());
let mut chars = sql.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'-' if chars.peek() == Some(&'-') => {
chars.next(); for ch in chars.by_ref() {
if ch == '\n' || ch == '\r' {
result.push(' ');
break;
}
}
}
'/' if chars.peek() == Some(&'*') => {
chars.next(); while let Some(ch) = chars.next() {
if ch == '*' && chars.peek() == Some(&'/') {
chars.next(); break;
}
}
result.push(' ');
}
'\'' | '"' | '`' => {
let quote = ch;
result.push(ch);
while let Some(ch) = chars.next() {
result.push(ch);
if ch == quote {
if chars.peek() == Some("e) {
chars.next(); result.push(quote);
} else {
break;
}
}
}
}
_ => result.push(ch),
}
}
result
}
fn parse_checked_sql(sql: &str) -> Result<Vec<sqlparser::ast::Statement>, SqlSurfaceViolation> {
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
if sql.len() > MAX_CHECKED_SQL_BYTES {
return Err(SqlSurfaceViolation::ResourceLimit);
}
let dialect = SQLiteDialect {};
let statements = Parser::new(&dialect)
.with_recursion_limit(MAX_CHECKED_SQL_RECURSION)
.try_with_sql(sql)
.and_then(|mut parser| parser.parse_statements())
.map_err(|_| {
check_sql_keywords_fallback(sql).unwrap_or(SqlSurfaceViolation::ParserRejected)
})?;
if contains_extension_loading_call(sql)? {
return Err(SqlSurfaceViolation::ExtensionLoading);
}
Ok(statements)
}
fn ensure_checked_sql_surface(
sql: &str,
cardinality: CheckedSqlCardinality,
) -> Result<(), SqliteError> {
let statements = parse_checked_sql(sql).map_err(|violation| {
SqliteError::UnsafeSql(violation.checked_surface_message().to_string())
})?;
if cardinality == CheckedSqlCardinality::ExactlyOne && statements.len() != 1 {
return Err(SqliteError::UnsafeSql(
SqlSurfaceViolation::StatementCount
.checked_surface_message()
.to_string(),
));
}
if let Some(violation) = check_parsed_statements(&statements) {
return Err(SqliteError::UnsafeSql(
violation.checked_surface_message().to_string(),
));
}
Ok(())
}
pub fn validate_checked_sql_statement(sql: &str) -> Result<(), SqliteError> {
ensure_checked_sql_surface(sql, CheckedSqlCardinality::ExactlyOne)
}
pub fn validate_checked_sql_batch(sql: &str) -> Result<(), SqliteError> {
ensure_checked_sql_surface(sql, CheckedSqlCardinality::Batch)
}
fn ensure_unchecked_sql_surface(sql: &str) -> Result<(), SqliteError> {
if unchecked_sql_contains_attach_detach(sql) {
return Err(SqliteError::UnsafeSql(
"ATTACH and DETACH are disabled on SQLite connections; open a separate validated connection instead"
.to_string(),
));
}
Ok(())
}
fn resolve_sqlite_open_path(path: &Path) -> Result<PathBuf, SqliteError> {
if path.exists() {
return std::fs::canonicalize(path).map_err(SqliteError::Io);
}
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let canonical_parent = std::fs::canonicalize(parent).map_err(SqliteError::Io)?;
let file_name = path.file_name().ok_or_else(|| {
SqliteError::UnsafePath("SQLite database path must resolve to a file name".to_string())
})?;
Ok(canonical_parent.join(file_name))
}
fn validate_sqlite_open_path_lexical(path: &Path) -> Result<(), SqliteError> {
let raw = path.as_os_str().to_string_lossy();
if raw.starts_with('~') {
return Err(SqliteError::UnsafePath(
"tilde-prefixed SQLite paths are rejected; pass an explicit validated path".to_string(),
));
}
if path
.components()
.any(|component| matches!(component, Component::ParentDir))
{
return Err(SqliteError::UnsafePath(
"parent-directory traversal in SQLite paths is rejected; pass a normalized validated path"
.to_string(),
));
}
Ok(())
}
#[cfg(any(test, feature = "test-internals"))]
fn validate_sqlite_open_path(path: &Path) -> Result<(), SqliteError> {
validate_sqlite_open_path_lexical(path)?;
let resolved = resolve_sqlite_open_path(path)?;
validate_resolved_sqlite_path(&resolved)
}
fn validate_resolved_sqlite_path(resolved_path: &Path) -> Result<(), SqliteError> {
fn resolves_into(resolved_path: &Path, restricted: &str) -> bool {
resolved_path.starts_with(Path::new(restricted))
|| std::fs::canonicalize(restricted)
.is_ok_and(|canonical| resolved_path.starts_with(&canonical))
}
if resolves_into(resolved_path, "/etc") {
return Err(SqliteError::UnsafePath(format!(
"SQLite database path resolves into restricted system directory: {}",
resolved_path.display()
)));
}
if resolved_path.starts_with(Path::new("/sys")) {
return Err(SqliteError::UnsafePath(format!(
"SQLite database path resolves into restricted /sys directory: {}",
resolved_path.display()
)));
}
if resolved_path.starts_with(Path::new("/proc")) {
return Err(SqliteError::UnsafePath(format!(
"SQLite database path resolves into restricted /proc directory: {}",
resolved_path.display()
)));
}
if resolved_path.starts_with(Path::new("/dev")) {
return Err(SqliteError::UnsafePath(format!(
"SQLite database path resolves into restricted /dev directory: {}",
resolved_path.display()
)));
}
Ok(())
}
#[cfg(feature = "test-internals")]
#[doc(hidden)]
pub fn fuzz_validate_sqlite_open_path(path: &Path) -> Result<(), SqliteError> {
validate_sqlite_open_path(path)
}
#[derive(Debug)]
pub enum SqliteError {
Sqlite(String),
Cancelled(CancelReason),
ConnectionClosed,
ColumnNotFound(String),
TypeMismatch {
column: String,
expected: &'static str,
actual: String,
},
Io(std::io::Error),
TransactionFinished,
LockPoisoned,
UnsafeSql(String),
UnsafePath(String),
InvalidTextEncoding {
column: String,
source: std::str::Utf8Error,
},
WalCheckpointFailed(String),
StatementTimeout {
limit: std::time::Duration,
},
}
impl SqliteError {
#[must_use]
pub fn is_busy(&self) -> bool {
match self {
Self::Sqlite(msg) => msg.contains("database is locked") || msg.contains("SQLITE_BUSY"),
_ => false,
}
}
#[must_use]
pub fn is_locked(&self) -> bool {
match self {
Self::Sqlite(msg) => {
msg.contains("database table is locked") || msg.contains("SQLITE_LOCKED")
}
_ => false,
}
}
#[must_use]
pub fn is_constraint_violation(&self) -> bool {
match self {
Self::Sqlite(msg) => {
msg.contains("SQLITE_CONSTRAINT")
|| msg.contains("UNIQUE constraint failed")
|| msg.contains("NOT NULL constraint failed")
|| msg.contains("FOREIGN KEY constraint failed")
|| msg.contains("CHECK constraint failed")
}
_ => false,
}
}
#[must_use]
pub fn is_unique_violation(&self) -> bool {
match self {
Self::Sqlite(msg) => msg.contains("UNIQUE constraint failed"),
_ => false,
}
}
#[must_use]
pub fn is_connection_error(&self) -> bool {
matches!(
self,
Self::Io(_) | Self::ConnectionClosed | Self::LockPoisoned
)
}
#[must_use]
pub fn is_transient(&self) -> bool {
if matches!(self, Self::Io(_) | Self::ConnectionClosed) {
return true;
}
self.is_busy() || self.is_locked()
}
#[must_use]
pub fn is_retryable(&self) -> bool {
self.is_transient()
}
#[must_use]
pub fn error_code(&self) -> Option<&str> {
match self {
Self::Sqlite(msg) => {
if msg.contains("SQLITE_BUSY") || msg.contains("database is locked") {
Some("SQLITE_BUSY")
} else if msg.contains("SQLITE_LOCKED") || msg.contains("database table is locked")
{
Some("SQLITE_LOCKED")
} else if msg.contains("SQLITE_CONSTRAINT") || msg.contains("constraint failed") {
Some("SQLITE_CONSTRAINT")
} else if msg.contains("SQLITE_ERROR") {
Some("SQLITE_ERROR")
} else {
None
}
}
Self::Io(_) => Some("SQLITE_IOERR"),
Self::ConnectionClosed => Some("SQLITE_MISUSE"),
Self::UnsafePath(_) => Some("SQLITE_PERM"),
_ => None,
}
}
}
impl fmt::Display for SqliteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Sqlite(msg) => write!(f, "SQLite error: {msg}"),
Self::Cancelled(reason) => write!(f, "SQLite operation cancelled: {reason:?}"),
Self::ConnectionClosed => write!(f, "SQLite connection is closed"),
Self::ColumnNotFound(name) => write!(f, "Column not found: {name}"),
Self::TypeMismatch {
column,
expected,
actual,
} => write!(
f,
"Type mismatch for column {column}: expected {expected}, got {actual}"
),
Self::Io(e) => write!(f, "SQLite I/O error: {e}"),
Self::TransactionFinished => write!(f, "Transaction already finished"),
Self::LockPoisoned => write!(f, "SQLite connection lock poisoned"),
Self::UnsafeSql(msg) => {
write!(
f,
"Unsafe SQLite control SQL on SQLite binding surface: {msg}"
)
}
Self::UnsafePath(msg) => write!(f, "Unsafe SQLite database path: {msg}"),
Self::InvalidTextEncoding { column, source } => {
write!(
f,
"SQLite text column {column} contained invalid UTF-8: {source}"
)
}
Self::WalCheckpointFailed(msg) => write!(f, "WAL checkpoint failed: {msg}"),
Self::StatementTimeout { limit } => write!(
f,
"statement aborted by budget-derived statement timeout ({limit:?})"
),
}
}
}
impl std::error::Error for SqliteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::InvalidTextEncoding { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for SqliteError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqliteOperation {
Open,
Prepare,
Bind,
Step,
ExecuteBatch,
TransactionBegin,
TransactionCommit,
TransactionRollback,
Configure,
Close,
BlockingPool,
Validation,
}
impl SqliteOperation {
#[must_use]
const fn as_str(self) -> &'static str {
match self {
Self::Open => "open",
Self::Prepare => "prepare",
Self::Bind => "bind",
Self::Step => "step",
Self::ExecuteBatch => "execute_batch",
Self::TransactionBegin => "transaction_begin",
Self::TransactionCommit => "transaction_commit",
Self::TransactionRollback => "transaction_rollback",
Self::Configure => "configure",
Self::Close => "close",
Self::BlockingPool => "blocking_pool",
Self::Validation => "validation",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqliteErrorCategory {
Busy,
Locked,
Constraint,
Interrupted,
Timeout,
PermissionDenied,
ReadOnly,
Io,
Corrupt,
ResourceExhausted,
InvalidInput,
NotFound,
Closed,
Cancelled,
Internal,
Unknown,
}
impl SqliteErrorCategory {
#[must_use]
const fn operator_code(self) -> &'static str {
match self {
Self::Busy => "sqlite.busy",
Self::Locked => "sqlite.locked",
Self::Constraint => "sqlite.constraint",
Self::Interrupted => "sqlite.interrupted",
Self::Timeout => "sqlite.timeout",
Self::PermissionDenied => "sqlite.permission_denied",
Self::ReadOnly => "sqlite.read_only",
Self::Io => "sqlite.io",
Self::Corrupt => "sqlite.corrupt",
Self::ResourceExhausted => "sqlite.resource_exhausted",
Self::InvalidInput => "sqlite.invalid_input",
Self::NotFound => "sqlite.not_found",
Self::Closed => "sqlite.closed",
Self::Cancelled => "sqlite.cancelled",
Self::Internal => "sqlite.internal",
Self::Unknown => "sqlite.unknown",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqliteRetryDisposition {
Never,
RetryOperation,
ReopenConnection,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SqliteErrorDiagnostic {
operation: SqliteOperation,
category: SqliteErrorCategory,
primary_code: Option<&'static str>,
extended_code: Option<i32>,
retry: SqliteRetryDisposition,
connection_error: bool,
}
impl SqliteErrorDiagnostic {
#[must_use]
pub const fn operation(&self) -> SqliteOperation {
self.operation
}
#[must_use]
pub const fn category(&self) -> SqliteErrorCategory {
self.category
}
#[must_use]
pub const fn operator_code(&self) -> &'static str {
self.category.operator_code()
}
#[must_use]
pub const fn primary_code(&self) -> Option<&'static str> {
self.primary_code
}
#[must_use]
pub const fn extended_code(&self) -> Option<i32> {
self.extended_code
}
#[must_use]
pub const fn retry_disposition(&self) -> SqliteRetryDisposition {
self.retry
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self.retry, SqliteRetryDisposition::RetryOperation)
}
#[must_use]
pub const fn is_connection_error(&self) -> bool {
self.connection_error
}
fn from_legacy(operation: SqliteOperation, error: &SqliteError) -> Self {
let (category, retry, connection_error) = match error {
SqliteError::Cancelled(_) => (
SqliteErrorCategory::Cancelled,
SqliteRetryDisposition::Never,
false,
),
SqliteError::ConnectionClosed => (
SqliteErrorCategory::Closed,
SqliteRetryDisposition::ReopenConnection,
true,
),
SqliteError::ColumnNotFound(_) => (
SqliteErrorCategory::NotFound,
SqliteRetryDisposition::Never,
false,
),
SqliteError::TypeMismatch { .. }
| SqliteError::UnsafeSql(_)
| SqliteError::InvalidTextEncoding { .. }
| SqliteError::TransactionFinished => (
SqliteErrorCategory::InvalidInput,
SqliteRetryDisposition::Never,
false,
),
SqliteError::UnsafePath(_) => (
SqliteErrorCategory::PermissionDenied,
SqliteRetryDisposition::Never,
false,
),
SqliteError::Io(_) => (
SqliteErrorCategory::Io,
SqliteRetryDisposition::ReopenConnection,
true,
),
SqliteError::LockPoisoned => (
SqliteErrorCategory::Internal,
SqliteRetryDisposition::ReopenConnection,
true,
),
SqliteError::StatementTimeout { .. } => (
SqliteErrorCategory::Timeout,
SqliteRetryDisposition::Never,
false,
),
SqliteError::WalCheckpointFailed(_) => (
SqliteErrorCategory::Io,
SqliteRetryDisposition::RetryOperation,
false,
),
SqliteError::Sqlite(_) => (
SqliteErrorCategory::Unknown,
SqliteRetryDisposition::Never,
false,
),
};
Self {
operation,
category,
primary_code: None,
extended_code: None,
retry,
connection_error,
}
}
fn from_rusqlite(operation: SqliteOperation, error: &rusqlite::Error) -> Self {
let (code, extended_code) = match error {
rusqlite::Error::SqlInputError { error, .. } => {
(Some(error.code), Some(error.extended_code))
}
_ => (
error.sqlite_error_code(),
error.sqlite_extended_error_code(),
),
};
let (category, primary_code, retry, connection_error) = match code {
Some(rusqlite::ffi::ErrorCode::DatabaseBusy) => (
SqliteErrorCategory::Busy,
Some("SQLITE_BUSY"),
SqliteRetryDisposition::RetryOperation,
false,
),
Some(rusqlite::ffi::ErrorCode::DatabaseLocked) => (
SqliteErrorCategory::Locked,
Some("SQLITE_LOCKED"),
SqliteRetryDisposition::RetryOperation,
false,
),
Some(rusqlite::ffi::ErrorCode::ConstraintViolation) => (
SqliteErrorCategory::Constraint,
Some("SQLITE_CONSTRAINT"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::OperationInterrupted) => (
SqliteErrorCategory::Interrupted,
Some("SQLITE_INTERRUPT"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::PermissionDenied) => (
SqliteErrorCategory::PermissionDenied,
Some("SQLITE_PERM"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::AuthorizationForStatementDenied) => (
SqliteErrorCategory::PermissionDenied,
Some("SQLITE_AUTH"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::ReadOnly) => (
SqliteErrorCategory::ReadOnly,
Some("SQLITE_READONLY"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::SystemIoFailure) => (
SqliteErrorCategory::Io,
Some("SQLITE_IOERR"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::DatabaseCorrupt) => (
SqliteErrorCategory::Corrupt,
Some("SQLITE_CORRUPT"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::NotADatabase) => (
SqliteErrorCategory::Corrupt,
Some("SQLITE_NOTADB"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::OutOfMemory) => (
SqliteErrorCategory::ResourceExhausted,
Some("SQLITE_NOMEM"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::DiskFull) => (
SqliteErrorCategory::ResourceExhausted,
Some("SQLITE_FULL"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::TooBig) => (
SqliteErrorCategory::ResourceExhausted,
Some("SQLITE_TOOBIG"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::CannotOpen) => (
SqliteErrorCategory::Io,
Some("SQLITE_CANTOPEN"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::NotFound) => (
SqliteErrorCategory::NotFound,
Some("SQLITE_NOTFOUND"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::SchemaChanged) => (
SqliteErrorCategory::Internal,
Some("SQLITE_SCHEMA"),
SqliteRetryDisposition::RetryOperation,
false,
),
Some(rusqlite::ffi::ErrorCode::TypeMismatch) => (
SqliteErrorCategory::InvalidInput,
Some("SQLITE_MISMATCH"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::ParameterOutOfRange) => (
SqliteErrorCategory::InvalidInput,
Some("SQLITE_RANGE"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::ApiMisuse) => (
SqliteErrorCategory::Internal,
Some("SQLITE_MISUSE"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::OperationAborted) => (
SqliteErrorCategory::Internal,
Some("SQLITE_ABORT"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::FileLockingProtocolFailed) => (
SqliteErrorCategory::Io,
Some("SQLITE_PROTOCOL"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::InternalMalfunction) => (
SqliteErrorCategory::Internal,
Some("SQLITE_INTERNAL"),
SqliteRetryDisposition::ReopenConnection,
true,
),
Some(rusqlite::ffi::ErrorCode::NoLargeFileSupport) => (
SqliteErrorCategory::Internal,
Some("SQLITE_NOLFS"),
SqliteRetryDisposition::Never,
false,
),
Some(rusqlite::ffi::ErrorCode::Unknown) => (
if matches!(
operation,
SqliteOperation::Prepare
| SqliteOperation::Bind
| SqliteOperation::TransactionBegin
) {
SqliteErrorCategory::InvalidInput
} else {
SqliteErrorCategory::Unknown
},
Some("SQLITE_ERROR"),
SqliteRetryDisposition::Never,
false,
),
None => {
let operation = match error {
rusqlite::Error::InvalidParameterCount(_, _)
| rusqlite::Error::InvalidParameterName(_)
| rusqlite::Error::NulError(_)
| rusqlite::Error::ToSqlConversionFailure(_) => SqliteOperation::Bind,
_ => operation,
};
return Self {
operation,
category: SqliteErrorCategory::InvalidInput,
primary_code: None,
extended_code: None,
retry: SqliteRetryDisposition::Never,
connection_error: false,
};
}
Some(_) => (
SqliteErrorCategory::Unknown,
None,
SqliteRetryDisposition::Never,
false,
),
};
Self {
operation,
category,
primary_code,
extended_code,
retry,
connection_error,
}
}
}
pub struct SqliteOperationError {
diagnostic: SqliteErrorDiagnostic,
legacy: SqliteError,
engine_source: Option<rusqlite::Error>,
}
impl SqliteOperationError {
fn from_rusqlite(operation: SqliteOperation, error: rusqlite::Error) -> Self {
let diagnostic = SqliteErrorDiagnostic::from_rusqlite(operation, &error);
let rendered = error.to_string();
Self {
diagnostic,
legacy: SqliteError::Sqlite(rendered),
engine_source: Some(error),
}
}
fn from_legacy(operation: SqliteOperation, legacy: SqliteError) -> Self {
let diagnostic = SqliteErrorDiagnostic::from_legacy(operation, &legacy);
Self {
diagnostic,
legacy,
engine_source: None,
}
}
#[must_use]
pub const fn diagnostic(&self) -> &SqliteErrorDiagnostic {
&self.diagnostic
}
#[must_use]
pub const fn legacy_error(&self) -> &SqliteError {
&self.legacy
}
#[must_use]
pub fn engine_source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.engine_source
.as_ref()
.map(|error| error as &(dyn std::error::Error + 'static))
}
#[must_use]
pub fn into_legacy(self) -> SqliteError {
self.legacy
}
}
impl fmt::Debug for SqliteOperationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqliteOperationError")
.field("diagnostic", &self.diagnostic)
.field("legacy", &"<redacted; call legacy_error() explicitly>")
.finish()
}
}
impl fmt::Display for SqliteOperationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] SQLite {} failed",
self.diagnostic.operator_code(),
self.diagnostic.operation().as_str()
)?;
if let Some(primary) = self.diagnostic.primary_code() {
write!(f, " ({primary}")?;
if let Some(extended) = self.diagnostic.extended_code() {
write!(f, ", extended={extended}")?;
}
write!(f, ")")?;
}
Ok(())
}
}
impl std::error::Error for SqliteOperationError {}
fn diagnose_legacy_outcome<T>(
operation: SqliteOperation,
outcome: Outcome<T, SqliteError>,
) -> Outcome<T, SqliteOperationError> {
match outcome {
Outcome::Ok(value) => Outcome::Ok(value),
Outcome::Err(error) => Outcome::Err(SqliteOperationError::from_legacy(operation, error)),
Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
Outcome::Panicked(payload) => Outcome::Panicked(payload),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SqliteValue {
Null,
Integer(i64),
Real(f64),
Text(String),
Blob(Vec<u8>),
}
impl SqliteValue {
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use]
pub fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub fn as_real(&self) -> Option<f64> {
match self {
Self::Real(v) => Some(*v),
#[allow(clippy::cast_precision_loss)]
Self::Integer(v) => Some(*v as f64),
_ => None,
}
}
#[must_use]
pub fn as_real_strict(&self) -> Option<f64> {
match self {
Self::Real(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_blob(&self) -> Option<&[u8]> {
match self {
Self::Blob(v) => Some(v),
_ => None,
}
}
}
impl fmt::Display for SqliteValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => write!(f, "NULL"),
Self::Integer(v) => write!(f, "{v}"),
Self::Real(v) => write!(f, "{v}"),
Self::Text(v) => write!(f, "{v}"),
Self::Blob(v) => write!(f, "<blob {} bytes>", v.len()),
}
}
}
#[derive(Clone)]
pub struct SqliteRow {
columns: Arc<BTreeMap<String, usize>>,
ordered_columns: Arc<[String]>,
values: Vec<SqliteValue>,
}
impl fmt::Debug for SqliteRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqliteRow")
.field("columns", &self.columns)
.field("values", &self.values)
.finish()
}
}
impl SqliteRow {
fn new(
columns: Arc<BTreeMap<String, usize>>,
ordered_columns: Arc<[String]>,
values: Vec<SqliteValue>,
) -> Self {
Self {
columns,
ordered_columns,
values,
}
}
pub fn get(&self, column: &str) -> Result<&SqliteValue, SqliteError> {
let idx = self
.columns
.get(column)
.ok_or_else(|| SqliteError::ColumnNotFound(column.to_string()))?;
self.values
.get(*idx)
.ok_or_else(|| SqliteError::ColumnNotFound(column.to_string()))
}
pub fn get_idx(&self, idx: usize) -> Result<&SqliteValue, SqliteError> {
self.values
.get(idx)
.ok_or_else(|| SqliteError::ColumnNotFound(format!("index {idx}")))
}
pub fn get_i64(&self, column: &str) -> Result<i64, SqliteError> {
let val = self.get(column)?;
val.as_integer().ok_or_else(|| SqliteError::TypeMismatch {
column: column.to_string(),
expected: "integer",
actual: format!("{val:?}"),
})
}
pub fn get_f64(&self, column: &str) -> Result<f64, SqliteError> {
let val = self.get(column)?;
val.as_real().ok_or_else(|| SqliteError::TypeMismatch {
column: column.to_string(),
expected: "real",
actual: format!("{val:?}"),
})
}
pub fn get_f64_strict(&self, column: &str) -> Result<f64, SqliteError> {
let val = self.get(column)?;
val.as_real_strict()
.ok_or_else(|| SqliteError::TypeMismatch {
column: column.to_string(),
expected: "real",
actual: format!("{val:?}"),
})
}
pub fn get_str(&self, column: &str) -> Result<&str, SqliteError> {
let val = self.get(column)?;
val.as_text().ok_or_else(|| SqliteError::TypeMismatch {
column: column.to_string(),
expected: "text",
actual: format!("{val:?}"),
})
}
pub fn get_blob(&self, column: &str) -> Result<&[u8], SqliteError> {
let val = self.get(column)?;
val.as_blob().ok_or_else(|| SqliteError::TypeMismatch {
column: column.to_string(),
expected: "blob",
actual: format!("{val:?}"),
})
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn column_names(&self) -> impl Iterator<Item = &str> {
self.columns.keys().map(String::as_str)
}
pub fn column_names_in_order(&self) -> impl ExactSizeIterator<Item = &str> {
self.ordered_columns.iter().map(String::as_str)
}
#[must_use]
pub fn column_name(&self, index: usize) -> Option<&str> {
self.ordered_columns.get(index).map(String::as_str)
}
#[must_use]
pub fn column_index(&self, name: &str) -> Option<usize> {
self.ordered_columns
.iter()
.position(|column| column.eq_ignore_ascii_case(name))
}
}
#[derive(Debug, Default)]
struct SqliteRowStreamCounters {
rows_stepped: AtomicUsize,
rows_yielded: AtomicUsize,
buffered_rows: AtomicUsize,
peak_buffered_rows: AtomicUsize,
}
impl SqliteRowStreamCounters {
fn record_buffered_row(&self) {
let buffered = self
.buffered_rows
.fetch_add(1, Ordering::AcqRel)
.saturating_add(1);
let observed = buffered.min(SQLITE_ROW_STREAM_CHANNEL_CAPACITY);
let mut peak = self.peak_buffered_rows.load(Ordering::Acquire);
while observed > peak {
match self.peak_buffered_rows.compare_exchange_weak(
peak,
observed,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(current) => peak = current,
}
}
}
fn record_yielded_row(&self) {
self.buffered_rows.fetch_sub(1, Ordering::AcqRel);
self.rows_yielded.fetch_add(1, Ordering::AcqRel);
}
fn snapshot(&self) -> SqliteRowStreamStats {
SqliteRowStreamStats {
rows_stepped: self.rows_stepped.load(Ordering::Acquire),
rows_yielded: self.rows_yielded.load(Ordering::Acquire),
buffered_rows: self.buffered_rows.load(Ordering::Acquire),
peak_buffered_rows: self.peak_buffered_rows.load(Ordering::Acquire),
channel_capacity: SQLITE_ROW_STREAM_CHANNEL_CAPACITY,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SqliteRowStreamStats {
pub rows_stepped: usize,
pub rows_yielded: usize,
pub buffered_rows: usize,
pub peak_buffered_rows: usize,
pub channel_capacity: usize,
}
type SqliteRowStreamMessage = Result<SqliteRow, SqliteError>;
fn send_sqlite_stream_message(
sender: &mpsc::Sender<SqliteRowStreamMessage>,
counters: &SqliteRowStreamCounters,
mut message: SqliteRowStreamMessage,
) -> bool {
let is_row = message.is_ok();
loop {
match sender.try_reserve() {
Ok(permit) => {
if is_row {
counters.record_buffered_row();
}
match permit.send(message) {
Outcome::Ok(()) => return true,
Outcome::Err(
mpsc::SendError::Disconnected(_) | mpsc::SendError::Cancelled(_),
) => {
if is_row {
counters.buffered_rows.fetch_sub(1, Ordering::AcqRel);
}
return false;
}
Outcome::Err(mpsc::SendError::Full(value)) => {
if is_row {
counters.buffered_rows.fetch_sub(1, Ordering::AcqRel);
}
message = value;
}
Outcome::Cancelled(_) | Outcome::Panicked(_) => return false,
}
}
Err(mpsc::SendError::Disconnected(()) | mpsc::SendError::Cancelled(())) => {
return false;
}
Err(mpsc::SendError::Full(())) => {
std::thread::sleep(SQLITE_ROW_STREAM_FULL_BACKOFF);
}
}
}
}
fn sqlite_row_from_rusqlite_row(
row: &rusqlite::Row<'_>,
column_names: &Arc<[String]>,
columns: &Arc<BTreeMap<String, usize>>,
) -> Result<SqliteRow, SqliteError> {
let column_count = column_names.len();
let mut values = Vec::with_capacity(column_count);
for i in 0..column_count {
let value = row
.get_ref(i)
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let column = column_name_or_index(column_names, i);
values.push(convert_value(value, &column)?);
}
Ok(SqliteRow::new(
Arc::clone(columns),
Arc::clone(column_names),
values,
))
}
fn sqlite_row_metadata(row: &rusqlite::Row<'_>) -> (Arc<[String]>, Arc<BTreeMap<String, usize>>) {
let column_names: Arc<[String]> = row
.as_ref()
.column_names()
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>()
.into();
let columns = column_names
.iter()
.enumerate()
.map(|(index, name)| (name.clone(), index))
.collect();
(column_names, Arc::new(columns))
}
pub struct SqliteRowStream<'connection> {
receiver: mpsc::Receiver<SqliteRowStreamMessage>,
handle: crate::runtime::blocking_pool::BlockingTaskHandle,
counters: Arc<SqliteRowStreamCounters>,
phase: Arc<Mutex<SqliteConnectionOpPhase>>,
finished: bool,
interrupt: Arc<rusqlite::InterruptHandle>,
_connection_lease: PhantomData<&'connection mut SqliteConnection>,
}
impl fmt::Debug for SqliteRowStream<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqliteRowStream")
.field("stats", &self.stats())
.field("finished", &self.finished)
.finish()
}
}
impl SqliteRowStream<'_> {
fn request_cancel(&self) -> SqliteConnectionOpPhase {
let mut phase = self.phase.lock();
let observed = *phase;
match observed {
SqliteConnectionOpPhase::Queued => {
*phase = SqliteConnectionOpPhase::CancelRequested;
}
SqliteConnectionOpPhase::Running => {
*phase = SqliteConnectionOpPhase::CancelRequested;
self.interrupt.interrupt();
}
SqliteConnectionOpPhase::CancelRequested | SqliteConnectionOpPhase::Completed => {}
}
observed
}
pub async fn next(&mut self, cx: &Cx) -> Outcome<Option<SqliteRow>, SqliteError> {
if self.finished {
return Outcome::Ok(None);
}
if cx.checkpoint().is_err() {
self.cancel_in_drain(cx).await;
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.receiver.recv(cx).await {
Ok(Ok(row)) => {
self.counters.record_yielded_row();
Outcome::Ok(Some(row))
}
Ok(Err(err)) => {
self.finish();
Outcome::Err(err)
}
Err(mpsc::RecvError::Disconnected) => {
self.finished = true;
Outcome::Ok(None)
}
Err(mpsc::RecvError::Cancelled) => {
self.cancel_in_drain(cx).await;
Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
)
}
Err(mpsc::RecvError::Empty) => Outcome::Err(SqliteError::Sqlite(
"sqlite row stream receive unexpectedly returned empty".to_string(),
)),
}
}
async fn cancel_in_drain(&mut self, cx: &Cx) {
const MASKED_DRAIN_POLLS: u32 = 1024;
if self.finished {
return;
}
self.finished = true;
let cancel_phase = self.request_cancel();
self.handle.cancel();
if !self.handle.is_done() {
match cancel_phase {
SqliteConnectionOpPhase::Running => cx.trace(
"client.wire_cancel proto=sqlite outcome=interrupt_sent op=row_stream",
),
SqliteConnectionOpPhase::Queued => cx.trace(
"client.wire_cancel proto=sqlite outcome=skipped op=row_stream reason=queued",
),
SqliteConnectionOpPhase::Completed => cx.trace(
"client.wire_cancel proto=sqlite outcome=skipped op=row_stream reason=completed",
),
SqliteConnectionOpPhase::CancelRequested => {}
}
let drained = crate::combinator::commit_section(cx, MASKED_DRAIN_POLLS, async {
loop {
match self.receiver.recv(cx).await {
Ok(_) => {
if self.handle.is_done() {
break true;
}
}
Err(mpsc::RecvError::Disconnected) => break true,
Err(_) => break false,
}
}
})
.await;
if drained {
cx.trace("client.wire_cancel proto=sqlite drain=job_completed op=row_stream");
} else {
cx.trace(
"client.wire_cancel proto=sqlite drain=masked_poll_budget_exhausted \
fallback=abandon_job op=row_stream",
);
}
}
self.receiver.close();
}
#[must_use]
pub fn stats(&self) -> SqliteRowStreamStats {
self.counters.snapshot()
}
fn finish(&mut self) {
if !self.finished {
self.finished = true;
self.receiver.close();
self.request_cancel();
self.handle.cancel();
}
}
}
impl Drop for SqliteRowStream<'_> {
fn drop(&mut self) {
self.finish();
}
}
struct SqliteConnectionInner {
conn: Option<rusqlite::Connection>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SqliteConnectionOpPhase {
Queued,
Running,
CancelRequested,
Completed,
}
enum SqliteConnectionOpCompletion<R, E> {
Finished(Result<R, E>),
Cancelled,
}
trait SqliteConnectionOpError: Send + 'static {
fn from_legacy(operation: SqliteOperation, error: SqliteError) -> Self;
fn is_interrupt(&self) -> bool;
fn statement_timeout(operation: SqliteOperation, limit: Duration) -> Self;
}
impl SqliteConnectionOpError for SqliteError {
fn from_legacy(_operation: SqliteOperation, error: SqliteError) -> Self {
error
}
fn is_interrupt(&self) -> bool {
sqlite_error_is_interrupt(self)
}
fn statement_timeout(_operation: SqliteOperation, limit: Duration) -> Self {
Self::StatementTimeout { limit }
}
}
impl SqliteConnectionOpError for SqliteOperationError {
fn from_legacy(operation: SqliteOperation, error: SqliteError) -> Self {
SqliteOperationError::from_legacy(operation, error)
}
fn is_interrupt(&self) -> bool {
self.diagnostic.category() == SqliteErrorCategory::Interrupted
}
fn statement_timeout(operation: SqliteOperation, limit: Duration) -> Self {
SqliteOperationError::from_legacy(operation, SqliteError::StatementTimeout { limit })
}
}
impl SqliteConnectionInner {
fn new(conn: rusqlite::Connection) -> Self {
Self { conn: Some(conn) }
}
fn get(&self) -> Result<&rusqlite::Connection, SqliteError> {
self.conn.as_ref().ok_or(SqliteError::ConnectionClosed)
}
fn close(&mut self) {
self.conn = None;
}
}
pub struct SqliteConnection {
inner: Arc<Mutex<SqliteConnectionInner>>,
pool: BlockingPoolHandle,
transaction_state: Arc<Mutex<TransactionState>>,
transaction_generation: Arc<AtomicU64>,
interrupt: Arc<rusqlite::InterruptHandle>,
statement_timeout_override: Option<std::time::Duration>,
}
impl fmt::Debug for SqliteConnection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = *self.transaction_state.lock();
f.debug_struct("SqliteConnection")
.field("open", &self.inner.lock().conn.is_some())
.field("pool", &self.pool)
.field("transaction_state", &state)
.field(
"transaction_generation",
&self.transaction_generation.load(Ordering::Acquire),
)
.finish()
}
}
impl SqliteConnection {
pub fn interrupt(&self) {
self.interrupt.interrupt();
}
pub fn set_statement_timeout_override(&mut self, timeout: Option<std::time::Duration>) {
self.statement_timeout_override = timeout;
}
#[must_use]
pub fn statement_timeout_override(&self) -> Option<std::time::Duration> {
self.statement_timeout_override
}
async fn run_connection_op<R, F>(
&self,
cx: &Cx,
op_name: &'static str,
f: F,
) -> Outcome<R, SqliteError>
where
R: Send + 'static,
F: FnOnce(&rusqlite::Connection) -> Result<R, SqliteError> + Send + 'static,
{
self.run_connection_op_inner(cx, op_name, SqliteOperation::BlockingPool, None, f)
.await
}
async fn run_connection_op_diagnosed<R, F>(
&self,
cx: &Cx,
op_name: &'static str,
operation: SqliteOperation,
f: F,
) -> Outcome<R, SqliteOperationError>
where
R: Send + 'static,
F: FnOnce(&rusqlite::Connection) -> Result<R, SqliteOperationError> + Send + 'static,
{
self.run_connection_op_inner(cx, op_name, operation, None, f)
.await
}
async fn run_connection_op_inner<R, E, F>(
&self,
cx: &Cx,
op_name: &'static str,
operation: SqliteOperation,
expected_generation: Option<u64>,
f: F,
) -> Outcome<R, E>
where
R: Send + 'static,
E: SqliteConnectionOpError,
F: FnOnce(&rusqlite::Connection) -> Result<R, E> + Send + 'static,
{
const TIMEOUT_PROGRESS_OPS: i32 = 1000;
const MASKED_DRAIN_POLLS: u32 = 1024;
let timeout =
crate::database::effective_statement_timeout(cx, self.statement_timeout_override);
if let Some(limit) = timeout {
let remaining_ns = crate::database::remaining_budget(cx)
.map_or_else(|| "none".to_string(), |d| d.as_nanos().to_string());
let base_ms = self.statement_timeout_override.map_or_else(
|| "none".to_string(),
|d| crate::database::statement_timeout_millis(d).to_string(),
);
cx.trace(&format!(
"client.budget_forwarded proto=sqlite base_ms={base_ms} \
remaining_ns={remaining_ns} statement_timeout_ms={}",
crate::database::statement_timeout_millis(limit)
));
}
let inner = Arc::clone(&self.inner);
let transaction_state = Arc::clone(&self.transaction_state);
let transaction_generation = Arc::clone(&self.transaction_generation);
let phase = Arc::new(Mutex::new(SqliteConnectionOpPhase::Queued));
let worker_phase = Arc::clone(&phase);
let (tx, mut rx) = crate::channel::oneshot::channel();
let permit = match tx.reserve(cx) {
Ok(permit) => permit,
Err(crate::channel::oneshot::SendError::Cancelled(())) => {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
Err(crate::channel::oneshot::SendError::Disconnected(())) => {
return Outcome::Err(E::from_legacy(
operation,
SqliteError::Sqlite(format!("failed to reserve result channel for {op_name}")),
));
}
};
let handle = self.pool.spawn(move || {
let completion = (|| {
let guard = inner.lock();
{
let mut phase = worker_phase.lock();
match *phase {
SqliteConnectionOpPhase::Queued => {
*phase = SqliteConnectionOpPhase::Running;
}
SqliteConnectionOpPhase::CancelRequested => {
*phase = SqliteConnectionOpPhase::Completed;
drop(phase);
drop(guard);
return SqliteConnectionOpCompletion::Cancelled;
}
SqliteConnectionOpPhase::Running | SqliteConnectionOpPhase::Completed => {
unreachable!("a SQLite connection operation starts exactly once")
}
}
}
let deadline_fired = Arc::new(AtomicBool::new(false));
let result = (|| {
let conn = guard
.get()
.map_err(|error| E::from_legacy(operation, error))?;
ensure_managed_transaction_open(
conn,
transaction_state.as_ref(),
transaction_generation.as_ref(),
expected_generation,
)
.map_err(|error| E::from_legacy(operation, error))?;
if let Some(limit) = timeout {
let deadline = std::time::Instant::now() + limit;
let fired = Arc::clone(&deadline_fired);
conn.progress_handler(
TIMEOUT_PROGRESS_OPS,
Some(move || {
if std::time::Instant::now() < deadline {
return false;
}
fired.store(true, Ordering::Release);
true
}),
)
.map_err(|e| {
E::from_legacy(
operation,
SqliteError::Sqlite(format!(
"failed to arm statement timeout: {e}"
)),
)
})?;
}
let result = f(conn);
if timeout.is_some() {
let _ = conn.progress_handler(0, None::<fn() -> bool>);
}
result
})();
let cancellation_requested = {
let mut phase = worker_phase.lock();
let cancellation_requested = *phase == SqliteConnectionOpPhase::CancelRequested;
*phase = SqliteConnectionOpPhase::Completed;
cancellation_requested
};
let result = match (cancellation_requested, timeout, result) {
(true, _, Err(err)) if err.is_interrupt() => {
drop(guard);
return SqliteConnectionOpCompletion::Cancelled;
}
(_, Some(limit), Err(err))
if deadline_fired.load(Ordering::Acquire) && err.is_interrupt() =>
{
Err(E::statement_timeout(operation, limit))
}
(_, _, result) => result,
};
drop(guard);
SqliteConnectionOpCompletion::Finished(result)
})();
let _ = permit.send(completion);
});
match rx.recv(cx).await {
Ok(SqliteConnectionOpCompletion::Finished(Ok(result))) => Outcome::Ok(result),
Ok(SqliteConnectionOpCompletion::Finished(Err(e))) => Outcome::Err(e),
Ok(SqliteConnectionOpCompletion::Cancelled) => Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
),
Err(crate::channel::oneshot::RecvError::Cancelled) => {
let cancel_phase = {
let mut phase = phase.lock();
let observed = *phase;
match observed {
SqliteConnectionOpPhase::Queued => {
*phase = SqliteConnectionOpPhase::CancelRequested;
}
SqliteConnectionOpPhase::Running => {
*phase = SqliteConnectionOpPhase::CancelRequested;
self.interrupt.interrupt();
}
SqliteConnectionOpPhase::CancelRequested
| SqliteConnectionOpPhase::Completed => {}
}
observed
};
handle.cancel();
match cancel_phase {
SqliteConnectionOpPhase::Running => cx.trace(&format!(
"client.wire_cancel proto=sqlite outcome=interrupt_sent op={op_name}"
)),
SqliteConnectionOpPhase::Queued => cx.trace(&format!(
"client.wire_cancel proto=sqlite outcome=skipped op={op_name} reason=queued"
)),
SqliteConnectionOpPhase::Completed => cx.trace(&format!(
"client.wire_cancel proto=sqlite outcome=skipped op={op_name} reason=completed"
)),
SqliteConnectionOpPhase::CancelRequested => {}
}
let drained =
crate::combinator::commit_section(cx, MASKED_DRAIN_POLLS, rx.recv(cx)).await;
match drained {
Ok(SqliteConnectionOpCompletion::Finished(result)) => {
cx.trace(
"client.wire_cancel proto=sqlite drain=job_completed completion=won",
);
return match result {
Ok(result) => Outcome::Ok(result),
Err(err) => Outcome::Err(err),
};
}
Ok(SqliteConnectionOpCompletion::Cancelled) => {
cx.trace("client.wire_cancel proto=sqlite drain=job_cancelled");
}
Err(crate::channel::oneshot::RecvError::Closed) => {
cx.trace("client.wire_cancel proto=sqlite drain=job_not_started");
}
Err(_) => cx.trace(
"client.wire_cancel proto=sqlite drain=masked_poll_budget_exhausted \
fallback=abandon_job",
),
}
Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
)
}
Err(crate::channel::oneshot::RecvError::Closed) => Outcome::Err(E::from_legacy(
operation,
SqliteError::Sqlite(format!("failed to receive result for {op_name}")),
)),
Err(crate::channel::oneshot::RecvError::PolledAfterCompletion) => {
unreachable!("{op_name} awaits a fresh oneshot recv future")
}
}
}
async fn drain_orphaned_transaction(&self, cx: &Cx) -> Outcome<(), SqliteError> {
let current_state = *self.transaction_state.lock();
if current_state != TransactionState::NeedsRollback {
return Outcome::Ok(());
}
let transaction_state = Arc::clone(&self.transaction_state);
let transaction_generation = Arc::clone(&self.transaction_generation);
self.run_connection_op(cx, "sqlite rollback cleanup", move |conn| {
rollback_orphaned_transaction_generation_guarded(
conn,
transaction_state.as_ref(),
transaction_generation.as_ref(),
)
})
.await
}
fn schedule_dropped_transaction_rollback(&self, expected_generation: u64) {
let inner = Arc::clone(&self.inner);
let transaction_state = Arc::clone(&self.transaction_state);
let transaction_generation = Arc::clone(&self.transaction_generation);
let _cleanup = self.pool.spawn(move || {
let guard = inner.lock();
let Some(conn) = guard.conn.as_ref() else {
if transaction_generation.load(Ordering::Acquire) == expected_generation {
let _ = advance_transaction_generation(transaction_generation.as_ref());
*transaction_state.lock() = TransactionState::Autocommit;
}
return;
};
if transaction_generation.load(Ordering::Acquire) != expected_generation {
return;
}
let _ = rollback_orphaned_transaction_generation_guarded(
conn,
transaction_state.as_ref(),
transaction_generation.as_ref(),
);
});
}
async fn open_with<E, F>(cx: &Cx, operation: SqliteOperation, open: F) -> Outcome<Self, E>
where
E: SqliteConnectionOpError,
F: FnOnce() -> Result<rusqlite::Connection, E> + Send + 'static,
{
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
let pool = get_sqlite_pool();
let pool_clone = pool.clone();
let (tx, mut rx) = crate::channel::oneshot::channel();
let permit = tx.reserve(cx);
let handle = pool.spawn(move || {
let result = open();
if let Ok(permit) = permit {
let _ = permit.send(result);
}
});
match rx.recv(cx).await {
Ok(Ok(conn)) => {
let interrupt = Arc::new(conn.get_interrupt_handle());
Outcome::Ok(Self {
inner: Arc::new(Mutex::new(SqliteConnectionInner::new(conn))),
pool: pool_clone,
transaction_state: Arc::new(Mutex::new(TransactionState::Autocommit)),
transaction_generation: Arc::new(AtomicU64::new(0)),
interrupt,
statement_timeout_override: None,
})
}
Ok(Err(error)) => Outcome::Err(error),
Err(crate::channel::oneshot::RecvError::Cancelled) => {
handle.cancel();
Outcome::Cancelled(sqlite_cancelled_reason(cx))
}
Err(crate::channel::oneshot::RecvError::Closed) => Outcome::Err(E::from_legacy(
operation,
SqliteError::Sqlite("failed to receive result".to_string()),
)),
Err(crate::channel::oneshot::RecvError::PolledAfterCompletion) => {
unreachable!("SQLite blocking-pool open awaits a fresh oneshot recv future")
}
}
}
pub async fn open(cx: &Cx, path: impl AsRef<Path>) -> Outcome<Self, SqliteError> {
let path = path.as_ref().to_path_buf();
Self::open_with(cx, SqliteOperation::Open, move || {
validate_sqlite_open_path_lexical(&path)?;
let resolved_path = resolve_sqlite_open_path(&path)?;
validate_resolved_sqlite_path(&resolved_path)?;
let conn = rusqlite::Connection::open(&resolved_path)
.map_err(|error| SqliteError::Sqlite(error.to_string()))?;
configure_connection_defaults(&conn, true)?;
Ok(conn)
})
.await
}
pub async fn open_diagnosed(
cx: &Cx,
path: impl AsRef<Path>,
) -> Outcome<Self, SqliteOperationError> {
let path = path.as_ref().to_path_buf();
Self::open_with(cx, SqliteOperation::Open, move || {
validate_sqlite_open_path_lexical(&path).map_err(|error| {
SqliteOperationError::from_legacy(SqliteOperation::Validation, error)
})?;
let resolved_path = resolve_sqlite_open_path(&path).map_err(|error| {
SqliteOperationError::from_legacy(SqliteOperation::Validation, error)
})?;
validate_resolved_sqlite_path(&resolved_path).map_err(|error| {
SqliteOperationError::from_legacy(SqliteOperation::Validation, error)
})?;
let conn = rusqlite::Connection::open(&resolved_path).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Open, error)
})?;
configure_connection_defaults_with(&conn, true, |operation, error| {
SqliteOperationError::from_rusqlite(operation, error)
})?;
Ok(conn)
})
.await
}
pub async fn open_in_memory(cx: &Cx) -> Outcome<Self, SqliteError> {
Self::open_with(cx, SqliteOperation::Open, move || {
let conn = rusqlite::Connection::open_in_memory()
.map_err(|error| SqliteError::Sqlite(error.to_string()))?;
configure_connection_defaults(&conn, false)?;
Ok(conn)
})
.await
}
pub async fn open_in_memory_diagnosed(cx: &Cx) -> Outcome<Self, SqliteOperationError> {
Self::open_with(cx, SqliteOperation::Open, move || {
let conn = rusqlite::Connection::open_in_memory().map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Open, error)
})?;
configure_connection_defaults_with(&conn, false, |operation, error| {
SqliteOperationError::from_rusqlite(operation, error)
})?;
Ok(conn)
})
.await
}
pub async fn execute(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.execute_unchecked(cx, sql, params).await
}
pub async fn execute_unchecked(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteError> {
self.execute_unchecked_with(cx, sql, params, None, execute_legacy_statement)
.await
}
async fn execute_in_transaction(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
generation: u64,
) -> Outcome<u64, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.execute_unchecked_in_transaction(cx, sql, params, generation)
.await
}
async fn execute_unchecked_in_transaction(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
generation: u64,
) -> Outcome<u64, SqliteError> {
self.execute_unchecked_with(cx, sql, params, Some(generation), execute_legacy_statement)
.await
}
pub async fn execute_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteOperationError> {
if let Err(error) = validate_checked_sql_statement(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.execute_unchecked_diagnosed(cx, sql, params).await
}
pub async fn execute_unchecked_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteOperationError> {
self.execute_unchecked_diagnosed_impl(cx, sql, params, None)
.await
}
async fn execute_diagnosed_in_transaction(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
generation: u64,
) -> Outcome<u64, SqliteOperationError> {
if let Err(error) = validate_checked_sql_statement(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.execute_unchecked_diagnosed_impl(cx, sql, params, Some(generation))
.await
}
async fn execute_unchecked_diagnosed_impl(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
expected_generation: Option<u64>,
) -> Outcome<u64, SqliteOperationError> {
if let Err(error) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
let sql = sql.to_string();
let params = params.to_vec();
self.run_connection_op_inner(
cx,
"sqlite diagnosed execute",
SqliteOperation::Step,
expected_generation,
move |conn| {
let mut statement = conn.prepare_cached(&sql).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Prepare, error)
})?;
let params_refs: Vec<&dyn rusqlite::ToSql> = params
.iter()
.map(|value| value as &dyn rusqlite::ToSql)
.collect();
statement
.execute(params_refs.as_slice())
.map(|rows| rows as u64)
.map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Step, error)
})
},
)
.await
}
async fn execute_transaction_control(
&self,
cx: &Cx,
sql: &'static str,
effect: TransactionWorkerEffect,
) -> Outcome<u64, SqliteError> {
self.execute_unchecked_with(cx, sql, &[], None, move |conn, sql, _params| {
effect.execute_worker(conn, sql)
})
.await
}
async fn execute_transaction_control_diagnosed(
&self,
cx: &Cx,
sql: &'static str,
operation: SqliteOperation,
effect: TransactionWorkerEffect,
) -> Outcome<u64, SqliteOperationError> {
if let Err(error) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
self.run_connection_op_diagnosed(
cx,
"sqlite diagnosed transaction control",
operation,
move |conn| effect.execute_worker_diagnosed(conn, sql, operation),
)
.await
}
async fn execute_unchecked_with<F>(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
expected_generation: Option<u64>,
execute: F,
) -> Outcome<u64, SqliteError>
where
F: FnOnce(&rusqlite::Connection, &str, &[SqliteValue]) -> Result<u64, SqliteError>
+ Send
+ 'static,
{
if let Err(err) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(err);
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
let sql = sql.to_string();
let params: Vec<SqliteValue> = params.to_vec();
self.run_connection_op_inner(
cx,
"sqlite execute",
SqliteOperation::BlockingPool,
expected_generation,
move |conn| execute(conn, &sql, ¶ms),
)
.await
}
pub async fn execute_batch(&self, cx: &Cx, sql: &str) -> Outcome<(), SqliteError> {
if let Err(err) = validate_checked_sql_batch(sql) {
return Outcome::Err(err);
}
self.execute_batch_unchecked(cx, sql).await
}
pub async fn execute_batch_unchecked(&self, cx: &Cx, sql: &str) -> Outcome<(), SqliteError> {
if let Err(err) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(err);
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
let sql = sql.to_string();
self.run_connection_op(cx, "sqlite execute_batch", move |conn| {
conn.execute_batch(&sql)
.map_err(|e| SqliteError::Sqlite(e.to_string()))
})
.await
}
pub async fn execute_batch_diagnosed(
&self,
cx: &Cx,
sql: &str,
) -> Outcome<(), SqliteOperationError> {
if let Err(error) = validate_checked_sql_batch(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.execute_batch_unchecked_diagnosed(cx, sql).await
}
pub async fn execute_batch_unchecked_diagnosed(
&self,
cx: &Cx,
sql: &str,
) -> Outcome<(), SqliteOperationError> {
if let Err(error) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
let sql = sql.to_string();
self.run_connection_op_diagnosed(
cx,
"sqlite diagnosed execute_batch",
SqliteOperation::ExecuteBatch,
move |conn| {
conn.execute_batch(&sql).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::ExecuteBatch, error)
})
},
)
.await
}
pub async fn query(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.query_unchecked(cx, sql, params).await
}
pub async fn query_unchecked(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteError> {
self.query_unchecked_impl(cx, sql, params, None).await
}
async fn query_in_transaction(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
generation: u64,
) -> Outcome<Vec<SqliteRow>, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.query_unchecked_impl(cx, sql, params, Some(generation))
.await
}
async fn query_unchecked_impl(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
expected_generation: Option<u64>,
) -> Outcome<Vec<SqliteRow>, SqliteError> {
if let Err(err) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(err);
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
let sql = sql.to_string();
let params: Vec<SqliteValue> = params.to_vec();
self.run_connection_op_inner(
cx,
"sqlite query",
SqliteOperation::BlockingPool,
expected_generation,
move |conn| {
let params_refs: Vec<&dyn rusqlite::ToSql> =
params.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
let mut stmt = conn
.prepare_cached(&sql)
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let mut rows = stmt
.query(params_refs.as_slice())
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let mut result = Vec::new();
let mut metadata = None;
while let Some(row) = rows
.next()
.map_err(|e| SqliteError::Sqlite(e.to_string()))?
{
let (column_names, columns) =
metadata.get_or_insert_with(|| sqlite_row_metadata(row));
result.push(sqlite_row_from_rusqlite_row(row, column_names, columns)?);
}
drop(rows);
drop(stmt);
Ok(result)
},
)
.await
}
pub async fn query_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteOperationError> {
if let Err(error) = validate_checked_sql_statement(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.query_unchecked_diagnosed(cx, sql, params).await
}
pub async fn query_unchecked_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteOperationError> {
self.query_unchecked_diagnosed_impl(cx, sql, params, None)
.await
}
async fn query_diagnosed_in_transaction(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
generation: u64,
) -> Outcome<Vec<SqliteRow>, SqliteOperationError> {
if let Err(error) = validate_checked_sql_statement(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.query_unchecked_diagnosed_impl(cx, sql, params, Some(generation))
.await
}
async fn query_unchecked_diagnosed_impl(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
expected_generation: Option<u64>,
) -> Outcome<Vec<SqliteRow>, SqliteOperationError> {
if let Err(error) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
let sql = sql.to_string();
let params = params.to_vec();
self.run_connection_op_inner(
cx,
"sqlite diagnosed query",
SqliteOperation::Step,
expected_generation,
move |conn| {
let params_refs: Vec<&dyn rusqlite::ToSql> = params
.iter()
.map(|value| value as &dyn rusqlite::ToSql)
.collect();
let mut statement = conn.prepare_cached(&sql).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Prepare, error)
})?;
let mut rows = statement.query(params_refs.as_slice()).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Bind, error)
})?;
let mut result = Vec::new();
let mut metadata = None;
while let Some(row) = rows.next().map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Step, error)
})? {
let (column_names, columns) =
metadata.get_or_insert_with(|| sqlite_row_metadata(row));
let converted = sqlite_row_from_rusqlite_row(row, column_names, columns)
.map_err(|error| {
SqliteOperationError::from_legacy(SqliteOperation::Step, error)
})?;
result.push(converted);
}
Ok(result)
},
)
.await
}
pub async fn query_stream<'connection>(
&'connection mut self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<SqliteRowStream<'connection>, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.query_stream_unchecked(cx, sql, params).await
}
pub async fn query_stream_unchecked<'connection>(
&'connection mut self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<SqliteRowStream<'connection>, SqliteError> {
if let Err(err) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(err);
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
let timeout =
crate::database::effective_statement_timeout(cx, self.statement_timeout_override);
if let Some(limit) = timeout {
let remaining_ns = crate::database::remaining_budget(cx)
.map_or_else(|| "none".to_string(), |d| d.as_nanos().to_string());
let base_ms = self.statement_timeout_override.map_or_else(
|| "none".to_string(),
|d| crate::database::statement_timeout_millis(d).to_string(),
);
cx.trace(&format!(
"client.budget_forwarded proto=sqlite base_ms={base_ms} \
remaining_ns={remaining_ns} statement_timeout_ms={} op=row_stream",
crate::database::statement_timeout_millis(limit)
));
}
let sql = sql.to_string();
let params: Vec<SqliteValue> = params.to_vec();
let inner = Arc::clone(&self.inner);
let transaction_state = Arc::clone(&self.transaction_state);
let transaction_generation = Arc::clone(&self.transaction_generation);
let counters = Arc::new(SqliteRowStreamCounters::default());
let worker_counters = Arc::clone(&counters);
let (sender, receiver) = mpsc::channel(SQLITE_ROW_STREAM_CHANNEL_CAPACITY);
let phase = Arc::new(Mutex::new(SqliteConnectionOpPhase::Queued));
let worker_phase = Arc::clone(&phase);
let handle = self.pool.spawn(move || {
const TIMEOUT_PROGRESS_OPS: i32 = 1000;
let deadline_fired = Arc::new(AtomicBool::new(false));
let result = (|| {
let guard = inner.lock();
{
let mut phase = worker_phase.lock();
match *phase {
SqliteConnectionOpPhase::Queued => {
*phase = SqliteConnectionOpPhase::Running;
}
SqliteConnectionOpPhase::CancelRequested => {
*phase = SqliteConnectionOpPhase::Completed;
drop(phase);
drop(guard);
return Ok(());
}
SqliteConnectionOpPhase::Running | SqliteConnectionOpPhase::Completed => {
unreachable!("a SQLite row-stream worker starts exactly once")
}
}
}
let body_result = (|| {
let conn = guard.get()?;
ensure_managed_transaction_open(
conn,
transaction_state.as_ref(),
transaction_generation.as_ref(),
None,
)?;
if let Some(limit) = timeout {
let deadline = std::time::Instant::now() + limit;
let fired = Arc::clone(&deadline_fired);
conn.progress_handler(
TIMEOUT_PROGRESS_OPS,
Some(move || {
if std::time::Instant::now() < deadline {
return false;
}
fired.store(true, Ordering::Release);
true
}),
)
.map_err(|e| {
SqliteError::Sqlite(format!("failed to arm statement timeout: {e}"))
})?;
}
let query_result = (|| {
let params_refs: Vec<&dyn rusqlite::ToSql> =
params.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
let mut stmt = conn
.prepare_cached(&sql)
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let mut rows = stmt
.query(params_refs.as_slice())
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let mut metadata = None;
while let Some(row) = rows
.next()
.map_err(|e| SqliteError::Sqlite(e.to_string()))?
{
worker_counters.rows_stepped.fetch_add(1, Ordering::AcqRel);
let (column_names, columns) =
metadata.get_or_insert_with(|| sqlite_row_metadata(row));
let row = sqlite_row_from_rusqlite_row(row, column_names, columns)?;
if !send_sqlite_stream_message(&sender, &worker_counters, Ok(row)) {
break;
}
}
Ok(())
})();
if timeout.is_some() {
let _ = conn.progress_handler(0, None::<fn() -> bool>);
}
query_result
})();
{
let mut phase = worker_phase.lock();
*phase = SqliteConnectionOpPhase::Completed;
}
drop(guard);
body_result
})();
let result = match (timeout, result) {
(Some(limit), Err(err))
if deadline_fired.load(Ordering::Acquire)
&& sqlite_error_is_interrupt(&err) =>
{
Err(SqliteError::StatementTimeout { limit })
}
(_, result) => result,
};
if let Err(err) = result {
let _ = send_sqlite_stream_message(&sender, &worker_counters, Err(err));
}
});
Outcome::Ok(SqliteRowStream {
receiver,
handle,
counters,
phase,
finished: false,
interrupt: Arc::clone(&self.interrupt),
_connection_lease: PhantomData,
})
}
pub async fn query_row(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Option<SqliteRow>, SqliteError> {
if let Err(err) = validate_checked_sql_statement(sql) {
return Outcome::Err(err);
}
self.query_row_unchecked(cx, sql, params).await
}
pub async fn query_row_unchecked(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Option<SqliteRow>, SqliteError> {
if let Err(err) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(err);
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
let sql = sql.to_string();
let params: Vec<SqliteValue> = params.to_vec();
self.run_connection_op(cx, "sqlite query_row", move |conn| {
let params_refs: Vec<&dyn rusqlite::ToSql> =
params.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
let mut stmt = conn
.prepare_cached(&sql)
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let mut rows = stmt
.query(params_refs.as_slice())
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let row_opt = rows
.next()
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
let result = if let Some(row) = row_opt {
let (column_names, columns) = sqlite_row_metadata(row);
Some(sqlite_row_from_rusqlite_row(row, &column_names, &columns)?)
} else {
None
};
drop(rows);
drop(stmt);
Ok(result)
})
.await
}
pub async fn query_row_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Option<SqliteRow>, SqliteOperationError> {
if let Err(error) = validate_checked_sql_statement(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
self.query_row_unchecked_diagnosed(cx, sql, params).await
}
pub async fn query_row_unchecked_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Option<SqliteRow>, SqliteOperationError> {
if let Err(error) = ensure_unchecked_sql_surface(sql) {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Validation,
error,
));
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
let sql = sql.to_string();
let params = params.to_vec();
self.run_connection_op_diagnosed(
cx,
"sqlite diagnosed query_row",
SqliteOperation::Step,
move |conn| {
let params_refs: Vec<&dyn rusqlite::ToSql> = params
.iter()
.map(|value| value as &dyn rusqlite::ToSql)
.collect();
let mut statement = conn.prepare_cached(&sql).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Prepare, error)
})?;
let mut rows = statement.query(params_refs.as_slice()).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Bind, error)
})?;
let row = rows.next().map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Step, error)
})?;
let result = match row {
Some(row) => {
let (column_names, columns) = sqlite_row_metadata(row);
Some(
sqlite_row_from_rusqlite_row(row, &column_names, &columns).map_err(
|error| {
SqliteOperationError::from_legacy(SqliteOperation::Step, error)
},
)?,
)
}
None => None,
};
Ok(result)
},
)
.await
}
async fn begin_with_sql<'conn>(
&'conn self,
cx: &Cx,
sql: &'static str,
operation: &'static str,
) -> Outcome<SqliteTransaction<'conn>, SqliteError> {
trace_database_transaction(cx, "sqlite", operation, "start");
let mut drop_guard = BeginDropGuard::new(
Arc::clone(&self.transaction_state),
Arc::clone(&self.transaction_generation),
);
let effect = TransactionWorkerEffect::Begin(drop_guard.attempt());
match self.execute_transaction_control(cx, sql, effect).await {
Outcome::Ok(_) => {
let Some(generation) = drop_guard.opened_generation() else {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", operation, "err");
return Outcome::Err(SqliteError::Sqlite(
"managed BEGIN completed without a transaction generation".to_string(),
));
};
let transaction = SqliteTransaction {
conn: self,
finished: false,
obligation: reserve_transaction_obligation(cx),
generation,
};
drop_guard.disarm();
trace_database_transaction(cx, "sqlite", operation, "ok");
Outcome::Ok(transaction)
}
Outcome::Err(e) => {
drop_guard.disarm();
trace_database_transaction(cx, "sqlite", operation, "err");
Outcome::Err(e)
}
Outcome::Cancelled(r) => {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", operation, "cancelled");
Outcome::Cancelled(r)
}
Outcome::Panicked(p) => {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", operation, "panicked");
Outcome::Panicked(p)
}
}
}
async fn begin_with_sql_diagnosed<'conn>(
&'conn self,
cx: &Cx,
sql: &'static str,
trace_operation: &'static str,
) -> Outcome<SqliteTransaction<'conn>, SqliteOperationError> {
trace_database_transaction(cx, "sqlite", trace_operation, "start");
let mut drop_guard = BeginDropGuard::new(
Arc::clone(&self.transaction_state),
Arc::clone(&self.transaction_generation),
);
let effect = TransactionWorkerEffect::Begin(drop_guard.attempt());
match self
.execute_transaction_control_diagnosed(
cx,
sql,
SqliteOperation::TransactionBegin,
effect,
)
.await
{
Outcome::Ok(_) => {
let Some(generation) = drop_guard.opened_generation() else {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", trace_operation, "err");
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::TransactionBegin,
SqliteError::Sqlite(
"managed BEGIN completed without a transaction generation".to_string(),
),
));
};
let transaction = SqliteTransaction {
conn: self,
finished: false,
obligation: reserve_transaction_obligation(cx),
generation,
};
drop_guard.disarm();
trace_database_transaction(cx, "sqlite", trace_operation, "ok");
Outcome::Ok(transaction)
}
Outcome::Err(error) => {
drop_guard.disarm();
trace_database_transaction(cx, "sqlite", trace_operation, "err");
Outcome::Err(error)
}
Outcome::Cancelled(reason) => {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", trace_operation, "cancelled");
Outcome::Cancelled(reason)
}
Outcome::Panicked(payload) => {
drop_guard.abandon();
trace_database_transaction(cx, "sqlite", trace_operation, "panicked");
Outcome::Panicked(payload)
}
}
}
pub async fn begin(&self, cx: &Cx) -> Outcome<SqliteTransaction<'_>, SqliteError> {
self.begin_with_sql(cx, "BEGIN", "begin").await
}
pub async fn begin_diagnosed(
&self,
cx: &Cx,
) -> Outcome<SqliteTransaction<'_>, SqliteOperationError> {
self.begin_with_sql_diagnosed(cx, "BEGIN", "begin_diagnosed")
.await
}
pub async fn begin_immediate(&self, cx: &Cx) -> Outcome<SqliteTransaction<'_>, SqliteError> {
self.begin_with_sql(cx, "BEGIN IMMEDIATE", "begin_immediate")
.await
}
pub async fn begin_immediate_diagnosed(
&self,
cx: &Cx,
) -> Outcome<SqliteTransaction<'_>, SqliteOperationError> {
self.begin_with_sql_diagnosed(cx, "BEGIN IMMEDIATE", "begin_immediate_diagnosed")
.await
}
pub async fn begin_exclusive(&self, cx: &Cx) -> Outcome<SqliteTransaction<'_>, SqliteError> {
self.begin_with_sql(cx, "BEGIN EXCLUSIVE", "begin_exclusive")
.await
}
pub async fn begin_exclusive_diagnosed(
&self,
cx: &Cx,
) -> Outcome<SqliteTransaction<'_>, SqliteOperationError> {
self.begin_with_sql_diagnosed(cx, "BEGIN EXCLUSIVE", "begin_exclusive_diagnosed")
.await
}
pub async fn set_busy_timeout(&self, cx: &Cx, timeout: Duration) -> Outcome<(), SqliteError> {
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.drain_orphaned_transaction(cx).await {
Outcome::Ok(()) => {}
Outcome::Err(e) => return Outcome::Err(e),
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
self.run_connection_op(cx, "sqlite set_busy_timeout", move |conn| {
conn.busy_timeout(timeout)
.map_err(|e| SqliteError::Sqlite(e.to_string()))?;
Ok(())
})
.await
}
pub async fn set_busy_timeout_diagnosed(
&self,
cx: &Cx,
timeout: Duration,
) -> Outcome<(), SqliteOperationError> {
if cx.checkpoint().is_err() {
return Outcome::Cancelled(sqlite_cancelled_reason(cx));
}
match diagnose_legacy_outcome(
SqliteOperation::TransactionRollback,
self.drain_orphaned_transaction(cx).await,
) {
Outcome::Ok(()) => {}
Outcome::Err(error) => return Outcome::Err(error),
Outcome::Cancelled(reason) => return Outcome::Cancelled(reason),
Outcome::Panicked(payload) => return Outcome::Panicked(payload),
}
self.run_connection_op_diagnosed(
cx,
"sqlite diagnosed set_busy_timeout",
SqliteOperation::Configure,
move |conn| {
conn.busy_timeout(timeout).map_err(|error| {
SqliteOperationError::from_rusqlite(SqliteOperation::Configure, error)
})
},
)
.await
}
pub fn close(&self) -> Result<(), SqliteError> {
let mut guard = self.inner.lock();
if let Some(conn) = guard.conn.as_ref() {
let _ =
rollback_orphaned_transaction_mutex_guarded(conn, self.transaction_state.as_ref());
match self.execute_wal_checkpoint_with_retry(conn) {
Ok(()) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::debug!(
"WAL checkpoint completed successfully during close"
);
}
Err(e) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::error!(
error = %e,
"WAL checkpoint failed during connection close - failing close to prevent data loss"
);
return Err(e);
}
}
conn.flush_prepared_statement_cache();
}
*self.transaction_state.lock() = TransactionState::Autocommit;
guard.close();
Ok(())
}
pub fn close_diagnosed(&self) -> Result<(), SqliteOperationError> {
self.close()
.map_err(|error| SqliteOperationError::from_legacy(SqliteOperation::Close, error))
}
pub async fn close_async(&self, cx: &Cx) -> Outcome<(), SqliteError> {
if cx.checkpoint().is_err() {
return Outcome::Cancelled(
cx.cancel_reason()
.unwrap_or_else(|| CancelReason::user("cancelled")),
);
}
match self.execute_wal_checkpoint_async_with_retry(cx).await {
Outcome::Ok(()) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::debug!("Async WAL checkpoint completed successfully");
}
Outcome::Err(e) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::error!(
error = %e,
"Async WAL checkpoint failed during connection close - failing close to prevent data loss"
);
return Outcome::Err(e);
}
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
match self.close_without_checkpoint() {
Ok(()) => Outcome::Ok(()),
Err(e) => Outcome::Err(e),
}
}
pub async fn close_async_diagnosed(&self, cx: &Cx) -> Outcome<(), SqliteOperationError> {
diagnose_legacy_outcome(SqliteOperation::Close, self.close_async(cx).await)
}
#[must_use]
pub fn is_open(&self) -> bool {
self.inner.lock().conn.is_some()
}
fn execute_wal_checkpoint_with_retry(
&self,
conn: &rusqlite::Connection,
) -> Result<(), SqliteError> {
const MAX_RETRY_ATTEMPTS: u32 = 3;
const RETRY_DELAY_MS: u64 = 50;
for attempt in 1..=MAX_RETRY_ATTEMPTS {
match self.execute_single_wal_checkpoint(conn) {
Ok(()) => {
#[cfg(feature = "tracing-integration")]
if attempt > 1 {
crate::tracing_compat::info!(
attempt = attempt,
"WAL checkpoint succeeded after retry"
);
}
return Ok(());
}
Err(e) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::warn!(
error = %e,
attempt = attempt,
max_attempts = MAX_RETRY_ATTEMPTS,
"WAL checkpoint attempt failed"
);
if attempt == MAX_RETRY_ATTEMPTS {
return Err(SqliteError::WalCheckpointFailed(format!(
"WAL checkpoint failed after {} attempts: {}",
MAX_RETRY_ATTEMPTS, e
)));
}
std::thread::sleep(std::time::Duration::from_millis(
RETRY_DELAY_MS * attempt as u64,
));
}
}
}
unreachable!("Loop should always return within max attempts")
}
fn execute_single_wal_checkpoint(
&self,
conn: &rusqlite::Connection,
) -> Result<(), rusqlite::Error> {
conn.execute_batch("PRAGMA wal_checkpoint(RESTART)")?;
let mut stmt = conn.prepare_cached("PRAGMA wal_checkpoint")?;
let result: (i32, i32, i32) =
stmt.query_row([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
let (busy, log_pages, checkpointed_pages) = result;
if busy != 0 {
return Err(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
Some("WAL checkpoint blocked by concurrent readers".to_string()),
));
}
if log_pages > 0 && checkpointed_pages == 0 {
return Err(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
Some(format!(
"WAL checkpoint failed - {} pages remain in WAL",
log_pages
)),
));
}
Ok(())
}
async fn execute_wal_checkpoint_async_with_retry(&self, cx: &Cx) -> Outcome<(), SqliteError> {
const MAX_RETRY_ATTEMPTS: u32 = 3;
for attempt in 1..=MAX_RETRY_ATTEMPTS {
match self.execute_wal_checkpoint_async_single(cx).await {
Outcome::Ok(()) => {
#[cfg(feature = "tracing-integration")]
if attempt > 1 {
crate::tracing_compat::info!(
attempt = attempt,
"Async WAL checkpoint succeeded after retry"
);
}
return Outcome::Ok(());
}
Outcome::Err(e) => {
#[cfg(feature = "tracing-integration")]
crate::tracing_compat::warn!(
error = %e,
attempt = attempt,
max_attempts = MAX_RETRY_ATTEMPTS,
"Async WAL checkpoint attempt failed"
);
if attempt == MAX_RETRY_ATTEMPTS {
return Outcome::Err(SqliteError::WalCheckpointFailed(format!(
"Async WAL checkpoint failed after {} attempts: {}",
MAX_RETRY_ATTEMPTS, e
)));
}
let retry_delay = Duration::from_millis(50 * u64::from(attempt));
if let Err(reason) = sqlite_wait_retry_delay(cx, retry_delay).await {
return Outcome::Cancelled(reason);
}
}
Outcome::Cancelled(r) => return Outcome::Cancelled(r),
Outcome::Panicked(p) => return Outcome::Panicked(p),
}
}
unreachable!("Loop should always return within max attempts")
}
async fn execute_wal_checkpoint_async_single(&self, cx: &Cx) -> Outcome<(), SqliteError> {
match self
.execute_batch_unchecked(cx, "PRAGMA wal_checkpoint(RESTART)")
.await
{
Outcome::Ok(()) => {
match self.query_unchecked(cx, "PRAGMA wal_checkpoint", &[]).await {
Outcome::Ok(rows) => {
if let Some(row) = rows.first() {
let busy = match wal_checkpoint_i64(row, "busy") {
Ok(value) => value,
Err(err) => return Outcome::Err(err),
};
let log_pages = match wal_checkpoint_i64(row, "log") {
Ok(value) => value,
Err(err) => return Outcome::Err(err),
};
let checkpointed_pages = match wal_checkpoint_i64(row, "checkpointed") {
Ok(value) => value,
Err(err) => return Outcome::Err(err),
};
if busy != 0 {
return Outcome::Err(SqliteError::WalCheckpointFailed(
"WAL checkpoint blocked by concurrent readers".to_string(),
));
}
if log_pages > 0 && checkpointed_pages == 0 {
return Outcome::Err(SqliteError::WalCheckpointFailed(format!(
"WAL checkpoint failed - {} pages remain in WAL",
log_pages
)));
}
}
Outcome::Ok(())
}
Outcome::Err(e) => Outcome::Err(e),
Outcome::Cancelled(r) => Outcome::Cancelled(r),
Outcome::Panicked(p) => Outcome::Panicked(p),
}
}
Outcome::Err(e) => Outcome::Err(e),
Outcome::Cancelled(r) => Outcome::Cancelled(r),
Outcome::Panicked(p) => Outcome::Panicked(p),
}
}
fn close_without_checkpoint(&self) -> Result<(), SqliteError> {
let mut guard = self.inner.lock();
if let Some(conn) = guard.conn.as_ref() {
let _ =
rollback_orphaned_transaction_mutex_guarded(conn, self.transaction_state.as_ref());
conn.flush_prepared_statement_cache();
}
*self.transaction_state.lock() = TransactionState::Autocommit;
guard.close();
Ok(())
}
}
pub struct SqliteTransaction<'a> {
conn: &'a SqliteConnection,
finished: bool,
obligation: Option<ObligationToken<TransactionKind>>,
generation: u64,
}
fn reserve_transaction_obligation(cx: &Cx) -> Option<ObligationToken<TransactionKind>> {
let region = cx.region_id();
if region.as_u64() == 0 {
None
} else {
Some(ObligationToken::reserve("db-transaction:sqlite", region))
}
}
impl SqliteTransaction<'_> {
#[must_use]
pub(crate) fn requires_rollback_before_commit(&self) -> bool {
let state = self.conn.transaction_state.lock();
self.conn.transaction_generation.load(Ordering::Acquire) == self.generation
&& *state == TransactionState::NeedsRollback
}
pub(crate) fn poison_for_rollback(&self) {
let mut state = self.conn.transaction_state.lock();
if self.conn.transaction_generation.load(Ordering::Acquire) == self.generation {
*state = TransactionState::NeedsRollback;
}
}
pub async fn commit(mut self, cx: &Cx) -> Outcome<(), SqliteError> {
if self.finished {
trace_database_transaction(cx, "sqlite", "commit", "already_finished");
return Outcome::Err(SqliteError::TransactionFinished);
}
trace_database_transaction(cx, "sqlite", "commit", "start");
let finish_effect = TransactionFinishEffect::new(
Arc::clone(&self.conn.transaction_state),
Arc::clone(&self.conn.transaction_generation),
self.generation,
TransactionFinishKind::Commit,
self.obligation.take(),
);
let effect = TransactionWorkerEffect::Finish(finish_effect);
match self
.conn
.execute_transaction_control(cx, "COMMIT", effect)
.await
{
Outcome::Ok(_) => {
self.finished = true;
trace_database_transaction(cx, "sqlite", "commit", "ok");
Outcome::Ok(())
}
Outcome::Err(e) => {
trace_database_transaction(cx, "sqlite", "commit", "err");
Outcome::Err(e)
}
Outcome::Cancelled(r) => {
trace_database_transaction(cx, "sqlite", "commit", "cancelled");
Outcome::Cancelled(r)
}
Outcome::Panicked(p) => {
trace_database_transaction(cx, "sqlite", "commit", "panicked");
Outcome::Panicked(p)
}
}
}
pub async fn commit_diagnosed(mut self, cx: &Cx) -> Outcome<(), SqliteOperationError> {
if self.finished {
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "already_finished");
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::TransactionCommit,
SqliteError::TransactionFinished,
));
}
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "start");
let finish_effect = TransactionFinishEffect::new(
Arc::clone(&self.conn.transaction_state),
Arc::clone(&self.conn.transaction_generation),
self.generation,
TransactionFinishKind::Commit,
self.obligation.take(),
);
let effect = TransactionWorkerEffect::Finish(finish_effect);
match self
.conn
.execute_transaction_control_diagnosed(
cx,
"COMMIT",
SqliteOperation::TransactionCommit,
effect,
)
.await
{
Outcome::Ok(_) => {
self.finished = true;
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "ok");
Outcome::Ok(())
}
Outcome::Err(error) => {
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "err");
Outcome::Err(error)
}
Outcome::Cancelled(reason) => {
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "cancelled");
Outcome::Cancelled(reason)
}
Outcome::Panicked(payload) => {
trace_database_transaction(cx, "sqlite", "commit_diagnosed", "panicked");
Outcome::Panicked(payload)
}
}
}
pub async fn rollback(mut self, cx: &Cx) -> Outcome<(), SqliteError> {
if self.finished {
trace_database_transaction(cx, "sqlite", "rollback", "already_finished");
return Outcome::Err(SqliteError::TransactionFinished);
}
trace_database_transaction(cx, "sqlite", "rollback", "start");
let finish_effect = TransactionFinishEffect::new(
Arc::clone(&self.conn.transaction_state),
Arc::clone(&self.conn.transaction_generation),
self.generation,
TransactionFinishKind::Rollback,
self.obligation.take(),
);
let effect = TransactionWorkerEffect::Finish(finish_effect);
match self
.conn
.execute_transaction_control(cx, "ROLLBACK", effect)
.await
{
Outcome::Ok(_) => {
self.finished = true;
trace_database_transaction(cx, "sqlite", "rollback", "ok");
Outcome::Ok(())
}
Outcome::Err(e) => {
trace_database_transaction(cx, "sqlite", "rollback", "err");
Outcome::Err(e)
}
Outcome::Cancelled(r) => {
trace_database_transaction(cx, "sqlite", "rollback", "cancelled");
Outcome::Cancelled(r)
}
Outcome::Panicked(p) => {
trace_database_transaction(cx, "sqlite", "rollback", "panicked");
Outcome::Panicked(p)
}
}
}
pub async fn rollback_diagnosed(mut self, cx: &Cx) -> Outcome<(), SqliteOperationError> {
if self.finished {
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "already_finished");
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::TransactionRollback,
SqliteError::TransactionFinished,
));
}
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "start");
let finish_effect = TransactionFinishEffect::new(
Arc::clone(&self.conn.transaction_state),
Arc::clone(&self.conn.transaction_generation),
self.generation,
TransactionFinishKind::Rollback,
self.obligation.take(),
);
let effect = TransactionWorkerEffect::Finish(finish_effect);
match self
.conn
.execute_transaction_control_diagnosed(
cx,
"ROLLBACK",
SqliteOperation::TransactionRollback,
effect,
)
.await
{
Outcome::Ok(_) => {
self.finished = true;
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "ok");
Outcome::Ok(())
}
Outcome::Err(error) => {
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "err");
Outcome::Err(error)
}
Outcome::Cancelled(reason) => {
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "cancelled");
Outcome::Cancelled(reason)
}
Outcome::Panicked(payload) => {
trace_database_transaction(cx, "sqlite", "rollback_diagnosed", "panicked");
Outcome::Panicked(payload)
}
}
}
pub async fn execute(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteError> {
if self.finished {
return Outcome::Err(SqliteError::TransactionFinished);
}
self.conn
.execute_in_transaction(cx, sql, params, self.generation)
.await
}
pub async fn execute_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteOperationError> {
if self.finished {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Step,
SqliteError::TransactionFinished,
));
}
self.conn
.execute_diagnosed_in_transaction(cx, sql, params, self.generation)
.await
}
pub(crate) async fn execute_unchecked(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<u64, SqliteError> {
if self.finished {
return Outcome::Err(SqliteError::TransactionFinished);
}
self.conn
.execute_unchecked_in_transaction(cx, sql, params, self.generation)
.await
}
pub async fn query(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteError> {
if self.finished {
return Outcome::Err(SqliteError::TransactionFinished);
}
self.conn
.query_in_transaction(cx, sql, params, self.generation)
.await
}
pub async fn query_diagnosed(
&self,
cx: &Cx,
sql: &str,
params: &[SqliteValue],
) -> Outcome<Vec<SqliteRow>, SqliteOperationError> {
if self.finished {
return Outcome::Err(SqliteOperationError::from_legacy(
SqliteOperation::Step,
SqliteError::TransactionFinished,
));
}
self.conn
.query_diagnosed_in_transaction(cx, sql, params, self.generation)
.await
}
}
impl Drop for SqliteTransaction<'_> {
fn drop(&mut self) {
if let Some(token) = self.obligation.take() {
let _ = token.abort();
}
if !self.finished {
self.poison_for_rollback();
self.conn
.schedule_dropped_transaction_rollback(self.generation);
}
}
}
fn column_name_or_index(column_names: &[String], idx: usize) -> String {
column_names
.get(idx)
.cloned()
.unwrap_or_else(|| format!("index {idx}"))
}
fn convert_value(
value: rusqlite::types::ValueRef<'_>,
column: &str,
) -> Result<SqliteValue, SqliteError> {
match value {
rusqlite::types::ValueRef::Null => Ok(SqliteValue::Null),
rusqlite::types::ValueRef::Integer(v) => Ok(SqliteValue::Integer(v)),
rusqlite::types::ValueRef::Real(v) => Ok(SqliteValue::Real(v)),
rusqlite::types::ValueRef::Text(v) => {
let text =
std::str::from_utf8(v).map_err(|source| SqliteError::InvalidTextEncoding {
column: column.to_string(),
source,
})?;
Ok(SqliteValue::Text(text.to_string()))
}
rusqlite::types::ValueRef::Blob(v) => Ok(SqliteValue::Blob(v.to_vec())),
}
}
impl rusqlite::ToSql for SqliteValue {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
use rusqlite::types::ToSqlOutput;
match self {
Self::Null => Ok(ToSqlOutput::Owned(rusqlite::types::Value::Null)),
Self::Integer(v) => Ok(ToSqlOutput::Owned(rusqlite::types::Value::Integer(*v))),
Self::Real(v) => Ok(ToSqlOutput::Owned(rusqlite::types::Value::Real(*v))),
Self::Text(v) => Ok(ToSqlOutput::Owned(rusqlite::types::Value::Text(v.clone()))),
Self::Blob(v) => Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(v.clone()))),
}
}
}
#[cfg(test)]
include!("sqlite_tests.rs");