From f230f4cf8804d4ade501c04062fc0d77c81ed93a Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 11 Nov 2026 12:36:58 +0900 Subject: [PATCH] feat: parse PostgreSQL base type definitions, ALTER TYPE SET and ALTER RULE CREATE TYPE name (INPUT = fn, OUTPUT = fn, ...) and ALTER TYPE name SET (property = value, ...) failed at the parenthesis, and ALTER RULE name ON table RENAME TO new_name was an unsupported statement. CreateType gets a BaseTypeDefinition next to the composite, enum and range definitions, and AlterType the action SET_PROPERTIES with getProperties(). Both hold DefinitionElement items read by one PostgreSqlDefinition() rule that follows the definition of PostgreSQL's grammar: any label, and an optional DefinitionArgument that is a type (which is how the grammar reads a function name), a reserved keyword, an operator, a number, a string constant or NONE. Property names and the kind of value a property takes are not checked, as PostgreSQL checks them when it runs the statement. ALTER RULE parses to AlterRule with the rule name, the table and the new name, and is wired into the deparser, visitors, table finder, validation (alterRule, enabled for PostgreSQL) and feature analysis. It is the only ALTER RULE form, so other text after ALTER RULE is now a parse error. Signed-off-by: minleejae --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 6 + .../statement/DefinitionArgument.java | 179 +++++++++ .../statement/DefinitionElement.java | 69 ++++ .../statement/StatementFeatureVisitor.java | 6 + .../statement/StatementVisitor.java | 9 + .../statement/StatementVisitorAdapter.java | 7 + .../jsqlparser/statement/alter/AlterRule.java | 84 ++++ .../jsqlparser/statement/alter/AlterType.java | 21 +- .../create/type/BaseTypeDefinition.java | 54 +++ .../statement/create/type/CreateType.java | 7 +- .../statement/create/type/TypeDefinition.java | 2 +- .../sf/jsqlparser/util/TablesNamesFinder.java | 7 + .../util/deparser/StatementDeParser.java | 6 + .../validation/feature/FeaturesAllowed.java | 2 +- .../validation/feature/PostgresqlVersion.java | 3 +- .../validator/StatementValidator.java | 8 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 179 ++++++++- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 28 +- .../parser/ASTNodeCoverageTest.java | 4 + ...tatementFeatureVisitorMetadataDdlTest.java | 2 + .../statement/alter/AlterRuleTest.java | 152 ++++++++ .../create/PostgreSqlBaseTypeTest.java | 365 ++++++++++++++++++ src/test/resources/postgresql/type-ddl.sql | 3 + 25 files changed, 1197 insertions(+), 9 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/DefinitionArgument.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterRule.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/AlterRuleTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlBaseTypeTest.java diff --git a/README.md b/README.md index 0fdeacbe3a..f331022e82 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` | | **MySQL metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` | | **PostgreSQL administration** | `CREATE`/`ALTER EVENT TRIGGER` · `REINDEX` · `SECURITY LABEL` | +| **PostgreSQL types and rules** | `CREATE TYPE` (shell, composite, enum, range and base types) · `ALTER TYPE` · `CREATE RULE` · `ALTER RULE` | | **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 7382b49e24..883e86ea01 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -945,6 +945,12 @@ public enum Feature { * @see net.sf.jsqlparser.statement.alter.AlterTrigger */ alterTrigger, + /** + * PostgreSQL "ALTER RULE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterRule + */ + alterRule, /** * SQL block starting with "BEGIN" and ends with "END" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/DefinitionArgument.java b/src/main/java/net/sf/jsqlparser/statement/DefinitionArgument.java new file mode 100644 index 0000000000..cedf241970 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/DefinitionArgument.java @@ -0,0 +1,179 @@ +/*- + * #%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; + +import java.io.Serializable; +import java.util.Objects; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +/** + * The value of a {@link DefinitionElement}: PostgreSQL's {@code def_arg}. The grammar reads a name, + * such as a function or a storage strategy, as a type name, so it is a {@link Kind#TYPE} argument. + */ +public final class DefinitionArgument implements Serializable { + public enum Kind { + /** A type or a name, see {@link #getDataType()}. */ + TYPE, + /** A reserved keyword such as {@code TRUE} or {@code NULL}, see {@link #getKeyword()}. */ + KEYWORD, + /** An operator, see {@link #getOperator()}. */ + OPERATOR, + /** A number with an optional sign, see {@link #getNumber()}. */ + NUMBER, + /** A string constant, see {@link #getString()}. */ + STRING, + /** The keyword {@code NONE}. */ + NONE + } + + private final Kind kind; + private ColDataType dataType; + private boolean setOf; + private boolean typeReference; + private String keyword; + private String operator; + private boolean operatorKeyword; + private Expression number; + private StringValue string; + + private DefinitionArgument(Kind kind) { + this.kind = kind; + } + + public static DefinitionArgument type(ColDataType dataType) { + return type(dataType, false, false); + } + + /** + * A type argument. + * + * @param dataType the type or name + * @param setOf whether {@code SETOF} precedes the type + * @param typeReference whether {@code %TYPE} follows the name of a column + * @return the argument + */ + public static DefinitionArgument type(ColDataType dataType, boolean setOf, + boolean typeReference) { + DefinitionArgument argument = new DefinitionArgument(Kind.TYPE); + argument.dataType = Objects.requireNonNull(dataType); + argument.setOf = setOf; + argument.typeReference = typeReference; + return argument; + } + + public static DefinitionArgument keyword(String keyword) { + DefinitionArgument argument = new DefinitionArgument(Kind.KEYWORD); + argument.keyword = Objects.requireNonNull(keyword); + return argument; + } + + /** + * An operator argument. + * + * @param operator the operator, with its schema when written as {@code OPERATOR(schema.op)} + * @param operatorKeyword whether the operator is written as {@code OPERATOR(...)} + * @return the argument + */ + public static DefinitionArgument operator(String operator, boolean operatorKeyword) { + DefinitionArgument argument = new DefinitionArgument(Kind.OPERATOR); + argument.operator = Objects.requireNonNull(operator); + argument.operatorKeyword = operatorKeyword; + return argument; + } + + public static DefinitionArgument number(Expression number) { + DefinitionArgument argument = new DefinitionArgument(Kind.NUMBER); + argument.number = Objects.requireNonNull(number); + return argument; + } + + public static DefinitionArgument string(StringValue string) { + DefinitionArgument argument = new DefinitionArgument(Kind.STRING); + argument.string = Objects.requireNonNull(string); + return argument; + } + + public static DefinitionArgument none() { + return new DefinitionArgument(Kind.NONE); + } + + public Kind getKind() { + return kind; + } + + /** + * The type of a {@link Kind#TYPE} argument. A plain name, such as the function of + * {@code INPUT = box3_in}, is the type's {@link ColDataType#getDataType() name}. + * + * @return the type, or {@code null} for another kind + */ + public ColDataType getDataType() { + return dataType; + } + + public boolean isSetOf() { + return setOf; + } + + /** + * Whether {@code %TYPE} follows the name, which then names a column. + * + * @return {@code true} for {@code table.column%TYPE} + */ + public boolean isTypeReference() { + return typeReference; + } + + public String getKeyword() { + return keyword; + } + + public String getOperator() { + return operator; + } + + public boolean isOperatorKeyword() { + return operatorKeyword; + } + + /** + * The number of a {@link Kind#NUMBER} argument: a long or double value, inside a signed + * expression when a sign is written. + * + * @return the number, or {@code null} for another kind + */ + public Expression getNumber() { + return number; + } + + public StringValue getString() { + return string; + } + + @Override + public String toString() { + switch (kind) { + case TYPE: + return (setOf ? "SETOF " : "") + dataType + (typeReference ? "%TYPE" : ""); + case KEYWORD: + return keyword; + case OPERATOR: + return operatorKeyword ? "OPERATOR(" + operator + ")" : operator; + case NUMBER: + return number.toString(); + case STRING: + return string.toString(); + default: + return "NONE"; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java b/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java new file mode 100644 index 0000000000..3c420ff49e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/DefinitionElement.java @@ -0,0 +1,69 @@ +/*- + * #%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; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * One element of a PostgreSQL definition list, {@code name [= value]}, as in + * {@code CREATE TYPE name (INPUT = fn, PASSEDBYVALUE)} or + * {@code ALTER TYPE name SET (SEND = NONE)}. The name keeps its spelling and quoting. + */ +public class DefinitionElement implements Serializable { + private String name; + private DefinitionArgument argument; + + public DefinitionElement() { + // empty constructor + } + + public DefinitionElement(String name, DefinitionArgument argument) { + this.name = name; + this.argument = argument; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The value after the equals sign. + * + * @return the argument, or {@code null} for an element without a value + */ + public DefinitionArgument getArgument() { + return argument; + } + + public void setArgument(DefinitionArgument argument) { + this.argument = argument; + } + + /** + * Renders a definition list in parentheses. + * + * @param elements the elements + * @return the list, such as {@code (INPUT = fn, PASSEDBYVALUE)} + */ + public static String toString(List elements) { + return elements.stream().map(Object::toString).collect(Collectors.joining(", ", "(", ")")); + } + + @Override + public String toString() { + return argument == null ? name : name + " = " + argument; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index be1c4cf274..651f134ad8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -36,6 +36,7 @@ import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterPublication; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -1489,4 +1490,9 @@ public Void visit(CreateCollation statement, S context) { return schemaOnly(); } + + @Override + public Void visit(AlterRule statement, S context) { + return schemaOnly(); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 2711d3a764..94ce1b5e33 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -24,6 +24,7 @@ import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.create.collation.CreateCollation; import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.AlterPolicy; @@ -1119,4 +1120,12 @@ default T visit(AlterTrigger statement, S context) { default void visit(AlterTrigger statement) { visit(statement, null); } + + default T visit(AlterRule statement, S context) { + return null; + } + + default void visit(AlterRule statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 2d43aff589..89575fcba2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterRoutine; @@ -1193,4 +1194,10 @@ public T visit(AlterTrigger statement, S context) { return null; } + + @Override + public T visit(AlterRule statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterRule.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRule.java new file mode 100644 index 0000000000..b37ac48b76 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRule.java @@ -0,0 +1,84 @@ +/*- + * #%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 net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code ALTER RULE name ON table RENAME TO new_name}. Names retain SQL identifier + * quoting. + */ +public class AlterRule extends ASTNodeAccessImpl implements Statement { + private String name; + private Table table; + private String newName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The table or view the rule applies to. + * + * @return the table + */ + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public AlterRule withName(String name) { + setName(name); + return this; + } + + public AlterRule withTable(Table table) { + setTable(table); + return this; + } + + public AlterRule withNewName(String newName) { + setNewName(newName); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("ALTER RULE ").append(name).append(" ON ").append(table) + .append(" RENAME TO ").append(newName); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterType.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterType.java index ba486a89ae..db987fca36 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterType.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterType.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.DefinitionElement; import net.sf.jsqlparser.statement.create.type.TypeAttribute; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -31,6 +32,7 @@ public class AlterType extends ASTNodeAccessImpl implements Statement { private boolean ifNotExists; private CascadeBehavior behavior; private List attributeChanges = new ArrayList<>(); + private List properties = new ArrayList<>(); public String getName() { return name; @@ -120,13 +122,27 @@ public void setAttributeChanges(List attributeChanges) { this.attributeChanges = attributeChanges; } + /** + * The properties of {@code SET (property = value, ...)}, which changes a base type. PostgreSQL + * checks the property names when it runs the statement, so they are not restricted here. + * + * @return the properties in source order, empty for another action + */ + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + @Override public T accept(StatementVisitor visitor, S context) { return visitor.visit(this, context); } public enum Action { - OWNER, RENAME, SET_SCHEMA, RENAME_ATTRIBUTE, ADD_VALUE, RENAME_VALUE, ATTRIBUTES + OWNER, RENAME, SET_SCHEMA, RENAME_ATTRIBUTE, ADD_VALUE, RENAME_VALUE, ATTRIBUTES, SET_PROPERTIES } public enum Position { BEFORE, AFTER @@ -238,6 +254,9 @@ public String toString() { sql.append(attributeChanges.stream().map(Object::toString) .collect(Collectors.joining(", "))); break; + case SET_PROPERTIES: + sql.append("SET ").append(DefinitionElement.toString(properties)); + break; default: throw new IllegalStateException("Unknown type alteration: " + action); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java new file mode 100644 index 0000000000..11e81f2a3f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/BaseTypeDefinition.java @@ -0,0 +1,54 @@ +/*- + * #%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.create.type; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.statement.DefinitionElement; + +/** + * Definition of a PostgreSQL base type, {@code CREATE TYPE name (INPUT = fn, OUTPUT = fn, ...)}. + * PostgreSQL only warns about a name it does not know, so the names are not restricted. + */ +public class BaseTypeDefinition implements TypeDefinition { + private List elements = new ArrayList<>(); + + public BaseTypeDefinition() { + // empty constructor + } + + public BaseTypeDefinition(List elements) { + this.elements = elements; + } + + public List getElements() { + return elements; + } + + public void setElements(List elements) { + this.elements = elements; + } + + /** + * Finds an element by its name, ignoring case. + * + * @param name the name, such as {@code INPUT} + * @return the first element of that name, or {@code null} + */ + public DefinitionElement getElement(String name) { + return elements.stream().filter(element -> name.equalsIgnoreCase(element.getName())) + .findFirst().orElse(null); + } + + @Override + public String toString() { + return DefinitionElement.toString(elements); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java b/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java index 4d84bbd229..592f702c2d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/CreateType.java @@ -41,6 +41,11 @@ public T accept(StatementVisitor visitor, S context) { @Override public String toString() { - return "CREATE TYPE " + name + (definition == null ? "" : " AS " + definition); + if (definition == null) { + return "CREATE TYPE " + name; + } + // only a base type definition follows the name directly + return "CREATE TYPE " + name + (definition instanceof BaseTypeDefinition ? " " : " AS ") + + definition; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/type/TypeDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/type/TypeDefinition.java index 274e2f7b09..fa85b9b462 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/type/TypeDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/type/TypeDefinition.java @@ -11,6 +11,6 @@ import java.io.Serializable; -/** Definition of a composite, enumeration or range type. */ +/** Definition of a composite, enumeration, range or base type. */ public interface TypeDefinition extends Serializable { } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f65ddf14f6..fc439877bb 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -129,6 +129,7 @@ import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterPublication; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -3445,4 +3446,10 @@ public Void visit(AlterTrigger statement, S context) { visit(statement.getTable(), context); return null; } + + @Override + public Void visit(AlterRule statement, S context) { + visit(statement.getTable(), context); + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 41defbd120..9001baa42d 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -32,6 +32,7 @@ import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; @@ -1122,4 +1123,9 @@ public StringBuilder visit(AlterRoutine statement, S context) { public StringBuilder visit(AlterTrigger statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(AlterRule statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 0a855c46e8..e8992ce572 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -121,7 +121,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS public static final FeaturesAllowed ALTER = new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, Feature.alterTextSearchConfiguration, - Feature.alterCollation, + Feature.alterCollation, Feature.alterRule, Feature.alterTablespace, Feature.alterEventTrigger, Feature.alterEvent, Feature.alterPolicy, Feature.alterStatistics, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index 23e3cc9a19..abd6616439 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -164,8 +164,9 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-alterprocedure.html // https://www.postgresql.org/docs/current/sql-alterroutine.html // https://www.postgresql.org/docs/current/sql-altertrigger.html + // https://www.postgresql.org/docs/current/sql-alterrule.html Feature.alterFunction, Feature.alterProcedure, Feature.alterRoutine, - Feature.alterTrigger, + Feature.alterTrigger, Feature.alterRule, // https://www.postgresql.org/docs/16/sql-refreshmaterializedview.html Feature.refreshMaterializedView, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index a69059598b..503fffe453 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterPolicy; @@ -1583,4 +1584,11 @@ public Void visit(AlterStatistics statement, S context) { return null; } + + @Override + public Void visit(AlterRule statement, S context) { + validateFeature(Feature.alterRule); + validateOptionalFromItem(statement.getTable()); + return null; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e4fcc2e76e..1be7d5e710 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2780,6 +2780,67 @@ public class CCJSqlParser extends AbstractJSqlParser { "FREEZE", "FULL", "ILIKE", "INNER", "IS", "ISNULL", "JOIN", "LEFT", "LIKE", "NATURAL", "NOTNULL", "OUTER", "OVERLAPS", "RIGHT", "SIMILAR", "TABLESAMPLE", "VERBOSE")); + /** + * PostgreSQL 18's column name keywords that are not a type name by themselves, so its grammar + * takes none of them as the single word of a definition value. NONE is one more, but a + * definition value takes it as the keyword it is. + */ + private static final Set POSTGRESQL_NON_TYPE_COL_NAME_KEYWORDS = new HashSet(Arrays.asList( + "BETWEEN", "COALESCE", "EXISTS", "EXTRACT", "GREATEST", "GROUPING", "INOUT", "JSON_ARRAY", + "JSON_ARRAYAGG", "JSON_EXISTS", "JSON_OBJECT", "JSON_OBJECTAGG", "JSON_QUERY", + "JSON_SCALAR", "JSON_SERIALIZE", "JSON_TABLE", "JSON_VALUE", "LEAST", "MERGE_ACTION", + "NATIONAL", "NORMALIZE", "NULLIF", "OUT", "OVERLAY", "POSITION", "PRECISION", "ROW", + "SETOF", "SUBSTRING", "TREAT", "TRIM", "VALUES", "XMLATTRIBUTES", "XMLCONCAT", + "XMLELEMENT", "XMLEXISTS", "XMLFOREST", "XMLNAMESPACES", "XMLPARSE", "XMLPI", "XMLROOT", + "XMLSERIALIZE", "XMLTABLE")); + + /** Whether the token is an identifier or a keyword: a word PostgreSQL takes as a label. */ + private static boolean isPostgreSqlWord(Token token) { + return token.kind == S_IDENTIFIER || token.kind == S_QUOTED_IDENTIFIER || token.kind == DATA_TYPE + || token.kind != EOF && token.kind != S_CHAR_LITERAL && token.image != null + && token.image.matches("[A-Za-z_][A-Za-z0-9_$]*"); + } + + /** Whether "()" occurs in the tokens from first to last, as in the type modifiers of f(). */ + private static boolean hasEmptyParentheses(Token first, Token last) { + for (Token token = first; token != last && token.next != null; token = token.next) { + if (token.kind == OPENING_BRACKET && token.next.kind == CLOSING_BRACKET) { + return true; + } + } + return false; + } + + /** Whether a definition list follows: a parenthesis, a label and then "=", "," or ")". */ + private boolean isDefinitionAhead() { + String next = getToken(3).image; + return getToken(1).kind == OPENING_BRACKET && isPostgreSqlWord(getToken(2)) + && ("=".equals(next) || ",".equals(next) || ")".equals(next)); + } + + /** Whether the value of a definition element is one word, which no type rule needs to read. */ + private boolean isDefinitionWordValueAhead() { + String next = getToken(2).image; + return isPostgreSqlWord(getToken(1)) && (",".equals(next) || ")".equals(next)); + } + + /** + * The one-word value of a definition element: NONE, a reserved keyword, or a name, which + * PostgreSQL reads as a type name. + */ + private DefinitionArgument definitionWordValue(Token token) throws ParseException { + String upper = token.image.toUpperCase(Locale.ROOT); + if (token.kind == K_NONE) { + return DefinitionArgument.none(); + } + if (POSTGRESQL_RESERVED_KEYWORDS.contains(upper)) { + return DefinitionArgument.keyword(token.image); + } + requireDdlSyntax(!POSTGRESQL_NON_TYPE_COL_NAME_KEYWORDS.contains(upper), + "PostgreSQL does not take " + token.image + " as a definition value"); + return DefinitionArgument.type(linkToLastToken(new ColDataType(token.image))); + } + /** Rejects an unquoted utility option name or word value that PostgreSQL reserves. */ private void requireUtilityOptionWord(String word, boolean name) throws ParseException { String upper = word.toUpperCase(Locale.ROOT); @@ -23507,6 +23568,8 @@ Statement Alter(): | LOOKAHEAD({ isKeywordAhead("COLLATION") }) statement = AlterCollation() | + LOOKAHEAD({ isKeywordAhead("RULE") }) statement = AlterRule() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | // Other routine forms, such as SQL Server definitions, remain unsupported. @@ -25976,10 +26039,15 @@ CreateType CreateType(): CompositeTypeDefinition composite = new CompositeTypeDefinition(); RangeTypeDefinition range = new RangeTypeDefinition(); RangeTypeDefinition.Option option; + List elements; } { name=TypeDdlName() { result.setName(name); } [ + // A base type. A parenthesis can also start the statement after a shell type. + LOOKAHEAD({ isDefinitionAhead() }) + elements=PostgreSqlDefinition() { result.setDefinition(new BaseTypeDefinition(elements)); } + | ( LOOKAHEAD({ isKeywordAhead("ENUM") }) TypeDdlKeyword("ENUM") "(" @@ -26000,6 +26068,94 @@ CreateType CreateType(): { return result; } } +/** + * PostgreSQL's definition, "(" def_elem [, ...] ")": the property list of a base type in CREATE + * TYPE name (...) and of ALTER TYPE name SET (...). + */ +List PostgreSqlDefinition(): +{ List elements = new ArrayList(); DefinitionElement element; } +{ + "(" element=PostgreSqlDefinitionElement() { elements.add(element); } + ( "," element=PostgreSqlDefinitionElement() { elements.add(element); } )* ")" + { return elements; } +} + +/** PostgreSQL's def_elem, label [ = def_arg ]. Any identifier or keyword is a label. */ +DefinitionElement PostgreSqlDefinitionElement(): +{ Token name; DefinitionArgument argument = null; } +{ + // The action keeps the JAVACODE call from being the production's first unit. + { } name=PostgreSqlWord() + [ "=" argument=PostgreSqlDefinitionArgument() ] + { return new DefinitionElement(name.image, argument); } +} + +/** Consumes an identifier or any keyword. As a JAVACODE production it adds no choice conflicts. */ +JAVACODE +Token PostgreSqlWord() { + Token token = getToken(1); + if (!isPostgreSqlWord(token)) { + throw new ParseException("Encountered unexpected token: \"" + token.image + "\" at line " + + token.beginLine + ", column " + token.beginColumn + ". Expected a name."); + } + return getNextToken(); +} + +/** + * PostgreSQL's def_arg: a type (which is also how a function or any other name is read), a + * reserved keyword, an operator, a number, a string constant or NONE. + */ +DefinitionArgument PostgreSqlDefinitionArgument(): +{ + Token token; Token first = getToken(1); Token sign = null; + String name; String operator; StringBuilder schema = new StringBuilder(); + ColDataType type; StringValue string; Expression number; + boolean setOf = false; boolean typeReference = false; + DefinitionArgument result; +} +{ + ( + LOOKAHEAD({ isDefinitionWordValueAhead() }) { } token=PostgreSqlWord() + { result = definitionWordValue(token); } + | + token= + { + string = stringValue(token); + requireDdlSyntax(isPostgreSqlStringConstant(string), + "PostgreSQL expects a string constant: " + token.image); + result = DefinitionArgument.string(string); + } + | + LOOKAHEAD(2) [ sign="+" | sign="-" ] + ( token= { number = linkToLastToken(new LongValue(token.image)); } + | token= { number = linkToLastToken(new DoubleValue(token.image)); } ) + { + if (sign != null) { + number = linked(new SignedExpression(sign.image.charAt(0), number), first); + } + result = DefinitionArgument.number(number); + } + | + LOOKAHEAD({ isKeywordAhead("OPERATOR") && getToken(2).kind == OPENING_BRACKET }) + ContextualKeyword("OPERATOR") "(" + ( LOOKAHEAD(2) name=RelObjectName() "." { schema.append(name).append('.'); } )* + operator=PostgreSqlOperatorName() ")" + { result = DefinitionArgument.operator(schema + operator, true); } + | + operator=PostgreSqlOperatorName() { result = DefinitionArgument.operator(operator, false); } + | + [ LOOKAHEAD({ isKeywordAhead("SETOF") }) ContextualKeyword("SETOF") { setOf = true; } ] + type=ColDataType() + { requireDdlSyntax(!hasEmptyParentheses(first, getToken(0)), "Type modifiers must not be empty"); } + [ "%" { + requireDdlSyntax(type.toString().indexOf('.') > 0, "%TYPE follows the name of a column"); + typeReference = true; + } ] + { result = DefinitionArgument.type(type, setOf, typeReference); } + ) + { return result; } +} + RangeTypeDefinition.Option RangeTypeOption(): { String name; RangeTypeDefinition.OptionKind kind; ColDataType type; } { @@ -26044,7 +26200,7 @@ AlterType.AttributeChange TypeAttributeChange(): AlterType AlterType(): { AlterType result = new AlterType(); String name; Token label; - AlterType.AttributeChange change; CascadeBehavior behavior; + AlterType.AttributeChange change; CascadeBehavior behavior; List properties; } { name=TypeDdlName() { result.setName(name); } @@ -26066,7 +26222,12 @@ AlterType AlterType(): [ behavior=TypeAlterBehavior() { result.setBehavior(behavior); } ] | name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.RENAME); } ) - | name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.SET_SCHEMA); } + | + ( + name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.SET_SCHEMA); } + | properties=PostgreSqlDefinition() + { result.setProperties(properties); result.setAction(AlterType.Action.SET_PROPERTIES); } + ) | LOOKAHEAD({ isKeywordAhead("OWNER") }) TypeDdlKeyword("OWNER") name=RelObjectName() { result.setNewName(name); result.setAction(AlterType.Action.OWNER); } | change=TypeAttributeChange() { result.setAction(AlterType.Action.ATTRIBUTES); result.getAttributeChanges().add(change); } @@ -27313,6 +27474,20 @@ CreateRule CreateRule(boolean orReplace): { return statement; } } +/** PostgreSQL's ALTER RULE name ON table RENAME TO new_name, its only ALTER RULE form. */ +AlterRule AlterRule(): +{ AlterRule statement = new AlterRule(); Token name; Table table; } +{ + ContextualKeyword("RULE") name=PostgreSqlWord() { requirePostgreSqlColId(name.image); statement.setName(name.image); } + table=Table() + { + requireDdlSyntax(table.getNameParts().size() <= 3, "A table name has at most three parts"); + statement.setTable(table); + } + name=PostgreSqlWord() { requirePostgreSqlColId(name.image); statement.setNewName(name.image); } + { return statement; } +} + Statement PostgreSqlRuleAction(): { Statement statement; } { diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 0097242f85..14bdf89cc3 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -180,6 +180,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``SHOW [FULL] COLUMNS``/``FIELDS`` · ``SHOW TABLE STATUS`` · ``SHOW CREATE TABLE``/``VIEW``/``PROCEDURE``/``FUNCTION``/``TRIGGER``/``EVENT``/``DATABASE``/``USER`` * - **PostgreSQL administration** - ``CREATE``/``ALTER EVENT TRIGGER`` · ``REINDEX`` · ``SECURITY LABEL`` + * - **PostgreSQL types and rules** + - ``CREATE TYPE`` (shell, composite, enum, range and base types) · ``ALTER TYPE`` · ``CREATE RULE`` · ``ALTER RULE`` * - **Salesforce SOQL** - ``INCLUDES`` · ``EXCLUDES`` diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 9d89430009..03e7bf7b4c 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1139,7 +1139,7 @@ See `CREATE PUBLICATION `_ and `ALTER TYPE `_. ``CreateDomain`` stores a ``ColDataType``, default expression and ordered ``DomainConstraint`` nodes. Each constraint distinguishes nullability from a check expression and preserves its optional name. ``AlterDomain`` models default/nullability changes, constraint actions, ownership, renaming and schema moves. Its ``isNotValid()`` flag describes the newly added check constraint. Domain expressions participate in statement visitors, expression deparsers and validation. @@ -2475,6 +2493,12 @@ list. Other forms after ``ALTER FUNCTION`` or ``ALTER PROCEDURE``, such as Oracl Validators report ``alterFunction``, ``alterProcedure``, ``alterRoutine`` and ``alterTrigger``; PostgreSQL enables all of them, MySQL and MariaDB the first two. +PostgreSQL's ``ALTER RULE name ON table RENAME TO new_name`` +(`ALTER RULE `_) parses to +``AlterRule`` with ``getName()``, ``getTable()`` and ``getNewName()``; table visitors report +its table and validators report ``alterRule``, which PostgreSQL enables. It is the only +``ALTER RULE`` form, so any other text after ``ALTER RULE`` is a parse error. + ``CreateFunctionalStatement.getDefiner()`` returns the MySQL ``DEFINER`` of ``CREATE DEFINER = ... FUNCTION|PROCEDURE`` as the ``MySqlDefiner`` that views and triggers use. With ``Dialect.MYSQL`` or ``Dialect.MARIADB``, diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java index f65d16d82e..52ffbf92da 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -142,6 +142,10 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException { "IF a = 1 SELECT 1;", "BEGIN SELECT 1; END;", "DISCARD TEMPORARY", "LISTEN \"Ch 1\"", "UNLISTEN *", + "ALTER RULE \"Rule 1\" ON s.t RENAME TO r2", + "CREATE TYPE s.box3 (INPUT = s.box3_in, PASSEDBYVALUE, INTERNALLENGTH = - 1, " + + "ELEMENT = setof float8[], LIKE = t.a%TYPE, DEFAULT = E'x', x = extended)", + "ALTER TYPE box3 SET (SEND = NONE, STORAGE = plain, x = +1.5, y = varchar(3))", "SHOW TRANSACTION ISOLATION LEVEL", "RESET SESSION AUTHORIZATION", "COPY s.t (a) TO STDOUT WITH NULL AS 'x' CSV FORCE QUOTE a, \"B\"", "COPY BINARY t FROM STDIN FREEZE ENCODING 'UTF8'", diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java index 0e4f938e7d..d31b4586c5 100644 --- a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java @@ -17,6 +17,7 @@ import java.util.stream.Stream; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; @@ -47,6 +48,7 @@ static Stream definitions() { Arguments.of("ALTER PROCEDURE p SET search_path = public", AlterRoutine.class), Arguments.of("ALTER FUNCTION f READS SQL DATA", AlterRoutine.class), Arguments.of("ALTER TRIGGER t ON tbl RENAME TO t2", AlterTrigger.class), + Arguments.of("ALTER RULE r ON tbl RENAME TO r2", AlterRule.class), Arguments.of("ALTER SCHEMA s RENAME TO s2", AlterSchema.class), Arguments.of("ALTER SCHEMA s OWNER TO CURRENT_USER", AlterSchema.class), Arguments.of("ALTER STATISTICS s RENAME TO s2", AlterStatistics.class), diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRuleTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRuleTest.java new file mode 100644 index 0000000000..55f1e778e2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRuleTest.java @@ -0,0 +1,152 @@ +/*- + * #%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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.PivotVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL ALTER RULE name ON table RENAME TO new_name, see + * https://www.postgresql.org/docs/18/sql-alterrule.html. + */ +class AlterRuleTest extends ValidationTestAsserts { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + @ParameterizedTest + @ValueSource(strings = { + "ALTER RULE r1 ON t RENAME TO r2", + "ALTER RULE r1 ON s.t RENAME TO r2", + "ALTER RULE \"Rule 1\" ON public.t RENAME TO \"Rule 2\"", + "ALTER RULE r9 ON d.public.t RENAME TO r2", + "ALTER RULE rule ON t RENAME TO rename", + "ALTER RULE int ON t RENAME TO between"}) + void testRenameRule(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertThat(statement).isInstanceOf(AlterRule.class); + assertThat(assertSqlCanBeParsedAndDeparsed(sql)).isInstanceOf(AlterRule.class); + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + statement.accept(deparser); + assertThat(deparser.getBuilder()).hasToString(sql); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + + Statements script = CCJSqlParserUtil.parseStatements( + sql + "; CREATE RULE r AS ON INSERT TO t DO NOTHING; " + sql, POSTGRESQL); + assertThat(script).hasSize(3); + assertThat(script.get(0)).hasToString(sql); + assertThat(script.get(1)).isInstanceOf(CreateRule.class); + assertThat(script.get(2)).hasToString(sql); + } + + @Test + void testFieldsTablesAndVisitors() throws Exception { + AlterRule rule = (AlterRule) CCJSqlParserUtil.parse( + "ALTER RULE \"Rule 1\" ON app.t RENAME TO r2", POSTGRESQL); + assertThat(rule.getName()).isEqualTo("\"Rule 1\""); + assertThat(rule.getTable().getFullyQualifiedName()).isEqualTo("app.t"); + assertThat(rule.getNewName()).isEqualTo("r2"); + assertThat(new TablesNamesFinder().getTables((Statement) rule)) + .containsExactly("app.t"); + + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter<>(); + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S context) { + visited.add(table.getFullyQualifiedName()); + return null; + } + }; + rule.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions, + new PivotVisitorAdapter<>(expressions), new SelectItemVisitorAdapter<>(expressions), + fromItems))); + assertThat(visited).containsExactly("app.t"); + assertThat(rule.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterRule statement, S context) { + return context + ":" + statement.getName(); + } + }, "context")).isEqualTo("context:\"Rule 1\""); + + rule.getTable().setName("u"); + rule.setNewName("r3"); + assertThat(rule).hasToString("ALTER RULE \"Rule 1\" ON app.u RENAME TO r3"); + assertThat(new AlterRule().withName("a").withTable(new Table("t")).withNewName("b")) + .hasToString("ALTER RULE a ON t RENAME TO b"); + } + + @Test + void testValidation() throws Exception { + String sql = "ALTER RULE r1 ON t RENAME TO r2"; + validateNoErrors(sql, 1, FeaturesAllowed.ALTER); + validateNotAllowed(sql, 1, 1, FeaturesAllowed.SELECT, Feature.alterRule); + validate(sql, 1, 1, MySqlVersion.V8_0); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER RULE r9 ON a.b.c.t RENAME TO r2", + "ALTER RULE IF EXISTS r9 ON t RENAME TO r2", + "ALTER RULE r9 ON ONLY t RENAME TO r2", + "ALTER RULE r9 ON t* RENAME TO r2", + "ALTER RULE r9 ON t RENAME r2", + "ALTER RULE r9 RENAME TO r2", + "ALTER RULE s.r9 ON t RENAME TO r2", + "ALTER RULE r9 ON t RENAME TO s.r2", + "ALTER RULE r9 ON t OWNER TO postgres", + "ALTER RULE r9 ON t SET SCHEMA s", + "ALTER RULE r9 ON t RENAME TO r2 CASCADE", + "ALTER RULE select ON t RENAME TO r2", + "ALTER RULE r9 ON t RENAME TO select", + "ALTER RULE left ON t RENAME TO r2", + "ALTER RULE r9 ON t RENAME TO inner", + "ALTER RULE 'r9' ON t RENAME TO r2", + "ALTER RULE r9 ON t RENAME TO 1"}) + void testRejectedForms(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlBaseTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlBaseTypeTest.java new file mode 100644 index 0000000000..80c60c1b65 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlBaseTypeTest.java @@ -0,0 +1,365 @@ +/*- + * #%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.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.SignedExpression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.DefinitionArgument; +import net.sf.jsqlparser.statement.DefinitionArgument.Kind; +import net.sf.jsqlparser.statement.DefinitionElement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.alter.AlterType; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.type.BaseTypeDefinition; +import net.sf.jsqlparser.statement.create.type.CompositeTypeDefinition; +import net.sf.jsqlparser.statement.create.type.CreateType; +import net.sf.jsqlparser.statement.create.type.EnumTypeDefinition; +import net.sf.jsqlparser.statement.create.type.RangeTypeDefinition; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL base types: CREATE TYPE name (definition) and ALTER TYPE name SET (definition), see + * https://www.postgresql.org/docs/18/sql-createtype.html and + * https://www.postgresql.org/docs/18/sql-altertype.html. + */ +class PostgreSqlBaseTypeTest { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static List elements(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, POSTGRESQL); + return statement instanceof CreateType + ? ((BaseTypeDefinition) ((CreateType) statement).getDefinition()).getElements() + : ((AlterType) statement).getProperties(); + } + + private static String deparse(Statement statement) { + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + statement.accept(deparser); + return deparser.getBuilder().toString(); + } + + private static DefinitionArgument argument(String value) throws JSQLParserException { + return elements("CREATE TYPE b1 (x = " + value + ")").get(0).getArgument(); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TYPE box3 (INPUT = box3_in, OUTPUT = box3_out, INTERNALLENGTH = 16)", + "CREATE TYPE box3 (INPUT = box3_in, OUTPUT = box3_out, INTERNALLENGTH = 4, PASSEDBYVALUE)", + "CREATE TYPE b1 (INPUT = box3_in, OUTPUT = box3_out, RECEIVE = box3_recv, SEND = box3_send, " + + "TYPMOD_IN = a, TYPMOD_OUT = b, ANALYZE = c, SUBSCRIPT = d, " + + "INTERNALLENGTH = VARIABLE, PASSEDBYVALUE, ALIGNMENT = double, " + + "STORAGE = extended, LIKE = int4, CATEGORY = 'U', PREFERRED = true, " + + "DEFAULT = 'x', ELEMENT = int4, DELIMITER = ',', COLLATABLE = false)", + "CREATE TYPE b1 (INPUT = public.box3_in, OUTPUT = box3_out, ELEMENT = int4[], " + + "LIKE = double precision, DEFAULT = NULL, INTERNALLENGTH = -1)", + "CREATE TYPE s.b1 (INPUT = box3_in)", + "CREATE TYPE a.b.c (x = 1)", + "CREATE TYPE b1 (foo = bar)", + "CREATE TYPE b1 (foo)", + "CREATE TYPE b1 (select = 1, table, from = x)", + "CREATE TYPE b1 (x = NONE, y = +, z = OPERATOR(pg_catalog.<=), w = 1.5, v = +2, u = -3)", + "CREATE TYPE b1 (x = 1e3, y = .5)", + "CREATE TYPE b1 (x = OPERATOR(+), y = operator)", + "CREATE TYPE b1 (x = OPERATOR(a.b.+))", + "CREATE TYPE b1 (x = OPERATOR(=), y = =, z = <>, w = !=, v = <=, u = >=, t = ||, " + + "s = ->>, r = @>, q = ~~*, p = %, o = ^, n = /, m = *, l = <, k = >)", + "CREATE TYPE b1 (x = @)", + "CREATE TYPE b1 (x = <@>)", + "CREATE TYPE b1 (x = select, y = user, z = true, w = null, v = default, u = on, t = table)", + "CREATE TYPE b1 (x = null, y = array, z = any)", + "CREATE TYPE b1 (x = left, y = inner, z = like, w = authorization)", + "CREATE TYPE b1 (x = extended, y = key, z = type, w = value)", + "CREATE TYPE b1 (x = int, y = char, z = bit, w = json, v = interval, u = timestamp)", + "CREATE TYPE b1 (x = a.b.c.d)", + "CREATE TYPE b1 (x = a.b.c%TYPE)", + "CREATE TYPE b1 (x = SETOF int[])", + "CREATE TYPE b1 (x = SETOF int, y = t.a%TYPE, z = SETOF t.a%TYPE, s = int ARRAY[3])", + "CREATE TYPE b1 (x = char varying, z = time without time zone)", + "ALTER TYPE box3 SET (SEND = box3_send, RECEIVE = box3_recv)", + "ALTER TYPE box3 SET (SEND = NONE)", + "ALTER TYPE box3 SET (SEND)", + "ALTER TYPE box3 SET (SEND = NONE, SEND = NONE)", + "ALTER TYPE box3 SET (STORAGE = plain)", + "ALTER TYPE box3 SET (STORAGE = extended)", + "ALTER TYPE box3 SET (STORAGE = 'plain')", + "ALTER TYPE box3 SET (STORAGE = 1)", + "ALTER TYPE box3 SET (ANALYZE = public.box3_an, TYPMOD_IN = NONE, TYPMOD_OUT = NONE, " + + "SUBSCRIPT = NONE)", + "ALTER TYPE box3 SET (SEND = 'x')", + "ALTER TYPE box3 SET (SEND = +)", + "ALTER TYPE box3 SET (SEND = OPERATOR(pg_catalog.+))", + "ALTER TYPE box3 SET (SEND = select)", + "ALTER TYPE box3 SET (SEND = true)", + "ALTER TYPE box3 SET (SEND = int4[])", + "ALTER TYPE box3 SET (SEND = SETOF t.a%TYPE)", + "ALTER TYPE box3 SET (SEND = double precision)", + "ALTER TYPE s.box3 SET (SEND = box3_send)", + "ALTER TYPE nosuch SET (foo = bar)", + "ALTER TYPE nosuch SET (foo)", + "ALTER TYPE nosuch SET (SEND = 1)", + "ALTER TYPE nosuch SET (SEND = -1.5)", + "ALTER TYPE nosuch SET (select = x)"}) + void testBaseTypeDefinitions(String sql) throws Exception { + // the text is kept as written, with the dialect and without one + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertThat(statement).isInstanceOfAny(CreateType.class, AlterType.class); + assertThat(assertSqlCanBeParsedAndDeparsed(sql)).hasToString(sql); + + assertThat(deparse(statement)).isEqualTo(sql); + assertThat(new TablesNamesFinder().getTables(statement)).isEmpty(); + assertThat(statement.getFeatures().getCertain()).contains(StmtFeature.MODIFIES_SCHEMA) + .doesNotContain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + + Statements script = CCJSqlParserUtil.parseStatements(sql + "; SELECT 1; " + sql, + POSTGRESQL); + assertThat(script).hasSize(3); + assertThat(script.get(0)).hasToString(sql); + assertThat(script.get(1)).isInstanceOf(Select.class); + assertThat(script.get(2)).hasToString(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TYPE b1(x=1) | CREATE TYPE b1 (x = 1)", + "CREATE TYPE b1 (x = none) | CREATE TYPE b1 (x = NONE)", + "CREATE TYPE \"B 1\" (\"In Put\" = 1) | CREATE TYPE \"B 1\" (\"In Put\" = 1)", + "CREATE TYPE b1 (z = - 1, w = + 2.5) | CREATE TYPE b1 (z = -1, w = +2.5)", + "CREATE TYPE b1 (q = E'a\\nb', r = $$x$$, p = U&'d\\0061t') " + + "| CREATE TYPE b1 (q = E'a\\nb', r = $$x$$, p = U&'d\\0061t')", + "CREATE TYPE b1 (x = setof int, w = varchar(10), q = numeric(10,2), r = \"char\") " + + "| CREATE TYPE b1 (x = SETOF int, w = varchar (10), q = numeric (10, 2), r = \"char\")", + "CREATE TYPE b1 (v = timestamp(3) with time zone, y = character(3), w = \"X\".y) " + + "| CREATE TYPE b1 (v = timestamp(3) with time zone, y = character (3), w = \"X\".y)", + "ALTER TYPE box3 SET (\"send\" = NONE) | ALTER TYPE box3 SET (\"send\" = NONE)", + "ALTER TYPE box3 SET (SEND = varchar(10)) | ALTER TYPE box3 SET (SEND = varchar (10))"}) + void testNormalizedRendering(String pair) throws Exception { + String[] parts = pair.split(" \\| "); + Statement statement = CCJSqlParserUtil.parse(parts[0], POSTGRESQL); + assertThat(statement).hasToString(parts[1]); + assertThat(deparse(statement)).isEqualTo(parts[1]); + assertThat(CCJSqlParserUtil.parse(parts[1], POSTGRESQL)).hasToString(parts[1]); + } + + @Test + void testElementsAndArgumentKinds() throws Exception { + CreateType type = (CreateType) CCJSqlParserUtil.parse( + "CREATE TYPE app.box3 (INPUT = app.box3_in, PASSEDBYVALUE, internallength = 16, " + + "DEFAULT = 'x', PREFERRED = true, SEND = NONE)", + POSTGRESQL); + assertThat(type.getName()).isEqualTo("app.box3"); + BaseTypeDefinition definition = (BaseTypeDefinition) type.getDefinition(); + assertThat(definition.getElements()).extracting(DefinitionElement::getName).containsExactly( + "INPUT", "PASSEDBYVALUE", "internallength", "DEFAULT", "PREFERRED", "SEND"); + assertThat(definition.getElements()).extracting( + element -> element.getArgument() == null ? null : element.getArgument().getKind()) + .containsExactly(Kind.TYPE, null, Kind.NUMBER, Kind.STRING, Kind.KEYWORD, + Kind.NONE); + + DefinitionArgument input = definition.getElement("input").getArgument(); + assertThat(input.getDataType().getDataType()).isEqualTo("app.box3_in"); + assertThat(input.isSetOf()).isFalse(); + assertThat(input.isTypeReference()).isFalse(); + assertThat(definition.getElement("PassedByValue").getArgument()).isNull(); + assertThat(definition.getElement("INTERNALLENGTH").getArgument().getNumber()) + .isEqualTo(new LongValue(16)); + assertThat(definition.getElement("DEFAULT").getArgument().getString().getValue()) + .isEqualTo("x"); + assertThat(definition.getElement("PREFERRED").getArgument().getKeyword()) + .isEqualTo("true"); + assertThat(definition.getElement("OUTPUT")).isNull(); + + // other kinds leave the getters of a kind empty + DefinitionArgument none = definition.getElement("SEND").getArgument(); + assertThat(none.getDataType()).isNull(); + assertThat(none.getKeyword()).isNull(); + assertThat(none.getOperator()).isNull(); + assertThat(none.getNumber()).isNull(); + assertThat(none.getString()).isNull(); + } + + @Test + void testArgumentForms() throws Exception { + assertThat(argument("int4[]").getDataType().getArrayData()).hasSize(1); + assertThat(argument("double precision").getDataType()).hasToString("double precision"); + assertThat(argument("extended").getDataType().getDataType()).isEqualTo("extended"); + assertThat(argument("\"none\"").getKind()).isEqualTo(Kind.TYPE); + + DefinitionArgument reference = argument("setof s.t.a%TYPE"); + assertThat(reference.isSetOf()).isTrue(); + assertThat(reference.isTypeReference()).isTrue(); + assertThat(reference.getDataType().getDataType()).isEqualTo("s.t.a"); + + DefinitionArgument operator = argument("OPERATOR(pg_catalog.<=)"); + assertThat(operator.getKind()).isEqualTo(Kind.OPERATOR); + assertThat(operator.getOperator()).isEqualTo("pg_catalog.<="); + assertThat(operator.isOperatorKeyword()).isTrue(); + assertThat(argument("<=").isOperatorKeyword()).isFalse(); + assertThat(argument("<=").getOperator()).isEqualTo("<="); + + assertThat(argument("1.5").getNumber()).isInstanceOf(DoubleValue.class); + SignedExpression negative = (SignedExpression) argument("- 1").getNumber(); + assertThat(negative.getSign()).isEqualTo('-'); + assertThat(negative.getExpression()).isEqualTo(new LongValue(1)); + + StringValue escaped = argument("E'a\\nb'").getString(); + assertThat(escaped.getPrefix()).isEqualTo("E"); + assertThat(argument("NULL").getKeyword()).isEqualTo("NULL"); + assertThat(argument("None").getKind()).isEqualTo(Kind.NONE); + assertThat(argument("None")).hasToString("NONE"); + } + + @Test + void testAlterTypeProperties() throws Exception { + AlterType alteration = (AlterType) CCJSqlParserUtil.parse( + "ALTER TYPE app.box3 SET (SEND = app.box3_send, RECEIVE = NONE, STORAGE = plain, " + + "SUBSCRIPT)", + POSTGRESQL); + assertThat(alteration.getName()).isEqualTo("app.box3"); + assertThat(alteration.getAction()).isEqualTo(AlterType.Action.SET_PROPERTIES); + assertThat(alteration.getProperties()).extracting(DefinitionElement::getName) + .containsExactly("SEND", "RECEIVE", "STORAGE", "SUBSCRIPT"); + assertThat(alteration.getProperties().get(0).getArgument().getDataType().getDataType()) + .isEqualTo("app.box3_send"); + assertThat(alteration.getProperties().get(1).getArgument().getKind()) + .isEqualTo(Kind.NONE); + assertThat(alteration.getProperties().get(3).getArgument()).isNull(); + + // the other actions carry no properties and keep their reading + AlterType schema = (AlterType) CCJSqlParserUtil.parse("ALTER TYPE box3 SET SCHEMA app"); + assertThat(schema.getAction()).isEqualTo(AlterType.Action.SET_SCHEMA); + assertThat(schema.getNewName()).isEqualTo("app"); + assertThat(schema.getProperties()).isEmpty(); + } + + @Test + void testConstruction() throws Exception { + CreateType type = new CreateType(); + type.setName("box3"); + BaseTypeDefinition definition = new BaseTypeDefinition(); + definition.setElements(Arrays.asList( + new DefinitionElement("INPUT", + DefinitionArgument.type(new ColDataType("box3_in"))), + new DefinitionElement("ELEMENT", + DefinitionArgument.type(new ColDataType("float8"), true, false)), + new DefinitionElement("INTERNALLENGTH", + DefinitionArgument.number(new LongValue(16))), + new DefinitionElement("DELIMITER", DefinitionArgument.string(new StringValue(";"))), + new DefinitionElement("COLLATABLE", DefinitionArgument.keyword("false")), + new DefinitionElement("x", DefinitionArgument.operator("pg_catalog.+", true)), + new DefinitionElement("y", DefinitionArgument.operator("+", false)), + new DefinitionElement("SEND", DefinitionArgument.none()))); + DefinitionElement bare = new DefinitionElement(); + bare.setName("PASSEDBYVALUE"); + bare.setArgument(null); + type.setDefinition(definition); + String sql = "CREATE TYPE box3 (INPUT = box3_in, ELEMENT = SETOF float8, " + + "INTERNALLENGTH = 16, DELIMITER = ';', COLLATABLE = false, " + + "x = OPERATOR(pg_catalog.+), y = +, SEND = NONE)"; + assertThat(type).hasToString(sql); + assertThat(CCJSqlParserUtil.parse(sql, POSTGRESQL)).hasToString(sql); + assertThat(bare).hasToString("PASSEDBYVALUE"); + + AlterType alteration = new AlterType(); + alteration.setName("box3"); + alteration.setAction(AlterType.Action.SET_PROPERTIES); + alteration.setProperties(Arrays.asList(bare, + new DefinitionElement("SEND", DefinitionArgument.none()))); + assertThat(alteration).hasToString("ALTER TYPE box3 SET (PASSEDBYVALUE, SEND = NONE)"); + assertThat(new BaseTypeDefinition(alteration.getProperties())) + .hasToString("(PASSEDBYVALUE, SEND = NONE)"); + } + + @Test + void testOtherTypeDefinitionsKeepTheirReading() throws Exception { + assertThat(((CreateType) assertSqlCanBeParsedAndDeparsed("CREATE TYPE shell_t")) + .getDefinition()).isNull(); + assertThat( + ((CreateType) assertSqlCanBeParsedAndDeparsed("CREATE TYPE c AS (a int, b text)")) + .getDefinition()) + .isInstanceOf(CompositeTypeDefinition.class); + assertThat(((CreateType) assertSqlCanBeParsedAndDeparsed("CREATE TYPE c AS ()")) + .getDefinition()).isInstanceOf(CompositeTypeDefinition.class); + assertThat(((CreateType) assertSqlCanBeParsedAndDeparsed("CREATE TYPE e AS ENUM ('a')")) + .getDefinition()).isInstanceOf(EnumTypeDefinition.class); + assertThat(((CreateType) assertSqlCanBeParsedAndDeparsed( + "CREATE TYPE r AS RANGE (SUBTYPE = int4)")).getDefinition()) + .isInstanceOf(RangeTypeDefinition.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TYPE b1 ()", + "CREATE TYPE b1 (x = 1,)", + "CREATE TYPE b1 (x := 1)", + "CREATE TYPE b1 (x 1)", + "CREATE TYPE b1 (x =)", + "CREATE TYPE b1 (a.b = 1)", + "CREATE TYPE b1 ('x' = 1)", + "CREATE TYPE b1 AS (x = 1)", + "CREATE TYPE IF NOT EXISTS b1 (x = 1)", + "CREATE OR REPLACE TYPE b1 (x = 1)", + "CREATE TYPE b1 (x = 1) WITH (y)", + "CREATE TYPE b1 (x = 'a' 'b')", + "CREATE TYPE b1 (x = a b)", + "CREATE TYPE b1 (x = 1 + 2)", + "CREATE TYPE b1 (x = (1))", + "CREATE TYPE b1 (x = f())", + "CREATE TYPE b1 (x = -x)", + "CREATE TYPE b1 (x = a.+)", + "CREATE TYPE b1 (o = B'101')", + "CREATE TYPE b1 (n = X'1F')", + "CREATE TYPE b1 (n = N'x')", + "CREATE TYPE b1 (x = between)", + "CREATE TYPE b1 (x = values)", + "CREATE TYPE b1 (x = coalesce)", + "CREATE TYPE b1 (x = setof)", + "CREATE TYPE b1 (x = precision)", + "CREATE TYPE b1 (x = national)", + "CREATE TYPE b1 (x = a%TYPE)", + "ALTER TYPE box3 SET ()", + "ALTER TYPE box3 SET (a.b = x)", + "ALTER TYPE box3 SET (SEND = box3_send,)", + "ALTER TYPE box3 SET (SEND = box3_send) CASCADE", + "ALTER TYPE box3 SET SEND = box3_send", + "ALTER TYPE IF EXISTS box3 SET (SEND = box3_send)", + "ALTER TYPE nosuch SET (SEND = between)", + "ALTER TYPE nosuch SET (SEND = B'1')"}) + void testRejectedForms(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/resources/postgresql/type-ddl.sql b/src/test/resources/postgresql/type-ddl.sql index 943260e4de..a774fdab1a 100644 --- a/src/test/resources/postgresql/type-ddl.sql +++ b/src/test/resources/postgresql/type-ddl.sql @@ -16,6 +16,8 @@ CREATE TYPE empty_composite AS () CREATE TYPE localized AS (label text COLLATE pg_catalog."C", tags text[], created timestamp with time zone) CREATE TYPE float8range AS RANGE (SUBTYPE = float8, SUBTYPE_DIFF = float8mi) CREATE TYPE app.r AS RANGE (SUBTYPE_OPCLASS = pg_catalog.text_ops, SUBTYPE = text, COLLATION = pg_catalog."C", CANONICAL = app.canonical, MULTIRANGE_TYPE_NAME = app.mr) +CREATE TYPE box3 (INPUT = box3_in, OUTPUT = box3_out, INTERNALLENGTH = 16) +CREATE TYPE app.box3 (INPUT = app.box3_in, OUTPUT = app.box3_out, ELEMENT = float8, LIKE = double precision, DELIMITER = ';', PASSEDBYVALUE, COLLATABLE = false) ALTER TYPE mood ADD VALUE IF NOT EXISTS 'great' AFTER 'happy' ALTER TYPE mood ADD VALUE 'bad' BEFORE 'sad' ALTER TYPE mood ADD VALUE 'new' @@ -23,6 +25,7 @@ ALTER TYPE mood RENAME VALUE 'ok' TO 'fine' ALTER TYPE mood OWNER TO CURRENT_USER ALTER TYPE mood RENAME TO feeling ALTER TYPE mood SET SCHEMA app +ALTER TYPE box3 SET (SEND = box3_send, RECEIVE = NONE, STORAGE = plain) ALTER TYPE inventory_item RENAME ATTRIBUTE name TO title CASCADE ALTER TYPE inventory_item ADD ATTRIBUTE sku text COLLATE "C" RESTRICT ALTER TYPE inventory_item DROP ATTRIBUTE IF EXISTS price CASCADE