use vibesql_ast::Statement;
use vibesql_parser::Parser;
use vibesql_types::SqlValue;
use super::super::*;
use crate::errors::ExecutorError;
fn exec_ok(db: &mut vibesql_storage::Database, sql: &str) {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::CreateTable(s) => {
CreateTableExecutor::execute(&s, db).expect("CREATE TABLE failed");
}
Statement::CreateTrigger(s) => {
crate::advanced_objects::execute_create_trigger(&s, db).expect("CREATE TRIGGER failed");
}
Statement::CreateView(s) => {
crate::advanced_objects::execute_create_view(&s, db).expect("CREATE VIEW failed");
}
Statement::Insert(s) => {
InsertExecutor::execute(db, &s).expect("INSERT failed");
}
Statement::Update(s) => {
UpdateExecutor::execute(&s, db).expect("UPDATE failed");
}
Statement::Delete(s) => {
DeleteExecutor::execute(&s, db).expect("DELETE failed");
}
other => panic!("Unsupported setup statement: {:?}", other),
}
}
fn exec_dml(db: &mut vibesql_storage::Database, sql: &str) -> Result<usize, ExecutorError> {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Insert(s) => InsertExecutor::execute(db, &s),
Statement::Update(s) => UpdateExecutor::execute(&s, db),
Statement::Delete(s) => DeleteExecutor::execute(&s, db),
other => panic!("Expected DML, got {:?}", other),
}
}
fn column_values(db: &vibesql_storage::Database, table: &str, col: &str) -> Vec<SqlValue> {
let schema = db.catalog.get_table(table).expect("table exists");
let idx = schema.columns.iter().position(|c| c.name == col).expect("column exists");
db.get_table(table)
.expect("table exists")
.scan()
.iter()
.map(|row| row.values[idx].clone())
.collect()
}
fn ints(db: &vibesql_storage::Database, table: &str, col: &str) -> Vec<i64> {
column_values(db, table, col)
.into_iter()
.map(|v| match v {
SqlValue::Integer(i) => i,
SqlValue::Bigint(i) => i,
other => panic!("expected integer, got {:?}", other),
})
.collect()
}
fn new_db_with_table() -> vibesql_storage::Database {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 20)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (3, 30)");
db
}
#[test]
fn raise_abort_aborts_update_with_message() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t WHEN NEW.v > 100 \
BEGIN SELECT raise(ABORT, 'value too big'); END",
);
let result = exec_dml(&mut db, "UPDATE t SET v = 200 WHERE id = 2");
match result {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
assert_eq!(message, "value too big");
}
other => panic!("expected RAISE(ABORT) error, got {:?}", other),
}
assert_eq!(ints(&db, "t", "v"), vec![10, 20, 30]);
}
#[test]
fn raise_abort_message_is_surfaced_verbatim() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t \
BEGIN SELECT raise(ABORT, 'custom error text'); END",
);
let err = exec_dml(&mut db, "UPDATE t SET v = 99 WHERE id = 1").unwrap_err();
assert_eq!(err.to_string(), "custom error text");
}
#[test]
fn raise_abort_coerces_non_string_message_to_text() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t \
BEGIN SELECT raise(ABORT, 42); END",
);
let err = exec_dml(&mut db, "UPDATE t SET v = 99 WHERE id = 1").unwrap_err();
assert_eq!(err.to_string(), "42");
}
#[test]
fn raise_fail_aborts_with_message() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t WHEN NEW.v > 100 \
BEGIN SELECT raise(FAIL, 'fail msg'); END",
);
match exec_dml(&mut db, "UPDATE t SET v = 200 WHERE id = 2") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
assert_eq!(message, "fail msg");
}
other => panic!("expected RAISE(FAIL) error, got {:?}", other),
}
}
#[test]
fn raise_rollback_aborts_with_message() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t WHEN NEW.v > 100 \
BEGIN SELECT raise(ROLLBACK, 'undo all'); END",
);
match exec_dml(&mut db, "UPDATE t SET v = 200 WHERE id = 2") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
assert_eq!(message, "undo all");
}
other => panic!("expected RAISE(ROLLBACK) error, got {:?}", other),
}
assert_eq!(ints(&db, "t", "v"), vec![10, 20, 30]);
}
#[test]
fn raise_ignore_skips_only_the_matching_update_row() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t WHEN NEW.v = 999 \
BEGIN SELECT raise(IGNORE); END",
);
let affected = exec_dml(&mut db, "UPDATE t SET v = CASE WHEN id = 2 THEN 999 ELSE v + 100 END")
.expect("RAISE(IGNORE) must not error");
assert_eq!(ints(&db, "t", "v"), vec![110, 20, 130]);
assert_eq!(affected, 2, "ignored row must not be counted as updated");
}
#[test]
fn raise_ignore_skips_only_the_matching_insert_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE INSERT ON t WHEN NEW.v = 999 \
BEGIN SELECT raise(IGNORE); END",
);
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 999)")
.expect("RAISE(IGNORE) must not error");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (3, 30)");
assert_eq!(ints(&db, "t", "id"), vec![1, 3]);
assert_eq!(ints(&db, "t", "v"), vec![10, 30]);
}
#[test]
fn raise_ignore_skips_only_the_matching_delete_row() {
let mut db = new_db_with_table();
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE DELETE ON t WHEN OLD.v = 20 \
BEGIN SELECT raise(IGNORE); END",
);
let affected = exec_dml(&mut db, "DELETE FROM t").expect("RAISE(IGNORE) must not error");
assert_eq!(ints(&db, "t", "id"), vec![2]);
assert_eq!(affected, 2, "only the non-ignored rows are deleted/counted");
}
fn db_with_trigger(timing_event: &str, action: &str) -> vibesql_storage::Database {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)");
exec_ok(
&mut db,
&format!(
"CREATE TRIGGER trg {timing_event} ON t WHEN NEW.v = 'BAD' \
BEGIN SELECT raise({action}, 'boom'); END"
),
);
db
}
fn texts(db: &vibesql_storage::Database, table: &str, col: &str) -> Vec<String> {
column_values(db, table, col)
.into_iter()
.map(|v| match v {
SqlValue::Varchar(s) => s.to_string(),
SqlValue::Null => "<null>".to_string(),
other => panic!("expected text, got {:?}", other),
})
.collect()
}
#[test]
fn raise_abort_keeps_transaction_open_and_prior_statements_survive() {
let mut db = db_with_trigger("BEFORE INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
assert_eq!(message, "boom");
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT must keep the transaction open");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (3, 'after')").expect("stmt 3 ok");
db.commit_transaction().expect("COMMIT");
assert_eq!(ints(&db, "t", "id"), vec![1, 3]);
assert_eq!(texts(&db, "t", "v"), vec!["before".to_string(), "after".to_string()]);
}
#[test]
fn raise_rollback_aborts_entire_transaction() {
let mut db = db_with_trigger("BEFORE INSERT", "ROLLBACK");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
assert_eq!(ints(&db, "t", "id"), vec![1], "row 1 visible before the RAISE");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
assert_eq!(message, "boom");
}
other => panic!("expected RAISE(ROLLBACK), got {:?}", other),
}
assert!(!db.in_transaction(), "ROLLBACK must end the transaction");
assert!(db.get_table("t").unwrap().scan().is_empty(), "all rows rolled back");
}
#[test]
fn raise_abort_undoes_whole_statement_with_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(ints(&db, "t", "id"), vec![1]);
}
#[test]
fn raise_fail_keeps_partial_statement_changes_with_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "FAIL");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
}
other => panic!("expected RAISE(FAIL), got {:?}", other),
}
assert!(db.in_transaction(), "FAIL keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("before".into())],
vec![ipk(2), SqlValue::Varchar("okA".into())],
vec![ipk(3), SqlValue::Varchar("BAD".into())],
],
);
}
#[test]
fn raise_outside_transaction_leaves_no_open_transaction() {
for action in ["ABORT", "FAIL", "ROLLBACK"] {
let mut db = db_with_trigger("BEFORE INSERT", action);
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'ok')").expect("seed row");
let err = exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')").unwrap_err();
assert!(matches!(err, ExecutorError::Raise { .. }), "{action}: expected RAISE");
assert!(!db.in_transaction(), "{action}: must not leave a transaction open");
assert_eq!(ints(&db, "t", "id"), vec![1], "{action}");
}
}
#[test]
fn autocommit_after_insert_abort_rolls_back_whole_statement() {
let mut db = db_with_trigger("AFTER INSERT", "ABORT");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(!db.in_transaction(), "auto-commit must not leak an open transaction");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
Vec::<Vec<SqlValue>>::new(),
"ABORT must undo even the rows applied before the offending row"
);
}
#[test]
fn autocommit_after_insert_fail_keeps_pre_offending_and_offending_rows() {
let mut db = db_with_trigger("AFTER INSERT", "FAIL");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
}
other => panic!("expected RAISE(FAIL), got {:?}", other),
}
assert!(!db.in_transaction(), "auto-commit must not leak an open transaction");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(2), SqlValue::Varchar("okA".into())],
vec![ipk(3), SqlValue::Varchar("BAD".into())],
],
);
}
#[test]
fn autocommit_before_insert_abort_rolls_back_whole_statement() {
let mut db = db_with_trigger("BEFORE INSERT", "ABORT");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(!db.in_transaction());
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
Vec::<Vec<SqlValue>>::new(),
"ABORT undoes okA (applied before the BAD row's BEFORE trigger raised)"
);
}
#[test]
fn autocommit_before_insert_fail_keeps_pre_offending_rows_only() {
let mut db = db_with_trigger("BEFORE INSERT", "FAIL");
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
}
other => panic!("expected RAISE(FAIL), got {:?}", other),
}
assert!(!db.in_transaction());
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(2), SqlValue::Varchar("okA".into())]],
);
}
#[test]
fn autocommit_multirow_unique_violation_rolls_back_whole_statement() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
let err = exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2,20),(3,30),(1,99),(4,40)")
.unwrap_err();
assert!(err.to_string().contains("UNIQUE"), "expected UNIQUE error, got {err}");
assert!(!db.in_transaction());
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
}
#[test]
fn autocommit_insert_or_ignore_keeps_non_conflicting_rows() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
let affected = exec_dml(&mut db, "INSERT OR IGNORE INTO t (id, v) VALUES (2,20),(1,99),(3,30)")
.expect("OR IGNORE must not error on a conflict");
assert_eq!(affected, 2, "two non-conflicting rows inserted");
assert!(!db.in_transaction());
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
vec![vec![ipk(1)], vec![ipk(2)], vec![ipk(3)]],
);
}
#[test]
fn autocommit_single_row_success_with_trigger_unaffected() {
let mut db = db_with_trigger("BEFORE INSERT", "ABORT");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'ok')").expect("non-BAD insert succeeds");
assert!(!db.in_transaction());
assert_eq!(
select_rows(&db, "SELECT id, v FROM t"),
vec![vec![ipk(1), SqlValue::Varchar("ok".into())]],
);
}
#[test]
fn raise_abort_in_insert_trigger_aborts() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE INSERT ON t WHEN NEW.v < 0 \
BEGIN SELECT raise(ABORT, 'no negatives'); END",
);
match exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, -5)") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
assert_eq!(message, "no negatives");
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.get_table("t").unwrap().scan().is_empty());
}
#[test]
fn raise_ignore_in_before_delete_blocks_replace_conflict() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 20)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE DELETE ON t \
BEGIN SELECT raise(IGNORE); END",
);
let result = exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (1, 99)");
assert!(
result.is_err(),
"REPLACE must fail: the IGNORE'd conflicting row still collides on the PK"
);
assert_eq!(ints(&db, "t", "id"), vec![1, 2]);
assert_eq!(ints(&db, "t", "v"), vec![10, 20]);
}
#[test]
fn raise_ignore_skips_only_matching_row_in_update_from() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 20)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (3, 30)");
exec_ok(&mut db, "CREATE TABLE d (id INTEGER PRIMARY KEY, nv INTEGER)");
exec_ok(&mut db, "INSERT INTO d (id, nv) VALUES (1, 111)");
exec_ok(&mut db, "INSERT INTO d (id, nv) VALUES (2, 999)");
exec_ok(&mut db, "INSERT INTO d (id, nv) VALUES (3, 333)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE UPDATE ON t WHEN NEW.v = 999 \
BEGIN SELECT raise(IGNORE); END",
);
let affected = exec_dml(&mut db, "UPDATE t SET v = d.nv FROM d WHERE t.id = d.id")
.expect("RAISE(IGNORE) must not error");
assert_eq!(ints(&db, "t", "v"), vec![111, 20, 333]);
assert_eq!(affected, 2, "the ignored row must not be counted as updated");
}
#[test]
fn raise_ignore_in_instead_of_insert_skips_base_write() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF INSERT ON vw \
BEGIN \
SELECT raise(IGNORE); \
INSERT INTO base (id, v) VALUES (NEW.id, NEW.v); \
END",
);
exec_dml(&mut db, "INSERT INTO vw (id, v) VALUES (1, 10)")
.expect("RAISE(IGNORE) must not error");
assert!(db.get_table("base").unwrap().scan().is_empty());
}
#[test]
fn instead_of_insert_without_ignore_writes_base() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF INSERT ON vw \
BEGIN \
INSERT INTO base (id, v) VALUES (NEW.id, NEW.v); \
END",
);
exec_dml(&mut db, "INSERT INTO vw (id, v) VALUES (1, 10)").expect("view insert must succeed");
assert_eq!(ints(&db, "base", "id"), vec![1]);
assert_eq!(ints(&db, "base", "v"), vec![10]);
}
#[test]
fn raise_ignore_in_instead_of_update_abandons_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF UPDATE ON vw \
BEGIN \
SELECT raise(IGNORE); \
UPDATE base SET v = NEW.v WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "UPDATE vw SET v = 999")
.expect("RAISE(IGNORE) in INSTEAD OF UPDATE must not error");
assert_eq!(ints(&db, "base", "id"), vec![1, 2]);
assert_eq!(ints(&db, "base", "v"), vec![10, 20]);
}
#[test]
fn instead_of_update_without_ignore_writes_base() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF UPDATE ON vw \
BEGIN \
UPDATE base SET v = NEW.v WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "UPDATE vw SET v = 999").expect("INSTEAD OF UPDATE must succeed");
assert_eq!(ints(&db, "base", "v"), vec![999, 999]);
}
#[test]
fn raise_ignore_in_instead_of_delete_abandons_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF DELETE ON vw \
BEGIN \
SELECT raise(IGNORE); \
DELETE FROM base WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "DELETE FROM vw")
.expect("RAISE(IGNORE) in INSTEAD OF DELETE must not error");
assert_eq!(ints(&db, "base", "id"), vec![1, 2]);
assert_eq!(ints(&db, "base", "v"), vec![10, 20]);
}
#[test]
fn instead_of_delete_without_ignore_deletes_base() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF DELETE ON vw \
BEGIN \
DELETE FROM base WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "DELETE FROM vw").expect("INSTEAD OF DELETE must succeed");
assert!(db.get_table("base").unwrap().scan().is_empty());
}
#[test]
fn instead_of_update_when_old_fires_selectively() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF UPDATE ON vw WHEN OLD.id = 1 \
BEGIN \
UPDATE base SET v = NEW.v WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "UPDATE vw SET v = 999")
.expect("INSTEAD OF UPDATE with WHEN must not error on a view");
assert_eq!(ints(&db, "base", "id"), vec![1, 2]);
assert_eq!(ints(&db, "base", "v"), vec![999, 20]);
}
#[test]
fn instead_of_insert_when_new_fires_selectively() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF INSERT ON vw WHEN NEW.v > 0 \
BEGIN \
INSERT INTO base (id, v) VALUES (NEW.id, NEW.v); \
END",
);
exec_dml(&mut db, "INSERT INTO vw (id, v) VALUES (1, 10)")
.expect("INSTEAD OF INSERT with WHEN must not error on a view");
exec_dml(&mut db, "INSERT INTO vw (id, v) VALUES (2, -5)")
.expect("WHEN-false INSTEAD OF INSERT must not error");
assert_eq!(ints(&db, "base", "id"), vec![1]);
assert_eq!(ints(&db, "base", "v"), vec![10]);
}
#[test]
fn instead_of_delete_when_old_fires_selectively() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE base (id INTEGER, v INTEGER)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (1, 10)");
exec_ok(&mut db, "INSERT INTO base (id, v) VALUES (2, 20)");
exec_ok(&mut db, "CREATE VIEW vw AS SELECT id, v FROM base");
exec_ok(
&mut db,
"CREATE TRIGGER trg INSTEAD OF DELETE ON vw WHEN OLD.v > 15 \
BEGIN \
DELETE FROM base WHERE id = OLD.id; \
END",
);
exec_dml(&mut db, "DELETE FROM vw")
.expect("INSTEAD OF DELETE with WHEN must not error on a view");
assert_eq!(
select_rows(&db, "SELECT id, v FROM base ORDER BY id"),
vec![vec![SqlValue::Integer(1), SqlValue::Integer(10)]],
);
}
#[test]
fn base_table_trigger_when_clause_still_works() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER, v INTEGER)");
exec_ok(&mut db, "CREATE TABLE log (id INTEGER, v INTEGER)");
exec_ok(
&mut db,
"CREATE TRIGGER trg AFTER INSERT ON t WHEN NEW.v > 0 \
BEGIN \
INSERT INTO log (id, v) VALUES (NEW.id, NEW.v); \
END",
);
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)").expect("base insert ok");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, -5)").expect("base insert ok");
assert_eq!(ints(&db, "log", "id"), vec![1]);
assert_eq!(ints(&db, "log", "v"), vec![10]);
}
fn select_rows(
db: &vibesql_storage::Database,
sql: &str,
) -> Vec<Vec<SqlValue>> {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Select(s) => {
let executor = crate::SelectExecutor::new(db);
let result = executor.execute_with_columns(&s).expect("SELECT failed");
result.rows.into_iter().map(|row| row.values.to_vec()).collect()
}
other => panic!("Expected SELECT, got {:?}", other),
}
}
fn ipk(i: i64) -> SqlValue {
SqlValue::Integer(i)
}
#[test]
fn replace_with_when_false_before_delete_trigger_leaves_one_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10), (2, 20)");
exec_ok(
&mut db,
"CREATE TRIGGER bd BEFORE DELETE ON t WHEN OLD.id = 999 \
BEGIN SELECT raise(IGNORE); END",
);
exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (2, 222)")
.expect("REPLACE must resolve the conflict");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(10)], vec![ipk(2), ipk(222)]],
"the conflicting row must be deleted, not duplicated"
);
}
#[test]
fn replace_with_non_raise_before_delete_trigger_fires_once_and_replaces() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "CREATE TABLE log (n INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10), (2, 20)");
exec_ok(
&mut db,
"CREATE TRIGGER bd BEFORE DELETE ON t \
BEGIN INSERT INTO log (n) VALUES (OLD.id); END",
);
exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (2, 222)")
.expect("REPLACE must resolve the conflict");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(10)], vec![ipk(2), ipk(222)]],
"exactly one row per id after REPLACE"
);
assert_eq!(
select_rows(&db, "SELECT n FROM log"),
vec![vec![ipk(2)]],
"BEFORE DELETE trigger must fire exactly once for the conflicting row"
);
}
#[test]
fn replace_unique_conflict_with_before_delete_trigger_leaves_one_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, u INTEGER UNIQUE, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, u, v) VALUES (1, 100, 10), (2, 200, 20)");
exec_ok(
&mut db,
"CREATE TRIGGER bd BEFORE DELETE ON t WHEN OLD.id = 999 \
BEGIN SELECT raise(IGNORE); END",
);
exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, u, v) VALUES (2, 200, 222)")
.expect("REPLACE must resolve the UNIQUE conflict");
assert_eq!(
select_rows(&db, "SELECT id, u, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(100), ipk(10)], vec![ipk(2), ipk(200), ipk(222)]],
);
}
#[test]
fn replace_multiple_conflicts_with_before_delete_trigger_deletes_all() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, u INTEGER UNIQUE, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, u, v) VALUES (1, 100, 10), (2, 200, 20), (3, 300, 30)");
exec_ok(
&mut db,
"CREATE TRIGGER bd BEFORE DELETE ON t WHEN OLD.id = 999 \
BEGIN SELECT raise(IGNORE); END",
);
exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, u, v) VALUES (2, 300, 222)")
.expect("REPLACE must resolve both conflicts");
assert_eq!(
select_rows(&db, "SELECT id, u, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(100), ipk(10)], vec![ipk(2), ipk(300), ipk(222)]],
"both conflicting rows (id=2 and id=3) must be deleted"
);
}
#[test]
fn replace_without_trigger_leaves_one_row() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10), (2, 20)");
exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (2, 222)")
.expect("REPLACE must resolve the conflict");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(10)], vec![ipk(2), ipk(222)]],
);
}
#[test]
fn replace_with_raise_ignore_before_delete_errors_and_leaves_table_unchanged() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10), (2, 20)");
exec_ok(&mut db, "CREATE TRIGGER bd BEFORE DELETE ON t BEGIN SELECT raise(IGNORE); END");
let result = exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (1, 99)");
assert!(
result.is_err(),
"the IGNORE'd conflicting row still collides on the PK -> error"
);
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(10)], vec![ipk(2), ipk(20)]],
"table must be unchanged after the failed REPLACE"
);
}
#[test]
fn replace_with_raise_abort_before_delete_aborts() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 10), (2, 20)");
exec_ok(
&mut db,
"CREATE TRIGGER bd BEFORE DELETE ON t BEGIN SELECT raise(ABORT, 'no delete'); END",
);
match exec_dml(&mut db, "INSERT OR REPLACE INTO t (id, v) VALUES (1, 99)") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
assert_eq!(message, "no delete");
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![vec![ipk(1), ipk(10)], vec![ipk(2), ipk(20)]],
"table must be unchanged after the aborted REPLACE"
);
}
fn exec_insert_returning(
db: &mut vibesql_storage::Database,
sql: &str,
) -> Result<Vec<Vec<SqlValue>>, ExecutorError> {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Insert(s) => InsertExecutor::execute_returning(db, &s).map(|outcome| {
outcome
.returning
.map(|r| r.rows.into_iter().map(|row| row.values.to_vec()).collect())
.unwrap_or_default()
}),
other => panic!("Expected INSERT, got {:?}", other),
}
}
fn exec_update_returning(
db: &mut vibesql_storage::Database,
sql: &str,
) -> Result<Vec<Vec<SqlValue>>, ExecutorError> {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Update(s) => UpdateExecutor::execute_returning(&s, db).map(|(_, returning)| {
returning
.map(|r| r.rows.into_iter().map(|row| row.values.to_vec()).collect())
.unwrap_or_default()
}),
other => panic!("Expected UPDATE, got {:?}", other),
}
}
fn exec_delete_returning(
db: &mut vibesql_storage::Database,
sql: &str,
) -> Result<Vec<Vec<SqlValue>>, ExecutorError> {
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Delete(s) => DeleteExecutor::execute_returning(&s, db).map(|(_, returning)| {
returning
.map(|r| r.rows.into_iter().map(|row| row.values.to_vec()).collect())
.unwrap_or_default()
}),
other => panic!("Expected DELETE, got {:?}", other),
}
}
fn exec_dml_procedural(
db: &mut vibesql_storage::Database,
sql: &str,
) -> Result<usize, ExecutorError> {
let ctx = crate::procedural::ExecutionContext::new();
let stmt =
Parser::parse_sql(sql).unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", sql, e));
match stmt {
Statement::Insert(s) => InsertExecutor::execute_with_procedural_context(db, &s, &ctx),
Statement::Update(s) => UpdateExecutor::execute_with_procedural_context(&s, db, &ctx),
Statement::Delete(s) => DeleteExecutor::execute_with_procedural_context(&s, db, &ctx),
other => panic!("Expected DML, got {:?}", other),
}
}
#[test]
fn returning_insert_raise_abort_undoes_statement_keeps_txn() {
let mut db = db_with_trigger("BEFORE INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_insert_returning(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD') RETURNING id") {
Err(ExecutorError::Raise { action, message }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
assert_eq!(message, "boom");
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT must keep the transaction open");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (3, 'after')").expect("stmt 3 ok");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
vec![vec![ipk(1)], vec![ipk(3)]],
"ABORT undoes the offending RETURNING INSERT but keeps the txn's other rows"
);
}
#[test]
fn returning_insert_raise_rollback_aborts_whole_txn() {
let mut db = db_with_trigger("BEFORE INSERT", "ROLLBACK");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_insert_returning(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD') RETURNING id") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
}
other => panic!("expected RAISE(ROLLBACK), got {:?}", other),
}
assert!(!db.in_transaction(), "ROLLBACK must end the transaction");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
Vec::<Vec<SqlValue>>::new(),
"ROLLBACK discards the entire transaction, including earlier rows"
);
}
#[test]
fn returning_insert_raise_abort_undoes_whole_statement_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_insert_returning(
&mut db,
"INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB') RETURNING id",
) {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
vec![vec![ipk(1)]],
"ABORT undoes the entire offending RETURNING statement"
);
}
#[test]
fn returning_insert_raise_fail_keeps_partial_statement_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "FAIL");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_insert_returning(
&mut db,
"INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB') RETURNING id",
) {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
}
other => panic!("expected RAISE(FAIL), got {:?}", other),
}
assert!(db.in_transaction(), "FAIL keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("before".into())],
vec![ipk(2), SqlValue::Varchar("okA".into())],
vec![ipk(3), SqlValue::Varchar("BAD".into())],
],
);
}
#[test]
fn returning_update_raise_abort_undoes_statement_keeps_txn() {
let mut db = db_with_trigger("BEFORE UPDATE", "ABORT");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 'keep')");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 'other')");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "UPDATE t SET v = 'changed' WHERE id = 2").expect("prior stmt ok");
match exec_update_returning(&mut db, "UPDATE t SET v = 'BAD' WHERE id = 1 RETURNING id") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("keep".into())],
vec![ipk(2), SqlValue::Varchar("changed".into())],
],
"ABORT undoes the offending RETURNING UPDATE; the prior statement survives"
);
}
#[test]
fn returning_delete_raise_rollback_aborts_whole_txn() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE DELETE ON t WHEN OLD.v = 'BAD' \
BEGIN SELECT raise(ROLLBACK, 'boom'); END",
);
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 'keep')");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "UPDATE t SET v = 'changed' WHERE id = 1").expect("prior stmt ok");
match exec_delete_returning(&mut db, "DELETE FROM t WHERE id = 2 RETURNING id") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
}
other => panic!("expected RAISE(ROLLBACK), got {:?}", other),
}
assert!(!db.in_transaction(), "ROLLBACK must end the transaction");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("keep".into())],
vec![ipk(2), SqlValue::Varchar("BAD".into())],
],
"ROLLBACK discards the whole transaction including the prior UPDATE"
);
}
#[test]
fn procedural_insert_raise_abort_undoes_statement_keeps_txn() {
let mut db = db_with_trigger("BEFORE INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT must keep the transaction open");
exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (3, 'after')").expect("stmt 3 ok");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
vec![vec![ipk(1)], vec![ipk(3)]],
);
}
#[test]
fn procedural_insert_raise_rollback_aborts_whole_txn() {
let mut db = db_with_trigger("BEFORE INSERT", "ROLLBACK");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
}
other => panic!("expected RAISE(ROLLBACK), got {:?}", other),
}
assert!(!db.in_transaction(), "ROLLBACK must end the transaction");
assert_eq!(
select_rows(&db, "SELECT id FROM t ORDER BY id"),
Vec::<Vec<SqlValue>>::new(),
);
}
#[test]
fn procedural_insert_raise_abort_undoes_whole_statement_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "ABORT");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml_procedural(
&mut db,
"INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')",
) {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
}
#[test]
fn procedural_insert_raise_fail_keeps_partial_statement_after_trigger() {
let mut db = db_with_trigger("AFTER INSERT", "FAIL");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "INSERT INTO t (id, v) VALUES (1, 'before')").expect("stmt 1 ok");
match exec_dml_procedural(
&mut db,
"INSERT INTO t (id, v) VALUES (2, 'okA'), (3, 'BAD'), (4, 'okB')",
) {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Fail);
}
other => panic!("expected RAISE(FAIL), got {:?}", other),
}
assert!(db.in_transaction(), "FAIL keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("before".into())],
vec![ipk(2), SqlValue::Varchar("okA".into())],
vec![ipk(3), SqlValue::Varchar("BAD".into())],
],
);
}
#[test]
fn procedural_update_raise_abort_undoes_statement_keeps_txn() {
let mut db = db_with_trigger("BEFORE UPDATE", "ABORT");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 'keep')");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 'other')");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "UPDATE t SET v = 'changed' WHERE id = 2").expect("prior stmt ok");
match exec_dml_procedural(&mut db, "UPDATE t SET v = 'BAD' WHERE id = 1") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Abort);
}
other => panic!("expected RAISE(ABORT), got {:?}", other),
}
assert!(db.in_transaction(), "ABORT keeps the transaction open");
db.commit_transaction().expect("COMMIT");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("keep".into())],
vec![ipk(2), SqlValue::Varchar("changed".into())],
],
);
}
#[test]
fn procedural_delete_raise_rollback_aborts_whole_txn() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)");
exec_ok(
&mut db,
"CREATE TRIGGER trg BEFORE DELETE ON t WHEN OLD.v = 'BAD' \
BEGIN SELECT raise(ROLLBACK, 'boom'); END",
);
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (1, 'keep')");
exec_ok(&mut db, "INSERT INTO t (id, v) VALUES (2, 'BAD')");
db.begin_transaction().expect("BEGIN");
exec_dml_procedural(&mut db, "UPDATE t SET v = 'changed' WHERE id = 1").expect("prior stmt ok");
match exec_dml_procedural(&mut db, "DELETE FROM t WHERE id = 2") {
Err(ExecutorError::Raise { action, .. }) => {
assert_eq!(action, vibesql_ast::RaiseAction::Rollback);
}
other => panic!("expected RAISE(ROLLBACK), got {:?}", other),
}
assert!(!db.in_transaction(), "ROLLBACK must end the transaction");
assert_eq!(
select_rows(&db, "SELECT id, v FROM t ORDER BY id"),
vec![
vec![ipk(1), SqlValue::Varchar("keep".into())],
vec![ipk(2), SqlValue::Varchar("BAD".into())],
],
);
}
#[test]
fn unique_violation_in_explicit_txn_rolls_back_statement_keeps_txn_open() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER UNIQUE)");
exec_ok(&mut db, "CREATE TRIGGER trg AFTER INSERT ON t BEGIN SELECT 1; END");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 100)").expect("stmt 1 ok");
let err = exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 200), (3, 100)")
.expect_err("expected UNIQUE violation");
assert!(err.to_string().contains("UNIQUE"), "expected UNIQUE error, got: {err}");
assert!(db.in_transaction(), "UNIQUE violation must keep the transaction open");
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db).expect("COMMIT ok");
assert!(!db.in_transaction());
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
}
#[test]
fn check_violation_in_explicit_txn_rolls_back_statement_keeps_txn_open() {
let mut db = vibesql_storage::Database::new();
exec_ok(&mut db, "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER CHECK (v < 50))");
exec_ok(&mut db, "CREATE TRIGGER trg AFTER INSERT ON t BEGIN SELECT 1; END");
db.begin_transaction().expect("BEGIN");
exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (1, 10)").expect("stmt 1 ok");
let err = exec_dml(&mut db, "INSERT INTO t (id, v) VALUES (2, 20), (3, 99)")
.expect_err("expected CHECK violation");
assert!(err.to_string().contains("CHECK"), "expected CHECK error, got: {err}");
assert!(db.in_transaction(), "CHECK violation must keep the transaction open");
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
crate::CommitExecutor::execute(&vibesql_ast::CommitStmt, &mut db).expect("COMMIT ok");
assert_eq!(select_rows(&db, "SELECT id FROM t ORDER BY id"), vec![vec![ipk(1)]]);
}