test: cover GroupingSetsTest and run

This commit is contained in:
dualfroz committed 2026-09-09 12:28:09 +02:00
1 parent 0cddb29e0e
commit 641dd74a09
1 file changed
+213
+213
View File
@@ -0,0 +1,213 @@
package kestrel
import kestrel.catalog.Catalog
import kestrel.engine.Executor
import kestrel.engine.SelectResult
import kestrel.engine.StatementResult
import kestrel.sql.lexer.Lexer
import kestrel.sql.parser.Parser
import kestrel.storage.Value
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
/**
* `GROUP BY ROLLUP (...)` / `CUBE (...)` / `GROUPING SETS (...)` and the `GROUPING(...)` function -
* a single query reporting subtotals (and a grand total) at several levels at once instead of a
* separate `UNION ALL` of differently-grouped queries. See [kestrel.sql.ast.GroupingSpec] and
* [kestrel.engine.GroupingSetExpansion] for how the sets are computed and
* [kestrel.engine.GroupAggregation] for how a collapsed column reads as NULL outside an aggregate.
*/
class GroupingSetsTest {
private val catalog = Catalog()
private val executor = Executor(catalog)
private fun run(sql: String): List<StatementResult> =
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
private fun select(sql: String): SelectResult = run(sql).single() as SelectResult
private fun int(value: Long) = Value.IntegerValue(value)
private fun text(value: String) = Value.TextValue(value)
private val NULL = Value.NullValue
private fun seed() {
run("CREATE TABLE sales (dept TEXT, region TEXT, amount INTEGER)")
run(
"INSERT INTO sales VALUES " +
"('eng', 'eu', 100), ('eng', 'us', 200), ('ops', 'eu', 50), ('ops', 'eu', 70)"
)
}
@Test
fun rollupProducesDetailSubtotalsAndAGrandTotal() {
seed()
val result = select(
"SELECT dept, region, SUM(amount) AS total, GROUPING(dept) AS gd, GROUPING(region) AS gr " +
"FROM sales GROUP BY ROLLUP (dept, region) " +
"ORDER BY GROUPING(dept), dept, GROUPING(region), region"
)
assertEquals(listOf("dept", "region", "total", "gd", "gr"), result.columns)
assertEquals(
listOf(
listOf(text("eng"), text("eu"), int(100), int(0), int(0)),
listOf(text("eng"), text("us"), int(200), int(0), int(0)),
listOf(text("eng"), NULL, int(300), int(0), int(1)),
listOf(text("ops"), text("eu"), int(120), int(0), int(0)),
listOf(text("ops"), NULL, int(120), int(0), int(1)),
listOf(NULL, NULL, int(420), int(1), int(1))
),
result.rows
)
}
@Test
fun cubeProducesEverySubsetOfGroupingColumns() {
seed()
val result = select(
"SELECT dept, region, SUM(amount) AS total " +
"FROM sales GROUP BY CUBE (dept, region) " +
"ORDER BY GROUPING(dept), GROUPING(region), dept, region"
)
assertEquals(
listOf(
listOf(text("eng"), text("eu"), int(100)),
listOf(text("eng"), text("us"), int(200)),
listOf(text("ops"), text("eu"), int(120)),
listOf(text("eng"), NULL, int(300)),
listOf(text("ops"), NULL, int(120)),
listOf(NULL, text("eu"), int(220)),
listOf(NULL, text("us"), int(200)),
listOf(NULL, NULL, int(420))
),
result.rows
)
}
@Test
fun explicitGroupingSetsSkipsTheFullDetailSetWhenNotListed() {
seed()
val result = select(
"SELECT dept, region, SUM(amount) AS total " +
"FROM sales GROUP BY GROUPING SETS ((dept), (region), ()) " +
"ORDER BY GROUPING(dept), GROUPING(region), dept, region"
)
assertEquals(
listOf(
listOf(text("eng"), NULL, int(300)),
listOf(text("ops"), NULL, int(120)),
listOf(NULL, text("eu"), int(220)),
listOf(NULL, text("us"), int(200)),
listOf(NULL, NULL, int(420))
),
result.rows
)
}
@Test
fun havingFiltersAcrossEverySetAtOnce() {
seed()
val result = select(
"SELECT dept, SUM(amount) AS total FROM sales GROUP BY ROLLUP (dept) " +
"HAVING SUM(amount) > 150 ORDER BY GROUPING(dept), dept"
)
assertEquals(listOf(listOf(text("eng"), int(300)), listOf(NULL, int(420))), result.rows)
}
@Test
fun plainGroupByAlwaysReportsGroupingAsZero() {
seed()
val result = select("SELECT dept, GROUPING(dept) FROM sales GROUP BY dept ORDER BY dept")
assertEquals(listOf(listOf(text("eng"), int(0)), listOf(text("ops"), int(0))), result.rows)
}
@Test
fun aCollapsedColumnInsideCaseEvaluatesAsNull() {
seed()
val result = select(
"SELECT CASE WHEN dept IS NULL THEN 'ALL DEPARTMENTS' ELSE dept END AS label, SUM(amount) AS total " +
"FROM sales GROUP BY ROLLUP (dept) ORDER BY GROUPING(dept), dept"
)
assertEquals(
listOf(
listOf(text("eng"), int(300)),
listOf(text("ops"), int(120)),
listOf(text("ALL DEPARTMENTS"), int(420))
),
result.rows
)
}
@Test
fun aggregatesInsideTheSameQuerySeeRealPerRowValuesRegardlessOfCollapsedColumns() {
seed()
// COUNT(*) on the grand-total row must count every row (4), not be tripped up by dept/region
// reading as NULL there - the nulling only ever applies outside an aggregate's own argument.
val result = select(
"SELECT dept, COUNT(*) AS n FROM sales GROUP BY ROLLUP (dept) ORDER BY GROUPING(dept), dept"
)
assertEquals(
listOf(listOf(text("eng"), int(2)), listOf(text("ops"), int(2)), listOf(NULL, int(4))),
result.rows
)
}
@Test
fun rollupOverAnEmptyTableStillProducesOneGrandTotalRow() {
run("CREATE TABLE empty_sales (dept TEXT, amount INTEGER)")
val result = select("SELECT dept, COUNT(*), SUM(amount) FROM empty_sales GROUP BY ROLLUP (dept)")
assertEquals(listOf(listOf(NULL, int(0), NULL)), result.rows)
}
@Test
fun groupingFunctionOutsideAGroupedQueryIsRejected() {
seed()
val failure = assertFailsWith<KestrelException> { select("SELECT GROUPING(dept) FROM sales") }
assertTrue(failure.message!!.contains("GROUP BY"), failure.message)
}
@Test
fun groupingFunctionArgumentMustItselfBeAGroupingColumn() {
seed()
val failure = assertFailsWith<KestrelException> {
select("SELECT GROUPING(amount) FROM sales GROUP BY ROLLUP (dept)")
}
assertTrue(failure.message!!.contains("must appear in GROUP BY"), failure.message)
}
@Test
fun groupingFunctionRequiresAtLeastOneArgument() {
seed()
val failure = assertFailsWith<KestrelException> {
select("SELECT GROUPING() FROM sales GROUP BY ROLLUP (dept)")
}
assertTrue(failure.message!!.contains("at least one argument"), failure.message)
}
@Test
fun cubeRejectsTooManyColumns() {
val columns = (1..17).joinToString(", ") { "c$it" }
run("CREATE TABLE wide (${(1..17).joinToString(", ") { "c$it INTEGER" }})")
val failure = assertFailsWith<KestrelException> {
select("SELECT COUNT(*) FROM wide GROUP BY CUBE ($columns)")
}
assertTrue(failure.message!!.contains("CUBE supports at most"), failure.message)
}
@Test
fun explainDescribesTheNumberOfGroupingSets() {
seed()
val plan = (run("EXPLAIN SELECT dept, region, SUM(amount) FROM sales GROUP BY CUBE (dept, region)").single()
as kestrel.engine.ExplainResult).plan
assertTrue(plan.any { it.contains("GroupAggregate") && it.contains("4 grouping sets") }, plan.toString())
}
@Test
fun explainOnAPlainGroupByDoesNotMentionGroupingSets() {
seed()
val plan = (run("EXPLAIN SELECT dept, SUM(amount) FROM sales GROUP BY dept").single()
as kestrel.engine.ExplainResult).plan
assertTrue(plan.any { it == "GroupAggregate keys: dept" }, plan.toString())
}
}