Skip to content

Commit 588bf07

Browse files
authored
Support MySQL SELECT INTO user variables with explicit dialect (#2583)
1 parent eddb1fb commit 588bf07

7 files changed

Lines changed: 256 additions & 18 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import net.sf.jsqlparser.statement.select.PivotVisitor;
6666
import net.sf.jsqlparser.statement.select.PivotVisitorAdapter;
6767
import net.sf.jsqlparser.statement.select.PlainSelect;
68+
import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause;
6869
import net.sf.jsqlparser.statement.select.Select;
6970
import net.sf.jsqlparser.statement.select.SelectItem;
7071
import net.sf.jsqlparser.statement.select.SelectItemVisitor;
@@ -795,6 +796,12 @@ public <S> Void visit(PlainSelect plainSelect, S context) {
795796
analysis.certain.remove(StmtFeature.RETURNS_RESULT_SET);
796797
}
797798

799+
MySqlSelectIntoClause mySqlInto = plainSelect.getMySqlSelectIntoClause();
800+
if (mySqlInto != null && mySqlInto.getType() == MySqlSelectIntoClause.Type.VARIABLES) {
801+
analysis.certain(StmtFeature.MODIFIES_SESSION);
802+
analysis.certain.remove(StmtFeature.RETURNS_RESULT_SET);
803+
}
804+
798805
if (plainSelect.getForMode() != null) {
799806
// FOR UPDATE / FOR SHARE take row locks
800807
analysis.certain(StmtFeature.MODIFIES_TRANSACTION);

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
package net.sf.jsqlparser.statement.select;
1111

1212
import java.io.Serializable;
13+
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.expression.Expression;
1315
import net.sf.jsqlparser.expression.StringValue;
16+
import net.sf.jsqlparser.expression.UserVariable;
17+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
1418
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1519

1620
public class MySqlSelectIntoClause extends ASTNodeAccessImpl implements Serializable {
@@ -20,7 +24,7 @@ public enum Position {
2024
}
2125

2226
public enum Type {
23-
OUTFILE, DUMPFILE
27+
OUTFILE, DUMPFILE, VARIABLES
2428
}
2529

2630
public enum FieldsKeyword {
@@ -30,6 +34,7 @@ public enum FieldsKeyword {
3034
private Position position = Position.TRAILING;
3135
private Type type;
3236
private StringValue fileName;
37+
private ExpressionList<UserVariable> variables;
3338
private String characterSet;
3439
private FieldsKeyword fieldsKeyword;
3540
private StringValue fieldsTerminatedBy;
@@ -60,6 +65,14 @@ public void setType(Type type) {
6065
this.type = type;
6166
}
6267

68+
public ExpressionList<UserVariable> getVariables() {
69+
return variables;
70+
}
71+
72+
public void setVariables(ExpressionList<UserVariable> variables) {
73+
this.variables = variables;
74+
}
75+
6376
public StringValue getFileName() {
6477
return fileName;
6578
}
@@ -142,7 +155,19 @@ public boolean hasLinesClause() {
142155
}
143156

144157
public StringBuilder appendTo(StringBuilder builder) {
145-
builder.append("INTO ").append(type);
158+
return appendTo(builder, expression -> builder.append(expression));
159+
}
160+
161+
/** Shares INTO rendering while allowing deparsers to visit variable targets. */
162+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
163+
builder.append("INTO ");
164+
if (type == Type.VARIABLES) {
165+
if (variables != null) {
166+
expressionRenderer.accept(variables);
167+
}
168+
return builder;
169+
}
170+
builder.append(type);
146171
appendFileName(builder);
147172
appendCharacterSet(builder);
148173
appendFieldsClause(builder);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public <S> T visit(PlainSelect plainSelect, S context) {
164164

165165
if (plainSelect.getMySqlSelectIntoClause() != null) {
166166
MySqlSelectIntoClause mySqlSelectIntoClause = plainSelect.getMySqlSelectIntoClause();
167+
expressionVisitor.visitExpressions(mySqlSelectIntoClause.getVariables(), context);
167168
expressionVisitor.visitExpression(mySqlSelectIntoClause.getFileName(), context);
168169
expressionVisitor.visitExpression(mySqlSelectIntoClause.getFieldsTerminatedBy(),
169170
context);

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,7 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
258258
}
259259
}
260260

261-
if (plainSelect.getMySqlSelectIntoClause() != null
262-
&& plainSelect.getMySqlSelectIntoClause()
263-
.getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) {
264-
builder.append(" ").append(plainSelect.getMySqlSelectIntoClause());
265-
}
261+
deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context);
266262

267263
if (plainSelect.getFromItem() != null) {
268264
builder.append(" FROM ");
@@ -401,11 +397,7 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
401397
if (plainSelect.isForUpdateBeforeOrderBy()) {
402398
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
403399
}
404-
if (plainSelect.getMySqlSelectIntoClause() != null
405-
&& plainSelect.getMySqlSelectIntoClause()
406-
.getPosition() == MySqlSelectIntoClause.Position.TRAILING) {
407-
builder.append(" ").append(plainSelect.getMySqlSelectIntoClause());
408-
}
400+
deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.TRAILING, context);
409401
if (plainSelect.getSettings() != null && !plainSelect.getSettings().isEmpty()) {
410402
builder.append(" SETTINGS ");
411403
deparseUpdateSets(plainSelect.getSettings(), builder, expressionVisitor);
@@ -720,6 +712,15 @@ public void setExpressionVisitor(ExpressionVisitor<StringBuilder> visitor) {
720712
expressionVisitor = visitor;
721713
}
722714

715+
private <S> void deparseMySqlSelectInto(PlainSelect select,
716+
MySqlSelectIntoClause.Position position, S context) {
717+
MySqlSelectIntoClause into = select.getMySqlSelectIntoClause();
718+
if (into != null && into.getPosition() == position) {
719+
builder.append(' ');
720+
into.appendTo(builder, expression -> expression.accept(expressionVisitor, context));
721+
}
722+
}
723+
723724
@SuppressWarnings({"PMD.CyclomaticComplexity"})
724725
public void deparseJoin(Join join) {
725726
if (join.isGlobal()) {

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,20 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14201420
return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect);
14211421
}
14221422

1423+
private boolean isMySqlSelectIntoAhead() {
1424+
if (getToken(1).kind != K_INTO) { return false; }
1425+
int kind = getToken(2).kind;
1426+
return kind == K_OUTFILE || kind == K_DUMPFILE
1427+
|| isMySqlDialect() && (kind == S_AT_IDENTIFIER || kind == K_AT_SIGN);
1428+
}
1429+
1430+
private static UserVariable requireMySqlIntoVariable(UserVariable variable) throws ParseException {
1431+
if (variable.isDoubleAdd()) {
1432+
throw new ParseException("INTO requires a user variable with a single @ prefix");
1433+
}
1434+
return variable;
1435+
}
1436+
14231437
private ExplainStatement.OptionType postgresqlExplainOptionType(String name) throws ParseException {
14241438
try {
14251439
return ExplainStatement.OptionType.from(name);
@@ -6190,12 +6204,14 @@ PlainSelect PlainSelect() #PlainSelect:
61906204

61916205
selectItems=SelectItemsList()
61926206

6193-
[ LOOKAHEAD(<K_INTO> (<K_OUTFILE> | <K_DUMPFILE>))
6194-
mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.BEFORE_FROM)
6195-
{ plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); }
6196-
]
61976207
[ LOOKAHEAD(<K_INTO>)
6198-
intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); }
6208+
(
6209+
LOOKAHEAD({ isMySqlSelectIntoAhead() })
6210+
mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.BEFORE_FROM)
6211+
{ plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); }
6212+
|
6213+
intoTables = IntoClause() { plainSelect.setIntoTables(intoTables); }
6214+
)
61996215
]
62006216
[ LOOKAHEAD(2) <K_FROM> fromItem=FromItem()
62016217
[ LOOKAHEAD(2) lateralViews=LateralViews() ]
@@ -6287,7 +6303,12 @@ PlainSelect PlainSelect() #PlainSelect:
62876303
]
62886304
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
62896305
]
6290-
[ LOOKAHEAD(<K_INTO> (<K_OUTFILE> | <K_DUMPFILE>))
6306+
[ LOOKAHEAD({ isMySqlSelectIntoAhead() })
6307+
{
6308+
if (mySqlSelectIntoClause != null || intoTables != null) {
6309+
throw new ParseException("Only one INTO clause is allowed per SELECT");
6310+
}
6311+
}
62916312
mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING)
62926313
{ plainSelect.setMySqlSelectIntoClause(mySqlSelectIntoClause); }
62936314
]
@@ -6941,6 +6962,8 @@ MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position posit
69416962
{
69426963
MySqlSelectIntoClause intoClause = new MySqlSelectIntoClause().withPosition(position);
69436964
Token token;
6965+
ExpressionList<UserVariable> variables = new ExpressionList<UserVariable>();
6966+
UserVariable variable;
69446967
}
69456968
{
69466969
<K_INTO>
@@ -6951,12 +6974,32 @@ MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position posit
69516974
|
69526975
<K_DUMPFILE> { intoClause.setType(MySqlSelectIntoClause.Type.DUMPFILE); }
69536976
token=<S_CHAR_LITERAL> { intoClause.setFileName(new StringValue(token.image)); }
6977+
|
6978+
LOOKAHEAD({ isMySqlDialect() })
6979+
variable=MySqlIntoVariable() { variables.add(variable); }
6980+
( "," variable=MySqlIntoVariable() { variables.add(variable); } )*
6981+
{ intoClause.setType(MySqlSelectIntoClause.Type.VARIABLES); intoClause.setVariables(variables); }
69546982
)
69556983
{
69566984
return intoClause;
69576985
}
69586986
}
69596987

6988+
UserVariable MySqlIntoVariable():
6989+
{
6990+
UserVariable variable;
6991+
Token name;
6992+
}
6993+
{
6994+
(
6995+
variable=UserVariable()
6996+
|
6997+
<K_AT_SIGN> (name=<S_CHAR_LITERAL> | name=<S_QUOTED_IDENTIFIER>)
6998+
{ variable = new UserVariable("@" + name.image); }
6999+
)
7000+
{ return requireMySqlIntoVariable(variable); }
7001+
}
7002+
69607003
MySqlProcedureAnalyse MySqlProcedureAnalyse():
69617004
{
69627005
MySqlProcedureAnalyse procedureAnalyse = new MySqlProcedureAnalyse();

src/site/sphinx/usage.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,12 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
716716

717717
Features set explicitly *after* the preset win over it.
718718

719+
MySQL user-variable targets in ``SELECT ... INTO @variable`` require
720+
``Dialect.MYSQL`` or ``Dialect.MARIADB``. They are stored in
721+
``PlainSelect.getMySqlSelectIntoClause().getVariables()`` as ``UserVariable``
722+
expressions, with the clause position preserved before ``FROM`` or at the end
723+
of the query. They are not table targets in ``getIntoTables()``.
724+
719725
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
720726
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
721727
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words

0 commit comments

Comments
 (0)