Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ Databases must be registered on the Rust side with a stable key before they can
- Parent directory auto-creation during registration validation for file paths.
- CI check that committed `api-iife.js` matches a fresh Rollup build.

### Changed

#### `close` aborts active transactions before closing

`Database.close()` (IPC `plugin:sqlite|close`), `Database.close_all()` (IPC `plugin:sqlite|close_all`), and the Rust [`Connection::close`](src/lib.rs) API now roll back or cancel in-flight transactions before closing connection pools.

- **Interruptible transactions** (`beginInterruptibleTransaction`): explicitly rolled back via `ROLLBACK` before the pool is closed.
- **Regular transactions** (`executeTransaction`): the in-flight task is aborted and awaited so pooled connections are released before the pool closes.

Previously, `close` only aborted active subscriptions; open transactions could block a clean shutdown or leave uncommitted work on pooled connections. The same cleanup logic is available to Rust callers through [`close_database`](src/lib.rs) and [`close_all_loaded_databases`](src/lib.rs).

Transaction cleanup failures propagate as errors rather than being logged and ignored, so a successful close indicates the database file is safe to delete or recreate.

### Fixed

- Regular transaction cleanup no longer uses string-prefix matching on database keys, which could abort transactions belonging to a different registered database when keys contain `:` (for example `:memory:` or `a` vs `a:b`).
- Transaction cleanup attempts all rollbacks/aborts before returning, rather than stopping at the first failure.
- `close` / `close_all` now attempt pool teardown even when transaction cleanup fails, avoiding a half-closed state where subscriptions are gone but the pool remains loaded.
- `close` / `close_all` are bounded by a 5-second timeout.
- Regular transactions cancelled by `close` now return `TRANSACTION_CANCELLED` instead of a generic "task dropped" error; task panics remain distinct.
- Transaction cleanup now returns `TRANSACTION_CLEANUP_FAILED` with all collected errors when multiple rollbacks or aborts fail, instead of discarding earlier failures.
- `load()` no longer hangs forever for databases registered without a migrator. Migration state is only tracked when a migrator is provided; otherwise `await_migrations` proceeds immediately.
- Regenerated `api-iife.js` so all IPC calls use `dbKey` consistently.
48 changes: 48 additions & 0 deletions crates/sqlx-sqlite-toolkit/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ pub enum Error {
#[error("transaction timed out for database: {0}")]
TransactionTimedOut(String),

/// Transaction was cancelled because the database was closed.
#[error("transaction cancelled because database was closed: {0}")]
TransactionCancelled(String),

/// Multiple errors occurred during transaction cleanup.
#[error("transaction cleanup failed with {} error(s): {}", .0.len(), Error::format_cleanup_errors(.0))]
TransactionCleanupFailed(Vec<Error>),

/// Error from the observer (change notifications).
#[cfg(feature = "observer")]
#[error(transparent)]
Expand Down Expand Up @@ -100,6 +108,24 @@ pub enum Error {
}

impl Error {
/// Format multiple cleanup errors for display.
fn format_cleanup_errors(errors: &[Error]) -> String {
errors
.iter()
.enumerate()
.map(|(index, err)| format!("[{index}] {err}"))
.collect::<Vec<_>>()
.join("; ")
}

/// Returns individual errors when this is a cleanup aggregate.
pub fn cleanup_errors(&self) -> Option<&[Error]> {
match self {
Self::TransactionCleanupFailed(errors) => Some(errors),
_ => None,
}
}

/// Extract a structured error code from the error type.
///
/// This provides machine-readable error codes for error handling.
Expand All @@ -120,6 +146,8 @@ impl Error {
Error::NoActiveTransaction(_) => "NO_ACTIVE_TRANSACTION".to_string(),
Error::InvalidTransactionToken => "INVALID_TRANSACTION_TOKEN".to_string(),
Error::TransactionTimedOut(_) => "TRANSACTION_TIMED_OUT".to_string(),
Error::TransactionCancelled(_) => "TRANSACTION_CANCELLED".to_string(),
Error::TransactionCleanupFailed(_) => "TRANSACTION_CLEANUP_FAILED".to_string(),
#[cfg(feature = "observer")]
Error::Observer(_) => "OBSERVER_ERROR".to_string(),
Error::Io(_) => "IO_ERROR".to_string(),
Expand Down Expand Up @@ -206,6 +234,26 @@ mod tests {
assert!(err.to_string().contains("test.db"));
}

#[test]
fn test_error_code_transaction_cancelled() {
let err = Error::TransactionCancelled("MAIN".into());
assert_eq!(err.error_code(), "TRANSACTION_CANCELLED");
assert!(err.to_string().contains("MAIN"));
}

#[test]
fn test_error_code_transaction_cleanup_failed() {
let err = Error::TransactionCleanupFailed(vec![
Error::Other("first failure".into()),
Error::Other("second failure".into()),
]);
assert_eq!(err.error_code(), "TRANSACTION_CLEANUP_FAILED");
assert!(err.to_string().contains("2 error(s)"));
assert!(err.to_string().contains("first failure"));
assert!(err.to_string().contains("second failure"));
assert_eq!(err.cleanup_errors().unwrap().len(), 2);
}

#[test]
fn test_error_code_other() {
let err = Error::Other("something went wrong".into());
Expand Down
2 changes: 1 addition & 1 deletion crates/sqlx-sqlite-toolkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub use error::{Error, Result};
pub use pagination::{KeysetColumn, KeysetPage, SortDirection};
pub use transactions::{
ActiveInterruptibleTransaction, ActiveInterruptibleTransactions, ActiveRegularTransactions,
Statement, TransactionWriter, cleanup_all_transactions,
Statement, TransactionWriter, cleanup_all_transactions, cleanup_transactions_for_db,
};
pub use wrapper::{
DatabaseWrapper, InterruptibleTransaction, InterruptibleTransactionBuilder,
Expand Down
Loading
Loading