Skip to content

Commit 01926d1

Browse files
authored
fix: share PostgreSQL column key and storage parameter parsing (#2709)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 386daa5 commit 01926d1

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
@@ -13921,7 +13921,7 @@ Index.ColumnParams IndexColumnWithParams(): {
1392113921
&& getToken(1).kind != K_NULLS && getToken(1).kind != K_COLLATE
1392213922
&& getToken(1).kind != K_WITH && getToken(1).kind != K_WITHOUT) })
1392313923
operatorClass=IndexKeyAttributeName()
13924-
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlIndexOptions() ]
13924+
[ LOOKAHEAD(2) operatorClassParameters=PostgreSqlOptions(false) ]
1392513925
{
1392613926
if (!postgres) {
1392713927
columnParams.add(operatorClass);
@@ -13975,15 +13975,23 @@ String IndexKeyAttributeName():
1397513975
{ return name; }
1397613976
}
1397713977

13978-
Index.Option PostgreSqlIndexOption():
13978+
Index.Option PostgreSqlIndexOption(boolean allowBare):
1397913979
{
1398013980
String name = null;
1398113981
Expression value = null;
1398213982
boolean useEquals = false;
1398313983
}
1398413984
{
13985-
name=PostgreSqlStorageParameterName() [ "=" { useEquals = true; } ] value=Expression()
13986-
{ return new Index.Option(name, value, useEquals); }
13985+
name=PostgreSqlStorageParameterName()
13986+
[ [ "=" { useEquals = true; } ] value=Expression() ]
13987+
{
13988+
requireDdlSyntax(allowBare || value != null,
13989+
"Operator-class parameters require a value");
13990+
requireDdlSyntax(value == null || useEquals
13991+
|| !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)),
13992+
"PostgreSQL parameter values require an equals sign");
13993+
return new Index.Option(name, value, useEquals);
13994+
}
1398713995
}
1398813996

1398913997
/** Storage parameters can have namespaces, such as toast.autovacuum_enabled. */
@@ -13995,14 +14003,19 @@ String PostgreSqlStorageParameterName():
1399514003
{ return name; }
1399614004
}
1399714005

14006+
/** Storage parameters allow a bare name; operator-class parameters require a value. */
1399814007
List<Index.Option> PostgreSqlIndexOptions():
14008+
{ List<Index.Option> options; }
14009+
{ options=PostgreSqlOptions(true) { return options; } }
14010+
14011+
List<Index.Option> PostgreSqlOptions(boolean allowBare):
1399914012
{
1400014013
List<Index.Option> options = new ArrayList<Index.Option>();
1400114014
Index.Option option = null;
1400214015
}
1400314016
{
14004-
"(" option=PostgreSqlIndexOption() { options.add(option); }
14005-
( "," option=PostgreSqlIndexOption() { options.add(option); } )* ")"
14017+
"(" option=PostgreSqlIndexOption(allowBare) { options.add(option); }
14018+
( "," option=PostgreSqlIndexOption(allowBare) { options.add(option); } )* ")"
1400614019
{ return options; }
1400714020
}
1400814021

@@ -14346,6 +14359,7 @@ NamedConstraint ColumnKeyConstraint():
1434614359
constraint.setNullsDistinct(nullsDistinct == null ? true : nullsDistinct);
1434714360
} ]
1434814361
)
14362+
PostgreSqlIndexStorageOptions(constraint)
1434914363
PostgreSqlConstraintAttributes(constraint)
1435014364
{
1435114365
requireDdlSyntax(constraint.getConstraintAttributes() == null
@@ -15002,11 +15016,19 @@ void TableIndexOptions(boolean createContext, List<String> options):
1500215016
void PostgreSqlConstraintOptions(Index index):
1500315017
{
1500415018
List<String> columns;
15019+
}
15020+
{
15021+
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
15022+
PostgreSqlIndexStorageOptions(index)
15023+
}
15024+
15025+
/** Column keys share storage/tablespace options, but do not allow INCLUDE. */
15026+
void PostgreSqlIndexStorageOptions(Index index):
15027+
{
1500515028
List<Index.Option> parameters;
1500615029
String tableSpace;
1500715030
}
1500815031
{
15009-
[ <K_INCLUDE> columns=ColumnsNamesList() { index.setIncludeColumns(columns); } ]
1501015032
[ LOOKAHEAD(2) <K_WITH> parameters=PostgreSqlIndexOptions() { index.setStorageParameters(parameters); } ]
1501115033
[ LOOKAHEAD(3) <K_USING> <K_INDEX> <K_TABLESPACE> tableSpace=RelObjectName() {
1501215034
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)