diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index 0ccccf2..5840ec9 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -1457,4 +1457,177 @@ class ParserTest { @Test fun frameWithNoExcludeClauseDefaultsToNoOthers() { + val statement = parseOne( + "SELECT SUM(salary) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM users" + ) as SelectStatement + val window = (statement.projection as ExpressionProjection).items.single().expression as WindowExpression + assertEquals(FrameExclude.NoOthers, window.frame!!.exclude) + } + + @Test + fun parsesEveryExcludeMode() { + fun excludeOf(clause: String): FrameExclude { + val statement = parseOne( + "SELECT SUM(salary) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING $clause) FROM users" + ) as SelectStatement + val window = (statement.projection as ExpressionProjection).items.single().expression as WindowExpression + return window.frame!!.exclude + } + assertEquals(FrameExclude.CurrentRow, excludeOf("EXCLUDE CURRENT ROW")) + assertEquals(FrameExclude.Group, excludeOf("EXCLUDE GROUP")) + assertEquals(FrameExclude.Ties, excludeOf("EXCLUDE TIES")) + assertEquals(FrameExclude.NoOthers, excludeOf("EXCLUDE NO OTHERS")) + } + + @Test + fun excludeWithoutAValidModeFailsToParse() { + assertFailsWith { + parseOne( + "SELECT SUM(salary) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE FOO) FROM users" + ) + } + } + + @Test + fun parsesUnionAsASetOperationStatement() { + val statement = parseOne("SELECT id FROM a UNION SELECT id FROM b") as SetOperationStatement + assertEquals(SetOperator.Union, statement.operator) + assertEquals("a", tableName(statement.left)) + assertEquals("b", tableName(statement.right)) + } + + @Test + fun parsesUnionAllIntersectAndExcept() { + assertEquals(SetOperator.UnionAll, (parseOne("SELECT id FROM a UNION ALL SELECT id FROM b") as SetOperationStatement).operator) + assertEquals(SetOperator.Intersect, (parseOne("SELECT id FROM a INTERSECT SELECT id FROM b") as SetOperationStatement).operator) + assertEquals(SetOperator.Except, (parseOne("SELECT id FROM a EXCEPT SELECT id FROM b") as SetOperationStatement).operator) + } + + @Test + fun parsesIntersectAllAndExceptAll() { + assertEquals(SetOperator.IntersectAll, (parseOne("SELECT id FROM a INTERSECT ALL SELECT id FROM b") as SetOperationStatement).operator) + assertEquals(SetOperator.ExceptAll, (parseOne("SELECT id FROM a EXCEPT ALL SELECT id FROM b") as SetOperationStatement).operator) + } + + @Test + fun parsesCreateIndexAndCreateUniqueIndex() { + val plain = parseOne("CREATE INDEX idx ON t (a)") as CreateIndexStatement + assertEquals(false, plain.unique) + assertEquals("idx", plain.indexName) + assertEquals("t", plain.tableName) + assertEquals(listOf("a"), plain.columnNames) + val unique = parseOne("CREATE UNIQUE INDEX idx ON t (a)") as CreateIndexStatement + assertEquals(true, unique.unique) + } + + @Test + fun chainedCompoundOperatorsAreLeftAssociative() { + val statement = parseOne("SELECT id FROM a UNION SELECT id FROM b INTERSECT SELECT id FROM c") as SetOperationStatement + assertEquals(SetOperator.Intersect, statement.operator) + val left = statement.left as SetOperationStatement + assertEquals(SetOperator.Union, left.operator) + assertEquals("a", tableName(left.left)) + assertEquals("b", tableName(left.right)) + assertEquals("c", tableName(statement.right)) + } + + @Test + fun trailingOrderByAndLimitAttachToTheWholeCompoundQuery() { + val statement = parseOne("SELECT id FROM a UNION SELECT id FROM b ORDER BY id DESC LIMIT 3") as SetOperationStatement + val key = statement.orderBy!!.keys.single() + assertEquals("id", (key.expression as ColumnExpression).name) + assertEquals(true, key.descending) + assertEquals(3, statement.limit) + } + + @Test + fun individualSelectCoresInACompoundQueryDoNotOwnOrderByOrLimit() { + val statement = parseOne("SELECT id FROM a UNION SELECT id FROM b") as SetOperationStatement + assertNull((statement.left as SelectStatement).orderBy) + assertNull((statement.left as SelectStatement).limit) + } + + @Test + fun parsesWithClauseAsAWithQuery() { + val statement = parseOne("WITH recent AS (SELECT id FROM a) SELECT id FROM recent") as WithQuery + assertEquals(false, statement.recursive) + val cte = statement.ctes.single() + assertEquals("recent", cte.name) + assertNull(cte.columns) + assertEquals("a", tableName(cte.query)) + assertEquals("recent", tableName(statement.body)) + } + + @Test + fun parsesWithRecursiveAndMultipleCtesWithColumnLists() { + val statement = parseOne( + "WITH RECURSIVE first_cte(x) AS (SELECT id FROM a), second_cte AS (SELECT id FROM b) SELECT x FROM first_cte" + ) as WithQuery + assertEquals(true, statement.recursive) + assertEquals(2, statement.ctes.size) + assertEquals(listOf("x"), statement.ctes[0].columns) + assertNull(statement.ctes[1].columns) + } + + @Test + fun explainAcceptsAUnionQuery() { + val statement = parseOne("EXPLAIN SELECT id FROM a UNION SELECT id FROM b") as ExplainStatement + assertTrue(statement.select is SetOperationStatement) + } + + @Test + fun createViewAcceptsAUnionQuery() { + val statement = parseOne("CREATE VIEW v AS SELECT id FROM a UNION SELECT id FROM b") as CreateViewStatement + assertTrue(statement.query is SetOperationStatement) + } + + @Test + fun derivedTableAcceptsACompoundQuery() { + val statement = parseOne("SELECT * FROM (SELECT id FROM a UNION SELECT id FROM b) AS u") as SelectStatement + val from = statement.from as SubquerySource + assertTrue(from.select is SetOperationStatement) + } + + @Test + fun derivedTableAcceptsAWithQuery() { + val statement = parseOne("SELECT * FROM (WITH x AS (SELECT id FROM a) SELECT id FROM x) AS u") as SelectStatement + val from = statement.from as SubquerySource + assertTrue(from.select is WithQuery) + } + + @Test + fun parsesColumnLevelForeignKeyWithDefaultActions() { + val statement = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))" + ) as CreateTableStatement + val fk = statement.foreignKeys.single() + assertEquals(listOf("dept_id"), fk.columns) + assertEquals("departments", fk.referencedTable) + assertEquals(listOf("id"), fk.referencedColumns) + assertEquals(ForeignKeyAction.NoAction, fk.onDelete) + assertEquals(ForeignKeyAction.NoAction, fk.onUpdate) + } + + @Test + fun parsesForeignKeyActionsInEitherOrder() { + val cascade = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER REFERENCES d(id) ON DELETE CASCADE ON UPDATE RESTRICT)" + ) as CreateTableStatement + val fk1 = cascade.foreignKeys.single() + assertEquals(ForeignKeyAction.Cascade, fk1.onDelete) + assertEquals(ForeignKeyAction.Restrict, fk1.onUpdate) + + val reordered = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER REFERENCES d(id) ON UPDATE SET NULL ON DELETE NO ACTION)" + ) as CreateTableStatement + val fk2 = reordered.foreignKeys.single() + assertEquals(ForeignKeyAction.NoAction, fk2.onDelete) + assertEquals(ForeignKeyAction.SetNull, fk2.onUpdate) + } + + @Test + fun parsesTableLevelForeignKeyConstraint() { + val statement = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, dept_id INTEGER, FOREIGN KEY (dept_id) REFERENCES departments(id))" + ) as CreateTableStatement }