Skip to content

Commit f01f6b6

Browse files
committed
feat: model qualified PostgreSQL exclusion operators
Signed-off-by: mj-db <mj.db@kakaocorp.com>
1 parent 60c8dc2 commit f01f6b6

5 files changed

Lines changed: 210 additions & 7 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.table;
11+
12+
import java.io.Serializable;
13+
14+
/** An exclusion operator, optionally qualified and enclosed in PostgreSQL OPERATOR(...). */
15+
public class ExclusionOperator implements Serializable {
16+
private String schemaName;
17+
private String name;
18+
private boolean useOperatorKeyword;
19+
20+
public String getSchemaName() {
21+
return schemaName;
22+
}
23+
24+
public void setSchemaName(String schemaName) {
25+
this.schemaName = schemaName;
26+
}
27+
28+
public String getName() {
29+
return name;
30+
}
31+
32+
public void setName(String name) {
33+
this.name = name;
34+
}
35+
36+
public boolean isUseOperatorKeyword() {
37+
return useOperatorKeyword;
38+
}
39+
40+
public void setUseOperatorKeyword(boolean useOperatorKeyword) {
41+
this.useOperatorKeyword = useOperatorKeyword;
42+
}
43+
44+
public ExclusionOperator withSchemaName(String schemaName) {
45+
setSchemaName(schemaName);
46+
return this;
47+
}
48+
49+
public ExclusionOperator withName(String name) {
50+
setName(name);
51+
return this;
52+
}
53+
54+
public ExclusionOperator withUseOperatorKeyword(boolean useOperatorKeyword) {
55+
setUseOperatorKeyword(useOperatorKeyword);
56+
return this;
57+
}
58+
59+
/** Returns the qualified name without an OPERATOR wrapper, preserving identifier quotes. */
60+
public String getQualifiedName() {
61+
return (schemaName == null ? "" : schemaName + ".") + name;
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return useOperatorKeyword ? "OPERATOR(" + getQualifiedName() + ")" : getQualifiedName();
67+
}
68+
}

src/main/java/net/sf/jsqlparser/statement/create/table/Index.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,32 @@ public enum NullOrdering {
414414
private List<Option> operatorClassParameters;
415415
private SortOrder sortOrder;
416416
private NullOrdering nullOrdering;
417-
private String exclusionOperator;
417+
private ExclusionOperator exclusionOperator;
418418

419419
public String getExclusionOperator() {
420-
return exclusionOperator;
420+
return exclusionOperator == null ? null : exclusionOperator.toString();
421421
}
422422

423+
/** Replaces the complete operator text; retained for source compatibility. */
423424
public void setExclusionOperator(String exclusionOperator) {
425+
this.exclusionOperator = exclusionOperator == null ? null
426+
: new ExclusionOperator().withName(exclusionOperator);
427+
}
428+
429+
/** Returns the editable operator reference, or null when this is not an exclusion key. */
430+
public ExclusionOperator getExclusionOperatorReference() {
431+
return exclusionOperator;
432+
}
433+
434+
public void setExclusionOperatorReference(ExclusionOperator exclusionOperator) {
424435
this.exclusionOperator = exclusionOperator;
425436
}
426437

438+
public ColumnParams withExclusionOperatorReference(ExclusionOperator exclusionOperator) {
439+
setExclusionOperatorReference(exclusionOperator);
440+
return this;
441+
}
442+
427443
public ColumnParams(String columnName) {
428444
this.columnName = columnName;
429445
this.params = null;

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14918,12 +14918,23 @@ Index.ColumnParams PostgreSqlExcludeElement():
1491814918
{
1491914919
Index.ColumnParams column;
1492014920
Token operator;
14921+
ExclusionOperator reference;
1492114922
}
1492214923
{
1492314924
column=IndexColumnWithParams() <K_WITH>
14924-
( operator="=" | operator=<OP_NOTEQUALSSTANDARD> | operator=<OP_NOTEQUALSBANG> | operator="<" | operator=">"
14925-
| operator=<OP_MINORTHANEQUALS> | operator=<OP_GREATERTHANEQUALS> | operator="&&" | operator="@>" | operator="<@" )
14926-
{ column.setExclusionOperator(operator.image); return column; }
14925+
(
14926+
LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
14927+
&& isKeywordAhead("OPERATOR") })
14928+
TypeDdlKeyword("OPERATOR") "(" reference=PostgreSqlOperatorReference() ")" {
14929+
reference.setUseOperatorKeyword(true);
14930+
column.setExclusionOperatorReference(reference);
14931+
}
14932+
|
14933+
( operator="=" | operator=<OP_NOTEQUALSSTANDARD> | operator=<OP_NOTEQUALSBANG> | operator="<" | operator=">"
14934+
| operator=<OP_MINORTHANEQUALS> | operator=<OP_GREATERTHANEQUALS> | operator="&&" | operator="@>" | operator="<@" )
14935+
{ column.setExclusionOperator(operator.image); }
14936+
)
14937+
{ return column; }
1492714938
}
1492814939

1492914940

@@ -19131,15 +19142,20 @@ RoutineReference RoutineReference(boolean aggregate):
1913119142
}
1913219143

1913319144
String ExtensionOperatorName():
19134-
{ String schema = null; String operator = ""; Token token; }
19145+
{ ExclusionOperator operator; }
19146+
{ operator=PostgreSqlOperatorReference() { return operator.getQualifiedName(); } }
19147+
19148+
/** Shared schema-qualified operator spelling used in DDL references. */
19149+
ExclusionOperator PostgreSqlOperatorReference():
19150+
{ String schema = null; Token token; }
1913519151
{
1913619152
[ LOOKAHEAD(RelObjectName() ".") schema=RelObjectName() "." ]
1913719153
( token="+" | token="-" | token="*" | token="/" | token="%" | token="="
1913819154
| token=<OP_NOTEQUALSSTANDARD> | token=<OP_NOTEQUALSBANG> | token="<" | token=">"
1913919155
| token=<OP_MINORTHANEQUALS> | token=<OP_GREATERTHANEQUALS> | token="&&" | token="@>" | token="<@" | token="~"
1914019156
| token="~*" | token="!~" | token="!~*" | token="?" | token="?|" | token="?&"
1914119157
| token=<OP_CONCAT> )
19142-
{ operator = token.image; return (schema == null ? "" : schema + ".") + operator; }
19158+
{ return new ExclusionOperator().withSchemaName(schema).withName(token.image); }
1914319159
}
1914419160

1914519161
ColDataType ExtensionOperatorType():

src/site/sphinx/usage.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ Ordinary views expose ordered ``ViewOption`` values for ``security_barrier``, ``
281281

282282
Table constraints expose ``Index.getNullsDistinct()``, ``getIncludeColumns()``, storage parameters and ``ConstraintAttributes``. ``ExcludeConstraint`` reuses ``Index.ColumnParams`` for its keys; each key exposes its expression and exclusion operator. Column identity clauses are represented by ``ColumnOption.Kind.IDENTITY`` and ``IdentityDefinition``, with a generation mode and ordered ``Sequence.Parameter`` values. These APIs cover the schema clauses described in `CREATE TABLE <https://www.postgresql.org/docs/18/sql-createtable.html>`_.
283283

284+
Exclusion keys expose ``getExclusionOperatorReference()`` for structured access
285+
to an operator's ``schemaName``, ``name`` and ``useOperatorKeyword`` flag. In
286+
``Dialect.POSTGRESQL``, both CREATE and ALTER accept ``WITH OPERATOR(pg_catalog.&&)``.
287+
Editing the reference updates both statement renderers. The existing string
288+
``getExclusionOperator()`` returns its complete SQL spelling; its setter remains
289+
available for replacing opaque operator text. New code can construct a reference
290+
with ``new ExclusionOperator().withSchemaName("pg_catalog").withName("&&")
291+
.withUseOperatorKeyword(true)``.
292+
284293
.. code-block:: java
285294
286295
Alter alter = (Alter) CCJSqlParserUtil.parse(
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.alter;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import net.sf.jsqlparser.JSQLParserException;
14+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.statement.Statement;
17+
import net.sf.jsqlparser.statement.create.table.CreateTable;
18+
import net.sf.jsqlparser.statement.create.table.ExcludeConstraint;
19+
import net.sf.jsqlparser.statement.create.table.ExclusionOperator;
20+
import net.sf.jsqlparser.statement.create.table.Index;
21+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
26+
class PostgreSqlExclusionOperatorTest {
27+
@ParameterizedTest
28+
@ValueSource(strings = {"OPERATOR(pg_catalog.&&)", "OPERATOR(&&)", "OPERATOR(\"My.Schema\".&&)",
29+
"OPERATOR(pg_catalog.=)", "&&"})
30+
void supportsSharedCreateAndAlterGrammar(String operator) throws JSQLParserException {
31+
for (String sql : new String[] {
32+
"CREATE TABLE t (c INT4RANGE, CONSTRAINT ex EXCLUDE USING gist (c WITH " + operator
33+
+ "))",
34+
"ALTER TABLE t ADD CONSTRAINT ex EXCLUDE USING gist (c WITH " + operator + ")"}) {
35+
Statement statement = parse(sql);
36+
assertEquals(sql, statement.toString());
37+
assertEquals(operator,
38+
constraint(statement).getColumns().get(0).getExclusionOperator());
39+
roundTrip(statement);
40+
}
41+
}
42+
43+
@Test
44+
void exposesEditableSchemaAndSymbolAndSupportsLegacySetter() throws JSQLParserException {
45+
Statement statement = parse(
46+
"ALTER TABLE t ADD CONSTRAINT ex EXCLUDE USING gist (c WITH OPERATOR(pg_catalog.&&))");
47+
Index.ColumnParams key = constraint(statement).getColumns().get(0);
48+
ExclusionOperator operator = key.getExclusionOperatorReference();
49+
assertEquals("pg_catalog", operator.getSchemaName());
50+
assertEquals("&&", operator.getName());
51+
assertTrue(operator.isUseOperatorKeyword());
52+
operator.setSchemaName("\"My.Schema\"");
53+
operator.setName("=");
54+
assertEquals("OPERATOR(\"My.Schema\".=)", key.getExclusionOperator());
55+
roundTrip(statement);
56+
key.setExclusionOperator("&&");
57+
assertEquals("&&", key.getExclusionOperator());
58+
assertNull(key.getExclusionOperatorReference().getSchemaName());
59+
roundTrip(statement);
60+
key.setExclusionOperator(null);
61+
assertNull(key.getExclusionOperatorReference());
62+
key.setExclusionOperatorReference(new ExclusionOperator().withSchemaName("pg_catalog")
63+
.withName("&&").withUseOperatorKeyword(true));
64+
roundTrip(statement);
65+
}
66+
67+
@Test
68+
void preservesElementOptionsAndMultipleElementsAndDefaultSyntax() throws JSQLParserException {
69+
roundTrip(parse(
70+
"ALTER TABLE t ADD EXCLUDE USING gist (c WITH OPERATOR(pg_catalog.&&), d WITH =) WHERE (c IS NOT NULL) DEFERRABLE INITIALLY DEFERRED"
71+
.replace("\n", "")));
72+
String legacy = "CREATE TABLE t (c INT4RANGE, EXCLUDE USING gist (c WITH &&))";
73+
assertEquals(legacy, CCJSqlParserUtil.parse(legacy).toString());
74+
roundTrip(parse(
75+
"ALTER TABLE t ADD EXCLUDE USING gist ((c) WITH OPERATOR(pg_catalog.&&)), ADD COLUMN extra INT"));
76+
}
77+
78+
private static ExcludeConstraint constraint(Statement statement) {
79+
return (ExcludeConstraint) (statement instanceof Alter
80+
? ((Alter) statement).getAlterExpressions().get(0).getIndex()
81+
: ((CreateTable) statement).getIndexes().get(0));
82+
}
83+
84+
private static Statement parse(String sql) throws JSQLParserException {
85+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
86+
}
87+
88+
private static void roundTrip(Statement statement) throws JSQLParserException {
89+
StringBuilder sql = new StringBuilder();
90+
statement.accept(new StatementDeParser(sql), null);
91+
assertEquals(statement.toString(), sql.toString());
92+
assertEquals(sql.toString(), parse(sql.toString()).toString());
93+
}
94+
}

0 commit comments

Comments
 (0)