Skip to content

Commit 27e9abc

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 e27929f commit 27e9abc

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
@@ -152,6 +152,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
152152
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
153153
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
154154
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
155+
| **MySQL data loading** | `LOAD DATA` · `LOAD XML` · `SELECT … INTO OUTFILE` with the same `FIELDS` and `LINES` clauses |
155156
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
156157
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
157158
| **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
@@ -1179,10 +1179,12 @@ public enum Feature {
11791179
export,
11801180

11811181
/**
1182-
* PostgreSQL and DuckDB "COPY" and BigQuery "LOAD DATA" statements are allowed
1182+
* PostgreSQL and DuckDB "COPY", BigQuery and MySQL "LOAD DATA" and MySQL "LOAD XML" statements
1183+
* are allowed
11831184
*
11841185
* @see net.sf.jsqlparser.statement.CopyStatement
11851186
* @see net.sf.jsqlparser.statement.load.LoadDataStatement
1187+
* @see net.sf.jsqlparser.statement.load.MySqlLoadDataStatement
11861188
*/
11871189
copy, loadData,
11881190

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
@@ -98,6 +98,7 @@
9898
import net.sf.jsqlparser.statement.insert.Insert;
9999
import net.sf.jsqlparser.statement.insert.InsertBulk;
100100
import net.sf.jsqlparser.statement.load.LoadDataStatement;
101+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
101102
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
102103
import net.sf.jsqlparser.statement.lock.LockStatement;
103104
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -681,6 +682,13 @@ public <S> Void visit(LoadDataStatement loadDataStatement, S context) {
681682
return null;
682683
}
683684

685+
@Override
686+
public <S> Void visit(MySqlLoadDataStatement loadData, S context) {
687+
analysis.claimTopLevel();
688+
analysis.certain(StmtFeature.MODIFIES_DATA);
689+
return super.visit(loadData, context);
690+
}
691+
684692
@Override
685693
public <S> Void visit(PurgeStatement purgeStatement, S context) {
686694
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
@@ -114,6 +114,7 @@
114114
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
115115
import net.sf.jsqlparser.statement.export.ExportDataStatement;
116116
import net.sf.jsqlparser.statement.load.LoadDataStatement;
117+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
117118

118119
public interface StatementVisitor<T> {
119120

@@ -677,6 +678,14 @@ default void visit(LoadDataStatement loadDataStatement) {
677678
this.visit(loadDataStatement, null);
678679
}
679680

681+
default <S> T visit(MySqlLoadDataStatement loadData, S context) {
682+
return null;
683+
}
684+
685+
default void visit(MySqlLoadDataStatement loadData) {
686+
this.visit(loadData, null);
687+
}
688+
680689
<S> T visit(PurgeStatement purgeStatement, S context);
681690

682691
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
@@ -132,6 +132,7 @@
132132
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
133133
import net.sf.jsqlparser.statement.export.ExportDataStatement;
134134
import net.sf.jsqlparser.statement.load.LoadDataStatement;
135+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
135136

136137
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
137138
public class StatementVisitorAdapter<T> implements StatementVisitor<T> {
@@ -868,6 +869,14 @@ public <S> T visit(LoadDataStatement loadDataStatement, S context) {
868869
return null;
869870
}
870871

872+
@Override
873+
public <S> T visit(MySqlLoadDataStatement loadData, S context) {
874+
fromItemVisitor.visitFromItem(loadData.getTable(), context);
875+
expressionVisitor.visitExpressions(loadData.getColumns(), context);
876+
expressionVisitor.visitUpdateSets(loadData.getUpdateSets(), context);
877+
return null;
878+
}
879+
871880
@Override
872881
public <S> T visit(PurgeStatement purgeStatement, S context) {
873882
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)