Files
turso/tests/integration/trigger.rs
Jussi Saurio 745cdc3aa2 Align trigger sql rewrite behavior with sqlite
SQLite doesn't rewrite INSERT lists or WHEN clause, it instead
lets the trigger go "stale" and will cause runtime errors. This
may not be great behavior, but it's compatible...
2025-11-19 14:29:33 +02:00

1920 lines
62 KiB
Rust

use crate::common::TempDatabase;
#[test]
fn test_create_trigger() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x, y TEXT)").unwrap();
conn.execute(
"CREATE TRIGGER t1 BEFORE INSERT ON test BEGIN
INSERT INTO test VALUES (100, 'triggered');
END",
)
.unwrap();
conn.execute("INSERT INTO test VALUES (1, 'hello')")
.unwrap();
let mut stmt = conn.prepare("SELECT * FROM test ORDER BY rowid").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).cast_text().unwrap().to_string(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Row inserted by trigger goes first
assert_eq!(results[0], (100, "triggered".to_string()));
assert_eq!(results[1], (1, "hello".to_string()));
}
#[test]
fn test_drop_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TRIGGER t1 BEFORE INSERT ON test BEGIN SELECT 1; END")
.unwrap();
// Verify trigger exists
let mut stmt = conn
.prepare("SELECT name FROM sqlite_schema WHERE type='trigger' AND name='t1'")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 1);
conn.execute("DROP TRIGGER t1").unwrap();
// Verify trigger is gone
let mut stmt = conn
.prepare("SELECT name FROM sqlite_schema WHERE type='trigger' AND name='t1'")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 0);
}
#[test]
fn test_trigger_after_insert() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY, y TEXT)")
.unwrap();
conn.execute("CREATE TABLE log (x INTEGER, y TEXT)")
.unwrap();
conn.execute(
"CREATE TRIGGER t1 AFTER INSERT ON test BEGIN
INSERT INTO log VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
conn.execute("INSERT INTO test VALUES (1, 'hello')")
.unwrap();
let mut stmt = conn.prepare("SELECT * FROM log").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).cast_text().unwrap().to_string(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 1);
assert_eq!(results[0], (1, "hello".to_string()));
}
#[test]
fn test_trigger_when_clause() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE log (x INTEGER)").unwrap();
conn.execute(
"CREATE TRIGGER t1 AFTER INSERT ON test WHEN NEW.y > 10 BEGIN
INSERT INTO log VALUES (NEW.x);
END",
)
.unwrap();
conn.execute("INSERT INTO test VALUES (1, 5)").unwrap();
conn.execute("INSERT INTO test VALUES (2, 15)").unwrap();
let mut stmt = conn.prepare("SELECT * FROM log").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).as_int().unwrap());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 1);
assert_eq!(results[0], 2);
}
#[test]
fn test_trigger_drop_table_drops_triggers() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TRIGGER t1 BEFORE INSERT ON test BEGIN SELECT 1; END")
.unwrap();
// Verify trigger exists
let mut stmt = conn
.prepare("SELECT name FROM sqlite_schema WHERE type='trigger' AND name='t1'")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 1);
conn.execute("DROP TABLE test").unwrap();
// Verify trigger is gone
let mut stmt = conn
.prepare("SELECT name FROM sqlite_schema WHERE type='trigger' AND name='t1'")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 0);
}
#[test]
fn test_trigger_new_old_references() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY, y TEXT)")
.unwrap();
conn.execute("CREATE TABLE log (msg TEXT)").unwrap();
conn.execute("INSERT INTO test VALUES (1, 'hello')")
.unwrap();
conn.execute(
"CREATE TRIGGER t1 AFTER UPDATE ON test BEGIN
INSERT INTO log VALUES ('old=' || OLD.y || ' new=' || NEW.y);
END",
)
.unwrap();
conn.execute("UPDATE test SET y = 'world' WHERE x = 1")
.unwrap();
let mut stmt = conn.prepare("SELECT * FROM log").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 1);
assert_eq!(results[0], "old=hello new=world");
}
#[test]
fn test_multiple_triggers_same_event() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TABLE log (msg TEXT)").unwrap();
conn.execute(
"CREATE TRIGGER t1 BEFORE INSERT ON test BEGIN
INSERT INTO log VALUES ('trigger1');
END",
)
.unwrap();
conn.execute(
"CREATE TRIGGER t2 BEFORE INSERT ON test BEGIN
INSERT INTO log VALUES ('trigger2');
END",
)
.unwrap();
conn.execute("INSERT INTO test VALUES (1)").unwrap();
let mut stmt = conn.prepare("SELECT * FROM log ORDER BY msg").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0], "trigger1");
assert_eq!(results[1], "trigger2");
}
#[test]
fn test_two_triggers_on_same_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE test (x, msg TEXT)").unwrap();
conn.execute("CREATE TABLE log (msg TEXT)").unwrap();
// Trigger A: fires on INSERT to test, inserts into log and test (which would trigger B)
conn.execute(
"CREATE TRIGGER trigger_a AFTER INSERT ON test BEGIN
INSERT INTO log VALUES ('trigger_a fired for x=' || NEW.x);
INSERT INTO test VALUES (NEW.x + 100, 'from_a');
END",
)
.unwrap();
// Trigger B: fires on INSERT to test, inserts into log and test (which would trigger A)
conn.execute(
"CREATE TRIGGER trigger_b AFTER INSERT ON test BEGIN
INSERT INTO log VALUES ('trigger_b fired for x=' || NEW.x);
INSERT INTO test VALUES (NEW.x + 200, 'from_b');
END",
)
.unwrap();
// Insert initial row - this should trigger A, which triggers B, which tries to trigger A again (prevented)
conn.execute("INSERT INTO test VALUES (1, 'initial')")
.unwrap();
// Check log entries to verify recursion was prevented
let mut stmt = conn.prepare("SELECT * FROM log ORDER BY rowid").unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// At minimum, we should see both triggers fire and not infinite loop
assert!(
results.len() >= 2,
"Expected at least 2 log entries, got {}",
results.len()
);
assert!(
results.iter().any(|s| s.contains("trigger_a")),
"trigger_a should have fired"
);
assert!(
results.iter().any(|s| s.contains("trigger_b")),
"trigger_b should have fired"
);
}
#[test]
fn test_trigger_mutual_recursion() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
conn.execute("CREATE TABLE t (id INTEGER, msg TEXT)")
.unwrap();
conn.execute("CREATE TABLE u (id INTEGER, msg TEXT)")
.unwrap();
// Trigger on T: fires on INSERT to t, inserts into u
conn.execute(
"CREATE TRIGGER trigger_on_t AFTER INSERT ON t BEGIN
INSERT INTO u VALUES (NEW.id + 1000, 'from_t');
END",
)
.unwrap();
// Trigger on U: fires on INSERT to u, inserts into t
conn.execute(
"CREATE TRIGGER trigger_on_u AFTER INSERT ON u BEGIN
INSERT INTO t VALUES (NEW.id + 2000, 'from_u');
END",
)
.unwrap();
// Insert initial row into t - this should trigger the chain
conn.execute("INSERT INTO t VALUES (1, 'initial')").unwrap();
// Check that both tables have entries
let mut stmt = conn.prepare("SELECT * FROM t ORDER BY rowid").unwrap();
let mut t_results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
t_results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).cast_text().unwrap().to_string(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let mut stmt = conn.prepare("SELECT * FROM u ORDER BY rowid").unwrap();
let mut u_results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
u_results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).cast_text().unwrap().to_string(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Verify the chain executed without infinite recursion
assert!(!t_results.is_empty(), "Expected at least 1 entry in t");
assert!(!u_results.is_empty(), "Expected at least 1 entry in u");
// Verify initial insert
assert_eq!(t_results[0], (1, "initial".to_string()));
// Verify trigger on t fired (inserted into u)
assert_eq!(u_results[0], (1001, "from_t".to_string()));
}
#[test]
fn test_after_insert_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table and log table
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
.unwrap();
conn.execute("CREATE TABLE audit_log (action TEXT, item_id INTEGER, item_name TEXT)")
.unwrap();
// Create AFTER INSERT trigger
conn.execute(
"CREATE TRIGGER after_insert_items
AFTER INSERT ON items
BEGIN
INSERT INTO audit_log VALUES ('INSERT', NEW.id, NEW.name);
END",
)
.unwrap();
// Insert data
conn.execute("INSERT INTO items VALUES (1, 'apple')")
.unwrap();
conn.execute("INSERT INTO items VALUES (2, 'banana')")
.unwrap();
// Verify audit log
let mut stmt = conn
.prepare("SELECT * FROM audit_log ORDER BY rowid")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).cast_text().unwrap().to_string(),
row.get_value(1).as_int().unwrap(),
row.get_value(2).cast_text().unwrap().to_string(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0], ("INSERT".to_string(), 1, "apple".to_string()));
assert_eq!(results[1], ("INSERT".to_string(), 2, "banana".to_string()));
}
#[test]
fn test_before_update_of_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table with multiple columns
conn.execute("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")
.unwrap();
conn.execute(
"CREATE TABLE price_history (product_id INTEGER, old_price INTEGER, new_price INTEGER)",
)
.unwrap();
// Create BEFORE UPDATE OF trigger - only fires when price column is updated
conn.execute(
"CREATE TRIGGER before_update_price
BEFORE UPDATE OF price ON products
BEGIN
INSERT INTO price_history VALUES (OLD.id, OLD.price, NEW.price);
END",
)
.unwrap();
// Insert initial data
conn.execute("INSERT INTO products VALUES (1, 'widget', 100)")
.unwrap();
conn.execute("INSERT INTO products VALUES (2, 'gadget', 200)")
.unwrap();
// Update price - should fire trigger
conn.execute("UPDATE products SET price = 150 WHERE id = 1")
.unwrap();
// Update name only - should NOT fire trigger
conn.execute("UPDATE products SET name = 'super widget' WHERE id = 1")
.unwrap();
// Update both name and price - should fire trigger
conn.execute("UPDATE products SET name = 'mega gadget', price = 250 WHERE id = 2")
.unwrap();
// Verify price history
let mut stmt = conn
.prepare("SELECT * FROM price_history ORDER BY rowid")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).as_int().unwrap(),
row.get_value(2).as_int().unwrap(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Should have 2 entries (not 3, because name-only update didn't fire)
assert_eq!(results.len(), 2);
assert_eq!(results[0], (1, 100, 150));
assert_eq!(results[1], (2, 200, 250));
}
#[test]
fn test_after_update_of_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary INTEGER)")
.unwrap();
conn.execute("CREATE TABLE salary_changes (emp_id INTEGER, old_salary INTEGER, new_salary INTEGER, change_amount INTEGER)")
.unwrap();
// Create AFTER UPDATE OF trigger with multiple statements
conn.execute(
"CREATE TRIGGER after_update_salary
AFTER UPDATE OF salary ON employees
BEGIN
INSERT INTO salary_changes VALUES (NEW.id, OLD.salary, NEW.salary, NEW.salary - OLD.salary);
END",
)
.unwrap();
// Insert initial data
conn.execute("INSERT INTO employees VALUES (1, 'Alice', 50000)")
.unwrap();
conn.execute("INSERT INTO employees VALUES (2, 'Bob', 60000)")
.unwrap();
// Update salary
conn.execute("UPDATE employees SET salary = 55000 WHERE id = 1")
.unwrap();
conn.execute("UPDATE employees SET salary = 65000 WHERE id = 2")
.unwrap();
// Verify salary changes
let mut stmt = conn
.prepare("SELECT * FROM salary_changes ORDER BY rowid")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).as_int().unwrap(),
row.get_value(2).as_int().unwrap(),
row.get_value(3).as_int().unwrap(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0], (1, 50000, 55000, 5000));
assert_eq!(results[1], (2, 60000, 65000, 5000));
}
fn log(s: &str) -> &str {
tracing::info!("{}", s);
s
}
#[test]
fn test_before_delete_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create tables
conn.execute(log(
"CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)",
))
.unwrap();
conn.execute(log(
"CREATE TABLE deleted_users (id INTEGER, username TEXT, deleted_at INTEGER)",
))
.unwrap();
// Create BEFORE DELETE trigger
conn.execute(log("CREATE TRIGGER before_delete_users
BEFORE DELETE ON users
BEGIN
INSERT INTO deleted_users VALUES (OLD.id, OLD.username, 12345);
END"))
.unwrap();
// Insert data
conn.execute(log("INSERT INTO users VALUES (1, 'alice')"))
.unwrap();
conn.execute(log("INSERT INTO users VALUES (2, 'bob')"))
.unwrap();
conn.execute(log("INSERT INTO users VALUES (3, 'charlie')"))
.unwrap();
// Delete some users
conn.execute(log("DELETE FROM users WHERE id = 2")).unwrap();
conn.execute(log("DELETE FROM users WHERE id = 3")).unwrap();
// Verify deleted_users table
let mut stmt = conn
.prepare(log("SELECT * FROM deleted_users ORDER BY id"))
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).cast_text().unwrap().to_string(),
row.get_value(2).as_int().unwrap(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0], (2, "bob".to_string(), 12345));
assert_eq!(results[1], (3, "charlie".to_string(), 12345));
// Verify remaining users
let mut stmt = conn.prepare(log("SELECT COUNT(*) FROM users")).unwrap();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
assert_eq!(row.get_value(0).as_int().unwrap(), 1);
break;
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
}
#[test]
fn test_after_delete_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create tables
conn.execute(
"CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER)",
)
.unwrap();
conn.execute(
"CREATE TABLE order_archive (order_id INTEGER, customer_id INTEGER, amount INTEGER)",
)
.unwrap();
// Create AFTER DELETE trigger
conn.execute(
"CREATE TRIGGER after_delete_orders
AFTER DELETE ON orders
BEGIN
INSERT INTO order_archive VALUES (OLD.id, OLD.customer_id, OLD.amount);
END",
)
.unwrap();
// Insert data
conn.execute("INSERT INTO orders VALUES (1, 100, 50)")
.unwrap();
conn.execute("INSERT INTO orders VALUES (2, 101, 75)")
.unwrap();
conn.execute("INSERT INTO orders VALUES (3, 100, 100)")
.unwrap();
// Delete orders
conn.execute("DELETE FROM orders WHERE customer_id = 100")
.unwrap();
// Verify archive
let mut stmt = conn
.prepare("SELECT * FROM order_archive ORDER BY order_id")
.unwrap();
let mut results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).as_int().unwrap(),
row.get_value(2).as_int().unwrap(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0], (1, 100, 50));
assert_eq!(results[1], (3, 100, 100));
}
#[test]
fn test_trigger_with_multiple_statements() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create tables
conn.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
.unwrap();
conn.execute(
"CREATE TABLE transactions (account_id INTEGER, old_balance INTEGER, new_balance INTEGER)",
)
.unwrap();
conn.execute("CREATE TABLE audit (message TEXT)").unwrap();
// Create trigger with multiple statements
conn.execute(
"CREATE TRIGGER track_balance_changes
AFTER UPDATE OF balance ON accounts
BEGIN
INSERT INTO transactions VALUES (NEW.id, OLD.balance, NEW.balance);
INSERT INTO audit VALUES ('Balance changed for account ' || NEW.id);
END",
)
.unwrap();
// Insert initial data
conn.execute("INSERT INTO accounts VALUES (1, 1000)")
.unwrap();
conn.execute("INSERT INTO accounts VALUES (2, 2000)")
.unwrap();
// Update balances
conn.execute("UPDATE accounts SET balance = 1500 WHERE id = 1")
.unwrap();
conn.execute("UPDATE accounts SET balance = 2500 WHERE id = 2")
.unwrap();
// Verify transactions table
let mut stmt = conn
.prepare("SELECT * FROM transactions ORDER BY rowid")
.unwrap();
let mut trans_results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trans_results.push((
row.get_value(0).as_int().unwrap(),
row.get_value(1).as_int().unwrap(),
row.get_value(2).as_int().unwrap(),
));
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(trans_results.len(), 2);
assert_eq!(trans_results[0], (1, 1000, 1500));
assert_eq!(trans_results[1], (2, 2000, 2500));
// Verify audit table
let mut stmt = conn.prepare("SELECT * FROM audit ORDER BY rowid").unwrap();
let mut audit_results = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
audit_results.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
assert_eq!(audit_results.len(), 2);
assert_eq!(audit_results[0], "Balance changed for account 1");
assert_eq!(audit_results[1], "Balance changed for account 2");
}
#[test]
fn test_alter_table_drop_column_fails_when_trigger_references_new_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table with columns
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create trigger that references y via NEW.y
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO t VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
// Attempting to drop column y should fail because trigger references it
let result = conn.execute("ALTER TABLE t DROP COLUMN y");
assert!(
result.is_err(),
"Dropping column y should fail when trigger references NEW.y"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot drop column") && error_msg.contains("trigger"),
"Error should mention column drop and trigger: {error_msg}",
);
}
#[test]
fn test_alter_table_drop_column_fails_when_trigger_references_old_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table with columns
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create trigger that references y via OLD.y
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE ON t BEGIN
INSERT INTO t VALUES (OLD.x, OLD.y);
END",
)
.unwrap();
// Attempting to drop column y should fail because trigger references it
let result = conn.execute("ALTER TABLE t DROP COLUMN y");
assert!(
result.is_err(),
"Dropping column y should fail when trigger references OLD.y"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot drop column") && error_msg.contains("trigger"),
"Error should mention column drop and trigger: {error_msg}",
);
}
#[test]
fn test_alter_table_drop_column_fails_when_trigger_references_unqualified_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table with columns
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create trigger that references y as unqualified column (in WHEN clause)
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t WHEN y > 10 BEGIN
INSERT INTO t VALUES (NEW.x, 100);
END",
)
.unwrap();
// Attempting to drop column y should fail because trigger references it
let result = conn.execute("ALTER TABLE t DROP COLUMN y");
assert!(
result.is_err(),
"Dropping column y should fail when trigger references it in WHEN clause"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot drop column") && error_msg.contains("trigger"),
"Error should mention column drop and trigger: {error_msg}",
);
}
#[test]
fn test_alter_table_drop_column_succeeds_when_trigger_references_other_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (z INTEGER)").unwrap();
// Create trigger on t that references column from u (not t)
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(z) VALUES (NEW.x);
END",
)
.unwrap();
// Dropping column y from t should succeed because trigger doesn't reference it
conn.execute("ALTER TABLE t DROP COLUMN y").unwrap();
// Verify column was dropped
let mut stmt = conn.prepare("PRAGMA table_info(t)").unwrap();
let mut columns = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
columns.push(row.get_value(1).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Should only have x column now
assert_eq!(columns.len(), 1);
assert_eq!(columns[0], "x");
}
#[test]
fn test_alter_table_drop_column_from_other_table_causes_parse_error_when_trigger_fires() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TABLE u (z INTEGER, zer INTEGER)")
.unwrap();
// Create trigger on t that references column zer from u
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(z, zer) VALUES (NEW.x, NEW.x);
END",
)
.unwrap();
// Dropping column zer from u should succeed (trigger is on t, not u)
conn.execute("ALTER TABLE u DROP COLUMN zer").unwrap();
// Verify column was dropped
let mut stmt = conn.prepare("PRAGMA table_info(u)").unwrap();
let mut columns = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
columns.push(row.get_value(1).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Should only have z column now
assert_eq!(columns.len(), 1);
assert_eq!(columns[0], "z");
// Now trying to insert into t should fail because trigger references non-existent column zer
let result = conn.execute("INSERT INTO t VALUES (1)");
assert!(
result.is_err(),
"Insert should fail because trigger references non-existent column zer"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("no column named") || error_msg.contains("zer"),
"Error should mention missing column: {error_msg}",
);
}
#[test]
fn test_alter_table_rename_column_propagates_to_trigger_on_owning_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create trigger on t that references y via NEW.y
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO t VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
// Rename column y to y_new
conn.execute("ALTER TABLE t RENAME COLUMN y TO y_new")
.unwrap();
// Verify trigger SQL was updated
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Trigger SQL should reference y_new instead of y
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
assert_eq!(
normalized_sql,
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN INSERT INTO t VALUES (NEW.x, NEW.y_new); END"
);
}
#[test]
fn test_alter_table_rename_column_propagates_to_trigger_referencing_other_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TABLE u (z INTEGER, zer INTEGER)")
.unwrap();
// Create trigger on t that references column z from u
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(z, zer) VALUES (NEW.x, NEW.x);
END",
)
.unwrap();
// Rename column z to zoo in table u
conn.execute("ALTER TABLE u RENAME COLUMN z TO zoo")
.unwrap();
// Verify trigger SQL was updated to reference zoo
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Trigger SQL should reference zoo instead of z
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
assert_eq!(
normalized_sql,
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN INSERT INTO u (zoo, zer) VALUES (NEW.x, NEW.x); END"
);
}
#[test]
fn test_alter_table_rename_column_propagates_to_trigger_with_multiple_references() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (z INTEGER, zer INTEGER)")
.unwrap();
// Create trigger on t that references y multiple times and z from u
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(z, zer) VALUES (NEW.y, NEW.y);
END",
)
.unwrap();
// Rename column y to y_new in table t
conn.execute("ALTER TABLE t RENAME COLUMN y TO y_new")
.unwrap();
// Verify trigger SQL was updated
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Trigger SQL should reference y_new instead of y
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
assert_eq!(
normalized_sql,
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN INSERT INTO u (z, zer) VALUES (NEW.y_new, NEW.y_new); END"
);
}
#[test]
fn test_alter_table_rename_column_fails_when_trigger_when_clause_references_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create trigger with WHEN clause referencing y
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t WHEN y > 10 BEGIN
INSERT INTO t VALUES (NEW.x, 100);
END",
)
.unwrap();
// Rename column y to y_new should fail (SQLite fails if WHEN clause references the column)
let result = conn.execute("ALTER TABLE t RENAME COLUMN y TO y_new");
assert!(
result.is_err(),
"RENAME COLUMN should fail when trigger WHEN clause references the column"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("error in trigger") && error_msg.contains("no such column"),
"Error should mention trigger and column: {error_msg}",
);
}
#[test]
fn test_alter_table_rename_column_propagates_to_multiple_triggers() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create multiple triggers referencing y
conn.execute(
"CREATE TRIGGER t1 BEFORE INSERT ON t BEGIN
INSERT INTO t VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
conn.execute(
"CREATE TRIGGER t2 AFTER UPDATE ON t BEGIN
INSERT INTO t VALUES (OLD.x, OLD.y);
END",
)
.unwrap();
// Rename column y to y_new
conn.execute("ALTER TABLE t RENAME COLUMN y TO y_new")
.unwrap();
// Verify both triggers were updated
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' ORDER BY name")
.unwrap();
let mut trigger_sqls = Vec::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sqls.push(row.get_value(0).cast_text().unwrap().to_string());
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Both triggers should reference y_new
assert_eq!(trigger_sqls.len(), 2);
let normalized_t1 = trigger_sqls[0]
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let normalized_t2 = trigger_sqls[1]
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert_eq!(
normalized_t1,
"CREATE TRIGGER t1 BEFORE INSERT ON t BEGIN INSERT INTO t VALUES (NEW.x, NEW.y_new); END"
);
assert_eq!(
normalized_t2,
"CREATE TRIGGER t2 AFTER UPDATE ON t BEGIN INSERT INTO t VALUES (OLD.x, OLD.y_new); END"
);
}
#[test]
fn test_alter_table_drop_column_fails_with_old_reference_in_update_trigger() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
// Create UPDATE trigger that references OLD.y
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE ON t BEGIN
INSERT INTO t VALUES (OLD.x, OLD.y);
END",
)
.unwrap();
// Attempting to drop column y should fail
let result = conn.execute("ALTER TABLE t DROP COLUMN y");
assert!(
result.is_err(),
"Dropping column y should fail when UPDATE trigger references OLD.y"
);
}
#[test]
fn test_alter_table_rename_column_in_insert_column_list() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TABLE u (z INTEGER, zer INTEGER)")
.unwrap();
// Create trigger that inserts into u with explicit column list
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(z, zer) VALUES (NEW.x, NEW.x);
END",
)
.unwrap();
// Rename column zer to zercher in table u
conn.execute("ALTER TABLE u RENAME COLUMN zer TO zercher")
.unwrap();
// Verify trigger SQL was updated
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
// Trigger SQL should reference zercher in INSERT column list
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
assert_eq!(
normalized_sql,
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN INSERT INTO u (z, zercher) VALUES (NEW.x, NEW.x); END"
);
}
#[test]
fn test_alter_table_rename_column_in_trigger_table_does_not_rewrite_other_table_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that references both t.x (via NEW.x) and u.x (in INSERT column list)
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(x, z) VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
// Rename column x to x_new in table t (the trigger's owning table)
conn.execute("ALTER TABLE t RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL was updated correctly:
// - NEW.x should become NEW.x_new (refers to table t)
// - INSERT INTO u(x, ...) should remain as x (refers to table u, not t)
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// NEW.x should be rewritten to NEW.x_new
assert!(
normalized_sql.contains("NEW.x_new"),
"Trigger SQL should contain NEW.x_new: {normalized_sql}",
);
// INSERT INTO u (x, ...) should still have x (not x_new) because it refers to table u's column
assert!(
normalized_sql.contains("INSERT INTO u (x,") || normalized_sql.contains("INSERT INTO u(x,"),
"Trigger SQL should contain INSERT INTO u (x, (not u (x_new,): {normalized_sql}",
);
assert!(
!normalized_sql.contains("INSERT INTO u (x_new,") && !normalized_sql.contains("INSERT INTO u(x_new,"),
"Trigger SQL should NOT contain INSERT INTO u (x_new, (x refers to table u): {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_in_insert_target_table_does_not_rewrite_trigger_table_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER PRIMARY KEY, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that references both t.x (via NEW.x) and u.x (in INSERT column list)
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(x, z) VALUES (NEW.x, NEW.y);
END",
)
.unwrap();
// Rename column x to x_new in table u (the INSERT target table)
conn.execute("ALTER TABLE u RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL was updated correctly:
// - NEW.x should remain as NEW.x (refers to table t, not u)
// - INSERT INTO u(x, ...) should become u(x_new, ...) (refers to table u)
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// NEW.x should remain as NEW.x (not rewritten because it refers to table t)
assert!(
normalized_sql.contains("NEW.x"),
"Trigger SQL should contain NEW.x (not NEW.x_new): {normalized_sql}",
);
assert!(
!normalized_sql.contains("NEW.x_new"),
"Trigger SQL should NOT contain NEW.x_new (x refers to table t, not u): {normalized_sql}",
);
// INSERT INTO u (x_new, ...) should have x_new (rewritten because it refers to table u)
assert!(
normalized_sql.contains("INSERT INTO u (x_new,")
|| normalized_sql.contains("INSERT INTO u(x_new,"),
"Trigger SQL should contain INSERT INTO u (x_new, (not u (x,): {normalized_sql}",
);
assert!(
!normalized_sql.contains("INSERT INTO u (x,") && !normalized_sql.contains("INSERT INTO u(x,"),
"Trigger SQL should NOT contain INSERT INTO u (x, (x was renamed to x_new in table u): {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_update_where_clause_does_not_rewrite_target_table_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that updates u, with WHERE clause referencing u.x (unqualified)
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE ON t BEGIN
UPDATE u SET z = NEW.x WHERE x = OLD.x;
END",
)
.unwrap();
// Rename column x to x_new in table t (the trigger's owning table)
conn.execute("ALTER TABLE t RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL: WHERE x should remain as x (refers to table u, not t)
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// NEW.x and OLD.x should be rewritten to x_new
assert!(
normalized_sql.contains("NEW.x_new") && normalized_sql.contains("OLD.x_new"),
"Trigger SQL should contain NEW.x_new and OLD.x_new: {normalized_sql}",
);
// WHERE x should remain as x (not x_new) because it refers to table u's column
assert!(
normalized_sql.contains("WHERE x =") || normalized_sql.contains("WHERE x="),
"Trigger SQL should contain WHERE x = (not WHERE x_new =): {normalized_sql}",
);
assert!(
!normalized_sql.contains("WHERE x_new =") && !normalized_sql.contains("WHERE x_new="),
"Trigger SQL should NOT contain WHERE x_new = (x refers to table u): {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_update_set_column_name_rewritten() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that updates u, setting u.x
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE ON t BEGIN
UPDATE u SET x = NEW.x WHERE u.x = OLD.x;
END",
)
.unwrap();
// Rename column x to x_new in table u (the UPDATE target table)
conn.execute("ALTER TABLE u RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL: SET x should become SET x_new, WHERE u.x should become u.x_new
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// SET x should become SET x_new
assert!(
normalized_sql.contains("SET x_new =") || normalized_sql.contains("SET x_new="),
"Trigger SQL should contain SET x_new =: {normalized_sql}",
);
assert!(
!normalized_sql.contains("SET x =") && !normalized_sql.contains("SET x="),
"Trigger SQL should NOT contain SET x = (x was renamed to x_new): {normalized_sql}",
);
// WHERE u.x should become u.x_new
assert!(
normalized_sql.contains("u.x_new =") || normalized_sql.contains("u.x_new="),
"Trigger SQL should contain u.x_new =: {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_delete_where_clause_does_not_rewrite_target_table_column() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that deletes from u, with WHERE clause referencing u.x (unqualified)
conn.execute(
"CREATE TRIGGER tu AFTER DELETE ON t BEGIN
DELETE FROM u WHERE x = OLD.x;
END",
)
.unwrap();
// Rename column x to x_new in table t (the trigger's owning table)
conn.execute("ALTER TABLE t RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL: WHERE x should remain as x (refers to table u, not t)
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// OLD.x should be rewritten to OLD.x_new
assert!(
normalized_sql.contains("OLD.x_new"),
"Trigger SQL should contain OLD.x_new: {normalized_sql}",
);
// WHERE x should remain as x (not x_new) because it refers to table u's column
assert!(
normalized_sql.contains("WHERE x =") || normalized_sql.contains("WHERE x="),
"Trigger SQL should contain WHERE x = (not WHERE x_new =): {normalized_sql}",
);
assert!(
!normalized_sql.contains("WHERE x_new =") && !normalized_sql.contains("WHERE x_new="),
"Trigger SQL should NOT contain WHERE x_new = (x refers to table u): {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_update_of_column_list_rewritten() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger with UPDATE OF x
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE OF x ON t BEGIN
UPDATE u SET z = NEW.x WHERE x = OLD.x;
END",
)
.unwrap();
// Rename column x to x_new in table t
conn.execute("ALTER TABLE t RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL: UPDATE OF x should become UPDATE OF x_new
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// UPDATE OF x should become UPDATE OF x_new
assert!(
normalized_sql.contains("UPDATE OF x_new"),
"Trigger SQL should contain UPDATE OF x_new: {normalized_sql}",
);
assert!(
!normalized_sql.contains("UPDATE OF x,") && !normalized_sql.contains("UPDATE OF x "),
"Trigger SQL should NOT contain UPDATE OF x (x was renamed to x_new): {normalized_sql}",
);
}
#[test]
fn test_alter_table_rename_column_update_of_multiple_columns_rewritten() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger with UPDATE OF x, y
conn.execute(
"CREATE TRIGGER tu AFTER UPDATE OF x, y ON t BEGIN
UPDATE u SET z = NEW.x WHERE x = OLD.x;
END",
)
.unwrap();
// Rename column x to x_new in table t
conn.execute("ALTER TABLE t RENAME COLUMN x TO x_new")
.unwrap();
// Verify trigger SQL: UPDATE OF x, y should become UPDATE OF x_new, y
let mut stmt = conn
.prepare("SELECT sql FROM sqlite_schema WHERE type='trigger' AND name='tu'")
.unwrap();
let mut trigger_sql = String::new();
loop {
match stmt.step().unwrap() {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
trigger_sql = row.get_value(0).cast_text().unwrap().to_string();
}
turso_core::StepResult::Done => break,
turso_core::StepResult::IO => {
stmt.run_once().unwrap();
}
_ => panic!("Unexpected step result"),
}
}
let normalized_sql = trigger_sql.split_whitespace().collect::<Vec<_>>().join(" ");
// UPDATE OF x, y should become UPDATE OF x_new, y
assert!(
normalized_sql.contains("UPDATE OF x_new, y")
|| normalized_sql.contains("UPDATE OF x_new,y"),
"Trigger SQL should contain UPDATE OF x_new, y: {normalized_sql}",
);
assert!(
!normalized_sql.contains("UPDATE OF x,") && !normalized_sql.contains("UPDATE OF x "),
"Trigger SQL should NOT contain UPDATE OF x (x was renamed to x_new): {normalized_sql}",
);
}
#[test]
fn test_alter_table_drop_column_allows_when_insert_targets_other_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that inserts into u(x) - this should NOT prevent dropping x from t
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO u(x) VALUES (NEW.y);
END",
)
.unwrap();
// Dropping column x from table t should succeed (INSERT INTO u(x) refers to u.x, not t.x)
conn.execute("ALTER TABLE t DROP COLUMN x").unwrap();
}
#[test]
fn test_alter_table_drop_column_allows_when_update_targets_other_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create two tables, both with column 'x'
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
conn.execute("CREATE TABLE u (x INTEGER, z INTEGER)")
.unwrap();
// Create trigger on t that updates u SET x = ... - this should NOT prevent dropping x from t
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
UPDATE u SET x = NEW.y WHERE z = 1;
END",
)
.unwrap();
// Dropping column x from table t should succeed (UPDATE u SET x refers to u.x, not t.x)
conn.execute("ALTER TABLE t DROP COLUMN x").unwrap();
}
#[test]
fn test_alter_table_drop_column_allows_when_insert_targets_owning_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
// Create trigger on t that inserts into t(x) - SQLite allows DROP COLUMN here
// The error only occurs when the trigger is actually executed
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
INSERT INTO t(x) VALUES (NEW.y);
END",
)
.unwrap();
// Dropping column x from table t should succeed (SQLite allows this)
conn.execute("ALTER TABLE t DROP COLUMN x").unwrap();
// Verify that executing the trigger now causes an error
let result = conn.execute("INSERT INTO t VALUES (5)");
assert!(
result.is_err(),
"INSERT should fail because trigger references dropped column"
);
}
#[test]
fn test_alter_table_drop_column_allows_when_update_set_targets_owning_table() {
let db = TempDatabase::new_empty();
let conn = db.connect_limbo();
// Create table
conn.execute("CREATE TABLE t (x INTEGER, y INTEGER)")
.unwrap();
// Create trigger on t that updates t SET x = ... - SQLite allows DROP COLUMN here
// The error only occurs when the trigger is actually executed
conn.execute(
"CREATE TRIGGER tu BEFORE INSERT ON t BEGIN
UPDATE t SET x = NEW.y WHERE y = 1;
END",
)
.unwrap();
// Dropping column x from table t should succeed (SQLite allows this)
conn.execute("ALTER TABLE t DROP COLUMN x").unwrap();
// Verify that executing the trigger now causes an error
let result = conn.execute("INSERT INTO t VALUES (5)");
assert!(
result.is_err(),
"INSERT should fail because trigger references dropped column"
);
}