diff --git a/src/test/kotlin/kestrel/SqlWriterTest.kt b/src/test/kotlin/kestrel/SqlWriterTest.kt index f9a3385..97bc3ac 100644 --- a/src/test/kotlin/kestrel/SqlWriterTest.kt +++ b/src/test/kotlin/kestrel/SqlWriterTest.kt @@ -321,3 +321,164 @@ class SqlWriterTest { assertEquals(listOf(listOf(text("eng"), text("ada")), listOf(text("sales"), text("sam"))), result.rows) } + @Test + fun dumpAndReplayOfALateralViewPreservesTheLateralKeyword() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, deptId INTEGER, name TEXT, salary INTEGER)") + run("INSERT INTO departments VALUES (1, 'eng')") + run("INSERT INTO employees VALUES (1, 1, 'ada', 300), (2, 1, 'linus', 250)") + run( + "CREATE VIEW top_earners AS SELECT d.name AS dept, top.name AS emp FROM departments d " + + "JOIN LATERAL (SELECT e.name FROM employees e WHERE e.deptId = d.id " + + "ORDER BY e.salary DESC LIMIT 1) AS top ON TRUE" + ) + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("LATERAL ("), dump) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + val result = Parser(Lexer("SELECT emp FROM top_earners").tokenize()).parseProgram().single() + .let { freshExecutor.execute(it) } as SelectResult + assertEquals(listOf(text("ada")), result.rows.single()) + } + + @Test + fun createViewOverARightFullAndCrossJoinRoundTripsThroughSqlWriterWithCorrectKeywords() { + run("CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT)") + run("CREATE TABLE orders3 (id INTEGER PRIMARY KEY, customerId INTEGER)") + run("INSERT INTO customers VALUES (1, 'ada'), (2, 'grace')") + // customerId 99 matches no customer, and customer 'grace' (id 2) has no order - so a plain + // INNER JOIN would drop both, letting RIGHT/FULL prove they actually kept what INNER cannot. + run("INSERT INTO orders3 VALUES (10, 1), (11, 99)") + + val rightStatement = parseOne("CREATE VIEW r AS SELECT customers.name, orders3.id FROM customers RIGHT JOIN orders3 ON customers.id = orders3.customerId") + val rightRendered = SqlWriter.write(rightStatement) + assertTrue(rightRendered.contains("RIGHT JOIN"), rightRendered) + run(rightRendered) + assertEquals( + setOf(listOf(text("ada"), Value.IntegerValue(10)), listOf(Value.NullValue, Value.IntegerValue(11))), + select("SELECT name, id FROM r").rows.toSet() + ) + + val fullStatement = parseOne("CREATE VIEW f AS SELECT customers.name, orders3.id FROM customers FULL JOIN orders3 ON customers.id = orders3.customerId") + val fullRendered = SqlWriter.write(fullStatement) + assertTrue(fullRendered.contains("FULL JOIN"), fullRendered) + run(fullRendered) + assertEquals(3, select("SELECT name, id FROM f").rows.size) + + val crossStatement = parseOne("CREATE VIEW x AS SELECT customers.name, orders3.id FROM customers CROSS JOIN orders3") + val crossRendered = SqlWriter.write(crossStatement) + assertTrue(crossRendered.contains("CROSS JOIN"), crossRendered) + assertTrue(!crossRendered.contains(" ON "), crossRendered) + run(crossRendered) + assertEquals(4, select("SELECT name, id FROM x").rows.size) + } + + @Test + fun createViewOverAUnionQueryRoundTripsThroughSqlWriter() { + seed() + writeThenReplay( + "CREATE VIEW letters AS SELECT category FROM t WHERE id = 1 UNION SELECT category FROM t WHERE id = 2" + ) + val result = select("SELECT category FROM letters ORDER BY category") + assertEquals(listOf(listOf(text("a")), listOf(text("b"))), result.rows) + } + + @Test + fun createViewOverAnIntersectAllQueryRoundTripsThroughSqlWriter() { + run("CREATE TABLE bag_a (v INTEGER)") + run("CREATE TABLE bag_b (v INTEGER)") + run("INSERT INTO bag_a VALUES (1), (1), (2)") + run("INSERT INTO bag_b VALUES (1), (2), (2)") + writeThenReplay("CREATE VIEW shared AS SELECT v FROM bag_a INTERSECT ALL SELECT v FROM bag_b") + val result = select("SELECT v FROM shared ORDER BY v") + assertEquals(listOf(listOf(Value.IntegerValue(1)), listOf(Value.IntegerValue(2))), result.rows) + } + + @Test + fun createViewOverARecursiveCteRoundTripsThroughSqlWriter() { + run("CREATE TABLE dual (x INTEGER)") + run("INSERT INTO dual VALUES (1)") + writeThenReplay( + "CREATE VIEW counter_view AS WITH RECURSIVE counter(n) AS (" + + "SELECT 1 FROM dual UNION ALL SELECT n + 1 FROM counter WHERE n < 3" + + ") SELECT n FROM counter" + ) + val result = select("SELECT n FROM counter_view ORDER BY n") + assertEquals(listOf(listOf(Value.IntegerValue(1)), listOf(Value.IntegerValue(2)), listOf(Value.IntegerValue(3))), result.rows) + } + + @Test + fun dumpIncludesViewDefinitionsAfterTablesAndIndexes() { + seed() + run("CREATE VIEW letters AS SELECT id, category FROM t WHERE category IS NOT NULL") + val dump = SqlWriter.dump(catalog) + assertTrue(dump.contains("CREATE VIEW letters AS")) + assertTrue(dump.indexOf("CREATE TABLE t") < dump.indexOf("CREATE VIEW letters")) + } + + @Test + fun createTableWithDecimalAndDateColumnsRoundTripsThroughSqlWriter() { + val rendered = SqlWriter.write(parseOne("CREATE TABLE priced (amount DECIMAL(10, 2), born DATE)")) + assertTrue(rendered.contains("DECIMAL(10, 2)")) + assertTrue(rendered.contains("DATE")) + writeThenReplay("CREATE TABLE priced (id INTEGER PRIMARY KEY, amount DECIMAL(10, 2), born DATE)") + run("INSERT INTO priced VALUES (1, 5, CAST('2024-01-01' AS DATE))") + val result = select("SELECT amount FROM priced") + assertEquals(listOf(listOf(Value.DecimalValue(BigDecimal("5.00")))), result.rows) + } + + @Test + fun castExpressionRoundTripsThroughSqlWriter() { + seed() + writeThenReplay("UPDATE t SET category = CAST(id AS TEXT) WHERE id = 1") + assertEquals(listOf(listOf(text("1"))), select("SELECT category FROM t WHERE id = 1").rows) + } + + @Test + fun dropTableAndDropViewCascadeRoundTripThroughSqlWriter() { + seed() + run("CREATE VIEW letters AS SELECT id FROM t") + writeThenReplay("DROP TABLE t CASCADE") + assertTrue(catalog.tableNames.isEmpty()) + assertTrue(catalog.viewNames.isEmpty()) + } + + @Test + fun dumpAndReplayPreservesDecimalAndDateValuesExactly() { + run("CREATE TABLE priced (id INTEGER PRIMARY KEY, amount DECIMAL(12, 4), born DATE)") + run("INSERT INTO priced VALUES (1, CAST('123456.7890' AS DECIMAL(12, 4)), CAST('2024-01-15' AS DATE))") + val dump = SqlWriter.dump(catalog) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + + val result = freshExecutor.execute( + Parser(Lexer("SELECT amount, born FROM priced").tokenize()).parseProgram().single() + ) as SelectResult + assertEquals( + listOf(listOf(Value.DecimalValue(BigDecimal("123456.7890")), Value.DateValue(LocalDate.of(2024, 1, 15)))), + result.rows + ) + } + + @Test + fun createTableWithForeignKeyAndCheckRoundTripsThroughSqlWriter() { + run(SqlWriter.write(parseOne("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)"))) + run( + SqlWriter.write( + parseOne( + "CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER " + + "REFERENCES departments(id) ON DELETE CASCADE, age INTEGER CHECK (age > 0))" + ) + ) + ) + run("INSERT INTO departments VALUES (1, 'eng')") + run("INSERT INTO employees VALUES (1, 1, 30)") + assertFailsWith { run("INSERT INTO employees VALUES (2, 1, -1)") } + run("DELETE FROM departments WHERE id = 1") + assertTrue(select("SELECT * FROM employees").rows.isEmpty()) + } +