Skip to content

Commit 4c6a4fb

Browse files
authored
feat: complete SQL Server OPTION query hint support (#2486)
Attach OPTION to MERGE statements and validate and deparse its parameters. Parse OPTIMIZE FOR (@variable UNKNOWN) as a table-less Column, reusing the existing expression visitor contract. Reject OPTION after standalone VALUES, including INSERT ... VALUES, because SQL Server allows query hints on INSERT only in a nested SELECT. Preserve exact spacing when MERGE combines OUTPUT and OPTION. Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 55b2363 commit 4c6a4fb

6 files changed

Lines changed: 125 additions & 5 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import net.sf.jsqlparser.statement.Statement;
1818
import net.sf.jsqlparser.statement.StatementVisitor;
1919
import net.sf.jsqlparser.statement.select.FromItem;
20+
import net.sf.jsqlparser.statement.select.OptionClause;
2021
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
2122
import net.sf.jsqlparser.statement.select.WithItem;
2223

@@ -41,6 +42,7 @@ public class Merge implements Statement {
4142
private List<MergeOperation> operations;
4243

4344
private OutputClause outputClause;
45+
private OptionClause option;
4446

4547
private void deriveOperationsFromStandardClauses() {
4648
List<MergeOperation> operations = new ArrayList<>();
@@ -214,6 +216,19 @@ public Merge setOutputClause(OutputClause outputClause) {
214216
return this;
215217
}
216218

219+
public OptionClause getOption() {
220+
return option;
221+
}
222+
223+
public Merge setOption(OptionClause option) {
224+
this.option = option;
225+
return this;
226+
}
227+
228+
public Merge withOption(OptionClause option) {
229+
return setOption(option);
230+
}
231+
217232
@Override
218233
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
219234
public String toString() {
@@ -248,6 +263,13 @@ public String toString() {
248263
b.append(outputClause);
249264
}
250265

266+
if (option != null) {
267+
if (outputClause != null) {
268+
b.setLength(b.length() - 1);
269+
}
270+
b.append(option);
271+
}
272+
251273
return b.toString();
252274
}
253275

src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515

1616
/**
1717
* Models the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, which attaches a list of
18-
* {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE} or {@code DELETE} statement,
19-
* see <a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query">Hints
18+
* {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE}, {@code DELETE} or
19+
* {@code MERGE} statement, see
20+
* <a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query">Hints
2021
* (Transact-SQL) - Query Hints</a>.
2122
*/
2223
public class OptionClause implements Serializable {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ public void deParse(Merge merge) {
6363
if (merge.getOutputClause() != null) {
6464
merge.getOutputClause().appendTo(builder);
6565
}
66+
67+
if (merge.getOption() != null) {
68+
if (merge.getOutputClause() != null) {
69+
builder.setLength(builder.length() - 1);
70+
}
71+
builder.append(merge.getOption());
72+
}
6673
}
6774

6875
@Override

src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import net.sf.jsqlparser.parser.feature.Feature;
1313
import net.sf.jsqlparser.statement.merge.*;
14+
import net.sf.jsqlparser.statement.select.OptionHint;
1415
import net.sf.jsqlparser.statement.update.UpdateSet;
1516
import net.sf.jsqlparser.util.validation.ValidationCapability;
1617

@@ -31,6 +32,14 @@ public void validate(Merge merge) {
3132
merge.getOperations().forEach(operation -> operation.accept(this, null));
3233
}
3334
validateOptionalFromItems(merge.getFromItem());
35+
if (merge.getOption() != null) {
36+
for (OptionHint optionHint : merge.getOption().getOptionHints()) {
37+
validateOptionalExpression(optionHint.getValue());
38+
if (optionHint.getParameters() != null) {
39+
optionHint.getParameters().forEach(this::validateOptionalExpression);
40+
}
41+
}
42+
}
3443
}
3544

3645
@Override

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4388,6 +4388,7 @@ Statement Merge( List<WithItem<?>> with ) : {
43884388
Expression condition;
43894389
List<MergeOperation> operations;
43904390
OutputClause outputClause;
4391+
OptionClause optionClause = null;
43914392
}
43924393
{
43934394
<K_MERGE> { merge.setOracleHint(getOracleHint()); } <K_INTO> table=TableWithAlias() { merge.setTable(table); }
@@ -4398,6 +4399,8 @@ Statement Merge( List<WithItem<?>> with ) : {
43984399

43994400
[ outputClause = OutputClause() { merge.setOutputClause(outputClause); } ]
44004401

4402+
[ LOOKAHEAD(2) optionClause = OptionClause() { merge.setOption(optionClause); } ]
4403+
44014404
{ return merge.withWithItemsList(with); }
44024405
}
44034406

@@ -4803,7 +4806,7 @@ Select Select() #Select:
48034806

48044807
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ]
48054808
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ]
4806-
[ LOOKAHEAD(2) optionClause = OptionClause() { select.setOption(optionClause); } ]
4809+
[ LOOKAHEAD(2, { !(select instanceof Values) }) optionClause = OptionClause() { select.setOption(optionClause); } ]
48074810

48084811
[ LOOKAHEAD(<K_LIMIT>) limit=LimitWithOffset() {select.setLimit(limit);} ]
48094812
[ LOOKAHEAD(<K_OFFSET>) offset = Offset() { select.setOffset(offset);} ]
@@ -6952,6 +6955,23 @@ InterpolateElement InterpolateElement():
69526955
{ return interpolateElement; }
69536956
}
69546957

6958+
Expression OptionHintParameter():
6959+
{
6960+
UserVariable userVariable = null;
6961+
Expression parameter = null;
6962+
}
6963+
{
6964+
(
6965+
LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && getToken(2).kind == K_UNKNOWN })
6966+
userVariable = UserVariable() <K_UNKNOWN>
6967+
{
6968+
return new Column(userVariable + " UNKNOWN");
6969+
}
6970+
|
6971+
parameter = Expression() { return parameter; }
6972+
)
6973+
}
6974+
69556975
OptionClause OptionClause():
69566976
{
69576977
OptionClause optionClause = new OptionClause();
@@ -6977,8 +6997,8 @@ OptionHint OptionHint():
69776997
(
69786998
LOOKAHEAD("=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); }
69796999
|
6980-
LOOKAHEAD("(") "(" parameter = Expression() { optionHint.addParameter(parameter); }
6981-
( "," parameter = Expression() { optionHint.addParameter(parameter); } )*
7000+
LOOKAHEAD("(") "(" parameter = OptionHintParameter() { optionHint.addParameter(parameter); }
7001+
( "," parameter = OptionHintParameter() { optionHint.addParameter(parameter); } )*
69827002
")"
69837003
|
69847004
LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); }

src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
import net.sf.jsqlparser.JSQLParserException;
1515
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.schema.Column;
1617
import net.sf.jsqlparser.statement.Statement;
18+
import net.sf.jsqlparser.statement.insert.Insert;
19+
import net.sf.jsqlparser.statement.merge.Merge;
1720
import org.junit.jupiter.api.Assertions;
1821
import org.junit.jupiter.api.Test;
1922

@@ -147,6 +150,64 @@ public void testOptionInUpdateAndDelete() throws JSQLParserException {
147150
Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption());
148151
}
149152

153+
@Test
154+
public void testOptionOptimizeForUnknownParameter() throws JSQLParserException {
155+
String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p UNKNOWN))";
156+
PlainSelect plainSelect =
157+
(PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true);
158+
OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0);
159+
Assertions.assertEquals(1, optimizeFor.getParameters().size());
160+
Column parameter =
161+
Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(0));
162+
Assertions.assertEquals("@p UNKNOWN", parameter.getColumnName());
163+
}
164+
165+
@Test
166+
public void testOptionOptimizeForMixedParameters() throws JSQLParserException {
167+
String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1, @q UNKNOWN))";
168+
PlainSelect plainSelect =
169+
(PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true);
170+
OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0);
171+
Assertions.assertEquals(2, optimizeFor.getParameters().size());
172+
Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString());
173+
Column parameter =
174+
Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(1));
175+
Assertions.assertEquals("@q UNKNOWN", parameter.getColumnName());
176+
}
177+
178+
@Test
179+
public void testOptionAfterInsertValuesIsRejected() {
180+
Assertions.assertThrows(JSQLParserException.class,
181+
() -> CCJSqlParserUtil.parse(
182+
"INSERT INTO t (a) VALUES (1) OPTION (RECOMPILE)"));
183+
}
184+
185+
@Test
186+
public void testOptionAfterInsertSelect() throws JSQLParserException {
187+
String sql = "INSERT INTO t (a) SELECT a FROM s OPTION (RECOMPILE)";
188+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true);
189+
Insert insert = (Insert) statement;
190+
Assertions.assertNotNull(insert.getSelect().getOption());
191+
}
192+
193+
@Test
194+
public void testOptionAfterMergeStatement() throws JSQLParserException {
195+
String sql =
196+
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a OPTION (HASH JOIN)";
197+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true);
198+
Merge merge = (Merge) statement;
199+
Assertions.assertNotNull(merge.getOption());
200+
Assertions.assertEquals("HASH JOIN", merge.getOption().getOptionHints().get(0).getName());
201+
}
202+
203+
@Test
204+
public void testOptionAfterMergeOutputClause() throws JSQLParserException {
205+
assertSqlCanBeParsedAndDeparsed(
206+
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a "
207+
+ "OUTPUT deleted.a OPTION (HASH JOIN)",
208+
false);
209+
}
210+
150211
@Test
151212
public void testOptionAsIdentifierStillWorks() throws JSQLParserException {
152213
// OPTION stays a non-reserved keyword usable as column and table name

0 commit comments

Comments
 (0)