diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index f4c55de..2228199 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -765,4 +765,177 @@ class ParserTest { assertEquals(false, expression.negated) } + @Test + fun parsesNotInSubquery() { + val expression = whereOf("SELECT 1 FROM t WHERE id NOT IN (SELECT id FROM other)") as InSubqueryExpression + assertEquals(true, expression.negated) + } + + @Test + fun parsesExistsAndNotExists() { + val exists = whereOf("SELECT 1 FROM t WHERE EXISTS (SELECT 1 FROM other)") as ExistsExpression + assertEquals("other", tableName(exists.select)) + val notExists = whereOf("SELECT 1 FROM t WHERE NOT EXISTS (SELECT 1 FROM other)") as UnaryExpression + assertEquals(UnaryOperator.Not, notExists.operator) + assertEquals("other", tableName((notExists.operand as ExistsExpression).select)) + } + + @Test + fun parenthesizedNonSelectExpressionIsStillJustGrouping() { + assertEquals(intLit(1), firstProjection("SELECT (1) FROM t")) + } + + @Test + fun parsesCreateView() { + val statement = parseOne("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 18") as CreateViewStatement + assertEquals("adults", statement.viewName) + assertEquals(false, statement.orReplace) + assertEquals("users", tableName(statement.query)) + } + + @Test + fun parsesCreateOrReplaceView() { + val statement = parseOne("CREATE OR REPLACE VIEW adults AS SELECT id FROM users") as CreateViewStatement + assertEquals(true, statement.orReplace) + } + + @Test + fun orReplaceOnNonViewFails() { + assertFailsWith { parseOne("CREATE OR REPLACE TABLE t (id INTEGER)") } + } + + @Test + fun parsesDropView() { + val statement = parseOne("DROP VIEW adults") as DropViewStatement + assertEquals("adults", statement.viewName) + assertEquals(DropMode.Restrict, statement.mode) + } + + @Test + fun dropTableAndDropViewAcceptExplicitCascadeAndRestrict() { + assertEquals(DropMode.Cascade, (parseOne("DROP TABLE t CASCADE") as DropTableStatement).mode) + assertEquals(DropMode.Restrict, (parseOne("DROP TABLE t RESTRICT") as DropTableStatement).mode) + assertEquals(DropMode.Cascade, (parseOne("DROP VIEW v CASCADE") as DropViewStatement).mode) + assertEquals(DropMode.Restrict, (parseOne("DROP VIEW v RESTRICT") as DropViewStatement).mode) + } + + @Test + fun parsesTruncateWithAndWithoutTheTableKeyword() { + val withKeyword = parseOne("TRUNCATE TABLE t") as TruncateTableStatement + assertEquals(listOf("t"), withKeyword.tableNames) + assertEquals(false, withKeyword.restartIdentity) + assertEquals(DropMode.Restrict, withKeyword.mode) + + val withoutKeyword = parseOne("TRUNCATE t") as TruncateTableStatement + assertEquals(listOf("t"), withoutKeyword.tableNames) + } + + @Test + fun parsesTruncateWithMultipleTableNames() { + val statement = parseOne("TRUNCATE TABLE a, b, c") as TruncateTableStatement + assertEquals(listOf("a", "b", "c"), statement.tableNames) + } + + @Test + fun parsesTruncateRestartIdentityAndContinueIdentity() { + assertEquals(true, (parseOne("TRUNCATE TABLE t RESTART IDENTITY") as TruncateTableStatement).restartIdentity) + assertEquals(false, (parseOne("TRUNCATE TABLE t CONTINUE IDENTITY") as TruncateTableStatement).restartIdentity) + assertEquals(false, (parseOne("TRUNCATE TABLE t") as TruncateTableStatement).restartIdentity) + } + + @Test + fun parsesTruncateCascadeAndRestrictAfterAnIdentityClause() { + val statement = parseOne("TRUNCATE TABLE t RESTART IDENTITY CASCADE") as TruncateTableStatement + assertEquals(true, statement.restartIdentity) + assertEquals(DropMode.Cascade, statement.mode) + } + + @Test + fun parsesMergeWithATableSourceAndAnUpdateClause() { + val statement = parseOne( + "MERGE INTO accounts USING staging ON accounts.id = staging.id " + + "WHEN MATCHED THEN UPDATE SET balance = staging.balance" + ) as MergeStatement + assertEquals("accounts", statement.targetTable) + assertNull(statement.targetAlias) + val source = statement.source as TableSource + assertEquals("staging", source.name) + assertNull(source.alias) + assertTrue(statement.onCondition is BinaryExpression) + assertEquals(1, statement.whenClauses.size) + val clause = statement.whenClauses.single() as MergeMatchedUpdate + assertNull(clause.condition) + assertEquals(listOf(Assignment("balance", ColumnExpression("balance", "staging"))), clause.assignments) + } + + @Test + fun parsesMergeTargetAndSourceAliasesWithAndWithoutAs() { + val explicitAs = parseOne( + "MERGE INTO accounts AS acc USING staging AS s ON acc.id = s.id WHEN MATCHED THEN DELETE" + ) as MergeStatement + assertEquals("acc", explicitAs.targetAlias) + assertEquals("s", (explicitAs.source as TableSource).alias) + + val bareAlias = parseOne( + "MERGE INTO accounts acc USING staging s ON acc.id = s.id WHEN MATCHED THEN DELETE" + ) as MergeStatement + assertEquals("acc", bareAlias.targetAlias) + assertEquals("s", (bareAlias.source as TableSource).alias) + } + + @Test + fun parsesMergeUsingADerivedTableSubquerySource() { + val statement = parseOne( + "MERGE INTO accounts USING (SELECT id FROM staging) AS s ON accounts.id = s.id WHEN MATCHED THEN DELETE" + ) as MergeStatement + val source = statement.source as SubquerySource + assertEquals("s", source.alias) + assertTrue(source.select is SelectStatement) + } + + @Test + fun parsesMergeWhenMatchedDeleteWithAnAndCondition() { + val statement = parseOne( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED AND s.flag THEN DELETE" + ) as MergeStatement + val clause = statement.whenClauses.single() as MergeMatchedDelete + assertEquals(ColumnExpression("flag", "s"), clause.condition) + } + + @Test + fun parsesMergeWhenNotMatchedInsertWithAndWithoutAColumnList() { + val withColumns = parseOne( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (id, label) VALUES (s.id, s.label)" + ) as MergeStatement + val withColumnsClause = withColumns.whenClauses.single() as MergeNotMatchedInsert + assertEquals(listOf("id", "label"), withColumnsClause.columns) + assertEquals(listOf(ColumnExpression("id", "s"), ColumnExpression("label", "s")), withColumnsClause.values) + + val withoutColumns = parseOne( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT VALUES (s.id, DEFAULT)" + ) as MergeStatement + val withoutColumnsClause = withoutColumns.whenClauses.single() as MergeNotMatchedInsert + assertNull(withoutColumnsClause.columns) + assertEquals(listOf(ColumnExpression("id", "s"), null), withoutColumnsClause.values) + } + + @Test + fun parsesMergeWhenNotMatchedWithAnAndCondition() { + val statement = parseOne( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED AND s.active THEN INSERT (id) VALUES (s.id)" + ) as MergeStatement + val clause = statement.whenClauses.single() as MergeNotMatchedInsert + assertEquals(ColumnExpression("active", "s"), clause.condition) + } + + @Test + fun parsesMergeWithMultipleWhenClausesInWrittenOrder() { + val statement = parseOne( + "MERGE INTO t USING s ON t.id = s.id " + + "WHEN MATCHED AND s.gone THEN DELETE " + + "WHEN MATCHED THEN UPDATE SET v = s.v " + + "WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)" + ) as MergeStatement + assertEquals(3, statement.whenClauses.size) + assertTrue(statement.whenClauses[0] is MergeMatchedDelete) }