From c971037db69b16b6fd9abcf5ee6bfd1fd2e21cbb Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 22:48:01 +0200 Subject: [PATCH] test(sql): cover nonTerminatingRecursiveCteFailsInsteadOfLoopingForever --- .../kestrel/store/sql/DiskExecutorTest.kt | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index b33df99..211eb46 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -1901,3 +1901,159 @@ class DiskExecutorTest { assertTrue(failure.message!!.contains("bad")) } + @Test + fun nonTerminatingRecursiveCteFailsInsteadOfLoopingForever() { + val db = newDatabase() + db.seedCompound() + val failure = assertFailsWith { + db.select( + "WITH RECURSIVE counter(n) AS (SELECT 1 FROM dual UNION ALL SELECT n + 1 FROM counter) SELECT n FROM counter" + ) + } + assertTrue(failure.message!!.contains("did not terminate")) + } + + @Test + fun aWithClauseThatNeverReferencesItsCteLeavesOrdinaryPrimaryKeyPushdownUntouched() { + val db = newDatabase() + db.seedUsers() + val table = db.catalog.table("users") + val pointLookupsBefore = table.pointLookupCount + val scansBefore = table.scanCount + val result = db.select("WITH unused AS (SELECT id FROM users) SELECT name FROM users WHERE id = 1") + assertEquals(listOf(listOf(text("ada"))), result.rows) + // Every CTE is materialized up front regardless of whether the body ends up using it (mirroring + // kestrel.engine.SelectRunner), so "unused" itself contributes exactly one full scan - the point is + // that the OUTER query's own WHERE id = 1 still gets its point lookup rather than also falling + // back to a scan just because a WITH clause is present. + assertEquals(scansBefore + 1, table.scanCount) + assertEquals(pointLookupsBefore + 1, table.pointLookupCount) + } + + /** + * eng (deptId 1): ada/linus tie at 400 (ada inserted first), grace at 300. sales (deptId 2): + * sam/kay tie at 200 (sam inserted first), meg at 100. Mirrors kestrel.WindowFunctionTest's seed() + * exactly so results can be cross-checked against the in-memory engine's own known-good values. + */ + private fun Database.seedWindowEmployees() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, deptId INTEGER, name TEXT, salary INTEGER)") + run("INSERT INTO departments VALUES (1, 'eng'), (2, 'sales')") + run( + "INSERT INTO employees VALUES " + + "(1, 1, 'ada', 400), (2, 1, 'linus', 400), (3, 1, 'grace', 300), " + + "(4, 2, 'sam', 200), (5, 2, 'kay', 200), (6, 2, 'meg', 100)" + ) + } + + @Test + fun rowNumberAssignsSequentialNumbersWithinEachPartition() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select( + "SELECT id, ROW_NUMBER() OVER (PARTITION BY deptId ORDER BY salary DESC) AS rn " + + "FROM employees ORDER BY id ASC" + ) + assertEquals( + listOf( + listOf(int(1), int(1)), listOf(int(2), int(2)), listOf(int(3), int(3)), + listOf(int(4), int(1)), listOf(int(5), int(2)), listOf(int(6), int(3)) + ), + result.rows + ) + } + + @Test + fun rowNumberWithoutOverOrderByFallsBackToPartitionArrivalOrder() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select("SELECT id, ROW_NUMBER() OVER (PARTITION BY deptId) AS rn FROM employees ORDER BY id ASC") + assertEquals( + listOf( + listOf(int(1), int(1)), listOf(int(2), int(2)), listOf(int(3), int(3)), + listOf(int(4), int(1)), listOf(int(5), int(2)), listOf(int(6), int(3)) + ), + result.rows + ) + } + + @Test + fun rankLeavesGapsAfterTiesButDenseRankDoesNot() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select( + "SELECT id, RANK() OVER (PARTITION BY deptId ORDER BY salary DESC) AS r, " + + "DENSE_RANK() OVER (PARTITION BY deptId ORDER BY salary DESC) AS dr " + + "FROM employees ORDER BY id ASC" + ) + assertEquals( + listOf( + listOf(int(1), int(1), int(1)), + listOf(int(2), int(1), int(1)), + listOf(int(3), int(3), int(2)), + listOf(int(4), int(1), int(1)), + listOf(int(5), int(1), int(1)), + listOf(int(6), int(3), int(2)) + ), + result.rows + ) + } + + @Test + fun rankWithoutPartitionOrdersTheWholeResultSet() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select("SELECT id, RANK() OVER (ORDER BY salary DESC) AS r FROM employees ORDER BY id ASC") + assertEquals( + listOf( + listOf(int(1), int(1)), listOf(int(2), int(1)), listOf(int(3), int(3)), + listOf(int(4), int(4)), listOf(int(5), int(4)), listOf(int(6), int(6)) + ), + result.rows + ) + } + + @Test + fun aggregateWindowWithoutOrderByRepeatsTheWholePartitionTotalOnEveryRow() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select("SELECT id, SUM(salary) OVER (PARTITION BY deptId) AS deptTotal FROM employees ORDER BY id ASC") + assertEquals( + listOf( + listOf(int(1), int(1100)), listOf(int(2), int(1100)), listOf(int(3), int(1100)), + listOf(int(4), int(500)), listOf(int(5), int(500)), listOf(int(6), int(500)) + ), + result.rows + ) + } + + @Test + fun aggregateWindowWithOrderByRunsCumulativelyAndTiesSharePeerGroupTotal() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select( + "SELECT id, SUM(salary) OVER (PARTITION BY deptId ORDER BY salary DESC) AS running FROM employees ORDER BY id ASC" + ) + assertEquals( + listOf( + listOf(int(1), int(800)), listOf(int(2), int(800)), listOf(int(3), int(1100)), + listOf(int(4), int(400)), listOf(int(5), int(400)), listOf(int(6), int(500)) + ), + result.rows + ) + } + + @Test + fun aggregateWindowWithUniqueOrderKeyProducesARunningTotal() { + val db = newDatabase() + db.seedWindowEmployees() + val result = db.select("SELECT id, SUM(salary) OVER (ORDER BY id) AS running FROM employees ORDER BY id ASC") + assertEquals( + listOf( + listOf(int(1), int(400)), listOf(int(2), int(800)), listOf(int(3), int(1100)), + listOf(int(4), int(1300)), listOf(int(5), int(1500)), listOf(int(6), int(1600)) + ), + result.rows + ) + } +