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 t (id INT PRIMARY KEY, n INT, val INT);
INSERT INTO t (id, n, val) VALUES (2, 3, 7), (1, 5, 20), (3, 9, 40);
"#;
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()
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_resolves_simple_projection_alias() {
let mut h = Harness::new();
let result = query(&mut h, "SELECT id AS ident FROM t ORDER BY ident");
assert_eq!(column_names(&result), vec!["ident".to_string()]);
assert_eq!(int_column(&result, 0), vec![1, 2, 3]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_resolves_expression_projection_alias() {
let mut h = Harness::new();
let result = query(&mut h, "SELECT n * 2 AS doubled FROM t ORDER BY doubled");
assert_eq!(column_names(&result), vec!["doubled".to_string()]);
assert_eq!(int_column(&result, 0), vec![6, 10, 18]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_alias_honours_descending_direction() {
let mut h = Harness::new();
let result = query(&mut h, "SELECT id AS ident FROM t ORDER BY ident DESC");
assert_eq!(int_column(&result, 0), vec![3, 2, 1]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_resolves_aggregate_alias() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(val) AS total FROM t GROUP BY id ORDER BY total DESC",
);
assert_eq!(
column_names(&result),
vec!["id".to_string(), "total".to_string()]
);
assert_eq!(int_column(&result, 0), vec![3, 1, 2]);
assert_eq!(int_column(&result, 1), vec![40, 20, 7]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn having_resolves_aggregate_alias() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(val) AS total FROM t GROUP BY id HAVING total > 15 ORDER BY id",
);
assert_eq!(int_column(&result, 0), vec![1, 3]);
assert_eq!(int_column(&result, 1), vec![20, 40]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn having_and_order_by_resolve_same_alias() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(val) AS total FROM t GROUP BY id HAVING total > 15 ORDER BY total ASC",
);
assert_eq!(int_column(&result, 1), vec![20, 40]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_prefers_projection_alias_over_shadowed_base_column() {
let mut h = Harness::new();
let result = query(&mut h, "SELECT id AS n FROM t ORDER BY n");
assert_eq!(column_names(&result), vec!["n".to_string()]);
assert_eq!(int_column(&result, 0), vec![1, 2, 3]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn order_by_base_column_with_aliased_projection_still_works() {
let mut h = Harness::new();
let result = query(&mut h, "SELECT id AS ident FROM t ORDER BY id");
assert_eq!(column_names(&result), vec!["ident".to_string()]);
assert_eq!(int_column(&result, 0), vec![1, 2, 3]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn having_aggregate_expression_without_alias_still_works() {
let mut h = Harness::new();
let result = query(
&mut h,
"SELECT id, SUM(val) AS total FROM t GROUP BY id HAVING SUM(val) > 15 ORDER BY id",
);
assert_eq!(int_column(&result, 0), vec![1, 3]);
assert_eq!(int_column(&result, 1), vec![20, 40]);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn where_does_not_see_projection_alias() {
let mut h = Harness::new();
let err = h.run_err("SELECT id AS ident FROM t WHERE ident > 1");
assert!(
err.contains("ALOPEX-C003") && err.contains("'ident'"),
"expected column-not-found for alias in WHERE, got: {err}"
);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn group_by_does_not_see_projection_alias() {
let mut h = Harness::new();
let err = h.run_err("SELECT id AS ident, SUM(val) FROM t GROUP BY ident");
assert!(
err.contains("ALOPEX-C003") && err.contains("'ident'"),
"expected column-not-found for alias in GROUP BY, got: {err}"
);
}