use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_executor::SelectExecutor;
use vibesql_parser::Parser;
use vibesql_storage::{Database, Row};
use vibesql_types::{DataType, SqlValue};
fn parse_select(sql: &str) -> vibesql_ast::SelectStmt {
match Parser::parse_sql(sql) {
Ok(vibesql_ast::Statement::Select(select_stmt)) => *select_stmt,
_ => panic!("Failed to parse SELECT statement: {}", sql),
}
}
fn setup_test_db() -> Database {
let mut db = Database::new();
let t1_schema = TableSchema::with_primary_key(
"T1".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
vec!["id".to_string()],
);
db.create_table(t1_schema).unwrap();
db.insert_row("T1", Row::new(vec![SqlValue::Integer(1)])).unwrap();
db.insert_row("T1", Row::new(vec![SqlValue::Integer(2)])).unwrap();
db.insert_row("T1", Row::new(vec![SqlValue::Integer(3)])).unwrap();
let t2_schema = TableSchema::new(
"T2".to_string(),
vec![ColumnSchema::new("tid".to_string(), DataType::Integer, true)],
);
db.create_table(t2_schema).unwrap();
db.insert_row("T2", Row::new(vec![SqlValue::Integer(2)])).unwrap();
db
}
fn extract_integers(result: &[Row]) -> Vec<i64> {
result
.iter()
.map(|row| match &row.values[0] {
SqlValue::Integer(i) => *i,
_ => panic!("Expected integer"),
})
.collect()
}
#[test]
fn test_left_join_anti_join_pattern() {
let db = setup_test_db();
let sql =
"SELECT t1.id FROM t1 LEFT JOIN t2 ON t1.id = t2.tid WHERE t2.tid IS NULL ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![1, 3], "Anti-join should return rows with no match");
}
#[test]
fn test_left_join_is_not_null() {
let db = setup_test_db();
let sql =
"SELECT t1.id FROM t1 LEFT JOIN t2 ON t1.id = t2.tid WHERE t2.tid IS NOT NULL ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![2], "Should return only matching row");
}
#[test]
fn test_left_join_no_filter() {
let db = setup_test_db();
let sql = "SELECT t1.id FROM t1 LEFT JOIN t2 ON t1.id = t2.tid ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![1, 2, 3], "LEFT JOIN should return all left rows");
}
#[test]
fn test_left_join_compound_predicate() {
let db = setup_test_db();
let sql = "SELECT t1.id FROM t1 LEFT JOIN t2 ON t1.id = t2.tid WHERE t1.id > 0 AND t2.tid IS NULL ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![1, 3], "Compound predicate should work correctly");
}
#[test]
fn test_not_exists_pattern() {
let db = setup_test_db();
let sql = "SELECT t1.id FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.tid = t1.id) ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![1, 3], "NOT EXISTS should match anti-join result");
}
#[test]
fn test_inner_join_still_works() {
let db = setup_test_db();
let sql = "SELECT t1.id FROM t1 INNER JOIN t2 ON t1.id = t2.tid ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![2], "INNER JOIN should return matching rows");
}
#[test]
fn test_left_join_left_side_predicate() {
let db = setup_test_db();
let sql = "SELECT t1.id FROM t1 LEFT JOIN t2 ON t1.id = t2.tid WHERE t1.id > 1 ORDER BY t1.id";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
let values = extract_integers(&result);
assert_eq!(values, vec![2, 3], "Left-side predicate should filter correctly");
}
fn setup_view_join_db() -> Database {
let mut db = Database::new();
let t4_schema = TableSchema::with_primary_key(
"T4".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("x".to_string(), DataType::Varchar { max_length: None }, true),
],
vec!["id".to_string()],
);
db.create_table(t4_schema).unwrap();
db.insert_row("T4", Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar("alice".into())]))
.unwrap();
db.insert_row("T4", Row::new(vec![SqlValue::Integer(4), SqlValue::Varchar("bob".into())]))
.unwrap();
db.insert_row("T4", Row::new(vec![SqlValue::Integer(6), SqlValue::Varchar("cindy".into())]))
.unwrap();
let t5_schema = TableSchema::with_primary_key(
"T5".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("y".to_string(), DataType::Varchar { max_length: None }, true),
],
vec!["id".to_string()],
);
db.create_table(t5_schema).unwrap();
db.insert_row("T5", Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar("orange".into())]))
.unwrap();
db.insert_row("T5", Row::new(vec![SqlValue::Integer(4), SqlValue::Varchar("green".into())]))
.unwrap();
let view_select = match Parser::parse_sql("SELECT id, x FROM t4") {
Ok(vibesql_ast::Statement::Select(select)) => *select,
_ => panic!("Failed to parse view SELECT"),
};
db.catalog
.create_view(vibesql_catalog::ViewDefinition::new_with_sql(
"v4".to_string(),
None,
view_select,
false,
"SELECT id, x FROM t4".to_string(),
))
.unwrap();
db
}
#[test]
fn test_issue_4918_unqualified_is_null_with_view_full_join() {
let db = setup_view_join_db();
let sql = "SELECT id, x, y FROM v4 NATURAL FULL JOIN t5 WHERE x IS NULL ORDER BY 1";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
assert_eq!(result.len(), 0, "x IS NULL should return 0 rows - no actual NULL x values");
let sql = "SELECT id, x, y FROM v4 NATURAL FULL JOIN t5 WHERE x IS NOT NULL ORDER BY 1";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
assert_eq!(result.len(), 3, "x IS NOT NULL should return all 3 rows");
let sql =
"SELECT id, x, y FROM v4 NATURAL FULL JOIN t5 WHERE x <> 'bob' OR x IS NULL ORDER BY 1";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
assert_eq!(result.len(), 2, "Should return 2 rows (alice and cindy)");
let first_x = match &result[0].values[1] {
SqlValue::Varchar(s) => s.as_str(),
_ => panic!("Expected varchar for x"),
};
assert_eq!(first_x, "alice", "First row should have x='alice'");
}
#[test]
fn test_qualified_column_with_view_full_join() {
let db = setup_view_join_db();
let sql = "SELECT id, x, y FROM v4 NATURAL FULL JOIN t5 WHERE v4.x IS NULL ORDER BY 1";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
assert_eq!(result.len(), 0, "v4.x IS NULL should return 0 rows");
}
#[test]
fn test_unqualified_is_null_with_tables_full_join() {
let db = setup_view_join_db();
let sql = "SELECT id, x, y FROM t4 NATURAL FULL JOIN t5 WHERE x IS NULL ORDER BY 1";
let select = parse_select(sql);
let result = SelectExecutor::new(&db).execute(&select).unwrap();
assert_eq!(result.len(), 0, "Direct table: x IS NULL should return 0 rows");
}