From 8d6208d6d478a7f56685c07d1367a569eb5d8e4e Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 13 Nov 2026 10:54:11 +0900 Subject: [PATCH] feat: parse PostgreSQL CREATE CAST, CREATE TRANSFORM, CREATE CONVERSION and ALTER CONVERSION CREATE CAST (source AS target) WITH FUNCTION f(args) | WITHOUT FUNCTION | WITH INOUT [AS ASSIGNMENT | AS IMPLICIT], CREATE [OR REPLACE] TRANSFORM FOR type LANGUAGE lang (FROM SQL WITH FUNCTION f, TO SQL WITH FUNCTION g), CREATE [DEFAULT] CONVERSION name FOR 'enc' TO 'enc' FROM function and ALTER CONVERSION name RENAME TO | OWNER TO | SET SCHEMA were unsupported statements. They parse to CreateCast, CreateTransform, CreateConversion and AlterConversion. A cast and a transform are named by the CastSignature and TransformSignature of DROP CAST and DROP TRANSFORM, and functions are RoutineReference objects, read by the existing productions. The statements are wired into the deparser, visitors, table finder, validation (createCast, createTransform, createConversion and alterConversion, enabled for PostgreSQL) and feature analysis. With the PostgreSQL dialect these words start only these statements, so forms the server reports as syntax errors are parse errors. In another dialect and without one, a statement is read this way when it has PostgreSQL's form, and statements of other products that start with the same words, such as CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f, stay unsupported statements. Signed-off-by: minleejae --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 24 + .../statement/StatementFeatureVisitor.java | 24 + .../statement/StatementVisitor.java | 36 ++ .../statement/StatementVisitorAdapter.java | 24 + .../statement/alter/AlterConversion.java | 130 +++++ .../statement/create/cast/CreateCast.java | 152 +++++ .../create/conversion/CreateConversion.java | 122 ++++ .../create/transform/CreateTransform.java | 150 +++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 24 + .../util/deparser/StatementDeParser.java | 24 + .../validation/feature/FeaturesAllowed.java | 2 + .../validation/feature/PostgresqlVersion.java | 7 + .../validator/StatementValidator.java | 28 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 217 ++++++++ src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 51 ++ .../parser/ASTNodeCoverageTest.java | 5 + ...tatementFeatureVisitorMetadataDdlTest.java | 10 + .../statement/UnsupportedStatementTest.java | 3 - ...PostgreSqlCastTransformConversionTest.java | 524 ++++++++++++++++++ 21 files changed, 1557 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterConversion.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/cast/CreateCast.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/conversion/CreateConversion.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/transform/CreateTransform.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCastTransformConversionTest.java diff --git a/README.md b/README.md index 9eda48d5b..18c2ca116 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,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` | | **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 a3a161774..87ee7aac9 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -1001,6 +1001,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, /** * SQL block starting with "BEGIN" and ends with "END" 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 f8b9a912c..0ba95e98b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -37,6 +37,10 @@ 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.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; @@ -1557,4 +1561,24 @@ public Void visit(CreateCollation statement, S context) { public Void visit(AlterRule statement, S context) { return schemaOnly(); } + + @Override + public Void visit(CreateCast statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTransform statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateConversion statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterConversion 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 ada0eb7a8..085bc9b8b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -25,6 +25,10 @@ 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.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; @@ -1182,4 +1186,36 @@ default T visit(AlterRule statement, S context) { 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); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 02f7795e8..2285e2845 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; @@ -1241,4 +1245,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 b9fc69a08..5a4f79436 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -131,6 +131,10 @@ 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.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; @@ -3532,4 +3536,24 @@ public Void visit(AlterRule statement, S context) { visit(statement.getTable(), context); return null; } + + @Override + public Void visit(CreateCast statement, S context) { + return null; + } + + @Override + public Void visit(CreateTransform statement, S context) { + return null; + } + + @Override + public Void visit(CreateConversion statement, S context) { + return null; + } + + @Override + public Void visit(AlterConversion 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 3f0b98ead..de378f4a3 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -33,6 +33,10 @@ 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.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; @@ -1153,4 +1157,24 @@ public StringBuilder visit(AlterTrigger statement, S context) { 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(CreateTransform statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(CreateConversion statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterConversion 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 e8992ce57..45dd3a78e 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.createSchema, Feature.createSequence, Feature.createTable, Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, Feature.createAccessMethod, Feature.createAccessMethodTable, @@ -122,6 +123,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, Feature.alterTextSearchConfiguration, Feature.alterCollation, Feature.alterRule, + Feature.alterConversion, 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 d85a3b438..767ffe571 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 @@ -123,6 +123,13 @@ public enum PostgresqlVersion implements Version { Feature.createTextSearchConfiguration, Feature.alterTextSearchConfiguration, Feature.createRule, Feature.notifyStatement, 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, Feature.createPublication, Feature.alterPublication, Feature.createSubscription, Feature.alterSubscription, Feature.createType, Feature.alterType, 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 f586fa9f4..5cc93f461 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 @@ -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.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterPolicy; @@ -1648,4 +1652,28 @@ public Void visit(AlterRule statement, S context) { validateOptionalFromItem(statement.getTable()); return null; } + + @Override + public Void visit(CreateCast statement, S context) { + validateFeature(Feature.createCast); + return null; + } + + @Override + public Void visit(CreateTransform statement, S context) { + validateFeature(Feature.createTransform); + return null; + } + + @Override + public Void visit(CreateConversion statement, S context) { + validateFeature(Feature.createConversion); + return null; + } + + @Override + public Void visit(AlterConversion statement, S context) { + validateFeature(Feature.alterConversion); + 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 f2ad5ec52..87a54ebb1 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.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; @@ -4230,6 +4233,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. @@ -24720,6 +24825,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() | @@ -26825,6 +26932,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({ isMySqlCreateRoleAhead() }) statement = MySqlCreateRole() { requireAccessSyntax(!isUsingOrReplace, "CREATE ROLE does not accept OR REPLACE"); } | @@ -28796,6 +28906,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 b03b08dbb..be5b2e845 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -188,6 +188,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`` * - **Salesforce SOQL** - ``INCLUDES`` · ``EXCLUDES`` diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index b6de3a5d7..16791a3f5 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2499,6 +2499,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 b4f179fc3..bd0dd9240 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -172,6 +172,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", "SELECT a FROM t AS x USE KEY (i1) FORCE INDEX FOR JOIN (i2, i3) WHERE a = 1", "SELECT a FROM t USE INDEX () JOIN s IGNORE INDEX FOR ORDER BY (i1) ON t.a = s.a", "UPDATE t USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2) SET a = 1", diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java index d31b4586c..7265492ab 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.AlterConversion; import net.sf.jsqlparser.statement.alter.AlterRule; import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterStatistics; @@ -25,7 +26,10 @@ import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.statement.create.cast.CreateCast; +import net.sf.jsqlparser.statement.create.conversion.CreateConversion; 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; @@ -49,6 +53,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("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/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 095fb8c0a..44f078cc7 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -210,9 +210,6 @@ void testInformixSetStatementIssue1945() throws JSQLParserException { "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plsample HANDLER plsample_call_handler", "CREATE OPERATOR CLASS gist__int_ops DEFAULT FOR TYPE _int4 USING gist AS OPERATOR 3 &&", "CREATE OPERATOR FAMILY myfam USING btree", - "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 AGGREGATE myavg(float8) (SFUNC = float8_accum, STYPE = float8[])"}) void unknownCreateFallsBackToUnsupportedStatement(String sql) throws JSQLParserException { for (Consumer dialect : Arrays.>asList(parser -> { 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..e194fa5b4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCastTransformConversionTest.java @@ -0,0 +1,524 @@ +/*- + * #%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; SELECT 1"; + for (Consumer mode : Arrays.>asList(POSTGRESQL, + parser -> { + })) { + Statements statements = CCJSqlParserUtil.parseStatements(script, mode); + assertThat(statements).hasSize(9); + assertThat(statements).extracting(statement -> statement.getClass().getSimpleName()) + .containsExactly("CreateCast", "CreateCast", "CreateTransform", + "CreateConversion", "AlterConversion", "AlterConversion", "Drop", + "Drop", "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); + } +}