use spg_engine::{Engine, QueryResult};
fn err(e: &mut Engine, sql: &str) -> String {
match e.execute(sql) {
Ok(v) => panic!("{sql}: expected an error, got {v:?}"),
Err(x) => format!("{x}").replace("unsupported: ", ""),
}
}
fn ok(e: &mut Engine, sql: &str) -> usize {
let r = e
.execute(sql)
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let QueryResult::Rows { rows, .. } = r else {
panic!("expected Rows from {sql}");
};
rows.len()
}
fn fixture() -> Engine {
let mut e = Engine::new();
e.execute("CREATE TABLE s1 (id int primary key, v int)")
.unwrap();
e.execute("CREATE TABLE s2 (id int primary key, w int)")
.unwrap();
e.execute("INSERT INTO s1 VALUES (1,10),(2,20)").unwrap();
e.execute("INSERT INTO s2 VALUES (1,100)").unwrap();
e
}
#[test]
fn the_four_disallowed_shapes_carry_pgs_wordings() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT count(*) FROM s1 FOR UPDATE"),
"FOR UPDATE is not allowed with aggregate functions",
);
assert_eq!(
err(&mut e, "SELECT DISTINCT v FROM s1 FOR UPDATE"),
"FOR UPDATE is not allowed with DISTINCT clause",
);
assert_eq!(
err(&mut e, "SELECT v FROM s1 GROUP BY v FOR UPDATE"),
"FOR UPDATE is not allowed with GROUP BY clause",
);
assert_eq!(
err(&mut e, "SELECT v FROM s1 UNION SELECT w FROM s2 FOR UPDATE"),
"FOR UPDATE is not allowed with UNION/INTERSECT/EXCEPT",
);
}
#[test]
fn the_wording_names_the_strength_that_was_asked_for() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT count(*) FROM s1 FOR SHARE"),
"FOR SHARE is not allowed with aggregate functions",
);
assert_eq!(
err(&mut e, "SELECT DISTINCT v FROM s1 FOR KEY SHARE"),
"FOR KEY SHARE is not allowed with DISTINCT clause",
);
assert_eq!(
err(&mut e, "SELECT count(*) FROM s1 FOR NO KEY UPDATE"),
"FOR NO KEY UPDATE is not allowed with aggregate functions",
);
}
#[test]
fn of_must_name_a_relation_from_the_from_clause() {
let mut e = fixture();
assert_eq!(
err(&mut e, "SELECT * FROM s1 FOR UPDATE OF nosuch"),
"relation \"nosuch\" in FOR UPDATE clause not found in FROM clause",
);
assert_eq!(ok(&mut e, "SELECT * FROM s1 FOR UPDATE OF s1"), 2);
assert_eq!(
ok(
&mut e,
"SELECT * FROM s1 JOIN s2 USING (id) FOR UPDATE OF s1"
),
1,
);
}
#[test]
fn the_allowed_shapes_still_run() {
let mut e = fixture();
assert_eq!(ok(&mut e, "SELECT * FROM s1 FOR UPDATE"), 2);
assert_eq!(ok(&mut e, "SELECT * FROM s1 ORDER BY id FOR UPDATE"), 2);
assert_eq!(ok(&mut e, "SELECT * FROM s1 LIMIT 1 FOR UPDATE"), 1);
assert_eq!(ok(&mut e, "SELECT 1 FOR UPDATE"), 1);
assert_eq!(
ok(&mut e, "SELECT * FROM (SELECT * FROM s1) x FOR UPDATE"),
2
);
assert_eq!(
ok(
&mut e,
"WITH c AS (SELECT * FROM s1) SELECT * FROM c FOR UPDATE"
),
2,
);
assert_eq!(
ok(&mut e, "SELECT * FROM s1 JOIN s2 USING (id) FOR UPDATE"),
1
);
}
#[test]
fn a_query_without_the_clause_is_unaffected() {
let mut e = fixture();
assert_eq!(ok(&mut e, "SELECT count(*) FROM s1"), 1);
assert_eq!(ok(&mut e, "SELECT DISTINCT v FROM s1"), 2);
assert_eq!(ok(&mut e, "SELECT v FROM s1 UNION SELECT w FROM s2"), 3);
}
#[test]
fn a_join_is_accepted_but_says_it_did_not_lock() {
let mut e = fixture();
let r = e
.execute("SELECT * FROM s1 JOIN s2 USING (id) FOR UPDATE")
.expect("PG accepts this shape, so SPG must too");
let QueryResult::Rows { rows, .. } = r else {
panic!("expected Rows");
};
assert_eq!(rows.len(), 1);
let notices: Vec<String> = e.take_notices().into_iter().map(|n| n.message).collect();
assert_eq!(notices.len(), 1, "{notices:?}");
assert!(
notices[0].contains("NOT enforced") && notices[0].starts_with("FOR UPDATE"),
"{notices:?}",
);
}
#[test]
fn a_single_table_lock_says_nothing() {
let mut e = fixture();
e.execute("SELECT * FROM s1 FOR UPDATE").unwrap();
assert!(e.take_notices().is_empty());
}