use motedb::types::Value;
use motedb::{DBConfig, Database, QueryResult};
use tempfile::TempDir;
fn make_db() -> (TempDir, Database) {
let dir = TempDir::new().unwrap();
let mut config = DBConfig::for_edge();
config.max_result_rows = None;
let db = Database::create_with_config(dir.path(), config).unwrap();
(dir, db)
}
fn select_rows(db: &Database, sql: &str) -> Vec<Vec<Value>> {
match db.execute(sql).unwrap().materialize().unwrap() {
QueryResult::Select { rows, .. } => rows,
_ => panic!("expected Select"),
}
}
fn count(db: &Database, table: &str) -> i64 {
match select_rows(db, &format!("SELECT COUNT(*) FROM {}", table)).first() {
Some(r) => match r.first() {
Some(Value::Integer(n)) => *n,
_ => panic!("COUNT non-int"),
},
None => panic!("COUNT empty"),
}
}
#[test]
fn test_multi_value_insert() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
.unwrap();
assert_eq!(count(&db, "t"), 3, "multi-value insert adds 3 rows");
let rows = select_rows(&db, "SELECT v FROM t ORDER BY id");
assert_eq!(rows[0][0], Value::Integer(10));
assert_eq!(rows[2][0], Value::Integer(30));
}
#[test]
fn test_batch_insert_preserves_order() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for batch_start in (0..100).step_by(25) {
let vals: Vec<String> = (0..25).map(|i| format!("({})", batch_start + i)).collect();
db.execute(&format!("INSERT INTO t (v) VALUES {}", vals.join(",")))
.unwrap();
}
assert_eq!(count(&db, "t"), 100);
let rows = select_rows(&db, "SELECT v FROM t ORDER BY id");
for (i, row) in rows.iter().enumerate() {
assert_eq!(row[0], Value::Integer(i as i64), "v at position {}", i);
}
}
#[test]
fn test_where_empty_result() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10)").unwrap();
let rows = select_rows(&db, "SELECT * FROM t WHERE id = 999");
assert!(rows.is_empty(), "no match returns empty");
}
#[test]
fn test_where_all_match() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, cat TEXT)")
.unwrap();
for _ in 0..5 {
db.execute("INSERT INTO t (cat) VALUES ('x')").unwrap();
}
let rows = select_rows(&db, "SELECT * FROM t WHERE cat = 'x'");
assert_eq!(rows.len(), 5, "all rows match filter");
}
#[test]
fn test_where_multiple_conditions_and() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, a INT, b INT)")
.unwrap();
let data = [(10, 1), (20, 2), (30, 1), (40, 3), (50, 1)];
for (a, b) in &data {
db.execute(&format!("INSERT INTO t (a, b) VALUES ({}, {})", a, b))
.unwrap();
}
let rows = select_rows(&db, "SELECT * FROM t WHERE a > 15 AND b = 1");
let matched: Vec<i64> = rows
.iter()
.filter_map(|r| match r.get(1) {
Some(Value::Integer(n)) => Some(*n),
_ => None,
})
.collect();
assert_eq!(matched, vec![30, 50], "a>15 AND b=1 matches id 3 and 5");
}
#[test]
fn test_where_multiple_conditions_or() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for v in [1, 5, 10, 15, 20] {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", v))
.unwrap();
}
let rows = select_rows(&db, "SELECT * FROM t WHERE v = 5 OR v = 15");
assert_eq!(rows.len(), 2);
}
#[test]
fn test_integer_extremes_roundtrip() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 0)").unwrap();
db.execute("INSERT INTO t VALUES (2, -1)").unwrap();
db.execute(&format!("INSERT INTO t VALUES (3, {})", i64::MAX))
.unwrap();
db.execute(&format!("INSERT INTO t VALUES (4, {})", i64::MIN + 1))
.unwrap();
let rows = select_rows(&db, "SELECT v FROM t ORDER BY id");
assert_eq!(rows[0][0], Value::Integer(0));
assert_eq!(rows[1][0], Value::Integer(-1));
assert_eq!(rows[2][0], Value::Integer(i64::MAX));
assert_eq!(
rows[3][0],
Value::Integer(i64::MIN + 1),
"MIN+1 round-trips"
);
}
#[test]
fn test_text_with_special_chars() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, s TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'hello world')")
.unwrap();
db.execute("INSERT INTO t VALUES (2, '')").unwrap();
db.execute("INSERT INTO t VALUES (3, ' spaces ')")
.unwrap();
let rows = select_rows(&db, "SELECT s FROM t ORDER BY id");
assert_eq!(rows[0][0], Value::text("hello world".to_string()));
assert_eq!(
rows[2][0],
Value::text(" spaces ".to_string()),
"surrounding spaces preserved"
);
}
#[test]
fn test_boolean_storage() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, active BOOLEAN)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, TRUE)").unwrap();
db.execute("INSERT INTO t VALUES (2, FALSE)").unwrap();
let rows = select_rows(&db, "SELECT active FROM t ORDER BY id");
assert_eq!(rows[0][0], Value::Bool(true));
assert_eq!(rows[1][0], Value::Bool(false));
}
#[test]
fn test_create_drop_recreate_same_table() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 100)").unwrap();
db.execute("DROP TABLE t").unwrap();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'reborn')").unwrap();
assert_eq!(count(&db, "t"), 1, "dropped table data must not linger");
let rows = select_rows(&db, "SELECT v FROM t");
assert_eq!(rows[0][0], Value::text("reborn".to_string()));
}
#[test]
fn test_create_index_query_drop_index_consistency() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, cat TEXT, v INT)")
.unwrap();
for i in 0..20 {
let cat = match i % 2 {
0 => "even",
_ => "odd",
};
db.execute(&format!("INSERT INTO t (cat, v) VALUES ('{}', {})", cat, i))
.unwrap();
}
let before = select_rows(&db, "SELECT * FROM t WHERE cat = 'even'");
db.execute("CREATE INDEX idx_cat ON t (cat) USING COLUMN")
.unwrap();
let with_idx = select_rows(&db, "SELECT * FROM t WHERE cat = 'even'");
assert_eq!(
before.len(),
with_idx.len(),
"index doesn't change result count"
);
db.execute("DROP INDEX idx_cat").unwrap();
let after = select_rows(&db, "SELECT * FROM t WHERE cat = 'even'");
assert_eq!(
after.len(),
with_idx.len(),
"drop index preserves result count"
);
}
#[test]
fn test_multiple_indexes_same_table() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, a TEXT, b TEXT)")
.unwrap();
for i in 0..30 {
let a = if i % 3 == 0 { "x" } else { "y" };
let b = if i % 2 == 0 { "p" } else { "q" };
db.execute(&format!("INSERT INTO t (a, b) VALUES ('{}', '{}')", a, b))
.unwrap();
}
db.execute("CREATE INDEX idx_a ON t (a) USING COLUMN")
.unwrap();
db.execute("CREATE INDEX idx_b ON t (b) USING COLUMN")
.unwrap();
assert_eq!(
select_rows(&db, "SELECT * FROM t WHERE a = 'x'").len(),
10,
"first index works"
);
assert_eq!(count(&db, "t"), 30, "all rows intact");
}
#[test]
fn test_transaction_commit_persists() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("BEGIN").unwrap();
for i in 1..=5 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i))
.unwrap();
}
db.execute("COMMIT").unwrap();
assert_eq!(count(&db, "t"), 5, "committed data persists");
}
#[test]
fn test_transaction_rollback_discards() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10)").unwrap();
db.execute("BEGIN").unwrap();
db.execute("INSERT INTO t VALUES (2, 20)").unwrap();
db.execute("ROLLBACK").unwrap();
assert_eq!(count(&db, "t"), 1, "rolled-back insert discarded");
}
#[test]
fn test_aggregate_on_empty_table() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
let rows = select_rows(&db, "SELECT COUNT(*), MIN(v), MAX(v) FROM t");
assert_eq!(rows[0][0], Value::Integer(0), "COUNT on empty = 0");
}
#[test]
fn test_count_with_where_no_match() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 0..10 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", i))
.unwrap();
}
let rows = select_rows(&db, "SELECT COUNT(*) FROM t WHERE v > 100");
assert_eq!(rows[0][0], Value::Integer(0), "no match → COUNT 0");
}
#[test]
fn test_min_max_extremes() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
db.execute("INSERT INTO t (v) VALUES (5)").unwrap();
db.execute("INSERT INTO t (v) VALUES (100)").unwrap();
db.execute("INSERT INTO t (v) VALUES (42)").unwrap();
let rows = select_rows(&db, "SELECT MIN(v), MAX(v) FROM t");
let to_i = |v: &Value| match v {
Value::Integer(n) => Some(*n),
Value::Float(f) => Some(*f as i64),
_ => None,
};
let min = rows[0].get(0).and_then(to_i);
let max = rows[0].get(1).and_then(to_i);
assert_eq!(min, Some(5), "MIN picks the smallest");
assert_eq!(max, Some(100), "MAX picks the largest");
}
#[test]
fn test_order_by_asc_with_limit() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for v in [50, 10, 40, 20, 30] {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", v))
.unwrap();
}
let rows = select_rows(&db, "SELECT v FROM t ORDER BY v ASC LIMIT 3");
assert_eq!(rows.len(), 3, "LIMIT 3 returns 3 rows");
let vals: Vec<i64> = rows
.iter()
.filter_map(|r| match r.first() {
Some(Value::Integer(n)) => Some(*n),
_ => None,
})
.collect();
for i in 1..vals.len() {
assert!(
vals[i] >= vals[i - 1],
"ORDER BY ASC not monotonic: {} < {}",
vals[i],
vals[i - 1]
);
}
}
#[test]
fn test_limit_larger_than_table() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
db.execute("INSERT INTO t (v) VALUES (1)").unwrap();
db.execute("INSERT INTO t (v) VALUES (2)").unwrap();
let rows = select_rows(&db, "SELECT v FROM t ORDER BY v ASC LIMIT 100");
assert_eq!(rows.len(), 2, "LIMIT > rows returns all rows");
}
#[test]
fn test_offset_pagination() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 1..=10 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", i))
.unwrap();
}
let page1 = select_rows(&db, "SELECT v FROM t ORDER BY v ASC LIMIT 3 OFFSET 0");
let page2 = select_rows(&db, "SELECT v FROM t ORDER BY v ASC LIMIT 3 OFFSET 3");
assert_eq!(page1[0][0], Value::Integer(1), "page 1 starts at 1");
assert_eq!(page2[0][0], Value::Integer(4), "page 2 starts at 4");
}
#[test]
fn test_delete_all_rows() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 0..5 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", i))
.unwrap();
}
db.execute("DELETE FROM t").unwrap();
assert_eq!(count(&db, "t"), 0, "all rows deleted");
assert!(select_rows(&db, "SELECT * FROM t").is_empty());
}
#[test]
fn test_delete_then_insert_cycle() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
db.execute("INSERT INTO t (v) VALUES (1)").unwrap();
db.execute("DELETE FROM t WHERE id = 1").unwrap();
db.execute("INSERT INTO t (v) VALUES (2)").unwrap();
assert_eq!(count(&db, "t"), 1);
let rows = select_rows(&db, "SELECT * FROM t");
assert_eq!(rows[0][1], Value::Integer(2));
}
#[test]
fn test_update_nonexistent_row() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 10)").unwrap();
db.execute("UPDATE t SET v = 999 WHERE id = 100").unwrap();
assert_eq!(count(&db, "t"), 1);
let rows = select_rows(&db, "SELECT v FROM t");
assert_eq!(
rows[0][0],
Value::Integer(10),
"no change when updating non-existent"
);
}
#[test]
fn test_update_all_rows() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 0..5 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", i))
.unwrap();
}
db.execute("UPDATE t SET v = 999").unwrap();
let rows = select_rows(&db, "SELECT v FROM t");
for row in &rows {
assert_eq!(row[0], Value::Integer(999), "all rows updated");
}
}