Skip to content

Commit c945986

Browse files
hayssamsclaude
andauthored
Support DuckDB syntax: FROM-first, SEMI/ANTI joins, COPY, ATTACH, MACRO, PRAGMA and DuckDB 2.0 additions (#2643)
* Support DuckDB GLOB pattern matching operator Adds GLOB as a LikeExpression keyword so `col GLOB 'pattern'` and `col NOT GLOB 'pattern'` parse and deparse. GLOB stays a non-reserved keyword and remains usable as an identifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support standalone SEMI and ANTI joins DuckDB allows SEMI/ANTI JOIN without a LEFT prefix, and ANTI as a join type at all. ANTI previously parsed silently as a table alias, producing a wrong AST from valid SQL. Adds Join.isAnti(), the LEFT/RIGHT/FULL SEMI|ANTI prefixes and the standalone form, keeping ANTI usable as an identifier outside a join prefix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB MAP(key, value) column type DuckDB parameterises a type with other types, e.g. MAP(VARCHAR, INTEGER). Type arguments previously accepted identifiers and literals but not data type tokens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB INSERT ... BY NAME / BY POSITION DuckDB matches an INSERT's source columns either positionally or by name. BY previously bound as a table alias, swallowing the clause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB USING SAMPLE sizes and methods DuckDB writes the sample size first and the method behind it: USING SAMPLE 10%, USING SAMPLE 10 PERCENT (bernoulli), USING SAMPLE 10 ROWS (system, 377), USING SAMPLE reservoir (50 ROWS). The method is now optional, RESERVOIR is a known method, and a bare sample size accepts a unit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB DESCRIBE <query> DESCRIBE now accepts a query as well as a table, so DESCRIBE SELECT ..., DESCRIBE FROM ... and DESCRIBE WITH ... parse. The described query is reachable from TablesNamesFinder and rendered by the deparser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support PRAGMA statement Adds PRAGMA name, PRAGMA name(arguments) and PRAGMA name = value, as used by DuckDB and SQLite to read and set database settings. PRAGMA stays a non-reserved keyword. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB INSTALL and LOAD extension statements Adds [FORCE] INSTALL extension [FROM repository] and LOAD extension, where the extension and repository may be an identifier or a string literal. INSTALL and LOAD stay non-reserved keywords. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB ATTACH and DETACH statements Adds ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)] and DETACH [DATABASE] [IF EXISTS] name, with options such as READ_ONLY or TYPE SQLITE kept as strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB 2.0 CONNECT and DISCONNECT statements Adds CONNECT target [AS alias] and DISCONNECT [name], which route a session's queries to a remote database. Oracle's CONNECT BY is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support PREPARE and DEALLOCATE statements Adds PREPARE name AS statement and DEALLOCATE [PREPARE] name. EXECUTE of a prepared statement already parsed. The prepared statement is a nested Statement, so TablesNamesFinder reaches its tables. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * 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 * Support DuckDB CREATE MACRO Adds CREATE [OR REPLACE] [TEMPORARY] MACRO name (parameters) AS body for both scalar macros and table macros (AS TABLE SELECT ...). Parameters may carry a default value, as in add(a, b := 5). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB 2.0 CREATE EXTENSION REPOSITORY Adds CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix], which registers a custom repository to install extensions from. PostgreSQL's CREATE EXTENSION is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB 2.0 triggers with transition tables and a statement body DuckDB writes CREATE TRIGGER ... REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT <statement>, i.e. the standard header with a SQL statement instead of EXECUTE FUNCTION. The standard-form parser now accepts a statement body, and such triggers are recognised and rendered as standard rather than MySQL-style triggers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB FROM-first queries DuckDB lets the FROM clause lead, with the SELECT clause optional: FROM tbl SELECT a and FROM tbl WHERE a > 1. PlainSelect records the order in a fromFirst flag, and both the AST's own rendering and SelectDeParser emit the clauses accordingly. Bare FROM queries and the pipe syntax are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB UNPIVOT statement Adds the simplified statement form UNPIVOT tbl ON columns [INTO NAME name VALUE values] as UnPivotQuery, the counterpart of the existing PivotQuery. The FROM-clause form UNPIVOT (...) is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB 2.0 APPROX NEAREST similarity joins Adds JOIN t APPROX NEAREST n BY SIMILARITY expression, the vector similarity join of DuckDB 2.0. APPROX no longer binds as a table alias in that position. Also adds the lookahead the UNPIVOT statement's INTO clause needed to keep the grammar free of choice conflicts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * Support DuckDB 2.0 recursive CTEs with USING KEY Adds WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...), which aggregates a recursive CTE by key instead of accumulating every row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 * fix: restore lines dropped during conflict resolution and fix LOAD DATA lookahead The merge of master resolved 26 conflict hunks by concatenating both sides while dropping the closing braces, @OverRide annotations and return statements that git had hoisted out of the conflict markers, leaving 6 visitor/deparser classes and the grammar unparseable. Also exclude LOAD DATA from the ExtensionStatement lookahead so BigQuery's LOAD DATA statement is not swallowed by DuckDB's LOAD extension alternative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dec8f5d commit c945986

38 files changed

Lines changed: 3454 additions & 164 deletions

‎src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public LikeExpression withRightExpression(Expression arg0) {
119119
}
120120

121121
public enum KeyWord {
122-
LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP;
122+
LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, GLOB, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP;
123123

124124
public static KeyWord from(String keyword) {
125125
return Enum.valueOf(KeyWord.class,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ public enum Feature {
186186
* "SEMI" join
187187
*/
188188
joinSemi,
189+
/**
190+
* @see net.sf.jsqlparser.statement.select.Join#isAnti()
191+
*/
192+
joinAnti,
189193
/**
190194
* "INNER" join
191195
*/
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.statement.select.PlainSelect;
13+
14+
import java.util.List;
15+
16+
/**
17+
* DuckDB's {@code ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)]}, which adds
18+
* another database file or remote database to the session.
19+
*
20+
* @see <a href="https://duckdb.org/docs/stable/sql/statements/attach">ATTACH</a>
21+
*/
22+
public class AttachStatement implements Statement {
23+
private String databasePath;
24+
private String alias;
25+
private boolean usingDatabaseKeyword;
26+
private boolean ifNotExists;
27+
private List<String> options;
28+
29+
public String getDatabasePath() {
30+
return databasePath;
31+
}
32+
33+
public void setDatabasePath(String databasePath) {
34+
this.databasePath = databasePath;
35+
}
36+
37+
public AttachStatement withDatabasePath(String databasePath) {
38+
setDatabasePath(databasePath);
39+
return this;
40+
}
41+
42+
public String getAlias() {
43+
return alias;
44+
}
45+
46+
public void setAlias(String alias) {
47+
this.alias = alias;
48+
}
49+
50+
public AttachStatement withAlias(String alias) {
51+
setAlias(alias);
52+
return this;
53+
}
54+
55+
public boolean isUsingDatabaseKeyword() {
56+
return usingDatabaseKeyword;
57+
}
58+
59+
public void setUsingDatabaseKeyword(boolean usingDatabaseKeyword) {
60+
this.usingDatabaseKeyword = usingDatabaseKeyword;
61+
}
62+
63+
public AttachStatement withUsingDatabaseKeyword(boolean usingDatabaseKeyword) {
64+
setUsingDatabaseKeyword(usingDatabaseKeyword);
65+
return this;
66+
}
67+
68+
public boolean isIfNotExists() {
69+
return ifNotExists;
70+
}
71+
72+
public void setIfNotExists(boolean ifNotExists) {
73+
this.ifNotExists = ifNotExists;
74+
}
75+
76+
public AttachStatement withIfNotExists(boolean ifNotExists) {
77+
setIfNotExists(ifNotExists);
78+
return this;
79+
}
80+
81+
public List<String> getOptions() {
82+
return options;
83+
}
84+
85+
public void setOptions(List<String> options) {
86+
this.options = options;
87+
}
88+
89+
public AttachStatement withOptions(List<String> options) {
90+
setOptions(options);
91+
return this;
92+
}
93+
94+
public StringBuilder appendTo(StringBuilder builder) {
95+
builder.append("ATTACH ");
96+
if (usingDatabaseKeyword) {
97+
builder.append("DATABASE ");
98+
}
99+
if (ifNotExists) {
100+
builder.append("IF NOT EXISTS ");
101+
}
102+
builder.append(databasePath);
103+
if (alias != null) {
104+
builder.append(" AS ").append(alias);
105+
}
106+
if (options != null && !options.isEmpty()) {
107+
builder.append(" ").append(PlainSelect.getStringList(options, true, true));
108+
}
109+
return builder;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return appendTo(new StringBuilder()).toString();
115+
}
116+
117+
@Override
118+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
119+
return statementVisitor.visit(this, context);
120+
}
121+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
/**
13+
* DuckDB's {@code CONNECT target [AS alias]}, which routes the session's queries to a remote
14+
* database.
15+
*/
16+
public class ConnectStatement implements Statement {
17+
private String target;
18+
private String alias;
19+
20+
public ConnectStatement() {}
21+
22+
public ConnectStatement(String target) {
23+
this.target = target;
24+
}
25+
26+
public String getTarget() {
27+
return target;
28+
}
29+
30+
public void setTarget(String target) {
31+
this.target = target;
32+
}
33+
34+
public ConnectStatement withTarget(String target) {
35+
setTarget(target);
36+
return this;
37+
}
38+
39+
public String getAlias() {
40+
return alias;
41+
}
42+
43+
public void setAlias(String alias) {
44+
this.alias = alias;
45+
}
46+
47+
public ConnectStatement withAlias(String alias) {
48+
setAlias(alias);
49+
return this;
50+
}
51+
52+
public StringBuilder appendTo(StringBuilder builder) {
53+
builder.append("CONNECT ").append(target);
54+
if (alias != null) {
55+
builder.append(" AS ").append(alias);
56+
}
57+
return builder;
58+
}
59+
60+
@Override
61+
public String toString() {
62+
return appendTo(new StringBuilder()).toString();
63+
}
64+
65+
@Override
66+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
67+
return statementVisitor.visit(this, context);
68+
}
69+
}
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+
}

0 commit comments

Comments
 (0)