use motedb::{sql::QueryResult, types::Value, Database};
use tempfile::TempDir;
fn create_db() -> (Database, TempDir) {
let dir = TempDir::new().unwrap();
let db = Database::create(dir.path()).unwrap();
(db, dir)
}
fn rows(db: &Database, sql: &str) -> Vec<Vec<Value>> {
match db.execute(sql).unwrap().materialize().unwrap() {
QueryResult::Select { rows, .. } => rows,
_ => vec![],
}
}
fn count(db: &Database, sql: &str) -> i64 {
rows(db, sql)
.first()
.and_then(|r| r.first())
.and_then(|v| {
if let Value::Integer(i) = v {
Some(*i)
} else {
None
}
})
.unwrap_or(-1)
}
#[test]
fn test_group_by_high_cardinality_spills_to_hashmap() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, cat TEXT, val INT)")
.unwrap();
for i in 0..300i64 {
db.execute(&format!(
"INSERT INTO t VALUES ({}, 'cat_{}', {})",
i,
i,
i * 10
))
.unwrap();
}
db.flush().unwrap();
let r = rows(&db, "SELECT cat, COUNT(*), SUM(val) FROM t GROUP BY cat");
assert_eq!(r.len(), 300, "Expected 300 groups, got {}", r.len());
for row in &r {
let c = match &row[1] {
Value::Integer(i) => *i,
_ => -1,
};
assert_eq!(c, 1, "Each group should have exactly 1 row");
}
assert_eq!(count(&db, "SELECT COUNT(*) FROM t"), 300);
}
#[test]
fn test_prepared_select_non_pk_where_after_checkpoint() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("prep.mote");
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name TEXT, age INT)")
.unwrap();
for i in 0..100i64 {
db.execute(&format!(
"INSERT INTO users VALUES (null, 'user{}', {})",
i,
20 + i
))
.unwrap();
}
db.checkpoint().unwrap();
db.close().unwrap();
}
let db = Database::open(&path).unwrap();
let r = db
.execute_prepared(
"SELECT * FROM users WHERE name = ?",
vec![Value::text("user50".to_string())],
)
.unwrap()
.materialize()
.unwrap();
match r {
QueryResult::Select { rows, .. } => {
assert_eq!(rows.len(), 1, "Should find 1 user named 'user50'");
assert_eq!(rows[0][1], Value::text("user50".to_string()));
}
other => panic!("Expected Select, got {:?}", other),
}
let r = rows(&db, "SELECT * FROM users WHERE name = 'user0'");
assert_eq!(r.len(), 1);
}
#[test]
fn test_count_where_after_update_changes_filter_value() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE oltp (id INT PRIMARY KEY, status TEXT)")
.unwrap();
for i in 1..=100i64 {
db.execute(&format!("INSERT INTO oltp VALUES ({}, 'active')", i))
.unwrap();
}
for i in 1..=50i64 {
db.execute(&format!(
"UPDATE oltp SET status = 'inactive' WHERE id = {}",
i
))
.unwrap();
}
assert_eq!(
count(&db, "SELECT COUNT(*) FROM oltp WHERE status = 'active'"),
50
);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM oltp WHERE status = 'inactive'"),
50
);
assert_eq!(count(&db, "SELECT COUNT(*) FROM oltp"), 100);
db.flush().unwrap();
assert_eq!(
count(&db, "SELECT COUNT(*) FROM oltp WHERE status = 'active'"),
50
);
assert_eq!(
count(&db, "SELECT COUNT(*) FROM oltp WHERE status = 'inactive'"),
50
);
}
#[test]
fn test_group_by_text_column_with_nulls() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, grp TEXT, val INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'A', 10)").unwrap();
db.execute("INSERT INTO t VALUES (2, 'B', 20)").unwrap();
db.execute("INSERT INTO t VALUES (3, NULL, 30)").unwrap();
db.execute("INSERT INTO t VALUES (4, 'A', 40)").unwrap();
db.execute("INSERT INTO t VALUES (5, NULL, 50)").unwrap();
db.flush().unwrap();
let r = rows(&db, "SELECT grp, COUNT(*) FROM t GROUP BY grp");
let total: i64 = r
.iter()
.map(|row| match &row[1] {
Value::Integer(i) => *i,
_ => 0,
})
.sum();
assert_eq!(
total, 5,
"Total rows across all groups should be 5, got {}",
total
);
let a_group = r
.iter()
.find(|row| matches!(&row[0], Value::Text(t) if t.as_str() == "A"));
assert!(a_group.is_some(), "Group 'A' should exist");
if let Some(g) = a_group {
assert_eq!(g[1], Value::Integer(2));
}
}
#[test]
fn test_count_star_col_segment_table_after_reopen() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)")
.unwrap();
for i in 1..=200i64 {
db.execute(&format!("INSERT INTO t VALUES ({}, 'row_{}')", i, i))
.unwrap();
}
db.checkpoint().unwrap();
db.close().unwrap();
}
let db = Database::open(&path).unwrap();
assert_eq!(
count(&db, "SELECT COUNT(*) FROM t"),
200,
"COUNT(*) after reopen should return 200"
);
let r = rows(&db, "SELECT * FROM t");
assert_eq!(r.len(), 200);
}
#[test]
fn test_update_text_to_empty_string_vs_null_distinct() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, s TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'hello')").unwrap();
db.execute("INSERT INTO t VALUES (2, 'world')").unwrap();
db.execute("UPDATE t SET s = '' WHERE id = 1").unwrap();
db.execute("UPDATE t SET s = NULL WHERE id = 2").unwrap();
let r1 = rows(&db, "SELECT s FROM t WHERE id = 1");
let r2 = rows(&db, "SELECT s FROM t WHERE id = 2");
let v1 = r1.first().and_then(|r| r.first());
let v2 = r2.first().and_then(|r| r.first());
assert_ne!(v1, v2, "Empty string and NULL should be distinguishable");
}
#[test]
fn test_inner_join_where_with_limit_early_termination() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE a (id INT PRIMARY KEY, cat TEXT)")
.unwrap();
db.execute("CREATE TABLE b (id INT PRIMARY KEY, a_id INT, val INT)")
.unwrap();
for i in 1..=50i64 {
let cat = if i % 2 == 0 { "X" } else { "Y" };
db.execute(&format!("INSERT INTO a VALUES ({}, '{}')", i, cat))
.unwrap();
db.execute(&format!("INSERT INTO b VALUES ({}, {}, {})", i, i, i * 10))
.unwrap();
}
db.flush().unwrap();
let r = rows(
&db,
"SELECT a.id, b.val FROM a INNER JOIN b ON a.id = b.a_id WHERE a.cat = 'X' LIMIT 5",
);
assert_eq!(
r.len(),
5,
"Should return exactly 5 rows after WHERE + LIMIT"
);
let total_x = rows(
&db,
"SELECT a.id FROM a INNER JOIN b ON a.id = b.a_id WHERE a.cat = 'X'",
);
assert!(total_x.len() >= 5, "Should have at least 5 cat='X' matches");
}
#[test]
fn test_in_empty_list_semantics() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)")
.unwrap();
for i in 1..=5 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i))
.unwrap();
}
db.flush().unwrap();
let _ = db.execute("SELECT * FROM t WHERE id IN ()");
}
#[test]
fn test_order_by_desc_limit_with_nulls() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, score FLOAT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10.0)").unwrap();
db.execute("INSERT INTO t VALUES (2, NULL)").unwrap();
db.execute("INSERT INTO t VALUES (3, 30.0)").unwrap();
db.execute("INSERT INTO t VALUES (4, NULL)").unwrap();
db.execute("INSERT INTO t VALUES (5, 50.0)").unwrap();
db.flush().unwrap();
let r = rows(&db, "SELECT id, score FROM t ORDER BY score DESC LIMIT 3");
assert_eq!(r.len(), 3, "Should return 3 rows");
let r2 = rows(&db, "SELECT id, score FROM t ORDER BY score ASC LIMIT 3");
assert_eq!(r2.len(), 3);
}
#[test]
fn test_self_join_employees_managers() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE emp (id INT PRIMARY KEY, name TEXT, manager_id INT)")
.unwrap();
db.execute("INSERT INTO emp VALUES (1, 'CEO', 0)").unwrap();
db.execute("INSERT INTO emp VALUES (2, 'VP', 1)").unwrap();
db.execute("INSERT INTO emp VALUES (3, 'Eng', 2)").unwrap();
db.execute("INSERT INTO emp VALUES (4, 'Sales', 2)")
.unwrap();
db.flush().unwrap();
let r = rows(
&db,
"SELECT e.name, m.name FROM emp e INNER JOIN emp m ON e.manager_id = m.id",
);
assert_eq!(r.len(), 3, "Self-join should find 3 employee-manager pairs");
}
#[test]
fn test_nested_subquery_three_levels() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE a (id INT PRIMARY KEY, val INT)")
.unwrap();
db.execute("CREATE TABLE b (id INT PRIMARY KEY, a_id INT)")
.unwrap();
db.execute("CREATE TABLE c (id INT PRIMARY KEY, b_id INT, flag TEXT)")
.unwrap();
for i in 1..=10 {
db.execute(&format!("INSERT INTO a VALUES ({}, {})", i, i * 10))
.unwrap();
db.execute(&format!("INSERT INTO b VALUES ({}, {})", i, i))
.unwrap();
db.execute(&format!(
"INSERT INTO c VALUES ({}, {}, '{}')",
i,
i,
if i <= 5 { "Y" } else { "N" }
))
.unwrap();
}
db.flush().unwrap();
let r = rows(&db, "SELECT val FROM a WHERE id IN (SELECT a_id FROM b WHERE id IN (SELECT b_id FROM c WHERE flag = 'Y'))");
assert!(!r.is_empty(), "3-level subquery should return results");
assert!(r.len() <= 5, "Should only match flag='Y' rows (max 5)");
}
#[test]
fn test_min_max_all_null_returns_null() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, NULL)").unwrap();
db.execute("INSERT INTO t VALUES (2, NULL)").unwrap();
db.execute("INSERT INTO t VALUES (3, NULL)").unwrap();
db.flush().unwrap();
let r = rows(
&db,
"SELECT MIN(v), MAX(v), SUM(v), AVG(v), COUNT(v) FROM t",
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::Null, "MIN over all-NULL should be NULL");
assert_eq!(r[0][1], Value::Null, "MAX over all-NULL should be NULL");
assert_eq!(
r[0][4],
Value::Integer(0),
"COUNT(col) over all-NULL should be 0"
);
}
#[test]
fn test_wide_table_mixed_types_with_nulls() {
let (db, _dir) = create_db();
let mut cols = vec!["id INT PRIMARY KEY".to_string()];
for i in 0..20 {
match i % 3 {
0 => cols.push(format!("c{} INT", i)),
1 => cols.push(format!("c{} FLOAT", i)),
2 => cols.push(format!("c{} TEXT", i)),
_ => {}
}
}
db.execute(&format!("CREATE TABLE wide ({})", cols.join(", ")))
.unwrap();
for row in 0..10i64 {
let mut vals = vec![row.to_string()];
for i in 0..20 {
if row % (i + 2) == 0 {
vals.push("NULL".to_string());
} else {
vals.push(match i % 3 {
0 => (row + i).to_string(),
1 => format!("{:.1}", (row + i) as f64 * 1.1),
2 => format!("'text_{}'", i),
_ => "NULL".to_string(),
});
}
}
db.execute(&format!("INSERT INTO wide VALUES ({})", vals.join(", ")))
.unwrap();
}
db.flush().unwrap();
assert_eq!(count(&db, "SELECT COUNT(*) FROM wide"), 10);
let r = rows(&db, "SELECT * FROM wide");
assert_eq!(r.len(), 10);
}
#[test]
fn test_table_all_text_columns_scan_and_filter() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT, c TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'alpha', 'beta', 'gamma')")
.unwrap();
db.execute("INSERT INTO t VALUES (2, 'delta', 'epsilon', 'zeta')")
.unwrap();
db.execute("INSERT INTO t VALUES (3, 'alpha', 'beta', 'eta')")
.unwrap();
db.flush().unwrap();
assert_eq!(rows(&db, "SELECT * FROM t").len(), 3);
assert_eq!(rows(&db, "SELECT * FROM t WHERE a = 'alpha'").len(), 2);
let r = rows(&db, "SELECT a, COUNT(*) FROM t GROUP BY a");
assert!(r.len() >= 2); }
#[test]
fn test_delete_all_then_reinsert_same_pks() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)")
.unwrap();
for i in 1..=10 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i))
.unwrap();
}
db.flush().unwrap();
for i in 1..=10 {
db.execute(&format!("DELETE FROM t WHERE id = {}", i))
.unwrap();
}
assert_eq!(count(&db, "SELECT COUNT(*) FROM t"), 0, "All rows deleted");
for i in 1..=10 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i * 100))
.unwrap();
}
assert_eq!(
count(&db, "SELECT COUNT(*) FROM t"),
10,
"Re-inserted 10 rows"
);
let r = rows(&db, "SELECT v FROM t WHERE id = 5");
assert_eq!(r[0][0], Value::Integer(500));
db.checkpoint().unwrap();
db.close().unwrap();
}
#[test]
fn test_integer_overflow_to_float_survives_recovery() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, v BIGINT)")
.unwrap();
db.execute(&format!("INSERT INTO t VALUES (1, {})", i64::MAX - 10))
.unwrap();
db.execute("UPDATE t SET v = v + 100 WHERE id = 1").unwrap();
db.checkpoint().unwrap();
db.close().unwrap();
}
let db = Database::open(&path).unwrap();
let r = rows(&db, "SELECT v FROM t WHERE id = 1");
assert_eq!(r.len(), 1);
match &r[0][0] {
Value::Float(f) => assert!(*f >= i64::MAX as f64, "Should be >= i64::MAX, got {}", f),
Value::Integer(i) => assert!(*i > 0, "Should be positive, got {}", i),
other => panic!("Expected numeric, got {:?}", other),
}
}
#[test]
fn test_checkpoint_excludes_uncommitted_transaction() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10)").unwrap();
db.checkpoint().unwrap();
db.close().unwrap();
}
let db = Database::open(&path).unwrap();
db.execute("BEGIN TRANSACTION").unwrap();
db.execute("INSERT INTO t VALUES (2, 20)").unwrap();
let _cnt_in_txn = count(&db, "SELECT COUNT(*) FROM t");
db.execute("ROLLBACK").unwrap();
let cnt_after = count(&db, "SELECT COUNT(*) FROM t");
assert_eq!(
cnt_after, 1,
"Rollback should leave only committed row (1), got {}",
cnt_after
);
}
#[test]
fn test_spatial_update_and_reread() {
let (db, _dir) = create_db();
db.execute("CREATE TABLE pts (id INT PRIMARY KEY, loc GEOMETRY)")
.unwrap();
db.execute("INSERT INTO pts VALUES (1, POINT(0, 0))")
.unwrap();
db.execute("INSERT INTO pts VALUES (2, POINT(10, 10))")
.unwrap();
db.flush().unwrap();
db.execute("UPDATE pts SET loc = POINT(5, 5) WHERE id = 1")
.unwrap();
db.flush().unwrap();
let r = rows(
&db,
"SELECT id FROM pts ORDER BY ST_DISTANCE(loc, 5, 5) LIMIT 1",
);
assert!(!r.is_empty(), "Should find nearest point after UPDATE");
let nearest_id = r.first().and_then(|row| row.first()).and_then(|v| {
if let Value::Integer(i) = v {
Some(*i)
} else {
None
}
});
assert_eq!(
nearest_id,
Some(1),
"Updated point should be nearest to (5,5)"
);
}