diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java index 7a3baf7a4..948a7d648 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java @@ -17,6 +17,7 @@ import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.WithItem; @@ -41,6 +42,7 @@ public class Merge implements Statement { private List operations; private OutputClause outputClause; + private OptionClause option; private void deriveOperationsFromStandardClauses() { List operations = new ArrayList<>(); @@ -214,6 +216,19 @@ public Merge setOutputClause(OutputClause outputClause) { return this; } + public OptionClause getOption() { + return option; + } + + public Merge setOption(OptionClause option) { + this.option = option; + return this; + } + + public Merge withOption(OptionClause option) { + return setOption(option); + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public String toString() { @@ -248,6 +263,13 @@ public String toString() { b.append(outputClause); } + if (option != null) { + if (outputClause != null) { + b.setLength(b.length() - 1); + } + b.append(option); + } + return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java index cb941819c..185166798 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java @@ -15,8 +15,9 @@ /** * Models the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, which attaches a list of - * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE} or {@code DELETE} statement, - * see Hints + * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE}, {@code DELETE} or + * {@code MERGE} statement, see + * Hints * (Transact-SQL) - Query Hints. */ public class OptionClause implements Serializable { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java index 003527770..249f9c134 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java @@ -63,6 +63,13 @@ public void deParse(Merge merge) { if (merge.getOutputClause() != null) { merge.getOutputClause().appendTo(builder); } + + if (merge.getOption() != null) { + if (merge.getOutputClause() != null) { + builder.setLength(builder.length() - 1); + } + builder.append(merge.getOption()); + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java index e82ebc91d..79d84c6c6 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.merge.*; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.update.UpdateSet; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -31,6 +32,14 @@ public void validate(Merge merge) { merge.getOperations().forEach(operation -> operation.accept(this, null)); } validateOptionalFromItems(merge.getFromItem()); + if (merge.getOption() != null) { + for (OptionHint optionHint : merge.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } } @Override diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index da45e5887..f6c400a45 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4388,6 +4388,7 @@ Statement Merge( List> with ) : { Expression condition; List operations; OutputClause outputClause; + OptionClause optionClause = null; } { { merge.setOracleHint(getOracleHint()); } table=TableWithAlias() { merge.setTable(table); } @@ -4398,6 +4399,8 @@ Statement Merge( List> with ) : { [ outputClause = OutputClause() { merge.setOutputClause(outputClause); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { merge.setOption(optionClause); } ] + { return merge.withWithItemsList(with); } } @@ -4803,7 +4806,7 @@ Select Select() #Select: [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] - [ LOOKAHEAD(2) optionClause = OptionClause() { select.setOption(optionClause); } ] + [ LOOKAHEAD(2, { !(select instanceof Values) }) optionClause = OptionClause() { select.setOption(optionClause); } ] [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -6952,6 +6955,23 @@ InterpolateElement InterpolateElement(): { return interpolateElement; } } +Expression OptionHintParameter(): +{ + UserVariable userVariable = null; + Expression parameter = null; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && getToken(2).kind == K_UNKNOWN }) + userVariable = UserVariable() + { + return new Column(userVariable + " UNKNOWN"); + } + | + parameter = Expression() { return parameter; } + ) +} + OptionClause OptionClause(): { OptionClause optionClause = new OptionClause(); @@ -6977,8 +6997,8 @@ OptionHint OptionHint(): ( LOOKAHEAD("=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); } | - LOOKAHEAD("(") "(" parameter = Expression() { optionHint.addParameter(parameter); } - ( "," parameter = Expression() { optionHint.addParameter(parameter); } )* + LOOKAHEAD("(") "(" parameter = OptionHintParameter() { optionHint.addParameter(parameter); } + ( "," parameter = OptionHintParameter() { optionHint.addParameter(parameter); } )* ")" | LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java index bfeba4be2..113d9235f 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -13,7 +13,10 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -147,6 +150,64 @@ public void testOptionInUpdateAndDelete() throws JSQLParserException { Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption()); } + @Test + public void testOptionOptimizeForUnknownParameter() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(0)); + Assertions.assertEquals("@p UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionOptimizeForMixedParameters() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1, @q UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(2, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(1)); + Assertions.assertEquals("@q UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionAfterInsertValuesIsRejected() { + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse( + "INSERT INTO t (a) VALUES (1) OPTION (RECOMPILE)")); + } + + @Test + public void testOptionAfterInsertSelect() throws JSQLParserException { + String sql = "INSERT INTO t (a) SELECT a FROM s OPTION (RECOMPILE)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Insert insert = (Insert) statement; + Assertions.assertNotNull(insert.getSelect().getOption()); + } + + @Test + public void testOptionAfterMergeStatement() throws JSQLParserException { + String sql = + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a OPTION (HASH JOIN)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Merge merge = (Merge) statement; + Assertions.assertNotNull(merge.getOption()); + Assertions.assertEquals("HASH JOIN", merge.getOption().getOptionHints().get(0).getName()); + } + + @Test + public void testOptionAfterMergeOutputClause() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a " + + "OUTPUT deleted.a OPTION (HASH JOIN)", + false); + } + @Test public void testOptionAsIdentifierStillWorks() throws JSQLParserException { // OPTION stays a non-reserved keyword usable as column and table name