Skip to content

Commit 8d8fa7f

Browse files
authored
feat: parse MySQL LOAD DATA and LOAD XML (#2905)
LOAD DATA INFILE failed at INFILE, since LOAD DATA only read BigQuery's LOAD DATA INTO, and LOAD XML was read as DuckDB's LOAD of an extension named xml. Both now parse to MySqlLoadDataStatement with every clause of the MySQL 8.4 grammar, in the MySQL and MariaDB dialects and without a dialect. The other LOAD readings and the other dialects are unchanged. The FIELDS and LINES clauses become MySqlFileFormat, one model and one production shared with SELECT ... INTO OUTFILE, whose getters and setters now read and write it. As in MySQL, each clause needs a subclause and the subclauses may come in any order, so a bare FIELDS or LINES after INTO OUTFILE is now rejected. INTO OUTFILE also takes CHARSET and a quoted character set name. BigQuery's LOAD DATA now deparses its options through the expression deparser, and LOAD DATA and EXPORT DATA share one WITH CONNECTION production. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent bccb1c8 commit 8d8fa7f

23 files changed

Lines changed: 1747 additions & 179 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
155155
| **MySQL servers, plugins and components** | `CREATE SERVER` · `ALTER SERVER` with `OPTIONS (… PORT 3306)` · `INSTALL`/`UNINSTALL PLUGIN` · `INSTALL`/`UNINSTALL COMPONENT` |
156156
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
157157
| **MySQL server administration** | `FLUSH` · `KILL` · `RESET PERSIST` · `HANDLER … OPEN`/`READ`/`CLOSE` |
158+
| **MySQL data loading** | `LOAD DATA` · `LOAD XML` · `SELECT … INTO OUTFILE` with the same `FIELDS` and `LINES` clauses |
158159
| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` |
159160
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
160161
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,10 +1342,12 @@ public enum Feature {
13421342
export,
13431343

13441344
/**
1345-
* PostgreSQL and DuckDB "COPY" and BigQuery "LOAD DATA" statements are allowed
1345+
* PostgreSQL and DuckDB "COPY", BigQuery and MySQL "LOAD DATA" and MySQL "LOAD XML" statements
1346+
* are allowed
13461347
*
13471348
* @see net.sf.jsqlparser.statement.CopyStatement
13481349
* @see net.sf.jsqlparser.statement.load.LoadDataStatement
1350+
* @see net.sf.jsqlparser.statement.load.MySqlLoadDataStatement
13491351
*/
13501352
copy, loadData,
13511353

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement;
11+
12+
import java.io.Serializable;
13+
import net.sf.jsqlparser.expression.StringValue;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
15+
16+
/**
17+
* The {@code {FIELDS | COLUMNS}} and {@code LINES} clauses that describe the text file of a MySQL
18+
* {@code LOAD DATA} or {@code SELECT ... INTO OUTFILE}:
19+
*
20+
* <pre>
21+
* [{FIELDS | COLUMNS} [TERMINATED BY 'string'] [[OPTIONALLY] ENCLOSED BY 'char'] [ESCAPED BY 'char']]
22+
* [LINES [STARTING BY 'string'] [TERMINATED BY 'string']]
23+
* </pre>
24+
*
25+
* Each clause has at least one subclause. MySQL accepts the subclauses of a clause in any order and
26+
* more than once; like the server, the model keeps the last value of each and prints them in the
27+
* order above. {@code OPTIONALLY} stays set once a subclause has written it.
28+
*
29+
* @see <a href="https://dev.mysql.com/doc/refman/8.4/en/load-data.html">LOAD DATA</a>
30+
*/
31+
public class MySqlFileFormat extends ASTNodeAccessImpl implements Serializable {
32+
33+
/** The spelling of the fields clause. */
34+
public enum FieldsKeyword {
35+
FIELDS, COLUMNS
36+
}
37+
38+
private FieldsKeyword fieldsKeyword;
39+
private StringValue fieldsTerminatedBy;
40+
private boolean fieldsOptionallyEnclosed;
41+
private StringValue fieldsEnclosedBy;
42+
private StringValue fieldsEscapedBy;
43+
private StringValue linesStartingBy;
44+
private StringValue linesTerminatedBy;
45+
46+
/** The spelling of the fields clause; null prints FIELDS when a subclause is set. */
47+
public FieldsKeyword getFieldsKeyword() {
48+
return fieldsKeyword;
49+
}
50+
51+
public void setFieldsKeyword(FieldsKeyword fieldsKeyword) {
52+
this.fieldsKeyword = fieldsKeyword;
53+
}
54+
55+
public MySqlFileFormat withFieldsKeyword(FieldsKeyword fieldsKeyword) {
56+
setFieldsKeyword(fieldsKeyword);
57+
return this;
58+
}
59+
60+
public StringValue getFieldsTerminatedBy() {
61+
return fieldsTerminatedBy;
62+
}
63+
64+
public void setFieldsTerminatedBy(StringValue fieldsTerminatedBy) {
65+
this.fieldsTerminatedBy = fieldsTerminatedBy;
66+
}
67+
68+
public MySqlFileFormat withFieldsTerminatedBy(StringValue fieldsTerminatedBy) {
69+
setFieldsTerminatedBy(fieldsTerminatedBy);
70+
return this;
71+
}
72+
73+
public boolean isFieldsOptionallyEnclosed() {
74+
return fieldsOptionallyEnclosed;
75+
}
76+
77+
public void setFieldsOptionallyEnclosed(boolean fieldsOptionallyEnclosed) {
78+
this.fieldsOptionallyEnclosed = fieldsOptionallyEnclosed;
79+
}
80+
81+
public MySqlFileFormat withFieldsOptionallyEnclosed(boolean fieldsOptionallyEnclosed) {
82+
setFieldsOptionallyEnclosed(fieldsOptionallyEnclosed);
83+
return this;
84+
}
85+
86+
public StringValue getFieldsEnclosedBy() {
87+
return fieldsEnclosedBy;
88+
}
89+
90+
public void setFieldsEnclosedBy(StringValue fieldsEnclosedBy) {
91+
this.fieldsEnclosedBy = fieldsEnclosedBy;
92+
}
93+
94+
public MySqlFileFormat withFieldsEnclosedBy(StringValue fieldsEnclosedBy) {
95+
setFieldsEnclosedBy(fieldsEnclosedBy);
96+
return this;
97+
}
98+
99+
public StringValue getFieldsEscapedBy() {
100+
return fieldsEscapedBy;
101+
}
102+
103+
public void setFieldsEscapedBy(StringValue fieldsEscapedBy) {
104+
this.fieldsEscapedBy = fieldsEscapedBy;
105+
}
106+
107+
public MySqlFileFormat withFieldsEscapedBy(StringValue fieldsEscapedBy) {
108+
setFieldsEscapedBy(fieldsEscapedBy);
109+
return this;
110+
}
111+
112+
public StringValue getLinesStartingBy() {
113+
return linesStartingBy;
114+
}
115+
116+
public void setLinesStartingBy(StringValue linesStartingBy) {
117+
this.linesStartingBy = linesStartingBy;
118+
}
119+
120+
public MySqlFileFormat withLinesStartingBy(StringValue linesStartingBy) {
121+
setLinesStartingBy(linesStartingBy);
122+
return this;
123+
}
124+
125+
public StringValue getLinesTerminatedBy() {
126+
return linesTerminatedBy;
127+
}
128+
129+
public void setLinesTerminatedBy(StringValue linesTerminatedBy) {
130+
this.linesTerminatedBy = linesTerminatedBy;
131+
}
132+
133+
public MySqlFileFormat withLinesTerminatedBy(StringValue linesTerminatedBy) {
134+
setLinesTerminatedBy(linesTerminatedBy);
135+
return this;
136+
}
137+
138+
/** Whether the format prints a fields clause: a spelling or a subclause is set. */
139+
public boolean hasFieldsClause() {
140+
return fieldsKeyword != null || fieldsTerminatedBy != null || fieldsEnclosedBy != null
141+
|| fieldsEscapedBy != null;
142+
}
143+
144+
/** Whether the format prints a lines clause: a subclause is set. */
145+
public boolean hasLinesClause() {
146+
return linesStartingBy != null || linesTerminatedBy != null;
147+
}
148+
149+
/** Whether the format prints nothing. */
150+
public boolean isEmpty() {
151+
return !hasFieldsClause() && !hasLinesClause();
152+
}
153+
154+
/** Appends the fields clause and then the lines clause, separated by a space. */
155+
public StringBuilder appendTo(StringBuilder builder) {
156+
if (hasFieldsClause()) {
157+
appendFieldsClause(builder);
158+
if (hasLinesClause()) {
159+
builder.append(' ');
160+
}
161+
}
162+
if (hasLinesClause()) {
163+
appendLinesClause(builder);
164+
}
165+
return builder;
166+
}
167+
168+
private void appendFieldsClause(StringBuilder builder) {
169+
builder.append(fieldsKeyword != null ? fieldsKeyword : FieldsKeyword.FIELDS);
170+
if (fieldsTerminatedBy != null) {
171+
builder.append(" TERMINATED BY ").append(fieldsTerminatedBy);
172+
}
173+
if (fieldsEnclosedBy != null) {
174+
builder.append(fieldsOptionallyEnclosed ? " OPTIONALLY ENCLOSED BY " : " ENCLOSED BY ")
175+
.append(fieldsEnclosedBy);
176+
}
177+
if (fieldsEscapedBy != null) {
178+
builder.append(" ESCAPED BY ").append(fieldsEscapedBy);
179+
}
180+
}
181+
182+
private void appendLinesClause(StringBuilder builder) {
183+
builder.append("LINES");
184+
if (linesStartingBy != null) {
185+
builder.append(" STARTING BY ").append(linesStartingBy);
186+
}
187+
if (linesTerminatedBy != null) {
188+
builder.append(" TERMINATED BY ").append(linesTerminatedBy);
189+
}
190+
}
191+
192+
@Override
193+
public String toString() {
194+
return appendTo(new StringBuilder()).toString();
195+
}
196+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import net.sf.jsqlparser.statement.insert.Insert;
113113
import net.sf.jsqlparser.statement.insert.InsertBulk;
114114
import net.sf.jsqlparser.statement.load.LoadDataStatement;
115+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
115116
import net.sf.jsqlparser.statement.admin.FlushStatement;
116117
import net.sf.jsqlparser.statement.admin.KillStatement;
117118
import net.sf.jsqlparser.statement.handler.HandlerStatement;
@@ -710,6 +711,13 @@ public <S> Void visit(LoadDataStatement loadDataStatement, S context) {
710711
return null;
711712
}
712713

714+
@Override
715+
public <S> Void visit(MySqlLoadDataStatement loadData, S context) {
716+
analysis.claimTopLevel();
717+
analysis.certain(StmtFeature.MODIFIES_DATA);
718+
return super.visit(loadData, context);
719+
}
720+
713721
@Override
714722
public <S> Void visit(PurgeStatement purgeStatement, S context) {
715723
analysis.claimTopLevel();

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
136136
import net.sf.jsqlparser.statement.export.ExportDataStatement;
137137
import net.sf.jsqlparser.statement.load.LoadDataStatement;
138+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
138139

139140
public interface StatementVisitor<T> {
140141

@@ -762,6 +763,14 @@ default void visit(LoadDataStatement loadDataStatement) {
762763
this.visit(loadDataStatement, null);
763764
}
764765

766+
default <S> T visit(MySqlLoadDataStatement loadData, S context) {
767+
return null;
768+
}
769+
770+
default void visit(MySqlLoadDataStatement loadData) {
771+
this.visit(loadData, null);
772+
}
773+
765774
<S> T visit(PurgeStatement purgeStatement, S context);
766775

767776
default void visit(PurgeStatement purgeStatement) {

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
146146
import net.sf.jsqlparser.statement.export.ExportDataStatement;
147147
import net.sf.jsqlparser.statement.load.LoadDataStatement;
148+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
148149

149150
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
150151
public class StatementVisitorAdapter<T> implements StatementVisitor<T> {
@@ -944,6 +945,14 @@ public <S> T visit(LoadDataStatement loadDataStatement, S context) {
944945
return null;
945946
}
946947

948+
@Override
949+
public <S> T visit(MySqlLoadDataStatement loadData, S context) {
950+
fromItemVisitor.visitFromItem(loadData.getTable(), context);
951+
expressionVisitor.visitExpressions(loadData.getColumns(), context);
952+
expressionVisitor.visitUpdateSets(loadData.getUpdateSets(), context);
953+
return null;
954+
}
955+
947956
@Override
948957
public <S> T visit(PurgeStatement purgeStatement, S context) {
949958
return null;

‎src/main/java/net/sf/jsqlparser/statement/load/LoadDataStatement.java‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package net.sf.jsqlparser.statement.load;
1111

12+
import net.sf.jsqlparser.expression.Expression;
1213
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
1314
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1415
import net.sf.jsqlparser.schema.Table;
@@ -18,6 +19,7 @@
1819
import net.sf.jsqlparser.statement.select.PlainSelect;
1920

2021
import java.util.List;
22+
import java.util.function.Consumer;
2123

2224
/**
2325
* BigQuery's {@code LOAD DATA {INTO | OVERWRITE} table [(column_definitions)] [OPTIONS
@@ -128,14 +130,19 @@ public LoadDataStatement withConnectionName(String connectionName) {
128130
}
129131

130132
public StringBuilder appendTo(StringBuilder builder) {
133+
return appendTo(builder, builder::append);
134+
}
135+
136+
/** Renders the options and file options through the caller's expression visitor. */
137+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
131138
builder.append("LOAD DATA ").append(overwrite ? "OVERWRITE " : "INTO ").append(table);
132139
if (columnDefinitions != null && !columnDefinitions.isEmpty()) {
133140
builder.append(" ").append(PlainSelect.getStringList(columnDefinitions, true, true));
134141
}
135142
if (options != null) {
136-
builder.append(" OPTIONS (").append(options).append(")");
143+
appendOptions(builder.append(" OPTIONS ("), options, expressionRenderer).append(")");
137144
}
138-
builder.append(" FROM FILES (").append(fileOptions).append(")");
145+
appendOptions(builder.append(" FROM FILES ("), fileOptions, expressionRenderer).append(")");
139146
if (withPartitionColumns) {
140147
builder.append(" WITH PARTITION COLUMNS");
141148
}
@@ -145,6 +152,19 @@ public StringBuilder appendTo(StringBuilder builder) {
145152
return builder;
146153
}
147154

155+
private static StringBuilder appendOptions(StringBuilder builder, ExpressionList<?> options,
156+
Consumer<Expression> expressionRenderer) {
157+
if (options != null) {
158+
for (int i = 0; i < options.size(); i++) {
159+
if (i > 0) {
160+
builder.append(", ");
161+
}
162+
expressionRenderer.accept(options.get(i));
163+
}
164+
}
165+
return builder;
166+
}
167+
148168
@Override
149169
public String toString() {
150170
return appendTo(new StringBuilder()).toString();

0 commit comments

Comments
 (0)