use motedb::Database;
use tempfile::TempDir;
use std::time::Instant;
fn create_db() -> (Database, TempDir) {
let dir = TempDir::new().expect("temp dir");
let db = Database::create(dir.path()).expect("create db");
(db, dir)
}
fn exec(db: &Database, sql: &str) -> motedb::sql::QueryResult {
db.execute(sql).expect("execute SQL").materialize().expect("materialize")
}
fn print_header(title: &str) {
println!("\n{}", "=".repeat(80));
println!(" {}", title);
println!("{}", "=".repeat(80));
}
fn print_result(name: &str, ops: usize, elapsed_ms: u64) {
let per_op_us = if ops > 0 { (elapsed_ms as f64 * 1000.0) / ops as f64 } else { 0.0 };
let throughput = if elapsed_ms > 0 { ops as f64 / (elapsed_ms as f64 / 1000.0) } else { f64::INFINITY };
println!(
"{:<55} | {:>6} ops | {:>8.1} ms | {:>8.1} µs/op | {:>10.0} ops/s",
name, ops, elapsed_ms as f64, per_op_us, throughput
);
}
#[test]
fn stress_insert_50k() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, score FLOAT, age INTEGER)");
const N: usize = 50_000;
let ms = {
let start = Instant::now();
for i in 1..=N as i64 {
exec(&db, &format!(
"INSERT INTO users VALUES ({}, 'user_{}', 'user_{}@test.com', {}, {})",
i, i, i, i as f64 * 1.5, 20 + (i % 50)
));
}
start.elapsed().as_millis() as u64
};
print_result("INSERT 50K rows (5 cols, PK auto)", N, ms);
db.flush().expect("flush");
std::thread::sleep(std::time::Duration::from_millis(500));
let cnt = exec_count(&db, "SELECT COUNT(*) AS cnt FROM users");
println!(" -> Row count after flush: {}", cnt);
assert!(cnt > 0, "Should have rows after flush");
}
#[test]
fn stress_pk_lookup() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT, score FLOAT, tag TEXT)");
const N: usize = 20_000;
const Q: usize = 10_000;
let seed_ms = {
let start = Instant::now();
for i in 1..=N as i64 {
exec(&db, &format!(
"INSERT INTO t VALUES ({}, 'val_{}', {}, 'tag_{}')",
i, i, i as f64, i % 10
));
}
start.elapsed().as_millis() as u64
};
print_result(&format!("Seed INSERT {} rows", N), N, seed_ms);
let mem_ms = {
let start = Instant::now();
for i in 1..=Q as i64 {
exec(&db, &format!("SELECT * FROM t WHERE id = {}", i));
}
start.elapsed().as_millis() as u64
};
print_result(&format!("PK SELECT {} queries (MemTable)", Q), Q, mem_ms);
db.flush().expect("flush");
std::thread::sleep(std::time::Duration::from_millis(500));
let sst_ms = {
let start = Instant::now();
for i in 1..=Q as i64 {
exec(&db, &format!("SELECT * FROM t WHERE id = {}", i));
}
start.elapsed().as_millis() as u64
};
print_result(&format!("PK SELECT {} queries (SSTable + Cache)", Q), Q, sst_ms);
let cached_ms = {
let start = Instant::now();
for _ in 0..100 {
for i in 1..=100i64 {
exec(&db, &format!("SELECT * FROM t WHERE id = {}", i));
}
}
start.elapsed().as_millis() as u64
};
print_result("PK SELECT 10K (repeated 100 queries × 100, stmt cache)", 10_000, cached_ms);
}
#[test]
fn stress_column_index() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category TEXT, price FLOAT, stock INTEGER)");
const N: usize = 30_000;
let seed_ms = {
let start = Instant::now();
for i in 1..=N as i64 {
let cat = match i % 5 {
0 => "electronics",
1 => "books",
2 => "clothing",
3 => "food",
_ => "toys",
};
exec(&db, &format!(
"INSERT INTO products VALUES ({}, 'product_{}', '{}', {}, {})",
i, i, cat, 10.0 + (i as f64 % 990.0), i % 100
));
}
start.elapsed().as_millis() as u64
};
print_result(&format!("Seed INSERT {} rows (5 categories)", N), N, seed_ms);
exec(&db, "CREATE INDEX idx_category ON products (category)");
exec(&db, "CREATE INDEX idx_price ON products (price)");
db.flush().expect("flush");
std::thread::sleep(std::time::Duration::from_millis(500));
let eq_ms = {
let start = Instant::now();
for _ in 0..100 {
exec(&db, "SELECT * FROM products WHERE category = 'electronics'");
}
start.elapsed().as_millis() as u64
};
print_result("Column eq scan × 100 (category='electronics')", 100, eq_ms);
let range_ms = {
let start = Instant::now();
for _ in 0..100 {
exec(&db, "SELECT * FROM products WHERE price > 500.0 AND price < 600.0");
}
start.elapsed().as_millis() as u64
};
print_result("Column range scan × 100 (500 < price < 600)", 100, range_ms);
}
#[test]
fn stress_full_scan() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE events (id INTEGER PRIMARY KEY, event_type TEXT, payload TEXT, ts INTEGER)");
const N: usize = 50_000;
let seed_ms = {
let start = Instant::now();
for i in 1..=N as i64 {
exec(&db, &format!(
"INSERT INTO events VALUES ({}, 'type_{}', 'payload_data_{}', {})",
i, i % 20, i, 1700000000 + i
));
}
start.elapsed().as_millis() as u64
};
print_result(&format!("Seed INSERT {} rows", N), N, seed_ms);
let mem_scan_ms = {
let start = Instant::now();
exec(&db, "SELECT * FROM events");
start.elapsed().as_millis() as u64
};
print_result(&format!("SELECT * {} rows (MemTable)", N), N, mem_scan_ms);
db.flush().expect("flush");
std::thread::sleep(std::time::Duration::from_millis(500));
let sst_scan_ms = {
let start = Instant::now();
exec(&db, "SELECT * FROM events");
start.elapsed().as_millis() as u64
};
print_result(&format!("SELECT * {} rows (SSTable)", N), N, sst_scan_ms);
let filter_ms = {
let start = Instant::now();
exec(&db, "SELECT * FROM events WHERE event_type = 'type_5'");
start.elapsed().as_millis() as u64
};
print_result("SELECT * with WHERE filter (SSTable, 1/20 match)", N, filter_ms);
let count_ms = {
let start = Instant::now();
for _ in 0..50 {
exec(&db, "SELECT COUNT(*) AS cnt FROM events");
}
start.elapsed().as_millis() as u64
};
print_result("COUNT(*) × 50 (SSTable)", 50, count_ms);
}
#[test]
fn stress_mixed_crud() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, amount FLOAT, status TEXT)");
const N: usize = 30_000;
let total_ms = {
let start = Instant::now();
for i in 1..=N as i64 {
exec(&db, &format!(
"INSERT INTO orders VALUES ({}, 'customer_{}', {}, 'pending')",
i, i % 1000, 10.0 + (i as f64 % 990.0)
));
}
let updates = N / 3;
for i in (1..=N as i64).step_by(3) {
exec(&db, &format!(
"UPDATE orders SET status = 'shipped', amount = amount + 10 WHERE id = {}", i
));
}
let deletes = N / 5;
for i in (1..=N as i64).step_by(5) {
exec(&db, &format!("DELETE FROM orders WHERE id = {}", i));
}
let selects = 5_000;
for i in (1..=selects as i64).rev() {
exec(&db, &format!("SELECT * FROM orders WHERE id = {}", i));
}
start.elapsed().as_millis() as u64
};
let total_ops = N + N / 3 + N / 5 + 5_000;
print_result(
&format!("Mixed CRUD ({} ops: {}ins + {}upd + {}del + {}sel)",
total_ops, N, N/3, N/5, 5000),
total_ops, total_ms
);
}
#[test]
fn stress_batch_insert() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE metrics (id INTEGER PRIMARY KEY, host TEXT, cpu FLOAT, mem FLOAT, ts INTEGER)");
const N: usize = 50_000;
const BATCH: usize = 500;
let ms = {
let start = Instant::now();
let mut id = 1i64;
while id <= N as i64 {
let end = (id + BATCH as i64 - 1).min(N as i64);
for i in id..=end {
exec(&db, &format!(
"INSERT INTO metrics VALUES ({}, 'host_{}', {}, {}, {})",
i, i % 10, (i as f64 % 100.0), (i as f64 % 50.0), 1700000000 + i
));
}
id = end + 1;
}
start.elapsed().as_millis() as u64
};
print_result(&format!("INSERT {} rows (batch {} chunks)", N, N / BATCH), N, ms);
}
#[test]
fn stress_prepared_statement_cache() {
let (db, _dir) = create_db();
exec(&db, "CREATE TABLE cache_test (id INTEGER PRIMARY KEY, data TEXT)");
const N: usize = 5_000;
for i in 1..=N as i64 {
exec(&db, &format!("INSERT INTO cache_test VALUES ({}, 'data_{}')", i, i));
}
let cold_ms = {
let start = Instant::now();
for i in 1..=1000i64 {
exec(&db, &format!("SELECT * FROM cache_test WHERE id = {}", i));
}
start.elapsed().as_millis() as u64
};
print_result("PK SELECT 1K queries (cold stmt cache → warm)", 1000, cold_ms);
let hot_ms = {
let start = Instant::now();
for _ in 0..100 {
for i in 1..=100i64 {
exec(&db, &format!("SELECT * FROM cache_test WHERE id = {}", i));
}
}
start.elapsed().as_millis() as u64
};
print_result("PK SELECT 10K (100 unique × 100 repeat, stmt cache hit)", 10_000, hot_ms);
let hot_per_op = hot_ms as f64 * 1000.0 / 10_000.0;
let cold_per_op = cold_ms as f64 * 1000.0 / 1_000.0;
let speedup = if hot_per_op > 0.0 { cold_per_op / hot_per_op } else { 0.0 };
println!(" -> Cold: {:.1} µs/op, Hot: {:.1} µs/op, Speedup: {:.1}x",
cold_per_op, hot_per_op, speedup);
}
fn exec_count(db: &Database, sql: &str) -> i64 {
let result = exec(db, sql);
match result {
motedb::sql::QueryResult::Select { rows, .. } => {
if let Some(row) = rows.first() {
if let Some(motedb::types::Value::Integer(cnt)) = row.first() {
return *cnt;
}
}
0
}
_ => 0,
}
}