Skip to content

Commit f49e3f3

Browse files
committed
feat: parse MySQL LOAD DATA and LOAD XML
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 cf057dd commit f49e3f3

23 files changed

Lines changed: 1748 additions & 180 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
154154
| **MySQL resource groups** | `CREATE`/`ALTER`/`DROP RESOURCE GROUP` · `SET RESOURCE GROUP` |
155155
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
156156
| **MySQL server administration** | `FLUSH` · `KILL` · `RESET PERSIST` · `HANDLER … OPEN`/`READ`/`CLOSE` |
157+
| **MySQL data loading** | `LOAD DATA` · `LOAD XML` · `SELECT … INTO OUTFILE` with the same `FIELDS` and `LINES` clauses |
157158
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
158159
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
159160
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |

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

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

12431243
/**
1244-
* PostgreSQL and DuckDB "COPY" and BigQuery "LOAD DATA" statements are allowed
1244+
* PostgreSQL and DuckDB "COPY", BigQuery and MySQL "LOAD DATA" and MySQL "LOAD XML" statements
1245+
* are allowed
12451246
*
12461247
* @see net.sf.jsqlparser.statement.CopyStatement
12471248
* @see net.sf.jsqlparser.statement.load.LoadDataStatement
1249+
* @see net.sf.jsqlparser.statement.load.MySqlLoadDataStatement
12481250
*/
12491251
copy, loadData,
12501252

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
@@ -99,6 +99,7 @@
9999
import net.sf.jsqlparser.statement.insert.Insert;
100100
import net.sf.jsqlparser.statement.insert.InsertBulk;
101101
import net.sf.jsqlparser.statement.load.LoadDataStatement;
102+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
102103
import net.sf.jsqlparser.statement.admin.FlushStatement;
103104
import net.sf.jsqlparser.statement.admin.KillStatement;
104105
import net.sf.jsqlparser.statement.handler.HandlerStatement;
@@ -688,6 +689,13 @@ public <S> Void visit(LoadDataStatement loadDataStatement, S context) {
688689
return null;
689690
}
690691

692+
@Override
693+
public <S> Void visit(MySqlLoadDataStatement loadData, S context) {
694+
analysis.claimTopLevel();
695+
analysis.certain(StmtFeature.MODIFIES_DATA);
696+
return super.visit(loadData, context);
697+
}
698+
691699
@Override
692700
public <S> Void visit(PurgeStatement purgeStatement, S context) {
693701
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
@@ -121,6 +121,7 @@
121121
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
122122
import net.sf.jsqlparser.statement.export.ExportDataStatement;
123123
import net.sf.jsqlparser.statement.load.LoadDataStatement;
124+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
124125

125126
public interface StatementVisitor<T> {
126127

@@ -732,6 +733,14 @@ default void visit(LoadDataStatement loadDataStatement) {
732733
this.visit(loadDataStatement, null);
733734
}
734735

736+
default <S> T visit(MySqlLoadDataStatement loadData, S context) {
737+
return null;
738+
}
739+
740+
default void visit(MySqlLoadDataStatement loadData) {
741+
this.visit(loadData, null);
742+
}
743+
735744
<S> T visit(PurgeStatement purgeStatement, S context);
736745

737746
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
@@ -139,6 +139,7 @@
139139
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
140140
import net.sf.jsqlparser.statement.export.ExportDataStatement;
141141
import net.sf.jsqlparser.statement.load.LoadDataStatement;
142+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
142143

143144
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
144145
public class StatementVisitorAdapter<T> implements StatementVisitor<T> {
@@ -910,6 +911,14 @@ public <S> T visit(LoadDataStatement loadDataStatement, S context) {
910911
return null;
911912
}
912913

914+
@Override
915+
public <S> T visit(MySqlLoadDataStatement loadData, S context) {
916+
fromItemVisitor.visitFromItem(loadData.getTable(), context);
917+
expressionVisitor.visitExpressions(loadData.getColumns(), context);
918+
expressionVisitor.visitUpdateSets(loadData.getUpdateSets(), context);
919+
return null;
920+
}
921+
913922
@Override
914923
public <S> T visit(PurgeStatement purgeStatement, S context) {
915924
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)