use powdb_query::executor::Engine;
use powdb_query::result::QueryResult;
use powdb_storage::pj1::parse_json_text;
use powdb_storage::types::Value;
fn json(text: &str) -> Value {
Value::Json(parse_json_text(text).unwrap().into())
}
fn temp_dir(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"powdb_null_sem_{name}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn exec(engine: &mut Engine, query: &str) -> QueryResult {
engine
.execute_powql(query)
.unwrap_or_else(|e| panic!("failed to execute `{query}`: {e}"))
}
fn scalar(engine: &mut Engine, query: &str) -> i64 {
match exec(engine, query) {
QueryResult::Scalar(Value::Int(n)) => n,
QueryResult::Rows { rows, .. } if rows.len() == 1 && rows[0].len() == 1 => {
match &rows[0][0] {
Value::Int(n) => *n,
other => panic!("non-int scalar {other:?}"),
}
}
other => panic!("expected scalar, got {other:?}"),
}
}
fn ids(result: QueryResult) -> Vec<i64> {
let QueryResult::Rows { rows, columns } = result else {
panic!("expected rows, got scalar");
};
let id_col = columns
.iter()
.position(|c| c == "id")
.expect("id column present");
let mut out: Vec<i64> = rows
.into_iter()
.map(|row| match &row[id_col] {
Value::Int(id) => *id,
other => panic!("expected int id, got {other:?}"),
})
.collect();
out.sort_unstable();
out
}
fn sql_ids(engine: &mut Engine, sql: &str) -> Vec<i64> {
let result = engine
.execute_sql(sql)
.unwrap_or_else(|e| panic!("failed to execute `{sql}`: {e}"));
ids(result)
}
fn fresh(name: &str) -> (std::path::PathBuf, Engine) {
let dir = temp_dir(name);
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type T { required id: int, s: str, v: int }");
exec(&mut engine, r#"insert T { id := 1, s := "a", v := 5 }"#);
exec(&mut engine, "insert T { id := 2 }");
(dir, engine)
}
#[test]
fn ordered_and_equality_comparisons_exclude_missing_values() {
let (dir, mut engine) = fresh("six_ops");
assert_eq!(
ids(exec(&mut engine, "T filter .v < 0 { .id }")),
Vec::<i64>::new()
);
assert_eq!(
ids(exec(&mut engine, "T filter .v <= 0 { .id }")),
Vec::<i64>::new()
);
assert_eq!(
ids(exec(&mut engine, "T filter .v > 100 { .id }")),
Vec::<i64>::new()
);
assert_eq!(
ids(exec(&mut engine, "T filter .v >= 100 { .id }")),
Vec::<i64>::new()
);
assert_eq!(
ids(exec(&mut engine, "T filter .v != 5 { .id }")),
Vec::<i64>::new()
);
assert_eq!(ids(exec(&mut engine, "T filter .v = 5 { .id }")), vec![1]);
assert_eq!(ids(exec(&mut engine, "T filter .v > 0 { .id }")), vec![1]);
assert_eq!(
ids(exec(&mut engine, "T filter .v != 999 { .id }")),
vec![1]
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn compiled_and_generic_paths_agree_on_missing() {
let (dir, mut engine) = fresh("compiled_vs_generic");
let compiled = scalar(&mut engine, "count(T filter .v < 0)");
let generic_or = scalar(&mut engine, "count(T filter .v < 0 or .id < 0)");
assert_eq!(
compiled, generic_or,
"`.v < 0` and `.v < 0 or .id < 0` must agree on the Empty row"
);
assert_eq!(compiled, 0);
let str_lt = scalar(&mut engine, r#"count(T filter .s < "a")"#);
assert_eq!(str_lt, 0, "`.s < \"a\"` must exclude the Empty-s row");
let compiled_neq = scalar(&mut engine, r#"count(T filter .s != "a")"#);
let generic_neq = scalar(&mut engine, r#"count(T filter .s != "a" or .id < 0)"#);
assert_eq!(
compiled_neq, generic_neq,
"compiled and generic `.s != \"a\"` must agree on the Empty-s row"
);
assert_eq!(compiled_neq, 0);
assert_eq!(
ids(exec(
&mut engine,
r#"T filter .s != "a" and (.id > 0 or .v = 99) { .id }"#
)),
Vec::<i64>::new(),
"generic AND/OR residual must not resurrect the Empty row"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn null_comparisons_agree_across_plan_shapes() {
let dir = temp_dir("agree_plan_shapes");
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type T { required id: int, s: str, v: int }");
exec(&mut engine, r#"insert T { id := 1, s := "a", v := 5 }"#);
exec(&mut engine, "insert T { id := 2 }");
let compiled = scalar(&mut engine, "count(T filter .v < 0)");
let generic = scalar(&mut engine, "count(T filter .v < 0 or .id < 0)");
assert_eq!(
compiled, generic,
"`.v < 0` and `.v < 0 or .id < 0` must agree on NULL rows"
);
let str_single = scalar(&mut engine, r#"count(T filter .s < "a")"#);
assert_eq!(
compiled, str_single,
"int and str single-atom comparisons must treat NULL the same way"
);
assert_eq!(
compiled, 0,
"the missing row must be excluded from `.v < 0`"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn or_with_missing_side_still_matches_on_present_side() {
let (dir, mut engine) = fresh("or_sibling");
assert_eq!(
ids(exec(&mut engine, "T filter .v > 100 or .id = 2 { .id }")),
vec![2],
"a true present-side comparison must still include the row"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn powql_and_sql_frontends_agree_on_missing() {
let (dir, mut engine) = fresh("powql_vs_sql");
let cases = [
("T filter .v < 0 { .id }", "SELECT id FROM T WHERE v < 0"),
("T filter .v != 5 { .id }", "SELECT id FROM T WHERE v != 5"),
(
"T filter .v < 0 or .id < 0 { .id }",
"SELECT id FROM T WHERE v < 0 OR id < 0",
),
(
r#"T filter .s < "a" { .id }"#,
"SELECT id FROM T WHERE s < 'a'",
),
];
for (powql, sql) in cases {
let p = ids(exec(&mut engine, powql));
let s = sql_ids(&mut engine, sql);
assert_eq!(p, s, "PowQL `{powql}` and SQL `{sql}` must agree");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn nested_residual_comparison_excludes_missing_children() {
let dir = temp_dir("nested_residual");
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type P { required pid: int }");
exec(
&mut engine,
"type C { required cid: int, required parent: int, v: int }",
);
exec(&mut engine, "insert P { pid := 1 }");
exec(&mut engine, "insert C { cid := 10, parent := 1, v := -3 }");
exec(&mut engine, "insert C { cid := 11, parent := 1 }");
let result = exec(
&mut engine,
"P as p { p.pid, kids: C as c filter c.parent = p.pid and c.v < 0 { c.cid } }",
);
let QueryResult::Rows { rows, columns } = result else {
panic!("expected rows");
};
let kids_col = columns
.iter()
.position(|c| c == "kids")
.expect("kids column");
assert_eq!(
rows[0][kids_col],
json(r#"[{"cid":10}]"#),
"nested residual `c.v < 0` must exclude the Empty-v child"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn empty_equals_empty_in_filter_is_excluded_but_exists_still_works() {
let dir = temp_dir("empty_vs_empty");
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type T { required id: int, a: int, b: int }");
exec(&mut engine, "insert T { id := 1, a := 7, b := 7 }");
exec(&mut engine, "insert T { id := 2 }");
exec(&mut engine, "insert T { id := 3, a := 7 }");
assert_eq!(
ids(exec(&mut engine, "T filter .a = .b { .id }")),
vec![1],
"Empty = Empty must not match in a filter"
);
assert_eq!(
ids(exec(&mut engine, "T filter .a != .b { .id }")),
Vec::<i64>::new(),
"a comparison with an Empty operand is excluded, not `not equal`"
);
assert_eq!(
ids(exec(&mut engine, "T filter exists .b { .id }")),
vec![1],
"exists selects rows where b is present"
);
assert_eq!(
ids(exec(&mut engine, "T filter not exists .b { .id }")),
vec![2, 3],
"not exists selects rows where b is missing"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn two_valued_not_includes_missing_rows_and_frontends_agree() {
let (dir, mut engine) = fresh("two_valued_not");
let powql = ids(exec(&mut engine, "T filter not (.v > 0) { .id }"));
assert_eq!(
powql,
vec![2],
"two-valued `not (.v > 0)` includes the missing-v row"
);
let sql = sql_ids(&mut engine, "SELECT id FROM T WHERE NOT (v > 0)");
assert_eq!(powql, sql, "PowQL and SQL must agree on two-valued NOT");
assert_eq!(
ids(exec(
&mut engine,
"T filter exists .v and not (.v > 0) { .id }"
)),
Vec::<i64>::new(),
"guarding presence excludes the missing-v row again"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn join_on_equality_with_missing_keys_unchanged() {
let dir = temp_dir("join_empty_keys");
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type L { required lid: int, k: int }");
exec(&mut engine, "type R { required rid: int, k: int }");
exec(&mut engine, "insert L { lid := 1, k := 5 }");
exec(&mut engine, "insert L { lid := 2 }");
exec(&mut engine, "insert R { rid := 1, k := 5 }");
exec(&mut engine, "insert R { rid := 2 }");
let n = scalar(&mut engine, "count(L as l join R as r on l.k = r.k)");
assert_eq!(
n, 2,
"join ON equality must still match Empty = Empty (present pair + missing pair)"
);
std::fs::remove_dir_all(&dir).ok();
}