Skip to content

Commit 8950776

Browse files
hayssamsclaude
andcommitted
Support DuckDB COPY statement
Adds COPY table [(columns)] FROM path [(options)] and COPY {table | (query)} TO path [(options)]. Options such as FORMAT PARQUET or AUTO_DETECT true are free-form keywords, so they are collected verbatim. COPY stays valid as MySQL's ALGORITHM value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent 4ae2499 commit 8950776

9 files changed

Lines changed: 310 additions & 16 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 net.sf.jsqlparser.expression.operators.relational.ExpressionList;
13+
import net.sf.jsqlparser.schema.Column;
14+
import net.sf.jsqlparser.schema.Table;
15+
import net.sf.jsqlparser.statement.select.PlainSelect;
16+
import net.sf.jsqlparser.statement.select.Select;
17+
18+
import java.util.List;
19+
20+
/**
21+
* {@code COPY table [(columns)] FROM path [(options)]} and
22+
* {@code COPY {table | (query)} TO path [(options)]}, DuckDB's bulk import and export statement.
23+
*
24+
* @see <a href="https://duckdb.org/docs/stable/sql/statements/copy">COPY</a>
25+
*/
26+
public class CopyStatement implements Statement {
27+
private Table table;
28+
private ExpressionList<Column> columns;
29+
private Select select;
30+
private boolean from;
31+
private String path;
32+
private List<String> options;
33+
34+
public Table getTable() {
35+
return table;
36+
}
37+
38+
public void setTable(Table table) {
39+
this.table = table;
40+
}
41+
42+
public CopyStatement withTable(Table table) {
43+
setTable(table);
44+
return this;
45+
}
46+
47+
public ExpressionList<Column> getColumns() {
48+
return columns;
49+
}
50+
51+
public void setColumns(ExpressionList<Column> columns) {
52+
this.columns = columns;
53+
}
54+
55+
public CopyStatement withColumns(ExpressionList<Column> columns) {
56+
setColumns(columns);
57+
return this;
58+
}
59+
60+
public Select getSelect() {
61+
return select;
62+
}
63+
64+
public void setSelect(Select select) {
65+
this.select = select;
66+
}
67+
68+
public CopyStatement withSelect(Select select) {
69+
setSelect(select);
70+
return this;
71+
}
72+
73+
/** {@code true} for COPY ... FROM (import), {@code false} for COPY ... TO (export). */
74+
public boolean isFrom() {
75+
return from;
76+
}
77+
78+
public void setFrom(boolean from) {
79+
this.from = from;
80+
}
81+
82+
public CopyStatement withFrom(boolean from) {
83+
setFrom(from);
84+
return this;
85+
}
86+
87+
public String getPath() {
88+
return path;
89+
}
90+
91+
public void setPath(String path) {
92+
this.path = path;
93+
}
94+
95+
public CopyStatement withPath(String path) {
96+
setPath(path);
97+
return this;
98+
}
99+
100+
public List<String> getOptions() {
101+
return options;
102+
}
103+
104+
public void setOptions(List<String> options) {
105+
this.options = options;
106+
}
107+
108+
public CopyStatement withOptions(List<String> options) {
109+
setOptions(options);
110+
return this;
111+
}
112+
113+
public StringBuilder appendTo(StringBuilder builder) {
114+
builder.append("COPY ");
115+
if (select != null) {
116+
builder.append("(").append(select).append(")");
117+
} else {
118+
builder.append(table);
119+
if (columns != null && !columns.isEmpty()) {
120+
builder.append(" (").append(columns).append(")");
121+
}
122+
}
123+
builder.append(from ? " FROM " : " TO ").append(path);
124+
if (options != null && !options.isEmpty()) {
125+
builder.append(" ").append(PlainSelect.getStringList(options, true, true));
126+
}
127+
return builder;
128+
}
129+
130+
@Override
131+
public String toString() {
132+
return appendTo(new StringBuilder()).toString();
133+
}
134+
135+
@Override
136+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
137+
return statementVisitor.visit(this, context);
138+
}
139+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,13 @@ public <S> Void visit(DeallocateStatement deallocateStatement, S context) {
577577
return null;
578578
}
579579

580+
@Override
581+
public <S> Void visit(CopyStatement copyStatement, S context) {
582+
analysis.claimTopLevel();
583+
analysis.certain(StmtFeature.READS_DATA, StmtFeature.MODIFIES_DATA);
584+
return null;
585+
}
586+
580587
@Override
581588
public <S> Void visit(PurgeStatement purgeStatement, S context) {
582589
analysis.claimTopLevel();

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,12 @@ default void visit(DeallocateStatement deallocateStatement) {
419419
this.visit(deallocateStatement, null);
420420
}
421421

422+
<S> T visit(CopyStatement copyStatement, S context);
423+
424+
default void visit(CopyStatement copyStatement) {
425+
this.visit(copyStatement, null);
426+
}
427+
422428
<S> T visit(PurgeStatement purgeStatement, S context);
423429

424430
default void visit(PurgeStatement purgeStatement) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,11 @@ public <S> T visit(DeallocateStatement deallocateStatement, S context) {
658658
return null;
659659
}
660660

661+
@Override
662+
public <S> T visit(CopyStatement copyStatement, S context) {
663+
return null;
664+
}
665+
661666
@Override
662667
public <S> T visit(PurgeStatement purgeStatement, S context) {
663668
return null;

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@
217217
import net.sf.jsqlparser.statement.DisconnectStatement;
218218
import net.sf.jsqlparser.statement.PrepareStatement;
219219
import net.sf.jsqlparser.statement.DeallocateStatement;
220+
import net.sf.jsqlparser.statement.CopyStatement;
220221

221222

222223
/**
@@ -2564,6 +2565,21 @@ public void visit(DeallocateStatement deallocateStatement) {
25642565
StatementVisitor.super.visit(deallocateStatement);
25652566
}
25662567

2568+
@Override
2569+
public <S> Void visit(CopyStatement copyStatement, S context) {
2570+
if (copyStatement.getSelect() != null) {
2571+
copyStatement.getSelect().accept((SelectVisitor<?>) this, context);
2572+
} else if (copyStatement.getTable() != null) {
2573+
copyStatement.getTable().accept(this, context);
2574+
}
2575+
return null;
2576+
}
2577+
2578+
@Override
2579+
public void visit(CopyStatement copyStatement) {
2580+
StatementVisitor.super.visit(copyStatement);
2581+
}
2582+
25672583
@Override
25682584
public <S> Void visit(PurgeStatement purgeStatement, S context) {
25692585
if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) {

‎src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
import net.sf.jsqlparser.statement.DisconnectStatement;
106106
import net.sf.jsqlparser.statement.PrepareStatement;
107107
import net.sf.jsqlparser.statement.DeallocateStatement;
108+
import net.sf.jsqlparser.statement.CopyStatement;
108109

109110
public class StatementDeParser extends AbstractDeParser<Statement>
110111
implements StatementVisitor<StringBuilder> {
@@ -612,6 +613,12 @@ public <S> StringBuilder visit(DeallocateStatement deallocateStatement, S contex
612613
return builder;
613614
}
614615

616+
@Override
617+
public <S> StringBuilder visit(CopyStatement copyStatement, S context) {
618+
copyStatement.appendTo(builder);
619+
return builder;
620+
}
621+
615622
@Override
616623
public <S> StringBuilder visit(PurgeStatement purgeStatement, S context) {
617624
purgeStatement.appendTo(builder);

‎src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
import net.sf.jsqlparser.statement.DisconnectStatement;
108108
import net.sf.jsqlparser.statement.PrepareStatement;
109109
import net.sf.jsqlparser.statement.DeallocateStatement;
110+
import net.sf.jsqlparser.statement.CopyStatement;
110111

111112
/**
112113
* @author gitmotte
@@ -533,6 +534,12 @@ public <S> Void visit(DeallocateStatement deallocateStatement, S context) {
533534
return null;
534535
}
535536

537+
@Override
538+
public <S> Void visit(CopyStatement copyStatement, S context) {
539+
// TODO: not yet implemented
540+
return null;
541+
}
542+
536543
@Override
537544
public <S> Void visit(PurgeStatement purgeStatement, S context) {
538545
// TODO: not yet implemented
@@ -781,6 +788,10 @@ public void visit(DeallocateStatement deallocateStatement) {
781788
visit(deallocateStatement, null);
782789
}
783790

791+
public void visit(CopyStatement copyStatement) {
792+
visit(copyStatement, null);
793+
}
794+
784795
public void visit(PurgeStatement purgeStatement) {
785796
visit(purgeStatement, null);
786797
}

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

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,7 @@ String NonReservedWord() :
20032003
| tk=<K_COLUMN:"COLUMN">
20042004
| tk=<K_COLUMNS:"COLUMNS">
20052005
| tk=<K_COMMIT:"COMMIT">
2006+
| tk=<K_COPY: "COPY">
20062007
| tk=<K_COMMENT:"COMMENT">
20072008
| tk=<K_COMMENTS:"COMMENTS">
20082009
| tk=<K_COMPLETION:"COMPLETION">
@@ -2964,6 +2965,8 @@ Statement SingleStatement() :
29642965
|
29652966
LOOKAHEAD({ getToken(1).kind == K_DEALLOCATE }) stm = DeallocateStatement()
29662967
|
2968+
LOOKAHEAD({ getToken(1).kind == K_COPY }) stm = CopyStatement()
2969+
|
29672970
stm = PurgeStatement()
29682971
|
29692972
stm = SessionStatement()
@@ -4365,6 +4368,46 @@ RenameTableStatement RenameTableStatement(): {
43654368
}
43664369
}
43674370

4371+
/**
4372+
* DuckDB's COPY table [(columns)] FROM path [(options)] and
4373+
* COPY {table | (query)} TO path [(options)].
4374+
*/
4375+
CopyStatement CopyStatement() #CopyStatement:
4376+
{
4377+
CopyStatement copyStatement = new CopyStatement();
4378+
ExpressionList<Column> columns = null;
4379+
List<String> options = new ArrayList<String>();
4380+
String option;
4381+
Select select;
4382+
Table table;
4383+
Token token;
4384+
}
4385+
{
4386+
<K_COPY>
4387+
(
4388+
LOOKAHEAD(2) "(" select=Select() ")" { copyStatement.setSelect(select); }
4389+
|
4390+
table=Table() { copyStatement.setTable(table); }
4391+
[ LOOKAHEAD(2) "(" columns=ColumnList() ")" { copyStatement.setColumns(columns); } ]
4392+
)
4393+
(
4394+
<K_FROM> { copyStatement.setFrom(true); }
4395+
|
4396+
<K_TO>
4397+
)
4398+
(
4399+
token=<S_CHAR_LITERAL> { copyStatement.setPath(token.image); }
4400+
|
4401+
token=<S_IDENTIFIER> { copyStatement.setPath(token.image); }
4402+
)
4403+
[
4404+
LOOKAHEAD(2) "(" option=DatabaseOption() { options.add(option); }
4405+
( "," option=DatabaseOption() { options.add(option); } )*
4406+
")" { copyStatement.setOptions(options); }
4407+
]
4408+
{ return copyStatement; }
4409+
}
4410+
43684411
/**
43694412
* PREPARE name AS statement, storing a parameterised statement for later EXECUTE.
43704413
*/
@@ -4463,22 +4506,35 @@ AttachStatement AttachStatement() #AttachStatement:
44634506
{ return attachStatement; }
44644507
}
44654508

4466-
/** One ATTACH option, such as READ_ONLY or TYPE SQLITE. */
4467-
String DatabaseOption():
4468-
{
4509+
/**
4510+
* One ATTACH or COPY option, such as READ_ONLY, TYPE SQLITE or FORMAT PARQUET. Option names and
4511+
* values are free-form keywords, so the tokens are collected verbatim up to the next comma or the
4512+
* closing bracket.
4513+
*/
4514+
JAVACODE
4515+
String DatabaseOption() {
44694516
StringBuilder builder = new StringBuilder();
4470-
String name;
4471-
Token token;
4472-
}
4473-
{
4474-
(
4475-
name=RelObjectName() { if (builder.length() > 0) builder.append(" "); builder.append(name); }
4476-
|
4477-
token=<S_CHAR_LITERAL> { if (builder.length() > 0) builder.append(" "); builder.append(token.image); }
4478-
|
4479-
token=<S_LONG> { if (builder.length() > 0) builder.append(" "); builder.append(token.image); }
4480-
)+
4481-
{ return builder.toString(); }
4517+
int depth = 0;
4518+
while (true) {
4519+
Token token = getToken(1);
4520+
if (token == null || token.kind == EOF) {
4521+
break;
4522+
}
4523+
if (depth == 0 && (",".equals(token.image) || ")".equals(token.image))) {
4524+
break;
4525+
}
4526+
if ("(".equals(token.image)) {
4527+
depth++;
4528+
} else if (")".equals(token.image)) {
4529+
depth--;
4530+
}
4531+
getNextToken();
4532+
if (builder.length() > 0) {
4533+
builder.append(" ");
4534+
}
4535+
builder.append(token.image);
4536+
}
4537+
return builder.toString();
44824538
}
44834539

44844540
/**
@@ -12890,7 +12946,9 @@ List<String> IndexAlgorithmOrLockOption():
1289012946
{
1289112947
( keyword=<K_ALGORITHM> | keyword=<K_LOCK> ) { option.add(keyword.image); }
1289212948
[ "=" { option.add("="); } ]
12893-
( value=<K_DEFAULT> | value=<K_NONE> | value=<K_EXCLUSIVE> | value=<S_IDENTIFIER> )
12949+
// COPY is one of MySQL's ALGORITHM values and also starts the COPY statement
12950+
( value=<K_DEFAULT> | value=<K_NONE> | value=<K_EXCLUSIVE> | value=<K_COPY>
12951+
| value=<S_IDENTIFIER> )
1289412952
{ option.add(value.image); return option; }
1289512953
}
1289612954

0 commit comments

Comments
 (0)