diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java index d871021f7..6dc7815fb 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java @@ -29,6 +29,29 @@ public enum MergeSide { * @throws IllegalArgumentException if {@code image} is neither {@code TARGET} nor * {@code SOURCE} */ + /** + * Validates the clause pairing of a {@code WHEN NOT MATCHED} branch: {@code BY TARGET} (the + * default) only allows an {@code INSERT} clause, {@code BY SOURCE} only allows {@code UPDATE} + * or {@code DELETE}. + * + * @param side the parsed {@code BY TARGET}/{@code BY SOURCE} qualifier, null when absent + * @param operation the parsed clause + * @return the passed {@code operation} + * @throws IllegalArgumentException when the pairing is not legal in any dialect + */ + public static MergeOperation validatePairing(MergeSide side, MergeOperation operation) { + if (side == MergeSide.SOURCE) { + if (operation instanceof MergeInsert) { + throw new IllegalArgumentException( + "WHEN NOT MATCHED BY SOURCE cannot take an INSERT clause"); + } + } else if (!(operation instanceof MergeInsert)) { + throw new IllegalArgumentException( + "WHEN NOT MATCHED [BY TARGET] cannot take an UPDATE or DELETE clause"); + } + return operation; + } + public static MergeSide fromImage(String image) { for (MergeSide value : values()) { if (value.name().equalsIgnoreCase(image)) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d86e227d7..9d0314204 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4489,7 +4489,7 @@ MergeOperation MergeWhenNotMatched() : { | operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); } ) - { return operation; } + { return MergeSide.validatePairing(side, operation); } } // table names seem to allow ":" delimiters, e.g. for Informix see #1134 diff --git a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java index 27c8c4da0..87d1d1448 100644 --- a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java @@ -25,6 +25,7 @@ import static net.sf.jsqlparser.test.TestUtils.assertOracleHintExists; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; /** @@ -363,4 +364,36 @@ private static Stream deriveStandardClausesFromOperationsCases() { Arguments.of(Arrays.asList(insert1, insert2, update1, update2, delete1), update1, insert1, true)); } + + @Test + void testMergeRejectsInvalidWhenNotMatchedClausePairings() { + // WHEN MATCHED rows exist in the target: INSERT is not legal + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN INSERT (a) VALUES (1)")); + + // WHEN NOT MATCHED [BY TARGET] rows do not exist in the target: only INSERT is legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DELETE"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN DELETE"); + + // WHEN NOT MATCHED BY SOURCE rows exist in the target: INSERT is not legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN INSERT (a) VALUES (1)"); + } + + private static void assertInvalidMergePairing(String sql) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql)); + Throwable cause = exception; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + assertThat(cause).isInstanceOf(IllegalArgumentException.class); + assertThat(cause.getMessage()).contains("WHEN NOT MATCHED"); + } }