diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 962f1b3bc..974538de0 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -2165,21 +2165,21 @@ pub fn halt( let auto_commit = program.connection.auto_commit.load(Ordering::SeqCst); tracing::trace!("halt(auto_commit={})", auto_commit); if auto_commit { - let res = program.commit_txn(pager.clone(), state, mv_store, false); - if res.is_ok() - && program.connection.foreign_keys_enabled() + // In autocommit mode, a statement that leaves deferred violations must fail here. + if program.connection.foreign_keys_enabled() && program .connection .fk_deferred_violations .swap(0, Ordering::AcqRel) > 0 { - // In autocommit mode, a statement that leaves deferred violations must fail here. return Err(LimboError::Constraint( "foreign key constraint failed".to_string(), )); } - res.map(Into::into) + program + .commit_txn(pager.clone(), state, mv_store, false) + .map(Into::into) } else { Ok(InsnFunctionStepResult::Done) } diff --git a/tests/integration/query_processing/test_transactions.rs b/tests/integration/query_processing/test_transactions.rs index 36df15f9d..89f102ae5 100644 --- a/tests/integration/query_processing/test_transactions.rs +++ b/tests/integration/query_processing/test_transactions.rs @@ -208,6 +208,37 @@ fn test_constraint_error_aborts_transaction() { assert_eq!(row, vec![Value::Integer(0)]); } +#[test] +/// Regression test for https://github.com/tursodatabase/turso/issues/3784 where dirty pages +/// were flushed to WAL _before_ deferred FK violations were checked. This resulted in the +/// violations being persisted to the database, even though the transaction was aborted. +/// This test ensures that dirty pages are not flushed to WAL until after deferred violations are checked. +fn test_deferred_fk_violation_rollback_in_autocommit() { + let tmp_db = TempDatabase::new("test_deferred_fk_violation_rollback.db", true); + let conn = tmp_db.connect_limbo(); + + // Enable foreign keys + conn.execute("PRAGMA foreign_keys = ON").unwrap(); + + // Create parent and child tables with deferred FK constraint + conn.execute("CREATE TABLE parent(a PRIMARY KEY)").unwrap(); + conn.execute("CREATE TABLE child(a, b, FOREIGN KEY(b) REFERENCES parent(a) DEFERRABLE INITIALLY DEFERRED)") + .unwrap(); + + // This insert should fail because parent(1) doesn't exist + // and the deferred FK violation should be caught at statement end in autocommit mode + let result = conn.execute("INSERT INTO child VALUES(1,1)"); + assert!(matches!(result, Err(LimboError::Constraint(_)))); + + // Do a truncating checkpoint + conn.execute("PRAGMA wal_checkpoint(TRUNCATE)").unwrap(); + + // Verify that the child table is empty (the insert was rolled back) + let stmt = conn.query("SELECT COUNT(*) FROM child").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(