diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/Alter.java b/src/main/java/net/sf/jsqlparser/statement/alter/Alter.java index 7c00077e2..ebb1efafe 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/Alter.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/Alter.java @@ -21,13 +21,46 @@ import net.sf.jsqlparser.statement.StatementVisitor; public class Alter implements Statement { - private Table table; private boolean useOnly = false; - private boolean useTableIfExists = false; - private List alterExpressions; + private boolean useDescendants; + private boolean foreignTable; + + + + public boolean isUseDescendants() { + return useDescendants; + } + + public void setUseDescendants(boolean useDescendants) { + this.useDescendants = useDescendants; + } + + public StringBuilder appendTargetTo(StringBuilder builder) { + builder.append(foreignTable ? "ALTER FOREIGN TABLE " : "ALTER TABLE "); + if (useTableIfExists) { + builder.append("IF EXISTS "); + } + if (useOnly) { + builder.append("ONLY "); + } + builder.append(table.getFullyQualifiedName()); + if (useDescendants) { + builder.append(" *"); + } + return builder.append(' '); + } + + + public boolean isForeignTable() { + return foreignTable; + } + + public void setForeignTable(boolean foreignTable) { + this.foreignTable = foreignTable; + } public Table getTable() { return table; @@ -82,16 +115,7 @@ public T accept(StatementVisitor statementVisitor, S context) { public String toString() { StringBuilder b = new StringBuilder(); - b.append("ALTER TABLE "); - if (useOnly) { - b.append("ONLY "); - } - - if (useTableIfExists) { - b.append("IF EXISTS "); - } - - b.append(table.getFullyQualifiedName()).append(" "); + appendTargetTo(b); Iterator altIter = alterExpressions.iterator(); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java new file mode 100644 index 000000000..dd90f6e2d --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java @@ -0,0 +1,46 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; + +/** Foreign table OPTIONS, optionally applied to one column. */ +public class AlterForeignDataOptions extends AlterExpression { + private List options; + + public AlterForeignDataOptions() { + setOperation(AlterOperation.FOREIGN_OPTIONS); + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (getColumnName() != null) { + builder.append(hasColumn() ? "ALTER COLUMN " : "ALTER ").append(getColumnName()) + .append(' '); + } + ForeignDataOption.appendOptionsTo(builder, options, expressionPrinter); + return builder; + } + + @Override + protected void appendBody(StringBuilder builder) { + appendTo(builder, builder::append); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index 1e7edf0e0..7d0fb9431 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ALTER_RELATION, ORDER_BY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ALTER_RELATION, ORDER_BY, FOREIGN_OPTIONS; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java index c00dd7f63..ef2d0db89 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java @@ -23,7 +23,7 @@ public class ColumnOption implements Serializable { public enum Kind { - SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, OTHER + SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, FOREIGN_OPTIONS, OTHER } /** PostgreSQL storage strategies and MySQL column storage locations. */ @@ -31,6 +31,24 @@ public enum Storage { PLAIN, EXTERNAL, EXTENDED, MAIN, DEFAULT, DISK, MEMORY } + private List foreignOptions; + + public List getForeignOptions() { + return foreignOptions; + } + + public void setForeignOptions(List options) { + foreignOptions = options; + kind = Kind.FOREIGN_OPTIONS; + tokens = null; + } + + public static ColumnOption foreignOptions(List options) { + ColumnOption option = new ColumnOption(); + option.setForeignOptions(options); + return option; + } + private Storage storage; private String compression; @@ -175,7 +193,9 @@ public void setGeneratedDefinition(GeneratedColumnDefinition definition) { /** Visits expressions of the selected option kind, including comment literals. */ public void visitExpressions(Consumer visitor) { - if (kind == Kind.DEFAULT) { + if (kind == Kind.FOREIGN_OPTIONS) { + ForeignDataOption.visitExpressions(foreignOptions, visitor); + } else if (kind == Kind.DEFAULT) { visitor.accept(defaultExpression); } else if (kind == Kind.COMMENT) { visitor.accept(comment); @@ -294,6 +314,9 @@ public String toString() { /** Appends the option using the supplied printer for structured expressions. */ public void appendTo(StringBuilder builder, Consumer expressionPrinter) { switch (kind) { + case FOREIGN_OPTIONS: + ForeignDataOption.appendOptionsTo(builder, foreignOptions, expressionPrinter); + break; case STORAGE: builder.append("STORAGE ").append(storage); break; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index 86d2d9bbb..d6ecdd3e7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -50,6 +50,19 @@ public void setExecute(net.sf.jsqlparser.statement.execute.Execute execute) { private Table table; private boolean unlogged = false; + + /** The structured SERVER clause, also present in the shared table-options list. */ + public ForeignTableOptions getForeignTableOptions() { + if (tableOptions != null) { + for (TableOption option : tableOptions) { + if (option.getForeignTableOptions() != null) { + return option.getForeignTableOptions(); + } + } + } + return null; + } + private List createOptionsStrings; private List tableOptionsStrings; private List tableOptions; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.java new file mode 100644 index 000000000..9bc11c1a1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignDataOption.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.create.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +/** A foreign table or column option; absent action means the implicit ADD form. */ +public class ForeignDataOption implements Serializable { + public enum Action { + ADD, SET, DROP + } + + private Action action; + private String name; + private StringValue value; + + public ForeignDataOption(String name, StringValue value) { + this.name = name; + this.value = value; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + if (action == Action.DROP) { + value = null; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public StringValue getValue() { + return value; + } + + public void setValue(StringValue value) { + this.value = value; + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (action != null) { + builder.append(action).append(' '); + } + builder.append(name); + if (action != Action.DROP && value != null) { + builder.append(' '); + expressionPrinter.accept(value); + } + } + + public static void appendOptionsTo(StringBuilder builder, List options, + Consumer expressionPrinter) { + builder.append("OPTIONS ("); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + options.get(i).appendTo(builder, expressionPrinter); + } + builder.append(')'); + } + + public static void visitExpressions(List options, + Consumer visitor) { + if (options != null) { + for (ForeignDataOption option : options) { + if (option.action != Action.DROP && option.value != null) { + visitor.accept(option.value); + } + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java new file mode 100644 index 000000000..1ff2017d6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignTableOptions.java @@ -0,0 +1,52 @@ +/*- + * #%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.table; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** PostgreSQL foreign server and optional table-level FDW options. */ +public class ForeignTableOptions implements Serializable { + private String server; + private List options; + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("SERVER ").append(server); + if (options != null && !options.isEmpty()) { + builder.append(' '); + ForeignDataOption.appendOptionsTo(builder, options, expressionPrinter); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java index 6f60a4f76..920120e5e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -21,7 +21,19 @@ public class TableOption implements Serializable { public enum Kind { - ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, FOREIGN_SERVER, OTHER + } + + private ForeignTableOptions foreignTableOptions; + + public ForeignTableOptions getForeignTableOptions() { + return foreignTableOptions; + } + + public static TableOption foreignServer(ForeignTableOptions options) { + TableOption option = new TableOption(Kind.FOREIGN_SERVER, "SERVER", null, false); + option.foreignTableOptions = options; + return option; } private Kind kind = Kind.OTHER; @@ -38,6 +50,7 @@ public List getStorageParameters() { public void setStorageParameters(List storageParameters) { this.storageParameters = storageParameters; + foreignTableOptions = null; kind = Kind.STORAGE_PARAMETERS; name = "WITH"; value = null; @@ -47,7 +60,9 @@ public void setStorageParameters(List storageParameters) { public void appendTo(StringBuilder builder, java.util.function.Consumer expressionPrinter) { - if (storageParameters != null) { + if (foreignTableOptions != null) { + foreignTableOptions.appendTo(builder, expressionPrinter); + } else if (storageParameters != null) { builder.append("WITH "); Index.Option.appendListTo(builder, storageParameters, expressionPrinter); } else { @@ -81,6 +96,9 @@ public Kind getKind() { public void setKind(Kind kind) { this.kind = kind; + if (kind != Kind.FOREIGN_SERVER) { + foreignTableOptions = null; + } if (kind != Kind.STORAGE_PARAMETERS) { storageParameters = null; } @@ -98,6 +116,9 @@ public void setName(String name) { } public String getValue() { + if (foreignTableOptions != null) { + return foreignTableOptions.toString().substring("SERVER ".length()); + } if (storageParameters != null) { return PlainSelect.getStringList(storageParameters, true, true); } @@ -106,6 +127,7 @@ public String getValue() { public void setValue(String value) { this.value = value; + foreignTableOptions = null; unionTables = null; storageParameters = null; } @@ -118,6 +140,7 @@ public List getUnionTables() { /** Replaces raw option contents with structured UNION table references. */ public void setUnionTables(List
unionTables) { this.unionTables = unionTables; + foreignTableOptions = null; kind = Kind.UNION; name = "UNION"; value = null; @@ -160,6 +183,7 @@ public List getTokens() { public void setTokens(List tokens) { this.tokens = tokens; if (tokens != null) { + foreignTableOptions = null; unionTables = null; storageParameters = null; } @@ -187,6 +211,9 @@ public TableOption withUseEquals(boolean useEquals) { @Override public String toString() { + if (foreignTableOptions != null) { + return foreignTableOptions.toString(); + } if (tokens != null) { return PlainSelect.getStringList(tokens, false, false); } diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java index d94fc9fb5..c0adf8769 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -26,7 +26,7 @@ public class Drop implements Statement { public enum ObjectType { - DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, OTHER + DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, OTHER } private String type; @@ -128,7 +128,8 @@ public Drop withTable(Table table) { /** Visits table/view targets and explicit index owners without resolving catalog objects. */ public void visitTables(Consumer
visitor) { - if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW) { + if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW + || objectType == ObjectType.FOREIGN_TABLE) { names.forEach(visitor); } else if (objectType == ObjectType.INDEX && table != null) { visitor.accept(table); @@ -149,7 +150,7 @@ public String getType() { public void setType(String string) { type = string; try { - objectType = ObjectType.valueOf(string.toUpperCase(Locale.ROOT)); + objectType = ObjectType.valueOf(string.toUpperCase(Locale.ROOT).replace(' ', '_')); } catch (IllegalArgumentException | NullPointerException ignored) { objectType = ObjectType.OTHER; } @@ -162,7 +163,7 @@ public ObjectType getObjectType() { public void setObjectType(ObjectType objectType) { this.objectType = objectType == null ? ObjectType.OTHER : objectType; if (this.objectType != ObjectType.OTHER) { - this.type = this.objectType.name(); + this.type = this.objectType.name().replace('_', ' '); } } @@ -235,7 +236,8 @@ private void appendNames(StringBuilder builder, Consumer
tablePrinter) { if (i > 0) { builder.append(", "); } - if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW) { + if (objectType == ObjectType.TABLE || objectType == ObjectType.VIEW + || objectType == ObjectType.FOREIGN_TABLE) { tablePrinter.accept(names.get(i)); } else { builder.append(names.get(i)); diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index b5bee5a29..0fccab980 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -22,6 +22,8 @@ import net.sf.jsqlparser.statement.create.table.CheckConstraint; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; @@ -62,6 +64,10 @@ public static void visit(AlterExpression action, Consumer expression if (action.getOperation() == AlterOperation.RENAME_TABLE) { accept(action.getNewTable(), tables); } + if (action instanceof AlterForeignDataOptions) { + ForeignDataOption.visitExpressions(((AlterForeignDataOptions) action).getOptions(), + expressions); + } if (action.getColumnSetDefaultList() != null) { action.getColumnSetDefaultList() .forEach(column -> accept(column.getDefaultExpression(), expressions)); @@ -112,6 +118,10 @@ public static void visit(CreateTable table, Consumer expressions, table.getIndexes().forEach(index -> visit(index, expressions, tables)); } } + if (table.getForeignTableOptions() != null) { + ForeignDataOption.visitExpressions(table.getForeignTableOptions().getOptions(), + expressions); + } if (table.getTableOptions() != null) { table.getTableOptions().forEach(option -> { visitOptions(option.getStorageParameters(), expressions); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index 586a5cb5e..8c62bf20b 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.util.deparser; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; @@ -33,14 +34,7 @@ public AlterDeParser(StringBuilder buffer, ExpressionVisitor expr @Override public void deParse(Alter alter) { - builder.append("ALTER TABLE "); - if (alter.isUseOnly()) { - builder.append("ONLY "); - } - if (alter.isUseTableIfExists()) { - builder.append("IF EXISTS "); - } - builder.append(alter.getTable().getFullyQualifiedName()).append(' '); + alter.appendTargetTo(builder); for (Iterator iterator = alter.getAlterExpressions().iterator(); iterator .hasNext();) { deParseAction(iterator.next()); @@ -51,6 +45,11 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action instanceof AlterForeignDataOptions) { + ((AlterForeignDataOptions) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) { ((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).appendTo(builder, expression -> expression.accept(expressionVisitor, null)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 32fe5266d..4b505dab0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14240,6 +14240,7 @@ ColumnDefinition ColumnDefinition(): { ColumnOption ColumnDefinitionOption(): { Token tk; + List foreignOptions; List parameter; ForeignKeyReference reference; ColumnOption option; @@ -14253,6 +14254,9 @@ ColumnOption ColumnDefinitionOption(): { String compression; } { ( + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && isKeywordAhead("OPTIONS") }) + foreignOptions=ForeignDataOptions(false) { option = ColumnOption.foreignOptions(foreignOptions); } + | LOOKAHEAD({ isKeywordAhead("GENERATED") && (getToken(4).kind == K_IDENTITY || getToken(5).kind == K_IDENTITY) }) identity=IdentityDefinition() { option = ColumnOption.identity(identity); } @@ -15285,6 +15289,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): List
parents = new ArrayList
(); ObjectNames typeName; boolean withData; + boolean foreign = false; + ForeignTableOptions foreignOptions; } { { createTable.setOrReplace(isUsingOrReplace);} @@ -15295,6 +15301,7 @@ CreateTable CreateTable(boolean isUsingOrReplace): (parameter = CreateParameter() { createOptions.addAll(parameter); })* + { for (String opt : createOptions) { if ("FOREIGN".equalsIgnoreCase(opt)) { foreign = true; } } } [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] table=Table() @@ -15337,6 +15344,9 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ LOOKAHEAD({ partitionOfTable != null }) partitionBound=PostgreSqlPartitionBound() { createTable.setPartitionBound(partitionBound); } ] + [ LOOKAHEAD({ isKeywordAhead("SERVER") }) foreignOptions=ForeignTableOptions() + { requireDdlSyntax(foreign, "SERVER requires CREATE FOREIGN TABLE"); + typedTableOptions.add(TableOption.foreignServer(foreignOptions)); } ] CreateTableOptions(typedTableOptions) [ LOOKAHEAD( ) partitioning=CreateTablePartitioning() { createTable.setPartitioning(partitioning); } ] @@ -15389,10 +15399,60 @@ CreateTable CreateTable(boolean isUsingOrReplace): createTable.setColumns(columns); } } + requireDdlSyntax(!foreign + || createTable.getForeignTableOptions() != null, "CREATE FOREIGN TABLE requires SERVER"); return createTable; } } +ForeignTableOptions ForeignTableOptions(): +{ + ForeignTableOptions result = new ForeignTableOptions(); + String name; + List options; +} +{ + ContextualKeyword("SERVER") name=RelObjectName() { result.setServer(name); } + [ LOOKAHEAD({ isKeywordAhead("OPTIONS") }) options=ForeignDataOptions(false) { result.setOptions(options); } ] + { return result; } +} + +List ForeignDataOptions(boolean alter): +{ + List options = new ArrayList(); + ForeignDataOption option; +} +{ + ContextualKeyword("OPTIONS") "(" option=ForeignDataOption() { options.add(option); } + ( "," option=ForeignDataOption() { options.add(option); } )* ")" + { + for (ForeignDataOption item : options) { + requireDdlSyntax(alter || item.getAction() == null, "CREATE options do not allow ADD, SET or DROP"); + } + return options; + } +} + +ForeignDataOption ForeignDataOption(): +{ + ForeignDataOption.Action action = null; + String name; + Token value; + ForeignDataOption option; +} +{ + ( + LOOKAHEAD() name=RelObjectName() + { option = new ForeignDataOption(name, null); option.setAction(ForeignDataOption.Action.DROP); } + | + [ LOOKAHEAD(1) ( { action = ForeignDataOption.Action.ADD; } + | { action = ForeignDataOption.Action.SET; } ) ] + name=RelObjectName() value= + { option = new ForeignDataOption(name, new StringValue(value.image)); option.setAction(action); } + ) + { return option; } +} + TableElement CreateTableElement(boolean typed): { TableElement element; } { @@ -16679,11 +16739,14 @@ Drop Drop(): List funcArgs = null; List indexOption; boolean useTemporary = false; + boolean foreign = false; } { [ { drop.setMaterialized(true);} ] ( + tk= { foreign = true; } + | tk= | ( @@ -16706,7 +16769,7 @@ Drop Drop(): | tk= ) - { drop.setType(tk.image); } + { drop.setType(foreign ? "FOREIGN TABLE" : tk.image); } [ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.INDEX && getToken(1).kind == K_CONCURRENTLY }) { drop.setConcurrently(true); } ] @@ -18172,6 +18235,10 @@ AlterExpression AlterExpression(): { ( + LOOKAHEAD({ isKeywordAhead("OPTIONS") || getToken(1).kind == K_ALTER + && "OPTIONS".equalsIgnoreCase(getToken(getToken(2).kind == K_COLUMN ? 4 : 3).image) }) + alterExp=AlterForeignDataOptions() + | LOOKAHEAD( ) alterExp=MySqlAlterOrderBy() | LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") }) @@ -18404,7 +18471,9 @@ Statement Alter(): LOOKAHEAD(( | | ) ) statement = PostgreSqlTablespaceMove() | - statement = AlterTable() + statement = AlterForeignTable() + | + statement = AlterTable(false) | statement = AlterSession() | @@ -18646,7 +18715,26 @@ RelationAlterAction PostgreSqlRelationAction(): { return action; } } -Alter AlterTable(): +AlterForeignDataOptions AlterForeignDataOptions(): +{ + AlterForeignDataOptions action = new AlterForeignDataOptions(); + String name; + List options; +} +{ + [ [ LOOKAHEAD(1) { action.hasColumn(true); } ] + name=RelObjectName() { action.setColumnName(name); } ] + options=ForeignDataOptions(true) { action.setOptions(options); } + { return action; } +} + +Alter AlterForeignTable(): +{ Alter alter; } +{ + alter=AlterTable(true) { alter.setForeignTable(true); return alter; } +} + +Alter AlterTable(boolean foreign): { Alter alter = new Alter(); Table table; @@ -18654,16 +18742,27 @@ Alter AlterTable(): boolean usingIfExists = false; } { - - [ { alter.setUseOnly(true); } ] + { alter.setForeignTable(foreign); } [ LOOKAHEAD(2) { alter.setUseTableIfExists(true); } ] + [ { alter.setUseOnly(true); } ] + [ LOOKAHEAD(2) { + requireDdlSyntax(!alter.isUseTableIfExists(), "Duplicate IF EXISTS"); + alter.setUseTableIfExists(true); + } ] table=Table() { alter.setTable(table); } + [ "*" { + requireDdlSyntax(!alter.isUseOnly(), "ONLY cannot be combined with descendant marker"); + alter.setUseDescendants(true); + } ] alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } ("," alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } )* { + for (AlterExpression action : alter.getAlterExpressions()) { + requireDdlSyntax(foreign || !(action instanceof AlterForeignDataOptions), "OPTIONS requires ALTER FOREIGN TABLE"); + } return alter; } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java new file mode 100644 index 000000000..32e39be1a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignTableTest.java @@ -0,0 +1,155 @@ +/*- + * #%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 java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterForeignDataOptions; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignDataOption; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlForeignTableTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN TABLE ft(id INT,label TEXT) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name 'remote_id'),label TEXT) SERVER ddl_audit_server OPTIONS(schema_name 'remote',table_name 'remote_t')", + "CREATE FOREIGN TABLE ft(id INT CONSTRAINT nn NOT NULL NO INHERIT,label TEXT) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(id INT,label TEXT,CONSTRAINT ck CHECK(id>0)) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft(LIKE t INCLUDING ALL) SERVER ddl_audit_server", + "CREATE FOREIGN TABLE ft PARTITION OF t FOR VALUES FROM(0) TO(100) SERVER ddl_audit_server", + "ALTER FOREIGN TABLE ft ADD COLUMN extra INT DEFAULT 1", + "ALTER FOREIGN TABLE ft DROP COLUMN IF EXISTS label", + "ALTER FOREIGN TABLE ft ALTER COLUMN id TYPE BIGINT", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET NOT NULL", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET STATISTICS 200", + "ALTER FOREIGN TABLE ft ALTER COLUMN label SET STORAGE MAIN", + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS(SET column_name 'new_id')", + "ALTER FOREIGN TABLE ft OPTIONS(SET table_name 'renamed', ADD schema_name 'public')", + "ALTER FOREIGN TABLE ft RENAME COLUMN id TO new_id", + "ALTER FOREIGN TABLE ft RENAME TO ft2", + "ALTER FOREIGN TABLE ft SET SCHEMA ddl_aux", + "ALTER FOREIGN TABLE ft ADD CONSTRAINT ck CHECK(id>0) NOT VALID", + "ALTER FOREIGN TABLE ft VALIDATE CONSTRAINT ck", + "DROP FOREIGN TABLE ft", + "DROP FOREIGN TABLE IF EXISTS ft CASCADE", + "ALTER FOREIGN TABLE IF EXISTS ONLY ft ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft * ALTER COLUMN id SET DEFAULT 2", + "ALTER FOREIGN TABLE ft OPTIONS(DROP table_name)", + "ALTER FOREIGN TABLE ft ALTER id OPTIONS(column_name 'remote')", + "ALTER FOREIGN TABLE ft ADD COLUMN id2 INT OPTIONS(column_name 'remote2')", + "CREATE FOREIGN TABLE ft() SERVER srv", + "DROP FOREIGN TABLE ft, public.ft2 RESTRICT"}) + void foreignDefinitionsAndActionsRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + if (statement instanceof CreateTable) { + assertNotNull(((CreateTable) statement).getForeignTableOptions()); + } else if (statement instanceof Alter) { + assertTrue(((Alter) statement).isForeignTable()); + } else { + assertEquals(Drop.ObjectType.FOREIGN_TABLE, ((Drop) statement).getObjectType()); + } + } + + @Test + void serverAndColumnOptionsAreMutableAndVisited() throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name 'remote')) SERVER srv OPTIONS(table_name 'source')"); + table.getForeignTableOptions().setServer("new_srv"); + table.getForeignTableOptions().getOptions().get(0).setValue(new StringValue("target")); + table.getColumnDefinitions().get(0).getColumnOptions().get(0).getForeignOptions() + .get(0).setValue(new StringValue("remote_id")); + assertEquals( + "CREATE FOREIGN TABLE ft (id INT OPTIONS (column_name 'remote_id')) SERVER new_srv OPTIONS (table_name 'target')", + table.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(table, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertEquals(Set.of("ft"), new TablesNamesFinder().getTables(table)); + assertRoundTrip(table); + } + + @Test + void replacingLegacyOptionsRemovesTheOldServer() throws JSQLParserException { + CreateTable table = (CreateTable) parse( + "CREATE FOREIGN TABLE ft(id INT) SERVER old_srv OPTIONS(table_name 'old')"); + assertTrue(table.getTableOptionsStrings().contains("SERVER")); + table.setTableOptionsStrings(List.of("SERVER", "new_srv")); + assertNull(table.getForeignTableOptions()); + assertEquals("CREATE FOREIGN TABLE ft (id INT) SERVER new_srv", table.toString()); + assertRoundTrip(table); + } + + @Test + void optionActionChangesDropStaleValues() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS(SET column_name 'old')"); + AlterForeignDataOptions action = + (AlterForeignDataOptions) alter.getAlterExpressions().get(0); + assertEquals("id", action.getColumnName()); + ForeignDataOption option = action.getOptions().get(0); + option.setAction(ForeignDataOption.Action.DROP); + assertNull(option.getValue()); + assertEquals("ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS (DROP column_name)", + alter.toString()); + assertRoundTrip(alter); + option.setAction(ForeignDataOption.Action.ADD); + option.setValue(new StringValue("new")); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(action, visited::add, ignored -> { + }); + assertEquals(1, visited.size()); + assertRoundTrip(alter); + Drop drop = (Drop) parse("DROP FOREIGN TABLE ft, other.ft2 CASCADE"); + assertEquals(Set.of("ft", "other.ft2"), new TablesNamesFinder().getTables(drop)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE FOREIGN TABLE ft(id INT)", + "CREATE FOREIGN TABLE ft(id INT) SERVER srv OPTIONS(ADD name 'value')", + "CREATE FOREIGN TABLE ft(id INT OPTIONS(column_name=1)) SERVER srv", + "ALTER FOREIGN TABLE ft OPTIONS(SET name)", + "ALTER FOREIGN TABLE ft OPTIONS(DROP name 'value')", + "ALTER FOREIGN TABLE ft OPTIONS()", "ALTER TABLE t OPTIONS(name 'value')", + "ALTER FOREIGN TABLE ONLY ft * ADD COLUMN extra INT"}) + void invalidForeignOptionsFail(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +}