From 8a381f8703ff2f7efdd8cd2ed6d3e98326cbb1a3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:57:56 +0200 Subject: [PATCH] test: cover parsesGroupByGroupingSetsWithAnEmptySet --- src/test/kotlin/kestrel/ParserTest.kt | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index e31f0f6..6891e29 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -1976,4 +1976,189 @@ class ParserTest { assertEquals(listOf("dept", "region"), spec.expressions.map { (it as ColumnExpression).name }) } + @Test + fun parsesGroupByGroupingSetsWithAnEmptySet() { + val statement = parseOne( + "SELECT dept, region, SUM(amount) FROM sales GROUP BY GROUPING SETS ((dept, region), (dept), ())" + ) as SelectStatement + val spec = statement.groupingSpec as GroupingSetsSpec + assertEquals(3, spec.sets.size) + assertEquals(listOf("dept", "region"), spec.sets[0].map { (it as ColumnExpression).name }) + assertEquals(listOf("dept"), spec.sets[1].map { (it as ColumnExpression).name }) + assertTrue(spec.sets[2].isEmpty()) + assertEquals(listOf("dept", "region"), statement.groupBy.map { (it as ColumnExpression).name }) + } + + @Test + fun rollupCubeAndGroupingSetsAreNotReservedWordsAsPlainColumnNames() { + val statement = parseOne("SELECT rollup FROM t GROUP BY rollup") as SelectStatement + assertNull(statement.groupingSpec) + assertEquals(listOf("rollup"), statement.groupBy.map { (it as ColumnExpression).name }) + } + + @Test + fun groupByRollupRequiresAClosingParenthesis() { + assertFailsWith { parse("SELECT 1 FROM t GROUP BY ROLLUP (dept") } + } + + @Test + fun groupByGroupingSetsRequiresParenthesizedElements() { + assertFailsWith { parse("SELECT 1 FROM t GROUP BY GROUPING SETS (dept)") } + } + + @Test + fun parsesGroupingFunctionCallLikeAnyOtherFunction() { + val statement = parseOne("SELECT dept, GROUPING(dept) FROM sales GROUP BY ROLLUP (dept)") as SelectStatement + val projection = statement.projection as ExpressionProjection + val call = projection.items[1].expression as CallExpression + assertEquals("GROUPING", call.name) + assertEquals(listOf("dept"), call.arguments.map { (it as ColumnExpression).name }) + } + + @Test + fun plainSelectDefaultsToNotDistinct() { + val statement = parseOne("SELECT dept FROM sales") as SelectStatement + assertFalse(statement.distinct) + assertTrue(statement.distinctOn.isEmpty()) + } + + @Test + fun selectAllParsesTheSameAsNoModifierAtAll() { + val statement = parseOne("SELECT ALL dept FROM sales") as SelectStatement + assertFalse(statement.distinct) + assertTrue(statement.distinctOn.isEmpty()) + } + + @Test + fun selectDistinctSetsTheFlagWithNoDistinctOnExpressions() { + val statement = parseOne("SELECT DISTINCT dept, region FROM sales") as SelectStatement + assertTrue(statement.distinct) + assertTrue(statement.distinctOn.isEmpty()) + val projection = statement.projection as ExpressionProjection + assertEquals(listOf("dept", "region"), projection.items.map { (it.expression as ColumnExpression).name }) + } + + @Test + fun selectDistinctOnParsesItsParenthesizedExpressionList() { + val statement = parseOne("SELECT DISTINCT ON (dept, region) dept, region, amount FROM sales") as SelectStatement + assertTrue(statement.distinct) + assertEquals(listOf("dept", "region"), statement.distinctOn.map { (it as ColumnExpression).name }) + } + + @Test + fun selectDistinctOnAcceptsAnArbitraryExpressionNotJustAColumn() { + val statement = parseOne("SELECT DISTINCT ON (amount + 1) dept FROM sales") as SelectStatement + assertTrue(statement.distinctOn.single() is BinaryExpression) + } + + @Test + fun selectDistinctOnRequiresParenthesesAfterOn() { + assertFailsWith { parse("SELECT DISTINCT ON dept FROM sales") } + } + + @Test + fun selectDistinctOnRequiresAtLeastOneExpression() { + assertFailsWith { parse("SELECT DISTINCT ON () dept FROM sales") } + } + + @Test + fun selectDistinctOnRequiresAClosingParenthesis() { + assertFailsWith { parse("SELECT DISTINCT ON (dept dept FROM sales") } + } + + @Test + fun distinctWorksInsideADerivedTableSubquery() { + val statement = parseOne("SELECT * FROM (SELECT DISTINCT dept FROM sales) AS d") as SelectStatement + val source = statement.from as SubquerySource + assertTrue((source.select as SelectStatement).distinct) + } + + @Test + fun distinctSurvivesOrderByAndLimitBeingAttachedAfterward() { + val statement = parseOne("SELECT DISTINCT dept FROM sales ORDER BY dept LIMIT 5") as SelectStatement + assertTrue(statement.distinct) + assertNotNull(statement.orderBy) + assertEquals(5, statement.limit) + } + + @Test + fun dollarDigitParsesAsAParameterExpression() { + val statement = parseOne("SELECT \$1 FROM sales") as SelectStatement + val projection = statement.projection as ExpressionProjection + assertEquals(ParameterExpression(1), projection.items.single().expression) + } + + @Test + fun multiDigitParameterIndexParses() { + val statement = parseOne("SELECT \$12 FROM sales") as SelectStatement + val projection = statement.projection as ExpressionProjection + assertEquals(ParameterExpression(12), projection.items.single().expression) + } + + @Test + fun parameterIndexZeroIsAParseError() { + assertFailsWith { parse("SELECT \$0 FROM sales") } + } + + @Test + fun prepareParsesTheNameAndWrapsTheTargetStatement() { + val statement = parseOne("PREPARE byId AS SELECT * FROM sales WHERE id = \$1") as PrepareStatement + assertEquals("byId", statement.name) + val target = statement.target as SelectStatement + val where = target.where as BinaryExpression + assertEquals(BinaryOperator.Equal, where.operator) + assertEquals(ParameterExpression(1), where.right) + } + + @Test + fun prepareAcceptsInsertUpdateDeleteAndMergeAsItsTarget() { + assertTrue((parseOne("PREPARE p AS INSERT INTO sales VALUES (\$1)") as PrepareStatement).target is InsertStatement) + assertTrue((parseOne("PREPARE p AS UPDATE sales SET dept = \$1") as PrepareStatement).target is UpdateStatement) + assertTrue((parseOne("PREPARE p AS DELETE FROM sales WHERE dept = \$1") as PrepareStatement).target is DeleteStatement) + assertTrue( + (parseOne("PREPARE p AS MERGE INTO sales USING sales2 ON sales.id = sales2.id WHEN MATCHED THEN DELETE") as PrepareStatement) + .target is MergeStatement + ) + } + + @Test + fun prepareRejectsDdlAndTransactionControlTargets() { + assertFailsWith { parse("PREPARE p AS CREATE TABLE t (id INTEGER)") } + assertFailsWith { parse("PREPARE p AS BEGIN") } + assertFailsWith { parse("PREPARE p AS EXPLAIN SELECT 1") } + } + + @Test + fun executeParsesNameAndPositionalArguments() { + val statement = parseOne("EXECUTE byId(1, 'ada')") as ExecuteStatement + assertEquals("byId", statement.name) + assertEquals(listOf(intLit(1), LiteralExpression(Value.TextValue("ada"))), statement.arguments) + } + + @Test + fun executeWithNoParenthesesHasNoArguments() { + val statement = parseOne("EXECUTE byId") as ExecuteStatement + assertEquals(emptyList(), statement.arguments) + } + + @Test + fun executeWithEmptyParenthesesHasNoArguments() { + val statement = parseOne("EXECUTE byId()") as ExecuteStatement + assertEquals(emptyList(), statement.arguments) + } + + @Test + fun deallocateParsesAName() { + assertEquals(DeallocateStatement("byId"), parseOne("DEALLOCATE byId")) + } + + @Test + fun deallocatePrepareKeywordIsOptional() { + assertEquals(DeallocateStatement("byId"), parseOne("DEALLOCATE PREPARE byId")) + } + + @Test + fun deallocateAllHasNoName() { + assertEquals(DeallocateStatement(null, all = true), parseOne("DEALLOCATE ALL")) + } }