From 4a817d0837b631c2d09717dd357746f3e14461ea Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 14:51:28 +0200 Subject: [PATCH] test: cover OrderByNullsTest and run --- src/test/kotlin/kestrel/OrderByNullsTest.kt | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/test/kotlin/kestrel/OrderByNullsTest.kt diff --git a/src/test/kotlin/kestrel/OrderByNullsTest.kt b/src/test/kotlin/kestrel/OrderByNullsTest.kt new file mode 100644 index 0000000..e66f6dd --- /dev/null +++ b/src/test/kotlin/kestrel/OrderByNullsTest.kt @@ -0,0 +1,117 @@ +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 + +/** + * `ORDER BY expr [ASC|DESC] [NULLS FIRST|NULLS LAST]` on the in-memory engine, covering every + * comparator that shares [kestrel.engine.compareOrderKeyValues]: plain top-level ORDER BY, a + * GROUP BY's own ORDER BY (over already-aggregated rows), a window function's `OVER (... ORDER BY + * ...)`, and STRING_AGG's internal ORDER BY. [kestrel.store.sql.DiskExecutorOrderByNullsTest] mirrors + * the plain-ORDER-BY and window cases against the disk engine. + */ +class OrderByNullsTest { + private val catalog = Catalog() + private val executor = Executor(catalog) + + private fun run(sql: String): List = + 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) + + /** ids 2 and 4 are NULL; the rest ascend 10/20/30 out of id order so ASC/DESC actually reorder them. */ + private fun seedScored() { + run("CREATE TABLE scored (id INTEGER PRIMARY KEY, score INTEGER)") + run("INSERT INTO scored VALUES (1, 10), (2, NULL), (3, 30), (4, NULL), (5, 20)") + } + + @Test + fun ascendingOrderByDefaultsToNullsFirstButNullsLastOverridesIt() { + seedScored() + assertEquals( + listOf(2, 4, 1, 5, 3).map { listOf(int(it.toLong())) }, + select("SELECT id FROM scored ORDER BY score").rows + ) + assertEquals( + listOf(1, 5, 3, 2, 4).map { listOf(int(it.toLong())) }, + select("SELECT id FROM scored ORDER BY score NULLS LAST").rows + ) + } + + @Test + fun descendingOrderByDefaultsToNullsLastButNullsFirstOverridesIt() { + seedScored() + assertEquals( + listOf(3, 5, 1, 2, 4).map { listOf(int(it.toLong())) }, + select("SELECT id FROM scored ORDER BY score DESC").rows + ) + assertEquals( + listOf(2, 4, 3, 5, 1).map { listOf(int(it.toLong())) }, + select("SELECT id FROM scored ORDER BY score DESC NULLS FIRST").rows + ) + } + + @Test + fun nullsLastOnASecondaryKeyOnlyAffectsThatKeyInMultiColumnOrderBy() { + run("CREATE TABLE grouped2 (id INTEGER PRIMARY KEY, grp TEXT, score INTEGER)") + run("INSERT INTO grouped2 VALUES (1, 'a', NULL), (2, 'a', 5), (3, 'b', NULL), (4, 'b', 1)") + val result = select("SELECT id FROM grouped2 ORDER BY grp, score NULLS LAST") + assertEquals(listOf(2, 1, 4, 3).map { listOf(int(it.toLong())) }, result.rows) + } + + @Test + fun explicitNullsLastControlsWhereAnAllNullAggregateGroupSortsRelativeToOthers() { + run("CREATE TABLE g (id INTEGER PRIMARY KEY, dept TEXT, bonus INTEGER)") + run("INSERT INTO g VALUES (1, 'x', 100), (2, 'x', NULL), (3, 'y', NULL)") + assertEquals( + listOf(listOf(text("y"), Value.NullValue), listOf(text("x"), int(100))), + select("SELECT dept, MAX(bonus) FROM g GROUP BY dept ORDER BY MAX(bonus)").rows + ) + assertEquals( + listOf(listOf(text("x"), int(100)), listOf(text("y"), Value.NullValue)), + select("SELECT dept, MAX(bonus) FROM g GROUP BY dept ORDER BY MAX(bonus) NULLS LAST").rows + ) + } + + @Test + fun windowFunctionOrderByHonorsAnExplicitNullsLastClause() { + seedScored() + val result = select( + "SELECT id, ROW_NUMBER() OVER (ORDER BY score NULLS LAST) AS rn FROM scored ORDER BY id ASC" + ) + assertEquals( + listOf( + listOf(int(1), int(1)), listOf(int(2), int(4)), listOf(int(3), int(3)), + listOf(int(4), int(5)), listOf(int(5), int(2)) + ), + result.rows + ) + } + + @Test + fun stringAggInternalOrderByHonorsAnExplicitNullsLastClause() { + run("CREATE TABLE namedScored (id INTEGER PRIMARY KEY, name TEXT, score INTEGER)") + run( + "INSERT INTO namedScored VALUES " + + "(1, 'a', 10), (2, 'b', NULL), (3, 'c', 30), (4, 'd', NULL), (5, 'e', 20)" + ) + assertEquals( + listOf(listOf(text("b,d,a,e,c"))), + select("SELECT STRING_AGG(name, ',' ORDER BY score) FROM namedScored").rows + ) + assertEquals( + listOf(listOf(text("a,e,c,b,d"))), + select("SELECT STRING_AGG(name, ',' ORDER BY score NULLS LAST) FROM namedScored").rows + ) + } +}