use spg_engine::{Engine, QueryResult};
fn rows(e: &mut Engine, sql: &str) -> Vec<Vec<spg_storage::Value<'static>>> {
let r = e
.execute(sql)
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let QueryResult::Rows { rows, .. } = r else {
panic!("expected Rows");
};
rows.into_iter()
.map(|row| row.values.into_iter().collect())
.collect()
}
fn as_i64(v: &spg_storage::Value<'_>) -> i64 {
match v {
spg_storage::Value::Int(n) => i64::from(*n),
spg_storage::Value::BigInt(n) => *n,
other => panic!("expected integer, got {other:?}"),
}
}
fn cell(v: &spg_storage::Value<'_>) -> Option<String> {
match v {
spg_storage::Value::Text(s) => Some(s.to_string()),
spg_storage::Value::Null => None,
other => panic!("expected Text/Null, got {other:?}"),
}
}
#[test]
fn two_key_rollup_subtotals() {
let mut e = Engine::new();
e.execute("CREATE TABLE sales (region TEXT, product TEXT, amt INT)")
.unwrap();
e.execute(
"INSERT INTO sales VALUES \
('east', 'a', 10), ('east', 'b', 20), ('west', 'a', 5)",
)
.unwrap();
let got = rows(
&mut e,
"SELECT region, product, SUM(amt) FROM sales \
GROUP BY ROLLUP(region, product) \
ORDER BY 1 NULLS LAST, 2 NULLS LAST",
);
assert_eq!(got.len(), 6);
assert_eq!(cell(&got[0][0]).as_deref(), Some("east"));
assert_eq!(cell(&got[0][1]).as_deref(), Some("a"));
assert_eq!(as_i64(&got[0][2]), 10);
assert_eq!(cell(&got[2][0]).as_deref(), Some("east"));
assert_eq!(cell(&got[2][1]), None);
assert_eq!(as_i64(&got[2][2]), 30);
assert_eq!(cell(&got[5][0]), None);
assert_eq!(cell(&got[5][1]), None);
assert_eq!(as_i64(&got[5][2]), 35);
}
#[test]
fn single_key_rollup() {
let mut e = Engine::new();
e.execute("CREATE TABLE t1 (k TEXT, v INT)").unwrap();
e.execute("INSERT INTO t1 VALUES ('x', 1), ('x', 2), ('y', 3)")
.unwrap();
let got = rows(
&mut e,
"SELECT k, SUM(v) FROM t1 GROUP BY ROLLUP(k) ORDER BY 1 NULLS LAST",
);
assert_eq!(got.len(), 3);
assert_eq!(as_i64(&got[0][1]), 3); assert_eq!(as_i64(&got[1][1]), 3); assert_eq!(cell(&got[2][0]), None); assert_eq!(as_i64(&got[2][1]), 6);
}
#[test]
fn cube_all_subsets() {
let mut e = Engine::new();
e.execute("CREATE TABLE c2 (a TEXT, b TEXT, v INT)")
.unwrap();
e.execute("INSERT INTO c2 VALUES ('x', 'p', 1), ('x', 'q', 2), ('y', 'p', 4)")
.unwrap();
let got = rows(
&mut e,
"SELECT a, b, SUM(v) FROM c2 GROUP BY CUBE(a, b) \
ORDER BY 1 NULLS LAST, 2 NULLS LAST",
);
assert_eq!(got.len(), 8);
assert!(got.iter().any(|r| {
cell(&r[0]).is_none() && cell(&r[1]).as_deref() == Some("p") && as_i64(&r[2]) == 5
}));
assert!(
got.iter()
.any(|r| { cell(&r[0]).is_none() && cell(&r[1]).is_none() && as_i64(&r[2]) == 7 })
);
}
#[test]
fn grouping_sets_explicit_list() {
let mut e = Engine::new();
e.execute("CREATE TABLE g2 (a TEXT, b TEXT, v INT)")
.unwrap();
e.execute("INSERT INTO g2 VALUES ('x', 'p', 1), ('y', 'p', 2)")
.unwrap();
let got = rows(
&mut e,
"SELECT a, b, SUM(v) FROM g2 \
GROUP BY GROUPING SETS ((a), (b)) \
ORDER BY 1 NULLS LAST, 2 NULLS LAST",
);
assert_eq!(got.len(), 3); assert!(
got.iter()
.all(|r| !(cell(&r[0]).is_none() && cell(&r[1]).is_none()))
);
assert!(got.iter().any(|r| {
cell(&r[0]).is_none() && cell(&r[1]).as_deref() == Some("p") && as_i64(&r[2]) == 3
}));
}
#[test]
fn grouping_sets_without_aggregate_collapses_empty_set() {
let mut e = Engine::new();
e.execute("CREATE TABLE gs0 (g1 TEXT, g2 TEXT)").unwrap();
e.execute("INSERT INTO gs0 VALUES ('a','x'), ('a','y'), ('b','x')")
.unwrap();
let got = rows(
&mut e,
"SELECT g1, g2 FROM gs0 GROUP BY GROUPING SETS ((g1), (g2), ())",
);
assert_eq!(got.len(), 5);
let totals = got
.iter()
.filter(|r| cell(&r[0]).is_none() && cell(&r[1]).is_none())
.count();
assert_eq!(totals, 1);
let got = rows(&mut e, "SELECT 1 FROM gs0 GROUP BY GROUPING SETS (())");
assert_eq!(got.len(), 1);
assert_eq!(as_i64(&got[0][0]), 1);
}
#[test]
fn grouping_marker_bitmask() {
let mut e = Engine::new();
e.execute("CREATE TABLE gm (a TEXT, b TEXT, v INT)")
.unwrap();
e.execute("INSERT INTO gm VALUES ('x', 'p', 1), ('y', 'q', 2)")
.unwrap();
let got = rows(
&mut e,
"SELECT a, b, SUM(v), grouping(a, b) FROM gm \
GROUP BY ROLLUP(a, b) ORDER BY 4, 1 NULLS LAST",
);
assert_eq!(got.len(), 5); assert_eq!(as_i64(&got[0][3]), 0);
assert_eq!(as_i64(&got[1][3]), 0);
assert_eq!(as_i64(&got[2][3]), 1);
assert_eq!(as_i64(&got[3][3]), 1);
assert_eq!(as_i64(&got[4][3]), 3);
}
#[test]
fn grouping_in_case_labels_subtotals() {
let mut e = Engine::new();
e.execute("CREATE TABLE gl (k TEXT, v INT)").unwrap();
e.execute("INSERT INTO gl VALUES ('x', 1), ('y', 2)")
.unwrap();
let got = rows(
&mut e,
"SELECT CASE WHEN grouping(k) = 1 THEN 'TOTAL' ELSE k END, SUM(v) \
FROM gl GROUP BY ROLLUP(k) ORDER BY 2",
);
assert_eq!(got.len(), 3);
assert!(got.iter().any(|r| {
matches!(&r[0], spg_storage::Value::Text(s) if s == "TOTAL") && as_i64(&r[1]) == 3
}));
}