use motedb::Database;
use std::time::Instant;
use tempfile::TempDir;
fn is_ci() -> bool {
std::env::var("CI").is_ok()
}
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_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]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
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)");
let n: usize = if is_ci() { 10_000 } else { 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(&format!("INSERT {} rows (5 cols, PK auto)", n), n, ms);
db.flush().expect("flush");
db.wait_for_indexes_ready();
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]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
fn stress_pk_lookup() {
let (db, _dir) = create_db();
exec(
&db,
"CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT, score FLOAT, tag TEXT)",
);
let n: usize = if is_ci() { 5_000 } else { 20_000 };
let q: usize = if is_ci() { 2_000 } else { 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");
db.wait_for_indexes_ready();
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_reps = if is_ci() { 20 } else { 100 };
let cached_total = 100 * cached_reps;
let cached_ms = {
let start = Instant::now();
for _ in 0..cached_reps {
for i in 1..=100i64 {
exec(&db, &format!("SELECT * FROM t WHERE id = {}", i));
}
}
start.elapsed().as_millis() as u64
};
print_result(
&format!(
"PK SELECT {} (repeated 100 queries × {}, stmt cache)",
cached_total, cached_reps
),
cached_total,
cached_ms,
);
}
#[test]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
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)");
let n: usize = if is_ci() { 5_000 } else { 30_000 };
let queries: usize = if is_ci() { 20 } else { 100 };
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");
db.wait_for_indexes_ready();
let eq_ms = {
let start = Instant::now();
for _ in 0..queries {
exec(&db, "SELECT * FROM products WHERE category = 'electronics'");
}
start.elapsed().as_millis() as u64
};
print_result(
&format!("Column eq scan × {} (category='electronics')", queries),
queries,
eq_ms,
);
let range_ms = {
let start = Instant::now();
for _ in 0..queries {
exec(
&db,
"SELECT * FROM products WHERE price > 500.0 AND price < 600.0",
);
}
start.elapsed().as_millis() as u64
};
print_result(
&format!("Column range scan × {} (500 < price < 600)", queries),
queries,
range_ms,
);
}
#[test]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
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)",
);
let n: usize = if is_ci() { 10_000 } else { 50_000 };
let count_rounds: usize = if is_ci() { 10 } else { 50 };
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");
db.wait_for_indexes_ready();
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..count_rounds {
exec(&db, "SELECT COUNT(*) AS cnt FROM events");
}
start.elapsed().as_millis() as u64
};
print_result(
&format!("COUNT(*) × {} (SSTable)", count_rounds),
count_rounds,
count_ms,
);
}
#[test]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
fn stress_mixed_crud() {
let (db, _dir) = create_db();
exec(
&db,
"CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, amount FLOAT, status TEXT)",
);
let n: usize = if is_ci() { 5_000 } else { 30_000 };
let selects: usize = if is_ci() { 1_000 } else { 5_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));
}
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 + selects;
print_result(
&format!(
"Mixed CRUD ({} ops: {}ins + {}upd + {}del + {}sel)",
total_ops,
n,
n / 3,
n / 5,
selects
),
total_ops,
total_ms,
);
}
#[test]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
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)");
let n: usize = if is_ci() { 10_000 } else { 50_000 };
let batch: usize = if is_ci() { 100 } else { 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]
#[ignore = "bench/stress/perf: slow in debug, run with --ignored or via bench examples"]
fn stress_prepared_statement_cache() {
let (db, _dir) = create_db();
exec(
&db,
"CREATE TABLE cache_test (id INTEGER PRIMARY KEY, data TEXT)",
);
let n: usize = if is_ci() { 2_000 } else { 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 x 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} us/op, Hot: {:.1} us/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,
}
}