diff --git a/Cargo.lock b/Cargo.lock index ace2430ce..69d059d0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4828,6 +4828,8 @@ dependencies = [ "tempfile", "thiserror 2.0.16", "tokio", + "tracing", + "tracing-subscriber", "turso_core", ] diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index d799b5320..42bce00cd 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -19,6 +19,8 @@ tracing_release = ["turso_core/tracing_release"] [dependencies] turso_core = { workspace = true, features = ["io_uring"] } thiserror = { workspace = true } +tracing-subscriber.workspace = true +tracing.workspace = true [dev-dependencies] tempfile = { workspace = true } diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs index 5d87ad5f0..94a6556c9 100644 --- a/bindings/rust/src/lib.rs +++ b/bindings/rust/src/lib.rs @@ -596,7 +596,11 @@ impl Statement { pub async fn query_row(&mut self, params: impl IntoParams) -> Result { let mut rows = self.query(params).await?; - rows.next().await?.ok_or(Error::QueryReturnedNoRows) + let first_row = rows.next().await?.ok_or(Error::QueryReturnedNoRows)?; + // Discard remaining rows so that the statement is executed to completion + // Otherwise Drop of the statement will cause transaction rollback + while rows.next().await?.is_some() {} + Ok(first_row) } } diff --git a/bindings/rust/src/transaction.rs b/bindings/rust/src/transaction.rs index b68cc1fab..6da5c133d 100644 --- a/bindings/rust/src/transaction.rs +++ b/bindings/rust/src/transaction.rs @@ -329,6 +329,7 @@ mod test { #[tokio::test] async fn test_drop() -> Result<()> { + let _ = tracing_subscriber::fmt::try_init(); let mut conn = checked_memory_handle().await?; { let tx = conn.transaction().await?; diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index ed8190092..be44dd0f7 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1002,6 +1002,7 @@ impl Program { } } else { pager.rollback_tx(&self.connection); + self.connection.auto_commit.store(true, Ordering::SeqCst); } self.connection.set_tx_state(TransactionState::None); } diff --git a/tests/integration/query_processing/test_transactions.rs b/tests/integration/query_processing/test_transactions.rs index a96235153..36df15f9d 100644 --- a/tests/integration/query_processing/test_transactions.rs +++ b/tests/integration/query_processing/test_transactions.rs @@ -176,6 +176,38 @@ fn test_transaction_visibility() { } } +#[test] +/// Currently, our default conflict resolution strategy is ROLLBACK, which ends the transaction. +/// In SQLite, the default is ABORT, which rolls back the current statement but allows the transaction to continue. +/// We should migrate to default ABORT once we support subtransactions. +fn test_constraint_error_aborts_transaction() { + let tmp_db = TempDatabase::new("test_constraint_error_aborts_transaction.db", true); + let conn = tmp_db.connect_limbo(); + + // Create table succeeds + conn.execute("CREATE TABLE t (a INTEGER PRIMARY KEY)") + .unwrap(); + + // Begin succeeds + conn.execute("BEGIN").unwrap(); + + // First insert succeeds + conn.execute("INSERT INTO t VALUES (1),(2)").unwrap(); + + // Second insert fails due to UNIQUE constraint + let result = conn.execute("INSERT INTO t VALUES (2),(3)"); + assert!(matches!(result, Err(LimboError::Constraint(_)))); + + // Commit fails because the transaction was aborted by the constraint error + let result = conn.execute("COMMIT"); + assert!(matches!(result, Err(LimboError::TxError(_)))); + + // Make sure table is empty + let stmt = conn.query("SELECT COUNT(*) FROM t").unwrap().unwrap(); + let row = helper_read_single_row(stmt); + assert_eq!(row, vec![Value::Integer(0)]); +} + #[test] fn test_mvcc_transactions_autocommit() { let tmp_db = TempDatabase::new_with_opts(