diff --git a/README.md b/README.md index a2f344996..7c37b2b20 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **MySQL utility statements** | `EXPLAIN`/`DESCRIBE [ANALYZE] FORMAT = …` · `EXPLAIN FOR CONNECTION` · `DESCRIBE t col` · `PREPARE … FROM` · `EXECUTE … USING` · `DROP PREPARE` · `DO` | | **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` | +| **PostgreSQL casts, transforms and conversions** | `CREATE CAST` · `DROP CAST` · `CREATE [OR REPLACE] TRANSFORM` · `DROP TRANSFORM` · `CREATE [DEFAULT] CONVERSION` · `ALTER CONVERSION` | | **PostgreSQL operators and aggregates** | `CREATE`/`ALTER OPERATOR` · `CREATE`/`ALTER OPERATOR CLASS` · `CREATE`/`ALTER OPERATOR FAMILY` · `CREATE`/`ALTER AGGREGATE` | | **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 471a7087e..9b9e417e0 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -1036,6 +1036,30 @@ public enum Feature { * @see net.sf.jsqlparser.statement.alter.AlterRule */ alterRule, + /** + * PostgreSQL "CREATE CAST" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.cast.CreateCast + */ + createCast, + /** + * PostgreSQL "CREATE TRANSFORM" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.transform.CreateTransform + */ + createTransform, + /** + * PostgreSQL "CREATE CONVERSION" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.conversion.CreateConversion + */ + createConversion, + /** + * PostgreSQL "ALTER CONVERSION" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterConversion + */ + alterConversion, /** * PostgreSQL "CREATE OPERATOR" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 85d73f136..75dbf907a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -45,6 +45,10 @@ import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -1623,21 +1627,41 @@ public Void visit(AlterRule statement, S context) { return schemaOnly(); } + @Override + public Void visit(CreateCast statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateOperator statement, S context) { return schemaOnly(); } + @Override + public Void visit(CreateTransform statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateOperatorClass statement, S context) { return schemaOnly(); } + @Override + public Void visit(CreateConversion statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateOperatorFamily statement, S context) { return schemaOnly(); } + @Override + public Void visit(AlterConversion statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateAggregate 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 cfc4a83d4..cb0aabf3c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -33,6 +33,10 @@ import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.AlterPolicy; @@ -1253,6 +1257,38 @@ default void visit(AlterRule statement) { visit(statement, null); } + default T visit(CreateCast statement, S context) { + return null; + } + + default void visit(CreateCast statement) { + visit(statement, null); + } + + default T visit(CreateTransform statement, S context) { + return null; + } + + default void visit(CreateTransform statement) { + visit(statement, null); + } + + default T visit(CreateConversion statement, S context) { + return null; + } + + default void visit(CreateConversion statement) { + visit(statement, null); + } + + default T visit(AlterConversion statement, S context) { + return null; + } + + default void visit(AlterConversion statement) { + visit(statement, null); + } + default T visit(CreateOperator statement, S context) { return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 82ae0285e..2c2c76726 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -11,6 +11,10 @@ import net.sf.jsqlparser.statement.alter.AlterRelation; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterRoutine; @@ -1285,4 +1289,24 @@ public T visit(AlterRule statement, S context) { fromItemVisitor.visitFromItem(statement.getTable(), context); return null; } + + @Override + public T visit(CreateCast statement, S context) { + return null; + } + + @Override + public T visit(CreateTransform statement, S context) { + return null; + } + + @Override + public T visit(CreateConversion statement, S context) { + return null; + } + + @Override + public T visit(AlterConversion statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConversion.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConversion.java new file mode 100644 index 000000000..609e47a19 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConversion.java @@ -0,0 +1,130 @@ +/*- + * #%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.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL + * {@code ALTER CONVERSION name {RENAME TO new_name | OWNER TO role | SET SCHEMA schema}}. Names + * retain SQL identifier quoting. + */ +public class AlterConversion extends ASTNodeAccessImpl implements Statement { + public enum Action { + RENAME, OWNER, SET_SCHEMA + } + + private String name; + private Action action; + private String newName; + private String owner; + private String schemaName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public AlterConversion withName(String name) { + setName(name); + return this; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public AlterConversion withAction(Action action) { + setAction(action); + return this; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public AlterConversion withNewName(String newName) { + setNewName(newName); + return this; + } + + /** + * The new owner: a role name, {@code CURRENT_ROLE}, {@code CURRENT_USER} or + * {@code SESSION_USER}. + * + * @return the owner of {@code OWNER TO} + */ + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public AlterConversion withOwner(String owner) { + setOwner(owner); + return this; + } + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public AlterConversion withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER CONVERSION ").append(name); + switch (action) { + case RENAME: + builder.append(" RENAME TO ").append(newName); + break; + case OWNER: + builder.append(" OWNER TO ").append(owner); + break; + case SET_SCHEMA: + builder.append(" SET SCHEMA ").append(schemaName); + break; + default: + throw new IllegalStateException("Unknown conversion action: " + action); + } + return builder; + } + + @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/create/cast/CreateCast.java b/src/main/java/net/sf/jsqlparser/statement/create/cast/CreateCast.java new file mode 100644 index 000000000..929753c3a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/cast/CreateCast.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.create.cast; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.CastSignature; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL + * {@code CREATE CAST (source AS target) {WITH FUNCTION f [(args)] | WITHOUT FUNCTION | WITH INOUT} [AS ASSIGNMENT | AS IMPLICIT]}. + * The cast is named by the {@link CastSignature} that {@code DROP CAST} uses. + */ +public class CreateCast extends ASTNodeAccessImpl implements Statement { + /** How the cast converts a value. */ + public enum Method { + WITH_FUNCTION("WITH FUNCTION"), WITHOUT_FUNCTION("WITHOUT FUNCTION"), WITH_INOUT( + "WITH INOUT"); + + private final String sql; + + Method(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } + } + + /** Where the cast applies without being written; an explicit-only cast has no context. */ + public enum Context { + ASSIGNMENT, IMPLICIT + } + + private CastSignature cast; + private Method method; + private RoutineReference function; + private Context context; + + public CastSignature getCast() { + return cast; + } + + public void setCast(CastSignature cast) { + this.cast = cast; + } + + public CreateCast withCast(CastSignature cast) { + setCast(cast); + return this; + } + + public Method getMethod() { + return method; + } + + /** + * Sets how the cast converts a value. Only {@link Method#WITH_FUNCTION} has a function, so + * another method clears it. + * + * @param method the conversion method + */ + public void setMethod(Method method) { + this.method = method; + if (method != Method.WITH_FUNCTION) { + function = null; + } + } + + public CreateCast withMethod(Method method) { + setMethod(method); + return this; + } + + /** + * The function of {@code WITH FUNCTION}. Its arguments are {@code null} when the statement + * names the function without an argument list. + * + * @return the function, or {@code null} for {@code WITHOUT FUNCTION} and {@code WITH INOUT} + */ + public RoutineReference getFunction() { + return function; + } + + /** + * Sets the conversion function; a function makes the method {@link Method#WITH_FUNCTION}. + * + * @param function the function + */ + public void setFunction(RoutineReference function) { + this.function = function; + if (function != null) { + method = Method.WITH_FUNCTION; + } + } + + public CreateCast withFunction(RoutineReference function) { + setFunction(function); + return this; + } + + /** + * The context of {@code AS ASSIGNMENT} or {@code AS IMPLICIT}. + * + * @return the context, or {@code null} for a cast that applies only when it is written + */ + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + + public CreateCast withContext(Context context) { + setContext(context); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE CAST "); + cast.appendTo(builder).append(' ').append(method); + if (method == Method.WITH_FUNCTION) { + builder.append(' ').append(function); + } + if (context != null) { + builder.append(" AS ").append(context); + } + return builder; + } + + @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/create/conversion/CreateConversion.java b/src/main/java/net/sf/jsqlparser/statement/create/conversion/CreateConversion.java new file mode 100644 index 000000000..6be4549c9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/conversion/CreateConversion.java @@ -0,0 +1,122 @@ +/*- + * #%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.conversion; + +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL + * {@code CREATE [DEFAULT] CONVERSION name FOR 'source_encoding' TO 'dest_encoding' FROM function}. + * Names retain SQL identifier quoting and their schema. + */ +public class CreateConversion extends ASTNodeAccessImpl implements Statement { + private boolean defaultConversion; + private String name; + private StringValue sourceEncoding; + private StringValue destinationEncoding; + private String function; + + /** + * Whether {@code DEFAULT} makes this the default conversion of the two encodings. + * + * @return whether the statement has {@code DEFAULT} + */ + public boolean isDefaultConversion() { + return defaultConversion; + } + + public void setDefaultConversion(boolean defaultConversion) { + this.defaultConversion = defaultConversion; + } + + public CreateConversion withDefaultConversion(boolean defaultConversion) { + setDefaultConversion(defaultConversion); + return this; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CreateConversion withName(String name) { + setName(name); + return this; + } + + public StringValue getSourceEncoding() { + return sourceEncoding; + } + + public void setSourceEncoding(StringValue sourceEncoding) { + this.sourceEncoding = sourceEncoding; + } + + public CreateConversion withSourceEncoding(StringValue sourceEncoding) { + setSourceEncoding(sourceEncoding); + return this; + } + + public StringValue getDestinationEncoding() { + return destinationEncoding; + } + + public void setDestinationEncoding(StringValue destinationEncoding) { + this.destinationEncoding = destinationEncoding; + } + + public CreateConversion withDestinationEncoding(StringValue destinationEncoding) { + setDestinationEncoding(destinationEncoding); + return this; + } + + /** + * The name of the conversion function. The statement names it without an argument list. + * + * @return the function name, with its schema if it has one + */ + public String getFunction() { + return function; + } + + public void setFunction(String function) { + this.function = function; + } + + public CreateConversion withFunction(String function) { + setFunction(function); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE "); + if (defaultConversion) { + builder.append("DEFAULT "); + } + return builder.append("CONVERSION ").append(name).append(" FOR ").append(sourceEncoding) + .append(" TO ").append(destinationEncoding).append(" FROM ").append(function); + } + + @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/create/transform/CreateTransform.java b/src/main/java/net/sf/jsqlparser/statement/create/transform/CreateTransform.java new file mode 100644 index 000000000..9de167479 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/transform/CreateTransform.java @@ -0,0 +1,150 @@ +/*- + * #%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.transform; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.TransformSignature; + +/** + * PostgreSQL + * {@code CREATE [OR REPLACE] TRANSFORM FOR type LANGUAGE lang (FROM SQL WITH FUNCTION f [(args)], TO SQL WITH FUNCTION g [(args)])}. + * The transform is named by the {@link TransformSignature} that {@code DROP TRANSFORM} uses. It has + * one or both of the functions, in either order. + */ +public class CreateTransform extends ASTNodeAccessImpl implements Statement { + private boolean orReplace; + private TransformSignature transform; + private RoutineReference fromSqlFunction; + private RoutineReference toSqlFunction; + private boolean toSqlFirst; + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean orReplace) { + this.orReplace = orReplace; + } + + public CreateTransform withOrReplace(boolean orReplace) { + setOrReplace(orReplace); + return this; + } + + public TransformSignature getTransform() { + return transform; + } + + public void setTransform(TransformSignature transform) { + this.transform = transform; + } + + public CreateTransform withTransform(TransformSignature transform) { + setTransform(transform); + return this; + } + + /** + * The function of {@code FROM SQL WITH FUNCTION}, which converts the type to the language. + * + * @return the function, or {@code null} without a {@code FROM SQL} item + */ + public RoutineReference getFromSqlFunction() { + return fromSqlFunction; + } + + public void setFromSqlFunction(RoutineReference fromSqlFunction) { + this.fromSqlFunction = fromSqlFunction; + } + + public CreateTransform withFromSqlFunction(RoutineReference fromSqlFunction) { + setFromSqlFunction(fromSqlFunction); + return this; + } + + /** + * The function of {@code TO SQL WITH FUNCTION}, which converts the language's value to the + * type. + * + * @return the function, or {@code null} without a {@code TO SQL} item + */ + public RoutineReference getToSqlFunction() { + return toSqlFunction; + } + + public void setToSqlFunction(RoutineReference toSqlFunction) { + this.toSqlFunction = toSqlFunction; + } + + public CreateTransform withToSqlFunction(RoutineReference toSqlFunction) { + setToSqlFunction(toSqlFunction); + return this; + } + + /** + * Whether the {@code TO SQL} item is written before the {@code FROM SQL} item. It matters only + * when the statement has both. + * + * @return whether {@code TO SQL} comes first + */ + public boolean isToSqlFirst() { + return toSqlFirst; + } + + public void setToSqlFirst(boolean toSqlFirst) { + this.toSqlFirst = toSqlFirst; + } + + public CreateTransform withToSqlFirst(boolean toSqlFirst) { + setToSqlFirst(toSqlFirst); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE "); + if (orReplace) { + builder.append("OR REPLACE "); + } + builder.append("TRANSFORM "); + transform.appendTo(builder).append(" ("); + String separator = ""; + if (toSqlFirst) { + separator = appendItem(builder, separator, "TO", toSqlFunction); + } + separator = appendItem(builder, separator, "FROM", fromSqlFunction); + if (!toSqlFirst) { + appendItem(builder, separator, "TO", toSqlFunction); + } + return builder.append(')'); + } + + private static String appendItem(StringBuilder builder, String separator, String direction, + RoutineReference function) { + if (function == null) { + return separator; + } + builder.append(separator).append(direction).append(" SQL WITH FUNCTION ") + .append(function); + return ", "; + } + + @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/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 98f1cb3f8..8cdb4bd9b 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -140,6 +140,10 @@ import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -3595,21 +3599,41 @@ public Void visit(AlterRule statement, S context) { return null; } + @Override + public Void visit(CreateCast statement, S context) { + return null; + } + @Override public Void visit(CreateOperator statement, S context) { return null; } + @Override + public Void visit(CreateTransform statement, S context) { + return null; + } + @Override public Void visit(CreateOperatorClass statement, S context) { return null; } + @Override + public Void visit(CreateConversion statement, S context) { + return null; + } + @Override public Void visit(CreateOperatorFamily statement, S context) { return null; } + @Override + public Void visit(AlterConversion statement, S context) { + return null; + } + @Override public Void visit(CreateAggregate statement, S 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 423f9be3f..7c933b8ae 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -41,6 +41,10 @@ import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; @@ -1207,21 +1211,41 @@ public StringBuilder visit(AlterRule statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(CreateCast statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateOperator statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(CreateTransform statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateOperatorClass statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(CreateConversion statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateOperatorFamily statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(AlterConversion statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateAggregate 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 88e36a40e..dd2693fa3 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 @@ -104,6 +104,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.createTextSearchConfiguration, Feature.createRule, Feature.createCollation, + Feature.createCast, Feature.createTransform, Feature.createConversion, Feature.createOperator, Feature.createOperatorClass, Feature.createOperatorFamily, Feature.createAggregate, Feature.createSchema, Feature.createSequence, Feature.createTable, @@ -124,6 +125,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, Feature.alterTextSearchConfiguration, Feature.alterCollation, Feature.alterRule, + Feature.alterConversion, Feature.alterOperator, Feature.alterOperatorClass, Feature.alterOperatorFamily, Feature.alterAggregate, Feature.alterTablespace, 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 8d638169d..cdc084bfe 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 @@ -122,7 +122,15 @@ public enum PostgresqlVersion implements Version { Feature.alterUserMapping, Feature.createTextSearchConfiguration, Feature.alterTextSearchConfiguration, Feature.createRule, Feature.notifyStatement, - Feature.createCollation, Feature.alterCollation, // https://www.postgresql.org/docs/current/sql-createoperator.html + Feature.createCollation, Feature.alterCollation, + + // https://www.postgresql.org/docs/current/sql-createcast.html + // https://www.postgresql.org/docs/current/sql-createtransform.html + // https://www.postgresql.org/docs/current/sql-createconversion.html + // https://www.postgresql.org/docs/current/sql-alterconversion.html + Feature.createCast, Feature.createTransform, Feature.createConversion, + Feature.alterConversion, + // https://www.postgresql.org/docs/current/sql-createoperator.html // https://www.postgresql.org/docs/current/sql-createopclass.html // https://www.postgresql.org/docs/current/sql-createopfamily.html // https://www.postgresql.org/docs/current/sql-createaggregate.html 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 b30fd8a52..1c7dd754c 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 @@ -19,6 +19,10 @@ import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterRule; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterPolicy; @@ -1717,24 +1721,48 @@ public Void visit(AlterRule statement, S context) { return null; } + @Override + public Void visit(CreateCast statement, S context) { + validateFeature(Feature.createCast); + return null; + } + @Override public Void visit(CreateOperator statement, S context) { validateFeature(Feature.createOperator); return null; } + @Override + public Void visit(CreateTransform statement, S context) { + validateFeature(Feature.createTransform); + return null; + } + @Override public Void visit(CreateOperatorClass statement, S context) { validateFeature(Feature.createOperatorClass); return null; } + @Override + public Void visit(CreateConversion statement, S context) { + validateFeature(Feature.createConversion); + return null; + } + @Override public Void visit(CreateOperatorFamily statement, S context) { validateFeature(Feature.createOperatorFamily); return null; } + @Override + public Void visit(AlterConversion statement, S context) { + validateFeature(Feature.alterConversion); + return null; + } + @Override public Void visit(CreateAggregate statement, S context) { validateFeature(Feature.createAggregate); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f21f6617b..5c970c47e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -59,6 +59,9 @@ import net.sf.jsqlparser.statement.create.rule.CreateRule; import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.notify.ListenStatement; import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; import net.sf.jsqlparser.statement.create.operator.*; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; @@ -4907,6 +4910,108 @@ public class CCJSqlParser extends AbstractJSqlParser { return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); } + /** + * With the PostgreSQL dialect, rejects an unquoted word its grammar does not take as the first + * part of a name, or as a role: a role may be a type or function name keyword, CURRENT_ROLE, + * CURRENT_USER or SESSION_USER. Other dialects keep the names they read today. + */ + private void requireConversionName(String name, boolean role) throws ParseException { + if (!isPostgreSqlDialect()) { + return; + } + int dot = name.indexOf('.'); + String upper = (dot < 0 ? name : name.substring(0, dot)).toUpperCase(Locale.ROOT); + boolean valid = role + ? !POSTGRESQL_RESERVED_KEYWORDS.contains(upper) || "CURRENT_ROLE".equals(upper) + || "CURRENT_USER".equals(upper) || "SESSION_USER".equals(upper) + : !POSTGRESQL_RESERVED_KEYWORDS.contains(upper) + && !POSTGRESQL_TYPE_FUNC_NAME_KEYWORDS.contains(upper); + requireDdlSyntax(valid, "PostgreSQL does not take " + name + " as a name here"); + } + + /** + * Whether CREATE [OR REPLACE] is followed by a PostgreSQL cast, transform or conversion + * definition. PostgreSQL reads CAST, TRANSFORM, CONVERSION and DEFAULT there as nothing else. + * Other products start different statements with these words, such as CREATE CAST (a AS b) + * WITH SPECIFIC FUNCTION f or CREATE TRANSFORM FOR type group (...), so in another dialect + * and without one the statement must also have PostgreSQL's form: CAST (...) WITH FUNCTION, + * WITH INOUT or WITHOUT FUNCTION, TRANSFORM FOR type LANGUAGE, or [DEFAULT] CONVERSION name + * FOR, with OR REPLACE only before TRANSFORM. + */ + private boolean isCastDdlAhead() { + Token next = getToken(1); + boolean postgreSql = isPostgreSqlDialect(); + boolean orReplace = getToken(0).kind == K_REPLACE; + if (next.kind == K_CAST) { + return postgreSql || !orReplace && isCastMethodAhead(); + } + if (isKeywordAhead("TRANSFORM")) { + return postgreSql || getToken(2).kind == K_FOR && isTransformLanguageAhead(); + } + int offset = next.kind == K_DEFAULT ? 2 : 1; + if (!"CONVERSION".equalsIgnoreCase(getToken(offset).image)) { + return postgreSql && next.kind == K_DEFAULT; + } + return postgreSql || !orReplace && getToken(qualifiedNameEnd(offset + 1) + 1).kind == K_FOR; + } + + /** Whether CAST (...) is followed by WITH FUNCTION, WITH INOUT or WITHOUT FUNCTION. */ + private boolean isCastMethodAhead() { + if (getToken(2).kind != OPENING_BRACKET) { + return false; + } + int depth = 1; + int offset = 3; + for (; depth > 0; offset++) { + int kind = getToken(offset).kind; + if (kind == EOF || kind == ST_SEMICOLON) { + return false; + } + depth += kind == OPENING_BRACKET ? 1 : kind == CLOSING_BRACKET ? -1 : 0; + } + Token method = getToken(offset); + Token next = getToken(offset + 1); + return method.kind == K_WITH && (next.kind == K_FUNCTION || "INOUT".equalsIgnoreCase(next.image)) + || method.kind == K_WITHOUT && next.kind == K_FUNCTION; + } + + /** Whether LANGUAGE follows the type of TRANSFORM FOR, outside the type's parentheses. */ + private boolean isTransformLanguageAhead() { + int depth = 0; + for (int offset = 3; ; offset++) { + Token token = getToken(offset); + if (token.kind == EOF || token.kind == ST_SEMICOLON) { + return false; + } + depth += token.kind == OPENING_BRACKET ? 1 : token.kind == CLOSING_BRACKET ? -1 : 0; + if (depth == 0 && "LANGUAGE".equalsIgnoreCase(token.image)) { + return offset > 3; + } + } + } + + /** The offset of the last part of the dotted name that starts at the offset. */ + private int qualifiedNameEnd(int offset) { + int end = offset; + while (".".equals(getToken(end + 1).image)) { + end += 2; + } + return end; + } + + /** + * Whether ALTER is followed by CONVERSION, and in another dialect than PostgreSQL also by a + * name and one of PostgreSQL's actions: RENAME, OWNER or SET. + */ + private boolean isAlterConversionAhead() { + if (!isKeywordAhead("CONVERSION")) { + return false; + } + Token action = getToken(qualifiedNameEnd(2) + 1); + return isPostgreSqlDialect() || action.kind == K_RENAME || action.kind == K_SET + || "OWNER".equalsIgnoreCase(action.image); + } + /** * PostgreSQL's persistence and TABLE keywords of SELECT INTO are read with that dialect and * without one, never with another dialect. @@ -26262,6 +26367,8 @@ Statement Alter(): | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | + LOOKAHEAD({ isAlterConversionAhead() }) statement = AlterConversion() + | // Other routine forms, such as SQL Server definitions, remain unsupported. LOOKAHEAD({ isAlterRoutineAhead() }) statement = AlterRoutine() | @@ -28373,6 +28480,9 @@ Statement Create(): | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | + // a statement of another product that starts with the same words stays unsupported + LOOKAHEAD({ isCastDdlAhead() }) statement = CastDdl(isUsingOrReplace) + | LOOKAHEAD({ isSpatialReferenceSystemAhead() }) statement = CreateSpatialReferenceSystem(isUsingOrReplace) | @@ -30624,6 +30734,113 @@ ListenStatement ListenStatement(): } } +/** + * PostgreSQL's CREATE CAST, CREATE [OR REPLACE] TRANSFORM and CREATE [DEFAULT] CONVERSION, after + * CREATE [OR REPLACE]. Only a transform takes OR REPLACE. + */ +Statement CastDdl(boolean orReplace): +{ Statement statement; } +{ + ( + { requireDdlSyntax(!orReplace, "CREATE CAST does not support OR REPLACE"); } + statement=CreateCast() + | + LOOKAHEAD({ isKeywordAhead("TRANSFORM") }) statement=CreateTransform(orReplace) + | + { requireDdlSyntax(!orReplace, "CREATE CONVERSION does not support OR REPLACE"); } + statement=CreateConversion() + ) + { return statement; } +} + +/** + * PostgreSQL's CreateCastStmt: CAST (source AS target) with WITH FUNCTION function, WITHOUT + * FUNCTION or WITH INOUT, and an optional AS ASSIGNMENT or AS IMPLICIT. + */ +CreateCast CreateCast(): +{ CreateCast statement = new CreateCast(); CastSignature cast; RoutineReference function; Token context; } +{ + cast=CastSignature() { statement.setCast(cast); } + ( + + ( function=RoutineReference(false) { statement.setFunction(function); } + | ContextualKeyword("INOUT") { statement.setMethod(CreateCast.Method.WITH_INOUT); } ) + | + { statement.setMethod(CreateCast.Method.WITHOUT_FUNCTION); } + ) + [ context= { statement.setContext(typeDdlEnum(CreateCast.Context.class, context.image)); } ] + { return statement; } +} + +/** + * PostgreSQL's CreateTransformStmt: TRANSFORM FOR type LANGUAGE lang with a FROM SQL item, a TO + * SQL item, or both in either order. + */ +CreateTransform CreateTransform(boolean orReplace): +{ CreateTransform statement = new CreateTransform(); TransformSignature transform; RoutineReference function; } +{ + ContextualKeyword("TRANSFORM") transform=TransformSignature() + { statement.setOrReplace(orReplace); statement.setTransform(transform); } + "(" + ( + function=TransformFunction() { statement.setFromSqlFunction(function); } + [ "," function=TransformFunction() { statement.setToSqlFunction(function); } ] + | + function=TransformFunction() { statement.setToSqlFunction(function); } + [ "," function=TransformFunction() + { statement.setFromSqlFunction(function); statement.setToSqlFirst(true); } ] + ) + ")" + { return statement; } +} + +/** SQL WITH FUNCTION function, after FROM or TO in a transform item. */ +RoutineReference TransformFunction(): +{ RoutineReference function; } +{ + ContextualKeyword("SQL") function=RoutineReference(false) + { return function; } +} + +/** PostgreSQL's CreateConversionStmt: [DEFAULT] CONVERSION name FOR 'source' TO 'dest' FROM function. */ +CreateConversion CreateConversion(): +{ CreateConversion statement = new CreateConversion(); String name; StringValue encoding; } +{ + [ { statement.setDefaultConversion(true); } ] + ContextualKeyword("CONVERSION") name=TypeDdlName() { requireConversionName(name, false); statement.setName(name); } + encoding=ConversionEncoding() { statement.setSourceEncoding(encoding); } + encoding=ConversionEncoding() { statement.setDestinationEncoding(encoding); } + name=TypeDdlName() { requireConversionName(name, false); statement.setFunction(name); } + { return statement; } +} + +/** An encoding name, which PostgreSQL's grammar reads as a string constant. */ +StringValue ConversionEncoding(): +{ Token token; StringValue value; } +{ + token= + { + value = stringValue(token); + requireDdlSyntax(!isPostgreSqlDialect() || isPostgreSqlStringConstant(value), + "PostgreSQL expects a string constant: " + token.image); + return value; + } +} + +/** PostgreSQL's ALTER CONVERSION name with RENAME TO, OWNER TO or SET SCHEMA. */ +AlterConversion AlterConversion(): +{ AlterConversion statement = new AlterConversion(); String name; } +{ + ContextualKeyword("CONVERSION") name=TypeDdlName() { requireConversionName(name, false); statement.setName(name); } + ( name=RelObjectName() + { requireConversionName(name, false); statement.setAction(AlterConversion.Action.RENAME); statement.setNewName(name); } + | ContextualKeyword("OWNER") name=RelObjectName() + { requireConversionName(name, true); statement.setAction(AlterConversion.Action.OWNER); statement.setOwner(name); } + | name=RelObjectName() + { requireConversionName(name, false); statement.setAction(AlterConversion.Action.SET_SCHEMA); statement.setSchemaName(name); } ) + { return statement; } +} + CreateCollation CreateCollation(): { CreateCollation statement = new CreateCollation(); String name; List options; diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 5eca7088c..2fed5c8ca 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -194,6 +194,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``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`` + * - **PostgreSQL casts, transforms and conversions** + - ``CREATE CAST`` · ``DROP CAST`` · ``CREATE [OR REPLACE] TRANSFORM`` · ``DROP TRANSFORM`` · ``CREATE [DEFAULT] CONVERSION`` · ``ALTER CONVERSION`` * - **PostgreSQL operators and aggregates** - ``CREATE``/``ALTER OPERATOR`` · ``CREATE``/``ALTER OPERATOR CLASS`` · ``CREATE``/``ALTER OPERATOR FAMILY`` · ``CREATE``/``ALTER AGGREGATE`` * - **Salesforce SOQL** diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index aba2e626a..4a1d17aae 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2637,6 +2637,57 @@ PostgreSQL's ``ALTER RULE name ON table RENAME TO new_name`` 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. +PostgreSQL's cast, transform and conversion definitions are typed statements: + +* ``CREATE CAST (source_type AS target_type) {WITH FUNCTION function [(argument_type [, ...])] | WITHOUT FUNCTION | WITH INOUT} [AS ASSIGNMENT | AS IMPLICIT]`` + (`CREATE CAST `_) parses to + ``CreateCast``. ``getCast()`` is the ``CastSignature`` that ``DROP CAST`` uses, + ``getMethod()`` is ``WITH_FUNCTION``, ``WITHOUT_FUNCTION`` or ``WITH_INOUT``, + ``getFunction()`` is the ``RoutineReference`` of ``WITH FUNCTION``, and ``getContext()`` + is ``ASSIGNMENT``, ``IMPLICIT`` or ``null`` for a cast that applies only when written. +* ``CREATE [OR REPLACE] TRANSFORM FOR type_name LANGUAGE lang_name (FROM SQL WITH FUNCTION function, TO SQL WITH FUNCTION function)`` + (`CREATE TRANSFORM `_) parses + to ``CreateTransform``. ``getTransform()`` is the ``TransformSignature`` that + ``DROP TRANSFORM`` uses. ``getFromSqlFunction()`` and ``getToSqlFunction()`` are + ``RoutineReference`` objects, one of them ``null`` when the statement has only the other + item, and ``isToSqlFirst()`` keeps the order of the two items. +* ``CREATE [DEFAULT] CONVERSION name FOR 'source_encoding' TO 'dest_encoding' FROM function_name`` + (`CREATE CONVERSION `_) parses + to ``CreateConversion`` with ``isDefaultConversion()``, ``getName()``, + ``getSourceEncoding()`` and ``getDestinationEncoding()`` as ``StringValue`` and + ``getFunction()``. +* ``ALTER CONVERSION name {RENAME TO new_name | OWNER TO role | SET SCHEMA schema}`` + (`ALTER CONVERSION `_) parses + to ``AlterConversion`` with ``getAction()`` and ``getNewName()``, ``getOwner()`` or + ``getSchemaName()``. + +.. code-block:: java + + CreateCast cast = (CreateCast) CCJSqlParserUtil.parse( + "CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT"); + cast.getCast().getSourceType(); // ColDataType bigint + cast.getMethod(); // CreateCast.Method.WITH_FUNCTION + cast.getFunction().getName(); // int4 + cast.getContext(); // CreateCast.Context.ASSIGNMENT + + CreateTransform transform = (CreateTransform) CCJSqlParserUtil.parse( + "CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (" + + "FROM SQL WITH FUNCTION hstore_to_plpython(internal), " + + "TO SQL WITH FUNCTION plpython_to_hstore(internal))"); + transform.getTransform().getLanguage(); // plpython3u + transform.getToSqlFunction().getName(); // plpython_to_hstore + +Validators report ``createCast``, ``createTransform``, ``createConversion`` and +``alterConversion``, which PostgreSQL enables. With ``Dialect.POSTGRESQL`` these are the only +statements that ``CREATE CAST``, ``CREATE TRANSFORM``, ``CREATE [DEFAULT] CONVERSION`` and +``ALTER CONVERSION`` start, so any other text after them is a parse error. In another dialect +and without one, a statement is read this way when it has PostgreSQL's form +(``CAST (...) WITH FUNCTION``, ``WITH INOUT`` or ``WITHOUT FUNCTION``, +``TRANSFORM FOR type LANGUAGE``, ``CONVERSION name FOR``, and ``ALTER CONVERSION name`` +followed by ``RENAME``, ``OWNER`` or ``SET``); a statement of another product that starts with +the same words, such as ``CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f``, remains an +``UnsupportedStatement``. + ``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 e738fed08..1a27f801d 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -188,6 +188,11 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException { "DROP CAST IF EXISTS (int[] AS public.t) CASCADE", "DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint)", "DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u", + "CREATE CAST (int[] AS public.t) WITH FUNCTION s.f(IN a int[], text) AS ASSIGNMENT", + "CREATE OR REPLACE TRANSFORM FOR public.hstore LANGUAGE plpython3u " + + "(TO SQL WITH FUNCTION g(internal), FROM SQL WITH FUNCTION s.f(internal))", + "CREATE DEFAULT CONVERSION s.c FOR 'UTF8' TO E'LATIN1' FROM s.f", + "ALTER CONVERSION s.c OWNER TO CURRENT_USER", "CREATE OPERATOR public.=== (FUNCTION = s.f, LEFTARG = int[], COMMUTATOR = <==>, " + "NEGATOR = OPERATOR(s.!==), HASHES)", "ALTER OPERATOR s.@#@ (NONE, varchar(3)) SET (RESTRICT = NONE, JOIN = s.f, x = 1)", diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java index cc27c7c09..26de5fae9 100644 --- a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java @@ -18,6 +18,7 @@ import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.alter.AlterAggregate; import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.alter.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterOperator; import net.sf.jsqlparser.statement.alter.AlterOperatorClass; import net.sf.jsqlparser.statement.alter.AlterOperatorFamily; @@ -30,10 +31,13 @@ import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.create.aggregate.CreateAggregate; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; import net.sf.jsqlparser.statement.create.operator.CreateOperator; import net.sf.jsqlparser.statement.create.operator.CreateOperatorClass; import net.sf.jsqlparser.statement.create.operator.CreateOperatorFamily; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -57,6 +61,12 @@ static Stream definitions() { 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 CONVERSION c RENAME TO c2", AlterConversion.class), + Arguments.of("CREATE CAST (int AS text) WITH INOUT", CreateCast.class), + Arguments.of("CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f)", + CreateTransform.class), + Arguments.of("CREATE DEFAULT CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f", + CreateConversion.class), Arguments.of("CREATE OPERATOR === (FUNCTION = f, LEFTARG = int)", CreateOperator.class), Arguments.of("ALTER OPERATOR === (int, int) SET (RESTRICT = NONE)", diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 640cf0fcb..f88eddc62 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -207,10 +207,7 @@ void testInformixSetStatementIssue1945() throws JSQLParserException { @ParameterizedTest @ValueSource(strings = { "CREATE LANGUAGE plperl", - "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler", - "CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM latin1_to_utf8", - "CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (FROM SQL WITH FUNCTION " - + "hstore_to_plpython(internal), TO SQL WITH FUNCTION plpython_to_hstore(internal))"}) + "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler"}) void unknownCreateFallsBackToUnsupportedStatement(String sql) throws JSQLParserException { for (Consumer dialect : Arrays.>asList(parser -> { }, parser -> parser.withDialect(Dialect.POSTGRESQL))) { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCastTransformConversionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCastTransformConversionTest.java new file mode 100644 index 000000000..9ea642513 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCastTransformConversionTest.java @@ -0,0 +1,526 @@ +/*- + * #%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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.ASTNodeAccess; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.Node; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.CastSignature; +import net.sf.jsqlparser.statement.RoutineReference; +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.TransformSignature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.AlterConversion; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.transform.CreateTransform; +import net.sf.jsqlparser.statement.drop.Drop; +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 CREATE CAST, CREATE TRANSFORM, CREATE CONVERSION and ALTER CONVERSION, see + * https://www.postgresql.org/docs/18/sql-createcast.html, + * https://www.postgresql.org/docs/18/sql-createtransform.html, + * https://www.postgresql.org/docs/18/sql-createconversion.html and + * https://www.postgresql.org/docs/18/sql-alterconversion.html. + */ +class PostgreSqlCastTransformConversionTest extends ValidationTestAsserts { + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + @ParameterizedTest + @ValueSource(strings = { + "CREATE CAST (int AS text) WITH INOUT AS ASSIGNMENT", + "CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS IMPLICIT", + "CREATE CAST (a.t AS b.u) WITHOUT FUNCTION", + "CREATE CAST (a.t AS b.u) WITH FUNCTION s.f", + "CREATE CAST (int AS text) WITH FUNCTION f()", + "CREATE CAST (int AS text) WITH FUNCTION \"F\"", + "CREATE CAST (int AS text) WITH FUNCTION left", + "CREATE CAST (int AS text) WITH FUNCTION pg_catalog.int4(bigint)", + "CREATE CAST (int AS int[]) WITH FUNCTION s.f(IN a varchar, integer, boolean) AS ASSIGNMENT", + "CREATE CAST (int AS text) WITH FUNCTION f(VARIADIC int[]) AS ASSIGNMENT", + "CREATE CAST (int AS text) WITH FUNCTION f(OUT a int, INOUT b text)", + "CREATE CAST (timestamp with time zone AS double precision) WITH FUNCTION f(internal)", + "CREATE CAST (\"My T\" AS s.\"U\") WITHOUT FUNCTION AS IMPLICIT", + "CREATE CAST (int AS text) WITH INOUT", + "CREATE TRANSFORM FOR int LANGUAGE plpython3u (FROM SQL WITH FUNCTION f(internal), TO SQL WITH FUNCTION g(internal))", + "CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plpython3u (TO SQL WITH FUNCTION g(internal))", + "CREATE TRANSFORM FOR s.hstore LANGUAGE \"L\" (TO SQL WITH FUNCTION s.g(internal), FROM SQL WITH FUNCTION s.f(internal))", + "CREATE OR REPLACE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION g(internal), FROM SQL WITH FUNCTION f(internal))", + "CREATE TRANSFORM FOR int LANGUAGE sql (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f, TO SQL WITH FUNCTION g)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f(internal, int))", + "CREATE TRANSFORM FOR int[] LANGUAGE l (TO SQL WITH FUNCTION f())", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE DEFAULT CONVERSION s.c FOR 'UTF8' TO 'LATIN1' FROM s.f", + "CREATE CONVERSION \"C 1\" FOR E'UTF8' TO $$LATIN1$$ FROM pg_catalog.utf8_to_iso8859_1", + "CREATE DEFAULT CONVERSION c FOR '' TO '' FROM f", + "CREATE CONVERSION a.b.c FOR 'UTF8' TO 'LATIN1' FROM a.b.f", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM \"F\"", + "ALTER CONVERSION c RENAME TO c2", + "ALTER CONVERSION c OWNER TO CURRENT_USER", + "ALTER CONVERSION c OWNER TO CURRENT_ROLE", + "ALTER CONVERSION c OWNER TO SESSION_USER", + "ALTER CONVERSION c OWNER TO public", + "ALTER CONVERSION c OWNER TO left", + "ALTER CONVERSION c SET SCHEMA s", + "ALTER CONVERSION s.\"C\" OWNER TO \"Role 1\"", + "ALTER CONVERSION a.b.c SET SCHEMA s", + "ALTER CONVERSION c RENAME TO \"select\"", + "ALTER CONVERSION c RENAME TO conversion", + "ALTER CONVERSION conversion SET SCHEMA public"}) + void testTypedStatementsRoundTrip(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertThat(statement).isInstanceOfAny(CreateCast.class, CreateTransform.class, + CreateConversion.class, AlterConversion.class); + // a statement in PostgreSQL's form is read the same without a dialect and in another one + assertThat(assertSqlCanBeParsedAndDeparsed(sql)).isInstanceOf(statement.getClass()); + assertThat(CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.ORACLE))) + .isInstanceOf(statement.getClass()).hasToString(sql); + + StatementDeParser deparser = new StatementDeParser(new StringBuilder()); + statement.accept(deparser); + assertThat(deparser.getBuilder()).hasToString(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, PostgresqlVersion.V10, PostgresqlVersion.V14, + DatabaseType.POSTGRESQL); + validateNoErrors(sql, 1, FeaturesAllowed.DDL); + + Node node = ((ASTNodeAccess) statement).getASTNode(); + assertThat(node.jjtGetFirstToken().beginColumn).isEqualTo(1); + assertThat(node.jjtGetLastToken().endColumn).isEqualTo(sql.length()); + } + + @ParameterizedTest + @ValueSource(strings = { + "create cast (int as text) with inout as assignment", + "create or replace transform for int language l (to sql with function g)", + "create default conversion c for 'UTF8' to 'LATIN1' from f", + "alter conversion c owner to r"}) + void testKeywordsInLowerCase(String sql) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql, POSTGRESQL); + assertThat(statement).isNotInstanceOf(UnsupportedStatement.class); + assertThat(statement.toString()).isEqualToIgnoringCase(sql); + assertThat(CCJSqlParserUtil.parse(statement.toString(), POSTGRESQL)) + .hasToString(statement.toString()); + } + + @Test + void testStatementBoundaries() throws Exception { + String script = "CREATE CAST (int AS text) WITH FUNCTION f; " + + "CREATE CAST (int AS text) WITH INOUT AS IMPLICIT;\n" + + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f);" + + "CREATE DEFAULT CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM s.f; " + + "ALTER CONVERSION c OWNER TO r; ALTER CONVERSION c RENAME TO d; " + + "DROP CAST (int AS text); DROP TRANSFORM FOR int LANGUAGE l; " + + "CREATE OPERATOR === (FUNCTION = f, LEFTARG = int); " + + "CREATE AGGREGATE agg(int) (SFUNC = f, STYPE = int); SELECT 1"; + for (Consumer mode : Arrays.>asList(POSTGRESQL, + parser -> { + })) { + Statements statements = CCJSqlParserUtil.parseStatements(script, mode); + assertThat(statements).hasSize(11); + assertThat(statements).extracting(statement -> statement.getClass().getSimpleName()) + .containsExactly("CreateCast", "CreateCast", "CreateTransform", + "CreateConversion", "AlterConversion", "AlterConversion", "Drop", + "Drop", "CreateOperator", "CreateAggregate", "PlainSelect"); + assertThat(statements.get(0)).hasToString("CREATE CAST (int AS text) WITH FUNCTION f"); + assertThat(statements.get(5)).hasToString("ALTER CONVERSION c RENAME TO d"); + } + } + + @Test + void testCastFields() throws Exception { + CreateCast cast = (CreateCast) CCJSqlParserUtil.parse( + "CREATE CAST (bigint AS s.t) WITH FUNCTION s.f(bigint, integer) AS ASSIGNMENT", + POSTGRESQL); + assertThat(cast.getCast().getSourceType().getDataType()).isEqualTo("bigint"); + assertThat(cast.getCast().getTargetType().getDataType()).isEqualTo("s.t"); + assertThat(cast.getMethod()).isEqualTo(CreateCast.Method.WITH_FUNCTION); + assertThat(cast.getFunction().getName()).isEqualTo("s.f"); + assertThat(cast.getFunction().getArguments()).hasSize(2); + assertThat(cast.getFunction().getArguments().get(1).getDataType().getDataType()) + .isEqualTo("integer"); + assertThat(cast.getContext()).isEqualTo(CreateCast.Context.ASSIGNMENT); + + // the cast is named by the signature object DROP CAST uses, with its own node + Drop drop = (Drop) CCJSqlParserUtil.parse("DROP CAST (bigint AS s.t)", POSTGRESQL); + assertThat(cast.getCast()).isInstanceOf(drop.getCast().getClass()) + .hasToString(drop.getCast().toString()); + Node node = cast.getCast().getASTNode(); + assertThat(node.jjtGetFirstToken().image).isEqualTo("("); + assertThat(node.jjtGetLastToken().image).isEqualTo(")"); + + CreateCast bare = (CreateCast) CCJSqlParserUtil + .parse("CREATE CAST (int AS text) WITH FUNCTION f", POSTGRESQL); + assertThat(bare.getFunction().getArguments()).isNull(); + assertThat(bare.getContext()).isNull(); + + CreateCast inout = (CreateCast) CCJSqlParserUtil + .parse("CREATE CAST (int AS text) WITH INOUT AS IMPLICIT", POSTGRESQL); + assertThat(inout.getMethod()).isEqualTo(CreateCast.Method.WITH_INOUT); + assertThat(inout.getFunction()).isNull(); + assertThat(inout.getContext()).isEqualTo(CreateCast.Context.IMPLICIT); + + CreateCast binary = (CreateCast) CCJSqlParserUtil + .parse("CREATE CAST (int AS text) WITHOUT FUNCTION", POSTGRESQL); + assertThat(binary.getMethod()).isEqualTo(CreateCast.Method.WITHOUT_FUNCTION); + assertThat(binary.getFunction()).isNull(); + } + + @Test + void testCastMethodAndFunctionStayConsistent() { + RoutineReference function = new RoutineReference(); + function.setName("f"); + CreateCast cast = new CreateCast() + .withCast(new CastSignature(new ColDataType("int"), new ColDataType("text"))) + .withFunction(function).withContext(CreateCast.Context.IMPLICIT); + assertThat(cast.getMethod()).isEqualTo(CreateCast.Method.WITH_FUNCTION); + assertThat(cast).hasToString("CREATE CAST (int AS text) WITH FUNCTION f AS IMPLICIT"); + + cast.setMethod(CreateCast.Method.WITH_INOUT); + assertThat(cast.getFunction()).isNull(); + assertThat(cast.withContext(null)).hasToString("CREATE CAST (int AS text) WITH INOUT"); + assertThat(cast.withMethod(CreateCast.Method.WITHOUT_FUNCTION)) + .hasToString("CREATE CAST (int AS text) WITHOUT FUNCTION"); + cast.setFunction(null); + assertThat(cast.getMethod()).isEqualTo(CreateCast.Method.WITHOUT_FUNCTION); + } + + @Test + void testTransformFields() throws Exception { + CreateTransform transform = (CreateTransform) CCJSqlParserUtil.parse( + "CREATE OR REPLACE TRANSFORM FOR public.hstore LANGUAGE plpython3u " + + "(TO SQL WITH FUNCTION g(internal), FROM SQL WITH FUNCTION s.f(internal))", + POSTGRESQL); + assertThat(transform.isOrReplace()).isTrue(); + assertThat(transform.getTransform().getDataType().getDataType()) + .isEqualTo("public.hstore"); + assertThat(transform.getTransform().getLanguage()).isEqualTo("plpython3u"); + assertThat(transform.getFromSqlFunction().getName()).isEqualTo("s.f"); + assertThat(transform.getToSqlFunction().getName()).isEqualTo("g"); + assertThat(transform.getToSqlFunction().getArguments()).hasSize(1); + assertThat(transform.isToSqlFirst()).isTrue(); + + // the transform is named by the signature object DROP TRANSFORM uses, with its own node + Drop drop = (Drop) CCJSqlParserUtil + .parse("DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u", POSTGRESQL); + assertThat(transform.getTransform()).isInstanceOf(drop.getTransform().getClass()) + .hasToString(drop.getTransform().toString()); + Node node = transform.getTransform().getASTNode(); + assertThat(node.jjtGetFirstToken().image).isEqualTo("FOR"); + assertThat(node.jjtGetLastToken().image).isEqualTo("plpython3u"); + + transform.setToSqlFirst(false); + transform.setOrReplace(false); + assertThat(transform).hasToString("CREATE TRANSFORM FOR public.hstore LANGUAGE plpython3u " + + "(FROM SQL WITH FUNCTION s.f(internal), TO SQL WITH FUNCTION g(internal))"); + transform.setFromSqlFunction(null); + assertThat(transform).hasToString("CREATE TRANSFORM FOR public.hstore LANGUAGE plpython3u " + + "(TO SQL WITH FUNCTION g(internal))"); + + CreateTransform single = (CreateTransform) CCJSqlParserUtil.parse( + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f)", POSTGRESQL); + assertThat(single.isOrReplace()).isFalse(); + assertThat(single.getToSqlFunction()).isNull(); + assertThat(single.isToSqlFirst()).isFalse(); + assertThat(single.getFromSqlFunction().getArguments()).isNull(); + + RoutineReference function = new RoutineReference(); + function.setName("g"); + assertThat(new CreateTransform().withOrReplace(true) + .withTransform(new TransformSignature(new ColDataType("int"), "l")) + .withToSqlFunction(function).withFromSqlFunction(function).withToSqlFirst(true)) + .hasToString("CREATE OR REPLACE TRANSFORM FOR int LANGUAGE l " + + "(TO SQL WITH FUNCTION g, FROM SQL WITH FUNCTION g)"); + } + + @Test + void testConversionFields() throws Exception { + CreateConversion conversion = (CreateConversion) CCJSqlParserUtil.parse( + "CREATE DEFAULT CONVERSION s.\"C\" FOR 'UTF8' TO E'LATIN1' FROM pg_catalog.f", + POSTGRESQL); + assertThat(conversion.isDefaultConversion()).isTrue(); + assertThat(conversion.getName()).isEqualTo("s.\"C\""); + assertThat(conversion.getSourceEncoding().getValue()).isEqualTo("UTF8"); + assertThat(conversion.getDestinationEncoding().getValue()).isEqualTo("LATIN1"); + assertThat(conversion.getDestinationEncoding().getPrefix()).isEqualTo("E"); + assertThat(conversion.getFunction()).isEqualTo("pg_catalog.f"); + assertThat(conversion.getSourceEncoding().getASTNode().jjtGetFirstToken().image) + .isEqualTo("'UTF8'"); + + // U&'...' is a string constant only for the PostgreSQL dialect's lexer + String unicode = "CREATE CONVERSION c FOR U&'UTF8' TO $$LATIN1$$ FROM f"; + assertThat(assertSqlCanBeParsedAndDeparsed(unicode, false, POSTGRESQL)) + .isInstanceOf(CreateConversion.class); + + assertThat(((CreateConversion) CCJSqlParserUtil + .parse("CREATE CONVERSION c FOR 'a' TO 'b' FROM f", POSTGRESQL)) + .isDefaultConversion()).isFalse(); + assertThat(new CreateConversion().withName("c").withSourceEncoding(new StringValue("a")) + .withDestinationEncoding(new StringValue("b")).withFunction("f") + .withDefaultConversion(true)) + .hasToString("CREATE DEFAULT CONVERSION c FOR 'a' TO 'b' FROM f"); + } + + @Test + void testAlterConversionFields() throws Exception { + AlterConversion rename = (AlterConversion) CCJSqlParserUtil + .parse("ALTER CONVERSION s.c RENAME TO \"C 2\"", POSTGRESQL); + assertThat(rename.getName()).isEqualTo("s.c"); + assertThat(rename.getAction()).isEqualTo(AlterConversion.Action.RENAME); + assertThat(rename.getNewName()).isEqualTo("\"C 2\""); + assertThat(rename.getOwner()).isNull(); + assertThat(rename.getSchemaName()).isNull(); + + AlterConversion owner = (AlterConversion) CCJSqlParserUtil + .parse("ALTER CONVERSION c OWNER TO CURRENT_USER", POSTGRESQL); + assertThat(owner.getAction()).isEqualTo(AlterConversion.Action.OWNER); + assertThat(owner.getOwner()).isEqualTo("CURRENT_USER"); + + AlterConversion schema = (AlterConversion) CCJSqlParserUtil + .parse("ALTER CONVERSION c SET SCHEMA app", POSTGRESQL); + assertThat(schema.getAction()).isEqualTo(AlterConversion.Action.SET_SCHEMA); + assertThat(schema.getSchemaName()).isEqualTo("app"); + + assertThat(new AlterConversion().withName("c").withAction(AlterConversion.Action.RENAME) + .withNewName("d")).hasToString("ALTER CONVERSION c RENAME TO d"); + assertThat(new AlterConversion().withName("c").withAction(AlterConversion.Action.OWNER) + .withOwner("r")).hasToString("ALTER CONVERSION c OWNER TO r"); + assertThat(new AlterConversion().withName("c") + .withAction(AlterConversion.Action.SET_SCHEMA).withSchemaName("s")) + .hasToString("ALTER CONVERSION c SET SCHEMA s"); + } + + @Test + void testVisitors() throws Exception { + StatementVisitorAdapter visitor = new StatementVisitorAdapter() { + @Override + public String visit(CreateCast statement, S context) { + return context + ":cast"; + } + + @Override + public String visit(CreateTransform statement, S context) { + return context + ":transform"; + } + + @Override + public String visit(CreateConversion statement, S context) { + return context + ":conversion"; + } + + @Override + public String visit(AlterConversion statement, S context) { + return context + ":alter"; + } + }; + assertThat(parse("CREATE CAST (int AS text) WITH INOUT").accept(visitor, "c")) + .isEqualTo("c:cast"); + assertThat(parse("CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f)") + .accept(visitor, "c")).isEqualTo("c:transform"); + assertThat(parse("CREATE CONVERSION c FOR 'a' TO 'b' FROM f").accept(visitor, "c")) + .isEqualTo("c:conversion"); + assertThat(parse("ALTER CONVERSION c SET SCHEMA s").accept(visitor, "c")) + .isEqualTo("c:alter"); + + // the default visitor methods return nothing and the one-argument forms delegate + StatementVisitorAdapter adapter = new StatementVisitorAdapter<>(); + for (String sql : new String[] {"CREATE CAST (int AS text) WITH INOUT", + "CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f)", + "CREATE CONVERSION c FOR 'a' TO 'b' FROM f", "ALTER CONVERSION c SET SCHEMA s"}) { + assertThat(parse(sql).accept(adapter, null)).isNull(); + } + adapter.visit((CreateCast) parse("CREATE CAST (int AS text) WITH INOUT")); + adapter.visit((CreateTransform) parse( + "CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f)")); + adapter.visit((CreateConversion) parse("CREATE CONVERSION c FOR 'a' TO 'b' FROM f")); + adapter.visit((AlterConversion) parse("ALTER CONVERSION c SET SCHEMA s")); + } + + @Test + void testValidation() throws Exception { + String cast = "CREATE CAST (int AS text) WITH INOUT"; + validateNoErrors(cast, 1, FeaturesAllowed.CREATE); + validateNotAllowed(cast, 1, 1, FeaturesAllowed.SELECT, Feature.createCast); + validate(cast, 1, 1, MySqlVersion.V8_0); + + String transform = "CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f)"; + validateNoErrors(transform, 1, FeaturesAllowed.CREATE); + validateNotAllowed(transform, 1, 1, FeaturesAllowed.SELECT, Feature.createTransform); + validate(transform, 1, 1, MySqlVersion.V8_0); + + String conversion = "CREATE CONVERSION c FOR 'a' TO 'b' FROM f"; + validateNoErrors(conversion, 1, FeaturesAllowed.CREATE); + validateNotAllowed(conversion, 1, 1, FeaturesAllowed.SELECT, Feature.createConversion); + validate(conversion, 1, 1, MySqlVersion.V8_0); + + String alter = "ALTER CONVERSION c RENAME TO d"; + validateNoErrors(alter, 1, FeaturesAllowed.ALTER); + validateNotAllowed(alter, 1, 1, FeaturesAllowed.SELECT, Feature.alterConversion); + validate(alter, 1, 1, MySqlVersion.V8_0); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OR REPLACE CAST (int AS text) WITH INOUT", + "CREATE CAST (int AS text)", + "CREATE CAST (int AS text) WITH FUNCTION", + "CREATE CAST (int AS text) WITHOUT FUNCTION f(int)", + "CREATE CAST (int AS text) WITH INOUT AS EXPLICIT", + "CREATE CAST (int AS text) WITH INOUT AS", + "CREATE CAST (int, text) WITH INOUT", + "CREATE CAST int AS text WITH INOUT", + "CREATE CAST (int AS text) WITH INOUT AS ASSIGNMENT AS IMPLICIT", + "CREATE CAST (int AS text) WITH FUNCTION f AS ASSIGNMENT IMPLICIT", + "CREATE CAST (int AS text) AS ASSIGNMENT WITH INOUT", + "CREATE CAST (int AS text) WITHOUT INOUT", + "CREATE CAST IF NOT EXISTS (int AS text) WITH INOUT", + "CREATE CAST (int AS text) WITH SPECIFIC FUNCTION f", + "CREATE CAST (int AS text) WITH FUNCTION f(*)", + "CREATE CAST (int AS text) WITH FUNCTION f, g", + "CREATE CAST (int AS text) WITH FUNCTION 'f'", + "CREATE CAST (int AS) WITH INOUT", + "CREATE CAST () WITH INOUT", + "CREATE CAST s.c (int AS text) WITH INOUT", + "CREATE CAST (int AS text) WITH INOUT CASCADE", + "CREATE DEFAULT CAST (int AS text) WITH INOUT", + "CREATE TRANSFORM FOR int LANGUAGE l ()", + "CREATE TRANSFORM FOR int LANGUAGE l", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f, FROM SQL WITH FUNCTION g)", + "CREATE TRANSFORM FOR int LANGUAGE l (TO SQL WITH FUNCTION f, TO SQL WITH FUNCTION g)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f, TO SQL WITH FUNCTION g, FROM SQL WITH FUNCTION h)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f TO SQL WITH FUNCTION g)", + "CREATE TRANSFORM FOR int (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM int LANGUAGE l (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE l (WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f,)", + "CREATE TRANSFORM FOR int LANGUAGE l FROM SQL WITH FUNCTION f", + "CREATE TRANSFORM IF NOT EXISTS FOR int LANGUAGE l (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE 'l' (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE s.l (FROM SQL WITH FUNCTION f)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITHOUT FUNCTION)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH INOUT)", + "CREATE TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f) CASCADE", + "CREATE DEFAULT TRANSFORM FOR int LANGUAGE l (FROM SQL WITH FUNCTION f)", + "CREATE OR REPLACE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE OR REPLACE DEFAULT CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR UTF8 TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR 'UTF8' TO LATIN1 FROM f", + "CREATE CONVERSION c FOR 'UTF8' 'LATIN1' FROM f", + "CREATE CONVERSION c TO 'LATIN1' FOR 'UTF8' FROM f", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1'", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f(int)", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM 'f'", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f, g", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' WITH FUNCTION f", + "CREATE CONVERSION FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR N'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR B'01' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR X'01' TO 'LATIN1' FROM f", + "CREATE CONVERSION IF NOT EXISTS c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION DEFAULT c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f WITH x", + "CREATE CONVERSION select FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM select", + "ALTER CONVERSION c", + "ALTER CONVERSION c RENAME c2", + "ALTER CONVERSION c RENAME TO s.c2", + "ALTER CONVERSION c RENAME TO left", + "ALTER CONVERSION c RENAME TO select", + "ALTER CONVERSION c SET SCHEMA a.b", + "ALTER CONVERSION c SET SCHEMA user", + "ALTER CONVERSION c SET SCHEMA", + "ALTER CONVERSION c SET (a = 1)", + "ALTER CONVERSION c SET TABLESPACE x", + "ALTER CONVERSION c OWNER TO a, b", + "ALTER CONVERSION c OWNER TO user", + "ALTER CONVERSION c OWNER TO 'r'", + "ALTER CONVERSION c OWNER TO CURRENT_USER()", + "ALTER CONVERSION c OWNER TO CURRENT_USER CASCADE", + "ALTER CONVERSION c OWNER r", + "ALTER CONVERSION c REFRESH VERSION", + "ALTER CONVERSION IF EXISTS c RENAME TO c2", + "ALTER CONVERSION c RENAME TO c2, OWNER TO r", + "ALTER CONVERSION c, d RENAME TO c2", + "ALTER CONVERSION select RENAME TO c2", + "ALTER DEFAULT CONVERSION c RENAME TO c2", + "ALTER CONVERSION c DEPENDS ON EXTENSION e"}) + void testRejectedWithPostgreSqlDialect(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + } + + /** + * Without the PostgreSQL dialect, statements of another product that start with the same words + * but do not have PostgreSQL's form stay unsupported statements, as they were before. + */ + @ParameterizedTest + @ValueSource(strings = { + "CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f", + "CREATE CAST (percent AS decimal)", + "CREATE OR REPLACE CAST (a AS b) WITH INOUT", + "CREATE TRANSFORM FOR t g1 (TO SQL WITH FUNCTION f, FROM SQL WITH FUNCTION g)", + "CREATE OR REPLACE CONVERSION c FOR 'UTF8' TO 'LATIN1' FROM f", + "CREATE DEFAULT conversion AS 1", + "CREATE DEFAULT x", + "CREATE CONVERSION c", + "ALTER CONVERSION c REFRESH", + "ALTER CONVERSION c"}) + void testOtherFormsStayUnsupportedWithoutThePostgreSqlDialect(String sql) throws Exception { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql, POSTGRESQL)) + .isInstanceOf(JSQLParserException.class); + assertThat(CCJSqlParserUtil.parse(sql)).isInstanceOf(UnsupportedStatement.class); + assertThat(CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.ORACLE))) + .isInstanceOf(UnsupportedStatement.class); + } + + /** Only the PostgreSQL dialect applies the name rules of PostgreSQL's grammar. */ + @ParameterizedTest + @ValueSource(strings = { + "ALTER CONVERSION c SET SCHEMA user", + "CREATE CONVERSION c FOR N'UTF8' TO 'LATIN1' FROM f"}) + void testPostgreSqlNameRulesNeedTheDialect(String sql) throws Exception { + assertThat(CCJSqlParserUtil.parse(sql)).isNotInstanceOf(UnsupportedStatement.class) + .hasToString(sql); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, POSTGRESQL); + } +}