use vibesql_ast::Statement;
use vibesql_storage::Database;
use vibesql_types::SqlValue;
fn exec(db: &mut Database, sql: &str) -> Result<String, String> {
let stmt =
vibesql_parser::Parser::parse_sql(sql).map_err(|e| format!("Parse error: {:?}", e))?;
match stmt {
Statement::CreateTable(s) => {
crate::CreateTableExecutor::execute(&s, db).map_err(|e| e.to_string())
}
Statement::CreateTrigger(s) => {
crate::TriggerExecutor::create_trigger(db, &s).map_err(|e| e.to_string())
}
Statement::Insert(s) => crate::InsertExecutor::execute(db, &s)
.map(|count| format!("{} row(s) inserted", count))
.map_err(|e| e.to_string()),
Statement::Delete(s) => crate::delete::DeleteExecutor::execute(&s, db)
.map(|count| format!("{} row(s) deleted", count))
.map_err(|e| e.to_string()),
Statement::Update(s) => crate::update::UpdateExecutor::execute(&s, db)
.map(|count| format!("{} row(s) updated", count))
.map_err(|e| e.to_string()),
other => Err(format!("Unsupported statement type: {:?}", other)),
}
}
fn query_col(db: &Database, sql: &str) -> Vec<SqlValue> {
let stmt = vibesql_parser::Parser::parse_sql(sql).expect("parse select");
let select = match stmt {
Statement::Select(s) => s,
other => panic!("expected SELECT, got {:?}", other),
};
let result = crate::SelectExecutor::new(db).execute_with_columns(&select).expect("run select");
result.rows.iter().map(|r| r.values[0].clone()).collect()
}
fn audit_msgs(db: &Database) -> Vec<String> {
query_col(db, "SELECT msg FROM audit ORDER BY seq")
.into_iter()
.map(|v| match v {
SqlValue::Varchar(s) => s.to_string(),
other => format!("{:?}", other),
})
.collect()
}
fn fresh_db() -> Database {
let mut db = Database::new();
db.set_foreign_keys_enabled(true);
db
}
fn create_audit(db: &mut Database) {
exec(db, "CREATE TABLE audit (seq INTEGER PRIMARY KEY AUTOINCREMENT, msg TEXT)").unwrap();
}
#[test]
fn cascade_delete_fires_child_after_delete_trigger() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_ad AFTER DELETE ON child BEGIN INSERT INTO audit(msg) VALUES('deleted ' || OLD.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1), (2)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1), (20, 2)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(audit_msgs(&db), vec!["deleted 10", "deleted 11"]);
assert_eq!(query_col(&db, "SELECT id FROM child ORDER BY id"), vec![SqlValue::Integer(20)]);
}
#[test]
fn cascade_delete_fires_before_and_after_child_triggers_in_order() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_bd BEFORE DELETE ON child BEGIN INSERT INTO audit(msg) VALUES('before ' || OLD.id); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_ad AFTER DELETE ON child BEGIN INSERT INTO audit(msg) VALUES('after ' || OLD.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(
audit_msgs(&db),
vec!["before 10", "after 10", "before 11", "after 11"]
);
}
#[test]
fn cascade_delete_child_before_raise_abort_rolls_back_statement() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_guard BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(ABORT, 'no delete 11'); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(err.contains("no delete 11"), "unexpected error: {err}");
assert!(db.in_transaction(), "ABORT must keep the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
}
#[test]
fn cascade_delete_child_before_raise_abort_rolls_back_in_auto_commit() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_guard BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(ABORT, 'no delete 11'); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(err.contains("no delete 11"), "unexpected error: {err}");
assert!(!db.in_transaction(), "auto-commit must not leak an open transaction");
assert_eq!(
query_col(&db, "SELECT id FROM parent"),
vec![SqlValue::Integer(1)]
);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
}
#[test]
fn cascade_update_child_before_raise_abort_rolls_back_in_auto_commit() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON UPDATE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_guard BEFORE UPDATE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(ABORT, 'no update 11'); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
let err = exec(&mut db, "UPDATE parent SET id = 99 WHERE id = 1").unwrap_err();
assert!(err.contains("no update 11"), "unexpected error: {err}");
assert!(!db.in_transaction());
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT pid FROM child ORDER BY id"),
vec![SqlValue::Integer(1), SqlValue::Integer(1)]
);
}
#[test]
fn cascade_update_fires_child_update_triggers() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON UPDATE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_bu BEFORE UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('before ' || OLD.id); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_au AFTER UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('after ' || OLD.pid || '->' || NEW.pid || ' id=' || NEW.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
exec(&mut db, "UPDATE parent SET id = 99 WHERE id = 1").unwrap();
assert_eq!(
audit_msgs(&db),
vec!["before 10", "after 1->99 id=10", "before 11", "after 1->99 id=11"]
);
assert_eq!(
query_col(&db, "SELECT pid FROM child ORDER BY id"),
vec![SqlValue::Integer(99), SqlValue::Integer(99)]
);
}
#[test]
fn cascade_update_child_before_raise_abort_rolls_back_statement() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON UPDATE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_guard BEFORE UPDATE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(ABORT, 'no update 11'); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
let err = exec(&mut db, "UPDATE parent SET id = 99 WHERE id = 1").unwrap_err();
assert!(err.contains("no update 11"), "unexpected error: {err}");
assert!(db.in_transaction(), "ABORT must keep the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT pid FROM child ORDER BY id"),
vec![SqlValue::Integer(1), SqlValue::Integer(1)]
);
}
#[test]
fn multi_level_cascade_delete_fires_triggers_at_each_level() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE a (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE b (id INTEGER PRIMARY KEY, aid INTEGER REFERENCES a(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TABLE c (id INTEGER PRIMARY KEY, bid INTEGER REFERENCES b(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER b_bd BEFORE DELETE ON b BEGIN INSERT INTO audit(msg) VALUES('b before ' || OLD.id); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER b_ad AFTER DELETE ON b BEGIN INSERT INTO audit(msg) VALUES('b after ' || OLD.id); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER c_bd BEFORE DELETE ON c BEGIN INSERT INTO audit(msg) VALUES('c before ' || OLD.id); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER c_ad AFTER DELETE ON c BEGIN INSERT INTO audit(msg) VALUES('c after ' || OLD.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO a VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO b VALUES (100, 1)").unwrap();
exec(&mut db, "INSERT INTO c VALUES (1000, 100)").unwrap();
exec(&mut db, "DELETE FROM a WHERE id = 1").unwrap();
assert_eq!(
audit_msgs(&db),
vec!["b before 100", "c before 1000", "c after 1000", "b after 100"]
);
assert_eq!(query_col(&db, "SELECT id FROM b"), Vec::<SqlValue>::new());
assert_eq!(query_col(&db, "SELECT id FROM c"), Vec::<SqlValue>::new());
}
#[test]
fn cascade_delete_raise_ignore_orphan_trips_statement_end_fk_check() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(
err.contains("FOREIGN KEY constraint failed"),
"expected FK violation, got: {err}"
);
assert!(!db.in_transaction(), "auto-commit must not leak an open transaction");
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
}
#[test]
fn cascade_update_raise_ignore_orphan_trips_statement_end_fk_check() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON UPDATE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE UPDATE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
let err = exec(&mut db, "UPDATE parent SET id = 2 WHERE id = 1").unwrap_err();
assert!(
err.contains("FOREIGN KEY constraint failed"),
"expected FK violation, got: {err}"
);
assert!(!db.in_transaction());
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT pid FROM child ORDER BY id"),
vec![SqlValue::Integer(1), SqlValue::Integer(1)]
);
}
#[test]
fn cascade_delete_raise_ignore_not_fired_leaves_consistent_state() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE DELETE ON child WHEN OLD.id = 999 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(query_col(&db, "SELECT id FROM parent"), Vec::<SqlValue>::new());
assert_eq!(query_col(&db, "SELECT id FROM child ORDER BY id"), Vec::<SqlValue>::new());
}
#[test]
fn cascade_delete_raise_ignore_deferred_fk_defers_orphan_to_commit() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(query_col(&db, "SELECT id FROM parent"), Vec::<SqlValue>::new());
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(11)]
);
let commit = crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db);
assert!(commit.is_err(), "COMMIT must fail on the deferred orphan");
let msg = format!("{:?}", commit.unwrap_err());
assert!(
msg.contains("FOREIGN KEY constraint failed"),
"expected FK violation at COMMIT, got: {msg}"
);
}
#[test]
fn cascade_delete_without_child_triggers_unaffected() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1), (2)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1), (20, 2)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(query_col(&db, "SELECT id FROM child ORDER BY id"), vec![SqlValue::Integer(20)]);
}
#[test]
fn set_null_fires_child_before_and_after_update_triggers() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE SET NULL)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_bu BEFORE UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('before update ' || OLD.id || ' old.pid=' || IFNULL(OLD.pid,'NULL') || ' new.pid=' || IFNULL(NEW.pid,'NULL')); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_au AFTER UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('after update ' || OLD.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(
audit_msgs(&db),
vec![
"before update 10 old.pid=1 new.pid=NULL",
"after update 10",
"before update 11 old.pid=1 new.pid=NULL",
"after update 11",
]
);
assert_eq!(
query_col(&db, "SELECT IFNULL(pid, -1) FROM child ORDER BY id"),
vec![SqlValue::Integer(-1), SqlValue::Integer(-1)]
);
}
#[test]
fn set_default_fires_child_before_and_after_update_triggers() {
let mut db = fresh_db();
create_audit(&mut db);
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER DEFAULT 99 REFERENCES parent(id) ON DELETE SET DEFAULT)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_bu BEFORE UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('bu ' || OLD.id || ' new.pid=' || IFNULL(NEW.pid,'NULL')); END",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_au AFTER UPDATE ON child BEGIN INSERT INTO audit(msg) VALUES('au ' || OLD.id); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1), (99)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(audit_msgs(&db), vec!["bu 10 new.pid=99", "au 10"]);
assert_eq!(query_col(&db, "SELECT pid FROM child"), vec![SqlValue::Integer(99)]);
}
#[test]
fn set_null_raise_ignore_orphan_trips_statement_end_fk_check() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE SET NULL)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE UPDATE ON child WHEN OLD.id = 10 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1)").unwrap();
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(
err.contains("FOREIGN KEY constraint failed"),
"expected FK violation, got: {err}"
);
assert!(!db.in_transaction(), "auto-commit must not leak an open transaction");
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(query_col(&db, "SELECT pid FROM child"), vec![SqlValue::Integer(1)]);
}
#[test]
fn set_default_raise_ignore_orphan_trips_statement_end_fk_check() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER DEFAULT 99 REFERENCES parent(id) ON DELETE SET DEFAULT)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE UPDATE ON child WHEN OLD.id = 10 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1), (99)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1)").unwrap();
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(
err.contains("FOREIGN KEY constraint failed"),
"expected FK violation, got: {err}"
);
assert!(!db.in_transaction());
assert_eq!(
query_col(&db, "SELECT id FROM parent ORDER BY id"),
vec![SqlValue::Integer(1), SqlValue::Integer(99)]
);
assert_eq!(query_col(&db, "SELECT pid FROM child"), vec![SqlValue::Integer(1)]);
}
#[test]
fn set_null_raise_ignore_deferred_fk_defers_orphan_to_commit() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE UPDATE ON child WHEN OLD.id = 10 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(query_col(&db, "SELECT id FROM parent"), Vec::<SqlValue>::new());
assert_eq!(query_col(&db, "SELECT pid FROM child"), vec![SqlValue::Integer(1)]);
let commit = crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db);
assert!(commit.is_err(), "COMMIT must fail on the deferred orphan");
let msg = format!("{:?}", commit.unwrap_err());
assert!(
msg.contains("FOREIGN KEY constraint failed"),
"expected FK violation at COMMIT, got: {msg}"
);
}
#[test]
fn set_null_without_child_triggers_unaffected() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE SET NULL)",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1), (2)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1), (20, 2)").unwrap();
exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap();
assert_eq!(
query_col(&db, "SELECT IFNULL(pid, -1) FROM child ORDER BY id"),
vec![SqlValue::Integer(-1), SqlValue::Integer(-1), SqlValue::Integer(2)]
);
}
#[test]
fn cascade_delete_raise_ignore_orphan_in_explicit_txn_rolls_back_statement_keeps_txn_open() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(
err.contains("FOREIGN KEY constraint failed"),
"expected FK violation, got: {err}"
);
assert!(db.in_transaction(), "constraint violation must keep the transaction open");
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db)
.expect("COMMIT should succeed — no orphan persisted after statement rollback");
assert!(!db.in_transaction());
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
}
#[test]
fn cascade_orphan_in_explicit_txn_preserves_earlier_statements() {
let mut db = fresh_db();
exec(&mut db, "CREATE TABLE parent (id INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
exec(
&mut db,
"CREATE TABLE log (id INTEGER PRIMARY KEY)",
)
.unwrap();
exec(
&mut db,
"CREATE TRIGGER child_skip BEFORE DELETE ON child WHEN OLD.id = 11 BEGIN SELECT RAISE(IGNORE); END",
)
.unwrap();
exec(&mut db, "INSERT INTO parent VALUES (1)").unwrap();
exec(&mut db, "INSERT INTO child VALUES (10, 1), (11, 1)").unwrap();
db.begin_transaction().expect("BEGIN");
exec(&mut db, "INSERT INTO log VALUES (100)").unwrap();
let err = exec(&mut db, "DELETE FROM parent WHERE id = 1").unwrap_err();
assert!(err.contains("FOREIGN KEY constraint failed"), "got: {err}");
assert!(db.in_transaction(), "txn must stay open");
assert_eq!(query_col(&db, "SELECT id FROM log"), vec![SqlValue::Integer(100)]);
assert_eq!(query_col(&db, "SELECT id FROM parent"), vec![SqlValue::Integer(1)]);
assert_eq!(
query_col(&db, "SELECT id FROM child ORDER BY id"),
vec![SqlValue::Integer(10), SqlValue::Integer(11)]
);
crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db).expect("COMMIT ok");
assert_eq!(query_col(&db, "SELECT id FROM log"), vec![SqlValue::Integer(100)]);
}