Skip to content

Commit c93fa8a

Browse files
committed
fix: reject illegal MERGE WHEN NOT MATCHED clause pairings (#2480)
WHEN NOT MATCHED accepted any clause regardless of the BY qualifier, and the AST collapsed the illegal pairings into the opposite semantics on deparse: WHEN NOT MATCHED [BY TARGET] THEN UPDATE/DELETE printed as WHEN MATCHED, WHEN NOT MATCHED BY SOURCE THEN INSERT printed as WHEN NOT MATCHED. SQL Server and BigQuery reject these pairings at parse time. Validate the pairing via MergeSide.validatePairing, mirroring the existing MergeSide.fromImage validation. Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent f41c0b8 commit c93fa8a

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ public enum MergeSide {
2929
* @throws IllegalArgumentException if {@code image} is neither {@code TARGET} nor
3030
* {@code SOURCE}
3131
*/
32+
/**
33+
* Validates the clause pairing of a {@code WHEN NOT MATCHED} branch: {@code BY TARGET} (the
34+
* default) only allows an {@code INSERT} clause, {@code BY SOURCE} only allows {@code UPDATE}
35+
* or {@code DELETE}.
36+
*
37+
* @param side the parsed {@code BY TARGET}/{@code BY SOURCE} qualifier, null when absent
38+
* @param operation the parsed clause
39+
* @return the passed {@code operation}
40+
* @throws IllegalArgumentException when the pairing is not legal in any dialect
41+
*/
42+
public static MergeOperation validatePairing(MergeSide side, MergeOperation operation) {
43+
if (side == MergeSide.SOURCE) {
44+
if (operation instanceof MergeInsert) {
45+
throw new IllegalArgumentException(
46+
"WHEN NOT MATCHED BY SOURCE cannot take an INSERT clause");
47+
}
48+
} else if (!(operation instanceof MergeInsert)) {
49+
throw new IllegalArgumentException(
50+
"WHEN NOT MATCHED [BY TARGET] cannot take an UPDATE or DELETE clause");
51+
}
52+
return operation;
53+
}
54+
3255
public static MergeSide fromImage(String image) {
3356
for (MergeSide value : values()) {
3457
if (value.name().equalsIgnoreCase(image)) {

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4489,7 +4489,7 @@ MergeOperation MergeWhenNotMatched() : {
44894489
|
44904490
operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); }
44914491
)
4492-
{ return operation; }
4492+
{ return MergeSide.validatePairing(side, operation); }
44934493
}
44944494

44954495
// table names seem to allow ":" delimiters, e.g. for Informix see #1134

‎src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static net.sf.jsqlparser.test.TestUtils.assertOracleHintExists;
2626
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
2727
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.junit.jupiter.api.Assertions.assertThrows;
2829
import static org.mockito.Mockito.mock;
2930

3031
/**
@@ -363,4 +364,36 @@ private static Stream<Arguments> deriveStandardClausesFromOperationsCases() {
363364
Arguments.of(Arrays.asList(insert1, insert2, update1, update2, delete1), update1,
364365
insert1, true));
365366
}
367+
368+
@Test
369+
void testMergeRejectsInvalidWhenNotMatchedClausePairings() {
370+
// WHEN MATCHED rows exist in the target: INSERT is not legal
371+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
372+
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN INSERT (a) VALUES (1)"));
373+
374+
// WHEN NOT MATCHED [BY TARGET] rows do not exist in the target: only INSERT is legal
375+
assertInvalidMergePairing(
376+
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN UPDATE SET a = 1");
377+
assertInvalidMergePairing(
378+
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DELETE");
379+
assertInvalidMergePairing(
380+
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN UPDATE SET a = 1");
381+
assertInvalidMergePairing(
382+
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN DELETE");
383+
384+
// WHEN NOT MATCHED BY SOURCE rows exist in the target: INSERT is not legal
385+
assertInvalidMergePairing(
386+
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN INSERT (a) VALUES (1)");
387+
}
388+
389+
private static void assertInvalidMergePairing(String sql) {
390+
JSQLParserException exception = assertThrows(JSQLParserException.class,
391+
() -> CCJSqlParserUtil.parse(sql));
392+
Throwable cause = exception;
393+
while (cause.getCause() != null) {
394+
cause = cause.getCause();
395+
}
396+
assertThat(cause).isInstanceOf(IllegalArgumentException.class);
397+
assertThat(cause.getMessage()).contains("WHEN NOT MATCHED");
398+
}
366399
}

0 commit comments

Comments
 (0)