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 scalar_i64(db: &Database, sql: &str) -> i64 {
match select_rows(db, sql).first().and_then(|r| r.first()) {
Some(Value::Integer(n)) => *n,
other => panic!("expected single Integer, got {:?}", other),
}
}
fn scalar_f64(db: &Database, sql: &str) -> f64 {
match select_rows(db, sql).first().and_then(|r| r.first()) {
Some(Value::Float(n)) => *n,
Some(Value::Integer(n)) => *n as f64,
other => panic!("expected single Float, got {:?}", other),
}
}
fn count(db: &Database, table: &str) -> i64 {
scalar_i64(db, &format!("SELECT COUNT(*) FROM {}", table))
}
#[test]
fn test_large_and_small_float_roundtrip() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v FLOAT)")
.unwrap();
let cases = [1e15, 1e-15, 1e300, 1e-300, 123456.789, 0.0001];
for (i, v) in cases.iter().enumerate() {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i + 1, v))
.unwrap();
}
let rows = select_rows(&db, "SELECT v FROM t ORDER BY id");
for (i, v) in cases.iter().enumerate() {
match rows[i][0] {
Value::Float(stored) => assert_eq!(stored, *v, "roundtrip mismatch at {}", i),
ref other => panic!("expected Float, got {:?}", other),
}
}
}
#[test]
fn test_infinity_literal_rejected() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v FLOAT)")
.unwrap();
let result = db.execute("INSERT INTO t VALUES (1, 1e400)");
assert!(
result.is_err() || result.is_ok(),
"1e400 handling is defined (reject or accept) β just must not panic"
);
}
#[test]
fn test_negative_zero_storage() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v FLOAT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 0.0)").unwrap();
db.execute("INSERT INTO t VALUES (2, -0.0)").unwrap();
let rows = select_rows(&db, "SELECT v FROM t ORDER BY id");
for row in &rows {
match row[0] {
Value::Float(f) => assert!(f == 0.0, "Β±0.0 should equal 0.0"),
_ => panic!("expected Float"),
}
}
}
#[test]
fn test_order_by_float_monotonic() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v FLOAT)")
.unwrap();
let vals = vec![-5.5, 0.0, 100.0, -0.1, 3.14, -100.0, 0.001];
for v in &vals {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", v))
.unwrap();
}
let rows = select_rows(&db, "SELECT v FROM t ORDER BY v ASC");
assert_eq!(rows.len(), vals.len());
let mut prev = f64::NEG_INFINITY;
for row in &rows {
match row[0] {
Value::Float(f) => {
assert!(f >= prev, "ORDER BY ASC not monotonic: {} < {}", f, prev);
prev = f;
}
_ => panic!("expected Float"),
}
}
}
#[test]
fn test_aggregates_mixed_sign_floats() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v FLOAT)")
.unwrap();
let vals = vec![-10.0, 20.0, -5.0, 15.0, 30.0];
for v in &vals {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", v))
.unwrap();
}
let sum = scalar_f64(&db, "SELECT SUM(v) FROM t");
let min = scalar_f64(&db, "SELECT MIN(v) FROM t");
let max = scalar_f64(&db, "SELECT MAX(v) FROM t");
assert_eq!(sum, 50.0, "SUM of mixed-sign");
assert_eq!(min, -10.0, "MIN must be most negative");
assert_eq!(max, 30.0, "MAX must be most positive");
}
#[test]
fn test_update_indexed_column_moves_entry() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, cat TEXT)")
.unwrap();
for i in 0..30 {
let cat = match i % 3 {
0 => "a",
1 => "b",
_ => "c",
};
db.execute(&format!("INSERT INTO t (cat) VALUES ('{}')", cat))
.unwrap();
}
db.execute("CREATE INDEX idx_cat ON t (cat) USING COLUMN")
.unwrap();
let a_ids: Vec<i64> = select_rows(&db, "SELECT id FROM t WHERE cat = 'a'")
.into_iter()
.take(10)
.filter_map(|r| match r.first() {
Some(Value::Integer(n)) => Some(*n),
_ => None,
})
.collect();
for id in &a_ids {
db.execute(&format!("UPDATE t SET cat = 'b' WHERE id = {}", id))
.unwrap();
}
let a_count = count_with_filter(&db, "cat = 'a'");
let b_count = count_with_filter(&db, "cat = 'b'");
assert_eq!(a_count, 0, "all 'a' moved to 'b'");
assert_eq!(b_count, 20, "'b' gained the moved rows");
let rows = select_rows(&db, "SELECT cat FROM t WHERE cat = 'a'");
assert!(rows.is_empty(), "old value must not be found after update");
}
fn count_with_filter(db: &Database, filter: &str) -> i64 {
scalar_i64(db, &format!("SELECT COUNT(*) FROM t WHERE {}", filter))
}
#[test]
fn test_update_indexed_column_same_value() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, cat TEXT)")
.unwrap();
db.execute("INSERT INTO t (cat) VALUES ('x')").unwrap();
db.execute("CREATE INDEX idx_cat ON t (cat) USING COLUMN")
.unwrap();
db.execute("UPDATE t SET cat = 'x' WHERE id = 1").unwrap();
let rows = select_rows(&db, "SELECT * FROM t WHERE cat = 'x'");
assert_eq!(rows.len(), 1, "same-value update keeps the row findable");
}
#[test]
fn test_delete_then_reinsert_indexed() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, cat TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'a')").unwrap();
db.execute("CREATE INDEX idx_cat ON t (cat) USING COLUMN")
.unwrap();
db.execute("DELETE FROM t WHERE id = 1").unwrap();
assert_eq!(
count_with_filter(&db, "cat = 'a'"),
0,
"deleted β not found"
);
db.execute("INSERT INTO t VALUES (1, 'a')").unwrap();
assert_eq!(
count_with_filter(&db, "cat = 'a'"),
1,
"re-inserted β found again"
);
}
#[test]
fn test_large_order_by_fully_sorted() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 0..1000 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", 1000 - i))
.unwrap(); }
let rows = select_rows(&db, "SELECT v FROM t ORDER BY v ASC");
assert_eq!(rows.len(), 1000);
let mut prev = i64::MIN;
for (i, row) in rows.iter().enumerate() {
let v = match row[0] {
Value::Integer(n) => n,
_ => panic!("expected Int"),
};
assert!(v >= prev, "row {} out of order: {} < {}", i, v, prev);
prev = v;
}
assert_eq!(rows.first().unwrap()[0], Value::Integer(1));
assert_eq!(rows.last().unwrap()[0], Value::Integer(1000));
}
#[test]
fn test_large_order_by_desc_fully_sorted() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, v INT)")
.unwrap();
for i in 0..1000 {
db.execute(&format!("INSERT INTO t (v) VALUES ({})", i))
.unwrap();
}
let rows = select_rows(&db, "SELECT v FROM t ORDER BY v DESC");
assert_eq!(rows.len(), 1000);
assert_eq!(rows.first().unwrap()[0], Value::Integer(999));
assert_eq!(rows.last().unwrap()[0], Value::Integer(0));
}
#[test]
fn test_group_by_exact_aggregate_values() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, g TEXT, v INT)")
.unwrap();
let data = [("A", 10), ("A", 20), ("A", 30), ("B", 5), ("B", 5)];
for (g, v) in &data {
db.execute(&format!("INSERT INTO t (g, v) VALUES ('{}', {})", g, v))
.unwrap();
}
let rows = select_rows(&db, "SELECT g, COUNT(*) FROM t GROUP BY g");
assert_eq!(rows.len(), 2);
let counts: std::collections::HashMap<String, i64> = rows
.iter()
.filter_map(|r| match (r.get(0), r.get(1)) {
(Some(Value::Text(t)), Some(Value::Integer(n))) => Some((t.to_string(), *n)),
_ => None,
})
.collect();
assert_eq!(counts.get("A"), Some(&3), "COUNT(A)");
assert_eq!(counts.get("B"), Some(&2), "COUNT(B)");
let s = scalar_f64(&db, "SELECT SUM(v) FROM t");
assert_eq!(s, 70.0, "SUM over whole table = 10+20+30+5+5");
}
#[test]
fn test_group_by_sum_correct_value() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, g TEXT, v INT)")
.unwrap();
let data = [("A", 10), ("A", 20), ("A", 30)];
for (g, v) in &data {
db.execute(&format!("INSERT INTO t (g, v) VALUES ('{}', {})", g, v))
.unwrap();
}
let rows = select_rows(&db, "SELECT g, SUM(v) FROM t GROUP BY g");
let sum = match rows[0].get(1) {
Some(v) => v.clone(),
None => Value::Null,
};
match sum {
Value::Integer(n) => assert_eq!(n, 60, "SUM(A)"),
Value::Float(f) => assert_eq!(f, 60.0, "SUM(A) as Float"),
other => panic!("SUM(A) should be 60, got {:?}", other),
}
}
#[test]
fn test_large_group_by_aggregates() {
let (_dir, db) = make_db();
db.execute("CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, g INT, v INT)")
.unwrap();
let mut expected: std::collections::HashMap<i64, i64> = std::collections::HashMap::new();
for i in 0..999 {
let g = i % 3;
let v = i as i64;
db.execute(&format!("INSERT INTO t (g, v) VALUES ({}, {})", g, v))
.unwrap();
*expected.entry(g).or_insert(0) += v;
}
let rows = select_rows(&db, "SELECT g, SUM(v) FROM t GROUP BY g");
for row in &rows {
let g = match row[0] {
Value::Integer(n) => n,
_ => continue,
};
let sum: i64 = match row[1] {
Value::Integer(n) => n,
Value::Float(f) => f as i64,
_ => continue,
};
assert_eq!(expected.get(&g), Some(&sum), "SUM for group {}", g);
}
}
#[test]
fn test_cross_connection_read_after_checkpoint() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)")
.unwrap();
for i in 1..=10 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i * 10))
.unwrap();
}
db.checkpoint().unwrap();
db.close().unwrap();
}
let db2 = Database::open(&path).unwrap();
assert_eq!(count(&db2, "t"), 10, "second connection must see all rows");
let rows = select_rows(&db2, "SELECT * FROM t ORDER BY id");
assert_eq!(rows.len(), 10);
assert_eq!(rows[5][1], Value::Integer(60), "values intact");
}
#[test]
fn test_repeated_reopen_accumulates() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)")
.unwrap();
for i in 1..=3 {
db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i * 100))
.unwrap();
}
db.checkpoint().unwrap();
db.close().unwrap();
let db = Database::open(&path).unwrap();
assert_eq!(count(&db, "t"), 3, "reopened db must see all 3 rows");
let rows = select_rows(&db, "SELECT * FROM t ORDER BY id");
assert_eq!(rows[0][1], Value::Integer(100));
assert_eq!(rows[2][1], Value::Integer(300));
db.close().unwrap();
}
#[test]
fn test_insert_after_reopen_multiple_rounds() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let db = Database::create(&path).unwrap();
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 100)").unwrap();
db.checkpoint().unwrap();
db.close().unwrap();
}
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let db = match Database::open(&path) {
Ok(d) => d,
Err(_) => {
let _ = tx.send(false);
return;
}
};
let res = db.execute("INSERT INTO t VALUES (2, 200)");
let _ = tx.send(res.is_ok());
db.close().unwrap();
});
let inserted = rx
.recv_timeout(std::time::Duration::from_secs(10))
.unwrap_or(false);
assert!(
inserted,
"INSERT after reopen must complete (not hang) within 10s"
);
}