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