Skip to content

Commit e7a09cf

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 a30a679 commit e7a09cf

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
@@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
150150
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151151
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
152152
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
153+
| **MySQL data loading** | `LOAD DATA` · `LOAD XML` · `SELECT … INTO OUTFILE` with the same `FIELDS` and `LINES` clauses |
153154
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
154155
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
155156
| **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
@@ -1153,10 +1153,12 @@ public enum Feature {
11531153
export,
11541154

11551155
/**
1156-
* PostgreSQL and DuckDB "COPY" and BigQuery "LOAD DATA" statements are allowed
1156+
* PostgreSQL and DuckDB "COPY", BigQuery and MySQL "LOAD DATA" and MySQL "LOAD XML" statements
1157+
* are allowed
11571158
*
11581159
* @see net.sf.jsqlparser.statement.CopyStatement
11591160
* @see net.sf.jsqlparser.statement.load.LoadDataStatement
1161+
* @see net.sf.jsqlparser.statement.load.MySqlLoadDataStatement
11601162
*/
11611163
copy, loadData,
11621164

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;
@@ -680,6 +681,13 @@ public <S> Void visit(LoadDataStatement loadDataStatement, S context) {
680681
return null;
681682
}
682683

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

117118
public interface StatementVisitor<T> {
118119

@@ -660,6 +661,14 @@ default void visit(LoadDataStatement loadDataStatement) {
660661
this.visit(loadDataStatement, null);
661662
}
662663

664+
default <S> T visit(MySqlLoadDataStatement loadData, S context) {
665+
return null;
666+
}
667+
668+
default void visit(MySqlLoadDataStatement loadData) {
669+
this.visit(loadData, null);
670+
}
671+
663672
<S> T visit(PurgeStatement purgeStatement, S context);
664673

665674
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
@@ -130,6 +130,7 @@
130130
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
131131
import net.sf.jsqlparser.statement.export.ExportDataStatement;
132132
import net.sf.jsqlparser.statement.load.LoadDataStatement;
133+
import net.sf.jsqlparser.statement.load.MySqlLoadDataStatement;
133134

134135
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
135136
public class StatementVisitorAdapter<T> implements StatementVisitor<T> {
@@ -850,6 +851,14 @@ public <S> T visit(LoadDataStatement loadDataStatement, S context) {
850851
return null;
851852
}
852853

854+
@Override
855+
public <S> T visit(MySqlLoadDataStatement loadData, S context) {
856+
fromItemVisitor.visitFromItem(loadData.getTable(), context);
857+
expressionVisitor.visitExpressions(loadData.getColumns(), context);
858+
expressionVisitor.visitUpdateSets(loadData.getUpdateSets(), context);
859+
return null;
860+
}
861+
853862
@Override
854863
public <S> T visit(PurgeStatement purgeStatement, S context) {
855864
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)