use alopex_core::kv::memory::MemoryKV;
use alopex_sql::catalog::MemoryCatalog;
use alopex_sql::dialect::AlopexDialect;
use alopex_sql::executor::{ExecutionResult, Executor, QueryResult};
use alopex_sql::parser::Parser;
use alopex_sql::planner::Planner;
use alopex_sql::storage::SqlValue;
use std::sync::{Arc, RwLock};
const FIXTURE: &str = r#"
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
region TEXT,
amount FLOAT,
qty INTEGER,
bonus FLOAT
);
INSERT INTO sales VALUES (1, 'east', 100.0, 3, 10.0);
INSERT INTO sales VALUES (2, 'east', 200.0, 1, NULL);
INSERT INTO sales VALUES (3, 'west', 150.0, 5, 20.0);
INSERT INTO sales VALUES (4, 'west', 150.0, 2, NULL);
INSERT INTO sales VALUES (5, 'north', 50.0, 0, 5.0);
"#;
struct Harness {
executor: Executor<MemoryKV, MemoryCatalog>,
catalog: Arc<RwLock<MemoryCatalog>>,
}
impl Harness {
fn new() -> Self {
let store = Arc::new(MemoryKV::new());
let catalog = Arc::new(RwLock::new(MemoryCatalog::new()));
let executor = Executor::new(store, Arc::clone(&catalog));
let mut harness = Self { executor, catalog };
harness.run_ok(FIXTURE);
harness
}
fn run_ok(&mut self, sql: &str) -> Option<QueryResult> {
match self.run(sql) {
Ok(result) => result,
Err(err) => panic!("expected `{}` to succeed, got: {err}", sql.trim()),
}
}
fn run(&mut self, sql: &str) -> Result<Option<QueryResult>, String> {
let statements =
Parser::parse_sql(&AlopexDialect, sql).map_err(|e| format!("parse: {e}"))?;
let mut last = None;
for stmt in statements {
let plan = {
let guard = self.catalog.read().unwrap();
Planner::new(&*guard)
.plan(&stmt)
.map_err(|e| format!("{e}"))?
};
if let ExecutionResult::Query(q) =
self.executor.execute(plan).map_err(|e| format!("{e}"))?
{
last = Some(q);
}
}
Ok(last)
}
fn run_err(&mut self, sql: &str) -> String {
match self.run(sql) {
Err(err) => err,
Ok(_) => panic!("expected `{}` to fail, but it succeeded", sql.trim()),
}
}
}
fn query(harness: &mut Harness, sql: &str) -> QueryResult {
harness
.run_ok(sql)
.unwrap_or_else(|| panic!("`{}` produced no query result", sql.trim()))
}
fn column_names(result: &QueryResult) -> Vec<String> {
result.columns.iter().map(|c| c.name.clone()).collect()
}
fn int_column(result: &QueryResult, index: usize) -> Vec<i64> {
result
.rows
.iter()
.map(|row| match &row[index] {
SqlValue::Integer(v) => i64::from(*v),
SqlValue::BigInt(v) => *v,
other => panic!("expected an integer at column {index}, got {other:?}"),
})
.collect()
}
fn float_column(result: &QueryResult, index: usize) -> Vec<f64> {
result
.rows
.iter()
.map(|row| match &row[index] {
SqlValue::Float(v) => f64::from(*v),
SqlValue::Double(v) => *v,
SqlValue::Integer(v) => f64::from(*v),
SqlValue::BigInt(v) => *v as f64,
other => panic!("expected a float at column {index}, got {other:?}"),
})
.collect()
}
fn text_column(result: &QueryResult, index: usize) -> Vec<String> {
result
.rows
.iter()
.map(|row| match &row[index] {
SqlValue::Text(v) => v.clone(),
other => panic!("expected text at column {index}, got {other:?}"),
})
.collect()
}
#[track_caller]
fn assert_floats_eq(actual: &[f64], expected: &[f64]) {
assert_eq!(
actual.len(),
expected.len(),
"row count mismatch: got {actual:?}, expected {expected:?}"
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!(
(a - e).abs() < 1e-6,
"value at row {i} differs: got {a}, expected {e} (full: {actual:?} vs {expected:?})"
);
}
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn empty_over_aggregates_all_rows_without_collapsing_them() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(amount) OVER () AS grand FROM sales ORDER BY id",
);
assert_eq!(
column_names(&result),
vec!["id".to_string(), "grand".to_string()]
);
assert_eq!(
result.rows.len(),
5,
"OVER () must not collapse rows; got {} row(s)",
result.rows.len()
);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_floats_eq(&float_column(&result, 1), &[650.0; 5]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn partition_by_scopes_the_aggregate_to_each_partition() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, region, SUM(amount) OVER (PARTITION BY region) AS rt \
FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_eq!(
text_column(&result, 1),
vec!["east", "east", "west", "west", "north"]
);
assert_floats_eq(
&float_column(&result, 2),
&[300.0, 300.0, 300.0, 300.0, 50.0],
);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_inside_over_produces_a_running_total() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(amount) OVER (ORDER BY id) AS running FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_floats_eq(
&float_column(&result, 1),
&[100.0, 300.0, 450.0, 600.0, 650.0],
);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn row_number_uses_window_local_ordering_independent_of_outer_order_by() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC, id) AS rn \
FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_eq!(int_column(&result, 1), vec![2, 1, 1, 2, 1]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn rank_leaves_gaps_after_ties_while_dense_rank_does_not() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, RANK() OVER (ORDER BY amount) AS r, \
DENSE_RANK() OVER (ORDER BY amount) AS dr \
FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_eq!(int_column(&result, 1), vec![2, 5, 3, 3, 1]);
assert_eq!(int_column(&result, 2), vec![2, 4, 3, 3, 1]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn multiple_window_functions_coexist_in_one_select() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, COUNT(*) OVER (PARTITION BY region) AS c, \
AVG(amount) OVER (PARTITION BY region) AS a \
FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_eq!(int_column(&result, 1), vec![2, 2, 2, 2, 1]);
assert_floats_eq(
&float_column(&result, 2),
&[150.0, 150.0, 150.0, 150.0, 50.0],
);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn window_aggregate_skips_null_inputs() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(bonus) OVER (PARTITION BY region) AS b FROM sales ORDER BY id",
);
assert_eq!(result.rows.len(), 5);
assert_eq!(int_column(&result, 0), vec![1, 2, 3, 4, 5]);
assert_floats_eq(&float_column(&result, 1), &[10.0, 10.0, 20.0, 20.0, 5.0]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn outer_order_by_resolves_window_function_alias() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT region, ROW_NUMBER() OVER (PARTITION BY region ORDER BY id) AS rn \
FROM sales ORDER BY region, rn",
);
assert_eq!(
column_names(&result),
vec!["region".to_string(), "rn".to_string()]
);
assert_eq!(result.rows.len(), 5);
assert_eq!(
text_column(&result, 0),
vec!["east", "east", "north", "west", "west"]
);
assert_eq!(int_column(&result, 1), vec![1, 2, 1, 1, 2]);
}
#[track_caller]
fn assert_rejects_named(err: &str, construct: &str) {
let haystack = err.to_ascii_lowercase();
assert!(
haystack.contains(&construct.to_ascii_lowercase()),
"error must name the unsupported construct `{construct}`, got: {err}"
);
assert!(
haystack.contains("not supported")
|| haystack.contains("unsupported")
|| haystack.contains("not implemented"),
"error must state that `{construct}` is unsupported, got: {err}"
);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn lag_is_rejected_as_unsupported() {
let mut h = Harness::new();
let err = h.run_err("SELECT LAG(amount) OVER (ORDER BY id) FROM sales");
assert_rejects_named(&err, "LAG");
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn lead_is_rejected_as_unsupported() {
let mut h = Harness::new();
let err = h.run_err("SELECT LEAD(amount) OVER (ORDER BY id) FROM sales");
assert_rejects_named(&err, "LEAD");
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn rows_between_frame_is_rejected_as_unsupported() {
let mut h = Harness::new();
let err = h.run_err(
"SELECT SUM(qty) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM sales",
);
assert_rejects_named(&err, "ROWS");
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn range_between_frame_is_rejected_as_unsupported() {
let mut h = Harness::new();
let err = h.run_err(
"SELECT SUM(qty) OVER (ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) \
FROM sales",
);
assert_rejects_named(&err, "RANGE");
}