Skip to content

Commit 9f4da8e

Browse files
committed
fix: share PostgreSQL column key and storage parameter parsing
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 8ffa565 commit 9f4da8e

2 files changed

Lines changed: 143 additions & 7 deletions

File tree

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13852,7 +13852,7 @@ Index.ColumnParams IndexColumnWithParams(): {
1385213852
&& getToken(1).kind != K_NULLS && getToken(1).kind != K_COLLATE
1385313853
&& getToken(1).kind != K_WITH && getToken(1).kind != K_WITHOUT) })
1385413854
operatorClass=IndexKeyAttributeName()
13855-
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlIndexOptions() ]
13855+
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlOptions(false) ]
1385613856
{
1385713857
if (!postgres) {
1385813858
columnParams.add(operatorClass);
@@ -13906,15 +13906,23 @@ String IndexKeyAttributeName():
1390613906
{ return name; }
1390713907
}
1390813908

13909-
Index.Option PostgreSqlIndexOption():
13909+
Index.Option PostgreSqlIndexOption(boolean allowBare):
1391013910
{
1391113911
String name = null;
1391213912
Expression value = null;
1391313913
boolean useEquals = false;
1391413914
}
1391513915
{
13916-
name=PostgreSqlStorageParameterName() [ "=" { useEquals = true; } ] value=Expression()
13917-
{ return new Index.Option(name, value, useEquals); }
13916+
name=PostgreSqlStorageParameterName()
13917+
[ [ "=" { useEquals = true; } ] value=Expression() ]
13918+
{
13919+
requireDdlSyntax(allowBare || value != null,
13920+
"Operator-class parameters require a value");
13921+
requireDdlSyntax(value == null || useEquals
13922+
|| !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)),
13923+
"PostgreSQL parameter values require an equals sign");
13924+
return new Index.Option(name, value, useEquals);
13925+
}
1391813926
}
1391913927

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

13937+
/** Storage parameters allow a bare name; operator-class parameters require a value. */
1392913938
List<Index.Option> PostgreSqlIndexOptions():
13939+
{ List<Index.Option> options; }
13940+
{ options=PostgreSqlOptions(true) { return options; } }
13941+
13942+
List<Index.Option> PostgreSqlOptions(boolean allowBare):
1393013943
{
1393113944
List<Index.Option> options = new ArrayList<Index.Option>();
1393213945
Index.Option option = null;
1393313946
}
1393413947
{
13935-
"(" option=PostgreSqlIndexOption() { options.add(option); }
13936-
( "," option=PostgreSqlIndexOption() { options.add(option); } )* ")"
13948+
"(" option=PostgreSqlIndexOption(allowBare) { options.add(option); }
13949+
( "," option=PostgreSqlIndexOption(allowBare) { options.add(option); } )* ")"
1393713950
{ return options; }
1393813951
}
1393913952

@@ -14277,6 +14290,7 @@ NamedConstraint ColumnKeyConstraint():
1427714290
constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct);
1427814291
} ]
1427914292
)
14293+
PostgreSqlIndexStorageOptions(constraint)
1428014294
PostgreSqlConstraintAttributes(constraint)
1428114295
{
1428214296
requireDdlSyntax(constraint.getConstraintAttributes() == null
@@ -14933,11 +14947,19 @@ void TableIndexOptions(boolean createContext, List<String> options):
1493314947
void PostgreSqlConstraintOptions(Index index):
1493414948
{
1493514949
List<String> columns;
14950+
}
14951+
{
14952+
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
14953+
PostgreSqlIndexStorageOptions(index)
14954+
}
14955+
14956+
/** Column keys share storage/tablespace options, but do not allow INCLUDE. */
14957+
void PostgreSqlIndexStorageOptions(Index index):
14958+
{
1493614959
List<Index.Option> parameters;
1493714960
String tableSpace;
1493814961
}
1493914962
{
14940-
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
1494114963
[ LOOKAHEAD(2) <K_WITH> parameters=PostgreSqlIndexOptions() { index.setStorageParameters(parameters); } ]
1494214964
[ LOOKAHEAD(3) <K_USING> <K_INDEX> <K_TABLESPACE> tableSpace=RelObjectName() {
1494314965
index.setTableSpace(tableSpace);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.List;
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.expression.LongValue;
16+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
17+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
18+
import net.sf.jsqlparser.statement.Statement;
19+
import net.sf.jsqlparser.statement.alter.Alter;
20+
import net.sf.jsqlparser.statement.create.index.CreateIndex;
21+
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
22+
import net.sf.jsqlparser.statement.create.table.ColumnOption;
23+
import net.sf.jsqlparser.statement.create.table.CreateTable;
24+
import net.sf.jsqlparser.statement.create.table.Index;
25+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.ValueSource;
29+
30+
class PostgreSqlColumnIndexOptionsTest {
31+
@ParameterizedTest
32+
@ValueSource(strings = {"PRIMARY KEY", "UNIQUE", "UNIQUE NULLS NOT DISTINCT"})
33+
void columnStorageAndTablespaceStayOnTheConstraint(String kind) throws JSQLParserException {
34+
for (boolean alter : new boolean[] {false, true}) {
35+
String prefix = alter ? "ALTER TABLE t ADD COLUMN " : "CREATE TABLE t (";
36+
Statement statement = parse(prefix + "id INT CONSTRAINT uq " + kind
37+
+ " WITH (fillfactor=70) USING INDEX TABLESPACE old_space DEFERRABLE INITIALLY DEFERRED"
38+
+ (alter ? "" : ")"));
39+
ColumnDefinition column = alter
40+
? ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0)
41+
: ((CreateTable) statement).getColumnDefinitions().get(0);
42+
assertEquals(1, column.getColumnOptions().size());
43+
ColumnOption option = column.getColumnOptions().get(0);
44+
assertEquals(ColumnOption.Kind.CONSTRAINT, option.getKind());
45+
Index key = option.getConstraint();
46+
assertEquals("uq", key.getName());
47+
assertEquals("old_space", key.getTableSpace());
48+
assertEquals("70", key.getStorageParameters().get(0).getValue().toString());
49+
assertNotNull(key.getConstraintAttributes());
50+
key.setStorageParameters(
51+
List.of(new Index.Option("fillfactor", new LongValue(80), true)));
52+
key.setTableSpace("new_space");
53+
String sql = statement.toString();
54+
assertTrue(sql.contains("WITH (fillfactor = 80) USING INDEX TABLESPACE new_space"));
55+
assertFalse(sql.contains("70"));
56+
assertFalse(sql.contains("old_space"));
57+
assertRoundTrip(statement);
58+
}
59+
}
60+
61+
@ParameterizedTest
62+
@ValueSource(strings = {
63+
"CREATE TABLE t (id INT) WITH (autovacuum_enabled,toast.autovacuum_enabled=false)",
64+
"CREATE TABLE t (id INT PRIMARY KEY WITH (deduplicate_items))",
65+
"CREATE TABLE t (id INT, CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items))",
66+
"ALTER TABLE t ADD CONSTRAINT uq UNIQUE(id) WITH (deduplicate_items)",
67+
"CREATE INDEX ix ON t(id) WITH (deduplicate_items)",
68+
"CREATE INDEX ix ON t USING gin(tags) WITH (fastupdate)",
69+
"CREATE MATERIALIZED VIEW mv WITH (autovacuum_enabled) AS SELECT * FROM t"})
70+
void bareStorageOptionsRoundTrip(String sql) throws JSQLParserException {
71+
assertRoundTrip(parse(sql));
72+
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1",
73+
p -> p.withDialect(Dialect.POSTGRESQL)).size());
74+
}
75+
76+
@Test
77+
void bareOptionsCanBeAssignedAndCleared() throws JSQLParserException {
78+
CreateIndex index = (CreateIndex) parse("CREATE INDEX ix ON t(id) WITH(deduplicate_items)");
79+
Index.Option option = index.getStorageParameters().get(0);
80+
assertNull(option.getValue());
81+
assertFalse(option.isUseEquals());
82+
option.setName("fillfactor");
83+
option.setValue(new LongValue(80));
84+
option.setUseEquals(true);
85+
assertTrue(index.toString().contains("WITH (fillfactor = 80)"));
86+
assertRoundTrip(index);
87+
option.setName("deduplicate_items");
88+
option.setValue(null);
89+
assertTrue(index.toString().contains("WITH (deduplicate_items)"));
90+
assertRoundTrip(index);
91+
}
92+
93+
@Test
94+
void columnIncludeAndIncompleteParametersAreRejected() {
95+
for (String sql : new String[] {
96+
"CREATE TABLE t(id INT UNIQUE INCLUDE(label),label TEXT)",
97+
"CREATE TABLE t(id INT) WITH (fillfactor=)",
98+
"CREATE TABLE t(id INT) WITH (fillfactor 70)",
99+
"CREATE INDEX ix ON t(id int4_ops(deduplicate_items))"}) {
100+
assertThrows(JSQLParserException.class, () -> parse(sql), sql);
101+
}
102+
}
103+
104+
private static Statement parse(String sql) throws JSQLParserException {
105+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
106+
}
107+
108+
private static void assertRoundTrip(Statement statement) throws JSQLParserException {
109+
StringBuilder sql = new StringBuilder();
110+
statement.accept(new StatementDeParser(sql), null);
111+
assertEquals(statement.toString(), sql.toString());
112+
assertEquals(statement.toString(), parse(sql.toString()).toString());
113+
}
114+
}

0 commit comments

Comments
 (0)