use citadel::{Argon2Profile, DatabaseBuilder};
use citadel_sql::{Connection, ExecutionResult, QueryResult, SqlError, Value};
fn create_db(dir: &std::path::Path) -> citadel::Database {
let db_path = dir.join("test.db");
DatabaseBuilder::new(db_path)
.passphrase(b"test-passphrase")
.argon2_profile(Argon2Profile::Iot)
.create()
.unwrap()
}
fn assert_ok(result: ExecutionResult) {
match result {
ExecutionResult::Ok => {}
other => panic!("expected Ok, got {other:?}"),
}
}
fn assert_rows_affected(result: ExecutionResult, expected: u64) {
match result {
ExecutionResult::RowsAffected(n) => assert_eq!(n, expected),
other => panic!("expected RowsAffected({expected}), got {other:?}"),
}
}
fn query(conn: &Connection, sql: &str) -> QueryResult {
match conn.execute(sql).unwrap() {
ExecutionResult::Query(qr) => qr,
other => panic!("expected Query, got {other:?}"),
}
}
fn setup_two_tables(conn: &Connection) {
assert_ok(
conn.execute("CREATE TABLE t1 (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, val) VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50)")
.unwrap(),
5,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, val) VALUES (2, 200), (4, 400), (6, 600)")
.unwrap(),
3,
);
}
#[test]
fn in_subquery_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(&conn, "SELECT id FROM t1 WHERE id IN (SELECT id FROM t2)");
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
other => panic!("expected int, got {other:?}"),
})
.collect();
ids.sort();
assert_eq!(ids, vec![2, 4]);
}
#[test]
fn not_in_subquery_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE id NOT IN (SELECT id FROM t2)",
);
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![1, 3, 5]);
}
#[test]
fn in_subquery_empty_result() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE id IN (SELECT id FROM t2 WHERE val > 9999)",
);
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn not_in_subquery_empty_result() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE id NOT IN (SELECT id FROM t2 WHERE val > 9999)",
);
assert_eq!(qr.rows.len(), 5);
}
#[test]
fn in_subquery_with_where_filter() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE id IN (SELECT id FROM t2 WHERE val >= 400)",
);
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![4]);
}
#[test]
fn in_subquery_with_null_in_subquery_result() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE items (id INTEGER NOT NULL PRIMARY KEY, cat INTEGER)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE cats (id INTEGER NOT NULL PRIMARY KEY, cat_id INTEGER)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO items (id, cat) VALUES (1, 10), (2, 20), (3, 30)")
.unwrap(),
3,
);
assert_rows_affected(
conn.execute("INSERT INTO cats (id, cat_id) VALUES (1, 10), (2, NULL), (3, 30)")
.unwrap(),
3,
);
let qr = query(
&conn,
"SELECT id FROM items WHERE cat IN (SELECT cat_id FROM cats)",
);
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![1, 3]);
}
#[test]
fn not_in_subquery_with_null_returns_zero_rows() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE a (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE b (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO a (id) VALUES (1), (2), (3)")
.unwrap(),
3,
);
assert_rows_affected(
conn.execute("INSERT INTO b (id, val) VALUES (1, 10), (2, NULL)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT id FROM a WHERE id NOT IN (SELECT val FROM b)",
);
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn null_lhs_in_subquery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE s (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t (id, val) VALUES (1, NULL), (2, 10)")
.unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO s (id) VALUES (10), (20)")
.unwrap(),
2,
);
let qr = query(&conn, "SELECT id FROM t WHERE val IN (SELECT id FROM s)");
let ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
assert_eq!(ids, vec![2]);
}
#[test]
fn not_in_all_null_subquery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE s (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t (id) VALUES (1), (2)").unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO s (id, val) VALUES (1, NULL), (2, NULL)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT id FROM t WHERE id NOT IN (SELECT val FROM s)",
);
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn in_empty_subquery_with_null_lhs() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE t (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE s (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t (id, val) VALUES (1, NULL)")
.unwrap(),
1,
);
let qr = query(&conn, "SELECT id FROM t WHERE val IN (SELECT id FROM s)");
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn scalar_subquery_in_where() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE val > (SELECT MIN(val) FROM t1 WHERE id <= 2)",
);
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![2, 3, 4, 5]);
}
#[test]
fn scalar_subquery_in_projection() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id, (SELECT COUNT(*) FROM t2) FROM t1 WHERE id = 1",
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[0][1], Value::Integer(3));
}
#[test]
fn scalar_subquery_empty_returns_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE val = (SELECT val FROM t2 WHERE id = 999)",
);
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn scalar_subquery_multiple_rows_error() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let result = conn.execute("SELECT id FROM t1 WHERE val = (SELECT val FROM t2)");
assert!(matches!(result, Err(SqlError::SubqueryMultipleRows)));
}
#[test]
fn exists_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = 2)",
);
assert_eq!(qr.rows.len(), 5);
}
#[test]
fn not_exists_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = 999)",
);
assert_eq!(qr.rows.len(), 5);
}
#[test]
fn exists_empty_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE t1 (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id) VALUES (1), (2), (3)")
.unwrap(),
3,
);
let qr = query(&conn, "SELECT id FROM t1 WHERE EXISTS (SELECT 1 FROM t2)");
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn exists_never_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE t1 (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, val INTEGER)")
.unwrap(),
);
assert_rows_affected(conn.execute("INSERT INTO t1 (id) VALUES (1)").unwrap(), 1);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, val) VALUES (1, NULL)")
.unwrap(),
1,
);
let qr = query(&conn, "SELECT id FROM t1 WHERE EXISTS (SELECT val FROM t2)");
assert_eq!(qr.rows.len(), 1);
}
#[test]
fn in_list_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(&conn, "SELECT id FROM t1 WHERE id IN (1, 3, 5)");
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![1, 3, 5]);
}
#[test]
fn not_in_list_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(&conn, "SELECT id FROM t1 WHERE id NOT IN (1, 3, 5)");
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![2, 4]);
}
#[test]
fn in_list_with_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(&conn, "SELECT id FROM t1 WHERE id IN (1, NULL, 3)");
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![1, 3]);
}
#[test]
fn in_subquery_multiple_columns_error() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let result = conn.execute("SELECT id FROM t1 WHERE id IN (SELECT id, val FROM t2)");
assert!(matches!(result, Err(SqlError::SubqueryMultipleColumns)));
}
#[test]
fn subquery_table_not_found() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let result = conn.execute("SELECT id FROM t1 WHERE id IN (SELECT id FROM nonexistent)");
assert!(matches!(result, Err(SqlError::TableNotFound(_))));
}
#[test]
fn in_subquery_with_order_by_limit() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
let qr = query(
&conn,
"SELECT id FROM t1 WHERE id IN (SELECT id FROM t2) ORDER BY id DESC LIMIT 1",
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Integer(4));
}
#[test]
fn in_subquery_with_group_by_having() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute(
"CREATE TABLE orders (id INTEGER NOT NULL PRIMARY KEY, product INTEGER NOT NULL, qty INTEGER NOT NULL)"
).unwrap());
assert_ok(
conn.execute("CREATE TABLE vip_products (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(conn.execute(
"INSERT INTO orders (id, product, qty) VALUES (1, 1, 5), (2, 1, 10), (3, 2, 3), (4, 3, 7)"
).unwrap(), 4);
assert_rows_affected(
conn.execute("INSERT INTO vip_products (id) VALUES (1), (2)")
.unwrap(),
2,
);
let qr = query(&conn,
"SELECT product, SUM(qty) FROM orders WHERE product IN (SELECT id FROM vip_products) GROUP BY product HAVING SUM(qty) > 5"
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[0][1], Value::Integer(15));
}
#[test]
fn in_subquery_with_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute(
"CREATE TABLE orders (id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER NOT NULL)",
)
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE vip (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
.unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO orders (id, user_id) VALUES (10, 1), (11, 2)")
.unwrap(),
2,
);
assert_rows_affected(conn.execute("INSERT INTO vip (id) VALUES (1)").unwrap(), 1);
let qr = query(&conn,
"SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id IN (SELECT id FROM vip)"
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
}
#[test]
fn subquery_in_transaction() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
conn.execute("BEGIN").unwrap();
let qr = query(&conn, "SELECT id FROM t1 WHERE id IN (SELECT id FROM t2)");
let mut ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
ids.sort();
assert_eq!(ids, vec![2, 4]);
conn.execute("COMMIT").unwrap();
}
#[test]
fn delete_with_in_subquery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
assert_rows_affected(
conn.execute("DELETE FROM t1 WHERE id IN (SELECT id FROM t2)")
.unwrap(),
2,
);
let qr = query(&conn, "SELECT id FROM t1 ORDER BY id");
let ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
assert_eq!(ids, vec![1, 3, 5]);
}
#[test]
fn update_with_scalar_subquery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
assert_rows_affected(
conn.execute("UPDATE t1 SET val = (SELECT MAX(val) FROM t2) WHERE id = 1")
.unwrap(),
1,
);
let qr = query(&conn, "SELECT val FROM t1 WHERE id = 1");
assert_eq!(qr.rows[0][0], Value::Integer(600));
}
#[test]
fn update_where_in_subquery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
assert_rows_affected(
conn.execute("UPDATE t1 SET val = 999 WHERE id IN (SELECT id FROM t2)")
.unwrap(),
2,
);
let qr = query(&conn, "SELECT id, val FROM t1 WHERE val = 999 ORDER BY id");
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Integer(2));
assert_eq!(qr.rows[1][0], Value::Integer(4));
}
#[test]
fn persistence_after_subquery_operations() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
{
let conn = Connection::open(&db).unwrap();
setup_two_tables(&conn);
assert_rows_affected(
conn.execute("DELETE FROM t1 WHERE id NOT IN (SELECT id FROM t2)")
.unwrap(),
3,
);
}
drop(db);
let db_path = dir.path().join("test.db");
let db = DatabaseBuilder::new(db_path)
.passphrase(b"test-passphrase")
.argon2_profile(Argon2Profile::Iot)
.open()
.unwrap();
let conn = Connection::open(&db).unwrap();
let qr = query(&conn, "SELECT id FROM t1 ORDER BY id");
let ids: Vec<i64> = qr
.rows
.iter()
.map(|r| match &r[0] {
Value::Integer(i) => *i,
_ => panic!(),
})
.collect();
assert_eq!(ids, vec![2, 4]);
}