Skip to content

Commit 1df0033

Browse files
hayssamsclaude
andcommitted
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
1 parent 9af64f8 commit 1df0033

9 files changed

Lines changed: 192 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import java.util.Set;
9595
import java.util.function.Predicate;
9696
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
97+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
9798

9899
/**
99100
* Derives a {@link StatementFeatures} verdict from a statement tree.
@@ -592,6 +593,13 @@ public <S> Void visit(CreateMacro createMacro, S context) {
592593
return null;
593594
}
594595

596+
@Override
597+
public <S> Void visit(CreateExtensionRepository createExtensionRepository, S context) {
598+
analysis.claimTopLevel();
599+
analysis.certain(StmtFeature.MODIFIES_SCHEMA);
600+
return null;
601+
}
602+
595603
@Override
596604
public <S> Void visit(PurgeStatement purgeStatement, S context) {
597605
analysis.claimTopLevel();

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import net.sf.jsqlparser.statement.update.Update;
7070
import net.sf.jsqlparser.statement.upsert.Upsert;
7171
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
72+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
7273

7374
public interface StatementVisitor<T> {
7475

@@ -432,6 +433,12 @@ default void visit(CreateMacro createMacro) {
432433
this.visit(createMacro, null);
433434
}
434435

436+
<S> T visit(CreateExtensionRepository createExtensionRepository, S context);
437+
438+
default void visit(CreateExtensionRepository createExtensionRepository) {
439+
this.visit(createExtensionRepository, null);
440+
}
441+
435442
<S> T visit(PurgeStatement purgeStatement, S context);
436443

437444
default void visit(PurgeStatement purgeStatement) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import java.util.List;
9191
import net.sf.jsqlparser.util.TableDefinitionTraversal;
9292
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
93+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
9394

9495
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
9596
public class StatementVisitorAdapter<T> implements StatementVisitor<T> {
@@ -669,6 +670,11 @@ public <S> T visit(CreateMacro createMacro, S context) {
669670
return null;
670671
}
671672

673+
@Override
674+
public <S> T visit(CreateExtensionRepository createExtensionRepository, S context) {
675+
return null;
676+
}
677+
672678
@Override
673679
public <S> T visit(PurgeStatement purgeStatement, S context) {
674680
return null;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.create.extension;
11+
12+
import net.sf.jsqlparser.expression.Expression;
13+
import net.sf.jsqlparser.statement.Statement;
14+
import net.sf.jsqlparser.statement.StatementVisitor;
15+
16+
/**
17+
* DuckDB 2.0's {@code CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix]}, which
18+
* registers a custom repository extensions may be installed from.
19+
*/
20+
public class CreateExtensionRepository implements Statement {
21+
private String name;
22+
private boolean ifNotExists;
23+
private Expression prefix;
24+
25+
public String getName() {
26+
return name;
27+
}
28+
29+
public void setName(String name) {
30+
this.name = name;
31+
}
32+
33+
public CreateExtensionRepository withName(String name) {
34+
setName(name);
35+
return this;
36+
}
37+
38+
public boolean isIfNotExists() {
39+
return ifNotExists;
40+
}
41+
42+
public void setIfNotExists(boolean ifNotExists) {
43+
this.ifNotExists = ifNotExists;
44+
}
45+
46+
public CreateExtensionRepository withIfNotExists(boolean ifNotExists) {
47+
setIfNotExists(ifNotExists);
48+
return this;
49+
}
50+
51+
public Expression getPrefix() {
52+
return prefix;
53+
}
54+
55+
public void setPrefix(Expression prefix) {
56+
this.prefix = prefix;
57+
}
58+
59+
public CreateExtensionRepository withPrefix(Expression prefix) {
60+
setPrefix(prefix);
61+
return this;
62+
}
63+
64+
public StringBuilder appendTo(StringBuilder builder) {
65+
builder.append("CREATE EXTENSION REPOSITORY ");
66+
if (ifNotExists) {
67+
builder.append("IF NOT EXISTS ");
68+
}
69+
builder.append(name);
70+
if (prefix != null) {
71+
builder.append(" WITH PREFIX ").append(prefix);
72+
}
73+
return builder;
74+
}
75+
76+
@Override
77+
public String toString() {
78+
return appendTo(new StringBuilder()).toString();
79+
}
80+
81+
@Override
82+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
83+
return statementVisitor.visit(this, context);
84+
}
85+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@
219219
import net.sf.jsqlparser.statement.DeallocateStatement;
220220
import net.sf.jsqlparser.statement.CopyStatement;
221221
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
222+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
222223

223224

224225
/**
@@ -2594,6 +2595,17 @@ public void visit(CreateMacro createMacro) {
25942595
StatementVisitor.super.visit(createMacro);
25952596
}
25962597

2598+
@Override
2599+
public <S> Void visit(CreateExtensionRepository createExtensionRepository, S context) {
2600+
// no tables involved in this statement
2601+
return null;
2602+
}
2603+
2604+
@Override
2605+
public void visit(CreateExtensionRepository createExtensionRepository) {
2606+
StatementVisitor.super.visit(createExtensionRepository);
2607+
}
2608+
25972609
@Override
25982610
public <S> Void visit(PurgeStatement purgeStatement, S context) {
25992611
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
@@ -107,6 +107,7 @@
107107
import net.sf.jsqlparser.statement.DeallocateStatement;
108108
import net.sf.jsqlparser.statement.CopyStatement;
109109
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
110+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
110111

111112
public class StatementDeParser extends AbstractDeParser<Statement>
112113
implements StatementVisitor<StringBuilder> {
@@ -626,6 +627,12 @@ public <S> StringBuilder visit(CreateMacro createMacro, S context) {
626627
return builder;
627628
}
628629

630+
@Override
631+
public <S> StringBuilder visit(CreateExtensionRepository createExtensionRepository, S context) {
632+
createExtensionRepository.appendTo(builder);
633+
return builder;
634+
}
635+
629636
@Override
630637
public <S> StringBuilder visit(PurgeStatement purgeStatement, S context) {
631638
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
@@ -109,6 +109,7 @@
109109
import net.sf.jsqlparser.statement.DeallocateStatement;
110110
import net.sf.jsqlparser.statement.CopyStatement;
111111
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
112+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
112113

113114
/**
114115
* @author gitmotte
@@ -547,6 +548,12 @@ public <S> Void visit(CreateMacro createMacro, S context) {
547548
return null;
548549
}
549550

551+
@Override
552+
public <S> Void visit(CreateExtensionRepository createExtensionRepository, S context) {
553+
// TODO: not yet implemented
554+
return null;
555+
}
556+
550557
@Override
551558
public <S> Void visit(PurgeStatement purgeStatement, S context) {
552559
// TODO: not yet implemented
@@ -803,6 +810,10 @@ public void visit(CreateMacro createMacro) {
803810
visit(createMacro, null);
804811
}
805812

813+
public void visit(CreateExtensionRepository createExtensionRepository) {
814+
visit(createExtensionRepository, null);
815+
}
816+
806817
public void visit(PurgeStatement purgeStatement) {
807818
visit(purgeStatement, null);
808819
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17307,6 +17307,12 @@ Statement Create():
1730717307
|
1730817308
LOOKAHEAD(<K_DOMAIN>) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE DOMAIN"); } statement = CreateDomain()
1730917309
|
17310+
LOOKAHEAD({ isKeywordAhead("EXTENSION")
17311+
&& "REPOSITORY".equalsIgnoreCase(getToken(2).image) })
17312+
{ requireTypeDdlSyntax(!isUsingOrReplace,
17313+
"OR REPLACE is not supported for CREATE EXTENSION REPOSITORY"); }
17314+
statement = CreateExtensionRepository()
17315+
|
1731017316
LOOKAHEAD({ isKeywordAhead("EXTENSION") }) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE EXTENSION"); } statement = CreateExtension()
1731117317
|
1731217318
LOOKAHEAD({ isKeywordAhead("PUBLICATION") })
@@ -17710,6 +17716,28 @@ AlterDomain AlterDomain():
1771017716
{ return result; }
1771117717
}
1771217718

17719+
/**
17720+
* DuckDB 2.0's CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix].
17721+
*/
17722+
CreateExtensionRepository CreateExtensionRepository() #CreateExtensionRepository:
17723+
{
17724+
CreateExtensionRepository createExtensionRepository = new CreateExtensionRepository();
17725+
Expression prefix;
17726+
String name;
17727+
}
17728+
{
17729+
TypeDdlKeyword("EXTENSION") TypeDdlKeyword("REPOSITORY")
17730+
[ LOOKAHEAD(3) <K_IF> <K_NOT> <K_EXISTS>
17731+
{ createExtensionRepository.setIfNotExists(true); } ]
17732+
name=RelObjectName() { createExtensionRepository.setName(name); }
17733+
// greedy: WITH PREFIX belongs to the repository definition
17734+
[
17735+
LOOKAHEAD(2) <K_WITH> TypeDdlKeyword("PREFIX") prefix=SimpleExpression()
17736+
{ createExtensionRepository.setPrefix(prefix); }
17737+
]
17738+
{ return createExtensionRepository; }
17739+
}
17740+
1771317741
CreateExtension CreateExtension():
1771417742
{ CreateExtension result = new CreateExtension(); String name; Expression version; }
1771517743
{

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.sf.jsqlparser.statement.DeallocateStatement;
2525
import net.sf.jsqlparser.statement.CopyStatement;
2626
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
27+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
2728
import net.sf.jsqlparser.test.TestUtils;
2829
import org.junit.jupiter.api.Assertions;
2930
import org.junit.jupiter.api.Test;
@@ -499,4 +500,31 @@ void testCreateTableMacro() throws JSQLParserException {
499500
Assertions.assertTrue(createMacro.isTable());
500501
Assertions.assertNotNull(createMacro.getSelect());
501502
}
503+
504+
@Test
505+
void testCreateExtensionRepository() throws JSQLParserException {
506+
String sqlStr =
507+
"CREATE EXTENSION REPOSITORY my_repo WITH PREFIX 'https://extensions.example.org'";
508+
CreateExtensionRepository createRepository =
509+
(CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
510+
511+
Assertions.assertEquals("my_repo", createRepository.getName());
512+
Assertions.assertEquals("'https://extensions.example.org'",
513+
createRepository.getPrefix().toString());
514+
}
515+
516+
@Test
517+
void testCreateExtensionRepositoryIfNotExists() throws JSQLParserException {
518+
String sqlStr = "CREATE EXTENSION REPOSITORY IF NOT EXISTS my_repo";
519+
CreateExtensionRepository createRepository =
520+
(CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
521+
522+
Assertions.assertTrue(createRepository.isIfNotExists());
523+
}
524+
525+
@Test
526+
void testCreatePostgresExtensionStillParses() throws JSQLParserException {
527+
String sqlStr = "CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public CASCADE";
528+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
529+
}
502530
}

0 commit comments

Comments
 (0)