use std::sync::Arc;
use limbo_core::{Connection, Database, StepResult};
fn new_io() -> Arc<dyn limbo_core::IO> {
Arc::new(limbo_core::SyscallIO::new().unwrap())
}
fn temp_db_path(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"oxisqlite_in_null_{}_{}_{}.db",
tag,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn cleanup(path: &std::path::Path) {
let _ = std::fs::remove_file(path);
let _ = std::fs::remove_file(format!("{}-wal", path.display()));
let _ = std::fs::remove_file(format!("{}-shm", path.display()));
}
fn exec(conn: &Arc<Connection>, sql: &str) {
conn.execute(sql)
.unwrap_or_else(|e| panic!("execute failed for {sql}: {e:?}"));
}
fn row_count(io: &Arc<dyn limbo_core::IO>, conn: &Arc<Connection>, sql: &str) -> usize {
let mut stmt = conn
.query(sql)
.unwrap_or_else(|e| panic!("prepare failed for {sql}: {e:?}"))
.unwrap_or_else(|| panic!("no statement produced for {sql}"));
let mut count = 0usize;
loop {
match stmt
.step()
.unwrap_or_else(|e| panic!("step failed for {sql}: {e:?}"))
{
StepResult::Row => count += 1,
StepResult::IO => io.run_once().unwrap(),
StepResult::Done => break,
other => panic!("unexpected step result for {sql}: {other:?}"),
}
}
count
}
fn setup() -> (Arc<dyn limbo_core::IO>, Arc<Connection>, std::path::PathBuf) {
let path = temp_db_path("setup");
cleanup(&path);
let io = new_io();
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
exec(&conn, "CREATE TABLE outer_t (x INTEGER)");
exec(&conn, "INSERT INTO outer_t VALUES (5)");
exec(&conn, "CREATE TABLE outer_match (x INTEGER)");
exec(&conn, "INSERT INTO outer_match VALUES (1)");
exec(&conn, "CREATE TABLE outer_null (x INTEGER)");
exec(&conn, "INSERT INTO outer_null VALUES (NULL)");
exec(&conn, "CREATE TABLE inner_with_null (v INTEGER)");
exec(&conn, "INSERT INTO inner_with_null VALUES (1)");
exec(&conn, "INSERT INTO inner_with_null VALUES (NULL)");
exec(&conn, "CREATE TABLE inner_no_null (v INTEGER)");
exec(&conn, "INSERT INTO inner_no_null VALUES (1)");
exec(&conn, "INSERT INTO inner_no_null VALUES (2)");
exec(&conn, "INSERT INTO inner_no_null VALUES (3)");
(io, conn, path)
}
#[test]
fn not_in_set_with_null_no_match_is_null() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_t WHERE x NOT IN (SELECT v FROM inner_with_null)",
);
assert_eq!(n, 0, "5 NOT IN {{1, NULL}} should be NULL, not 1");
cleanup(&path);
}
#[test]
fn in_set_with_null_no_match_is_null() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_t WHERE x IN (SELECT v FROM inner_with_null)",
);
assert_eq!(n, 0, "5 IN {{1, NULL}} should be NULL, not 0");
cleanup(&path);
}
#[test]
fn not_in_set_with_null_but_lhs_matches_is_false() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_match WHERE x NOT IN (SELECT v FROM inner_with_null)",
);
assert_eq!(n, 0, "1 NOT IN {{1, NULL}} should be 0 (match found)");
cleanup(&path);
}
#[test]
fn in_set_no_null_no_match_is_false_definite() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_t WHERE x IN (SELECT v FROM inner_no_null)",
);
assert_eq!(n, 0, "5 IN {{1,2,3}} should be definite 0");
cleanup(&path);
}
#[test]
fn not_in_set_no_null_no_match_is_true_definite() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_t WHERE x NOT IN (SELECT v FROM inner_no_null)",
);
assert_eq!(n, 1, "5 NOT IN {{1,2,3}} should be definite 1");
cleanup(&path);
}
#[test]
fn lhs_null_still_null() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_null WHERE x IN (SELECT v FROM inner_no_null)",
);
assert_eq!(n, 0, "NULL IN {{1,2,3}} should be NULL");
cleanup(&path);
}
#[test]
fn in_set_with_null_but_lhs_matches_is_true() {
let (io, conn, path) = setup();
let n = row_count(
&io,
&conn,
"SELECT x FROM outer_match WHERE x IN (SELECT v FROM inner_with_null)",
);
assert_eq!(n, 1, "1 IN {{1, NULL}} should be 1 (match found)");
cleanup(&path);
}