use reddb_server::{RedDBOptions, RedDBRuntime};
const RUNS: usize = 20;
fn seed_groupby(rt: &RedDBRuntime) {
rt.execute_query("CREATE TABLE sales (region TEXT, tier INT, amount INT, units INT)")
.expect("create sales");
rt.execute_query(
"INSERT INTO sales (region, tier, amount, units) VALUES \
('us', 1, 10, 1), ('eu', 1, 20, 2), ('us', 2, 30, 3), \
('eu', 2, 40, 4), ('asia', 1, 50, 5), ('us', 1, 60, 6), \
('asia', 2, 70, 7), ('eu', 1, 80, 8), ('us', 2, 90, 9), \
('asia', 1, 100, 10)",
)
.expect("insert sales");
}
fn columns_for(rt: &RedDBRuntime, sql: &str) -> Vec<String> {
let r = rt
.execute_query(sql)
.unwrap_or_else(|e| panic!("query failed: {sql}\n err: {e}"));
r.result.columns.clone()
}
fn rows_for(rt: &RedDBRuntime, sql: &str) -> Vec<Vec<String>> {
let r = rt
.execute_query(sql)
.unwrap_or_else(|e| panic!("query failed: {sql}\n err: {e}"));
r.result
.records
.iter()
.map(|rec| {
rec.column_names()
.iter()
.map(|c| format!("{:?}", rec.get(c.as_ref())))
.collect()
})
.collect()
}
#[test]
fn groupby_column_and_row_ordering_is_stable_across_runs() {
let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime boots");
seed_groupby(&rt);
let sql = "SELECT region, SUM(amount), SUM(units) FROM sales GROUP BY region";
let first_cols = columns_for(&rt, sql);
let first_rows = rows_for(&rt, sql);
assert!(
first_cols.len() >= 2,
"GROUP BY query should expose multiple result columns, got {first_cols:?}"
);
assert!(
!first_rows.is_empty(),
"GROUP BY query should return rows for the seeded data"
);
for run in 1..RUNS {
let cols = columns_for(&rt, sql);
assert_eq!(
cols, first_cols,
"GROUP BY column ordering drifted on run {run}: first={first_cols:?} now={cols:?}"
);
let rows = rows_for(&rt, sql);
assert_eq!(
rows, first_rows,
"GROUP BY row ordering drifted on run {run}: first={first_rows:?} now={rows:?}"
);
}
}
#[test]
fn multi_aggregate_column_ordering_is_stable_across_runs() {
let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime boots");
seed_groupby(&rt);
let sql = "SELECT COUNT(*), SUM(amount), SUM(units), MIN(amount), MAX(amount) FROM sales";
let first_cols = columns_for(&rt, sql);
let first_rows = rows_for(&rt, sql);
for run in 1..RUNS {
assert_eq!(
columns_for(&rt, sql),
first_cols,
"multi-aggregate column order drifted on run {run}"
);
assert_eq!(
rows_for(&rt, sql),
first_rows,
"multi-aggregate row order drifted on run {run}"
);
}
}
#[test]
fn select_star_after_reopen_keeps_declared_column_order() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("ordering.rdb");
{
let rt =
RedDBRuntime::with_options(RedDBOptions::persistent(&db_path)).expect("runtime boots");
rt.execute_query("CREATE TABLE chapter_words (chapter TEXT, word TEXT, freq INTEGER)")
.expect("create table");
rt.execute_query(
"INSERT INTO chapter_words (chapter, word, freq) VALUES \
('one', 'alpha', 1), ('two', 'beta', 2), ('three', 'gamma', 3)",
)
.expect("insert rows");
drop(rt);
}
let rt2 =
RedDBRuntime::with_options(RedDBOptions::persistent(&db_path)).expect("runtime reopens");
let sql = "SELECT * FROM chapter_words";
let first_cols = columns_for(&rt2, sql);
for declared in ["chapter", "word", "freq"] {
assert!(
first_cols.iter().any(|c| c == declared),
"SELECT * dropped declared column {declared:?} after reopen (got {first_cols:?})"
);
}
let first_rows = rows_for(&rt2, sql);
for run in 1..RUNS {
let cols = columns_for(&rt2, sql);
assert_eq!(
cols, first_cols,
"SELECT * column order drifted on run {run}: first={first_cols:?} now={cols:?}"
);
let rows = rows_for(&rt2, sql);
assert_eq!(rows, first_rows, "SELECT * row order drifted on run {run}");
}
}