Skip to content

Commit 56147f7

Browse files
committed
feat: support MERGE ... WHEN NOT MATCHED BY TARGET / BY SOURCE
BigQuery and SQL Server allow the MERGE NOT MATCHED clause to be qualified with BY TARGET (allows INSERT) or BY SOURCE (allows UPDATE/DELETE), e.g.: MERGE INTO t USING s ON ... WHEN NOT MATCHED BY TARGET THEN INSERT (...) WHEN NOT MATCHED BY SOURCE THEN DELETE Previously JSQLParser rejected the BY TARGET/BY SOURCE qualifier as an UnsupportedStatement (#2421). Add an optional [BY TARGET|SOURCE] to the WHEN NOT MATCHED production, allow INSERT/update/delete on that branch, and store the side on the MergeInsert/MergeUpdate/MergeDelete operations via a new MergeSide enum. Standard WHEN [NOT] MATCHED (no BY) keeps working unchanged. TARGET/SOURCE are matched as identifiers (not reserved keywords), so existing SQL using them as column/table names is unaffected. Fixes #2421 Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 18b12f1 commit 56147f7

7 files changed

Lines changed: 162 additions & 19 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/merge/MergeDelete.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
public class MergeDelete implements Serializable, MergeOperation {
1717
private Expression andPredicate;
18+
private MergeSide side;
1819

1920
public Expression getAndPredicate() {
2021
return andPredicate;
@@ -24,11 +25,25 @@ public void setAndPredicate(Expression andPredicate) {
2425
this.andPredicate = andPredicate;
2526
}
2627

28+
public MergeSide getSide() {
29+
return side;
30+
}
31+
32+
public MergeDelete setSide(MergeSide side) {
33+
this.side = side;
34+
return this;
35+
}
36+
2737
public MergeDelete withAndPredicate(Expression andPredicate) {
2838
this.setAndPredicate(andPredicate);
2939
return this;
3040
}
3141

42+
public MergeDelete withSide(MergeSide side) {
43+
this.setSide(side);
44+
return this;
45+
}
46+
3247
@Override
3348
public <S, T> T accept(MergeOperationVisitor<T> mergeOperationVisitor, S context) {
3449
return mergeOperationVisitor.visit(this, context);
@@ -37,7 +52,7 @@ public <S, T> T accept(MergeOperationVisitor<T> mergeOperationVisitor, S context
3752
@Override
3853
public String toString() {
3954
StringBuilder b = new StringBuilder();
40-
b.append(" WHEN MATCHED");
55+
b.append(side == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED");
4156
if (andPredicate != null) {
4257
b.append(" AND ").append(andPredicate.toString());
4358
}

src/main/java/net/sf/jsqlparser/statement/merge/MergeInsert.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public class MergeInsert implements Serializable, MergeOperation {
2424
private ExpressionList<Column> columns;
2525
private ExpressionList<Expression> values;
2626
private Expression whereCondition;
27+
private MergeSide side;
28+
29+
public MergeSide getSide() {
30+
return side;
31+
}
32+
33+
public void setSide(MergeSide side) {
34+
this.side = side;
35+
}
2736

2837
public Expression getAndPredicate() {
2938
return andPredicate;
@@ -66,6 +75,9 @@ public <S, T> T accept(MergeOperationVisitor<T> mergeOperationVisitor, S context
6675
public String toString() {
6776
StringBuilder b = new StringBuilder();
6877
b.append(" WHEN NOT MATCHED");
78+
if (side == MergeSide.TARGET) {
79+
b.append(" BY TARGET");
80+
}
6981
if (andPredicate != null) {
7082
b.append(" AND ").append(andPredicate);
7183
}
@@ -122,6 +134,11 @@ public MergeInsert withWhereCondition(Expression whereCondition) {
122134
return this;
123135
}
124136

137+
public MergeInsert withSide(MergeSide side) {
138+
this.setSide(side);
139+
return this;
140+
}
141+
125142
public <E extends Expression> E getAndPredicate(Class<E> type) {
126143
return type.cast(getAndPredicate());
127144
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2024 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.merge;
11+
12+
/**
13+
* Identifies the side of a {@code MERGE ... WHEN NOT MATCHED [BY TARGET|BY SOURCE]} clause.
14+
*
15+
* <p>Standard SQL only knows {@code WHEN [NOT] MATCHED}; SQL Server and BigQuery additionally
16+
* allow the NOT MATCHED clause to be qualified with {@code BY TARGET} (the default, allows
17+
* {@code INSERT}) or {@code BY SOURCE} (allows {@code UPDATE}/{@code DELETE}).</p>
18+
*/
19+
public enum MergeSide {
20+
TARGET,
21+
SOURCE;
22+
23+
/**
24+
* Parses a {@code BY TARGET}/{@code BY SOURCE} qualifier, case-insensitively.
25+
*
26+
* @param image the raw identifier image following {@code BY}
27+
* @return the matching {@link MergeSide}
28+
* @throws IllegalArgumentException if {@code image} is neither {@code TARGET} nor {@code SOURCE}
29+
*/
30+
public static MergeSide fromImage(String image) {
31+
for (MergeSide value : values()) {
32+
if (value.name().equalsIgnoreCase(image)) {
33+
return value;
34+
}
35+
}
36+
throw new IllegalArgumentException("Expected TARGET or SOURCE after BY but found: " + image);
37+
}
38+
}

src/main/java/net/sf/jsqlparser/statement/merge/MergeUpdate.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class MergeUpdate implements Serializable, MergeOperation {
2121
private Expression andPredicate;
2222
private Expression whereCondition;
2323
private Expression deleteWhereCondition;
24+
private MergeSide side;
2425

2526
public MergeUpdate() {}
2627

@@ -61,6 +62,15 @@ public void setDeleteWhereCondition(Expression deleteWhereCondition) {
6162
this.deleteWhereCondition = deleteWhereCondition;
6263
}
6364

65+
public MergeSide getSide() {
66+
return side;
67+
}
68+
69+
public MergeUpdate setSide(MergeSide side) {
70+
this.side = side;
71+
return this;
72+
}
73+
6474
@Override
6575
public <S, T> T accept(MergeOperationVisitor<T> mergeOperationVisitor, S context) {
6676
return mergeOperationVisitor.visit(this, context);
@@ -69,7 +79,7 @@ public <S, T> T accept(MergeOperationVisitor<T> mergeOperationVisitor, S context
6979
@Override
7080
public String toString() {
7181
StringBuilder b = new StringBuilder();
72-
b.append(" WHEN MATCHED");
82+
b.append(side == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED");
7383
if (andPredicate != null) {
7484
b.append(" AND ").append(andPredicate.toString());
7585
}
@@ -100,6 +110,11 @@ public MergeUpdate withDeleteWhereCondition(Expression deleteWhereCondition) {
100110
return this;
101111
}
102112

113+
public MergeUpdate withSide(MergeSide side) {
114+
this.setSide(side);
115+
return this;
116+
}
117+
103118
public <E extends Expression> E getAndPredicate(Class<E> type) {
104119
return type.cast(getAndPredicate());
105120
}

src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public void deParse(Merge merge) {
6767

6868
@Override
6969
public <S> StringBuilder visit(MergeDelete mergeDelete, S context) {
70-
builder.append(" WHEN MATCHED");
70+
builder.append(mergeDelete.getSide() == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED");
7171
if (mergeDelete.getAndPredicate() != null) {
7272
builder.append(" AND ");
7373
mergeDelete.getAndPredicate().accept(expressionDeParser, context);
@@ -82,7 +82,7 @@ public void visit(MergeDelete mergeDelete) {
8282

8383
@Override
8484
public <S> StringBuilder visit(MergeUpdate mergeUpdate, S context) {
85-
builder.append(" WHEN MATCHED");
85+
builder.append(mergeUpdate.getSide() == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED");
8686
if (mergeUpdate.getAndPredicate() != null) {
8787
builder.append(" AND ");
8888
mergeUpdate.getAndPredicate().accept(expressionDeParser, context);
@@ -110,6 +110,9 @@ public void visit(MergeUpdate mergeUpdate) {
110110
@Override
111111
public <S> StringBuilder visit(MergeInsert mergeInsert, S context) {
112112
builder.append(" WHEN NOT MATCHED");
113+
if (mergeInsert.getSide() == MergeSide.TARGET) {
114+
builder.append(" BY TARGET");
115+
}
113116
if (mergeInsert.getAndPredicate() != null) {
114117
builder.append(" AND ");
115118
mergeInsert.getAndPredicate().accept(expressionDeParser, context);

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4225,31 +4225,53 @@ MergeOperation MergeUpdateClause(Expression predicate) : {
42254225
{ return mu; }
42264226
}
42274227

4228-
MergeOperation MergeWhenNotMatched() : {
4228+
MergeInsert MergeInsertClause(Expression predicate) : {
42294229
MergeInsert mi = new MergeInsert();
4230-
Expression predicate;
42314230
ExpressionList<Column> columns;
42324231
ExpressionList expList;
42334232
Expression condition;
42344233
}
42354234
{
4236-
<K_WHEN> <K_NOT> <K_MATCHED>
4237-
[ <K_AND> predicate = Expression() { mi.setAndPredicate(predicate); } ]
4238-
<K_THEN>
4239-
<K_INSERT>
4240-
[ "(" columns = ColumnList() ")"
4241-
{
4242-
mi.setColumns( new ParenthesedExpressionList<Column>(columns) );
4243-
}
4244-
]
4245-
<K_VALUES> "(" expList = SimpleExpressionList() ")"
4235+
<K_INSERT>
4236+
[ "(" columns = ColumnList() ")"
42464237
{
4247-
mi.setValues( new ParenthesedExpressionList(expList) );
4238+
mi.setColumns( new ParenthesedExpressionList<Column>(columns) );
42484239
}
4240+
]
4241+
<K_VALUES> "(" expList = SimpleExpressionList() ")"
4242+
{
4243+
mi.setValues( new ParenthesedExpressionList(expList) );
4244+
}
4245+
4246+
[ <K_WHERE> condition = Expression() { mi.setWhereCondition(condition); } ]
42494247

4250-
[ <K_WHERE> condition = Expression() { mi.setWhereCondition(condition); }]
4248+
{
4249+
mi.setAndPredicate(predicate);
4250+
return mi;
4251+
}
4252+
}
42514253

4252-
{ return mi; }
4254+
MergeOperation MergeWhenNotMatched() : {
4255+
MergeOperation operation;
4256+
Expression predicate = null;
4257+
MergeSide side = null;
4258+
Token sideToken;
4259+
}
4260+
{
4261+
<K_WHEN> <K_NOT> <K_MATCHED>
4262+
[ <K_BY> sideToken = <S_IDENTIFIER> {
4263+
side = MergeSide.fromImage(sideToken.image);
4264+
} ]
4265+
[ <K_AND> predicate = Expression() ]
4266+
<K_THEN>
4267+
(
4268+
operation = MergeInsertClause(predicate) { ((MergeInsert) operation).setSide(side); }
4269+
|
4270+
operation = MergeUpdateClause(predicate) { ((MergeUpdate) operation).setSide(side); }
4271+
|
4272+
operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); }
4273+
)
4274+
{ return operation; }
42534275
}
42544276

42554277
// 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
@@ -272,6 +272,39 @@ void testSnowflakeMergeStatementWithManyWhensAndDelete() throws JSQLParserExcept
272272
assertSqlCanBeParsedAndDeparsed(sql, true);
273273
}
274274

275+
@Test
276+
void testBigQueryMergeNotMatchedByTargetAndSource() throws JSQLParserException {
277+
// BigQuery/SQL Server allow "WHEN NOT MATCHED [BY TARGET|BY SOURCE]" (issue #2421)
278+
String sql = "MERGE INTO target_table AS tt USING (SELECT key, field FROM source_table) AS st"
279+
+ " ON tt.key = st.key"
280+
+ " WHEN NOT MATCHED BY TARGET THEN INSERT (key, field) VALUES (st.key, st.field)"
281+
+ " WHEN NOT MATCHED BY SOURCE THEN DELETE";
282+
283+
assertSqlCanBeParsedAndDeparsed(sql, true);
284+
285+
Merge merge = (Merge) CCJSqlParserUtil.parse(sql);
286+
assertThat(merge.getOperations()).hasSize(2);
287+
assertThat(merge.getOperations().get(0)).isInstanceOf(MergeInsert.class);
288+
assertThat(((MergeInsert) merge.getOperations().get(0)).getSide()).isEqualTo(MergeSide.TARGET);
289+
assertThat(merge.getOperations().get(1)).isInstanceOf(MergeDelete.class);
290+
assertThat(((MergeDelete) merge.getOperations().get(1)).getSide()).isEqualTo(MergeSide.SOURCE);
291+
}
292+
293+
@Test
294+
void testBigQueryMergeNotMatchedBySourceUpdate() throws JSQLParserException {
295+
// BY SOURCE allows UPDATE/DELETE with an AND search condition
296+
String sql = "MERGE INTO t USING s ON t.id = s.id"
297+
+ " WHEN NOT MATCHED BY SOURCE AND s.id IS NULL THEN UPDATE SET t.v = 0";
298+
299+
assertSqlCanBeParsedAndDeparsed(sql, true);
300+
301+
Merge merge = (Merge) CCJSqlParserUtil.parse(sql);
302+
assertThat(merge.getOperations()).hasSize(1);
303+
assertThat(merge.getOperations().get(0)).isInstanceOf(MergeUpdate.class);
304+
assertThat(((MergeUpdate) merge.getOperations().get(0)).getSide()).isEqualTo(MergeSide.SOURCE);
305+
assertThat(((MergeUpdate) merge.getOperations().get(0)).getAndPredicate()).isNotNull();
306+
}
307+
275308
@ParameterizedTest
276309
@MethodSource("deriveOperationsFromStandardClausesCases")
277310
void testDeriveOperationsFromStandardClauses(List<MergeOperation> expectedOperations,

0 commit comments

Comments
 (0)