use stoolap::Database;
fn setup_sales_table(db: &Database) {
db.execute(
"CREATE TABLE sales (region TEXT, product TEXT, amount FLOAT)",
(),
)
.expect("Failed to create table");
let inserts = [
"INSERT INTO sales VALUES ('East', 'A', 100.0)",
"INSERT INTO sales VALUES ('East', 'B', 150.0)",
"INSERT INTO sales VALUES ('West', 'A', 200.0)",
"INSERT INTO sales VALUES ('West', 'B', 250.0)",
];
for insert in &inserts {
db.execute(insert, ()).expect("Failed to insert data");
}
}
#[test]
fn test_rollup_basic() {
let db = Database::open("memory://rollup_basic").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY ROLLUP(region, product)",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
let mut has_region_subtotal = false;
let mut has_detail = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let sum: f64 = row.get(2).unwrap();
if region.is_none() && product.is_none() {
has_grand_total = true;
assert!(
(sum - 700.0).abs() < 0.01,
"Grand total should be 700, got {}",
sum
);
} else if region.is_some() && product.is_none() {
has_region_subtotal = true;
let r = region.unwrap();
if r == "East" {
assert!(
(sum - 250.0).abs() < 0.01,
"East subtotal should be 250, got {}",
sum
);
} else if r == "West" {
assert!(
(sum - 450.0).abs() < 0.01,
"West subtotal should be 450, got {}",
sum
);
}
} else {
has_detail = true;
}
}
assert_eq!(
row_count, 7,
"ROLLUP should produce 7 rows (4 detail + 2 subtotal + 1 grand total)"
);
assert!(has_grand_total, "Should have a grand total row");
assert!(has_region_subtotal, "Should have region subtotal rows");
assert!(has_detail, "Should have detail rows");
}
#[test]
fn test_rollup_single_column() {
let db = Database::open("memory://rollup_single").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount) FROM sales GROUP BY ROLLUP(region)",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
if region.is_none() {
has_grand_total = true;
assert!(
(sum - 700.0).abs() < 0.01,
"Grand total should be 700, got {}",
sum
);
}
}
assert_eq!(
row_count, 3,
"ROLLUP(region) should produce 3 rows (2 regions + 1 grand total)"
);
assert!(has_grand_total, "Should have a grand total row");
}
#[test]
fn test_cube_basic() {
let db = Database::open("memory://cube_basic").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY CUBE(region, product)",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
let mut has_region_subtotal = false;
let mut has_product_subtotal = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let sum: f64 = row.get(2).unwrap();
if region.is_none() && product.is_none() {
has_grand_total = true;
assert!(
(sum - 700.0).abs() < 0.01,
"Grand total should be 700, got {}",
sum
);
} else if region.is_some() && product.is_none() {
has_region_subtotal = true;
} else if let (None, Some(p)) = (®ion, &product) {
has_product_subtotal = true;
if p == "A" {
assert!(
(sum - 300.0).abs() < 0.01,
"Product A total should be 300, got {}",
sum
);
} else if p == "B" {
assert!(
(sum - 400.0).abs() < 0.01,
"Product B total should be 400, got {}",
sum
);
}
}
}
assert_eq!(
row_count, 9,
"CUBE should produce 9 rows (4 detail + 2 region + 2 product + 1 grand)"
);
assert!(has_grand_total, "Should have a grand total row");
assert!(has_region_subtotal, "Should have region subtotal rows");
assert!(has_product_subtotal, "Should have product subtotal rows");
}
#[test]
fn test_rollup_with_count() {
let db = Database::open("memory://rollup_count").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, COUNT(*) FROM sales GROUP BY ROLLUP(region, product)",
(),
)
.expect("Failed to query");
let mut grand_total_count = 0i64;
for row in result {
let row = row.expect("Failed to get row");
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let count: i64 = row.get(2).unwrap();
if region.is_none() && product.is_none() {
grand_total_count = count;
}
}
assert_eq!(grand_total_count, 4, "Grand total COUNT(*) should be 4");
}
#[test]
fn test_rollup_with_multiple_aggregates() {
let db = Database::open("memory://rollup_multi_agg").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount), AVG(amount), COUNT(*) FROM sales GROUP BY ROLLUP(region)",
(),
)
.expect("Failed to query");
let mut has_grand_total = false;
for row in result {
let row = row.expect("Failed to get row");
let region: Option<String> = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
let avg: f64 = row.get(2).unwrap();
let count: i64 = row.get(3).unwrap();
if region.is_none() {
has_grand_total = true;
assert!((sum - 700.0).abs() < 0.01, "Grand total SUM should be 700");
assert!((avg - 175.0).abs() < 0.01, "Grand total AVG should be 175");
assert_eq!(count, 4, "Grand total COUNT should be 4");
}
}
assert!(has_grand_total, "Should have a grand total row");
}
#[test]
fn test_regular_group_by_unchanged() {
let db = Database::open("memory://regular_group_by").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query("SELECT region, SUM(amount) FROM sales GROUP BY region", ())
.expect("Failed to query");
let mut row_count = 0;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: String = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
assert!(!region.is_empty(), "Region should not be empty");
assert!(sum > 0.0, "Sum should be positive");
}
assert_eq!(
row_count, 2,
"Regular GROUP BY should produce 2 rows (East and West)"
);
}
#[test]
fn test_grouping_sets_basic() {
let db = Database::open("memory://grouping_sets_basic").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region, product), (region), ())",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
let mut has_region_subtotal = false;
let mut has_detail = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let sum: f64 = row.get(2).unwrap();
if region.is_none() && product.is_none() {
has_grand_total = true;
assert!(
(sum - 700.0).abs() < 0.01,
"Grand total should be 700, got {}",
sum
);
} else if region.is_some() && product.is_none() {
has_region_subtotal = true;
} else {
has_detail = true;
}
}
assert_eq!(
row_count, 7,
"GROUPING SETS should produce 7 rows (4 detail + 2 region subtotal + 1 grand total)"
);
assert!(has_grand_total, "Should have a grand total row");
assert!(has_region_subtotal, "Should have region subtotal rows");
assert!(has_detail, "Should have detail rows");
}
#[test]
fn test_grouping_sets_single_column() {
let db = Database::open("memory://grouping_sets_single").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region), ())",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
if region.is_none() {
has_grand_total = true;
assert!(
(sum - 700.0).abs() < 0.01,
"Grand total should be 700, got {}",
sum
);
}
}
assert_eq!(
row_count, 3,
"GROUPING SETS ((region), ()) should produce 3 rows"
);
assert!(has_grand_total, "Should have a grand total row");
}
#[test]
fn test_grouping_sets_no_grand_total() {
let db = Database::open("memory://grouping_sets_no_grand").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region), (product))",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
if region.is_none() && product.is_none() {
has_grand_total = true;
}
}
assert_eq!(
row_count, 4,
"Should produce 4 rows (2 regions + 2 products)"
);
assert!(
!has_grand_total,
"Should NOT have grand total (no empty set)"
);
}
#[test]
fn test_grouping_sets_with_grouping_function() {
let db = Database::open("memory://grouping_sets_function").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, product, SUM(amount), GROUPING(region), GROUPING(product) FROM sales GROUP BY GROUPING SETS ((region, product), (region), ())",
(),
)
.expect("Failed to query");
let mut grand_total_groupings = None;
let mut region_subtotal_groupings = None;
let mut detail_groupings = None;
for row in result {
let row = row.expect("Failed to get row");
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let g_region: i64 = row.get(3).unwrap();
let g_product: i64 = row.get(4).unwrap();
if region.is_none() && product.is_none() {
grand_total_groupings = Some((g_region, g_product));
} else if region.is_some() && product.is_none() {
region_subtotal_groupings = Some((g_region, g_product));
} else if region.is_some() && product.is_some() {
detail_groupings = Some((g_region, g_product));
}
}
assert_eq!(
grand_total_groupings,
Some((1, 1)),
"Grand total should have GROUPING(region)=1, GROUPING(product)=1"
);
assert_eq!(
region_subtotal_groupings,
Some((0, 1)),
"Region subtotal should have GROUPING(region)=0, GROUPING(product)=1"
);
assert_eq!(
detail_groupings,
Some((0, 0)),
"Detail row should have GROUPING(region)=0, GROUPING(product)=0"
);
}
#[test]
fn test_grouping_sets_equivalent_to_rollup() {
let db = Database::open("memory://grouping_sets_equiv").expect("Failed to create database");
setup_sales_table(&db);
let rollup_result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY ROLLUP(region, product) ORDER BY region, product",
(),
)
.expect("Failed to query ROLLUP");
let grouping_sets_result = db
.query(
"SELECT region, product, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region, product), (region), ()) ORDER BY region, product",
(),
)
.expect("Failed to query GROUPING SETS");
let rollup_rows: Vec<_> = rollup_result.collect();
let gs_rows: Vec<_> = grouping_sets_result.collect();
assert_eq!(
rollup_rows.len(),
gs_rows.len(),
"ROLLUP and equivalent GROUPING SETS should have same row count"
);
for (r_row, gs_row) in rollup_rows.iter().zip(gs_rows.iter()) {
let r_row = r_row.as_ref().expect("ROLLUP row error");
let gs_row = gs_row.as_ref().expect("GROUPING SETS row error");
let r_sum: f64 = r_row.get(2).unwrap();
let gs_sum: f64 = gs_row.get(2).unwrap();
assert!(
(r_sum - gs_sum).abs() < 0.01,
"Sums should match: ROLLUP={}, GROUPING SETS={}",
r_sum,
gs_sum
);
}
}
#[test]
fn test_grouping_sets_with_multiple_aggregates() {
let db = Database::open("memory://grouping_sets_multi_agg").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount), AVG(amount), COUNT(*) FROM sales GROUP BY GROUPING SETS ((region), ())",
(),
)
.expect("Failed to query");
let mut has_grand_total = false;
for row in result {
let row = row.expect("Failed to get row");
let region: Option<String> = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
let avg: f64 = row.get(2).unwrap();
let count: i64 = row.get(3).unwrap();
if region.is_none() {
has_grand_total = true;
assert!((sum - 700.0).abs() < 0.01, "Grand total SUM should be 700");
assert!((avg - 175.0).abs() < 0.01, "Grand total AVG should be 175");
assert_eq!(count, 4, "Grand total COUNT should be 4");
}
}
assert!(has_grand_total, "Should have a grand total row");
}
#[test]
fn test_grouping_sets_only_grand_total() {
let db =
Database::open("memory://grouping_sets_grand_only").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT SUM(amount), COUNT(*) FROM sales GROUP BY GROUPING SETS (())",
(),
)
.expect("Failed to query");
let mut row_count = 0;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let sum: f64 = row.get(0).unwrap();
let count: i64 = row.get(1).unwrap();
assert!((sum - 700.0).abs() < 0.01, "Sum should be 700");
assert_eq!(count, 4, "Count should be 4");
}
assert_eq!(row_count, 1, "Should have exactly 1 row (grand total)");
}
#[test]
fn test_grouping_sets_with_having() {
let db = Database::open("memory://grouping_sets_having").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region), ()) HAVING SUM(amount) > 300",
(),
)
.expect("Failed to query");
let mut row_count = 0;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let sum: f64 = row.get(1).unwrap();
assert!(sum > 300.0, "All rows should have SUM > 300 due to HAVING");
}
assert_eq!(
row_count, 2,
"Should have 2 rows after HAVING filter (West + Grand)"
);
}
#[test]
fn test_grouping_sets_with_where() {
let db = Database::open("memory://grouping_sets_where").expect("Failed to create database");
setup_sales_table(&db);
let result = db
.query(
"SELECT region, SUM(amount) FROM sales WHERE amount >= 150 GROUP BY GROUPING SETS ((region), ())",
(),
)
.expect("Failed to query");
let mut grand_total = 0.0;
for row in result {
let row = row.expect("Failed to get row");
let region: Option<String> = row.get(0).unwrap();
let sum: f64 = row.get(1).unwrap();
if region.is_none() {
grand_total = sum;
}
}
assert!(
(grand_total - 600.0).abs() < 0.01,
"Grand total with WHERE should be 600, got {}",
grand_total
);
}
#[test]
fn test_grouping_sets_three_columns() {
let db = Database::open("memory://grouping_sets_three").expect("Failed to create database");
db.execute(
"CREATE TABLE orders (region TEXT, product TEXT, channel TEXT, amount FLOAT)",
(),
)
.expect("Failed to create table");
let inserts = [
"INSERT INTO orders VALUES ('East', 'A', 'Online', 100.0)",
"INSERT INTO orders VALUES ('East', 'B', 'Store', 150.0)",
"INSERT INTO orders VALUES ('West', 'A', 'Online', 200.0)",
"INSERT INTO orders VALUES ('West', 'B', 'Store', 250.0)",
];
for insert in &inserts {
db.execute(insert, ()).expect("Failed to insert data");
}
let result = db
.query(
"SELECT region, product, channel, SUM(amount) FROM orders GROUP BY GROUPING SETS ((region, product, channel), (region, product), (region), ())",
(),
)
.expect("Failed to query");
let mut row_count = 0;
let mut has_grand_total = false;
let mut has_region_only = false;
let mut has_region_product = false;
let mut has_all_three = false;
for row in result {
let row = row.expect("Failed to get row");
row_count += 1;
let region: Option<String> = row.get(0).unwrap();
let product: Option<String> = row.get(1).unwrap();
let channel: Option<String> = row.get(2).unwrap();
match (®ion, &product, &channel) {
(None, None, None) => has_grand_total = true,
(Some(_), None, None) => has_region_only = true,
(Some(_), Some(_), None) => has_region_product = true,
(Some(_), Some(_), Some(_)) => has_all_three = true,
_ => {}
}
}
assert_eq!(
row_count, 11,
"Should produce 11 rows for this GROUPING SETS"
);
assert!(has_grand_total, "Should have grand total");
assert!(has_region_only, "Should have region-only rows");
assert!(has_region_product, "Should have region-product rows");
assert!(has_all_three, "Should have all-three-columns rows");
}