From 45ca433d69e906c48d12c78feb2f64f78ae68e3f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 15:39:16 +0200 Subject: [PATCH] test: cover stringAggJoinsWithTheGivenSeparatorInRowOrderWhenThereIsN... --- src/test/kotlin/kestrel/AggregateTest.kt | 208 +++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/src/test/kotlin/kestrel/AggregateTest.kt b/src/test/kotlin/kestrel/AggregateTest.kt index ae01950..686a5a7 100644 --- a/src/test/kotlin/kestrel/AggregateTest.kt +++ b/src/test/kotlin/kestrel/AggregateTest.kt @@ -143,4 +143,212 @@ class AggregateTest { assertEquals(listOf(text("eng"), int(303)), result.rows[0]) assertEquals(listOf(text("ops"), int(122)), result.rows[1]) } + + @Test + fun stringAggJoinsWithTheGivenSeparatorInRowOrderWhenThereIsNoOrderByClause() { + run("CREATE TABLE t (label TEXT)") + run("INSERT INTO t VALUES ('c'), ('a'), ('b')") + assertEquals(listOf(listOf(text("c, a, b"))), select("SELECT STRING_AGG(label, ', ') FROM t").rows) + } + + @Test + fun stringAggOrderByInsideTheCallSortsIndependentlyOfInsertionOrder() { + run("CREATE TABLE t (label TEXT, rank INTEGER)") + run("INSERT INTO t VALUES ('c', 3), ('a', 1), ('b', 2)") + val result = select("SELECT STRING_AGG(label, '-' ORDER BY rank) FROM t") + assertEquals(listOf(listOf(text("a-b-c"))), result.rows) + } + + @Test + fun stringAggOrderByDescendingReversesTheConcatenationOrder() { + run("CREATE TABLE t (label TEXT, rank INTEGER)") + run("INSERT INTO t VALUES ('a', 1), ('b', 2), ('c', 3)") + val result = select("SELECT STRING_AGG(label, '-' ORDER BY rank DESC) FROM t") + assertEquals(listOf(listOf(text("c-b-a"))), result.rows) + } + + @Test + fun stringAggSkipsNullValuesButKeepsNonNullOnesInOrder() { + run("CREATE TABLE t (label TEXT)") + run("INSERT INTO t VALUES ('a'), (NULL), ('b')") + assertEquals(listOf(listOf(text("a, b"))), select("SELECT STRING_AGG(label, ', ') FROM t").rows) + } + + @Test + fun stringAggOverOnlyNullValuesIsNull() { + run("CREATE TABLE t (label TEXT)") + run("INSERT INTO t VALUES (NULL), (NULL)") + assertEquals(listOf(listOf(Value.NullValue)), select("SELECT STRING_AGG(label, ', ') FROM t").rows) + } + + @Test + fun stringAggOverAnEmptyTableIsNull() { + run("CREATE TABLE t (label TEXT)") + assertEquals(listOf(listOf(Value.NullValue)), select("SELECT STRING_AGG(label, ', ') FROM t").rows) + } + + @Test + fun stringAggPerGroupWithGroupBy() { + seed() + val result = select("SELECT dept, STRING_AGG(region, '/' ORDER BY region) FROM sales GROUP BY dept ORDER BY dept ASC") + assertEquals(listOf(text("eng"), text("eu/us/us")), result.rows[0]) + assertEquals(listOf(text("ops"), text("eu/eu")), result.rows[1]) + } + + @Test + fun stringAggRejectsANonTextValueArgument() { + seed() + val failure = assertFailsWith { select("SELECT STRING_AGG(amount, ', ') FROM sales") } + assertEquals("function STRING_AGG requires a TEXT argument but got INTEGER", failure.message) + } + + @Test + fun stringAggRejectsANonTextSeparator() { + run("CREATE TABLE t (label TEXT)") + run("INSERT INTO t VALUES ('a')") + assertFailsWith { select("SELECT STRING_AGG(label, 1) FROM t") } + } + + @Test + fun stringAggCannotBeUsedAsAWindowFunction() { + assertFailsWith { + Parser(Lexer("SELECT STRING_AGG(label, ', ') OVER () FROM t").tokenize()).parseProgram() + } + } + + @Test + fun stringAggRequiresBothAValueAndASeparatorArgument() { + assertFailsWith { + Parser(Lexer("SELECT STRING_AGG(label) FROM t").tokenize()).parseProgram() + } + } + + private fun seedDistinct() { + run("CREATE TABLE t (dept TEXT, amount INTEGER)") + run("INSERT INTO t VALUES ('eng', 100), ('eng', 100), ('eng', 200), ('ops', 100), ('ops', NULL)") + } + + @Test + fun countDistinctCountsEachDistinctNonNullValueOnce() { + seedDistinct() + assertEquals(listOf(listOf(int(4))), select("SELECT COUNT(amount) FROM t").rows) + assertEquals(listOf(listOf(int(2))), select("SELECT COUNT(DISTINCT amount) FROM t").rows) + } + + @Test + fun sumDistinctAddsEachDistinctValueOnce() { + seedDistinct() + assertEquals(listOf(listOf(int(500))), select("SELECT SUM(amount) FROM t").rows) + assertEquals(listOf(listOf(int(300))), select("SELECT SUM(DISTINCT amount) FROM t").rows) + } + + @Test + fun avgDistinctAveragesEachDistinctValueOnce() { + seedDistinct() + assertEquals(listOf(listOf(real(125.0))), select("SELECT AVG(amount) FROM t").rows) + assertEquals(listOf(listOf(real(150.0))), select("SELECT AVG(DISTINCT amount) FROM t").rows) + } + + @Test + fun minAndMaxDistinctAreUnaffectedByDeduplication() { + seedDistinct() + val result = select("SELECT MIN(DISTINCT amount) AS lo, MAX(DISTINCT amount) AS hi FROM t") + assertEquals(listOf(int(100), int(200)), result.rows[0]) + } + + @Test + fun countDistinctIgnoresNulls() { + seedDistinct() + assertEquals(listOf(listOf(int(0))), select("SELECT COUNT(DISTINCT amount) FROM t WHERE dept = 'nope'").rows) + } + + @Test + fun countDistinctPerGroupWithGroupBy() { + seedDistinct() + val result = select("SELECT dept, COUNT(DISTINCT amount) FROM t GROUP BY dept ORDER BY dept ASC") + assertEquals(listOf(listOf(text("eng"), int(2)), listOf(text("ops"), int(1))), result.rows) + } + + @Test + fun countDistinctStarAndDistinctOverAWindowAreRejectedAtParseTime() { + assertFailsWith { Parser(Lexer("SELECT COUNT(DISTINCT *) FROM t").tokenize()).parseProgram() } + assertFailsWith { + Parser(Lexer("SELECT SUM(DISTINCT amount) OVER () FROM t").tokenize()).parseProgram() + } + } + + @Test + fun countStarFilterCountsOnlyRowsMatchingThePredicate() { + seedDistinct() + assertEquals(listOf(listOf(int(5))), select("SELECT COUNT(*) FROM t").rows) + assertEquals(listOf(listOf(int(1))), select("SELECT COUNT(*) FILTER (WHERE amount > 100) FROM t").rows) + } + + @Test + fun countColumnFilterCombinesNullSkippingWithThePredicate() { + seedDistinct() + assertEquals(listOf(listOf(int(3))), select("SELECT COUNT(amount) FILTER (WHERE dept = 'eng') FROM t").rows) + assertEquals(listOf(listOf(int(1))), select("SELECT COUNT(amount) FILTER (WHERE dept = 'ops') FROM t").rows) + } + + @Test + fun sumFilterSumsOnlyRowsMatchingThePredicate() { + seedDistinct() + assertEquals(listOf(listOf(int(400))), select("SELECT SUM(amount) FILTER (WHERE dept = 'eng') FROM t").rows) + assertEquals(listOf(listOf(int(100))), select("SELECT SUM(amount) FILTER (WHERE dept = 'ops') FROM t").rows) + } + + @Test + fun avgMinMaxFilterOperateOnlyOnRowsMatchingThePredicate() { + seedDistinct() + val result = select( + "SELECT AVG(amount) FILTER (WHERE amount >= 150) AS a, " + + "MIN(amount) FILTER (WHERE dept = 'eng') AS lo, " + + "MAX(amount) FILTER (WHERE dept = 'eng') AS hi FROM t" + ) + assertEquals(listOf(real(200.0), int(100), int(200)), result.rows[0]) + } + + @Test + fun filterExcludingEveryRowLeavesCountZeroAndSumAvgMinMaxNull() { + seedDistinct() + val result = select( + "SELECT COUNT(*) FILTER (WHERE dept = 'nope') AS c, " + + "SUM(amount) FILTER (WHERE dept = 'nope') AS s, " + + "AVG(amount) FILTER (WHERE dept = 'nope') AS a FROM t" + ) + assertEquals(listOf(int(0), Value.NullValue, Value.NullValue), result.rows[0]) + } + + @Test + fun filterCombinesWithDistinct() { + seedDistinct() + assertEquals( + listOf(listOf(int(2))), + select("SELECT COUNT(DISTINCT amount) FILTER (WHERE dept = 'eng') FROM t").rows + ) + } + + @Test + fun filterAppliesIndependentlyPerAggregatePerGroup() { + seedDistinct() + val result = select( + "SELECT dept, COUNT(*) FILTER (WHERE amount > 50) AS matching, COUNT(*) AS total " + + "FROM t GROUP BY dept ORDER BY dept ASC" + ) + assertEquals( + listOf(listOf(text("eng"), int(3), int(3)), listOf(text("ops"), int(1), int(2))), + result.rows + ) + } + + @Test + fun filterIsRejectedOnNonAggregateFunctionsAndOnWindowFunctions() { + assertFailsWith { + Parser(Lexer("SELECT UPPER(dept) FILTER (WHERE amount > 0) FROM t").tokenize()).parseProgram() + } + assertFailsWith { + Parser(Lexer("SELECT SUM(amount) FILTER (WHERE amount > 0) OVER () FROM t").tokenize()).parseProgram() + } + } }