Skip to content

Commit 7d4eac7

Browse files
hayssamsclaude
andcommitted
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
1 parent d0b0642 commit 7d4eac7

9 files changed

Lines changed: 249 additions & 7 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 extension management statements {@code [FORCE] INSTALL extension [FROM repository]} and
14+
* {@code LOAD extension}.
15+
*
16+
* @see <a href="https://duckdb.org/docs/stable/extensions/overview">Extensions</a>
17+
*/
18+
public class ExtensionStatement implements Statement {
19+
private Operation operation;
20+
private boolean force;
21+
private String extensionName;
22+
private String repository;
23+
24+
public ExtensionStatement() {}
25+
26+
public ExtensionStatement(Operation operation, String extensionName) {
27+
this.operation = operation;
28+
this.extensionName = extensionName;
29+
}
30+
31+
public Operation getOperation() {
32+
return operation;
33+
}
34+
35+
public void setOperation(Operation operation) {
36+
this.operation = operation;
37+
}
38+
39+
public ExtensionStatement withOperation(Operation operation) {
40+
setOperation(operation);
41+
return this;
42+
}
43+
44+
public boolean isForce() {
45+
return force;
46+
}
47+
48+
public void setForce(boolean force) {
49+
this.force = force;
50+
}
51+
52+
public ExtensionStatement withForce(boolean force) {
53+
setForce(force);
54+
return this;
55+
}
56+
57+
public String getExtensionName() {
58+
return extensionName;
59+
}
60+
61+
public void setExtensionName(String extensionName) {
62+
this.extensionName = extensionName;
63+
}
64+
65+
public ExtensionStatement withExtensionName(String extensionName) {
66+
setExtensionName(extensionName);
67+
return this;
68+
}
69+
70+
public String getRepository() {
71+
return repository;
72+
}
73+
74+
public void setRepository(String repository) {
75+
this.repository = repository;
76+
}
77+
78+
public ExtensionStatement withRepository(String repository) {
79+
setRepository(repository);
80+
return this;
81+
}
82+
83+
public StringBuilder appendTo(StringBuilder builder) {
84+
if (force) {
85+
builder.append("FORCE ");
86+
}
87+
builder.append(operation).append(" ").append(extensionName);
88+
if (repository != null) {
89+
builder.append(" FROM ").append(repository);
90+
}
91+
return builder;
92+
}
93+
94+
@Override
95+
public String toString() {
96+
return appendTo(new StringBuilder()).toString();
97+
}
98+
99+
@Override
100+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
101+
return statementVisitor.visit(this, context);
102+
}
103+
104+
public enum Operation {
105+
INSTALL, LOAD
106+
}
107+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,13 @@ public <S> Void visit(PragmaStatement pragmaStatement, S context) {
528528
return null;
529529
}
530530

531+
@Override
532+
public <S> Void visit(ExtensionStatement extensionStatement, S context) {
533+
analysis.claimTopLevel();
534+
analysis.certain(StmtFeature.MODIFIES_SESSION);
535+
return null;
536+
}
537+
531538
@Override
532539
public <S> Void visit(PurgeStatement purgeStatement, S context) {
533540
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
@@ -377,6 +377,12 @@ default void visit(PragmaStatement pragmaStatement) {
377377
this.visit(pragmaStatement, null);
378378
}
379379

380+
<S> T visit(ExtensionStatement extensionStatement, S context);
381+
382+
default void visit(ExtensionStatement extensionStatement) {
383+
this.visit(extensionStatement, null);
384+
}
385+
380386
<S> T visit(PurgeStatement purgeStatement, S context);
381387

382388
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
@@ -623,6 +623,11 @@ public <S> T visit(PragmaStatement pragmaStatement, S context) {
623623
return null;
624624
}
625625

626+
@Override
627+
public <S> T visit(ExtensionStatement extensionStatement, S context) {
628+
return null;
629+
}
630+
626631
@Override
627632
public <S> T visit(PurgeStatement purgeStatement, S context) {
628633
return null;

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
import net.sf.jsqlparser.statement.update.UpdateSet;
211211
import net.sf.jsqlparser.statement.upsert.Upsert;
212212
import net.sf.jsqlparser.statement.PragmaStatement;
213+
import net.sf.jsqlparser.statement.ExtensionStatement;
213214

214215

215216
/**
@@ -2480,6 +2481,17 @@ public void visit(PragmaStatement pragmaStatement) {
24802481
StatementVisitor.super.visit(pragmaStatement);
24812482
}
24822483

2484+
@Override
2485+
public <S> Void visit(ExtensionStatement extensionStatement, S context) {
2486+
// no tables involved in this statement
2487+
return null;
2488+
}
2489+
2490+
@Override
2491+
public void visit(ExtensionStatement extensionStatement) {
2492+
StatementVisitor.super.visit(extensionStatement);
2493+
}
2494+
24832495
@Override
24842496
public <S> Void visit(PurgeStatement purgeStatement, S context) {
24852497
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
@@ -98,6 +98,7 @@
9898
import net.sf.jsqlparser.statement.update.Update;
9999
import net.sf.jsqlparser.statement.upsert.Upsert;
100100
import net.sf.jsqlparser.statement.PragmaStatement;
101+
import net.sf.jsqlparser.statement.ExtensionStatement;
101102

102103
public class StatementDeParser extends AbstractDeParser<Statement>
103104
implements StatementVisitor<StringBuilder> {
@@ -563,6 +564,12 @@ public <S> StringBuilder visit(PragmaStatement pragmaStatement, S context) {
563564
return builder;
564565
}
565566

567+
@Override
568+
public <S> StringBuilder visit(ExtensionStatement extensionStatement, S context) {
569+
extensionStatement.appendTo(builder);
570+
return builder;
571+
}
572+
566573
@Override
567574
public <S> StringBuilder visit(PurgeStatement purgeStatement, S context) {
568575
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
@@ -100,6 +100,7 @@
100100
import net.sf.jsqlparser.util.validation.ValidationCapability;
101101
import net.sf.jsqlparser.util.validation.metadata.NamedObject;
102102
import net.sf.jsqlparser.statement.PragmaStatement;
103+
import net.sf.jsqlparser.statement.ExtensionStatement;
103104

104105
/**
105106
* @author gitmotte
@@ -484,6 +485,12 @@ public <S> Void visit(PragmaStatement pragmaStatement, S context) {
484485
return null;
485486
}
486487

488+
@Override
489+
public <S> Void visit(ExtensionStatement extensionStatement, S context) {
490+
// TODO: not yet implemented
491+
return null;
492+
}
493+
487494
@Override
488495
public <S> Void visit(PurgeStatement purgeStatement, S context) {
489496
// TODO: not yet implemented
@@ -704,6 +711,10 @@ public void visit(PragmaStatement pragmaStatement) {
704711
visit(pragmaStatement, null);
705712
}
706713

714+
public void visit(ExtensionStatement extensionStatement) {
715+
visit(extensionStatement, null);
716+
}
717+
707718
public void visit(PurgeStatement purgeStatement) {
708719
visit(purgeStatement, null);
709720
}

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

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,7 @@ String NonReservedWord() :
20992099
| tk=<K_INCLUDING:"INCLUDING">
21002100
| tk=<K_INCREMENT:"INCREMENT">
21012101
| tk=<K_INDEX: "INDEX">
2102+
| tk=<K_INSTALL: "INSTALL">
21022103
| tk=<K_INFORMATION: "INFORMATION">
21032104
| tk=<K_INSERT:"INSERT">
21042105
| tk=<K_INTERLEAVE: "INTERLEAVE">
@@ -2127,6 +2128,7 @@ String NonReservedWord() :
21272128
| tk=<K_LINEAR:"LINEAR">
21282129
| tk=<K_LINES:"LINES">
21292130
| tk=<K_LIST:"LIST">
2131+
| tk=<K_LOAD: "LOAD">
21302132
| tk=<K_LOCAL:"LOCAL">
21312133
| tk=<K_LOCK:"LOCK">
21322134
| tk=<K_LOCKED:"LOCKED">
@@ -2942,6 +2944,10 @@ Statement SingleStatement() :
29422944
|
29432945
LOOKAHEAD({ getToken(1).kind == K_PRAGMA }) stm = PragmaStatement()
29442946
|
2947+
LOOKAHEAD({ getToken(1).kind == K_INSTALL || getToken(1).kind == K_LOAD
2948+
|| (getToken(1).kind == K_FORCE && getToken(2).kind == K_INSTALL) })
2949+
stm = ExtensionStatement()
2950+
|
29452951
stm = PurgeStatement()
29462952
|
29472953
stm = SessionStatement()
@@ -4343,6 +4349,38 @@ RenameTableStatement RenameTableStatement(): {
43434349
}
43444350
}
43454351

4352+
/**
4353+
* DuckDB's [FORCE] INSTALL extension [FROM repository] and LOAD extension.
4354+
*/
4355+
ExtensionStatement ExtensionStatement() #ExtensionStatement:
4356+
{
4357+
ExtensionStatement extensionStatement = new ExtensionStatement();
4358+
ObjectNames name;
4359+
Token token;
4360+
}
4361+
{
4362+
[ <K_FORCE> { extensionStatement.setForce(true); } ]
4363+
(
4364+
<K_INSTALL> { extensionStatement.setOperation(ExtensionStatement.Operation.INSTALL); }
4365+
|
4366+
<K_LOAD> { extensionStatement.setOperation(ExtensionStatement.Operation.LOAD); }
4367+
)
4368+
(
4369+
token=<S_CHAR_LITERAL> { extensionStatement.setExtensionName(token.image); }
4370+
|
4371+
name=RelObjectNames() { extensionStatement.setExtensionName(String.join(".", name.getNames())); }
4372+
)
4373+
[
4374+
LOOKAHEAD(2) <K_FROM>
4375+
(
4376+
token=<S_CHAR_LITERAL> { extensionStatement.setRepository(token.image); }
4377+
|
4378+
name=RelObjectNames() { extensionStatement.setRepository(String.join(".", name.getNames())); }
4379+
)
4380+
]
4381+
{ return extensionStatement; }
4382+
}
4383+
43464384
/**
43474385
* DuckDB/SQLite PRAGMA name [(arguments) | = value].
43484386
*/
@@ -16428,14 +16466,20 @@ RollbackStatement RollbackStatement():
1642816466
{
1642916467
<K_ROLLBACK> { rollbackStatement = new RollbackStatement(); }
1643016468
[ <K_WORK> { rollbackStatement.setUsingWorkKeyword(true); } ]
16431-
[ (
16432-
<K_TO> [ <K_SAVEPOINT> { rollbackStatement.setUsingSavepointKeyword(true); }]
16433-
token=<S_IDENTIFIER> { rollbackStatement.setSavepointName(token.image); }
16434-
)
16435-
|
16469+
// explicit lookahead: FORCE INSTALL starts a new statement, FORCE 'id' belongs here
16470+
[
16471+
LOOKAHEAD(2)
1643616472
(
16437-
<K_FORCE> token=<S_CHAR_LITERAL> { rollbackStatement.setForceDistributedTransactionIdentifier(token.image); }
16438-
) ]
16473+
(
16474+
<K_TO> [ <K_SAVEPOINT> { rollbackStatement.setUsingSavepointKeyword(true); }]
16475+
token=<S_IDENTIFIER> { rollbackStatement.setSavepointName(token.image); }
16476+
)
16477+
|
16478+
(
16479+
<K_FORCE> token=<S_CHAR_LITERAL> { rollbackStatement.setForceDistributedTransactionIdentifier(token.image); }
16480+
)
16481+
)
16482+
]
1643916483

1644016484
{
1644116485
return rollbackStatement;

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import net.sf.jsqlparser.statement.insert.Insert;
1616
import net.sf.jsqlparser.statement.DescribeStatement;
1717
import net.sf.jsqlparser.statement.PragmaStatement;
18+
import net.sf.jsqlparser.statement.ExtensionStatement;
1819
import net.sf.jsqlparser.test.TestUtils;
1920
import org.junit.jupiter.api.Assertions;
2021
import org.junit.jupiter.api.Test;
@@ -237,4 +238,46 @@ void testPragmaRemainsUsableAsIdentifier() throws JSQLParserException {
237238
String sqlStr = "SELECT pragma FROM t";
238239
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
239240
}
241+
242+
@Test
243+
void testLoadExtension() throws JSQLParserException {
244+
String sqlStr = "LOAD httpfs";
245+
ExtensionStatement extension =
246+
(ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
247+
248+
Assertions.assertEquals(ExtensionStatement.Operation.LOAD, extension.getOperation());
249+
Assertions.assertEquals("httpfs", extension.getExtensionName());
250+
}
251+
252+
@Test
253+
void testInstallExtension() throws JSQLParserException {
254+
String sqlStr = "INSTALL spatial";
255+
ExtensionStatement extension =
256+
(ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
257+
258+
Assertions.assertEquals(ExtensionStatement.Operation.INSTALL, extension.getOperation());
259+
Assertions.assertFalse(extension.isForce());
260+
}
261+
262+
@Test
263+
void testForceInstallExtensionFromRepository() throws JSQLParserException {
264+
String sqlStr = "FORCE INSTALL h3 FROM community";
265+
ExtensionStatement extension =
266+
(ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
267+
268+
Assertions.assertTrue(extension.isForce());
269+
Assertions.assertEquals("community", extension.getRepository());
270+
}
271+
272+
@Test
273+
void testInstallExtensionFromUrl() throws JSQLParserException {
274+
String sqlStr = "INSTALL 'path/to/ext.duckdb_extension'";
275+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
276+
}
277+
278+
@Test
279+
void testLoadRemainsUsableAsIdentifier() throws JSQLParserException {
280+
String sqlStr = "SELECT load, install FROM t";
281+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
282+
}
240283
}

0 commit comments

Comments
 (0)