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_users_orders(conn: &Connection) {
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, amount REAL NOT NULL)"
).unwrap());
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie')",
)
.unwrap(),
3,
);
assert_rows_affected(conn.execute(
"INSERT INTO orders (id, user_id, amount) VALUES (10, 1, 50.0), (11, 1, 30.0), (12, 2, 100.0)"
).unwrap(), 3);
}
#[test]
fn basic_inner_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id ORDER BY o.id"
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(
qr.rows[0],
vec![Value::Text("Alice".into()), Value::Real(50.0)]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("Alice".into()), Value::Real(30.0)]
);
assert_eq!(
qr.rows[2],
vec![Value::Text("Bob".into()), Value::Real(100.0)]
);
}
#[test]
fn inner_join_excludes_non_matching() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id",
);
let names: Vec<&Value> = qr.rows.iter().map(|r| &r[0]).collect();
assert!(!names.contains(&&Value::Text("Charlie".into())));
}
#[test]
fn join_bare_keyword_is_inner() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id",
);
assert_eq!(qr.rows.len(), 3);
}
#[test]
fn one_to_many_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE u.name = 'Alice' ORDER BY o.amount"
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][1], Value::Real(30.0));
assert_eq!(qr.rows[1][1], Value::Real(50.0));
}
#[test]
fn cross_join_cartesian_product() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE a (x INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE b (y INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO a (x) VALUES (1), (2)").unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO b (y) VALUES (10), (20), (30)")
.unwrap(),
3,
);
let qr = query(
&conn,
"SELECT a.x, b.y FROM a CROSS JOIN b ORDER BY a.x, b.y",
);
assert_eq!(qr.rows.len(), 6);
assert_eq!(qr.rows[0], vec![Value::Integer(1), Value::Integer(10)]);
assert_eq!(qr.rows[5], vec![Value::Integer(2), Value::Integer(30)]);
}
#[test]
fn left_join_includes_unmatched() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id ORDER BY u.id, o.id"
);
assert_eq!(qr.rows.len(), 4);
assert_eq!(
qr.rows[0],
vec![Value::Text("Alice".into()), Value::Real(50.0)]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("Alice".into()), Value::Real(30.0)]
);
assert_eq!(
qr.rows[2],
vec![Value::Text("Bob".into()), Value::Real(100.0)]
);
assert_eq!(qr.rows[3], vec![Value::Text("Charlie".into()), Value::Null]);
}
#[test]
fn self_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute(
"CREATE TABLE employees (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, manager_id INTEGER)"
).unwrap());
assert_rows_affected(conn.execute(
"INSERT INTO employees (id, name, manager_id) VALUES (1, 'Boss', NULL), (2, 'Alice', 1), (3, 'Bob', 1)"
).unwrap(), 3);
let qr = query(&conn,
"SELECT e.name, m.name FROM employees e JOIN employees m ON e.manager_id = m.id ORDER BY e.id"
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(
qr.rows[0],
vec![Value::Text("Alice".into()), Value::Text("Boss".into())]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("Bob".into()), Value::Text("Boss".into())]
);
}
#[test]
fn three_table_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute(
"CREATE TABLE customers (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL)",
)
.unwrap(),
);
assert_ok(
conn.execute(
"CREATE TABLE products (id INTEGER NOT NULL PRIMARY KEY, title TEXT NOT NULL)",
)
.unwrap(),
);
assert_ok(conn.execute(
"CREATE TABLE purchases (id INTEGER NOT NULL PRIMARY KEY, cust_id INTEGER NOT NULL, prod_id INTEGER NOT NULL)"
).unwrap());
assert_rows_affected(
conn.execute("INSERT INTO customers (id, name) VALUES (1, 'Alice'), (2, 'Bob')")
.unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, title) VALUES (100, 'Widget'), (200, 'Gadget')")
.unwrap(),
2,
);
assert_rows_affected(conn.execute(
"INSERT INTO purchases (id, cust_id, prod_id) VALUES (1, 1, 100), (2, 1, 200), (3, 2, 100)"
).unwrap(), 3);
let qr = query(
&conn,
"SELECT c.name, p.title FROM customers c \
JOIN purchases pu ON c.id = pu.cust_id \
JOIN products p ON pu.prod_id = p.id \
ORDER BY c.name, p.title",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(
qr.rows[0],
vec![Value::Text("Alice".into()), Value::Text("Gadget".into())]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("Alice".into()), Value::Text("Widget".into())]
);
assert_eq!(
qr.rows[2],
vec![Value::Text("Bob".into()), Value::Text("Widget".into())]
);
}
#[test]
fn join_with_where_filter() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 40.0 ORDER BY o.amount"
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][1], Value::Real(50.0));
assert_eq!(qr.rows[1][1], Value::Real(100.0));
}
#[test]
fn join_with_order_limit_offset() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.amount LIMIT 2 OFFSET 1"
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][1], Value::Real(50.0));
assert_eq!(qr.rows[1][1], Value::Real(100.0));
}
#[test]
fn join_with_aggregation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(&conn,
"SELECT u.name, COUNT(*), SUM(o.amount) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name ORDER BY u.name"
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
assert_eq!(qr.rows[0][1], Value::Integer(2));
assert_eq!(qr.rows[0][2], Value::Real(80.0));
assert_eq!(qr.rows[1][0], Value::Text("Bob".into()));
assert_eq!(qr.rows[1][1], Value::Integer(1));
assert_eq!(qr.rows[1][2], Value::Real(100.0));
}
#[test]
fn join_with_distinct() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT DISTINCT u.name FROM users u JOIN orders o ON u.id = o.user_id ORDER BY u.name",
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
assert_eq!(qr.rows[1][0], Value::Text("Bob".into()));
}
#[test]
fn qualified_column_resolution() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT u.id, o.id FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0], vec![Value::Integer(1), Value::Integer(10)]);
}
#[test]
fn unqualified_unique_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT name, amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
assert_eq!(qr.rows[0][1], Value::Real(50.0));
}
#[test]
fn ambiguous_column_error() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let result = conn.execute("SELECT id FROM users u JOIN orders o ON u.id = o.user_id");
assert!(matches!(result, Err(SqlError::AmbiguousColumn(_))));
}
#[test]
fn select_star_with_join() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT * FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.columns.len(), 5);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[0][1], Value::Text("Alice".into()));
assert_eq!(qr.rows[0][2], Value::Integer(10));
assert_eq!(qr.rows[0][3], Value::Integer(1));
assert_eq!(qr.rows[0][4], Value::Real(50.0));
}
#[test]
fn join_on_non_pk_column() {
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, val INTEGER NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, val INTEGER NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, val) VALUES (1, 10), (2, 20)")
.unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, val) VALUES (100, 10), (200, 30)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.id, t2.id FROM t1 JOIN t2 ON t1.val = t2.val",
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0], vec![Value::Integer(1), Value::Integer(100)]);
}
#[test]
fn join_null_equality_excludes_rows() {
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, ref_id INTEGER)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, ref_id) VALUES (1, NULL), (2, 100)")
.unwrap(),
2,
);
assert_rows_affected(conn.execute("INSERT INTO t2 (id) VALUES (100)").unwrap(), 1);
let qr = query(
&conn,
"SELECT t1.id, t2.id FROM t1 JOIN t2 ON t1.ref_id = t2.id",
);
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0], vec![Value::Integer(2), Value::Integer(100)]);
}
#[test]
fn join_with_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)").unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.id, t2.id FROM t1 JOIN t2 ON t1.id = t2.id",
);
assert_eq!(qr.rows.len(), 0);
}
#[test]
fn join_with_complex_on_clause() {
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, x INTEGER NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, y INTEGER NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, x) VALUES (1, 10), (2, 20), (3, 5)")
.unwrap(),
3,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, y) VALUES (100, 10), (200, 20)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.id, t2.id FROM t1 JOIN t2 ON t1.x = t2.y AND t1.x > 5 ORDER BY t1.id",
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0], vec![Value::Integer(1), Value::Integer(100)]);
assert_eq!(qr.rows[1], vec![Value::Integer(2), Value::Integer(200)]);
}
#[test]
fn join_same_table_twice() {
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, name TEXT NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO items (id, name) VALUES (1, 'A'), (2, 'B'), (3, 'C')")
.unwrap(),
3,
);
let qr = query(&conn,
"SELECT a.name, b.name FROM items a CROSS JOIN items b WHERE a.id < b.id ORDER BY a.id, b.id"
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(
qr.rows[0],
vec![Value::Text("A".into()), Value::Text("B".into())]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("A".into()), Value::Text("C".into())]
);
assert_eq!(
qr.rows[2],
vec![Value::Text("B".into()), Value::Text("C".into())]
);
}
#[test]
fn join_scale_100x1000() {
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, uid INTEGER NOT NULL, amt INTEGER NOT NULL)"
).unwrap());
for i in 0..100 {
conn.execute(&format!(
"INSERT INTO users (id, name) VALUES ({i}, 'user_{i}')"
))
.unwrap();
}
for i in 0..1000 {
let uid = i % 100;
conn.execute(&format!(
"INSERT INTO orders (id, uid, amt) VALUES ({i}, {uid}, {})",
i * 10
))
.unwrap();
}
let qr = query(
&conn,
"SELECT COUNT(*) FROM users u JOIN orders o ON u.id = o.uid",
);
assert_eq!(qr.rows[0][0], Value::Integer(1000));
}
#[test]
fn join_within_transaction() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("BEGIN").unwrap();
assert_ok(
conn.execute("CREATE TABLE t1 (id INTEGER NOT NULL PRIMARY KEY, val TEXT NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, ref_id INTEGER NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, val) VALUES (1, 'x'), (2, 'y')")
.unwrap(),
2,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, ref_id) VALUES (10, 1), (20, 2)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.val, t2.id FROM t1 JOIN t2 ON t1.id = t2.ref_id ORDER BY t2.id",
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(
qr.rows[0],
vec![Value::Text("x".into()), Value::Integer(10)]
);
conn.execute("COMMIT").unwrap();
}
#[test]
fn right_join_includes_unmatched_right() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_orders(&conn);
let qr = query(
&conn,
"SELECT u.name, o.id FROM users u RIGHT JOIN orders o ON u.id = o.user_id ORDER BY o.id",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(
qr.rows[0],
vec![Value::Text("Alice".into()), Value::Integer(10)]
);
assert_eq!(
qr.rows[1],
vec![Value::Text("Alice".into()), Value::Integer(11)]
);
assert_eq!(
qr.rows[2],
vec![Value::Text("Bob".into()), Value::Integer(12)]
);
}
#[test]
fn right_join_null_pads_left_side() {
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, val TEXT NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, ref_id INTEGER NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, val) VALUES (1, 'A')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, ref_id) VALUES (10, 1), (20, 999)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.val, t2.id FROM t1 RIGHT JOIN t2 ON t1.id = t2.ref_id ORDER BY t2.id",
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(
qr.rows[0],
vec![Value::Text("A".into()), Value::Integer(10)]
);
assert_eq!(qr.rows[1], vec![Value::Null, Value::Integer(20)]);
}
#[test]
fn right_join_empty_left_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 t2 (id) VALUES (1), (2), (3)")
.unwrap(),
3,
);
let qr = query(
&conn,
"SELECT t1.id, t2.id FROM t1 RIGHT JOIN t2 ON t1.id = t2.id ORDER BY t2.id",
);
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0], vec![Value::Null, Value::Integer(1)]);
assert_eq!(qr.rows[1], vec![Value::Null, Value::Integer(2)]);
assert_eq!(qr.rows[2], vec![Value::Null, Value::Integer(3)]);
}
#[test]
fn right_join_within_transaction() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("BEGIN").unwrap();
assert_ok(
conn.execute("CREATE TABLE t1 (id INTEGER NOT NULL PRIMARY KEY, val TEXT NOT NULL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE t2 (id INTEGER NOT NULL PRIMARY KEY, ref_id INTEGER NOT NULL)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO t1 (id, val) VALUES (1, 'x')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO t2 (id, ref_id) VALUES (10, 1), (20, 999)")
.unwrap(),
2,
);
let qr = query(
&conn,
"SELECT t1.val, t2.id FROM t1 RIGHT JOIN t2 ON t1.id = t2.ref_id ORDER BY t2.id",
);
assert_eq!(qr.rows.len(), 2);
assert_eq!(
qr.rows[0],
vec![Value::Text("x".into()), Value::Integer(10)]
);
assert_eq!(qr.rows[1], vec![Value::Null, Value::Integer(20)]);
conn.execute("COMMIT").unwrap();
}
#[test]
fn join_table_not_found() {
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(),
);
let result = conn.execute("SELECT * FROM t1 JOIN nonexistent ON t1.id = nonexistent.id");
assert!(matches!(result, Err(SqlError::TableNotFound(_))));
}