use std::time::Instant;
use stoolap::Database;
fn count_rows(db: &Database, sql: &str) -> usize {
let mut rows = db.query(sql, ()).unwrap();
let mut count = 0;
while let Some(Ok(_)) = rows.next() {
count += 1;
}
count
}
fn collect_col0(db: &Database, sql: &str) -> Vec<String> {
let mut rows = db.query(sql, ()).unwrap();
let mut result = Vec::new();
while let Some(Ok(row)) = rows.next() {
result.push(row.get::<String>(0).unwrap_or_default());
}
result
}
fn setup_candlestick_db(name: &str) -> Database {
let db = Database::open(&format!("memory://{}", name)).unwrap();
db.execute(
"CREATE TABLE candlesticks (
id INTEGER PRIMARY KEY,
exchange TEXT,
symbol TEXT,
time TIMESTAMP,
open FLOAT,
high FLOAT,
low FLOAT,
close FLOAT,
volume FLOAT
)",
(),
)
.unwrap();
db.execute("CREATE INDEX idx_time ON candlesticks (time)", ())
.unwrap();
db.execute("CREATE INDEX idx_symbol ON candlesticks (symbol)", ())
.unwrap();
db
}
fn insert_test_data(db: &Database, count: usize) {
let exchanges = ["binance", "coinbase"];
let symbols = ["BTCUSDT", "ETHUSDT"];
let mut id = 1;
for day in 10..=13 {
for hour in 0..24 {
for minute in (0..60).step_by(1) {
for exchange in &exchanges {
for symbol in &symbols {
if id > count {
return;
}
db.execute(
&format!(
"INSERT INTO candlesticks VALUES ({}, '{}', '{}', '2026-03-{:02}T{:02}:{:02}:00Z', 100.0, 105.0, 95.0, 102.0, 1000.0)",
id, exchange, symbol, day, hour, minute
),
(),
)
.unwrap();
id += 1;
}
}
}
}
}
}
#[test]
fn test_alias_shadow_correctness() {
let db = setup_candlestick_db("alias_shadow_correct");
insert_test_data(&db, 500);
let result_no_shadow = count_rows(
&db,
"SELECT TIME_TRUNC('3m', time) AS t, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
let explain_no_shadow = collect_col0(
&db,
"EXPLAIN SELECT TIME_TRUNC('3m', time) AS t, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
let explain_shadow = collect_col0(
&db,
"EXPLAIN SELECT TIME_TRUNC('3m', time) AS time, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
eprintln!("--- EXPLAIN (no shadow) ---");
for line in &explain_no_shadow {
eprintln!(" {}", line);
}
eprintln!("--- EXPLAIN (shadow) ---");
for line in &explain_shadow {
eprintln!(" {}", line);
}
let result_shadow = count_rows(
&db,
"SELECT TIME_TRUNC('3m', time) AS time, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
eprintln!("Non-shadowing alias rows: {}", result_no_shadow);
eprintln!("Shadowing alias rows: {}", result_shadow);
assert_eq!(
result_no_shadow, result_shadow,
"Alias shadowing should not change the result count"
);
}
#[test]
fn test_alias_shadow_performance() {
let db = setup_candlestick_db("alias_shadow_perf");
insert_test_data(&db, 2000);
let warmup_count = count_rows(
&db,
"SELECT COUNT(*) FROM candlesticks WHERE time >= '2026-03-13T00:00:00Z'",
);
eprintln!("Rows matching WHERE: {}", warmup_count);
let start = Instant::now();
for _ in 0..5 {
let _ = count_rows(
&db,
"SELECT TIME_TRUNC('3m', time) AS t, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
}
let no_shadow_time = start.elapsed();
let start = Instant::now();
for _ in 0..5 {
let _ = count_rows(
&db,
"SELECT TIME_TRUNC('3m', time) AS time, exchange, symbol, MAX(close)
FROM candlesticks
WHERE time >= '2026-03-13T00:00:00Z'
GROUP BY TIME_TRUNC('3m', time), exchange, symbol",
);
}
let shadow_time = start.elapsed();
eprintln!(
"Non-shadowing: {:?}, Shadowing: {:?}, ratio: {:.1}x",
no_shadow_time,
shadow_time,
shadow_time.as_secs_f64() / no_shadow_time.as_secs_f64()
);
let ratio = shadow_time.as_secs_f64() / no_shadow_time.as_secs_f64();
assert!(
ratio < 10.0,
"Alias shadowing causes {:.1}x slowdown (expected < 10x), shadow={:?} vs non-shadow={:?}",
ratio,
shadow_time,
no_shadow_time
);
}
#[test]
fn test_alias_shadow_simple_group_by() {
let db = Database::open("memory://alias_shadow_simple").unwrap();
db.execute(
"CREATE TABLE events (id INTEGER PRIMARY KEY, time TIMESTAMP, category TEXT, value INTEGER)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (1, '2026-03-10T10:00:00Z', 'A', 10)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (2, '2026-03-10T10:01:00Z', 'A', 20)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (3, '2026-03-10T10:02:00Z', 'B', 30)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (4, '2026-03-10T10:03:00Z', 'B', 40)",
(),
)
.unwrap();
let r1 = collect_col0(
&db,
"SELECT TIME_TRUNC('5m', time) AS t, SUM(value)
FROM events
GROUP BY TIME_TRUNC('5m', time)",
);
let r2 = collect_col0(
&db,
"SELECT TIME_TRUNC('5m', time) AS time, SUM(value)
FROM events
GROUP BY TIME_TRUNC('5m', time)",
);
eprintln!("Non-shadowing result: {:?}", r1);
eprintln!("Shadowing result: {:?}", r2);
assert_eq!(r1, r2, "Alias shadowing should not change GROUP BY results");
}
#[test]
fn test_alias_shadow_group_by_uses_alias_identifier() {
let db = Database::open("memory://alias_shadow_gb_alias").unwrap();
db.execute(
"CREATE TABLE events (id INTEGER PRIMARY KEY, time TIMESTAMP, value INTEGER)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (1, '2026-03-10T10:00:00Z', 10)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (2, '2026-03-10T10:01:00Z', 20)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (3, '2026-03-10T10:05:00Z', 30)",
(),
)
.unwrap();
let r1 = count_rows(
&db,
"SELECT TIME_TRUNC('5m', time) AS time, SUM(value)
FROM events
GROUP BY time",
);
let r2 = count_rows(
&db,
"SELECT TIME_TRUNC('5m', time) AS t, SUM(value)
FROM events
GROUP BY t",
);
eprintln!("GROUP BY 'time' (alias=time): {} rows", r1);
eprintln!("GROUP BY 't' (alias=t): {} rows", r2);
assert_eq!(
r1, r2,
"GROUP BY alias should work the same regardless of alias name"
);
}
#[test]
fn test_alias_no_shadow_expression_group_by() {
let db = Database::open("memory://alias_no_shadow_expr_gb").unwrap();
db.execute(
"CREATE TABLE events (id INTEGER PRIMARY KEY, time TIMESTAMP, value INTEGER)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (1, '2026-03-10T10:00:00Z', 10)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (2, '2026-03-10T10:01:00Z', 20)",
(),
)
.unwrap();
db.execute(
"INSERT INTO events VALUES (3, '2026-03-10T10:05:00Z', 30)",
(),
)
.unwrap();
let r1 = count_rows(
&db,
"SELECT TIME_TRUNC('5m', time) AS time, SUM(value)
FROM events
GROUP BY TIME_TRUNC('5m', time)",
);
let r2 = count_rows(
&db,
"SELECT TIME_TRUNC('5m', time) AS t, SUM(value)
FROM events
GROUP BY TIME_TRUNC('5m', time)",
);
eprintln!("Expression GB, alias=time: {} rows", r1);
eprintln!("Expression GB, alias=t: {} rows", r2);
assert_eq!(
r1, r2,
"GROUP BY expression should not be affected by SELECT alias name"
);
}