use spg_sql::ast::{FkAction, ForeignKeyConstraint, MatchType, Statement};
use spg_sql::parser::parse_statement;
fn parse_create_table(sql: &str) -> Vec<ForeignKeyConstraint> {
match parse_statement(sql).expect("parses") {
Statement::CreateTable(t) => t.foreign_keys,
other => panic!("expected CREATE TABLE, got {other:?}"),
}
}
#[test]
fn no_fk_means_empty_vec() {
let fks = parse_create_table("CREATE TABLE u (id INT NOT NULL, name TEXT)");
assert!(fks.is_empty());
}
#[test]
fn column_level_references_normalises_to_table_level() {
let fks =
parse_create_table("CREATE TABLE o (id INT NOT NULL, uid INT NOT NULL REFERENCES u(id))");
assert_eq!(fks.len(), 1);
let fk = &fks[0];
assert_eq!(fk.name, None);
assert_eq!(fk.columns, vec!["uid"]);
assert_eq!(fk.parent_table, "u");
assert_eq!(fk.parent_columns, vec!["id"]);
assert_eq!(fk.on_delete, FkAction::NoAction);
assert_eq!(fk.on_update, FkAction::NoAction);
}
#[test]
fn table_level_foreign_key_basic() {
let fks = parse_create_table(
"CREATE TABLE o (id INT NOT NULL, uid INT NOT NULL, FOREIGN KEY (uid) REFERENCES u(id))",
);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].columns, vec!["uid"]);
assert_eq!(fks[0].parent_table, "u");
assert_eq!(fks[0].parent_columns, vec!["id"]);
}
#[test]
fn table_level_with_constraint_name() {
let fks = parse_create_table(
"CREATE TABLE o (id INT NOT NULL, uid INT NOT NULL, \
CONSTRAINT fk_user FOREIGN KEY (uid) REFERENCES u(id))",
);
assert_eq!(fks[0].name, Some("fk_user".into()));
}
#[test]
fn on_delete_cascade() {
let fks = parse_create_table(
"CREATE TABLE o (id INT NOT NULL, uid INT NOT NULL, \
FOREIGN KEY (uid) REFERENCES u(id) ON DELETE CASCADE)",
);
assert_eq!(fks[0].on_delete, FkAction::Cascade);
assert_eq!(fks[0].on_update, FkAction::NoAction);
}
#[test]
fn on_delete_set_null_set_default_no_action() {
let cases = [
("ON DELETE SET NULL", FkAction::SetNull),
("ON DELETE SET DEFAULT", FkAction::SetDefault),
("ON DELETE NO ACTION", FkAction::NoAction),
("ON DELETE RESTRICT", FkAction::Restrict),
];
for (clause, want) in cases {
let sql = format!(
"CREATE TABLE o (uid INT NOT NULL, FOREIGN KEY (uid) REFERENCES u(id) {clause})"
);
let fks = parse_create_table(&sql);
assert_eq!(fks[0].on_delete, want, "clause = {clause}");
}
}
#[test]
fn on_delete_and_on_update_combined_either_order() {
let fks = parse_create_table(
"CREATE TABLE o (uid INT NOT NULL, \
FOREIGN KEY (uid) REFERENCES u(id) ON DELETE CASCADE ON UPDATE SET NULL)",
);
assert_eq!(fks[0].on_delete, FkAction::Cascade);
assert_eq!(fks[0].on_update, FkAction::SetNull);
let fks = parse_create_table(
"CREATE TABLE o (uid INT NOT NULL, \
FOREIGN KEY (uid) REFERENCES u(id) ON UPDATE SET NULL ON DELETE CASCADE)",
);
assert_eq!(fks[0].on_delete, FkAction::Cascade);
assert_eq!(fks[0].on_update, FkAction::SetNull);
}
#[test]
fn composite_fk_multiple_columns() {
let fks = parse_create_table(
"CREATE TABLE child (a INT NOT NULL, b INT NOT NULL, \
FOREIGN KEY (a, b) REFERENCES parent(x, y))",
);
assert_eq!(fks[0].columns, vec!["a", "b"]);
assert_eq!(fks[0].parent_columns, vec!["x", "y"]);
}
#[test]
fn arity_mismatch_is_rejected() {
let r = parse_statement(
"CREATE TABLE child (a INT NOT NULL, b INT NOT NULL, \
FOREIGN KEY (a, b) REFERENCES parent(x))",
);
assert!(r.is_err(), "arity mismatch must be a parse error");
}
#[test]
fn repeated_on_delete_is_rejected() {
let r = parse_statement(
"CREATE TABLE o (uid INT NOT NULL, FOREIGN KEY (uid) REFERENCES u(id) \
ON DELETE CASCADE ON DELETE RESTRICT)",
);
assert!(r.is_err());
}
#[test]
fn display_round_trips_simple_fk() {
let sql_in =
"CREATE TABLE o (uid INT NOT NULL, FOREIGN KEY (uid) REFERENCES u(id) ON DELETE CASCADE)";
let stmt = parse_statement(sql_in).unwrap();
let rendered = format!("{stmt}");
let stmt2 = parse_statement(&rendered)
.unwrap_or_else(|e| panic!("re-parse failed for {rendered:?}: {e:?}"));
assert_eq!(stmt, stmt2);
}
#[test]
fn multiple_fks_in_one_table() {
let fks = parse_create_table(
"CREATE TABLE o (a INT NOT NULL, b INT NOT NULL, \
FOREIGN KEY (a) REFERENCES p1(id), \
FOREIGN KEY (b) REFERENCES p2(id) ON DELETE CASCADE)",
);
assert_eq!(fks.len(), 2);
assert_eq!(fks[0].parent_table, "p1");
assert_eq!(fks[1].parent_table, "p2");
assert_eq!(fks[1].on_delete, FkAction::Cascade);
}
#[test]
fn match_simple_table_level_accepted() {
let fks = parse_create_table(
"CREATE TABLE c (a INT, b INT, \
FOREIGN KEY (a, b) REFERENCES p (a, b) MATCH SIMPLE)",
);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].columns, vec!["a", "b"]);
assert_eq!(fks[0].parent_table, "p");
}
#[test]
fn match_simple_column_level_accepted() {
let fks = parse_create_table("CREATE TABLE c (x INT REFERENCES q(id) MATCH SIMPLE)");
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].parent_table, "q");
}
#[test]
fn match_simple_before_on_delete() {
let fks = parse_create_table(
"CREATE TABLE c (a INT, b INT, \
FOREIGN KEY (a, b) REFERENCES p (a, b) MATCH SIMPLE ON DELETE CASCADE)",
);
assert_eq!(fks[0].on_delete, FkAction::Cascade);
}
#[test]
fn match_full_single_column_is_accepted() {
parse_statement("CREATE TABLE c (x INT REFERENCES q(id) MATCH FULL)")
.expect("single-column MATCH FULL should parse");
}
#[test]
fn match_full_multi_column_parses() {
let fks = parse_create_table(
"CREATE TABLE c (a INT, b INT, FOREIGN KEY (a, b) REFERENCES p (a, b) MATCH FULL)",
);
assert_eq!(fks.len(), 1);
assert_eq!(fks[0].match_type, MatchType::Full);
assert_eq!(fks[0].columns, ["a", "b"]);
}
#[test]
fn match_partial_is_rejected() {
let err = parse_statement("CREATE TABLE c (x INT REFERENCES q(id) MATCH PARTIAL)")
.expect_err("MATCH PARTIAL should be rejected");
assert!(
format!("{err:?}").contains("MATCH PARTIAL not yet implemented"),
"got {err:?}"
);
}