Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -13852,7 +13852,7 @@ Index.ColumnParams IndexColumnWithParams(): {
&& getToken(1).kind != K_NULLS && getToken(1).kind != K_COLLATE
&& getToken(1).kind != K_WITH && getToken(1).kind != K_WITHOUT) })
operatorClass=IndexKeyAttributeName()
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlIndexOptions() ]
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlOptions(false) ]
{
if (!postgres) {
columnParams.add(operatorClass);
Expand Down Expand Up @@ -13906,15 +13906,23 @@ String IndexKeyAttributeName():
{ return name; }
}

Index.Option PostgreSqlIndexOption():
Index.Option PostgreSqlIndexOption(boolean allowBare):
{
String name = null;
Expression value = null;
boolean useEquals = false;
}
{
name=PostgreSqlStorageParameterName() [ "=" { useEquals = true; } ] value=Expression()
{ return new Index.Option(name, value, useEquals); }
name=PostgreSqlStorageParameterName()
[ [ "=" { useEquals = true; } ] value=Expression() ]
{
requireDdlSyntax(allowBare || value != null,
"Operator-class parameters require a value");
requireDdlSyntax(value == null || useEquals
|| !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)),
"PostgreSQL parameter values require an equals sign");
return new Index.Option(name, value, useEquals);
}
}

/** Storage parameters can have namespaces, such as toast.autovacuum_enabled. */
Expand All @@ -13926,14 +13934,19 @@ String PostgreSqlStorageParameterName():
{ return name; }
}

/** Storage parameters allow a bare name; operator-class parameters require a value. */
List<Index.Option> PostgreSqlIndexOptions():
{ List<Index.Option> options; }
{ options=PostgreSqlOptions(true) { return options; } }

List<Index.Option> PostgreSqlOptions(boolean allowBare):
{
List<Index.Option> options = new ArrayList<Index.Option>();
Index.Option option = null;
}
{
"(" option=PostgreSqlIndexOption() { options.add(option); }
( "," option=PostgreSqlIndexOption() { options.add(option); } )* ")"
"(" option=PostgreSqlIndexOption(allowBare) { options.add(option); }
( "," option=PostgreSqlIndexOption(allowBare) { options.add(option); } )* ")"
{ return options; }
}

Expand Down Expand Up @@ -14277,6 +14290,7 @@ NamedConstraint ColumnKeyConstraint():
constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct);
} ]
)
PostgreSqlIndexStorageOptions(constraint)
PostgreSqlConstraintAttributes(constraint)
{
requireDdlSyntax(constraint.getConstraintAttributes() == null
Expand Down Expand Up @@ -14933,11 +14947,19 @@ void TableIndexOptions(boolean createContext, List<String> options):
void PostgreSqlConstraintOptions(Index index):
{
List<String> columns;
}
{
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
PostgreSqlIndexStorageOptions(index)
}

/** Column keys share storage/tablespace options, but do not allow INCLUDE. */
void PostgreSqlIndexStorageOptions(Index index):
{
List<Index.Option> parameters;
String tableSpace;
}
{
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
[ LOOKAHEAD(2) <K_WITH> parameters=PostgreSqlIndexOptions() { index.setStorageParameters(parameters); } ]
[ LOOKAHEAD(3) <K_USING> <K_INDEX> <K_TABLESPACE> tableSpace=RelObjectName() {
index.setTableSpace(tableSpace);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*-
* #%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 org.junit.jupiter.api.Assertions.*;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.create.index.CreateIndex;
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.statement.create.table.ColumnOption;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.Index;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PostgreSqlColumnIndexOptionsTest {
@ParameterizedTest
@ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"})
void columnStorageAndTablespaceStayOnTheConstraint(String kind) throws JSQLParserException {
for (boolean alter : new boolean[] {false, true}) {
String prefix = alter ? "ALTER TABLE t ADD COLUMN " : "CREATE TABLE t (";
Statement statement = parse(prefix + "id INT CONSTRAINT uq " + kind
+ " WITH (fillfactor=70) USING INDEX TABLESPACE old_space DEFERRABLE INITIALLY DEFERRED"
+ (alter ? "" : ")"));
ColumnDefinition column = alter
? ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0)
: ((CreateTable) statement).getColumnDefinitions().get(0);
assertEquals(1, column.getColumnOptions().size());
ColumnOption option = column.getColumnOptions().get(0);
assertEquals(ColumnOption.Kind.CONSTRAINT, option.getKind());
Index key = option.getConstraint();
assertEquals("uq", key.getName());
assertEquals("old_space", key.getTableSpace());
assertEquals("70", key.getStorageParameters().get(0).getValue().toString());
assertNotNull(key.getConstraintAttributes());
key.setStorageParameters(
List.of(new Index.Option("fillfactor", new LongValue(80), true)));
key.setTableSpace("new_space");
String sql = statement.toString();
assertTrue(sql.contains("WITH (fillfactor = 80) USING INDEX TABLESPACE new_space"));
assertFalse(sql.contains("70"));
assertFalse(sql.contains("old_space"));
assertRoundTrip(statement);
}
}

@ParameterizedTest
@ValueSource(strings = {
"CREATE TABLE t (id INT) WITH (autovacuum_enabled,toast.autovacuum_enabled=false)",
"CREATE TABLE t (id INT PRIMARY KEY WITH (deduplicate_items))",
"CREATE TABLE t (id INT, CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items))",
"ALTER TABLE t ADD CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items)",
"CREATE INDEX ix ON t(id) WITH (deduplicate_items)",
"CREATE INDEX ix ON t USING gin(tags) WITH (fastupdate)",
"CREATE MATERIALIZED VIEW mv WITH (autovacuum_enabled) AS SELECT * FROM t"})
void bareStorageOptionsRoundTrip(String sql) throws JSQLParserException {
assertRoundTrip(parse(sql));
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1",
p -> p.withDialect(Dialect.POSTGRESQL)).size());
}

@Test
void bareOptionsCanBeAssignedAndCleared() throws JSQLParserException {
CreateIndex index = (CreateIndex) parse("CREATE INDEX ix ON t(id) WITH(deduplicate_items)");
Index.Option option = index.getStorageParameters().get(0);
assertNull(option.getValue());
assertFalse(option.isUseEquals());
option.setName("fillfactor");
option.setValue(new LongValue(80));
option.setUseEquals(true);
assertTrue(index.toString().contains("WITH (fillfactor = 80)"));
assertRoundTrip(index);
option.setName("deduplicate_items");
option.setValue(null);
assertTrue(index.toString().contains("WITH (deduplicate_items)"));
assertRoundTrip(index);
}

@Test
void columnIncludeAndIncompleteParametersAreRejected() {
for (String sql : new String[] {
"CREATE TABLE t(id INT UNIQUE INCLUDE(label),label TEXT)",
"CREATE TABLE t(id INT) WITH (fillfactor=)",
"CREATE TABLE t(id INT) WITH (fillfactor 70)",
"CREATE INDEX ix ON t(id int4_ops(deduplicate_items))"}) {
assertThrows(JSQLParserException.class, () -> parse(sql), sql);
}
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}

private static void assertRoundTrip(Statement statement) throws JSQLParserException {
StringBuilder sql = new StringBuilder();
statement.accept(new StatementDeParser(sql), null);
assertEquals(statement.toString(), sql.toString());
assertEquals(statement.toString(), parse(sql.toString()).toString());
}
}
Loading