From b5f8b447284e2077c640fe2b43837b6697a5567f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 14:56:57 +0200 Subject: [PATCH] test: cover dumpAfterAlterTableAddColumnReflectsTheNewShape --- src/test/kotlin/kestrel/SqlWriterTest.kt | 159 +++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/test/kotlin/kestrel/SqlWriterTest.kt b/src/test/kotlin/kestrel/SqlWriterTest.kt index 5eb5290..8cc228e 100644 --- a/src/test/kotlin/kestrel/SqlWriterTest.kt +++ b/src/test/kotlin/kestrel/SqlWriterTest.kt @@ -643,3 +643,162 @@ class SqlWriterTest { // (not just the value 10 already sitting in the dump's INSERT statement). freshExecutor.execute(Parser(Lexer("INSERT INTO t (a) VALUES (7)").tokenize()).parseProgram().single()) + val result = freshExecutor.execute( + Parser(Lexer("SELECT b FROM t ORDER BY a ASC").tokenize()).parseProgram().single() + ) as SelectResult + assertEquals(listOf(listOf(Value.IntegerValue(10)), listOf(Value.IntegerValue(14))), result.rows) + } + + @Test + fun dumpAfterAlterTableAddColumnReflectsTheNewShape() { + seed() + run("ALTER TABLE t ADD COLUMN status TEXT DEFAULT 'new'") + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("status TEXT DEFAULT 'new'")) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + val result = freshExecutor.execute( + Parser(Lexer("SELECT status FROM t WHERE id = 1").tokenize()).parseProgram().single() + ) as SelectResult + assertEquals(listOf(listOf(text("new"))), result.rows) + } + + @Test + fun dumpAndReplayAfterAlterTableAddColumnGeneratedPreservesTheExpressionAndBackfilledValues() { + run("CREATE TABLE t (a INTEGER)") + run("INSERT INTO t (a) VALUES (5)") + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("GENERATED ALWAYS AS"), dump) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + // A row inserted only AFTER replay proves the GENERATED expression itself round-tripped, + // not just the value backfilled into row a=5 before the dump was taken. + freshExecutor.execute(Parser(Lexer("INSERT INTO t (a) VALUES (7)").tokenize()).parseProgram().single()) + + val result = freshExecutor.execute( + Parser(Lexer("SELECT b FROM t ORDER BY a ASC").tokenize()).parseProgram().single() + ) as SelectResult + assertEquals(listOf(listOf(Value.IntegerValue(10)), listOf(Value.IntegerValue(14))), result.rows) + } + + @Test + fun createCompositeUniqueIndexRoundTripsThroughSqlWriter() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER, b INTEGER)") + writeThenReplay("CREATE UNIQUE INDEX idx_ab ON t (a, b)") + run("INSERT INTO t VALUES (1, 1, 1)") + assertFailsWith { run("INSERT INTO t VALUES (2, 1, 1)") } + run("INSERT INTO t VALUES (3, 1, 2)") + } + + @Test + fun dumpAndReplayPreservesACompositeUniqueIndex() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER, b INTEGER)") + run("CREATE UNIQUE INDEX idx_ab ON t (a, b)") + run("INSERT INTO t VALUES (1, 1, 1)") + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("CREATE UNIQUE INDEX idx_ab ON t (a, b);")) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + + assertFailsWith { + Parser(Lexer("INSERT INTO t VALUES (2, 1, 1)").tokenize()).parseProgram() + .forEach { freshExecutor.execute(it) } + } + } + + @Test + fun createViewOverAWindowFunctionRoundTripsThroughSqlWriter() { + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept TEXT, salary INTEGER)") + run( + "INSERT INTO employees VALUES (1, 'eng', 300), (2, 'eng', 400), (3, 'sales', 150)" + ) + val statement = parseOne( + "CREATE VIEW ranked AS SELECT id, dept, " + + "RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS r FROM employees" + ) + val rendered = SqlWriter.write(statement) + assertTrue(rendered.contains("OVER (PARTITION BY dept ORDER BY salary DESC)"), rendered) + run(rendered) + val result = select("SELECT id, r FROM ranked ORDER BY id ASC") + assertEquals( + listOf( + listOf(Value.IntegerValue(1), Value.IntegerValue(2)), + listOf(Value.IntegerValue(2), Value.IntegerValue(1)), + listOf(Value.IntegerValue(3), Value.IntegerValue(1)) + ), + result.rows + ) + } + + @Test + fun dumpAndReplayOfAWindowFunctionViewPreservesTheOverClause() { + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept TEXT, salary INTEGER)") + run("INSERT INTO employees VALUES (1, 'eng', 300), (2, 'eng', 400)") + run( + "CREATE VIEW totals AS SELECT id, SUM(salary) OVER (PARTITION BY dept) AS deptTotal FROM employees" + ) + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("OVER (PARTITION BY dept)"), dump) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + val result = Parser(Lexer("SELECT deptTotal FROM totals ORDER BY id ASC").tokenize()).parseProgram().single() + .let { freshExecutor.execute(it) } as SelectResult + assertEquals(listOf(listOf(Value.IntegerValue(700)), listOf(Value.IntegerValue(700))), result.rows) + } + + @Test + fun createViewOverLagWithOffsetAndDefaultRoundTripsThroughSqlWriter() { + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept TEXT, salary INTEGER)") + run("INSERT INTO employees VALUES (1, 'eng', 400), (2, 'eng', 300)") + val statement = parseOne( + "CREATE VIEW withPrev AS SELECT id, dept, " + + "LAG(salary, 1, 0) OVER (PARTITION BY dept ORDER BY salary DESC) AS prevSalary FROM employees" + ) + val rendered = SqlWriter.write(statement) + assertTrue(rendered.contains("LAG(salary, 1, 0) OVER (PARTITION BY dept ORDER BY salary DESC)"), rendered) + run(rendered) + val result = select("SELECT id, prevSalary FROM withPrev ORDER BY id ASC") + assertEquals( + listOf( + listOf(Value.IntegerValue(1), Value.IntegerValue(0)), + listOf(Value.IntegerValue(2), Value.IntegerValue(400)) + ), + result.rows + ) + } + + @Test + fun createViewOverAnExplicitRowsFrameRoundTripsThroughSqlWriter() { + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept TEXT, salary INTEGER)") + run("INSERT INTO employees VALUES (1, 'eng', 300), (2, 'eng', 400), (3, 'eng', 100)") + val statement = parseOne( + "CREATE VIEW moving AS SELECT id, dept, " + + "SUM(salary) OVER (PARTITION BY dept ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS s " + + "FROM employees" + ) + val rendered = SqlWriter.write(statement) + assertTrue( + rendered.contains("OVER (PARTITION BY dept ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)"), + rendered + ) + run(rendered) + val result = select("SELECT id, s FROM moving ORDER BY id ASC") + assertEquals( + listOf( + listOf(Value.IntegerValue(1), Value.IntegerValue(300)), + listOf(Value.IntegerValue(2), Value.IntegerValue(700)), + listOf(Value.IntegerValue(3), Value.IntegerValue(500)) + ), + result.rows + ) + } +