Skip to content

Commit 6aa6b51

Browse files
committed
feat: structure PostgreSQL relation properties and bulk tablespace moves
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent d2c4bde commit 6aa6b51

7 files changed

Lines changed: 265 additions & 5 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package net.sf.jsqlparser.statement;
1111

1212
import net.sf.jsqlparser.statement.alter.AlterRelation;
13+
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
1314
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
1415
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
1516
import net.sf.jsqlparser.statement.oracle.OracleBlock;
@@ -689,4 +690,11 @@ default void visit(AlterDatabase statement) {
689690
visit(statement, null);
690691
}
691692

693+
default <S> T visit(AlterTablespaceMove statement, S context) {
694+
return null;
695+
}
696+
697+
default void visit(AlterTablespaceMove statement) {
698+
visit(statement, null);
699+
}
692700
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 java.util.ArrayList;
13+
import java.util.List;
14+
import net.sf.jsqlparser.statement.Statement;
15+
import net.sf.jsqlparser.statement.StatementVisitor;
16+
import net.sf.jsqlparser.statement.select.PlainSelect;
17+
18+
/** Moves all matching relations from one PostgreSQL tablespace to another. */
19+
public class AlterTablespaceMove implements Statement {
20+
public enum ObjectType {
21+
TABLE, INDEX, MATERIALIZED_VIEW
22+
}
23+
24+
private ObjectType objectType;
25+
private String sourceTablespace;
26+
private String targetTablespace;
27+
private List<String> owners = new ArrayList<>();
28+
private boolean noWait;
29+
30+
public ObjectType getObjectType() {
31+
return objectType;
32+
}
33+
34+
public void setObjectType(ObjectType objectType) {
35+
this.objectType = objectType;
36+
}
37+
38+
public String getSourceTablespace() {
39+
return sourceTablespace;
40+
}
41+
42+
public void setSourceTablespace(String sourceTablespace) {
43+
this.sourceTablespace = sourceTablespace;
44+
}
45+
46+
public String getTargetTablespace() {
47+
return targetTablespace;
48+
}
49+
50+
public void setTargetTablespace(String targetTablespace) {
51+
this.targetTablespace = targetTablespace;
52+
}
53+
54+
public List<String> getOwners() {
55+
return owners;
56+
}
57+
58+
public void setOwners(List<String> owners) {
59+
this.owners = owners;
60+
}
61+
62+
public boolean isNoWait() {
63+
return noWait;
64+
}
65+
66+
public void setNoWait(boolean noWait) {
67+
this.noWait = noWait;
68+
}
69+
70+
public StringBuilder appendTo(StringBuilder builder) {
71+
builder.append("ALTER ").append(objectType.name().replace('_', ' '))
72+
.append(" ALL IN TABLESPACE ").append(sourceTablespace);
73+
if (owners != null && !owners.isEmpty()) {
74+
builder.append(" OWNED BY ").append(PlainSelect.getStringList(owners));
75+
}
76+
builder.append(" SET TABLESPACE ").append(targetTablespace);
77+
if (noWait) {
78+
builder.append(" NOWAIT");
79+
}
80+
return builder;
81+
}
82+
83+
@Override
84+
public String toString() {
85+
return appendTo(new StringBuilder()).toString();
86+
}
87+
88+
@Override
89+
public <T, S> T accept(StatementVisitor<T> visitor, S context) {
90+
return visitor.visit(this, context);
91+
}
92+
}

‎src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
/** Structured PostgreSQL property actions shared by tables, indexes and views. */
2020
public class RelationAlterAction extends AlterExpression {
2121
public enum Kind {
22-
RENAME, RENAME_COLUMN, OWNER, SET_SCHEMA, SET_TABLESPACE, SET_ACCESS_METHOD, SET_OPTIONS, RESET_OPTIONS, ALTER_COLUMN, ATTACH_PARTITION, DEPENDS_ON_EXTENSION, VALIDATE_CONSTRAINT, INHERIT, ALTER_CONSTRAINT_INHERIT, REPLICA_IDENTITY
22+
RENAME, RENAME_COLUMN, OWNER, SET_SCHEMA, SET_TABLESPACE, SET_ACCESS_METHOD, SET_OPTIONS, RESET_OPTIONS, ALTER_COLUMN, ATTACH_PARTITION, DEPENDS_ON_EXTENSION, VALIDATE_CONSTRAINT, INHERIT, ALTER_CONSTRAINT_INHERIT, REPLICA_IDENTITY, CLUSTER_ON, SET_WITHOUT_CLUSTER, SET_WITHOUT_OIDS, SET_LOGGED, SET_UNLOGGED, OF, NOT_OF, TRIGGER_STATE
2323
}
2424

2525
public enum ColumnAction {
@@ -30,6 +30,32 @@ public enum ReplicaIdentity {
3030
DEFAULT, FULL, NOTHING, USING_INDEX
3131
}
3232

33+
public enum TriggerState {
34+
ENABLE, DISABLE, ENABLE_ALWAYS, ENABLE_REPLICA
35+
}
36+
public enum TriggerTarget {
37+
NAME, ALL, USER
38+
}
39+
40+
private TriggerState triggerState;
41+
private TriggerTarget triggerTarget;
42+
43+
public TriggerState getTriggerState() {
44+
return triggerState;
45+
}
46+
47+
public void setTriggerState(TriggerState triggerState) {
48+
this.triggerState = triggerState;
49+
}
50+
51+
public TriggerTarget getTriggerTarget() {
52+
return triggerTarget;
53+
}
54+
55+
public void setTriggerTarget(TriggerTarget triggerTarget) {
56+
this.triggerTarget = triggerTarget;
57+
}
58+
3359
private Kind kind;
3460
private ColumnAction columnAction;
3561
private ReplicaIdentity replicaIdentity;
@@ -178,6 +204,23 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expres
178204

179205
private void appendDefinition(StringBuilder builder, Consumer<Expression> expressionPrinter) {
180206
switch (kind) {
207+
case CLUSTER_ON:
208+
builder.append("CLUSTER ON ").append(value);
209+
break;
210+
case SET_WITHOUT_CLUSTER:
211+
case SET_WITHOUT_OIDS:
212+
case SET_LOGGED:
213+
case SET_UNLOGGED:
214+
case NOT_OF:
215+
builder.append(kind.name().replace('_', ' '));
216+
break;
217+
case OF:
218+
builder.append("OF ").append(value);
219+
break;
220+
case TRIGGER_STATE:
221+
builder.append(triggerState.name().replace('_', ' ')).append(" TRIGGER ")
222+
.append(triggerTarget == TriggerTarget.NAME ? value : triggerTarget);
223+
break;
181224
case RENAME:
182225
builder.append("RENAME TO ").append(newName);
183226
break;

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package net.sf.jsqlparser.util;
1111

1212
import net.sf.jsqlparser.statement.alter.AlterRelation;
13+
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
1314
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
1415
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
1516
import net.sf.jsqlparser.statement.select.MatchRecognize;
@@ -2934,4 +2935,8 @@ public <S> Void visit(AlterDatabase statement, S context) {
29342935
return null;
29352936
}
29362937

2938+
@Override
2939+
public <S> Void visit(AlterTablespaceMove statement, S context) {
2940+
return null;
2941+
}
29372942
}

‎src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package net.sf.jsqlparser.util.deparser;
1111

1212
import net.sf.jsqlparser.statement.alter.AlterRelation;
13+
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
1314
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
1415
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
1516
import net.sf.jsqlparser.statement.oracle.OracleBlock;
@@ -822,4 +823,8 @@ public <S> StringBuilder visit(AlterDatabase statement, S context) {
822823
return statement.appendTo(builder);
823824
}
824825

826+
@Override
827+
public <S> StringBuilder visit(AlterTablespaceMove statement, S context) {
828+
return statement.appendTo(builder);
829+
}
825830
}

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

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,11 +1817,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
18171817
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
18181818
Token first = getToken(1);
18191819
if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT")
1820-
|| isKeywordAhead("REPLICA") || isKeywordAhead("OWNER")) { return true; }
1820+
|| isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER")
1821+
|| first.kind == K_OF || first.kind == K_NOT && getToken(2).kind == K_OF
1822+
|| (first.kind == K_ENABLE || first.kind == K_DISABLE)
1823+
&& (getToken(2).kind == K_TRIGGER || getToken(2).kind == K_ALWAYS
1824+
|| "REPLICA".equalsIgnoreCase(getToken(2).image))) { return true; }
18211825
if (first.kind == K_NO) { return "INHERIT".equalsIgnoreCase(getToken(2).image); }
18221826
if (first.kind == K_SET) {
18231827
return getToken(2).kind == OPENING_BRACKET || getToken(2).kind == K_SCHEMA
1824-
|| getToken(2).kind == K_TABLESPACE || "ACCESS".equalsIgnoreCase(getToken(2).image);
1828+
|| getToken(2).kind == K_TABLESPACE || "ACCESS".equalsIgnoreCase(getToken(2).image)
1829+
|| getToken(2).kind == K_WITHOUT || getToken(2).kind == K_UNLOGGED
1830+
|| "LOGGED".equalsIgnoreCase(getToken(2).image);
18251831
}
18261832
if (first.kind != K_ALTER) { return false; }
18271833
if (getToken(2).kind == K_CONSTRAINT) {
@@ -1850,7 +1856,8 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
18501856
RelationAlterAction.Kind.OWNER, RelationAlterAction.Kind.SET_SCHEMA));
18511857
if (type == AlterRelation.ObjectType.MATERIALIZED_VIEW) {
18521858
allowed.addAll(EnumSet.of(RelationAlterAction.Kind.SET_TABLESPACE,
1853-
RelationAlterAction.Kind.SET_ACCESS_METHOD, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION));
1859+
RelationAlterAction.Kind.SET_ACCESS_METHOD, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION,
1860+
RelationAlterAction.Kind.CLUSTER_ON, RelationAlterAction.Kind.SET_WITHOUT_CLUSTER));
18541861
}
18551862
}
18561863
requireDdlSyntax(allowed.contains(kind), "Unsupported " + type + " action: " + kind);
@@ -18259,6 +18266,9 @@ Statement Alter():
1825918266
|
1826018267
statement = AlterEvent()
1826118268
|
18269+
LOOKAHEAD(( <K_TABLE> | <K_INDEX> | <K_MATERIALIZED> <K_VIEW> ) <K_ALL> <K_IN> <K_TABLESPACE>)
18270+
statement = PostgreSqlTablespaceMove()
18271+
|
1826218272
statement = AlterTable()
1826318273
|
1826418274
statement = AlterSession()
@@ -18296,6 +18306,23 @@ Statement Alter():
1829618306
}
1829718307
}
1829818308

18309+
AlterTablespaceMove PostgreSqlTablespaceMove():
18310+
{
18311+
AlterTablespaceMove result = new AlterTablespaceMove();
18312+
String name;
18313+
}
18314+
{
18315+
( <K_TABLE> { result.setObjectType(AlterTablespaceMove.ObjectType.TABLE); }
18316+
| <K_INDEX> { result.setObjectType(AlterTablespaceMove.ObjectType.INDEX); }
18317+
| <K_MATERIALIZED> <K_VIEW> { result.setObjectType(AlterTablespaceMove.ObjectType.MATERIALIZED_VIEW); } )
18318+
<K_ALL> <K_IN> <K_TABLESPACE> name=RelObjectName() { result.setSourceTablespace(name); }
18319+
[ ContextualKeyword("OWNED") <K_BY> name=RelObjectName() { result.getOwners().add(name); }
18320+
( "," name=RelObjectName() { result.getOwners().add(name); } )* ]
18321+
<K_SET> <K_TABLESPACE> name=RelObjectName() { result.setTargetTablespace(name); }
18322+
[ <K_NOWAIT> { result.setNoWait(true); } ]
18323+
{ return result; }
18324+
}
18325+
1829918326
/** The new relation statements reuse the same actions as structured ALTER TABLE properties. */
1830018327
AlterRelation PostgreSqlAlterRelation():
1830118328
{
@@ -18341,6 +18368,30 @@ RelationAlterAction PostgreSqlRelationAction():
1834118368
}
1834218369
{
1834318370
(
18371+
LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER") <K_ON> name=RelObjectName()
18372+
{ action.setKind(RelationAlterAction.Kind.CLUSTER_ON); action.setValue(name); }
18373+
|
18374+
<K_OF> name=AccessQualifiedName()
18375+
{ action.setKind(RelationAlterAction.Kind.OF); action.setValue(name); }
18376+
|
18377+
<K_NOT> <K_OF> { action.setKind(RelationAlterAction.Kind.NOT_OF); }
18378+
|
18379+
( <K_DISABLE> { action.setTriggerState(RelationAlterAction.TriggerState.DISABLE); }
18380+
| <K_ENABLE> { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE); }
18381+
[ <K_ALWAYS> { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE_ALWAYS); }
18382+
| ContextualKeyword("REPLICA") { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE_REPLICA); } ] )
18383+
<K_TRIGGER>
18384+
( LOOKAHEAD(1) <K_ALL> { action.setTriggerTarget(RelationAlterAction.TriggerTarget.ALL); }
18385+
| LOOKAHEAD(1) <K_USER> { action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER); }
18386+
| name=RelObjectName() { action.setTriggerTarget(RelationAlterAction.TriggerTarget.NAME); action.setValue(name); } )
18387+
{
18388+
action.setKind(RelationAlterAction.Kind.TRIGGER_STATE);
18389+
requireDdlSyntax(action.getTriggerTarget() == RelationAlterAction.TriggerTarget.NAME
18390+
|| action.getTriggerState() == RelationAlterAction.TriggerState.ENABLE
18391+
|| action.getTriggerState() == RelationAlterAction.TriggerState.DISABLE,
18392+
"ENABLE ALWAYS/REPLICA requires a trigger name");
18393+
}
18394+
|
1834418395
<K_RENAME>
1834518396
(
1834618397
LOOKAHEAD(1) <K_TO> name=RelObjectName() {
@@ -18359,7 +18410,14 @@ RelationAlterAction PostgreSqlRelationAction():
1835918410
|
1836018411
<K_SET>
1836118412
(
18362-
<K_SCHEMA> name=RelObjectName()
18413+
<K_WITHOUT>
18414+
( LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER")
18415+
{ action.setKind(RelationAlterAction.Kind.SET_WITHOUT_CLUSTER); }
18416+
| ContextualKeyword("OIDS") { action.setKind(RelationAlterAction.Kind.SET_WITHOUT_OIDS); } )
18417+
| <K_UNLOGGED> { action.setKind(RelationAlterAction.Kind.SET_UNLOGGED); }
18418+
| LOOKAHEAD({ isKeywordAhead("LOGGED") }) ContextualKeyword("LOGGED")
18419+
{ action.setKind(RelationAlterAction.Kind.SET_LOGGED); }
18420+
| <K_SCHEMA> name=RelObjectName()
1836318421
{ action.setKind(RelationAlterAction.Kind.SET_SCHEMA); action.setValue(name); }
1836418422
| <K_TABLESPACE> name=RelObjectName()
1836518423
{ action.setKind(RelationAlterAction.Kind.SET_TABLESPACE); action.setValue(name); }

‎src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
class PostgreSqlRelationAlterTest {
3131
@ParameterizedTest
3232
@ValueSource(strings = {
33+
"ALTER TABLE t CLUSTER ON ix", "ALTER TABLE t SET WITHOUT CLUSTER",
34+
"ALTER TABLE t SET WITHOUT OIDS", "ALTER TABLE t SET LOGGED",
35+
"ALTER TABLE t SET UNLOGGED",
36+
"ALTER TABLE t OF public.typ", "ALTER TABLE t NOT OF",
37+
"ALTER TABLE t ENABLE ALWAYS TRIGGER trg", "ALTER TABLE t ENABLE REPLICA TRIGGER trg",
38+
"ALTER TABLE t DISABLE TRIGGER ALL", "ALTER TABLE t ENABLE TRIGGER USER",
39+
"ALTER TABLE t ENABLE TRIGGER \"ALL\"",
40+
"ALTER MATERIALIZED VIEW mv CLUSTER ON ix",
41+
"ALTER MATERIALIZED VIEW mv SET WITHOUT CLUSTER",
3342
"ALTER INDEX ix RENAME TO ix2",
3443
"ALTER INDEX ix SET TABLESPACE pg_default",
3544
"ALTER INDEX ix SET (fillfactor=80)",
@@ -160,6 +169,46 @@ void objectBoundariesAreEnforced(String sql) {
160169
assertThrows(JSQLParserException.class, () -> parse(sql));
161170
}
162171

172+
@Test
173+
void triggerTargetsAndTypeNamesAreMutableWithoutInventingTables() throws JSQLParserException {
174+
Alter statement = (Alter) parse("ALTER TABLE t ENABLE REPLICA TRIGGER trg");
175+
RelationAlterAction action = (RelationAlterAction) statement.getAlterExpressions().get(0);
176+
assertEquals(RelationAlterAction.TriggerState.ENABLE_REPLICA, action.getTriggerState());
177+
action.setTriggerState(RelationAlterAction.TriggerState.DISABLE);
178+
action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER);
179+
assertEquals("ALTER TABLE t DISABLE TRIGGER USER", statement.toString());
180+
assertRoundTrip(statement);
181+
statement = (Alter) parse("ALTER TABLE t OF public.typ");
182+
action = (RelationAlterAction) statement.getAlterExpressions().get(0);
183+
action.setValue("other.typ");
184+
assertEquals(java.util.Set.of("t"), new TablesNamesFinder().getTables(statement));
185+
assertRoundTrip(statement);
186+
for (String sql : List.of("ALTER TABLE t ENABLE ALWAYS TRIGGER ALL",
187+
"ALTER INDEX ix SET LOGGED", "ALTER VIEW v CLUSTER ON ix")) {
188+
assertThrows(JSQLParserException.class, () -> parse(sql));
189+
}
190+
}
191+
192+
@ParameterizedTest
193+
@ValueSource(strings = {"TABLE", "INDEX", "MATERIALIZED VIEW"})
194+
void bulkTablespaceMovesShareOneModel(String objectType) throws JSQLParserException {
195+
AlterTablespaceMove statement = (AlterTablespaceMove) parse("ALTER " + objectType
196+
+ " ALL IN TABLESPACE old_space OWNED BY alice, \"Bob\" SET TABLESPACE new_space NOWAIT");
197+
assertEquals(objectType.replace(' ', '_'), statement.getObjectType().name());
198+
assertEquals(List.of("alice", "\"Bob\""), statement.getOwners());
199+
assertEquals("old_space", statement.getSourceTablespace());
200+
assertTrue(statement.isNoWait());
201+
assertTrue(new TablesNamesFinder().getTables(statement).isEmpty());
202+
statement.setTargetTablespace("pg_default");
203+
statement.getOwners().clear();
204+
statement.setNoWait(false);
205+
assertEquals(
206+
"ALTER " + objectType + " ALL IN TABLESPACE old_space SET TABLESPACE pg_default",
207+
statement.toString());
208+
assertRoundTrip(statement);
209+
assertEquals(2, CCJSqlParserUtil.parseStatements(statement + "; SELECT 1").size());
210+
}
211+
163212
private static Statement parse(String sql) throws JSQLParserException {
164213
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
165214
}

0 commit comments

Comments
 (0)