diff --git a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java index 4e0f45ffe..65301efa8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java +++ b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java @@ -20,6 +20,7 @@ import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.WithItem; @@ -45,6 +46,7 @@ public class Delete implements Statement { private Expression where; private PreferringClause preferringClause; private Limit limit; + private OptionClause option; private List orderByElements; private boolean hasFrom = true; private DeleteModifierPriority modifierPriority; @@ -147,6 +149,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Delete setOption(OptionClause option) { + this.option = option; + return this; + } + + public Delete withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -280,6 +295,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java new file mode 100644 index 000000000..cb941819c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java @@ -0,0 +1,59 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 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 + * (Transact-SQL) - Query Hints. + */ +public class OptionClause implements Serializable { + + private List optionHints; + + public OptionClause() { + super(); + } + + public OptionClause(List optionHints) { + this.optionHints = optionHints; + } + + public List getOptionHints() { + return optionHints; + } + + public void setOptionHints(List optionHints) { + this.optionHints = optionHints; + } + + public OptionClause withOptionHints(List optionHints) { + setOptionHints(optionHints); + return this; + } + + public OptionClause addOptionHint(OptionHint optionHint) { + if (optionHints == null) { + optionHints = new ArrayList<>(); + } + optionHints.add(optionHint); + return this; + } + + @Override + public String toString() { + return " OPTION (" + Select.getStringList(optionHints, true, false) + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java new file mode 100644 index 000000000..85dc0c12d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java @@ -0,0 +1,113 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * Models one hint of the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, see + * Hints + * (Transact-SQL) - Query Hints. + *

+ * A hint is a (possibly multi-word) keyword such as {@code RECOMPILE}, {@code HASH JOIN} or + * {@code OPTIMIZE FOR UNKNOWN}, optionally followed by a single value argument ({@code FAST 100}, + * {@code MAXDOP 4}, {@code MAX_GRANT_PERCENT = 25}) or by a parenthesized argument list + * ({@code USE HINT ('...')}, {@code OPTIMIZE FOR (@p = 1)}, {@code TABLE HINT (t, INDEX (i))}). + */ +public class OptionHint implements Serializable { + + private String name; + private Expression value; + private ExpressionList parameters; + private boolean useEquals = false; + + public OptionHint() { + super(); + } + + public OptionHint(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OptionHint withName(String name) { + setName(name); + return this; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public OptionHint withValue(Expression value) { + setValue(value); + return this; + } + + public ExpressionList getParameters() { + return parameters; + } + + public void setParameters(ExpressionList parameters) { + this.parameters = parameters; + } + + public OptionHint withParameters(ExpressionList parameters) { + setParameters(parameters); + return this; + } + + public OptionHint addParameter(Expression parameter) { + if (parameters == null) { + parameters = new ExpressionList<>(); + } + parameters.add(parameter); + return this; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public OptionHint withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(name); + if (value != null) { + b.append(useEquals ? " = " : " ").append(value); + } + if (parameters != null) { + b.append(" (").append(Select.getStringList(parameters, true, false)).append(')'); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 0dc57e32a..501e21728 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -34,6 +34,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp boolean oracleSiblings = false; ForClause forClause = null; + OptionClause option = null; List orderByElements; List interpolate; @@ -195,6 +196,19 @@ public Select setForClause(ForClause forClause) { return this; } + public OptionClause getOption() { + return option; + } + + public Select setOption(OptionClause option) { + this.option = option; + return this; + } + + public Select withOption(OptionClause option) { + return setOption(option); + } + public List getOrderByElements() { return orderByElements; } @@ -494,6 +508,10 @@ public StringBuilder appendTo(StringBuilder builder) { forClause.appendTo(builder); } + if (option != null) { + builder.append(option); + } + if (limitBy != null) { builder.append(limitBy); } diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index b09854fe6..03d9d2580 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -21,6 +21,7 @@ import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -48,6 +49,7 @@ public class Update implements Statement { private OracleHint oracleHint = null; private List orderByElements; private Limit limit; + private OptionClause option; private ReturningClause returningClause; private UpdateModifierPriority modifierPriority; private boolean modifierIgnore; @@ -255,6 +257,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Update setOption(OptionClause option) { + this.option = option; + return this; + } + + public Update withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -356,6 +371,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java index 2ab59ea14..7472c968f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java @@ -105,6 +105,10 @@ public void deParse(Delete delete) { new LimitDeparser(expressionVisitor, builder).deParse(delete.getLimit()); } + if (delete.getOption() != null) { + builder.append(delete.getOption()); + } + if (delete.getReturningClause() != null) { delete.getReturningClause().appendTo(builder); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 46421c1c8..c061ff45f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -140,6 +140,10 @@ public StringBuilder visit(ParenthesedSelect select, S context) { deParseInterpolate(select.getInterpolate()); } + if (select.getOption() != null) { + builder.append(select.getOption()); + } + Alias alias = select.getAlias(); if (alias != null) { builder.append(alias); @@ -345,6 +349,10 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { plainSelect.getForClause().appendTo(builder); } + if (plainSelect.getOption() != null) { + builder.append(plainSelect.getOption()); + } + if (plainSelect.isEmitChanges()) { builder.append(" EMIT CHANGES"); } @@ -758,6 +766,10 @@ public StringBuilder visit(SetOperationList list, S context) { deParseInterpolate(list.getInterpolate()); } + if (list.getOption() != null) { + builder.append(list.getOption()); + } + if (list.getLimit() != null) { new LimitDeparser(expressionVisitor, builder).deParse(list.getLimit()); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index d5dcac7f1..b18d58090 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -103,6 +103,10 @@ public void deParse(Update update) { new LimitDeparser(expressionVisitor, builder).deParse(update.getLimit()); } + if (update.getOption() != null) { + builder.append(update.getOption()); + } + if (update.getReturningClause() != null) { update.getReturningClause().appendTo(builder); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java index 19588e3b7..4accb3df4 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.util.validation.ValidationCapability; /** @@ -48,6 +49,15 @@ public void validate(Delete delete) { getValidator(LimitValidator.class).validate(delete.getLimit()); } + if (delete.getOption() != null) { + for (OptionHint optionHint : delete.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (delete.getReturningClause() != null) { delete.getReturningClause().forEach(c -> c.accept(v, null)); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 92334db52..a0511d480 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -37,6 +37,8 @@ import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectItemVisitor; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.OptionClause; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; @@ -138,6 +140,7 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalExpression(plainSelect.getHaving()); validateOptionalOrderByElements(plainSelect.getOrderByElements()); validateOptionalInterpolate(plainSelect.getInterpolate()); + validateOptionalOption(plainSelect.getOption()); if (plainSelect.getLimit() != null) { getValidator(LimitValidator.class).validate(plainSelect.getLimit()); @@ -156,6 +159,17 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + private void validateOptionalOption(OptionClause option) { + if (option != null) { + for (OptionHint optionHint : option.getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + } + private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { validateOptionalExpression(mySqlSelectIntoClause.getFileName()); validateOptionalExpression(mySqlSelectIntoClause.getFieldsTerminatedBy()); @@ -320,6 +334,7 @@ public Void visit(SetOperationList setOperation, S context) { validateOptionalOrderByElements(setOperation.getOrderByElements()); validateOptionalInterpolate(setOperation.getInterpolate()); + validateOptionalOption(setOperation.getOption()); if (setOperation.getLimit() != null) { getValidator(LimitValidator.class).validate(setOperation.getLimit()); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java index 058b6aa9f..5701b4b7c 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -60,6 +61,15 @@ public void validate(Update update) { getValidator(LimitValidator.class).validate(update.getLimit()); } + if (update.getOption() != null) { + for (OptionHint optionHint : update.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (update.getReturningClause() != null) { SelectValidator v = getValidator(SelectValidator.class); update.getReturningClause().forEach(c -> c.accept(v, null)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5f8e91d11..f995af018 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -721,6 +721,45 @@ public class CCJSqlParser extends AbstractJSqlParser { return (Function) inner; } + /** + * Whether the next token can continue the (possibly multi-word) keyword of + * an OPTION query hint, e.g. any word of {@code HASH JOIN}, {@code KEEP PLAN}, + * {@code OPTIMIZE FOR UNKNOWN} or {@code IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX}. + * + * Reserved words usable inside hint keywords are enumerated here because + * they cannot be matched by {@code RelObjectName()}. + */ + private boolean isOptionHintWordAhead() { + int kind = getToken(1).kind; + if (kind == S_IDENTIFIER + || (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD)) { + return true; + } + switch (kind) { + case K_JOIN: case K_FOR: case K_ORDER: + case K_UNION: case K_EXCEPT: case K_INTERSECT: + case K_USE: case K_OPTIMIZE: case K_FORCE: case K_UNKNOWN: + return true; + default: + return false; + } + } + + /** + * Whether the next token can start the single value argument of an OPTION + * query hint, e.g. the {@code 100} of {@code FAST 100} or the {@code 25} + * of {@code MAX_GRANT_PERCENT = 25}. + */ + private boolean isOptionHintValueAhead() { + switch (getToken(1).kind) { + case S_LONG: case S_DOUBLE: case S_CHAR_LITERAL: + case S_PARAMETER: + return true; + default: + return false; + } + } + /** * Follower-based disambiguation for reserved keywords in ambiguous * positions (implicit alias, clause boundary, after parenthesised @@ -927,6 +966,11 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; } + // OPTION (...) introduces a query hint clause, not an alias + if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) { + return false; + } + // Non-reserved keywords if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { return true; @@ -1373,6 +1417,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3724,6 +3769,7 @@ Update Update(): FromItem fromItem = null; List joins = null; Limit limit = null; + OptionClause optionClause = null; List orderByElements; boolean useColumnsBrackets = false; ReturningClause returningClause; @@ -3751,6 +3797,7 @@ Update Update(): [ orderByElements = OrderByElements() { update.setOrderByElements(orderByElements); } ] [ limit = PlainLimit() { update.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { update.setOption(optionClause); } ] [ returningClause = ReturningClause() { update.setReturningClause(returningClause); } ] { @@ -4250,6 +4297,7 @@ Delete Delete(): Expression where = null; PreferringClause preferringClause = null; Limit limit = null; + OptionClause optionClause = null; List orderByElements; boolean hasFrom = false; Token tk = null; @@ -4277,6 +4325,7 @@ Delete Delete(): [preferringClause=PreferringClause() { delete.setPreferringClause(preferringClause);} ] [orderByElements = OrderByElements() { delete.setOrderByElements(orderByElements); } ] [limit=PlainLimit() {delete.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { delete.setOption(optionClause); } ] [ returningClause = ReturningClause() { delete.setReturningClause(returningClause); } ] { @@ -4691,6 +4740,7 @@ Select Select() #Select: List> with = null; List orderByElements = null; List interpolateElements = null; + OptionClause optionClause = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -4715,6 +4765,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() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -5338,6 +5389,7 @@ PlainSelect PlainSelect() #PlainSelect: Expression preWhere = null; Expression where = null; ForClause forClause = null; + OptionClause optionClause = null; List orderByElements; List interpolateElements = null; GroupByElement groupBy = null; @@ -5470,6 +5522,7 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] + [ LOOKAHEAD(2) optionClause = OptionClause() { plainSelect.setOption(optionClause); } ] [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then // optionally attach ClickHouse's `LIMIT ... BY ...`. Checking BY right after the limit @@ -5606,6 +5659,10 @@ Select SetOperationList(Select select) #SetOperationList: { list.setInterpolate(ps.getInterpolate()); ps.setInterpolate(null); } + if (ps.getOption() != null) { + list.setOption(ps.getOption()); + ps.setOption(null); + } if (ps.getFetch() != null) { list.setFetch(ps.getFetch()); ps.setFetch(null); @@ -6855,6 +6912,64 @@ InterpolateElement InterpolateElement(): { return interpolateElement; } } +OptionClause OptionClause(): +{ + OptionClause optionClause = new OptionClause(); + OptionHint optionHint = null; +} +{ + "(" optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } + ( "," optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } )* + ")" + { return optionClause; } +} + +OptionHint OptionHint(): +{ + OptionHint optionHint = new OptionHint(); + String name = null; + Expression value = null; + Expression parameter = null; +} +{ + name = OptionHintName() { optionHint.setName(name); } + [ + ( + LOOKAHEAD("=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); } + | + LOOKAHEAD("(") "(" parameter = Expression() { optionHint.addParameter(parameter); } + ( "," parameter = Expression() { optionHint.addParameter(parameter); } )* + ")" + | + LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); } + ) + ] + { return optionHint; } +} + +String OptionHintName(): +{ + Token tk = null; + String word = null; + StringBuilder name = new StringBuilder(); +} +{ + ( + LOOKAHEAD({ isOptionHintWordAhead() }) + ( tk= | word = NonReservedWord() + | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= ) + { + if (name.length() > 0) { name.append(' '); } + name.append(tk != null ? tk.image : word); + tk = null; + word = null; + } + )+ + { return name.toString(); } +} + JdbcParameter JdbcParameter() : { Token tk; JdbcParameter retval; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java new file mode 100644 index 000000000..bfeba4be2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -0,0 +1,158 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OptionClauseTest { + + @Test + public void testOptionHintIssue161() throws JSQLParserException { + String sql = + "SELECT CustomerID, PersonID, StoreID FROM cte OPTION (MAXRECURSION 2)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + PlainSelect plainSelect = (PlainSelect) ((Select) statement).getPlainSelect(); + Assertions.assertNotNull(plainSelect.getOption()); + Assertions.assertEquals(1, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("MAXRECURSION", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("2", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionWithoutArguments() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE x = 1 OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionMultiWordHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (KEEP PLAN, KEEPFIXED PLAN, FORCE ORDER, EXPAND VIEWS, NO_PERFORMANCE_SPOOL, IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(6, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("KEEP PLAN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("FORCE ORDER", + plainSelect.getOption().getOptionHints().get(2).getName()); + } + + @Test + public void testOptionJoinAndUnionHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (HASH JOIN, LOOP JOIN, MERGE JOIN, CONCAT UNION)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(4, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("HASH JOIN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("CONCAT UNION", + plainSelect.getOption().getOptionHints().get(3).getName()); + } + + @Test + public void testOptionNumericValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (FAST 100, MAXDOP 4, MAXRECURSION 0)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("FAST", plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("100", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + Assertions.assertFalse(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + } + + @Test + public void testOptionEqualsValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (MAX_GRANT_PERCENT = 25.5, MIN_GRANT_PERCENT = 10)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("MAX_GRANT_PERCENT", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertTrue(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + Assertions.assertEquals("25.5", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionParameterizedHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (OPTIMIZE FOR UNKNOWN, USE HINT ('ASSUME_JOIN_PREDICATE_DEPENDS_ON_COLUMNS', 'DISABLE_PARAMETER_SNIFFING'))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("OPTIMIZE FOR UNKNOWN", + plainSelect.getOption().getOptionHints().get(0).getName()); + OptionHint useHint = plainSelect.getOption().getOptionHints().get(1); + Assertions.assertEquals("USE HINT", useHint.getName()); + Assertions.assertEquals(2, useHint.getParameters().size()); + } + + @Test + public void testOptionOptimizeForAssignments() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("OPTIMIZE FOR", optimizeFor.getName()); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + } + + @Test + public void testOptionTableHint() throws JSQLParserException { + String sql = "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a OPTION (TABLE HINT (t2, INDEX(ix2)))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint tableHint = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("TABLE HINT", tableHint.getName()); + Assertions.assertEquals("t2", tableHint.getParameters().get(0).toString()); + Assertions.assertEquals("INDEX(ix2)", tableHint.getParameters().get(1).toString()); + } + + @Test + public void testOptionAfterUnion() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a OPTION (RECOMPILE, MAXDOP 1) LIMIT 10"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = + (SetOperationList) ((Select) statement).getSelectBody(); + Assertions.assertNotNull(setOperationList.getOption()); + Assertions.assertEquals(2, setOperationList.getOption().getOptionHints().size()); + } + + @Test + public void testOptionAfterForXml() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t FOR XML PATH('row') OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionInUpdateAndDelete() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("UPDATE t SET a = 1 WHERE b = 2 OPTION (RECOMPILE)", true); + assertSqlCanBeParsedAndDeparsed("DELETE FROM t WHERE a = 1 OPTION (LOOP JOIN)", true); + + Statement update = CCJSqlParserUtil.parse("UPDATE t SET a = 1 OPTION (RECOMPILE)"); + Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption()); + } + + @Test + public void testOptionAsIdentifierStillWorks() throws JSQLParserException { + // OPTION stays a non-reserved keyword usable as column and table name + assertSqlCanBeParsedAndDeparsed("SELECT option FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM option", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT a, option AS o FROM t WHERE option = 1 ORDER BY option", true); + } +}