From 14fb6ffb5945258a30a4bbccdb016779c12d4f3e Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 22:45:01 +0900 Subject: [PATCH] feat: reuse foreign-data options for PostgreSQL wrappers, servers and user mappings Signed-off-by: minleejae --- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../statement/StatementVisitor.java | 54 ++++++ .../alter/AlterForeignDataWrapper.java | 100 +++++++++++ .../statement/alter/AlterServer.java | 104 ++++++++++++ .../statement/alter/AlterUserMapping.java | 48 ++++++ .../create/fdw/CreateForeignDataWrapper.java | 49 ++++++ .../fdw/ForeignDataWrapperFunctions.java | 65 +++++++ .../statement/create/server/CreateServer.java | 96 +++++++++++ .../create/usermapping/CreateUserMapping.java | 61 +++++++ .../foreign/ForeignDataStatement.java | 54 ++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 42 +++++ .../util/deparser/StatementDeParser.java | 42 +++++ .../validation/feature/FeaturesAllowed.java | 4 +- .../validation/feature/PostgresqlVersion.java | 3 + .../validator/StatementValidator.java | 48 ++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 145 +++++++++++++++- .../PostgreSqlForeignDataObjectsTest.java | 158 ++++++++++++++++++ 17 files changed, 1073 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataWrapper.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterServer.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterUserMapping.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/fdw/CreateForeignDataWrapper.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/fdw/ForeignDataWrapperFunctions.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/usermapping/CreateUserMapping.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/foreign/ForeignDataStatement.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java 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 e09ae8553..8e57b1990 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -580,6 +580,8 @@ public enum Feature { */ createSequence, /** Publication and subscription definitions. */ + createForeignDataWrapper, alterForeignDataWrapper, createServer, alterServer, createUserMapping, alterUserMapping, + createPublication, alterPublication, createSubscription, alterSubscription, /** * Structured type, domain and extension statements. diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 03755d652..bc94336ae 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -733,4 +739,52 @@ default T visit(AlterStatistics statement, S context) { default void visit(AlterStatistics statement) { visit(statement, null); } + + default T visit(CreateForeignDataWrapper statement, S context) { + return null; + } + + default void visit(CreateForeignDataWrapper statement) { + visit(statement, null); + } + + default T visit(AlterForeignDataWrapper statement, S context) { + return null; + } + + default void visit(AlterForeignDataWrapper statement) { + visit(statement, null); + } + + default T visit(CreateServer statement, S context) { + return null; + } + + default void visit(CreateServer statement) { + visit(statement, null); + } + + default T visit(AlterServer statement, S context) { + return null; + } + + default void visit(AlterServer statement) { + visit(statement, null); + } + + default T visit(CreateUserMapping statement, S context) { + return null; + } + + default void visit(CreateUserMapping statement) { + visit(statement, null); + } + + default T visit(AlterUserMapping statement, S context) { + return null; + } + + default void visit(AlterUserMapping statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataWrapper.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataWrapper.java new file mode 100644 index 000000000..2da15e6ac --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataWrapper.java @@ -0,0 +1,100 @@ +/*- + * #%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 java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; +import net.sf.jsqlparser.statement.create.fdw.ForeignDataWrapperFunctions; + +public class AlterForeignDataWrapper extends ForeignDataStatement { + public enum Action { + OPTIONS, OWNER, RENAME + } + + private String name; + private Action action = Action.OPTIONS; + private String newName; + private String owner; + private ForeignDataWrapperFunctions functions = new ForeignDataWrapperFunctions(); + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public Action getAction() { + return action; + } + + public void setAction(Action value) { + action = value; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String value) { + newName = value; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String value) { + owner = value; + } + + public ForeignDataWrapperFunctions getFunctions() { + return functions; + } + + public void setFunctions(ForeignDataWrapperFunctions value) { + functions = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("ALTER FOREIGN DATA WRAPPER ").append(name); + switch (action) { + case RENAME: + sql.append(" RENAME TO ").append(newName); + break; + case OWNER: + sql.append(" OWNER TO ").append(owner); + break; + case OPTIONS: + functions.appendTo(sql); + appendOptionsTo(sql, printer); + break; + default: + throw new IllegalStateException("Unknown foreign-data wrapper action: " + action); + } + return sql; + } + + @Override + public void visitExpressions(Consumer visitor) { + if (action == Action.OPTIONS) { + super.visitExpressions(visitor); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterServer.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterServer.java new file mode 100644 index 000000000..55b5f0411 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterServer.java @@ -0,0 +1,104 @@ +/*- + * #%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 java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + +public class AlterServer extends ForeignDataStatement { + public enum Action { + OPTIONS, OWNER, RENAME + } + + private String name; + private Action action = Action.OPTIONS; + private String newName; + private String owner; + private Expression version; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public Action getAction() { + return action; + } + + public void setAction(Action value) { + action = value; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String value) { + newName = value; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String value) { + owner = value; + } + + public Expression getVersion() { + return version; + } + + public void setVersion(Expression value) { + version = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("ALTER SERVER ").append(name); + switch (action) { + case RENAME: + sql.append(" RENAME TO ").append(newName); + break; + case OWNER: + sql.append(" OWNER TO ").append(owner); + break; + case OPTIONS: + if (version != null) { + sql.append(" VERSION "); + printer.accept(version); + } + appendOptionsTo(sql, printer); + break; + default: + throw new IllegalStateException("Unknown server action: " + action); + } + return sql; + } + + @Override + public void visitExpressions(Consumer visitor) { + if (action != Action.OPTIONS) { + return; + } + super.visitExpressions(visitor); + visit(version, visitor); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterUserMapping.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterUserMapping.java new file mode 100644 index 000000000..f82d633fd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterUserMapping.java @@ -0,0 +1,48 @@ +/*- + * #%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 java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + +public class AlterUserMapping extends ForeignDataStatement { + private String user; + private String server; + + public String getUser() { + return user; + } + + public void setUser(String value) { + user = value; + } + + public String getServer() { + return server; + } + + public void setServer(String value) { + server = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("ALTER USER MAPPING FOR ").append(user).append(" SERVER ").append(server); + appendOptionsTo(sql, printer); + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/fdw/CreateForeignDataWrapper.java b/src/main/java/net/sf/jsqlparser/statement/create/fdw/CreateForeignDataWrapper.java new file mode 100644 index 000000000..a7f8f7053 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/fdw/CreateForeignDataWrapper.java @@ -0,0 +1,49 @@ +/*- + * #%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.fdw; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + +public class CreateForeignDataWrapper extends ForeignDataStatement { + private String name; + private ForeignDataWrapperFunctions functions = new ForeignDataWrapperFunctions(); + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public ForeignDataWrapperFunctions getFunctions() { + return functions; + } + + public void setFunctions(ForeignDataWrapperFunctions value) { + functions = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE FOREIGN DATA WRAPPER ").append(name); + functions.appendTo(sql); + appendOptionsTo(sql, printer); + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/fdw/ForeignDataWrapperFunctions.java b/src/main/java/net/sf/jsqlparser/statement/create/fdw/ForeignDataWrapperFunctions.java new file mode 100644 index 000000000..81a29a37b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/fdw/ForeignDataWrapperFunctions.java @@ -0,0 +1,65 @@ +/*- + * #%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.fdw; + +import java.io.Serializable; + +/** Distinguishes omitted handler/validator clauses from explicit NO HANDLER/VALIDATOR. */ +public class ForeignDataWrapperFunctions implements Serializable { + private boolean handlerSpecified; + private String handler; + private boolean validatorSpecified; + private String validator; + + public boolean isHandlerSpecified() { + return handlerSpecified; + } + + public String getHandler() { + return handler; + } + + public void setHandler(String handler) { + this.handler = handler; + handlerSpecified = true; + } + + public void clearHandler() { + handler = null; + handlerSpecified = false; + } + + public boolean isValidatorSpecified() { + return validatorSpecified; + } + + public String getValidator() { + return validator; + } + + public void setValidator(String validator) { + this.validator = validator; + validatorSpecified = true; + } + + public void clearValidator() { + validator = null; + validatorSpecified = false; + } + + public void appendTo(StringBuilder sql) { + if (handlerSpecified) { + sql.append(handler == null ? " NO HANDLER" : " HANDLER " + handler); + } + if (validatorSpecified) { + sql.append(validator == null ? " NO VALIDATOR" : " VALIDATOR " + validator); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java b/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java new file mode 100644 index 000000000..e4ebc4f2e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/server/CreateServer.java @@ -0,0 +1,96 @@ +/*- + * #%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.server; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; +import net.sf.jsqlparser.expression.StringValue; + +public class CreateServer extends ForeignDataStatement { + private String name; + private boolean ifNotExists; + private StringValue type; + private Expression version; + private String foreignDataWrapper; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean value) { + ifNotExists = value; + } + + public StringValue getType() { + return type; + } + + public void setType(StringValue value) { + type = value; + } + + public Expression getVersion() { + return version; + } + + public void setVersion(Expression value) { + version = value; + } + + public String getForeignDataWrapper() { + return foreignDataWrapper; + } + + public void setForeignDataWrapper(String value) { + foreignDataWrapper = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE SERVER "); + if (ifNotExists) { + sql.append("IF NOT EXISTS "); + } + sql.append(name); + if (type != null) { + sql.append(" TYPE "); + printer.accept(type); + } + if (version != null) { + sql.append(" VERSION "); + printer.accept(version); + } + sql.append(" FOREIGN DATA WRAPPER ").append(foreignDataWrapper); + appendOptionsTo(sql, printer); + return sql; + } + + @Override + public void visitExpressions(Consumer visitor) { + super.visitExpressions(visitor); + visit(type, visitor); + visit(version, visitor); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/usermapping/CreateUserMapping.java b/src/main/java/net/sf/jsqlparser/statement/create/usermapping/CreateUserMapping.java new file mode 100644 index 000000000..5502ddb1e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/usermapping/CreateUserMapping.java @@ -0,0 +1,61 @@ +/*- + * #%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.usermapping; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + +public class CreateUserMapping extends ForeignDataStatement { + private String user; + private String server; + private boolean ifNotExists; + + public String getUser() { + return user; + } + + public void setUser(String value) { + user = value; + } + + public String getServer() { + return server; + } + + public void setServer(String value) { + server = value; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean value) { + ifNotExists = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE USER MAPPING "); + if (ifNotExists) { + sql.append("IF NOT EXISTS "); + } + sql.append("FOR ").append(user).append(" SERVER ").append(server); + appendOptionsTo(sql, printer); + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/foreign/ForeignDataStatement.java b/src/main/java/net/sf/jsqlparser/statement/foreign/ForeignDataStatement.java new file mode 100644 index 000000000..b97afa709 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/foreign/ForeignDataStatement.java @@ -0,0 +1,54 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.foreign; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; + +/** Shared structured options for PostgreSQL foreign-data DDL. */ +public abstract class ForeignDataStatement implements Statement { + private List options; + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + protected void appendOptionsTo(StringBuilder sql, Consumer printer) { + if (options != null) { + sql.append(' '); + ForeignDataOption.appendOptionsTo(sql, options, printer); + } + } + + public void visitExpressions(Consumer visitor) { + ForeignDataOption.visitExpressions(options, visitor); + } + + protected static void visit(Expression expression, Consumer visitor) { + if (expression != null) { + visitor.accept(expression); + } + } + + public abstract StringBuilder appendTo(StringBuilder sql, Consumer printer); + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 9d21d8377..b4fff16a8 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -2968,4 +2974,40 @@ public Void visit(CreateStatistics statement, S context) { public Void visit(AlterStatistics statement, S context) { return null; } + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(CreateServer statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterServer statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, 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 d949c3e62..fe29f6a12 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -853,4 +859,40 @@ public StringBuilder visit(CreateStatistics statement, S context) { public StringBuilder visit(AlterStatistics statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(CreateForeignDataWrapper statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterForeignDataWrapper statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateServer statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterServer statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(CreateUserMapping statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterUserMapping statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } } 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 08e137e54..3c74b26c8 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 @@ -102,6 +102,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS */ public static final FeaturesAllowed CREATE = new FeaturesAllowed("CREATE", Feature.createIndex, Feature.createSchema, Feature.createSequence, Feature.createTable, + Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, Feature.createTableIfNotExists, Feature.createTableRowMovement, @@ -113,7 +114,8 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS */ public static final FeaturesAllowed ALTER = new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, - Feature.alterView, Feature.alterIndex, Feature.alterSchema) + Feature.alterView, Feature.alterIndex, Feature.alterSchema, + Feature.alterForeignDataWrapper, Feature.alterServer, Feature.alterUserMapping) .unmodifyable(); /** * all "DROP" {@link Feature}'s 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 e5a9b43f1..37a523e41 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 @@ -97,6 +97,9 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, + Feature.createForeignDataWrapper, Feature.alterForeignDataWrapper, + Feature.createServer, Feature.alterServer, Feature.createUserMapping, + Feature.alterUserMapping, 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 0b8d2765d..7f556ee51 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 @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -1024,4 +1030,46 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + validateFeature(Feature.createForeignDataWrapper); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + validateFeature(Feature.alterForeignDataWrapper); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(CreateServer statement, S context) { + validateFeature(Feature.createServer); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterServer statement, S context) { + validateFeature(Feature.alterServer); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + validateFeature(Feature.createUserMapping); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + validateFeature(Feature.alterUserMapping); + statement.visitExpressions(this::validateOptionalExpression); + 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 4b505dab0..ef0b7fd92 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -49,6 +49,9 @@ import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; +import net.sf.jsqlparser.statement.create.fdw.*; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; @@ -1534,7 +1537,7 @@ public class CCJSqlParser extends AbstractJSqlParser { private boolean isKeywordAhead(String keyword) { Token token = getToken(1); - return token.image != null && keyword.equalsIgnoreCase(token.image); + return token.kind != EOF && token.image != null && keyword.equalsIgnoreCase(token.image); } private boolean isXmlTypeName(String name) { @@ -18441,6 +18444,12 @@ Statement Alter(): ( ( + LOOKAHEAD( ) statement = AlterForeignDataWrapper() + | + LOOKAHEAD({ isKeywordAhead("SERVER") }) statement = AlterServer() + | + LOOKAHEAD({ getToken(1).kind == K_USER && "MAPPING".equalsIgnoreCase(getToken(2).image) }) statement = AlterUserMapping() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | LOOKAHEAD() statement = AlterPolicy() @@ -19650,6 +19659,12 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD( ) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateForeignDataWrapper() + | + LOOKAHEAD({ isKeywordAhead("SERVER") }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateServer() + | + LOOKAHEAD({ getToken(1).kind == K_USER && "MAPPING".equalsIgnoreCase(getToken(2).image) }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateUserMapping() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() @@ -20952,3 +20967,131 @@ AlterStatistics AlterStatistics(): statement.setStatistics(target.intValue()); } ) ) ) { return statement; } } + +CreateForeignDataWrapper CreateForeignDataWrapper(): +{ + CreateForeignDataWrapper statement = new CreateForeignDataWrapper(); + String name; ForeignDataWrapperFunctions functions; List options; +} +{ + ContextualKeyword("WRAPPER") name=RelObjectName() + functions=ForeignDataWrapperFunctions() + { statement.setName(name); statement.setFunctions(functions); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +AlterForeignDataWrapper AlterForeignDataWrapper(): +{ + AlterForeignDataWrapper statement = new AlterForeignDataWrapper(); + String name; ForeignDataWrapperFunctions functions; List options; +} +{ + ContextualKeyword("WRAPPER") name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(AlterForeignDataWrapper.Action.RENAME); statement.setNewName(name); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterForeignDataWrapper.Action.OWNER); statement.setOwner(name); } + | functions=ForeignDataWrapperFunctions() { statement.setFunctions(functions); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(true) { statement.setOptions(options); } ] + { requireDdlSyntax(functions.isHandlerSpecified() || functions.isValidatorSpecified() + || statement.getOptions() != null, "ALTER FOREIGN DATA WRAPPER requires an action"); } + ) + { return statement; } +} + +ForeignDataWrapperFunctions ForeignDataWrapperFunctions(): +{ + ForeignDataWrapperFunctions functions = new ForeignDataWrapperFunctions(); + String name; boolean handler; +} +{ + ( LOOKAHEAD({ isKeywordAhead("HANDLER") || isKeywordAhead("VALIDATOR") || getToken(1).kind == K_NO }) + { name = null; } + ( + + ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; } + | ContextualKeyword("VALIDATOR") { handler = false; } ) + | + ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; } + | ContextualKeyword("VALIDATOR") { handler = false; } ) + name=AccessQualifiedName() + ) + { + if (handler) { + requireDdlSyntax(!functions.isHandlerSpecified(), "Duplicate HANDLER clause"); functions.setHandler(name); + } else { + requireDdlSyntax(!functions.isValidatorSpecified(), "Duplicate VALIDATOR clause"); functions.setValidator(name); + } + } + )* + { return functions; } +} + +CreateServer CreateServer(): +{ + CreateServer statement = new CreateServer(); String name; Token type; + Expression version; List options; +} +{ + ContextualKeyword("SERVER") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=RelObjectName() { statement.setName(name); } + [ type= { statement.setType(new StringValue(type.image)); } ] + [ version=ForeignServerVersion() { statement.setVersion(version); } ] + ContextualKeyword("WRAPPER") name=RelObjectName() { statement.setForeignDataWrapper(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +Expression ForeignServerVersion(): +{ Token value; Expression version; } +{ + ( value= { version = new StringValue(value.image); } + | { version = new NullValue(); } ) + { return version; } +} + +AlterServer AlterServer(): +{ + AlterServer statement = new AlterServer(); String name; + Expression version; List options; +} +{ + ContextualKeyword("SERVER") name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(AlterServer.Action.RENAME); statement.setNewName(name); } + | LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterServer.Action.OWNER); statement.setOwner(name); } + | [ version=ForeignServerVersion() { statement.setVersion(version); } ] + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(true) { statement.setOptions(options); } ] + { requireDdlSyntax(statement.getVersion() != null || statement.getOptions() != null, + "ALTER SERVER requires VERSION or OPTIONS"); } + ) + { return statement; } +} + +CreateUserMapping CreateUserMapping(): +{ + CreateUserMapping statement = new CreateUserMapping(); String name; List options; +} +{ + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { statement.setOptions(options); } ] + { return statement; } +} + +AlterUserMapping AlterUserMapping(): +{ + AlterUserMapping statement = new AlterUserMapping(); String name; List options; +} +{ + ContextualKeyword("MAPPING") name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + options=ForeignDataOptions(true) { statement.setOptions(options); return statement; } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java new file mode 100644 index 000000000..772ff17da --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java @@ -0,0 +1,158 @@ +/*- + * #%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 org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlForeignDataObjectsTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN DATA WRAPPER w", + "CREATE FOREIGN DATA WRAPPER w NO HANDLER NO VALIDATOR OPTIONS(x 'y')", + "CREATE FOREIGN DATA WRAPPER w HANDLER public.handler VALIDATOR public.validator", + "CREATE FOREIGN DATA WRAPPER w VALIDATOR public.validator HANDLER public.handler", + "ALTER FOREIGN DATA WRAPPER w OPTIONS(ADD x 'y')", + "ALTER FOREIGN DATA WRAPPER w NO HANDLER NO VALIDATOR", + "ALTER FOREIGN DATA WRAPPER w HANDLER public.handler OPTIONS(SET x 'z',DROP y)", + "ALTER FOREIGN DATA WRAPPER w OWNER TO CURRENT_USER", + "ALTER FOREIGN DATA WRAPPER w RENAME TO w2", + "CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS(host 'localhost')", + "CREATE SERVER IF NOT EXISTS s TYPE 'postgres' VERSION '18' FOREIGN DATA WRAPPER w", + "CREATE SERVER s VERSION NULL FOREIGN DATA WRAPPER w", + "ALTER SERVER s OPTIONS(SET host '127.0.0.1')", "ALTER SERVER s VERSION NULL", + "ALTER SERVER s VERSION '18' OPTIONS(ADD port '5432',DROP host)", + "ALTER SERVER s OWNER TO CURRENT_ROLE", "ALTER SERVER s RENAME TO s2", + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(user 'u')", + "CREATE USER MAPPING IF NOT EXISTS FOR PUBLIC SERVER s", + "CREATE USER MAPPING FOR USER SERVER s OPTIONS(user 'u',password 'a,b')", + "CREATE USER MAPPING FOR CURRENT_ROLE SERVER s", + "CREATE USER MAPPING FOR \"user name\" SERVER \"server name\"", + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(SET \"user\" 'u2')", + "ALTER USER MAPPING FOR PUBLIC SERVER s OPTIONS(DROP password,ADD user 'u')"}) + void foreignObjectsShareOptionsAndKeepBoundaries(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertInstanceOf(ForeignDataStatement.class, statement); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + } + + @Test + void omittedAndRemovedHandlersStayDistinct() throws JSQLParserException { + CreateForeignDataWrapper create = (CreateForeignDataWrapper) CCJSqlParserUtil.parse( + "CREATE FOREIGN DATA WRAPPER w"); + assertFalse(create.getFunctions().isHandlerSpecified()); + create.getFunctions().setHandler(null); + assertTrue(create.getFunctions().isHandlerSpecified()); + assertEquals("CREATE FOREIGN DATA WRAPPER w NO HANDLER", create.toString()); + create.getFunctions().setHandler("public.handler"); + roundTrip(create); + create.getFunctions().clearHandler(); + assertEquals("CREATE FOREIGN DATA WRAPPER w", create.toString()); + AlterForeignDataWrapper alter = (AlterForeignDataWrapper) CCJSqlParserUtil.parse( + "ALTER FOREIGN DATA WRAPPER w OPTIONS(SET x 'y')"); + alter.getOptions().get(0).setValue(new StringValue("changed")); + assertTrue(alter.toString().contains("SET x 'changed'")); + roundTrip(alter); + } + + @Test + void serverLiteralsAndOptionsAreVisitedAndMutable() throws JSQLParserException { + CreateServer create = (CreateServer) CCJSqlParserUtil.parse( + "CREATE SERVER s TYPE 'pg' VERSION '18' FOREIGN DATA WRAPPER w OPTIONS(host 'local')"); + List visited = new ArrayList<>(); + create.visitExpressions(visited::add); + assertEquals(3, visited.size()); + create.setVersion(new NullValue()); + create.setForeignDataWrapper("other_wrapper"); + create.getOptions().get(0).setValue(new StringValue("remote")); + assertTrue(create.toString().contains("VERSION NULL FOREIGN DATA WRAPPER other_wrapper")); + roundTrip(create); + AlterServer alter = (AlterServer) CCJSqlParserUtil.parse("ALTER SERVER s VERSION NULL"); + assertInstanceOf(NullValue.class, alter.getVersion()); + alter.setAction(AlterServer.Action.RENAME); + alter.setNewName("new_server"); + visited.clear(); + alter.visitExpressions(visited::add); + assertTrue(visited.isEmpty()); + assertEquals("ALTER SERVER s RENAME TO new_server", alter.toString()); + } + + @Test + void userMappingOptionsUseExistingForeignDataNodes() throws JSQLParserException { + CreateUserMapping create = (CreateUserMapping) CCJSqlParserUtil.parse( + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(user 'u')"); + assertNull(create.getOptions().get(0).getAction()); + AlterUserMapping alter = (AlterUserMapping) CCJSqlParserUtil.parse( + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS(SET user 'u2')"); + assertEquals(ForeignDataOption.Action.SET, alter.getOptions().get(0).getAction()); + alter.getOptions().get(0).setAction(ForeignDataOption.Action.DROP); + assertNull(alter.getOptions().get(0).getValue()); + List expressions = new ArrayList<>(); + alter.visitExpressions(expressions::add); + assertTrue(expressions.isEmpty()); + roundTrip(alter); + assertEquals("s", create.accept(new StatementVisitorAdapter() { + @Override + public String visit(CreateUserMapping statement, S context) { + return statement.getServer(); + } + }, null)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE OR REPLACE SERVER s FOREIGN DATA WRAPPER w", + "CREATE OR REPLACE FOREIGN DATA WRAPPER w", "CREATE SERVER s OPTIONS(x 'y')", + "CREATE FOREIGN DATA WRAPPER w HANDLER h HANDLER h2", + "CREATE FOREIGN DATA WRAPPER w NO VALIDATOR VALIDATOR v", + "ALTER FOREIGN DATA WRAPPER w", "ALTER SERVER s", + "ALTER USER MAPPING FOR USER SERVER s", + "CREATE USER MAPPING FOR USER SERVER s OPTIONS(SET x 'y')", + "CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS(DROP x)", + "ALTER SERVER s OPTIONS(SET x)", "ALTER SERVER s OPTIONS(DROP x 'y')", + "ALTER SERVER s OPTIONS()", "ALTER SERVER s OPTIONS(host = 'localhost')", + "CREATE SERVER s TYPE NULL FOREIGN DATA WRAPPER w", "ALTER SERVER s VERSION 18", + "ALTER SERVER s RENAME TO s2 OPTIONS(x 'y')"}) + void rejectsWrongObjectAndOptionForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +}