use stoolap::Database;
fn setup() -> Database {
Database::open_in_memory().expect("Failed to create in-memory database")
}
fn setup_parent_child(db: &Database) {
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"INSERT INTO parents VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie')",
(),
)
.unwrap();
}
fn count(db: &Database, sql: &str) -> i64 {
let mut rows = db.query(sql, ()).unwrap();
if let Some(row) = rows.next() {
let row = row.unwrap();
return row.get::<i64>(0).unwrap();
}
0
}
#[test]
fn test_fk_insert_valid_parent() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (2, 2, 'Child2')", ())
.unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 2);
}
#[test]
fn test_fk_insert_invalid_parent() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
let result = db.execute("INSERT INTO children VALUES (1, 999, 'Orphan')", ());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("foreign key"),
"Expected FK error, got: {}",
err
);
}
#[test]
fn test_fk_insert_null_allowed() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, NULL, 'NoParent')", ())
.unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 1);
}
#[test]
fn test_fk_insert_select_valid() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute(
"CREATE TABLE temp_data (id INTEGER PRIMARY KEY, pid INTEGER, n TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO temp_data VALUES (10, 1, 'Via Select')", ())
.unwrap();
let result = db.execute("INSERT INTO children SELECT * FROM temp_data", ());
assert!(result.is_ok());
}
#[test]
fn test_fk_insert_select_invalid() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute(
"CREATE TABLE temp_data (id INTEGER PRIMARY KEY, pid INTEGER, n TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO temp_data VALUES (10, 999, 'Bad Ref')", ())
.unwrap();
let result = db.execute("INSERT INTO children SELECT * FROM temp_data", ());
assert!(result.is_err());
}
#[test]
fn test_fk_delete_restrict_blocks() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("DELETE FROM parents WHERE id = 1", ());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("foreign key") || err.contains("referenced"),
"Expected FK error, got: {}",
err
);
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 1"), 1);
}
#[test]
fn test_fk_delete_no_children_ok() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("DELETE FROM parents WHERE id = 3", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 3"), 0);
}
#[test]
fn test_fk_delete_cascade() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE orders (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE, item TEXT)",
(),
).unwrap();
db.execute(
"INSERT INTO orders VALUES (1, 1, 'Item1'), (2, 1, 'Item2'), (3, 2, 'Item3')",
(),
)
.unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 1"), 0);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM orders WHERE parent_id = 1"),
0
);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM orders WHERE parent_id = 2"),
1
);
}
#[test]
fn test_fk_delete_set_null() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE orders (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE SET NULL, item TEXT)",
(),
).unwrap();
db.execute(
"INSERT INTO orders VALUES (1, 1, 'Item1'), (2, 1, 'Item2'), (3, 2, 'Item3')",
(),
)
.unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 1"), 0);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM orders WHERE parent_id IS NULL"),
2
);
assert_eq!(count(&db, "SELECT COUNT(*) FROM orders"), 3);
}
#[test]
fn test_fk_drop_parent_blocked() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("DROP TABLE parents", ());
assert!(result.is_err());
}
#[test]
fn test_fk_drop_child_ok() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("DROP TABLE children", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents"), 3);
}
#[test]
fn test_fk_truncate_parent_blocked() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("TRUNCATE TABLE parents", ());
assert!(result.is_err());
}
#[test]
fn test_fk_truncate_child_ok() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("TRUNCATE TABLE children", ()).unwrap();
}
#[test]
fn test_fk_create_table_invalid_parent() {
let db = setup();
let result = db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES nonexistent(id))",
(),
);
assert!(result.is_err());
}
#[test]
fn test_fk_create_table_table_level_constraint() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER, name TEXT, FOREIGN KEY(parent_id) REFERENCES parents(id))",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("INSERT INTO children VALUES (2, 999, 'Bad')", ());
assert!(result.is_err());
}
#[test]
fn test_fk_create_table_with_actions() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE orders (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY(parent_id) REFERENCES parents(id) ON DELETE CASCADE ON UPDATE CASCADE)",
(),
).unwrap();
db.execute("INSERT INTO orders VALUES (1, 1)", ()).unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM orders"), 0);
}
#[test]
fn test_fk_multiple_fk_columns() {
let db = setup();
db.execute(
"CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE managers (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"INSERT INTO departments VALUES (1, 'Engineering'), (2, 'Sales')",
(),
)
.unwrap();
db.execute("INSERT INTO managers VALUES (10, 'Alice'), (20, 'Bob')", ())
.unwrap();
db.execute(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id), mgr_id INTEGER REFERENCES managers(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO employees VALUES (1, 1, 10, 'Employee1')", ())
.unwrap();
let result = db.execute("INSERT INTO employees VALUES (2, 999, 10, 'BadDept')", ());
assert!(result.is_err());
let result = db.execute("INSERT INTO employees VALUES (3, 1, 999, 'BadMgr')", ());
assert!(result.is_err());
db.execute("INSERT INTO employees VALUES (4, 1, NULL, 'NoMgr')", ())
.unwrap();
}
#[test]
fn test_fk_survives_restart() {
let dir = tempfile::tempdir().unwrap();
let path = format!("file://{}", dir.path().to_str().unwrap());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
}
{
let db = Database::open(&path).expect("Failed to reopen database");
db.execute("INSERT INTO children VALUES (2, 1, 'Child2')", ())
.unwrap();
let result = db.execute("INSERT INTO children VALUES (3, 999, 'Orphan')", ());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("foreign key"),
"Expected FK error after restart, got: {}",
err
);
}
}
#[test]
fn test_fk_transaction_rollback() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("INSERT INTO children VALUES (2, 999, 'Bad')", ());
assert!(result.is_err());
db.execute("ROLLBACK", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 0);
}
#[test]
fn test_fk_no_action_blocks() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE NO ACTION, name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("DELETE FROM parents WHERE id = 1", ());
assert!(result.is_err());
}
#[test]
fn test_non_fk_table_unaffected() {
let db = setup();
db.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)", ())
.unwrap();
db.execute("INSERT INTO items VALUES (1, 'Item1')", ())
.unwrap();
db.execute("INSERT INTO items VALUES (2, 'Item2')", ())
.unwrap();
db.execute("UPDATE items SET name = 'Updated' WHERE id = 1", ())
.unwrap();
db.execute("DELETE FROM items WHERE id = 2", ()).unwrap();
db.execute("TRUNCATE TABLE items", ()).unwrap();
db.execute("DROP TABLE items", ()).unwrap();
}
#[test]
fn test_fk_default_references_pk() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents, name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("INSERT INTO children VALUES (2, 999, 'Bad')", ());
assert!(result.is_err());
}
#[test]
fn test_fk_update_fk_column_to_valid() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("UPDATE children SET parent_id = 2 WHERE id = 1", ())
.unwrap();
assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE parent_id = 2"),
1
);
}
#[test]
fn test_fk_update_fk_column_to_invalid() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("UPDATE children SET parent_id = 999 WHERE id = 1", ());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("foreign key"),
"Expected FK error, got: {}",
err
);
}
#[test]
fn test_fk_update_non_fk_column_ok() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("UPDATE children SET name = 'Updated' WHERE id = 1", ())
.unwrap();
}
#[test]
fn test_fk_cascade_rollback_atomicity() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE, name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (2, 1, 'Child2')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (3, 2, 'Child3')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
db.execute("ROLLBACK", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents"), 3);
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 3);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE parent_id = 1"),
2
);
}
#[test]
fn test_fk_sees_uncommitted_parent_insert() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("INSERT INTO parents VALUES (100, 'NewParent')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (1, 100, 'Child')", ())
.unwrap();
db.execute("COMMIT", ()).unwrap();
assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE parent_id = 100"),
1
);
}
#[test]
fn test_fk_set_null_rollback_atomicity() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE SET NULL, name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
db.execute("ROLLBACK", ()).unwrap();
assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE parent_id = 1"),
1
);
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 1"), 1);
}
#[test]
fn test_fk_cascade_update_pk_rejected() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON UPDATE CASCADE, name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, 1, 'Child1')", ())
.unwrap();
let result = db.execute("UPDATE parents SET id = 100 WHERE id = 1", ());
assert!(result.is_err(), "UPDATE on PK should be rejected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("primary key"),
"Error should mention primary key, got: {}",
err
);
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents WHERE id = 1"), 1);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE parent_id = 1"),
1
);
}
#[test]
fn test_fk_drop_parent_cleans_child_fk() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("DROP TABLE parents", ()).unwrap();
db.execute("INSERT INTO children VALUES (1, 999, 'Orphan')", ())
.unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 1);
}
#[test]
fn test_fk_multi_level_cascade_delete() {
let db = setup();
db.execute(
"CREATE TABLE grandparents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute("CREATE TABLE parents (id INTEGER PRIMARY KEY, gp_id INTEGER REFERENCES grandparents(id) ON DELETE CASCADE, name TEXT)", ()).unwrap();
db.execute("CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE, name TEXT)", ()).unwrap();
db.execute("INSERT INTO grandparents VALUES (1, 'GP1'), (2, 'GP2')", ())
.unwrap();
db.execute(
"INSERT INTO parents VALUES (10, 1, 'P1'), (20, 1, 'P2'), (30, 2, 'P3')",
(),
)
.unwrap();
db.execute("INSERT INTO children VALUES (100, 10, 'C1'), (200, 10, 'C2'), (300, 20, 'C3'), (400, 30, 'C4')", ()).unwrap();
db.execute("DELETE FROM grandparents WHERE id = 1", ())
.unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM grandparents"), 1); assert_eq!(count(&db, "SELECT COUNT(*) FROM parents"), 1); assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 1); assert_eq!(
count(&db, "SELECT COUNT(*) FROM children WHERE id = 400"),
1
);
}
#[test]
fn test_fk_multi_level_cascade_restrict_blocks() {
let db = setup();
db.execute(
"CREATE TABLE grandparents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute("CREATE TABLE parents (id INTEGER PRIMARY KEY, gp_id INTEGER REFERENCES grandparents(id) ON DELETE CASCADE, name TEXT)", ()).unwrap();
db.execute("CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE RESTRICT, name TEXT)", ()).unwrap();
db.execute("INSERT INTO grandparents VALUES (1, 'GP1')", ())
.unwrap();
db.execute("INSERT INTO parents VALUES (10, 1, 'P1')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (100, 10, 'C1')", ())
.unwrap();
let result = db.execute("DELETE FROM grandparents WHERE id = 1", ());
assert!(
result.is_err(),
"Should fail because grandchild RESTRICT blocks cascade"
);
}
#[test]
fn test_fk_drop_table_with_null_fk_children() {
let db = setup();
setup_parent_child(&db);
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
).unwrap();
db.execute("INSERT INTO children VALUES (1, NULL, 'Orphan1')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (2, NULL, 'Orphan2')", ())
.unwrap();
db.execute("DROP TABLE parents", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 2);
}
#[test]
fn test_fk_drop_table_blocked_for_cascade_fk() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE, name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ())
.unwrap();
let result = db.execute("DROP TABLE parents", ());
assert!(
result.is_err(),
"DROP TABLE should be blocked when child rows with CASCADE FK exist"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("still reference it"),
"Error should mention referencing rows: {}",
err
);
db.execute("DELETE FROM children WHERE parent_id = 1", ())
.unwrap();
db.execute("DROP TABLE parents", ()).unwrap();
}
#[test]
fn test_fk_drop_table_blocked_for_set_null_fk() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE SET NULL, name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ())
.unwrap();
let result = db.execute("DROP TABLE parents", ());
assert!(
result.is_err(),
"DROP TABLE should be blocked when child rows with SET NULL FK exist"
);
db.execute("UPDATE children SET parent_id = NULL WHERE id = 10", ())
.unwrap();
db.execute("DROP TABLE parents", ()).unwrap();
}
#[test]
fn test_fk_drop_table_sees_uncommitted_child_deletes() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("DELETE FROM children WHERE parent_id = 1", ())
.unwrap();
let result = db.execute("DROP TABLE parents", ());
assert!(
result.is_ok(),
"DROP TABLE should succeed after uncommitted child delete in same txn: {:?}",
result
);
db.execute("COMMIT", ()).unwrap();
let result = db.execute("SELECT * FROM parents", ());
assert!(result.is_err(), "parents table should not exist after DROP");
}
#[test]
fn test_fk_restrict_sees_uncommitted_child_delete() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id) ON DELETE RESTRICT, name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("DELETE FROM children WHERE parent_id = 1", ())
.unwrap();
let result = db.execute("DELETE FROM parents WHERE id = 1", ());
assert!(
result.is_ok(),
"DELETE parent should succeed after uncommitted child delete in same txn: {:?}",
result
);
db.execute("COMMIT", ()).unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM parents"), 0);
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 0);
}
#[test]
fn test_fk_insert_blocked_after_uncommitted_parent_delete() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id), name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'Alice')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
db.execute("DELETE FROM parents WHERE id = 1", ()).unwrap();
let result = db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ());
assert!(
result.is_err(),
"INSERT child should fail when parent was deleted (uncommitted) in same txn"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("does not exist"),
"Error should mention parent not existing: {}",
err
);
db.execute("ROLLBACK", ()).unwrap();
db.execute("INSERT INTO children VALUES (10, 1, 'Child1')", ())
.unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM children"), 1);
}
#[test]
fn test_fk_auto_index_creation() {
let db = setup();
db.execute(
"CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE employees (
id INTEGER PRIMARY KEY,
dept_id INTEGER REFERENCES departments(id),
name TEXT
)",
(),
)
.unwrap();
let rows = db.query("SHOW INDEXES FROM employees", ()).unwrap();
let mut found_fk_index = false;
for row in rows {
let row = row.unwrap();
let idx_name: String = row.get(1).unwrap(); if idx_name.contains("fk_") && idx_name.contains("dept_id") {
found_fk_index = true;
}
}
assert!(
found_fk_index,
"FK auto-index should be created on dept_id column"
);
}
#[test]
fn test_fk_auto_index_skips_unique_column() {
let db = setup();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (
id INTEGER PRIMARY KEY,
parent_id INTEGER UNIQUE REFERENCES parents(id),
name TEXT
)",
(),
)
.unwrap();
let rows = db.query("SHOW INDEXES FROM children", ()).unwrap();
let mut found_fk_index = false;
for row in rows {
let row = row.unwrap();
let idx_name: String = row.get(1).unwrap(); if idx_name.starts_with("fk_") {
found_fk_index = true;
}
}
assert!(
!found_fk_index,
"FK auto-index should NOT be created when column already has UNIQUE index"
);
}
#[test]
fn test_fk_cascade_delete_performance_with_auto_index() {
let db = setup();
db.execute(
"CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE products (
id INTEGER PRIMARY KEY,
cat_id INTEGER REFERENCES categories(id) ON DELETE CASCADE,
name TEXT
)",
(),
)
.unwrap();
for i in 1..=10 {
db.execute(
&format!("INSERT INTO categories VALUES ({}, 'Cat{}')", i, i),
(),
)
.unwrap();
}
for i in 1..=1000 {
let cat_id = (i % 10) + 1;
db.execute(
&format!(
"INSERT INTO products VALUES ({}, {}, 'Prod{}')",
i, cat_id, i
),
(),
)
.unwrap();
}
assert_eq!(count(&db, "SELECT COUNT(*) FROM products"), 1000);
let start = std::time::Instant::now();
db.execute("DELETE FROM categories WHERE id = 1", ())
.unwrap();
let elapsed = start.elapsed();
assert_eq!(count(&db, "SELECT COUNT(*) FROM products"), 900);
assert_eq!(count(&db, "SELECT COUNT(*) FROM categories"), 9);
assert!(
elapsed.as_millis() < 100,
"CASCADE DELETE took {}ms — expected <100ms with FK index",
elapsed.as_millis()
);
}
#[test]
fn test_fk_set_null_on_not_null_column_rejected() {
let db = setup();
setup_parent_child(&db);
let result = db.execute(
"CREATE TABLE bad_child (
id INTEGER PRIMARY KEY,
parent_id INTEGER NOT NULL REFERENCES parents(id) ON DELETE SET NULL,
name TEXT
)",
(),
);
assert!(
result.is_err(),
"SET NULL on NOT NULL column must be rejected at CREATE TABLE time"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("SET NULL") && err.contains("NOT NULL"),
"error should mention SET NULL and NOT NULL, got: {}",
err
);
}
#[test]
fn test_fk_cascade_restrict_no_partial_state() {
let db = setup();
db.execute("CREATE TABLE gp (id INTEGER PRIMARY KEY, name TEXT)", ())
.unwrap();
db.execute(
"CREATE TABLE par (id INTEGER PRIMARY KEY, gp_id INTEGER REFERENCES gp(id) ON DELETE CASCADE, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE ch (id INTEGER PRIMARY KEY, par_id INTEGER REFERENCES par(id) ON DELETE RESTRICT, name TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO gp VALUES (1, 'G1')", ()).unwrap();
db.execute("INSERT INTO par VALUES (10, 1, 'P1')", ())
.unwrap();
db.execute("INSERT INTO ch VALUES (100, 10, 'C1')", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
let result = db.execute("DELETE FROM gp WHERE id = 1", ());
assert!(result.is_err(), "RESTRICT on grandchild must block");
let par_count = count(&db, "SELECT COUNT(*) FROM par WHERE id = 10");
assert_eq!(
par_count, 1,
"parent row must still exist — RESTRICT failure should prevent cascade deletes"
);
db.execute("ROLLBACK", ()).unwrap();
}
#[test]
fn test_fk_update_constant_no_dirty_state_in_explicit_txn() {
let db = Database::open_in_memory().unwrap();
db.execute(
"CREATE TABLE parents (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE children (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parents(id))",
(),
)
.unwrap();
db.execute("INSERT INTO parents VALUES (1, 'p1')", ())
.unwrap();
db.execute("INSERT INTO children VALUES (1, 1)", ())
.unwrap();
db.execute("BEGIN", ()).unwrap();
let result = db.execute("UPDATE children SET parent_id = 999 WHERE id = 1", ());
assert!(result.is_err(), "FK violation expected");
let parent_id = count(&db, "SELECT parent_id FROM children WHERE id = 1");
assert_eq!(
parent_id, 1,
"row must be unchanged after failed FK update in explicit transaction"
);
db.execute("ROLLBACK", ()).unwrap();
}
#[test]
fn test_recursive_on_update_cascade() {
let db = setup();
db.execute(
"CREATE TABLE regions (id INTEGER PRIMARY KEY, code INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE countries (id INTEGER PRIMARY KEY, region_code INTEGER UNIQUE REFERENCES regions(code) ON UPDATE CASCADE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE cities (id INTEGER PRIMARY KEY, region_code INTEGER REFERENCES countries(region_code) ON UPDATE CASCADE)",
(),
)
.unwrap();
db.execute("INSERT INTO regions VALUES (1, 100)", ())
.unwrap();
db.execute("INSERT INTO countries VALUES (1, 100)", ())
.unwrap();
db.execute("INSERT INTO cities VALUES (1, 100)", ())
.unwrap();
db.execute("UPDATE regions SET code = 200 WHERE id = 1", ())
.expect("CASCADE update should succeed");
let country_code = count(&db, "SELECT region_code FROM countries WHERE id = 1");
assert_eq!(country_code, 200, "Country should have cascaded to 200");
let city_code = count(&db, "SELECT region_code FROM cities WHERE id = 1");
assert_eq!(
city_code, 200,
"City should have recursively cascaded to 200"
);
}
#[test]
fn test_recursive_on_update_cascade_blocked_by_restrict() {
let db = setup();
db.execute(
"CREATE TABLE r_parent (id INTEGER PRIMARY KEY, code INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE r_child (id INTEGER PRIMARY KEY, pcode INTEGER UNIQUE REFERENCES r_parent(code) ON UPDATE CASCADE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE r_grandchild (id INTEGER PRIMARY KEY, pcode INTEGER REFERENCES r_child(pcode) ON UPDATE RESTRICT)",
(),
)
.unwrap();
db.execute("INSERT INTO r_parent VALUES (1, 10)", ())
.unwrap();
db.execute("INSERT INTO r_child VALUES (1, 10)", ())
.unwrap();
db.execute("INSERT INTO r_grandchild VALUES (1, 10)", ())
.unwrap();
let result = db.execute("UPDATE r_parent SET code = 20 WHERE id = 1", ());
assert!(
result.is_err(),
"Grandchild RESTRICT should block recursive cascade"
);
let parent_code = count(&db, "SELECT code FROM r_parent WHERE id = 1");
assert_eq!(parent_code, 10, "Parent should be unchanged");
}
#[test]
fn test_set_null_with_deeper_restrict() {
let db = setup();
db.execute(
"CREATE TABLE sn_a (id INTEGER PRIMARY KEY, code INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE sn_b (id INTEGER PRIMARY KEY, code INTEGER UNIQUE REFERENCES sn_a(code) ON UPDATE SET NULL)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE sn_c (id INTEGER PRIMARY KEY, code INTEGER REFERENCES sn_b(code) ON UPDATE RESTRICT)",
(),
)
.unwrap();
db.execute("INSERT INTO sn_a VALUES (1, 10)", ()).unwrap();
db.execute("INSERT INTO sn_b VALUES (1, 10)", ()).unwrap();
db.execute("INSERT INTO sn_c VALUES (1, 10)", ()).unwrap();
let result = db.execute("UPDATE sn_a SET code = 20 WHERE id = 1", ());
assert!(
result.is_err(),
"Deeper RESTRICT behind SET NULL should block the update"
);
let a_code = count(&db, "SELECT code FROM sn_a WHERE id = 1");
assert_eq!(a_code, 10, "A should be unchanged");
}
#[test]
fn test_multi_column_cascade_dispatches_correctly() {
let db = setup();
db.execute(
"CREATE TABLE mc_parent (id INTEGER PRIMARY KEY, a INTEGER UNIQUE, b INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE mc_child_a (id INTEGER PRIMARY KEY, pa INTEGER REFERENCES mc_parent(a) ON UPDATE CASCADE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE mc_child_b (id INTEGER PRIMARY KEY, pb INTEGER REFERENCES mc_parent(b) ON UPDATE CASCADE)",
(),
)
.unwrap();
db.execute("INSERT INTO mc_parent VALUES (1, 10, 20)", ())
.unwrap();
db.execute("INSERT INTO mc_child_a VALUES (1, 10)", ())
.unwrap();
db.execute("INSERT INTO mc_child_b VALUES (1, 20)", ())
.unwrap();
db.execute("UPDATE mc_parent SET a = 11, b = 22 WHERE id = 1", ())
.expect("Multi-column cascade should succeed");
let child_a = count(&db, "SELECT pa FROM mc_child_a WHERE id = 1");
assert_eq!(child_a, 11, "child_a should cascade from 10 to 11");
let child_b = count(&db, "SELECT pb FROM mc_child_b WHERE id = 1");
assert_eq!(child_b, 22, "child_b should cascade from 20 to 22");
}
#[test]
fn test_restrict_precheck_no_dirty_state_in_explicit_txn() {
let db = setup();
db.execute(
"CREATE TABLE sp (id INTEGER PRIMARY KEY, code INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE sc (id INTEGER PRIMARY KEY, code INTEGER REFERENCES sp(code) ON UPDATE RESTRICT)",
(),
)
.unwrap();
db.execute("INSERT INTO sp VALUES (1, 10)", ()).unwrap();
db.execute("INSERT INTO sc VALUES (1, 10)", ()).unwrap();
db.execute("BEGIN", ()).unwrap();
let result = db.execute("UPDATE sp SET code = 20 WHERE id = 1", ());
assert!(result.is_err(), "RESTRICT should reject");
let code = count(&db, "SELECT code FROM sp WHERE id = 1");
assert_eq!(code, 10, "Parent should be unchanged after failed UPDATE");
db.execute("ROLLBACK", ()).unwrap();
}
#[test]
fn test_noop_update_no_false_restrict_rejection() {
let db = setup();
db.execute(
"CREATE TABLE np (id INTEGER PRIMARY KEY, code INTEGER UNIQUE)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE nc (id INTEGER PRIMARY KEY, code INTEGER REFERENCES np(code) ON UPDATE RESTRICT)",
(),
)
.unwrap();
db.execute("INSERT INTO np VALUES (1, 10)", ()).unwrap();
db.execute("INSERT INTO nc VALUES (1, 10)", ()).unwrap();
let result = db.execute(
"UPDATE np SET code = 20 WHERE code = 10 AND id + 1 = 999",
(),
);
assert!(
result.is_ok(),
"No-op update should succeed, got: {:?}",
result.unwrap_err()
);
let code = count(&db, "SELECT code FROM np WHERE id = 1");
assert_eq!(code, 10, "Row unchanged");
}
#[test]
fn test_correlated_update_validates_fk_parent() {
let db = setup();
db.execute(
"CREATE TABLE fk_parent (id INTEGER PRIMARY KEY, name TEXT)",
(),
)
.unwrap();
db.execute(
"CREATE TABLE fk_child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES fk_parent(id))",
(),
)
.unwrap();
db.execute(
"CREATE TABLE mapping (id INTEGER PRIMARY KEY, new_pid INTEGER)",
(),
)
.unwrap();
db.execute("INSERT INTO fk_parent VALUES (1, 'Parent1')", ())
.unwrap();
db.execute("INSERT INTO fk_child VALUES (1, 1)", ())
.unwrap();
db.execute("INSERT INTO mapping VALUES (1, 999)", ())
.unwrap();
let result = db.execute(
"UPDATE fk_child SET pid = (SELECT new_pid FROM mapping WHERE mapping.id = fk_child.id) WHERE id = 1",
(),
);
assert!(
result.is_err(),
"Correlated UPDATE to non-existent parent should fail"
);
let pid = count(&db, "SELECT pid FROM fk_child WHERE id = 1");
assert_eq!(pid, 1, "Child should still reference parent 1");
}