use vibesql_ast::{
CreateTriggerStmt, DropTriggerStmt, TriggerAction, TriggerEvent, TriggerGranularity,
TriggerTiming,
};
use vibesql_storage::Database;
use crate::CreateTableExecutor;
#[test]
fn test_create_trigger() {
let mut db = Database::new();
let create_table_sql = "CREATE TABLE test_table (id INT, name VARCHAR(255));";
let create_table_stmt = vibesql_parser::Parser::parse_sql(create_table_sql).unwrap();
match create_table_stmt {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "my_trigger".to_string(),
name_source: None,
timing: TriggerTiming::Before,
event: TriggerEvent::Insert,
table_name: "test_table".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let result = crate::advanced_objects::execute_create_trigger(&stmt, &mut db);
assert!(result.is_ok(), "Failed to create trigger: {:?}", result.err());
let trigger = db.catalog.get_trigger("my_trigger");
assert!(trigger.is_some(), "Trigger not found after creation");
let trigger = trigger.unwrap();
assert_eq!(trigger.name, "my_trigger");
assert_eq!(trigger.timing, TriggerTiming::Before);
assert_eq!(trigger.event, TriggerEvent::Insert);
}
#[test]
fn test_create_trigger_duplicate_error() {
let mut db = Database::new();
let create_table_sql = "CREATE TABLE test_table (id INT, name VARCHAR(255));";
let create_table_stmt = vibesql_parser::Parser::parse_sql(create_table_sql).unwrap();
match create_table_stmt {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "my_trigger".to_string(),
name_source: None,
timing: TriggerTiming::Before,
event: TriggerEvent::Insert,
table_name: "test_table".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let result = crate::advanced_objects::execute_create_trigger(&stmt, &mut db);
assert!(result.is_ok(), "Failed to create trigger: {:?}", result.err());
let result = crate::advanced_objects::execute_create_trigger(&stmt, &mut db);
assert!(result.is_err(), "Should fail when creating duplicate trigger");
}
#[test]
fn test_create_trigger_if_not_exists_is_noop_when_present() {
let mut db = Database::new();
let create_table_sql = "CREATE TABLE test_table (id INT, name VARCHAR(255));";
match vibesql_parser::Parser::parse_sql(create_table_sql).unwrap() {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
let stmt = CreateTriggerStmt {
if_not_exists: true,
schema: None,
trigger_name: "my_trigger".to_string(),
name_source: None,
timing: TriggerTiming::Before,
event: TriggerEvent::Insert,
table_name: "test_table".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("BEGIN SELECT 1; END".to_string()),
};
crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect("first create should succeed");
let result = crate::advanced_objects::execute_create_trigger(&stmt, &mut db);
assert!(result.is_ok(), "IF NOT EXISTS create should be a no-op: {:?}", result.err());
let strict = CreateTriggerStmt { if_not_exists: false, ..stmt.clone() };
let result = crate::advanced_objects::execute_create_trigger(&strict, &mut db);
assert!(result.is_err(), "duplicate without IF NOT EXISTS should error");
}
fn create_t1(db: &mut Database) {
match vibesql_parser::Parser::parse_sql("CREATE TABLE t1 (a INT, b INT);").unwrap() {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, db).unwrap();
}
other => panic!("Expected CreateTable, got {:?}", other),
}
}
fn parse_create_trigger(sql: &str) -> CreateTriggerStmt {
match vibesql_parser::Parser::parse_sql(sql).expect("parse failed") {
vibesql_ast::Statement::CreateTrigger(stmt) => stmt,
other => panic!("Expected CreateTrigger, got {:?}", other),
}
}
#[test]
fn test_duplicate_trigger_already_exists_echoes_source_quoting() {
let cases = [
("CREATE TRIGGER tr1 DELETE ON t1 BEGIN SELECT 1; END;", "Trigger 'tr1' already exists"),
(
"CREATE TRIGGER \"tr1\" DELETE ON t1 BEGIN SELECT 1; END;",
"Trigger '\"tr1\"' already exists",
),
("CREATE TRIGGER [tr1] DELETE ON t1 BEGIN SELECT 1; END;", "Trigger '[tr1]' already exists"),
];
for (dup_sql, expected) in cases {
let mut db = Database::new();
create_t1(&mut db);
let first = parse_create_trigger("CREATE TRIGGER tr1 DELETE ON t1 BEGIN SELECT 1; END;");
crate::advanced_objects::execute_create_trigger(&first, &mut db)
.expect("first create should succeed");
let dup = parse_create_trigger(dup_sql);
let err = crate::advanced_objects::execute_create_trigger(&dup, &mut db)
.expect_err("duplicate trigger must be rejected");
assert_eq!(err.to_string(), expected, "for SQL: {dup_sql}");
}
}
#[test]
fn test_create_trigger_on_system_table_wording() {
let mut db = Database::new();
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "tr1".to_string(),
name_source: None,
timing: TriggerTiming::After,
event: TriggerEvent::Update(None),
table_name: "sqlite_master".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let err = crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect_err("trigger on system table must be rejected");
assert_eq!(err.to_string(), "cannot create trigger on system table");
}
#[test]
fn test_create_instead_of_trigger_on_table_wording() {
let mut db = Database::new();
create_t1(&mut db);
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "t1t".to_string(),
name_source: None,
timing: TriggerTiming::InsteadOf,
event: TriggerEvent::Update(None),
table_name: "t1".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let err = crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect_err("INSTEAD OF trigger on a table must be rejected");
assert_eq!(err.to_string(), "cannot create INSTEAD OF trigger on table: t1");
}
#[test]
fn test_create_trigger_missing_table_is_main_qualified() {
let cases = [
"CREATE TRIGGER trig UPDATE ON no_such_table BEGIN SELECT 1; END;",
"CREATE TRIGGER IF NOT EXISTS trig UPDATE ON no_such_table BEGIN SELECT 1; END;",
"CREATE TRIGGER trig INSTEAD OF UPDATE ON no_such_table BEGIN SELECT 1; END;",
];
for sql in cases {
let mut db = Database::new();
let stmt = parse_create_trigger(sql);
let err = crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect_err("CREATE TRIGGER on a missing table must be rejected");
assert_eq!(err.to_string(), "Table 'main.no_such_table' not found", "for SQL: {sql}");
}
}
#[test]
fn test_create_temp_trigger_missing_table_is_not_qualified() {
let mut db = Database::new();
let stmt =
parse_create_trigger("CREATE TEMP TRIGGER trig UPDATE ON no_such_table BEGIN SELECT 1; END;");
let err = crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect_err("CREATE TEMP TRIGGER on a missing table must be rejected");
assert_eq!(err.to_string(), "Table 'no_such_table' not found");
}
#[test]
fn test_create_before_after_trigger_on_view_wording() {
let mut db = Database::new();
create_t1(&mut db);
match vibesql_parser::Parser::parse_sql("CREATE VIEW v1 AS SELECT * FROM t1;").unwrap() {
vibesql_ast::Statement::CreateView(stmt) => {
crate::advanced_objects::execute_create_view(&stmt, &mut db).unwrap();
}
other => panic!("Expected CreateView, got {:?}", other),
}
for (timing, label) in [(TriggerTiming::Before, "BEFORE"), (TriggerTiming::After, "AFTER")] {
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "v1t".to_string(),
name_source: None,
timing,
event: TriggerEvent::Update(None),
table_name: "v1".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let err = crate::advanced_objects::execute_create_trigger(&stmt, &mut db)
.expect_err("BEFORE/AFTER trigger on a view must be rejected");
assert_eq!(err.to_string(), format!("cannot create {} trigger on view: v1", label));
}
}
#[test]
fn test_drop_trigger() {
let mut db = Database::new();
let create_table_sql = "CREATE TABLE test_table (id INT, name VARCHAR(255));";
let create_table_stmt = vibesql_parser::Parser::parse_sql(create_table_sql).unwrap();
match create_table_stmt {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: "my_trigger".to_string(),
name_source: None,
timing: TriggerTiming::Before,
event: TriggerEvent::Insert,
table_name: "test_table".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
crate::advanced_objects::execute_create_trigger(&stmt, &mut db).unwrap();
assert!(db.catalog.get_trigger("my_trigger").is_some());
let drop_stmt =
DropTriggerStmt { trigger_name: "my_trigger".to_string(), cascade: false, if_exists: false };
let result = crate::advanced_objects::execute_drop_trigger(&drop_stmt, &mut db);
assert!(result.is_ok(), "Failed to drop trigger: {:?}", result.err());
assert!(db.catalog.get_trigger("my_trigger").is_none(), "Trigger still exists after drop");
}
#[test]
fn test_drop_trigger_not_found() {
let mut db = Database::new();
let drop_stmt = DropTriggerStmt {
trigger_name: "nonexistent_trigger".to_string(),
cascade: false,
if_exists: false,
};
let result = crate::advanced_objects::execute_drop_trigger(&drop_stmt, &mut db);
assert!(result.is_err(), "Should fail when dropping non-existent trigger");
}
#[test]
fn test_drop_trigger_if_exists_missing_is_noop() {
let mut db = Database::new();
let drop_stmt = DropTriggerStmt {
trigger_name: "nonexistent_trigger".to_string(),
cascade: false,
if_exists: true,
};
let result = crate::advanced_objects::execute_drop_trigger(&drop_stmt, &mut db);
assert!(result.is_ok(), "DROP TRIGGER IF EXISTS on missing trigger should be a no-op");
}
#[test]
fn test_create_trigger_all_variations() {
let mut db = Database::new();
let create_table_sql = "CREATE TABLE test_table (id INT, name VARCHAR(255));";
let create_table_stmt = vibesql_parser::Parser::parse_sql(create_table_sql).unwrap();
match create_table_stmt {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
match vibesql_parser::Parser::parse_sql("CREATE VIEW test_view AS SELECT * FROM test_table;")
.unwrap()
{
vibesql_ast::Statement::CreateView(stmt) => {
crate::advanced_objects::execute_create_view(&stmt, &mut db).unwrap();
}
other => panic!("Expected CreateView, got {:?}", other),
}
let timings = vec![
(TriggerTiming::Before, "before", "test_table"),
(TriggerTiming::After, "after", "test_table"),
(TriggerTiming::InsteadOf, "insteadof", "test_view"),
];
for (timing, suffix, target) in timings {
let stmt = CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: format!("trigger_{}", suffix),
name_source: None,
timing: timing.clone(),
event: TriggerEvent::Insert,
table_name: target.to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
};
let result = crate::advanced_objects::execute_create_trigger(&stmt, &mut db);
assert!(result.is_ok(), "Failed to create {} trigger: {:?}", suffix, result.err());
let trigger = db.catalog.get_trigger(&format!("trigger_{}", suffix)).unwrap();
assert_eq!(trigger.timing, timing);
}
}
fn make_test_table(db: &mut Database) {
let sql = "CREATE TABLE t1 (a INT, b INT, c INT);";
match vibesql_parser::Parser::parse_sql(sql).unwrap() {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, db).unwrap();
}
_ => panic!("Expected CreateTable"),
}
}
fn create_trigger_stmt(name: &str) -> CreateTriggerStmt {
CreateTriggerStmt {
if_not_exists: false,
schema: None,
trigger_name: name.to_string(),
name_source: None,
timing: TriggerTiming::After,
event: TriggerEvent::Insert,
table_name: "t1".to_string(),
granularity: TriggerGranularity::Row,
when_condition: None,
triggered_action: TriggerAction::RawSql("SELECT 1;".to_string()),
}
}
#[test]
fn test_create_trigger_rolled_back_with_transaction() {
let mut db = Database::new();
make_test_table(&mut db);
db.begin_transaction().expect("BEGIN");
crate::TriggerExecutor::create_trigger(&mut db, &create_trigger_stmt("tr2"))
.expect("CREATE TRIGGER inside txn");
assert!(db.catalog.get_trigger("tr2").is_some(), "trigger visible inside txn");
db.rollback_transaction().expect("ROLLBACK after CREATE TRIGGER");
assert!(
db.catalog.get_trigger("tr2").is_none(),
"trigger must be gone after ROLLBACK (transactional DDL, #5497)"
);
crate::TriggerExecutor::create_trigger(&mut db, &create_trigger_stmt("tr2"))
.expect("re-CREATE TRIGGER after rollback");
assert!(db.catalog.get_trigger("tr2").is_some());
}
#[test]
fn test_create_trigger_kept_after_commit() {
let mut db = Database::new();
make_test_table(&mut db);
db.begin_transaction().expect("BEGIN");
crate::TriggerExecutor::create_trigger(&mut db, &create_trigger_stmt("tr2"))
.expect("CREATE TRIGGER inside txn");
db.commit_transaction().expect("COMMIT after CREATE TRIGGER");
assert!(
db.catalog.get_trigger("tr2").is_some(),
"trigger must survive COMMIT (transactional DDL, #5497)"
);
}
#[test]
fn test_drop_trigger_rolled_back_with_transaction() {
let mut db = Database::new();
make_test_table(&mut db);
crate::TriggerExecutor::create_trigger(&mut db, &create_trigger_stmt("tr2"))
.expect("CREATE TRIGGER");
db.begin_transaction().expect("BEGIN");
crate::TriggerExecutor::drop_trigger(
&mut db,
&DropTriggerStmt { trigger_name: "tr2".to_string(), cascade: false, if_exists: false },
)
.expect("DROP TRIGGER inside txn");
assert!(db.catalog.get_trigger("tr2").is_none(), "trigger dropped inside txn");
db.rollback_transaction().expect("ROLLBACK after DROP TRIGGER");
assert!(
db.catalog.get_trigger("tr2").is_some(),
"dropped trigger must be restored after ROLLBACK (#5497)"
);
}
#[test]
fn test_rename_table_rewrites_trigger_body_and_fires() {
use crate::{AlterTableExecutor, InsertExecutor, SelectExecutor};
let mut db = Database::new();
for sql in [
"CREATE TABLE t1 (x INTEGER, d VARCHAR(50));",
"CREATE TABLE t2 (y INTEGER);",
"CREATE TABLE t3 (z INTEGER);",
"CREATE TABLE log (msg VARCHAR(50));",
] {
match vibesql_parser::Parser::parse_sql(sql).unwrap() {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
other => panic!("Expected CreateTable, got {:?}", other),
}
}
let trigger_sql = "CREATE TRIGGER r1 AFTER INSERT ON t1 BEGIN\n \
INSERT INTO log VALUES('fired-' || (SELECT count(*) FROM t3));\nEND";
match vibesql_parser::Parser::parse_sql(trigger_sql).unwrap() {
vibesql_ast::Statement::CreateTrigger(stmt) => {
crate::TriggerExecutor::create_trigger_with_sql(&mut db, &stmt, Some(trigger_sql))
.unwrap();
}
other => panic!("Expected CreateTrigger, got {:?}", other),
}
for sql in ["INSERT INTO t3 VALUES (1);", "INSERT INTO t3 VALUES (2);"] {
match vibesql_parser::Parser::parse_sql(sql).unwrap() {
vibesql_ast::Statement::Insert(stmt) => {
InsertExecutor::execute(&mut db, &stmt).unwrap();
}
other => panic!("Expected Insert, got {:?}", other),
}
}
match vibesql_parser::Parser::parse_sql("ALTER TABLE t3 RENAME TO t5;").unwrap() {
vibesql_ast::Statement::AlterTable(stmt) => {
AlterTableExecutor::execute(&stmt, &mut db).unwrap();
}
other => panic!("Expected AlterTable, got {:?}", other),
}
let schema = crate::sqlite_schema::execute_sqlite_schema_query(&db.catalog).unwrap();
let trigger_row = schema
.rows
.iter()
.find(|r| matches!(&r.values[0], vibesql_types::SqlValue::Varchar(s) if s.as_str() == "trigger"))
.expect("trigger row in sqlite_master");
let sql_text = match &trigger_row.values[4] {
vibesql_types::SqlValue::Varchar(s) => s.to_string(),
other => panic!("expected sql text, got {:?}", other),
};
let expected = "CREATE TRIGGER r1 AFTER INSERT ON t1 BEGIN\n \
INSERT INTO log VALUES('fired-' || (SELECT count(*) FROM \"t5\"));\nEND";
assert_eq!(sql_text, expected, "sqlite_master.sql not rewritten to match SQLite");
match vibesql_parser::Parser::parse_sql("INSERT INTO t1 VALUES (10, 'a');").unwrap() {
vibesql_ast::Statement::Insert(stmt) => {
InsertExecutor::execute(&mut db, &stmt).unwrap();
}
other => panic!("Expected Insert, got {:?}", other),
}
let select = match vibesql_parser::Parser::parse_sql("SELECT msg FROM log;").unwrap() {
vibesql_ast::Statement::Select(stmt) => stmt,
other => panic!("Expected Select, got {:?}", other),
};
let rows = SelectExecutor::new(&db).execute(&select).expect("select from log");
assert_eq!(rows.len(), 1, "trigger should have inserted exactly one log row");
assert_eq!(
rows[0].values[0],
vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("fired-2")),
"trigger did not fire correctly against the renamed table"
);
}
#[test]
fn test_create_temp_trigger_parses_records_schema_and_fires() {
use crate::{InsertExecutor, SelectExecutor};
let mut db = Database::new();
for sql in ["CREATE TABLE t1 (x INTEGER);", "CREATE TABLE log (y INTEGER);"] {
match vibesql_parser::Parser::parse_sql(sql).unwrap() {
vibesql_ast::Statement::CreateTable(stmt) => {
CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
other => panic!("Expected CreateTable, got {:?}", other),
}
}
let trigger_sql =
"CREATE TEMP TRIGGER tr1 AFTER INSERT ON t1 BEGIN INSERT INTO log VALUES(new.x + 100); END";
match vibesql_parser::Parser::parse_sql(trigger_sql).unwrap() {
vibesql_ast::Statement::CreateTrigger(stmt) => {
assert_eq!(stmt.schema.as_deref(), Some("temp"), "TEMP modifier sets schema");
crate::TriggerExecutor::create_trigger(&mut db, &stmt).unwrap();
}
other => panic!("Expected CreateTrigger, got {:?}", other),
}
let def = db.catalog.get_trigger("tr1").expect("temp trigger registered");
assert!(def.is_temp(), "stored trigger must record the temp schema");
match vibesql_parser::Parser::parse_sql("INSERT INTO t1 VALUES (5);").unwrap() {
vibesql_ast::Statement::Insert(stmt) => {
InsertExecutor::execute(&mut db, &stmt).unwrap();
}
other => panic!("Expected Insert, got {:?}", other),
}
let select = match vibesql_parser::Parser::parse_sql("SELECT y FROM log;").unwrap() {
vibesql_ast::Statement::Select(stmt) => stmt,
other => panic!("Expected Select, got {:?}", other),
};
let rows = SelectExecutor::new(&db).execute(&select).expect("select from log");
assert_eq!(rows.len(), 1, "temp trigger should have fired once");
assert_eq!(
rows[0].values[0],
vibesql_types::SqlValue::Integer(105),
"temp trigger body did not fire with the inserted NEW row"
);
}