Skip to content

Commit 485e6f9

Browse files
authored
fix: structure PostgreSQL column type and generated expression replacements (#2716)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 51e6420 commit 485e6f9

5 files changed

Lines changed: 199 additions & 9 deletions

File tree

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1536,7 +1536,17 @@ public void setHasEqualForCollate(boolean hasEqualForCollate) {
15361536
public static final class ColumnDataType extends ColumnDefinition {
15371537

15381538
private final boolean withType;
1539+
private boolean useSetData;
15391540
private Expression usingExpression;
1541+
1542+
public boolean isUseSetData() {
1543+
return useSetData;
1544+
}
1545+
1546+
public void setUseSetData(boolean useSetData) {
1547+
this.useSetData = useSetData;
1548+
}
1549+
15401550
private List<IdentityAlteration> identityAlterations;
15411551

15421552
public boolean isWithType() {
@@ -1587,7 +1597,8 @@ public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrint
15871597
.append(PlainSelect.getStringList(identityAlterations, false, false));
15881598
return;
15891599
}
1590-
builder.append(withType ? " TYPE " : getColDataType() == null ? "" : " ");
1600+
builder.append(withType ? (useSetData ? " SET DATA TYPE " : " TYPE ")
1601+
: getColDataType() == null ? "" : " ");
15911602
appendDataTypeAndSpecTo(builder, expressionPrinter);
15921603
if (usingExpression != null) {
15931604
builder.append(" USING ");

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public class RelationAlterAction extends AlterExpression {
2929
private Long statistics;
3030
private boolean statisticsDefault;
3131
private Expression defaultExpression;
32+
private Expression generationExpression;
33+
34+
public Expression getGenerationExpression() {
35+
return generationExpression;
36+
}
37+
38+
public void setGenerationExpression(Expression expression) {
39+
generationExpression = expression;
40+
}
41+
3242
private Table relation;
3343
private boolean noInherit;
3444
private boolean noDependency;
@@ -40,7 +50,7 @@ public enum Kind {
4050
}
4151

4252
public enum ColumnAction {
43-
SET_DEFAULT, DROP_DEFAULT, SET_STATISTICS, SET_STORAGE, SET_COMPRESSION, DROP_EXPRESSION, SET_OPTIONS, RESET_OPTIONS
53+
SET_DEFAULT, DROP_DEFAULT, SET_STATISTICS, SET_STORAGE, SET_COMPRESSION, SET_EXPRESSION, DROP_EXPRESSION, SET_OPTIONS, RESET_OPTIONS
4454
}
4555

4656
public enum ReplicaIdentity {
@@ -311,6 +321,12 @@ private void appendColumnChange(StringBuilder builder, Consumer<Expression> expr
311321
case SET_COMPRESSION:
312322
builder.append(" SET COMPRESSION ").append(value);
313323
break;
324+
case SET_EXPRESSION:
325+
builder.append(" SET EXPRESSION ");
326+
net.sf.jsqlparser.statement.create.table.GeneratedColumnDefinition
327+
.appendExpressionTo(
328+
builder, generationExpression, expressionPrinter);
329+
break;
314330
case DROP_EXPRESSION:
315331
builder.append(" DROP EXPRESSION");
316332
if (isUsingIfExists()) {
@@ -325,6 +341,8 @@ private void appendColumnChange(StringBuilder builder, Consumer<Expression> expr
325341
public void visitExpressions(Consumer<Expression> visitor) {
326342
if (kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_DEFAULT) {
327343
visitor.accept(defaultExpression);
344+
} else if (kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_EXPRESSION) {
345+
visitor.accept(generationExpression);
328346
} else if ((kind == Kind.SET_OPTIONS || kind == Kind.ALTER_COLUMN
329347
&& columnAction == ColumnAction.SET_OPTIONS) && options != null) {
330348
options.stream().map(Index.Option::getValue).filter(java.util.Objects::nonNull)

‎src/main/java/net/sf/jsqlparser/statement/create/table/GeneratedColumnDefinition.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,21 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expres
7373
if (generatedAlways) {
7474
builder.append("GENERATED ALWAYS ");
7575
}
76-
builder.append("AS (");
77-
expressionPrinter.accept(expression);
78-
builder.append(')');
76+
appendExpressionTo(builder, expression, expressionPrinter);
7977
if (storage != null) {
8078
builder.append(' ').append(storage);
8179
}
8280
return builder;
8381
}
8482

83+
/** Prints the common AS (expression) body of declarations and expression replacements. */
84+
public static StringBuilder appendExpressionTo(StringBuilder builder, Expression expression,
85+
Consumer<Expression> expressionPrinter) {
86+
builder.append("AS (");
87+
expressionPrinter.accept(expression);
88+
return builder.append(')');
89+
}
90+
8591
@Override
8692
public String toString() {
8793
StringBuilder builder = new StringBuilder();

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,8 +1833,13 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
18331833
}
18341834

18351835
private boolean isPostgreSqlRelationActionAhead() {
1836-
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
18371836
Token first = getToken(1);
1837+
int columnActionOffset = getToken(2).kind == K_COLUMN ? 4 : 3;
1838+
if (first.kind == K_ALTER && getToken(columnActionOffset).kind == K_SET
1839+
&& "EXPRESSION".equalsIgnoreCase(getToken(columnActionOffset + 1).image)) {
1840+
return true;
1841+
}
1842+
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
18381843
if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT")
18391844
|| isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER")
18401845
|| first.kind == K_OF || first.kind == K_NOT && getToken(2).kind == K_OF
@@ -14355,7 +14360,7 @@ GeneratedColumnDefinition GeneratedColumnDefinition():
1435514360
{
1435614361
[ LOOKAHEAD({ isKeywordAhead("GENERATED") }) ContextualKeyword("GENERATED") <K_ALWAYS>
1435714362
{ generatedAlways = true; } ]
14358-
<K_AS> "(" expression=Expression() ")"
14363+
expression=ColumnGenerationExpression()
1435914364
[ LOOKAHEAD({ getToken(1).kind == K_STORED || isKeywordAhead("VIRTUAL") })
1436014365
( <K_STORED> { storage = GeneratedColumnDefinition.Storage.STORED; }
1436114366
| ContextualKeyword("VIRTUAL") { storage = GeneratedColumnDefinition.Storage.VIRTUAL; } ) ]
@@ -14367,6 +14372,14 @@ GeneratedColumnDefinition GeneratedColumnDefinition():
1436714372
}
1436814373
}
1436914374

14375+
/** Shared expression body for generated declarations and ALTER SET EXPRESSION. */
14376+
Expression ColumnGenerationExpression():
14377+
{ Expression expression; }
14378+
{
14379+
<K_AS> "(" expression=Expression() ")"
14380+
{ return expression; }
14381+
}
14382+
1437014383
/** Keeps a column key's name and attributes on the same node in CREATE and ALTER. */
1437114384
NamedConstraint ColumnKeyConstraint():
1437214385
{
@@ -16959,6 +16972,7 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1695916972
{
1696016973
String columnName = null;
1696116974
boolean withType = false;
16975+
boolean useSetData = false;
1696216976
ColDataType dataType = null;
1696316977
List<String> columnSpecs = new ArrayList<String>();
1696416978
List<ColumnOption> columnOptions = new ArrayList<ColumnOption>();
@@ -16977,7 +16991,8 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1697716991
( LOOKAHEAD({ isIdentityAlterationAhead() })
1697816992
identityAlteration=ColumnIdentityAlteration() { identityAlterations.add(identityAlteration); } )*
1697916993
|
16980-
( LOOKAHEAD(2) <K_TYPE> { withType = true; } )?
16994+
[ LOOKAHEAD({ getToken(1).kind == K_TYPE || getToken(1).kind == K_SET && getToken(2).kind == K_DATA })
16995+
useSetData=TypeChangePrefix() { withType = true; } ]
1698116996
( LOOKAHEAD(2) dataType = ColDataType() )?
1698216997
( LOOKAHEAD(2, { !(withType && getToken(1).kind == K_USING) }) option = ColumnDefinitionOption() {
1698316998
columnOptions.add(option);
@@ -16986,7 +17001,9 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1698617001
[ LOOKAHEAD({ withType && getToken(1).kind == K_USING }) <K_USING> usingExpression=Expression() ]
1698717002
)
1698817003
{
17004+
requireDdlSyntax(!withType || dataType != null, "TYPE requires a data type");
1698917005
result = new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs);
17006+
result.setUseSetData(useSetData);
1699017007
result.setUsingExpression(usingExpression);
1699117008
result.setIdentityAlterations(identityAlterations);
1699217009
if (hasStructuredColumnOption(columnOptions)) {
@@ -16996,6 +17013,14 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1699617013
}
1699717014
}
1699817015

17016+
/** Type changes in ALTER TABLE and ALTER TYPE accept the same optional SET DATA prefix. */
17017+
boolean TypeChangePrefix():
17018+
{ boolean useSetData = false; }
17019+
{
17020+
[ <K_SET> <K_DATA> { useSetData = true; } ] <K_TYPE>
17021+
{ return useSetData; }
17022+
}
17023+
1699917024
IdentityAlteration ColumnIdentityAlteration():
1700017025
{
1700117026
IdentityAlteration alteration;
@@ -18638,6 +18663,12 @@ RelationAlterAction PostgreSqlRelationAction():
1863818663
action.setDefaultExpression(expression);
1863918664
}
1864018665
|
18666+
LOOKAHEAD({ isKeywordAhead("EXPRESSION") }) ContextualKeyword("EXPRESSION")
18667+
expression=ColumnGenerationExpression() {
18668+
action.setColumnAction(RelationAlterAction.ColumnAction.SET_EXPRESSION);
18669+
action.setGenerationExpression(expression);
18670+
}
18671+
|
1864118672
LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS")
1864218673
{ action.setColumnAction(RelationAlterAction.ColumnAction.SET_STATISTICS); }
1864318674
( <K_DEFAULT> { action.setStatisticsDefault(true); }
@@ -19952,6 +19983,7 @@ AlterType.AttributeChange TypeAttributeChange():
1995219983
AlterType.AttributeChange result = new AlterType.AttributeChange();
1995319984
TypeAttribute attribute = new TypeAttribute();
1995419985
String name; ColDataType type; AlterType.Behavior behavior;
19986+
boolean useSetData;
1995519987
}
1995619988
{
1995719989
(
@@ -19960,7 +19992,7 @@ AlterType.AttributeChange TypeAttributeChange():
1996019992
| <K_DROP> TypeDdlKeyword("ATTRIBUTE") [ LOOKAHEAD(2) <K_IF> <K_EXISTS> { result.setIfExists(true); } ]
1996119993
name=RelObjectName() { attribute.setName(name); result.setKind(AlterType.AttributeChange.Kind.DROP); }
1996219994
| <K_ALTER> TypeDdlKeyword("ATTRIBUTE") name=RelObjectName() { attribute.setName(name); }
19963-
[ <K_SET> <K_DATA> { result.setUseSetData(true); } ] <K_TYPE>
19995+
useSetData=TypeChangePrefix() { result.setUseSetData(useSetData); }
1996419996
type=ColDataType() { attribute.setDataType(type); result.setKind(AlterType.AttributeChange.Kind.ALTER); }
1996519997
[ <K_COLLATE> name=TypeDdlName() { attribute.setCollation(name); } ]
1996619998
)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.Expression;
18+
import net.sf.jsqlparser.expression.LongValue;
19+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
20+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
21+
import net.sf.jsqlparser.statement.Statement;
22+
import net.sf.jsqlparser.util.TableDefinitionTraversal;
23+
import net.sf.jsqlparser.util.deparser.AlterDeParser;
24+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
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 PostgreSqlColumnReplacementTest {
31+
@ParameterizedTest
32+
@ValueSource(strings = {"BIGINT", "BIGINT USING id::bigint",
33+
"VARCHAR(20) COLLATE \"C\" USING id::text"})
34+
void sharesTypeChangePrefixes(String type) throws JSQLParserException {
35+
for (String column : new String[] {"COLUMN ", ""}) {
36+
for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) {
37+
String sql = "ALTER TABLE t ALTER " + column + "id " + prefix + type;
38+
Alter table = (Alter) parse(sql);
39+
AlterExpression.ColumnDataType definition = table.getAlterExpressions().get(0)
40+
.getColDataTypeList().get(0);
41+
assertTrue(definition.isWithType());
42+
assertEquals(prefix.startsWith("SET"), definition.isUseSetData());
43+
assertNotEquals("SET", definition.getColDataType().getDataType());
44+
if (type.contains("USING")) {
45+
assertNotNull(definition.getUsingExpression());
46+
}
47+
roundTrip(table);
48+
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
49+
definition.setUseSetData(!definition.isUseSetData());
50+
roundTrip(table);
51+
}
52+
}
53+
for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) {
54+
roundTrip(parse("ALTER TYPE row_type ALTER ATTRIBUTE id " + prefix + "BIGINT"));
55+
}
56+
}
57+
58+
@ParameterizedTest
59+
@ValueSource(strings = {"id + 2", "COALESCE(id, 0) * 2", "CASE WHEN id > 0 THEN id ELSE 0 END"})
60+
void generatedExpressionIsAnActionAndNotADataType(String expression)
61+
throws JSQLParserException {
62+
for (String column : new String[] {"COLUMN ", ""}) {
63+
String sql = "ALTER TABLE t ALTER " + column + "g SET EXPRESSION AS (" + expression
64+
+ "), ADD COLUMN extra INT";
65+
for (Statement statement : List.of(parse(sql), CCJSqlParserUtil.parse(sql))) {
66+
Alter table = (Alter) statement;
67+
RelationAlterAction action = assertInstanceOf(RelationAlterAction.class,
68+
table.getAlterExpressions().get(0));
69+
assertEquals(RelationAlterAction.ColumnAction.SET_EXPRESSION,
70+
action.getColumnAction());
71+
assertEquals("g", action.getColumnName());
72+
assertNull(action.getColDataTypeList());
73+
assertNotNull(action.getGenerationExpression());
74+
List<Expression> expressions = new ArrayList<>();
75+
TableDefinitionTraversal.visit(action, expressions::add, ignored -> {
76+
});
77+
assertEquals(List.of(action.getGenerationExpression()), expressions);
78+
action.setGenerationExpression(new LongValue(42));
79+
assertTrue(table.toString().contains("SET EXPRESSION AS (42)"));
80+
roundTrip(table);
81+
}
82+
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
83+
}
84+
}
85+
86+
@Test
87+
void replacementUsesTheExpressionDeparser() throws JSQLParserException {
88+
Alter table = (Alter) parse("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (1)");
89+
StringBuilder output = new StringBuilder();
90+
ExpressionDeParser expressions = new ExpressionDeParser() {
91+
@Override
92+
public <S> StringBuilder visit(LongValue value, S context) {
93+
return getBuilder().append(2);
94+
}
95+
};
96+
expressions.setBuilder(output);
97+
new AlterDeParser(output, expressions).deParse(table);
98+
assertEquals("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (2)", output.toString());
99+
}
100+
101+
@ParameterizedTest
102+
@ValueSource(strings = {"ALTER TABLE t ALTER COLUMN id SET DATA TYPE",
103+
"ALTER TABLE t ALTER COLUMN id SET DATA BIGINT",
104+
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS ()",
105+
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS id + 2",
106+
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (id+2) STORED",
107+
"ALTER VIEW v ALTER COLUMN g SET EXPRESSION AS (id+2)",
108+
"ALTER INDEX ix ALTER COLUMN 1 SET EXPRESSION AS (id+2)"})
109+
void rejectsIncompleteOrWrongContextActions(String sql) {
110+
assertThrows(JSQLParserException.class, () -> parse(sql));
111+
}
112+
113+
private static Statement parse(String sql) throws JSQLParserException {
114+
return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL));
115+
}
116+
117+
private static void roundTrip(Statement statement) throws JSQLParserException {
118+
StringBuilder output = new StringBuilder();
119+
statement.accept(new StatementDeParser(output));
120+
assertEquals(statement.toString(), output.toString());
121+
assertEquals(output.toString(), parse(output.toString()).toString());
122+
}
123+
}

0 commit comments

Comments
 (0)