diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index d07d9179a..90119d476 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -531,6 +531,12 @@ default void visit(UserVariable userVariable) { this.visit(userVariable, null); } + T visit(UnknownVariable unknownVariable, S context); + + default void visit(UnknownVariable unknownVariable) { + this.visit(unknownVariable, null); + } + T visit(NumericBind numericBind, S context); default void visit(NumericBind numericBind) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 4727e93e8..e9d53145c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -536,6 +536,11 @@ public T visit(UserVariable userVariable, S context) { return applyExpression(userVariable, context); } + @Override + public T visit(UnknownVariable unknownVariable, S context) { + return applyExpression(unknownVariable, context); + } + @Override public T visit(NumericBind numericBind, S context) { return applyExpression(numericBind, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/UnknownVariable.java b/src/main/java/net/sf/jsqlparser/expression/UnknownVariable.java new file mode 100644 index 000000000..68e7be2f4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/UnknownVariable.java @@ -0,0 +1,54 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0. + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * A user variable followed by the bare {@code UNKNOWN} keyword, as used in the SQL Server (T-SQL) + * {@code OPTIMIZE FOR (@variable_name UNKNOWN)} query hint parameter form, see + * Hints + * (Transact-SQL) - Query Hints. + */ +public class UnknownVariable extends ASTNodeAccessImpl implements Expression { + + private UserVariable variable; + + public UnknownVariable() { + // empty constructor + } + + public UnknownVariable(UserVariable variable) { + this.variable = variable; + } + + public UserVariable getVariable() { + return variable; + } + + public void setVariable(UserVariable variable) { + this.variable = variable; + } + + public UnknownVariable withVariable(UserVariable variable) { + this.setVariable(variable); + return this; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + return variable.toString() + " UNKNOWN"; + } +} 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..7e372be5d 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,10 @@ public String toString() { b.append(outputClause); } + if (option != null) { + b.append(option); + } + return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 3e15be9a7..642a28047 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1137,6 +1137,12 @@ public Void visit(UserVariable userVariable, S context) { return null; } + @Override + public Void visit(UnknownVariable unknownVariable, S context) { + + return null; + } + @Override public Void visit(NumericBind numericBind, S context) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 2767f5e23..d650f938f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -74,6 +74,7 @@ import net.sf.jsqlparser.expression.TimezoneExpression; import net.sf.jsqlparser.expression.TranscodingFunction; import net.sf.jsqlparser.expression.TrimFunction; +import net.sf.jsqlparser.expression.UnknownVariable; import net.sf.jsqlparser.expression.UserVariable; import net.sf.jsqlparser.expression.VariableAssignment; import net.sf.jsqlparser.expression.WhenClause; @@ -1417,6 +1418,12 @@ public StringBuilder visit(UserVariable var, S context) { return builder; } + @Override + public StringBuilder visit(UnknownVariable var, S context) { + builder.append(var.toString()); + return builder; + } + @Override public StringBuilder visit(NumericBind bind, S context) { builder.append(bind.toString()); 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..580bcac61 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,10 @@ public void deParse(Merge merge) { if (merge.getOutputClause() != null) { merge.getOutputClause().appendTo(builder); } + + if (merge.getOption() != null) { + builder.append(merge.getOption()); + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java index eb39481d0..1942793f7 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java @@ -26,6 +26,9 @@ public ValuesStatementDeParser(ExpressionVisitor expressionVisito public void deParse(Values values) { builder.append("VALUES "); values.getExpressions().accept(expressionVisitor, null); + if (values.getOption() != null) { + builder.append(values.getOption()); + } if (values.getAlias() != null) { builder.append(" ").append(values.getAlias()); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 128fc7d92..d1ed53f81 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -68,6 +68,7 @@ import net.sf.jsqlparser.expression.TimezoneExpression; import net.sf.jsqlparser.expression.TranscodingFunction; import net.sf.jsqlparser.expression.TrimFunction; +import net.sf.jsqlparser.expression.UnknownVariable; import net.sf.jsqlparser.expression.UserVariable; import net.sf.jsqlparser.expression.VariableAssignment; import net.sf.jsqlparser.expression.WhenClause; @@ -819,6 +820,12 @@ public Void visit(UserVariable var, S context) { return null; } + @Override + public Void visit(UnknownVariable var, S context) { + // nothing to validate + return null; + } + @Override public Void visit(NumericBind bind, S context) { // nothing to validate 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/java/net/sf/jsqlparser/util/validation/validator/ValuesStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ValuesStatementValidator.java index 79643c889..0d8087918 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ValuesStatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ValuesStatementValidator.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.Values; /** @@ -21,5 +22,13 @@ public class ValuesStatementValidator extends AbstractValidator { public void validate(Values values) { validateFeature(Feature.values); validateOptionalExpression(values.getExpressions()); + if (values.getOption() != null) { + for (OptionHint optionHint : values.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index da45e5887..1eada8cf8 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); } } @@ -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 UnknownVariable(userVariable); + } + | + 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..da7bdd863 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -12,8 +12,11 @@ import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.UnknownVariable; import net.sf.jsqlparser.parser.CCJSqlParserUtil; 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,55 @@ 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()); + Assertions.assertTrue(optimizeFor.getParameters().get(0) instanceof UnknownVariable); + Assertions.assertEquals("@p UNKNOWN", optimizeFor.getParameters().get(0).toString()); + } + + @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()); + Assertions.assertTrue(optimizeFor.getParameters().get(1) instanceof UnknownVariable); + Assertions.assertEquals("@q UNKNOWN", optimizeFor.getParameters().get(1).toString()); + } + + @Test + public void testOptionAfterInsertValues() throws JSQLParserException { + String sql = "INSERT INTO t (a) VALUES (1) 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)", + true); + } + @Test public void testOptionAsIdentifierStillWorks() throws JSQLParserException { // OPTION stays a non-reserved keyword usable as column and table name