Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,17 @@ public void setHasEqualForCollate(boolean hasEqualForCollate) {
public static final class ColumnDataType extends ColumnDefinition {

private final boolean withType;
private boolean useSetData;
private Expression usingExpression;

public boolean isUseSetData() {
return useSetData;
}

public void setUseSetData(boolean useSetData) {
this.useSetData = useSetData;
}

private List<IdentityAlteration> identityAlterations;

public boolean isWithType() {
Expand Down Expand Up @@ -1587,7 +1597,8 @@ public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrint
.append(PlainSelect.getStringList(identityAlterations, false, false));
return;
}
builder.append(withType ? " TYPE " : getColDataType() == null ? "" : " ");
builder.append(withType ? (useSetData ? " SET DATA TYPE " : " TYPE ")
: getColDataType() == null ? "" : " ");
appendDataTypeAndSpecTo(builder, expressionPrinter);
if (usingExpression != null) {
builder.append(" USING ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ public class RelationAlterAction extends AlterExpression {
private Long statistics;
private boolean statisticsDefault;
private Expression defaultExpression;
private Expression generationExpression;

public Expression getGenerationExpression() {
return generationExpression;
}

public void setGenerationExpression(Expression expression) {
generationExpression = expression;
}

private Table relation;
private boolean noInherit;
private boolean noDependency;
Expand All @@ -40,7 +50,7 @@ public enum Kind {
}

public enum ColumnAction {
SET_DEFAULT, DROP_DEFAULT, SET_STATISTICS, SET_STORAGE, SET_COMPRESSION, DROP_EXPRESSION, SET_OPTIONS, RESET_OPTIONS
SET_DEFAULT, DROP_DEFAULT, SET_STATISTICS, SET_STORAGE, SET_COMPRESSION, SET_EXPRESSION, DROP_EXPRESSION, SET_OPTIONS, RESET_OPTIONS
}

public enum ReplicaIdentity {
Expand Down Expand Up @@ -311,6 +321,12 @@ private void appendColumnChange(StringBuilder builder, Consumer<Expression> expr
case SET_COMPRESSION:
builder.append(" SET COMPRESSION ").append(value);
break;
case SET_EXPRESSION:
builder.append(" SET EXPRESSION ");
net.sf.jsqlparser.statement.create.table.GeneratedColumnDefinition
.appendExpressionTo(
builder, generationExpression, expressionPrinter);
break;
case DROP_EXPRESSION:
builder.append(" DROP EXPRESSION");
if (isUsingIfExists()) {
Expand All @@ -325,6 +341,8 @@ private void appendColumnChange(StringBuilder builder, Consumer<Expression> expr
public void visitExpressions(Consumer<Expression> visitor) {
if (kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_DEFAULT) {
visitor.accept(defaultExpression);
} else if (kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_EXPRESSION) {
visitor.accept(generationExpression);
} else if ((kind == Kind.SET_OPTIONS || kind == Kind.ALTER_COLUMN
&& columnAction == ColumnAction.SET_OPTIONS) && options != null) {
options.stream().map(Index.Option::getValue).filter(java.util.Objects::nonNull)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,21 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expres
if (generatedAlways) {
builder.append("GENERATED ALWAYS ");
}
builder.append("AS (");
expressionPrinter.accept(expression);
builder.append(')');
appendExpressionTo(builder, expression, expressionPrinter);
if (storage != null) {
builder.append(' ').append(storage);
}
return builder;
}

/** Prints the common AS (expression) body of declarations and expression replacements. */
public static StringBuilder appendExpressionTo(StringBuilder builder, Expression expression,
Consumer<Expression> expressionPrinter) {
builder.append("AS (");
expressionPrinter.accept(expression);
return builder.append(')');
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Expand Down
40 changes: 36 additions & 4 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1821,8 +1821,13 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}

private boolean isPostgreSqlRelationActionAhead() {
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
Token first = getToken(1);
int columnActionOffset = getToken(2).kind == K_COLUMN ? 4 : 3;
if (first.kind == K_ALTER && getToken(columnActionOffset).kind == K_SET
&& "EXPRESSION".equalsIgnoreCase(getToken(columnActionOffset + 1).image)) {
return true;
}
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT")
|| isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER")
|| first.kind == K_OF || first.kind == K_NOT && getToken(2).kind == K_OF
Expand Down Expand Up @@ -14343,7 +14348,7 @@ GeneratedColumnDefinition GeneratedColumnDefinition():
{
[ LOOKAHEAD({ isKeywordAhead("GENERATED") }) ContextualKeyword("GENERATED") <K_ALWAYS>
{ generatedAlways = true; } ]
<K_AS> "(" expression=Expression() ")"
expression=ColumnGenerationExpression()
[ LOOKAHEAD({ getToken(1).kind == K_STORED || isKeywordAhead("VIRTUAL") })
( <K_STORED> { storage = GeneratedColumnDefinition.Storage.STORED; }
| ContextualKeyword("VIRTUAL") { storage = GeneratedColumnDefinition.Storage.VIRTUAL; } ) ]
Expand All @@ -14355,6 +14360,14 @@ GeneratedColumnDefinition GeneratedColumnDefinition():
}
}

/** Shared expression body for generated declarations and ALTER SET EXPRESSION. */
Expression ColumnGenerationExpression():
{ Expression expression; }
{
<K_AS> "(" expression=Expression() ")"
{ return expression; }
}

/** Keeps a column key's name and attributes on the same node in CREATE and ALTER. */
NamedConstraint ColumnKeyConstraint():
{
Expand Down Expand Up @@ -16909,6 +16922,7 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
{
String columnName = null;
boolean withType = false;
boolean useSetData = false;
ColDataType dataType = null;
List<String> columnSpecs = new ArrayList<String>();
List<ColumnOption> columnOptions = new ArrayList<ColumnOption>();
Expand All @@ -16927,7 +16941,8 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
( LOOKAHEAD({ isIdentityAlterationAhead() })
identityAlteration=ColumnIdentityAlteration() { identityAlterations.add(identityAlteration); } )*
|
( LOOKAHEAD(2) <K_TYPE> { withType = true; } )?
[ LOOKAHEAD({ getToken(1).kind == K_TYPE || getToken(1).kind == K_SET && getToken(2).kind == K_DATA })
useSetData=TypeChangePrefix() { withType = true; } ]
( LOOKAHEAD(2) dataType = ColDataType() )?
( LOOKAHEAD(2, { !(withType && getToken(1).kind == K_USING) }) option = ColumnDefinitionOption() {
columnOptions.add(option);
Expand All @@ -16936,7 +16951,9 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
[ LOOKAHEAD({ withType && getToken(1).kind == K_USING }) <K_USING> usingExpression=Expression() ]
)
{
requireDdlSyntax(!withType || dataType != null, "TYPE requires a data type");
result = new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs);
result.setUseSetData(useSetData);
result.setUsingExpression(usingExpression);
result.setIdentityAlterations(identityAlterations);
if (hasStructuredColumnOption(columnOptions)) {
Expand All @@ -16946,6 +16963,14 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
}
}

/** Type changes in ALTER TABLE and ALTER TYPE accept the same optional SET DATA prefix. */
boolean TypeChangePrefix():
{ boolean useSetData = false; }
{
[ <K_SET> <K_DATA> { useSetData = true; } ] <K_TYPE>
{ return useSetData; }
}

IdentityAlteration ColumnIdentityAlteration():
{
IdentityAlteration alteration;
Expand Down Expand Up @@ -18651,6 +18676,12 @@ RelationAlterAction PostgreSqlRelationAction():
action.setDefaultExpression(expression);
}
|
LOOKAHEAD({ isKeywordAhead("EXPRESSION") }) ContextualKeyword("EXPRESSION")
expression=ColumnGenerationExpression() {
action.setColumnAction(RelationAlterAction.ColumnAction.SET_EXPRESSION);
action.setGenerationExpression(expression);
}
|
LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS")
{ action.setColumnAction(RelationAlterAction.ColumnAction.SET_STATISTICS); }
( <K_DEFAULT> { action.setStatisticsDefault(true); }
Expand Down Expand Up @@ -19960,6 +19991,7 @@ AlterType.AttributeChange TypeAttributeChange():
AlterType.AttributeChange result = new AlterType.AttributeChange();
TypeAttribute attribute = new TypeAttribute();
String name; ColDataType type; AlterType.Behavior behavior;
boolean useSetData;
}
{
(
Expand All @@ -19968,7 +20000,7 @@ AlterType.AttributeChange TypeAttributeChange():
| <K_DROP> TypeDdlKeyword("ATTRIBUTE") [ LOOKAHEAD(2) <K_IF> <K_EXISTS> { result.setIfExists(true); } ]
name=RelObjectName() { attribute.setName(name); result.setKind(AlterType.AttributeChange.Kind.DROP); }
| <K_ALTER> TypeDdlKeyword("ATTRIBUTE") name=RelObjectName() { attribute.setName(name); }
[ <K_SET> <K_DATA> { result.setUseSetData(true); } ] <K_TYPE>
useSetData=TypeChangePrefix() { result.setUseSetData(useSetData); }
type=ColDataType() { attribute.setDataType(type); result.setKind(AlterType.AttributeChange.Kind.ALTER); }
[ <K_COLLATE> name=TypeDdlName() { attribute.setCollation(name); } ]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.alter;

import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TableDefinitionTraversal;
import net.sf.jsqlparser.util.deparser.AlterDeParser;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PostgreSqlColumnReplacementTest {
@ParameterizedTest
@ValueSource(strings = {"BIGINT", "BIGINT USING id::bigint",
"VARCHAR(20) COLLATE \"C\" USING id::text"})
void sharesTypeChangePrefixes(String type) throws JSQLParserException {
for (String column : new String[] {"COLUMN ", ""}) {
for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) {
String sql = "ALTER TABLE t ALTER " + column + "id " + prefix + type;
Alter table = (Alter) parse(sql);
AlterExpression.ColumnDataType definition = table.getAlterExpressions().get(0)
.getColDataTypeList().get(0);
assertTrue(definition.isWithType());
assertEquals(prefix.startsWith("SET"), definition.isUseSetData());
assertNotEquals("SET", definition.getColDataType().getDataType());
if (type.contains("USING")) {
assertNotNull(definition.getUsingExpression());
}
roundTrip(table);
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
definition.setUseSetData(!definition.isUseSetData());
roundTrip(table);
}
}
for (String prefix : new String[] {"TYPE ", "SET DATA TYPE "}) {
roundTrip(parse("ALTER TYPE row_type ALTER ATTRIBUTE id " + prefix + "BIGINT"));
}
}

@ParameterizedTest
@ValueSource(strings = {"id + 2", "COALESCE(id, 0) * 2", "CASE WHEN id > 0 THEN id ELSE 0 END"})
void generatedExpressionIsAnActionAndNotADataType(String expression)
throws JSQLParserException {
for (String column : new String[] {"COLUMN ", ""}) {
String sql = "ALTER TABLE t ALTER " + column + "g SET EXPRESSION AS (" + expression
+ "), ADD COLUMN extra INT";
for (Statement statement : List.of(parse(sql), CCJSqlParserUtil.parse(sql))) {
Alter table = (Alter) statement;
RelationAlterAction action = assertInstanceOf(RelationAlterAction.class,
table.getAlterExpressions().get(0));
assertEquals(RelationAlterAction.ColumnAction.SET_EXPRESSION,
action.getColumnAction());
assertEquals("g", action.getColumnName());
assertNull(action.getColDataTypeList());
assertNotNull(action.getGenerationExpression());
List<Expression> expressions = new ArrayList<>();
TableDefinitionTraversal.visit(action, expressions::add, ignored -> {
});
assertEquals(List.of(action.getGenerationExpression()), expressions);
action.setGenerationExpression(new LongValue(42));
assertTrue(table.toString().contains("SET EXPRESSION AS (42)"));
roundTrip(table);
}
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
}
}

@Test
void replacementUsesTheExpressionDeparser() throws JSQLParserException {
Alter table = (Alter) parse("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (1)");
StringBuilder output = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(LongValue value, S context) {
return getBuilder().append(2);
}
};
expressions.setBuilder(output);
new AlterDeParser(output, expressions).deParse(table);
assertEquals("ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (2)", output.toString());
}

@ParameterizedTest
@ValueSource(strings = {"ALTER TABLE t ALTER COLUMN id SET DATA TYPE",
"ALTER TABLE t ALTER COLUMN id SET DATA BIGINT",
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS ()",
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS id + 2",
"ALTER TABLE t ALTER COLUMN g SET EXPRESSION AS (id+2) STORED",
"ALTER VIEW v ALTER COLUMN g SET EXPRESSION AS (id+2)",
"ALTER INDEX ix ALTER COLUMN 1 SET EXPRESSION AS (id+2)"})
void rejectsIncompleteOrWrongContextActions(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql));
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL));
}

private static void roundTrip(Statement statement) throws JSQLParserException {
StringBuilder output = new StringBuilder();
statement.accept(new StatementDeParser(output));
assertEquals(statement.toString(), output.toString());
assertEquals(output.toString(), parse(output.toString()).toString());
}
}
Loading