Merge 'Fix deferred FK violations check before committing to WAL' from Jussi Saurio

DEFERRED was a bit too deferred - it allowed the dirty pages to be
written out to WAL before checking for violations, resulting in the
violations effectively being committed even though the transaction ended
up aborting
Closes #3784

Closes #3785
This commit is contained in:
Pekka Enberg
2025-10-20 14:51:25 +03:00
committed by GitHub
2 changed files with 36 additions and 5 deletions

View File

@@ -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)
}

View File

@@ -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(