diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java new file mode 100644 index 000000000..5c5f93bab --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlSelectIntoClause.java @@ -0,0 +1,205 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 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.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class MySqlSelectIntoClause extends ASTNodeAccessImpl implements Serializable { + + public enum Position { + BEFORE_FROM, TRAILING + } + + public enum Type { + OUTFILE, DUMPFILE + } + + public enum FieldsKeyword { + FIELDS, COLUMNS + } + + private Position position = Position.TRAILING; + private Type type; + private StringValue fileName; + private String characterSet; + private FieldsKeyword fieldsKeyword; + private StringValue fieldsTerminatedBy; + private boolean fieldsOptionallyEnclosed; + private StringValue fieldsEnclosedBy; + private StringValue fieldsEscapedBy; + private StringValue linesStartingBy; + private StringValue linesTerminatedBy; + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public MySqlSelectIntoClause withPosition(Position position) { + this.setPosition(position); + return this; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public StringValue getFileName() { + return fileName; + } + + public void setFileName(StringValue fileName) { + this.fileName = fileName; + } + + public String getCharacterSet() { + return characterSet; + } + + public void setCharacterSet(String characterSet) { + this.characterSet = characterSet; + } + + public FieldsKeyword getFieldsKeyword() { + return fieldsKeyword; + } + + public void setFieldsKeyword(FieldsKeyword fieldsKeyword) { + this.fieldsKeyword = fieldsKeyword; + } + + public StringValue getFieldsTerminatedBy() { + return fieldsTerminatedBy; + } + + public void setFieldsTerminatedBy(StringValue fieldsTerminatedBy) { + this.fieldsTerminatedBy = fieldsTerminatedBy; + } + + public boolean isFieldsOptionallyEnclosed() { + return fieldsOptionallyEnclosed; + } + + public void setFieldsOptionallyEnclosed(boolean fieldsOptionallyEnclosed) { + this.fieldsOptionallyEnclosed = fieldsOptionallyEnclosed; + } + + public StringValue getFieldsEnclosedBy() { + return fieldsEnclosedBy; + } + + public void setFieldsEnclosedBy(StringValue fieldsEnclosedBy) { + this.fieldsEnclosedBy = fieldsEnclosedBy; + } + + public StringValue getFieldsEscapedBy() { + return fieldsEscapedBy; + } + + public void setFieldsEscapedBy(StringValue fieldsEscapedBy) { + this.fieldsEscapedBy = fieldsEscapedBy; + } + + public StringValue getLinesStartingBy() { + return linesStartingBy; + } + + public void setLinesStartingBy(StringValue linesStartingBy) { + this.linesStartingBy = linesStartingBy; + } + + public StringValue getLinesTerminatedBy() { + return linesTerminatedBy; + } + + public void setLinesTerminatedBy(StringValue linesTerminatedBy) { + this.linesTerminatedBy = linesTerminatedBy; + } + + public boolean hasFieldsClause() { + return fieldsKeyword != null || fieldsTerminatedBy != null || fieldsEnclosedBy != null + || fieldsEscapedBy != null; + } + + public boolean hasLinesClause() { + return linesStartingBy != null || linesTerminatedBy != null; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("INTO ").append(type); + appendFileName(builder); + appendCharacterSet(builder); + appendFieldsClause(builder); + appendLinesClause(builder); + return builder; + } + + private void appendFileName(StringBuilder builder) { + if (fileName != null) { + builder.append(" ").append(fileName); + } + } + + private void appendCharacterSet(StringBuilder builder) { + if (characterSet != null) { + builder.append(" CHARACTER SET ").append(characterSet); + } + } + + private void appendFieldsClause(StringBuilder builder) { + if (!hasFieldsClause()) { + return; + } + + builder.append(" ").append(fieldsKeyword != null ? fieldsKeyword : FieldsKeyword.FIELDS); + + if (fieldsTerminatedBy != null) { + builder.append(" TERMINATED BY ").append(fieldsTerminatedBy); + } + if (fieldsEnclosedBy != null) { + builder.append(" "); + if (fieldsOptionallyEnclosed) { + builder.append("OPTIONALLY "); + } + builder.append("ENCLOSED BY ").append(fieldsEnclosedBy); + } + if (fieldsEscapedBy != null) { + builder.append(" ESCAPED BY ").append(fieldsEscapedBy); + } + } + + private void appendLinesClause(StringBuilder builder) { + if (!hasLinesClause()) { + return; + } + + builder.append(" LINES"); + if (linesStartingBy != null) { + builder.append(" STARTING BY ").append(linesStartingBy); + } + if (linesTerminatedBy != null) { + builder.append(" TERMINATED BY ").append(linesTerminatedBy); + } + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java index 2e1057987..1d76255e0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java @@ -34,6 +34,7 @@ public class PlainSelect extends Select { private BigQuerySelectQualifier bigQuerySelectQualifier = null; private List> selectItems; private List intoTables; + private MySqlSelectIntoClause mySqlSelectIntoClause; private FromItem fromItem; private List lateralViews; private List joins; @@ -142,6 +143,14 @@ public void setIntoTables(List
intoTables) { this.intoTables = intoTables; } + public MySqlSelectIntoClause getMySqlSelectIntoClause() { + return mySqlSelectIntoClause; + } + + public void setMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { + this.mySqlSelectIntoClause = mySqlSelectIntoClause; + } + public List> getSelectItems() { return selectItems; } @@ -564,6 +573,12 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { } } + if (mySqlSelectIntoClause != null + && mySqlSelectIntoClause + .getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) { + builder.append(" ").append(mySqlSelectIntoClause); + } + if (fromItem != null) { builder.append(" FROM "); if (isUsingOnly) { @@ -646,6 +661,11 @@ public String toString() { StringBuilder builder = new StringBuilder(); super.appendTo(builder); + if (mySqlSelectIntoClause != null + && mySqlSelectIntoClause.getPosition() == MySqlSelectIntoClause.Position.TRAILING) { + builder.append(" ").append(mySqlSelectIntoClause); + } + if (settings != null && !settings.isEmpty()) { builder.append(" SETTINGS "); UpdateSet.appendUpdateSetsTo(builder, settings); @@ -698,6 +718,11 @@ public PlainSelect withIntoTables(List
intoTables) { return this; } + public PlainSelect withMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { + this.setMySqlSelectIntoClause(mySqlSelectIntoClause); + return this; + } + public PlainSelect withWhere(Expression where) { this.setWhere(where); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index f968f9015..9b0eb33a6 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -138,6 +138,18 @@ public T visit(PlainSelect plainSelect, S context) { selectItem.accept(selectItemVisitor, context); } + if (plainSelect.getMySqlSelectIntoClause() != null) { + MySqlSelectIntoClause mySqlSelectIntoClause = plainSelect.getMySqlSelectIntoClause(); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getFileName(), context); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getFieldsTerminatedBy(), + context); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getFieldsEnclosedBy(), context); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getFieldsEscapedBy(), context); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getLinesStartingBy(), context); + expressionVisitor.visitExpression(mySqlSelectIntoClause.getLinesTerminatedBy(), + context); + } + fromItemVisitor.visitTables(plainSelect.getIntoTables(), context); fromItemVisitor.visitFromItem(plainSelect.getFromItem(), context); 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 ba7d8ef3d..73366ce28 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -54,6 +54,7 @@ import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.LateralView; +import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; import net.sf.jsqlparser.statement.select.Offset; import net.sf.jsqlparser.statement.select.OptimizeFor; import net.sf.jsqlparser.statement.select.OrderByElement; @@ -250,6 +251,12 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { } } + if (plainSelect.getMySqlSelectIntoClause() != null + && plainSelect.getMySqlSelectIntoClause() + .getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) { + builder.append(" ").append(plainSelect.getMySqlSelectIntoClause()); + } + if (plainSelect.getFromItem() != null) { builder.append(" FROM "); if (plainSelect.isUsingOnly()) { @@ -369,6 +376,11 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { builder.append(" SKIP LOCKED"); } } + if (plainSelect.getMySqlSelectIntoClause() != null + && plainSelect.getMySqlSelectIntoClause() + .getPosition() == MySqlSelectIntoClause.Position.TRAILING) { + builder.append(" ").append(plainSelect.getMySqlSelectIntoClause()); + } if (plainSelect.getSettings() != null && !plainSelect.getSettings().isEmpty()) { builder.append(" SETTINGS "); deparseUpdateSets(plainSelect.getSettings(), builder, expressionVisitor); 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 36741ecd6..bbe176f3e 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 @@ -10,7 +10,6 @@ package net.sf.jsqlparser.util.validation.validator; import java.util.List; - import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.MySQLIndexHint; import net.sf.jsqlparser.expression.SQLServerHints; @@ -26,6 +25,7 @@ import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.MinusOp; +import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; import net.sf.jsqlparser.statement.select.Offset; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; @@ -122,6 +122,8 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalExpression(plainSelect.getPreWhere()); validateOptionalExpression(plainSelect.getWhere()); validateOptionalExpression(plainSelect.getOracleHierarchical()); + validateOptional(plainSelect.getMySqlSelectIntoClause(), + this::validateMySqlSelectIntoClause); if (plainSelect.getGroupBy() != null) { plainSelect.getGroupBy().accept(getValidator(GroupByValidator.class), context); @@ -147,6 +149,15 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { + validateOptionalExpression(mySqlSelectIntoClause.getFileName()); + validateOptionalExpression(mySqlSelectIntoClause.getFieldsTerminatedBy()); + validateOptionalExpression(mySqlSelectIntoClause.getFieldsEnclosedBy()); + validateOptionalExpression(mySqlSelectIntoClause.getFieldsEscapedBy()); + validateOptionalExpression(mySqlSelectIntoClause.getLinesStartingBy()); + validateOptionalExpression(mySqlSelectIntoClause.getLinesTerminatedBy()); + } + @Override public Void visit(SelectItem selectExpressionItem, S context) { selectExpressionItem.getExpression().accept(getValidator(ExpressionValidator.class), diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8344b3309..ff2de3b85 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -854,13 +854,16 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -879,6 +882,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -924,6 +928,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -970,9 +975,11 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1050,6 +1057,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1066,6 +1074,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -4912,6 +4921,7 @@ PlainSelect PlainSelect() #PlainSelect: ExpressionList expressionList = null; boolean partitionByBrackets = false; List
intoTables = null; + MySqlSelectIntoClause mySqlSelectIntoClause = null; Table updateTable = null; Wait wait = null; boolean mySqlSqlCalcFoundRows = false; @@ -4974,7 +4984,13 @@ PlainSelect PlainSelect() #PlainSelect: selectItems=SelectItemsList() - [ LOOKAHEAD(2) intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); } ] + [ LOOKAHEAD( ( | )) + mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.BEFORE_FROM) + { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } + ] + [ LOOKAHEAD() + intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); } + ] [ LOOKAHEAD(2) fromItem=FromItem() [ LOOKAHEAD(2) lateralViews=LateralViews() ] [ LOOKAHEAD(2) joins=JoinsList() ] @@ -5038,6 +5054,10 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD(2) ( { plainSelect.setNoWait(true); } | { plainSelect.setSkipLocked(true); }) ] ] + [ LOOKAHEAD( ( | )) + mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING) + { plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); } + ] [ LOOKAHEAD(2) settings = UpdateSets() { plainSelect.setSettings(settings); } ] [ LOOKAHEAD() optimize = OptimizeFor() { plainSelect.setOptimizeFor(optimize); } ] [ LOOKAHEAD(3) intoTempTable = Table() { plainSelect.setIntoTempTable(intoTempTable);} ] @@ -5629,6 +5649,128 @@ List
IntoClause(): } } +MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position position): +{ + MySqlSelectIntoClause intoClause = new MySqlSelectIntoClause().withPosition(position); + Token token; +} +{ + + ( + { intoClause.setType(MySqlSelectIntoClause.Type.OUTFILE); } + token= { intoClause.setFileName(new StringValue(token.image)); } + MySqlSelectIntoOutfileTail(intoClause) + | + { intoClause.setType(MySqlSelectIntoClause.Type.DUMPFILE); } + token= { intoClause.setFileName(new StringValue(token.image)); } + ) + { + return intoClause; + } +} + +void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause): +{ + Token token; +} +{ + ( + LOOKAHEAD( ) + + (token= | token=) + { intoClause.setCharacterSet(token.image); } + ( + LOOKAHEAD(( | )) + MySqlSelectIntoFieldsClause(intoClause) + [ LOOKAHEAD() MySqlSelectIntoLinesClause(intoClause) ] + | + LOOKAHEAD() + MySqlSelectIntoLinesClause(intoClause) + | + { } + ) + | + LOOKAHEAD(( | )) + MySqlSelectIntoFieldsClause(intoClause) + [ LOOKAHEAD() MySqlSelectIntoLinesClause(intoClause) ] + | + LOOKAHEAD() + MySqlSelectIntoLinesClause(intoClause) + | + { } + ) +} + +void MySqlSelectIntoFieldsClause(MySqlSelectIntoClause intoClause): +{ + Token token; +} +{ + ( + { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.FIELDS); } + | { intoClause.setFieldsKeyword(MySqlSelectIntoClause.FieldsKeyword.COLUMNS); } + ) + ( + LOOKAHEAD( ) + token= + { intoClause.setFieldsTerminatedBy(new StringValue(token.image)); } + ( + LOOKAHEAD(( | )) + [ { intoClause.setFieldsOptionallyEnclosed(true); } ] + token= + { intoClause.setFieldsEnclosedBy(new StringValue(token.image)); } + [ LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } + ] + | + LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } + | + { } + ) + | + LOOKAHEAD(( | )) + [ { intoClause.setFieldsOptionallyEnclosed(true); } ] + token= + { intoClause.setFieldsEnclosedBy(new StringValue(token.image)); } + [ LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } + ] + | + LOOKAHEAD( ) + token= + { intoClause.setFieldsEscapedBy(new StringValue(token.image)); } + | + { } + ) +} + +void MySqlSelectIntoLinesClause(MySqlSelectIntoClause intoClause): +{ + Token token; +} +{ + + ( + LOOKAHEAD( ) + token= + { intoClause.setLinesStartingBy(new StringValue(token.image)); } + [ LOOKAHEAD( ) + token= + { intoClause.setLinesTerminatedBy(new StringValue(token.image)); } + ] + | + LOOKAHEAD( ) + token= + { intoClause.setLinesTerminatedBy(new StringValue(token.image)); } + | + { } + ) +} + FromItem ParenthesedFromItem(): { ParenthesedFromItem ParenthesedFromItem = new ParenthesedFromItem(); diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index a398efaf0..e2233a885 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -3222,6 +3222,57 @@ public void testSelectInto1() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT * INTO user_copy FROM user"); } + @Test + public void testMySqlSelectIntoOutfileBeforeFrom() throws JSQLParserException { + String stmt = "SELECT a, b INTO OUTFILE '/tmp/result.txt' " + + "FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' " + + "LINES TERMINATED BY '\\n' FROM test_table"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(stmt, true); + MySqlSelectIntoClause intoClause = select.getPlainSelect().getMySqlSelectIntoClause(); + assertNotNull(intoClause); + assertEquals(MySqlSelectIntoClause.Position.BEFORE_FROM, intoClause.getPosition()); + assertEquals(MySqlSelectIntoClause.Type.OUTFILE, intoClause.getType()); + assertEquals("'/tmp/result.txt'", intoClause.getFileName().toString()); + assertEquals("','", intoClause.getFieldsTerminatedBy().toString()); + assertTrue(intoClause.isFieldsOptionallyEnclosed()); + assertEquals("'\"'", intoClause.getFieldsEnclosedBy().toString()); + assertEquals("'\\n'", intoClause.getLinesTerminatedBy().toString()); + } + + @Test + public void testMySqlSelectIntoOutfileTrailing() throws JSQLParserException { + String stmt = "SELECT * FROM users INTO OUTFILE '/tmp/users.csv' " + + "FIELDS TERMINATED BY ',' ENCLOSED BY '\"' " + + "LINES TERMINATED BY '\\n'"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(stmt, true); + MySqlSelectIntoClause intoClause = select.getPlainSelect().getMySqlSelectIntoClause(); + assertNotNull(intoClause); + assertEquals(MySqlSelectIntoClause.Position.TRAILING, intoClause.getPosition()); + assertEquals(MySqlSelectIntoClause.Type.OUTFILE, intoClause.getType()); + assertEquals("'/tmp/users.csv'", intoClause.getFileName().toString()); + assertEquals("'\"'", intoClause.getFieldsEnclosedBy().toString()); + assertEquals("'\\n'", intoClause.getLinesTerminatedBy().toString()); + } + + @Test + public void testMySqlSelectIntoDumpfileTrailing() throws JSQLParserException { + String stmt = "SELECT id FROM users INTO DUMPFILE '/tmp/users.dump'"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(stmt, true); + MySqlSelectIntoClause intoClause = select.getPlainSelect().getMySqlSelectIntoClause(); + assertNotNull(intoClause); + assertEquals(MySqlSelectIntoClause.Position.TRAILING, intoClause.getPosition()); + assertEquals(MySqlSelectIntoClause.Type.DUMPFILE, intoClause.getType()); + assertEquals("'/tmp/users.dump'", intoClause.getFileName().toString()); + } + + @Test + public void testMySqlSelectIntoOutfileRejectsFieldsAfterLines() { + String stmt = "SELECT * FROM users INTO OUTFILE '/tmp/users.csv' " + + "LINES TERMINATED BY '\\n' FIELDS TERMINATED BY ','"; + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(stmt)); + } + @Test public void testSelectForUpdate() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT * FROM user_table FOR UPDATE");