From a2eb3dc3b61a6abee9234daed816f5a078b59bdf Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Wed, 2 Sep 2026 22:32:32 +0900 Subject: [PATCH] Support PostgreSQL CREATE INDEX header options --- .../statement/create/index/CreateIndex.java | 43 ++++++++++++-- .../util/deparser/CreateIndexDeParser.java | 15 +++-- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 15 ++++- .../create/PostgreSQLCreateIndexTest.java | 58 +++++++++++++++++++ 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index 0da992dd6..fd7573e20 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -23,6 +23,8 @@ public class CreateIndex implements Statement { private List tailParameters; private boolean indexTypeBeforeOn = false; private boolean usingIfNotExists = false; + private boolean concurrently; + private boolean only; public boolean isIndexTypeBeforeOn() { return indexTypeBeforeOn; @@ -41,6 +43,22 @@ public CreateIndex setUsingIfNotExists(boolean usingIfNotExists) { return this; } + public boolean isConcurrently() { + return concurrently; + } + + public void setConcurrently(boolean concurrently) { + this.concurrently = concurrently; + } + + public boolean isOnly() { + return only; + } + + public void setOnly(boolean only) { + this.only = only; + } + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); @@ -82,17 +100,24 @@ public String toString() { } buffer.append("INDEX "); + if (concurrently) { + buffer.append("CONCURRENTLY "); + } if (usingIfNotExists) { buffer.append("IF NOT EXISTS "); } - buffer.append(index.getName()); + if (index.getName() != null) { + buffer.append(index.getName()).append(" "); + } if (index.getUsing() != null && isIndexTypeBeforeOn()) { - buffer.append(" USING "); - buffer.append(index.getUsing()); + buffer.append("USING ").append(index.getUsing()).append(" "); } - buffer.append(" ON "); + buffer.append("ON "); + if (only) { + buffer.append("ONLY "); + } buffer.append(table.getFullyQualifiedName()); if (index.getUsing() != null && !isIndexTypeBeforeOn()) { @@ -134,4 +159,14 @@ public CreateIndex withTailParameters(List tailParameters) { this.setTailParameters(tailParameters); return this; } + + public CreateIndex withConcurrently(boolean concurrently) { + setConcurrently(concurrently); + return this; + } + + public CreateIndex withOnly(boolean only) { + setOnly(only); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java index cc5d71ba1..c76515812 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java @@ -32,18 +32,25 @@ public void deParse(CreateIndex createIndex) { } builder.append("INDEX "); + if (createIndex.isConcurrently()) { + builder.append("CONCURRENTLY "); + } if (createIndex.isUsingIfNotExists()) { builder.append("IF NOT EXISTS "); } - builder.append(index.getName()); + if (index.getName() != null) { + builder.append(index.getName()).append(" "); + } String using = index.getUsing(); if (using != null && createIndex.isIndexTypeBeforeOn()) { - builder.append(" USING "); - builder.append(using); + builder.append("USING ").append(using).append(" "); } - builder.append(" ON "); + builder.append("ON "); + if (createIndex.isOnly()) { + builder.append("ONLY "); + } builder.append(createIndex.getTable().getFullyQualifiedName()); if (using != null && !createIndex.isIndexTypeBeforeOn()) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d52b7a9c2..5b0318741 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11173,11 +11173,20 @@ CreateIndex CreateIndex(): [ parameter=CreateParameter() ] + [ LOOKAHEAD({ getToken(1).kind == K_CONCURRENTLY }) + { createIndex.setConcurrently(true); } ] [ LOOKAHEAD(2) { createIndex.setUsingIfNotExists(true);} ] - index = Index() { index.setType(parameter.isEmpty() ? null : parameter.get(0)); } + [ LOOKAHEAD({ getToken(1).kind != K_ON && getToken(1).kind != K_USING }) + index = Index() ] + { + if (index == null) { + index = new Index(); + } + index.setType(parameter.isEmpty() ? null : parameter.get(0)); + } ( LOOKAHEAD(3)( - table=Table() + [ { createIndex.setOnly(true); } ] table=Table() [ indexType=UsingIndexType() { index.setUsing(indexType); } ] ) | @@ -11187,7 +11196,7 @@ CreateIndex CreateIndex(): createIndex.setIndexTypeBeforeOn(true); } ] - table=Table() + [ { createIndex.setOnly(true); } ] table=Table() ) ) colNames = IndexColumnsWithParamsList() diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java new file mode 100644 index 000000000..23d8442f9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java @@ -0,0 +1,58 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import org.junit.jupiter.api.Test; + +/** PostgreSQL {@code CREATE INDEX} syntax verified against PostgreSQL 18.6. */ +public class PostgreSQLCreateIndexTest { + + @Test + public void testCreateIndexConcurrently() throws JSQLParserException { + String sql = "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS pg_idx_email " + + "ON pg_index_test USING btree (email)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertTrue(createIndex.isConcurrently()); + assertTrue(createIndex.isUsingIfNotExists()); + assertFalse(createIndex.isOnly()); + assertEquals("pg_idx_email", createIndex.getIndex().getName()); + } + + @Test + public void testCreateIndexWithOmittedNameAndOnly() throws JSQLParserException { + String sql = "CREATE INDEX ON ONLY pg_index_test USING btree (id DESC)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertNull(createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + assertFalse(createIndex.isConcurrently()); + } + + @Test + public void testCreateNamedIndexOnOnlyTable() throws JSQLParserException { + String sql = "CREATE INDEX pg_idx_only ON ONLY pg_index_test (id)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals("pg_idx_only", createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + } +}