diff --git a/README.md b/README.md index d39e81ea6..4b50c4f07 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` | | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` | +| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | | **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 0d3d080fb..f140d6e3d 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -451,6 +451,19 @@ public enum Feature { */ alterIndex, + /** + * MySQL and PostgreSQL "CREATE TABLESPACE" statement is allowed + * + * @see net.sf.jsqlparser.statement.create.tablespace.CreateTablespace + */ + createTablespace, + /** + * MySQL and PostgreSQL "ALTER TABLESPACE" statement is allowed + * + * @see net.sf.jsqlparser.statement.alter.AlterTablespace + */ + alterTablespace, + /** * SQL "ANALYZE" 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 91b272124..ddaf5f12e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -37,6 +37,7 @@ import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterSubscription; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; import net.sf.jsqlparser.statement.alter.AlterType; @@ -67,6 +68,7 @@ import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.type.CreateType; @@ -1201,6 +1203,16 @@ public Void visit(AlterTablespaceMove statement, S context) { return schemaOnly(); } + @Override + public Void visit(CreateTablespace statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTablespace statement, S context) { + return schemaOnly(); + } + @Override public Void visit(AlterTextSearchConfiguration 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 bf0bf3e8b..3765e284d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -28,6 +28,7 @@ import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; @@ -66,6 +67,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -740,6 +742,22 @@ default void visit(AlterTablespaceMove statement) { visit(statement, null); } + default T visit(CreateTablespace statement, S context) { + return null; + } + + default void visit(CreateTablespace statement) { + visit(statement, null); + } + + default T visit(AlterTablespace statement, S context) { + return null; + } + + default void visit(AlterTablespace statement) { + visit(statement, null); + } + default T visit(AlterPolicy statement, S context) { return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 8a80763ca..ac9b47ad5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -68,6 +69,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -937,6 +939,18 @@ public T visit(AlterRelation statement, S context) { return null; } + @Override + public T visit(CreateTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + @Override public T visit(AlterPolicy statement, S context) { fromItemVisitor.visitFromItem(statement.getTable(), context); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespace.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespace.java new file mode 100644 index 000000000..13a0b2be2 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespace.java @@ -0,0 +1,216 @@ +/*- + * #%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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** + * A MySQL {@code ALTER [UNDO] TABLESPACE} or PostgreSQL {@code ALTER TABLESPACE} statement. The + * {@link Action} selects which properties are rendered. Moving all relations of a tablespace with + * {@code ALTER TABLE ALL IN TABLESPACE} remains {@link AlterTablespaceMove}. + */ +public class AlterTablespace implements Statement { + + public enum Action { + /** {@code RENAME TO newName}. */ + RENAME, + /** PostgreSQL {@code OWNER TO owner}. */ + OWNER, + /** PostgreSQL {@code SET (parameter = value, ...)}. */ + SET_PARAMETERS, + /** PostgreSQL {@code RESET (parameter, ...)}. */ + RESET_PARAMETERS, + /** MySQL undo tablespace {@code SET ACTIVE} or {@code SET INACTIVE}, then any options. */ + SET_STATE, + /** MySQL options such as {@code ENGINE} or {@code ENCRYPTION}. */ + OPTIONS + } + + /** The state of a MySQL undo tablespace. */ + public enum State { + ACTIVE, INACTIVE + } + + private String name; + private boolean undo; + private Action action; + private String newName; + private String owner; + private List parameters; + private List resetParameters; + private State state; + private final List options = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** Whether this is a MySQL {@code ALTER UNDO TABLESPACE} statement. */ + public boolean isUndo() { + return undo; + } + + public void setUndo(boolean undo) { + this.undo = undo; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + /** Returns the new name of a {@link Action#RENAME}. */ + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + /** Returns the new owner of an {@link Action#OWNER}. */ + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + /** Returns the PostgreSQL parameters of a {@link Action#SET_PARAMETERS}. */ + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } + + /** Returns the PostgreSQL parameter names of a {@link Action#RESET_PARAMETERS}. */ + public List getResetParameters() { + return resetParameters; + } + + public void setResetParameters(List resetParameters) { + this.resetParameters = resetParameters; + } + + /** Returns the MySQL undo tablespace state of a {@link Action#SET_STATE}. */ + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + /** + * Returns the mutable MySQL options of a {@link Action#SET_STATE} or {@link Action#OPTIONS} in + * source order; commas between them are not kept. + */ + public List getOptions() { + return options; + } + + public void setOptions(Collection options) { + this.options.clear(); + if (options != null) { + this.options.addAll(options); + } + } + + public AlterTablespace addOptions(TableOption... options) { + Collections.addAll(this.options, options); + return this; + } + + /** Returns the first MySQL option of the given kind. */ + public Optional getOption(TableOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + if (action == null) { + throw new IllegalStateException("ALTER TABLESPACE requires an action"); + } + builder.append(undo ? "ALTER UNDO TABLESPACE " : "ALTER TABLESPACE ").append(name); + switch (action) { + case RENAME: + builder.append(" RENAME TO ").append(newName); + break; + case OWNER: + builder.append(" OWNER TO ").append(owner); + break; + case SET_PARAMETERS: + builder.append(" SET "); + IndexOption.appendListTo(builder, parameters, expressionPrinter); + break; + case RESET_PARAMETERS: + builder.append(" RESET ") + .append(PlainSelect.getStringList(resetParameters, true, true)); + break; + case SET_STATE: + builder.append(" SET ").append(state); + appendOptionsTo(builder, expressionPrinter); + break; + case OPTIONS: + appendOptionsTo(builder, expressionPrinter); + break; + default: + throw new IllegalStateException("Unknown tablespace action: " + action); + } + return builder; + } + + private void appendOptionsTo(StringBuilder builder, Consumer expressionPrinter) { + for (TableOption option : options) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + } + + /** Visits the values of PostgreSQL {@code SET} parameters. */ + public void visitExpressions(Consumer visitor) { + if (action == Action.SET_PARAMETERS && parameters != null) { + parameters.stream().map(IndexOption::getValue).filter(Objects::nonNull) + .forEach(visitor); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).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/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java index a25474069..75b710291 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 @@ -20,7 +20,8 @@ /** * A shared CREATE/ALTER TABLE option with either opaque tokens or structured properties. Setting * non-null tokens discards all structured properties. A structured setter discards the tokens; - * their contents are never parsed or restored by a later setter. + * their contents are never parsed or restored by a later setter. MySQL tablespace statements reuse + * this model for their options; {@link Kind#FILE_BLOCK_SIZE} occurs only there. */ public class TableOption implements Serializable { @@ -35,7 +36,7 @@ public class TableOption implements Serializable { private ColumnOption.Storage tablespaceStorage; 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, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, 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, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, FILE_BLOCK_SIZE, OTHER } public ForeignTableOptions getForeignTableOptions() { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java b/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java new file mode 100644 index 000000000..a743055a6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/tablespace/CreateTablespace.java @@ -0,0 +1,159 @@ +/*- + * #%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.tablespace; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; + +/** + * A MySQL {@code CREATE [UNDO] TABLESPACE} or PostgreSQL {@code CREATE TABLESPACE} statement. The + * parser fills either the PostgreSQL owner, location and parameters, or the MySQL data file and + * options. MySQL options reuse the {@link TableOption} models of the same name, plus + * {@link TableOption.Kind#FILE_BLOCK_SIZE}. + */ +public class CreateTablespace implements Statement { + private String name; + private boolean undo; + private String owner; + private StringValue location; + private List parameters; + private StringValue dataFile; + private final List options = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** Whether this is a MySQL {@code CREATE UNDO TABLESPACE} statement. */ + public boolean isUndo() { + return undo; + } + + public void setUndo(boolean undo) { + this.undo = undo; + } + + /** Returns the PostgreSQL {@code OWNER} role, or null when the clause is omitted. */ + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + /** Returns the PostgreSQL {@code LOCATION} directory. */ + public StringValue getLocation() { + return location; + } + + public void setLocation(StringValue location) { + this.location = location; + } + + /** + * Returns the PostgreSQL {@code WITH (...)} tablespace parameters, or null when the clause is + * omitted. + */ + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } + + /** Returns the MySQL {@code ADD DATAFILE} file name, or null when the clause is omitted. */ + public StringValue getDataFile() { + return dataFile; + } + + public void setDataFile(StringValue dataFile) { + this.dataFile = dataFile; + } + + /** Returns the mutable MySQL options in source order; commas between them are not kept. */ + public List getOptions() { + return options; + } + + public void setOptions(Collection options) { + this.options.clear(); + if (options != null) { + this.options.addAll(options); + } + } + + public CreateTablespace addOptions(TableOption... options) { + Collections.addAll(this.options, options); + return this; + } + + /** Returns the first MySQL option of the given kind. */ + public Optional getOption(TableOption.Kind kind) { + return options.stream().filter(option -> option.getKind() == kind).findFirst(); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append(undo ? "CREATE UNDO TABLESPACE " : "CREATE TABLESPACE ").append(name); + if (owner != null) { + builder.append(" OWNER ").append(owner); + } + if (location != null) { + builder.append(" LOCATION ").append(location); + } + if (parameters != null) { + builder.append(" WITH "); + IndexOption.appendListTo(builder, parameters, expressionPrinter); + } + if (dataFile != null) { + builder.append(" ADD DATAFILE ").append(dataFile); + } + for (TableOption option : options) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + return builder; + } + + /** Visits the values of the PostgreSQL tablespace parameters. */ + public void visitExpressions(Consumer visitor) { + if (parameters != null) { + parameters.stream().map(IndexOption::getValue).filter(Objects::nonNull) + .forEach(visitor); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).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 aeecdb89d..8d952360b 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -123,6 +123,7 @@ import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterSubscription; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; import net.sf.jsqlparser.statement.alter.AlterType; @@ -154,6 +155,7 @@ import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.type.CreateType; @@ -3033,6 +3035,18 @@ public Void visit(AlterTablespaceMove statement, S context) { return null; } + @Override + public Void visit(CreateTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterTablespace statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + @Override public Void visit(AlterPolicy statement, S context) { visit(statement.getTable(), context); 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 2490baa3e..4e282f2a2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -28,6 +28,7 @@ import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; @@ -94,6 +95,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -846,6 +848,18 @@ public StringBuilder visit(AlterTablespaceMove statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(CreateTablespace statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterTablespace statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + @Override public StringBuilder visit(AlterPolicy 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 6f45efefc..328934cfe 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 @@ -107,6 +107,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.createSchema, Feature.createSequence, Feature.createTable, Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, Feature.createAccessMethod, Feature.createAccessMethodTable, + Feature.createTablespace, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, Feature.createTableIfNotExists, Feature.createTableRowMovement, @@ -120,6 +121,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, Feature.alterTextSearchConfiguration, Feature.alterCollation, + Feature.alterTablespace, Feature.alterView, Feature.alterIndex, Feature.alterSchema, Feature.alterForeignDataWrapper, Feature.alterServer, Feature.alterUserMapping) .unmodifyable(); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java index 6aabf1fe0..699c49745 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java @@ -93,6 +93,10 @@ public enum MySqlVersion implements Version { Feature.alterTable, // https://dev.mysql.com/doc/refman/8.0/en/alter-view.html Feature.alterView, + // https://dev.mysql.com/doc/refman/8.0/en/create-tablespace.html + Feature.createTablespace, // https://dev.mysql.com/doc/refman/8.0/en/alter-tablespace.html + Feature.alterTablespace, + // https://dev.mysql.com/doc/refman/8.0/en/create-database.html Feature.createSchema, // https://dev.mysql.com/doc/refman/8.0/en/create-view.html Feature.createView, 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 023a8c0e1..d06d14167 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 @@ -99,6 +99,7 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, Feature.createAccessMethod, + Feature.createTablespace, Feature.alterTablespace, Feature.createForeignDataWrapper, Feature.alterForeignDataWrapper, Feature.createServer, Feature.alterServer, Feature.createUserMapping, Feature.alterUserMapping, 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 1de8380a7..1682b7f97 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -95,6 +96,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -1200,6 +1202,20 @@ public Void visit(AlterRelation statement, S context) { return null; } + @Override + public Void visit(CreateTablespace statement, S context) { + validateFeature(Feature.createTablespace); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterTablespace statement, S context) { + validateFeature(Feature.alterTablespace); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + @Override public Void visit(AlterPolicy statement, S context) { validateOptionalFromItem(statement.getTable()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d..54cfd578d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -78,6 +78,7 @@ import net.sf.jsqlparser.statement.create.schema.*; import net.sf.jsqlparser.statement.create.synonym.*; import net.sf.jsqlparser.statement.create.sequence.*; import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; import net.sf.jsqlparser.statement.create.trigger.*; import net.sf.jsqlparser.statement.create.user.*; import net.sf.jsqlparser.statement.create.view.*; @@ -2460,6 +2461,28 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** Whether a MySQL tablespace option, which reuses a table option model, starts here. */ + private boolean isMySqlTablespaceOptionAhead(int offset) { + Token token = getToken(offset); + if (token.kind == K_ENGINE || token.kind == K_ENCRYPTION) { + return true; + } + return token.kind == S_IDENTIFIER && ("AUTOEXTEND_SIZE".equalsIgnoreCase(token.image) + || "FILE_BLOCK_SIZE".equalsIgnoreCase(token.image) + || "ENGINE_ATTRIBUTE".equalsIgnoreCase(token.image)); + } + + /** Only CREATE TABLESPACE sets FILE_BLOCK_SIZE, and undo tablespaces accept only ENGINE. */ + private TableOption validateTablespaceOption(TableOption option, boolean create, boolean undo) + throws ParseException { + TableOption.Kind kind = option.getKind(); + requireDdlSyntax(kind == TableOption.Kind.ENGINE + || !undo && (create || kind != TableOption.Kind.FILE_BLOCK_SIZE), + (create ? "CREATE " : "ALTER ") + (undo ? "UNDO " : "") + "TABLESPACE does not accept " + + option.getName()); + return option; + } + private boolean isIdentityAlterationAhead() { int first = getToken(1).kind; int second = getToken(2).kind; @@ -19612,6 +19635,10 @@ Statement Alter(): LOOKAHEAD(( | | ) ) statement = PostgreSqlTablespaceMove() | + // other ALTER TABLESPACE forms, such as Oracle data file clauses, remain unsupported + LOOKAHEAD(AlterTablespace() ( | )) + statement = AlterTablespace() + | statement = AlterForeignTable() | statement = AlterTable(false) @@ -19668,6 +19695,139 @@ AlterTablespaceMove PostgreSqlTablespaceMove(): { return result; } } +/** + * MySQL CREATE [UNDO] TABLESPACE with InnoDB options, or PostgreSQL CREATE TABLESPACE with an + * owner, a location and tablespace parameters. + */ +CreateTablespace CreateTablespace(): +{ + CreateTablespace statement = new CreateTablespace(); + String name; + Token token; + StringValue dataFile; + List parameters; + List options; +} +{ + ( + LOOKAHEAD({ isKeywordAhead("UNDO") }) ContextualKeyword("UNDO") + name=RelObjectName() { statement.setUndo(true); statement.setName(name); } + dataFile=MySqlTablespaceDataFile() { statement.setDataFile(dataFile); } + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(true, true) + { statement.setOptions(options); } ] + | + name=RelObjectName() { statement.setName(name); } + ( + LOOKAHEAD({ isKeywordAhead("OWNER") || isKeywordAhead("LOCATION") }) + [ LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") + name=RelObjectName() { statement.setOwner(name); } ] + ContextualKeyword("LOCATION") token= + { statement.setLocation(stringValue(token)); } + [ LOOKAHEAD( "(") parameters=PostgreSqlIndexOptions() + { statement.setParameters(parameters); } ] + | + [ dataFile=MySqlTablespaceDataFile() { statement.setDataFile(dataFile); } ] + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(true, false) + { statement.setOptions(options); } ] + ) + ) + { return statement; } +} + +StringValue MySqlTablespaceDataFile(): +{ Token token; } +{ + ContextualKeyword("DATAFILE") token= { return stringValue(token); } +} + +/** + * MySQL ALTER [UNDO] TABLESPACE and PostgreSQL ALTER TABLESPACE. Alter() selects this production + * only when it matches the whole statement. + */ +AlterTablespace AlterTablespace(): +{ + AlterTablespace statement = new AlterTablespace(); + String name; + List parameters; + List names = new ArrayList(); + List options; +} +{ + ( + LOOKAHEAD({ isKeywordAhead("UNDO") }) ContextualKeyword("UNDO") + name=RelObjectName() { statement.setUndo(true); statement.setName(name); } + + ( + { statement.setState(AlterTablespace.State.ACTIVE); } + | LOOKAHEAD({ isKeywordAhead("INACTIVE") }) ContextualKeyword("INACTIVE") + { statement.setState(AlterTablespace.State.INACTIVE); } + ) + { statement.setAction(AlterTablespace.Action.SET_STATE); } + [ LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(false, true) + { statement.setOptions(options); } ] + | + name=RelObjectName() { statement.setName(name); } + ( + name=RelObjectName() + { statement.setAction(AlterTablespace.Action.RENAME); statement.setNewName(name); } + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterTablespace.Action.OWNER); statement.setOwner(name); } + | + parameters=PostgreSqlIndexOptions() + { statement.setAction(AlterTablespace.Action.SET_PARAMETERS); statement.setParameters(parameters); } + | + "(" name=PostgreSqlStorageParameterName() { names.add(name); } + ( "," name=PostgreSqlStorageParameterName() { names.add(name); } )* ")" + { statement.setAction(AlterTablespace.Action.RESET_PARAMETERS); statement.setResetParameters(names); } + | + LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) }) options=MySqlTablespaceOptions(false, false) + { statement.setAction(AlterTablespace.Action.OPTIONS); statement.setOptions(options); } + ) + ) + { return statement; } +} + +/** + * MySQL tablespace options in source order, optionally separated by commas. Each clause accepts + * only the options that it defines. + */ +List MySqlTablespaceOptions(boolean create, boolean undo): +{ + List options = new ArrayList(); + TableOption option; +} +{ + option=MySqlTablespaceOption() { options.add(validateTablespaceOption(option, create, undo)); } + ( + LOOKAHEAD({ isMySqlTablespaceOptionAhead(1) + || getToken(1).kind == K_COMMA && isMySqlTablespaceOptionAhead(2) }) + [ "," ] option=MySqlTablespaceOption() + { options.add(validateTablespaceOption(option, create, undo)); } + )* + { return options; } +} + +/** Reuses the table option models for ENGINE, ENCRYPTION, AUTOEXTEND_SIZE and ENGINE_ATTRIBUTE. */ +TableOption MySqlTablespaceOption(): +{ + TableOption option; + String value; + boolean useEquals = false; +} +{ + ( + LOOKAHEAD({ isKeywordAhead("FILE_BLOCK_SIZE") }) ContextualKeyword("FILE_BLOCK_SIZE") + [ "=" { useEquals = true; } ] value=MySqlSizeNumber() + { option = new TableOption(TableOption.Kind.FILE_BLOCK_SIZE, "FILE_BLOCK_SIZE", value, useEquals); } + | + LOOKAHEAD({ getToken(1).kind == K_ENGINE }) option=MySqlTableOption() + | + option=MySqlScalarTableOption() + ) + { return option; } +} + /** The new relation statements reuse the same actions as structured ALTER TABLE properties. */ AlterRelation PostgreSqlAlterRelation(): { @@ -20923,6 +21083,11 @@ Statement Create(): | LOOKAHEAD({ isKeywordAhead("COLLATION") }) { requireDdlSyntax(!isUsingOrReplace, "CREATE COLLATION does not support OR REPLACE"); } statement = CreateCollation() | + LOOKAHEAD({ getToken(1).kind == K_TABLESPACE + || isKeywordAhead("UNDO") && getToken(2).kind == K_TABLESPACE }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE TABLESPACE does not support OR REPLACE"); } + statement = CreateTablespace() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 5f3b0157b..eced5bb54 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -158,6 +158,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``CREATE …`` · ``ALTER …`` · ``DROP …`` * - **MySQL table maintenance** - ``ANALYZE`` · ``CHECK`` · ``OPTIMIZE`` · ``REPAIR TABLE`` + * - **Tablespaces** + - MySQL and PostgreSQL ``CREATE TABLESPACE`` · ``ALTER TABLESPACE`` * - **PostgreSQL RLS** - ``CREATE POLICY`` · ``ALTER TABLE … ENABLE``/``DISABLE``/``FORCE``/``NO FORCE ROW LEVEL SECURITY`` * - **Salesforce SOQL** diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 901608d62..88d16401a 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1857,6 +1857,38 @@ can be built with ``new NotNullConstraint().withName("nn") .withColumn(new Column("id"))``. This is distinct from column definitions and ``ALTER COLUMN ... SET NOT NULL``; those retain their existing APIs. +MySQL and PostgreSQL tablespaces +-------------------------------- + +``CREATE TABLESPACE`` parses to ``CreateTablespace`` and ``ALTER TABLESPACE`` to +``AlterTablespace`` in every dialect; ``isUndo()`` records the MySQL ``UNDO`` keyword. +A PostgreSQL definition exposes ``getOwner()``, ``getLocation()`` and the ``WITH`` +parameters as ``IndexOption`` values in ``getParameters()``. A MySQL definition exposes +``getDataFile()`` and keeps its options in source order in ``getOptions()``. They reuse the +``TableOption`` kinds ``ENGINE``, ``ENCRYPTION``, ``AUTOEXTEND_SIZE`` and +``ENGINE_ATTRIBUTE``; CREATE also accepts ``FILE_BLOCK_SIZE``. + +.. code-block:: java + + CreateTablespace create = (CreateTablespace) CCJSqlParserUtil.parse( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' FILE_BLOCK_SIZE = 8192 ENGINE = InnoDB"); + create.getDataFile().getValue(); // ts1.ibd + create.getOption(TableOption.Kind.ENGINE).get().getValue(); // InnoDB + + AlterTablespace alter = (AlterTablespace) CCJSqlParserUtil.parse( + "ALTER TABLESPACE fast SET (random_page_cost = 1.1)"); + alter.getAction(); // SET_PARAMETERS + alter.getParameters(); // [random_page_cost = 1.1] + +``getAction()`` is ``RENAME``, ``OWNER``, ``SET_PARAMETERS`` or ``RESET_PARAMETERS``, +``SET_STATE`` for ``ALTER UNDO TABLESPACE … SET ACTIVE|INACTIVE``, or ``OPTIONS`` for MySQL +options. Undo tablespaces accept only ``ENGINE``, ``CREATE UNDO TABLESPACE`` requires +``ADD DATAFILE``, and ALTER rejects ``FILE_BLOCK_SIZE``. Other ``ALTER TABLESPACE`` forms, +such as Oracle and NDB data file clauses, still parse as ``UnsupportedStatement``, and +``ALTER TABLE ALL IN TABLESPACE`` remains ``AlterTablespaceMove``. Validators report the +``createTablespace`` and ``alterTablespace`` features, which the MySQL and PostgreSQL +capabilities enable; feature analysis reports ``MODIFIES_SCHEMA``. + SQL Server identity inserts --------------------------- diff --git a/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java new file mode 100644 index 000000000..6182bdef0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/TablespaceStatementTest.java @@ -0,0 +1,368 @@ +/*- + * #%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.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +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.StatementFeatures; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.alter.AlterTablespace; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.IndexOption; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TablespaceStatementTest { + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + /** Parses with the dialect, compares both renderers with the input and reparses the output. */ + private static T assertRoundTrip(String sql, Dialect dialect, + Class type) throws JSQLParserException { + Statement statement = + assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withDialect(dialect)); + assertInstanceOf(type, statement); + Statement reparsed = parse(statement.toString(), dialect); + assertInstanceOf(type, reparsed); + assertEquals(statement.toString(), reparsed.toString()); + return type.cast(statement); + } + + private static List kinds(List options) { + return options.stream().map(TableOption::getKind).collect(Collectors.toList()); + } + + private static List names(List parameters) { + return parameters.stream().map(IndexOption::getName).collect(Collectors.toList()); + } + + @Test + void mysqlCreateTablespaceWithDataFileAndEngine() throws JSQLParserException { + CreateTablespace statement = assertRoundTrip( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE=InnoDB", Dialect.MYSQL, + CreateTablespace.class); + + assertEquals("ts1", statement.getName()); + assertFalse(statement.isUndo()); + assertEquals("ts1.ibd", statement.getDataFile().getValue()); + TableOption engine = statement.getOption(TableOption.Kind.ENGINE).orElseThrow(); + assertEquals("InnoDB", engine.getValue()); + assertTrue(engine.isUseEquals()); + assertNull(statement.getOwner()); + assertNull(statement.getLocation()); + assertNull(statement.getParameters()); + assertEquals("CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE = InnoDB", + statement.toString()); + } + + @Test + void mysqlOptionsKeepSourceOrderAndReuseTableOptionModels() throws JSQLParserException { + CreateTablespace statement = (CreateTablespace) parse( + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' AUTOEXTEND_SIZE 4M, " + + "FILE_BLOCK_SIZE = 8192 ENCRYPTION 'Y', ENGINE_ATTRIBUTE = '{}' " + + "ENGINE InnoDB", + Dialect.MYSQL); + + assertEquals(List.of(TableOption.Kind.AUTOEXTEND_SIZE, TableOption.Kind.FILE_BLOCK_SIZE, + TableOption.Kind.ENCRYPTION, TableOption.Kind.ENGINE_ATTRIBUTE, + TableOption.Kind.ENGINE), kinds(statement.getOptions())); + assertEquals(List.of("4M", "8192", "'Y'", "'{}'", "InnoDB"), statement.getOptions() + .stream().map(TableOption::getValue).collect(Collectors.toList())); + assertEquals(List.of(false, true, false, true, false), statement.getOptions().stream() + .map(TableOption::isUseEquals).collect(Collectors.toList())); + // commas between options are optional and not retained + String expected = "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' AUTOEXTEND_SIZE 4M " + + "FILE_BLOCK_SIZE = 8192 ENCRYPTION 'Y' ENGINE_ATTRIBUTE = '{}' ENGINE InnoDB"; + assertEquals(expected, statement.toString()); + assertDeparse(statement, expected); + assertRoundTrip(expected, Dialect.MYSQL, CreateTablespace.class); + + CreateTable table = (CreateTable) parse( + "CREATE TABLE t (a INT) AUTOEXTEND_SIZE 4M ENCRYPTION 'Y' ENGINE InnoDB", + Dialect.MYSQL); + for (TableOption tableOption : table.getTableOptions()) { + TableOption tablespaceOption = + statement.getOption(tableOption.getKind()).orElseThrow(); + assertEquals(tableOption.getTokens(), tablespaceOption.getTokens()); + } + } + + @Test + void mysqlDataFileAndOptionsAreOptional() throws JSQLParserException { + CreateTablespace statement = + assertRoundTrip("CREATE TABLESPACE ts1", Dialect.MYSQL, CreateTablespace.class); + assertNull(statement.getDataFile()); + assertTrue(statement.getOptions().isEmpty()); + + statement = assertRoundTrip("CREATE TABLESPACE `ts 1` ENGINE = 'InnoDB'", Dialect.MYSQL, + CreateTablespace.class); + assertEquals("`ts 1`", statement.getName()); + assertNull(statement.getDataFile()); + assertEquals("'InnoDB'", statement.getOption(TableOption.Kind.ENGINE).orElseThrow() + .getValue()); + } + + @Test + void mysqlUndoTablespaces() throws JSQLParserException { + CreateTablespace create = assertRoundTrip( + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu' ENGINE = InnoDB", Dialect.MYSQL, + CreateTablespace.class); + assertTrue(create.isUndo()); + assertEquals("u1", create.getName()); + assertEquals("u1.ibu", create.getDataFile().getValue()); + assertEquals(List.of(TableOption.Kind.ENGINE), kinds(create.getOptions())); + + AlterTablespace inactive = assertRoundTrip("ALTER UNDO TABLESPACE u1 SET INACTIVE", + Dialect.MYSQL, AlterTablespace.class); + assertTrue(inactive.isUndo()); + assertEquals(AlterTablespace.Action.SET_STATE, inactive.getAction()); + assertEquals(AlterTablespace.State.INACTIVE, inactive.getState()); + assertTrue(inactive.getOptions().isEmpty()); + + AlterTablespace active = assertRoundTrip( + "ALTER UNDO TABLESPACE u1 SET ACTIVE ENGINE = InnoDB", Dialect.MYSQL, + AlterTablespace.class); + assertEquals(AlterTablespace.State.ACTIVE, active.getState()); + assertEquals(List.of(TableOption.Kind.ENGINE), kinds(active.getOptions())); + } + + @Test + void mysqlAlterTablespace() throws JSQLParserException { + AlterTablespace rename = assertRoundTrip("ALTER TABLESPACE ts1 RENAME TO ts2", + Dialect.MYSQL, AlterTablespace.class); + assertEquals("ts1", rename.getName()); + assertFalse(rename.isUndo()); + assertEquals(AlterTablespace.Action.RENAME, rename.getAction()); + assertEquals("ts2", rename.getNewName()); + + AlterTablespace options = (AlterTablespace) parse( + "ALTER TABLESPACE ts1 AUTOEXTEND_SIZE = 64M, ENCRYPTION = 'N' ENGINE = InnoDB " + + "ENGINE_ATTRIBUTE '{}'", + Dialect.MYSQL); + assertEquals(AlterTablespace.Action.OPTIONS, options.getAction()); + assertEquals(List.of(TableOption.Kind.AUTOEXTEND_SIZE, TableOption.Kind.ENCRYPTION, + TableOption.Kind.ENGINE, TableOption.Kind.ENGINE_ATTRIBUTE), + kinds(options.getOptions())); + assertEquals("'N'", options.getOption(TableOption.Kind.ENCRYPTION).orElseThrow() + .getValue()); + String expected = "ALTER TABLESPACE ts1 AUTOEXTEND_SIZE = 64M ENCRYPTION = 'N' " + + "ENGINE = InnoDB ENGINE_ATTRIBUTE '{}'"; + assertEquals(expected, options.toString()); + assertDeparse(options, expected); + assertRoundTrip(expected, Dialect.MYSQL, AlterTablespace.class); + } + + @Test + void postgresqlCreateTablespace() throws JSQLParserException { + CreateTablespace statement = assertRoundTrip( + "CREATE TABLESPACE fast OWNER u1 LOCATION '/ssd1/pg' " + + "WITH (random_page_cost = 1.1, effective_io_concurrency = 200)", + Dialect.POSTGRESQL, CreateTablespace.class); + + assertEquals("fast", statement.getName()); + assertEquals("u1", statement.getOwner()); + assertEquals("/ssd1/pg", statement.getLocation().getValue()); + assertEquals(List.of("random_page_cost", "effective_io_concurrency"), + names(statement.getParameters())); + assertEquals("1.1", statement.getParameters().get(0).getValue().toString()); + assertNull(statement.getDataFile()); + assertTrue(statement.getOptions().isEmpty()); + List values = new ArrayList<>(); + statement.visitExpressions(values::add); + assertEquals(2, values.size()); + + statement = assertRoundTrip("CREATE TABLESPACE fast LOCATION '/ssd1/pg'", + Dialect.POSTGRESQL, CreateTablespace.class); + assertNull(statement.getOwner()); + assertNull(statement.getParameters()); + + statement = assertRoundTrip("CREATE TABLESPACE \"Fast\" OWNER CURRENT_USER LOCATION ''", + Dialect.POSTGRESQL, CreateTablespace.class); + assertEquals("CURRENT_USER", statement.getOwner()); + assertEquals("", statement.getLocation().getValue()); + } + + @Test + void postgresqlAlterTablespace() throws JSQLParserException { + AlterTablespace rename = assertRoundTrip("ALTER TABLESPACE fast RENAME TO faster", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.RENAME, rename.getAction()); + assertEquals("faster", rename.getNewName()); + + AlterTablespace owner = assertRoundTrip("ALTER TABLESPACE fast OWNER TO SESSION_USER", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.OWNER, owner.getAction()); + assertEquals("SESSION_USER", owner.getOwner()); + + AlterTablespace set = assertRoundTrip( + "ALTER TABLESPACE fast SET (random_page_cost = 1.1, seq_page_cost = 2)", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.SET_PARAMETERS, set.getAction()); + assertEquals(List.of("random_page_cost", "seq_page_cost"), names(set.getParameters())); + List values = new ArrayList<>(); + set.visitExpressions(values::add); + assertEquals(2, values.size()); + + AlterTablespace reset = assertRoundTrip( + "ALTER TABLESPACE fast RESET (random_page_cost, seq_page_cost)", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals(AlterTablespace.Action.RESET_PARAMETERS, reset.getAction()); + assertEquals(List.of("random_page_cost", "seq_page_cost"), reset.getResetParameters()); + values.clear(); + reset.visitExpressions(values::add); + assertTrue(values.isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE = InnoDB", + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu'", + "CREATE TABLESPACE fast OWNER u1 LOCATION '/ssd1/pg' WITH (seq_page_cost = 1)", + "ALTER TABLESPACE ts1 RENAME TO ts2", "ALTER TABLESPACE ts1 ENCRYPTION = 'Y'", + "ALTER UNDO TABLESPACE u1 SET INACTIVE", "ALTER TABLESPACE fast OWNER TO bob", + "ALTER TABLESPACE fast SET (seq_page_cost = 1)", + "ALTER TABLESPACE fast RESET (seq_page_cost)"}) + void statementsOnlyModifySchemaAndKeepScriptBoundaries(String sql) + throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertTrue(statement instanceof CreateTablespace || statement instanceof AlterTablespace); + StatementFeatures features = statement.getFeatures(); + assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA), features.getCertain()); + assertTrue(features.getUncertain().isEmpty()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\n" + sql + "; SELECT 1"); + assertEquals(3, script.size()); + assertEquals(statement.getClass(), script.get(0).getClass()); + assertEquals(statement.getClass(), script.get(1).getClass()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE UNDO TABLESPACE u1", + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu' ENCRYPTION = 'Y'", + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' KEY_BLOCK_SIZE = 8", + "CREATE TABLESPACE ts1 AUTOEXTEND_SIZE = 4X", + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd', ENGINE = InnoDB", + "CREATE TABLESPACE ts1 ADD DATAFILE ts1", + "CREATE OR REPLACE TABLESPACE ts1", + "CREATE TABLESPACE fast OWNER u1", + "CREATE TABLESPACE fast LOCATION '/ssd1/pg' ENGINE = InnoDB", + "CREATE TABLESPACE fast ADD DATAFILE 'f.ibd' LOCATION '/ssd1/pg'", + "ALTER TABLESPACE ts1 FILE_BLOCK_SIZE = 8K", + "ALTER UNDO TABLESPACE u1 SET ACTIVE ENCRYPTION = 'Y'"}) + void rejectsFormsOutsideTheStatementDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @Test + void postgresqlParameterValuesRequireEquals() { + assertThrows(JSQLParserException.class, () -> parse( + "CREATE TABLESPACE fast LOCATION '/ssd1/pg' WITH (random_page_cost 1.1)", + Dialect.POSTGRESQL)); + assertThrows(JSQLParserException.class, + () -> parse("ALTER TABLESPACE fast SET (random_page_cost 1.1)", + Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLESPACE users ADD DATAFILE '/u01/users02.dbf' SIZE 100M AUTOEXTEND ON", + "ALTER TABLESPACE users OFFLINE", "ALTER TABLESPACE users READ ONLY", + "ALTER TABLESPACE users ENCRYPTION ONLINE USING 'AES256' ENCRYPT", + "ALTER TABLESPACE ts1 ADD DATAFILE 'ts1_2.dat' INITIAL_SIZE = 1M", + "ALTER TABLESPACE ts1 ENGINE = NDB WAIT", + "ALTER TABLESPACE ts1 RENAME TO ts2 ENGINE = InnoDB", + "ALTER UNDO TABLESPACE u1 RENAME TO u2"}) + void otherAlterTablespaceFormsRemainUnsupportedStatements(String sql) + throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertInstanceOf(UnsupportedStatement.class, statement); + assertEquals(sql, statement.toString()); + + Statements script = CCJSqlParserUtil.parseStatements( + sql + ";\nALTER TABLESPACE ts1 RENAME TO ts2;"); + assertInstanceOf(UnsupportedStatement.class, script.get(0)); + assertInstanceOf(AlterTablespace.class, script.get(1)); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE ALL IN TABLESPACE slow SET TABLESPACE fast", + "ALTER INDEX ALL IN TABLESPACE slow OWNED BY alice SET TABLESPACE fast NOWAIT", + "ALTER MATERIALIZED VIEW ALL IN TABLESPACE slow SET TABLESPACE fast"}) + void bulkTablespaceMovesRemainSeparate(String sql) throws JSQLParserException { + assertRoundTrip(sql, Dialect.POSTGRESQL, AlterTablespaceMove.class); + } + + @Test + void clauseWordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (undo INT, location INT, datafile INT, " + + "inactive INT, file_block_size INT, owner INT)"); + CreateTablespace create = assertRoundTrip("CREATE TABLESPACE undo ADD DATAFILE 'u.ibd'", + Dialect.MYSQL, CreateTablespace.class); + assertEquals("undo", create.getName()); + assertFalse(create.isUndo()); + AlterTablespace alter = assertRoundTrip("ALTER TABLESPACE location RENAME TO owner", + Dialect.POSTGRESQL, AlterTablespace.class); + assertEquals("location", alter.getName()); + assertEquals("owner", alter.getNewName()); + } + + @Test + void buildsAndDeparses() { + CreateTablespace create = new CreateTablespace(); + create.setName("fast"); + create.setOwner("bob"); + create.setLocation(new StringValue("/data")); + create.setParameters(List.of(new IndexOption("seq_page_cost", new LongValue(2), true))); + String expected = "CREATE TABLESPACE fast OWNER bob LOCATION '/data' " + + "WITH (seq_page_cost = 2)"; + assertEquals(expected, create.toString()); + assertDeparse(create, expected); + + AlterTablespace alter = new AlterTablespace(); + alter.setUndo(true); + alter.setName("u1"); + alter.setAction(AlterTablespace.Action.SET_STATE); + alter.setState(AlterTablespace.State.INACTIVE); + alter.addOptions(new TableOption(TableOption.Kind.ENGINE, "ENGINE", "InnoDB", true)); + expected = "ALTER UNDO TABLESPACE u1 SET INACTIVE ENGINE = InnoDB"; + assertEquals(expected, alter.toString()); + assertDeparse(alter, expected); + + alter.setAction(null); + assertThrows(IllegalStateException.class, alter::toString); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/TablespaceValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/TablespaceValidatorTest.java new file mode 100644 index 000000000..5b230c467 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/TablespaceValidatorTest.java @@ -0,0 +1,59 @@ +/*- + * #%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.util.validation.validator; + +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MariaDbVersion; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; + +public class TablespaceValidatorTest extends ValidationTestAsserts { + + @Test + public void testMySqlTablespaces() { + for (String sql : new String[] { + "CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' ENGINE = InnoDB", + "CREATE UNDO TABLESPACE u1 ADD DATAFILE 'u1.ibu'", + "ALTER TABLESPACE ts1 RENAME TO ts2", "ALTER UNDO TABLESPACE u1 SET INACTIVE"}) { + validateNoErrors(sql, 1, MySqlVersion.V8_0, FeaturesAllowed.DDL); + } + } + + @Test + public void testPostgreSqlTablespaces() { + for (String sql : new String[] { + "CREATE TABLESPACE fast OWNER u1 LOCATION '/ssd1/pg' WITH (random_page_cost = 1.1)", + "ALTER TABLESPACE fast OWNER TO bob", + "ALTER TABLESPACE fast SET (seq_page_cost = 2)", + "ALTER TABLESPACE fast RESET (seq_page_cost)"}) { + validateNoErrors(sql, 1, PostgresqlVersion.V10, PostgresqlVersion.V14, + FeaturesAllowed.DDL); + } + } + + @Test + public void testNotSupportedByMariaDb() { + validateNotSupported("CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd'", 1, 1, + MariaDbVersion.V10_5_4, Feature.createTablespace); + validateNotSupported("ALTER TABLESPACE ts1 RENAME TO ts2", 1, 1, + MariaDbVersion.V10_5_4, Feature.alterTablespace); + } + + @Test + public void testFeatureNotAllowed() { + validateNotAllowed("CREATE TABLESPACE fast LOCATION '/ssd1/pg'", 1, 1, + FeaturesAllowed.ALTER, Feature.createTablespace); + validateNotAllowed("ALTER TABLESPACE fast RENAME TO faster", 1, 1, + FeaturesAllowed.CREATE, Feature.alterTablespace); + } +}