diff --git a/README.md b/README.md
index 26d8c3978..d074079d8 100644
--- a/README.md
+++ b/README.md
@@ -158,6 +158,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` |
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
+| **MySQL spatial reference systems and instance** | `CREATE`/`DROP SPATIAL REFERENCE SYSTEM` · `ALTER INSTANCE` |
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
| **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` |
| **MySQL metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` |
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 992597c05..216690771 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -487,6 +487,18 @@ public enum Feature {
* @see net.sf.jsqlparser.statement.alter.AlterTablespace
*/
alterTablespace,
+ /**
+ * MySQL "CREATE SPATIAL REFERENCE SYSTEM" statement is allowed
+ *
+ * @see net.sf.jsqlparser.statement.create.spatial.CreateSpatialReferenceSystem
+ */
+ createSpatialReferenceSystem,
+ /**
+ * MySQL "ALTER INSTANCE" statement is allowed
+ *
+ * @see net.sf.jsqlparser.statement.alter.AlterInstance
+ */
+ alterInstance,
/**
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
index 1fb2eb10d..56ab3b2f0 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
@@ -42,6 +42,7 @@
import net.sf.jsqlparser.statement.alter.AlterStatistics;
import net.sf.jsqlparser.statement.alter.AlterSubscription;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.AlterTablespace;
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
@@ -76,6 +77,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
@@ -1482,6 +1484,17 @@ public Void visit(CreateTablespace statement, S context) {
return schemaOnly();
}
+ @Override
+ public Void visit(CreateSpatialReferenceSystem statement, S context) {
+ return schemaOnly();
+ }
+
+ /** A server operation, like ALTER SYSTEM. */
+ @Override
+ public Void visit(AlterInstance statement, S context) {
+ return sessionOnly();
+ }
+
@Override
public Void visit(AlterTablespace 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 d3a8f79fd..f7122c847 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
@@ -60,6 +60,7 @@
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
import net.sf.jsqlparser.statement.alter.AlterUser;
@@ -76,6 +77,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
@@ -1026,6 +1028,22 @@ default void visit(CreateTablespace statement) {
visit(statement, null);
}
+ default T visit(CreateSpatialReferenceSystem statement, S context) {
+ return null;
+ }
+
+ default void visit(CreateSpatialReferenceSystem statement) {
+ visit(statement, null);
+ }
+
+ default T visit(AlterInstance statement, S context) {
+ return null;
+ }
+
+ default void visit(AlterInstance statement) {
+ visit(statement, null);
+ }
+
default T visit(AlterTablespace 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 f1d2c5326..85dd23819 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
@@ -62,6 +62,7 @@
import net.sf.jsqlparser.statement.alter.AlterExpression;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
import net.sf.jsqlparser.statement.alter.AlterUser;
@@ -78,6 +79,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
@@ -1150,6 +1152,16 @@ public T visit(CreateTablespace statement, S context) {
return null;
}
+ @Override
+ public T visit(CreateSpatialReferenceSystem statement, S context) {
+ return null;
+ }
+
+ @Override
+ public T visit(AlterInstance statement, S context) {
+ return null;
+ }
+
@Override
public T visit(AlterTablespace statement, S context) {
statement.visitExpressions(expression -> expression.accept(expressionVisitor, context));
diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterInstance.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterInstance.java
new file mode 100644
index 000000000..a79f4bab0
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterInstance.java
@@ -0,0 +1,148 @@
+/*-
+ * #%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.Objects;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+
+/**
+ * MySQL {@code ALTER INSTANCE}, which acts on the server instance:
+ * {@code {ENABLE | DISABLE} INNODB REDO_LOG}, {@code ROTATE INNODB MASTER KEY},
+ * {@code ROTATE BINLOG MASTER KEY}, {@code RELOAD TLS [FOR CHANNEL channel] [NO ROLLBACK ON ERROR]}
+ * and {@code RELOAD KEYRING}.
+ *
+ *
+ * MySQL reads INNODB, BINLOG and REDO_LOG as identifiers, so they may be quoted; the statement
+ * renders them as plain words. The channel is kept as written: MySQL's grammar takes any
+ * identifier, and the server accepts {@code mysql_main} and {@code mysql_admin} when it runs the
+ * statement.
+ *
+ * @see MySQL ALTER
+ * INSTANCE
+ */
+public class AlterInstance extends ASTNodeAccessImpl implements Statement {
+ /** The words after ALTER INSTANCE, without the options of RELOAD TLS. */
+ public enum Action {
+ ENABLE_INNODB_REDO_LOG("ENABLE INNODB REDO_LOG"), DISABLE_INNODB_REDO_LOG(
+ "DISABLE INNODB REDO_LOG"), ROTATE_INNODB_MASTER_KEY(
+ "ROTATE INNODB MASTER KEY"), ROTATE_BINLOG_MASTER_KEY(
+ "ROTATE BINLOG MASTER KEY"), RELOAD_TLS(
+ "RELOAD TLS"), RELOAD_KEYRING("RELOAD KEYRING");
+
+ private final String sql;
+
+ Action(String sql) {
+ this.sql = sql;
+ }
+
+ public String getSql() {
+ return sql;
+ }
+ }
+
+ private Action action;
+ private String channel;
+ private boolean noRollbackOnError;
+
+ public AlterInstance(Action action) {
+ this.action = Objects.requireNonNull(action, "action");
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ /**
+ * Sets the action.
+ *
+ * @throws IllegalArgumentException if the statement has options of RELOAD TLS and the action is
+ * another one
+ */
+ public void setAction(Action action) {
+ Objects.requireNonNull(action, "action");
+ if (action != Action.RELOAD_TLS && (channel != null || noRollbackOnError)) {
+ throw new IllegalArgumentException(
+ "FOR CHANNEL and NO ROLLBACK ON ERROR belong to RELOAD TLS");
+ }
+ this.action = action;
+ }
+
+ public AlterInstance withAction(Action action) {
+ setAction(action);
+ return this;
+ }
+
+ /** The identifier of {@code RELOAD TLS FOR CHANNEL channel} as written, or null. */
+ public String getChannel() {
+ return channel;
+ }
+
+ /**
+ * Sets the channel of RELOAD TLS; null removes the clause.
+ *
+ * @throws IllegalArgumentException if the action is not RELOAD TLS
+ */
+ public void setChannel(String channel) {
+ if (channel != null && action != Action.RELOAD_TLS) {
+ throw new IllegalArgumentException("FOR CHANNEL belongs to RELOAD TLS");
+ }
+ this.channel = channel;
+ }
+
+ public AlterInstance withChannel(String channel) {
+ setChannel(channel);
+ return this;
+ }
+
+ /** Whether RELOAD TLS ends with NO ROLLBACK ON ERROR. */
+ public boolean isNoRollbackOnError() {
+ return noRollbackOnError;
+ }
+
+ /**
+ * Sets NO ROLLBACK ON ERROR.
+ *
+ * @throws IllegalArgumentException if the action is not RELOAD TLS
+ */
+ public void setNoRollbackOnError(boolean noRollbackOnError) {
+ if (noRollbackOnError && action != Action.RELOAD_TLS) {
+ throw new IllegalArgumentException("NO ROLLBACK ON ERROR belongs to RELOAD TLS");
+ }
+ this.noRollbackOnError = noRollbackOnError;
+ }
+
+ public AlterInstance withNoRollbackOnError(boolean noRollbackOnError) {
+ setNoRollbackOnError(noRollbackOnError);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append("ALTER INSTANCE ").append(action.getSql());
+ if (channel != null) {
+ builder.append(" FOR CHANNEL ").append(channel);
+ }
+ if (noRollbackOnError) {
+ builder.append(" NO ROLLBACK ON ERROR");
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/spatial/CreateSpatialReferenceSystem.java b/src/main/java/net/sf/jsqlparser/statement/create/spatial/CreateSpatialReferenceSystem.java
new file mode 100644
index 000000000..28e4f6b30
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/create/spatial/CreateSpatialReferenceSystem.java
@@ -0,0 +1,195 @@
+/*-
+ * #%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.spatial;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+
+/**
+ * MySQL {@code CREATE OR REPLACE SPATIAL REFERENCE SYSTEM srid srs_attribute ...} and
+ * {@code CREATE SPATIAL REFERENCE SYSTEM [IF NOT EXISTS] srid srs_attribute ...}. OR REPLACE and IF
+ * NOT EXISTS exclude each other.
+ *
+ *
+ * The attributes keep their source order. MySQL's grammar takes them in any order and any number;
+ * the server then reports a repeated attribute and a missing NAME or DEFINITION as errors of their
+ * own, not as syntax errors, so the parser accepts them. The typed getters return the first
+ * attribute of their kind.
+ *
+ * @see MySQL
+ * CREATE SPATIAL REFERENCE SYSTEM
+ */
+public class CreateSpatialReferenceSystem extends ASTNodeAccessImpl implements Statement {
+ private boolean orReplace;
+ private boolean ifNotExists;
+ private Expression srid;
+ private final List attributes = new ArrayList<>();
+
+ public CreateSpatialReferenceSystem(Expression srid) {
+ setSrid(srid);
+ }
+
+ public boolean isOrReplace() {
+ return orReplace;
+ }
+
+ /**
+ * Sets OR REPLACE.
+ *
+ * @throws IllegalArgumentException if the statement has IF NOT EXISTS
+ */
+ public void setOrReplace(boolean orReplace) {
+ if (orReplace && ifNotExists) {
+ throw new IllegalArgumentException("OR REPLACE excludes IF NOT EXISTS");
+ }
+ this.orReplace = orReplace;
+ }
+
+ public CreateSpatialReferenceSystem withOrReplace(boolean orReplace) {
+ setOrReplace(orReplace);
+ return this;
+ }
+
+ public boolean isIfNotExists() {
+ return ifNotExists;
+ }
+
+ /**
+ * Sets IF NOT EXISTS.
+ *
+ * @throws IllegalArgumentException if the statement has OR REPLACE
+ */
+ public void setIfNotExists(boolean ifNotExists) {
+ if (ifNotExists && orReplace) {
+ throw new IllegalArgumentException("IF NOT EXISTS excludes OR REPLACE");
+ }
+ this.ifNotExists = ifNotExists;
+ }
+
+ public CreateSpatialReferenceSystem withIfNotExists(boolean ifNotExists) {
+ setIfNotExists(ifNotExists);
+ return this;
+ }
+
+ /**
+ * The SRID as written: a {@link net.sf.jsqlparser.expression.LongValue}, or a
+ * {@link net.sf.jsqlparser.expression.HexValue} for {@code 0x...} and {@code X'...'}.
+ */
+ public Expression getSrid() {
+ return srid;
+ }
+
+ public final void setSrid(Expression srid) {
+ this.srid = Objects.requireNonNull(srid, "srid");
+ }
+
+ public CreateSpatialReferenceSystem withSrid(Expression srid) {
+ setSrid(srid);
+ return this;
+ }
+
+ /** The mutable attributes in source order. */
+ public List getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Collection extends SpatialReferenceSystemAttribute> attributes) {
+ this.attributes.clear();
+ if (attributes != null) {
+ this.attributes.addAll(attributes);
+ }
+ }
+
+ public CreateSpatialReferenceSystem withAttributes(
+ Collection extends SpatialReferenceSystemAttribute> attributes) {
+ setAttributes(attributes);
+ return this;
+ }
+
+ public CreateSpatialReferenceSystem addAttributes(
+ SpatialReferenceSystemAttribute... attributes) {
+ Collections.addAll(this.attributes, attributes);
+ return this;
+ }
+
+ /** The first attribute of the kind, or null. */
+ public SpatialReferenceSystemAttribute getAttribute(
+ SpatialReferenceSystemAttribute.Kind kind) {
+ for (SpatialReferenceSystemAttribute attribute : attributes) {
+ if (attribute.getKind() == kind) {
+ return attribute;
+ }
+ }
+ return null;
+ }
+
+ private StringValue getValue(SpatialReferenceSystemAttribute.Kind kind) {
+ SpatialReferenceSystemAttribute attribute = getAttribute(kind);
+ return attribute == null ? null : attribute.getValue();
+ }
+
+ /** The string of {@code NAME 'srs_name'}, or null. */
+ public StringValue getName() {
+ return getValue(SpatialReferenceSystemAttribute.Kind.NAME);
+ }
+
+ /** The string of {@code DEFINITION 'definition'}, or null. */
+ public StringValue getDefinition() {
+ return getValue(SpatialReferenceSystemAttribute.Kind.DEFINITION);
+ }
+
+ /** The organization name of {@code ORGANIZATION 'org_name' IDENTIFIED BY org_id}, or null. */
+ public StringValue getOrganization() {
+ return getValue(SpatialReferenceSystemAttribute.Kind.ORGANIZATION);
+ }
+
+ /** The number of {@code ORGANIZATION 'org_name' IDENTIFIED BY org_id}, or null. */
+ public Expression getOrganizationId() {
+ SpatialReferenceSystemAttribute attribute =
+ getAttribute(SpatialReferenceSystemAttribute.Kind.ORGANIZATION);
+ return attribute == null ? null : attribute.getOrganizationId();
+ }
+
+ /** The string of {@code DESCRIPTION 'description'}, or null. */
+ public StringValue getDescription() {
+ return getValue(SpatialReferenceSystemAttribute.Kind.DESCRIPTION);
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append(orReplace ? "CREATE OR REPLACE " : "CREATE ")
+ .append("SPATIAL REFERENCE SYSTEM ");
+ if (ifNotExists) {
+ builder.append("IF NOT EXISTS ");
+ }
+ builder.append(srid);
+ for (SpatialReferenceSystemAttribute attribute : attributes) {
+ attribute.appendTo(builder.append(' '));
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemAttribute.java b/src/main/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemAttribute.java
new file mode 100644
index 000000000..95002fedf
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemAttribute.java
@@ -0,0 +1,110 @@
+/*-
+ * #%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.spatial;
+
+import java.util.Objects;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+
+/**
+ * One attribute of MySQL's {@code CREATE SPATIAL REFERENCE SYSTEM}: {@code NAME 'srs_name'},
+ * {@code DEFINITION 'definition'}, {@code ORGANIZATION 'org_name' IDENTIFIED BY org_id} or
+ * {@code DESCRIPTION 'description'}.
+ *
+ * @see CreateSpatialReferenceSystem
+ */
+public class SpatialReferenceSystemAttribute extends ASTNodeAccessImpl {
+ /** The keyword that starts the attribute. */
+ public enum Kind {
+ NAME, DEFINITION, ORGANIZATION, DESCRIPTION
+ }
+
+ private final Kind kind;
+ private StringValue value;
+ private Expression organizationId;
+
+ /**
+ * A NAME, DEFINITION or DESCRIPTION attribute.
+ *
+ * @throws IllegalArgumentException for {@link Kind#ORGANIZATION}, which needs an identifier
+ */
+ public SpatialReferenceSystemAttribute(Kind kind, StringValue value) {
+ this.kind = Objects.requireNonNull(kind, "kind");
+ if (kind == Kind.ORGANIZATION) {
+ throw new IllegalArgumentException("ORGANIZATION requires IDENTIFIED BY");
+ }
+ setValue(value);
+ }
+
+ /** An {@code ORGANIZATION 'org_name' IDENTIFIED BY org_id} attribute. */
+ public SpatialReferenceSystemAttribute(StringValue organization, Expression organizationId) {
+ this.kind = Kind.ORGANIZATION;
+ setValue(organization);
+ this.organizationId = Objects.requireNonNull(organizationId, "organizationId");
+ }
+
+ public Kind getKind() {
+ return kind;
+ }
+
+ /** The string after the keyword; the organization name for {@link Kind#ORGANIZATION}. */
+ public StringValue getValue() {
+ return value;
+ }
+
+ public final void setValue(StringValue value) {
+ this.value = Objects.requireNonNull(value, "value");
+ }
+
+ public SpatialReferenceSystemAttribute withValue(StringValue value) {
+ setValue(value);
+ return this;
+ }
+
+ /**
+ * The number after IDENTIFIED BY as written, a {@link net.sf.jsqlparser.expression.LongValue}
+ * or a {@link net.sf.jsqlparser.expression.HexValue}; null unless the attribute is an
+ * {@link Kind#ORGANIZATION}.
+ */
+ public Expression getOrganizationId() {
+ return organizationId;
+ }
+
+ /**
+ * Sets the number after IDENTIFIED BY.
+ *
+ * @throws IllegalArgumentException if the attribute is not an ORGANIZATION
+ */
+ public void setOrganizationId(Expression organizationId) {
+ if (kind != Kind.ORGANIZATION) {
+ throw new IllegalArgumentException(kind + " takes no IDENTIFIED BY");
+ }
+ this.organizationId = Objects.requireNonNull(organizationId, "organizationId");
+ }
+
+ public SpatialReferenceSystemAttribute withOrganizationId(Expression organizationId) {
+ setOrganizationId(organizationId);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append(kind).append(' ').append(value);
+ if (organizationId != null) {
+ builder.append(" IDENTIFIED BY ").append(organizationId);
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
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 176ffd5f1..42c914af7 100644
--- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
+++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
@@ -35,7 +35,7 @@
public class Drop extends ASTNodeAccessImpl implements Statement {
public enum ObjectType {
- DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, CAST, OPERATOR, TRANSFORM, RESOURCE_GROUP, OTHER;
+ DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, CAST, OPERATOR, TRANSFORM, RESOURCE_GROUP, SPATIAL_REFERENCE_SYSTEM, OTHER;
/** Targets are {@link RoutineReference}s, each with an optional argument signature. */
public boolean hasRoutineTargets() {
@@ -129,7 +129,8 @@ public void setName(Table name) {
/**
* Returns the dropped object names. Routine, account and operator targets return a read-only
* snapshot of their names; edit {@link #getRoutines()}, {@link #getAccounts()} or
- * {@link #getOperators()} to change them. A cast or transform has no name.
+ * {@link #getOperators()} to change them. A cast or transform has no name. The name of a MySQL
+ * {@link ObjectType#SPATIAL_REFERENCE_SYSTEM} is its SRID as written.
*/
public List getNames() {
if (!routines.isEmpty()) {
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index 4243649c6..f1ec12b46 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -137,6 +137,7 @@
import net.sf.jsqlparser.statement.alter.AlterStatistics;
import net.sf.jsqlparser.statement.alter.AlterSubscription;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.AlterTablespace;
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
@@ -172,6 +173,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
@@ -3419,6 +3421,16 @@ public Void visit(CreateTablespace statement, S context) {
return null;
}
+ @Override
+ public Void visit(CreateSpatialReferenceSystem statement, S context) {
+ return null;
+ }
+
+ @Override
+ public Void visit(AlterInstance statement, S context) {
+ return null;
+ }
+
@Override
public Void visit(AlterTablespace statement, S context) {
statement.visitExpressions(expression -> expression.accept(this, 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 d103d3f8b..6837fb903 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -95,6 +95,7 @@
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
import net.sf.jsqlparser.statement.alter.AlterUser;
@@ -111,6 +112,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
@@ -1046,6 +1048,16 @@ public StringBuilder visit(CreateTablespace statement, S context) {
expression -> expression.accept(expressionDeParser, context));
}
+ @Override
+ public StringBuilder visit(CreateSpatialReferenceSystem statement, S context) {
+ return statement.appendTo(builder);
+ }
+
+ @Override
+ public StringBuilder visit(AlterInstance statement, S context) {
+ return statement.appendTo(builder);
+ }
+
@Override
public StringBuilder visit(AlterTablespace statement, S context) {
return statement.appendTo(builder,
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 ca8dc9343..a96d37ca2 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
@@ -130,6 +130,12 @@ public enum MySqlVersion implements Version {
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-spatial-reference-system.html
+ Feature.createSpatialReferenceSystem,
+
+ // https://dev.mysql.com/doc/refman/8.0/en/alter-instance.html
+ Feature.alterInstance,
+
// https://dev.mysql.com/doc/refman/8.0/en/create-database.html
// https://dev.mysql.com/doc/refman/8.0/en/alter-database.html
Feature.createDatabase, Feature.alterDatabase,
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 969e69d5e..2f63bb1ae 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
@@ -99,6 +99,7 @@
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
+import net.sf.jsqlparser.statement.alter.AlterInstance;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
import net.sf.jsqlparser.statement.alter.AlterUser;
@@ -118,6 +119,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.spatial.CreateSpatialReferenceSystem;
import net.sf.jsqlparser.statement.create.tablespace.CreateTablespace;
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
@@ -1631,6 +1633,18 @@ public Void visit(CreateTablespace statement, S context) {
return null;
}
+ @Override
+ public Void visit(CreateSpatialReferenceSystem statement, S context) {
+ validateFeature(Feature.createSpatialReferenceSystem);
+ return null;
+ }
+
+ @Override
+ public Void visit(AlterInstance statement, S context) {
+ validateFeature(Feature.alterInstance);
+ return null;
+ }
+
@Override
public Void visit(AlterTablespace statement, S context) {
validateFeature(Feature.alterTablespace);
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 5e67e2c58..fd19a7bbd 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -73,6 +73,7 @@ import net.sf.jsqlparser.statement.create.event.*;
import net.sf.jsqlparser.statement.create.function.*;
import net.sf.jsqlparser.statement.create.index.*;
import net.sf.jsqlparser.statement.create.macro.*;
+import net.sf.jsqlparser.statement.create.spatial.*;
import net.sf.jsqlparser.statement.create.statistics.CreateStatistics;
import net.sf.jsqlparser.statement.create.policy.*;
import net.sf.jsqlparser.statement.create.procedure.*;
@@ -2807,6 +2808,91 @@ public class CCJSqlParser extends AbstractJSqlParser {
return kind;
}
+ /** MySQL statements that MariaDB does not have are read with the MySQL dialect or none. */
+ private boolean isMySqlOnlyStatementDialect(boolean orNone) {
+ String dialect = getAsString(Feature.dialect);
+ return orNone && dialect == null || Dialect.MYSQL.name().equals(dialect);
+ }
+
+ /**
+ * Whether MySQL's SPATIAL REFERENCE SYSTEM follows CREATE or DROP. The MySQL dialect has no
+ * other statement that continues with SPATIAL but CREATE SPATIAL INDEX, so there a misspelled
+ * object type is an error; without a dialect only the three words select the statement.
+ */
+ private boolean isSpatialReferenceSystemAhead() {
+ if (getToken(1).kind != K_SPATIAL) {
+ return false;
+ }
+ return isMySqlOnlyStatementDialect(false) && getToken(2).kind != K_INDEX
+ || isMySqlOnlyStatementDialect(true) && isWordAt(2, "REFERENCE")
+ && getToken(3).kind == K_SYSTEM;
+ }
+
+ /**
+ * Whether MySQL's ALTER INSTANCE follows ALTER. Without a dialect, one of its actions must
+ * follow; other text stays an unsupported statement.
+ */
+ private boolean isAlterInstanceAhead() {
+ if (!isWordAt(1, "INSTANCE")) {
+ return false;
+ }
+ int action = getToken(2).kind;
+ return isMySqlOnlyStatementDialect(false)
+ || isMySqlOnlyStatementDialect(true) && (action == K_ENABLE || action == K_DISABLE
+ || isWordAt(2, "ROTATE") || isWordAt(2, "RELOAD"));
+ }
+
+ private boolean isSpatialReferenceSystemAttributeAhead() {
+ return getToken(1).kind == K_NAME || isKeywordAhead("DEFINITION")
+ || isKeywordAhead("ORGANIZATION") || isKeywordAhead("DESCRIPTION");
+ }
+
+ /**
+ * Whether MySQL reads the token as a plain string: quoted with ' or ", without a prefix such
+ * as N or _utf8. A double-quoted token is a string in MySQL's default SQL mode.
+ */
+ private static boolean isMySqlTextString(Token token) {
+ return token.image.startsWith("'") || token.image.startsWith("\"");
+ }
+
+ /**
+ * The number MySQL reads as an SRID or an organization's identifier: an unsigned integer up to
+ * 2^64 - 1, 0x... with a lower-case x, or X'...' with whole bytes. The server checks the range
+ * of the value when it runs the statement.
+ */
+ private Expression spatialReferenceSystemNumber(Token token) throws ParseException {
+ boolean valid;
+ if (token.kind == S_HEX) {
+ valid = token.image.matches("[Xx]'([0-9A-Fa-f]{2})*'|0x[0-9A-Fa-f]+");
+ } else {
+ valid = token.image.matches("[0-9]+")
+ && new java.math.BigInteger(token.image).bitLength() <= 64;
+ }
+ requireDdlSyntax(valid, "Expected an unsigned integer: " + token.image);
+ Expression number = token.kind == S_HEX ? new HexValue(token.image)
+ : new LongValue(token.image);
+ linkAST((ASTNodeAccess) number, token, token);
+ return number;
+ }
+
+ /**
+ * The word of ALTER INSTANCE that MySQL reads as an identifier and compares with a fixed name:
+ * plain or in backticks, and where {@code text} is set also a string.
+ */
+ private static String alterInstanceWord(Token token, boolean text) throws ParseException {
+ String image = token.image;
+ if (token.kind == S_IDENTIFIER) {
+ return image;
+ }
+ if (image.startsWith("`")) {
+ return image.substring(1, image.length() - 1).replace("``", "`");
+ }
+ if (text && isMySqlTextString(token)) {
+ return new StringValue(image).getValue();
+ }
+ throw new ParseException("Unexpected " + image + " in ALTER INSTANCE");
+ }
+
/**
* Whether NO_WRITE_TO_BINLOG or LOCAL modifies a MySQL partition operation. Followed by a
* comma, INTO or the end of the statement, the word remains a partition name.
@@ -6976,6 +7062,147 @@ BackupLockStatement BackupLockStatement():
{ return statement; }
}
+/**
+ * MySQL ALTER INSTANCE, after ALTER. MySQL reads INNODB, BINLOG and REDO_LOG as identifiers that
+ * it compares with these names, and the engine of ROTATE also as a string.
+ */
+// https://dev.mysql.com/doc/refman/8.4/en/alter-instance.html
+AlterInstance AlterInstance():
+{
+ AlterInstance statement;
+ Token word;
+ String name;
+ String channel;
+}
+{
+ ContextualKeyword("INSTANCE")
+ (
+ (
+ { statement = new AlterInstance(AlterInstance.Action.ENABLE_INNODB_REDO_LOG); }
+ |
+ { statement = new AlterInstance(AlterInstance.Action.DISABLE_INNODB_REDO_LOG); }
+ )
+ word=AlterInstanceWord()
+ { requireDdlSyntax("INNODB".equalsIgnoreCase(alterInstanceWord(word, false)), "Expected INNODB but found " + word.image); }
+ word=AlterInstanceWord()
+ { requireDdlSyntax("REDO_LOG".equalsIgnoreCase(alterInstanceWord(word, false)), "Expected REDO_LOG but found " + word.image); }
+ |
+ LOOKAHEAD({ isKeywordAhead("ROTATE") }) ContextualKeyword("ROTATE") word=AlterInstanceWord()
+ {
+ name = alterInstanceWord(word, true);
+ requireDdlSyntax("INNODB".equalsIgnoreCase(name) || "BINLOG".equalsIgnoreCase(name),
+ "Expected INNODB or BINLOG but found " + word.image);
+ statement = new AlterInstance("INNODB".equalsIgnoreCase(name)
+ ? AlterInstance.Action.ROTATE_INNODB_MASTER_KEY
+ : AlterInstance.Action.ROTATE_BINLOG_MASTER_KEY);
+ }
+ ContextualKeyword("MASTER")
+ |
+ ContextualKeyword("RELOAD")
+ (
+ LOOKAHEAD({ isKeywordAhead("KEYRING") }) ContextualKeyword("KEYRING")
+ { statement = new AlterInstance(AlterInstance.Action.RELOAD_KEYRING); }
+ |
+ ContextualKeyword("TLS") { statement = new AlterInstance(AlterInstance.Action.RELOAD_TLS); }
+ [
+ ContextualKeyword("CHANNEL") channel=RelObjectName()
+ {
+ // a double-quoted name is a string in MySQL, which takes an identifier here
+ requireDdlSyntax(!channel.startsWith("\"") && !channel.startsWith("["),
+ "Expected a channel name but found " + channel);
+ statement.setChannel(channel);
+ }
+ ]
+ [ { statement.setNoRollbackOnError(true); } ]
+ )
+ )
+ { return statement; }
+}
+
+Token AlterInstanceWord():
+{ Token token; }
+{
+ ( token= | token= | token= )
+ { return token; }
+}
+
+/**
+ * MySQL CREATE [OR REPLACE] SPATIAL REFERENCE SYSTEM [IF NOT EXISTS] srid srs_attribute ...,
+ * after CREATE [OR REPLACE]. The attributes come in any order; the server, not its grammar, reports
+ * a repeated or a missing one.
+ */
+// https://dev.mysql.com/doc/refman/8.4/en/create-spatial-reference-system.html
+CreateSpatialReferenceSystem CreateSpatialReferenceSystem(boolean orReplace):
+{
+ CreateSpatialReferenceSystem statement;
+ boolean ifNotExists = false;
+ Expression srid;
+ SpatialReferenceSystemAttribute attribute;
+}
+{
+ ContextualKeyword("REFERENCE")
+ [
+
+ { requireDdlSyntax(!orReplace, "CREATE OR REPLACE SPATIAL REFERENCE SYSTEM does not take IF NOT EXISTS"); ifNotExists = true; }
+ ]
+ srid=SpatialReferenceSystemNumber()
+ {
+ statement = new CreateSpatialReferenceSystem(srid).withOrReplace(orReplace)
+ .withIfNotExists(ifNotExists);
+ }
+ (
+ LOOKAHEAD({ isSpatialReferenceSystemAttributeAhead() })
+ attribute=SpatialReferenceSystemAttribute() { statement.addAttributes(attribute); }
+ )*
+ { return statement; }
+}
+
+SpatialReferenceSystemAttribute SpatialReferenceSystemAttribute():
+{
+ Token first = getToken(1);
+ SpatialReferenceSystemAttribute attribute;
+ StringValue value;
+ Expression organizationId;
+}
+{
+ (
+ value=SpatialReferenceSystemString()
+ { attribute = new SpatialReferenceSystemAttribute(SpatialReferenceSystemAttribute.Kind.NAME, value); }
+ |
+ LOOKAHEAD({ isKeywordAhead("DEFINITION") })
+ ContextualKeyword("DEFINITION") value=SpatialReferenceSystemString()
+ { attribute = new SpatialReferenceSystemAttribute(SpatialReferenceSystemAttribute.Kind.DEFINITION, value); }
+ |
+ LOOKAHEAD({ isKeywordAhead("DESCRIPTION") })
+ ContextualKeyword("DESCRIPTION") value=SpatialReferenceSystemString()
+ { attribute = new SpatialReferenceSystemAttribute(SpatialReferenceSystemAttribute.Kind.DESCRIPTION, value); }
+ |
+ ContextualKeyword("ORGANIZATION") value=SpatialReferenceSystemString()
+ organizationId=SpatialReferenceSystemNumber()
+ { attribute = new SpatialReferenceSystemAttribute(value, organizationId); }
+ )
+ { return linked(attribute, first); }
+}
+
+/** A plain MySQL string; the grammar takes no prefix and no adjacent strings here. */
+StringValue SpatialReferenceSystemString():
+{ Token token; }
+{
+ ( token= | token= )
+ {
+ requireDdlSyntax(isMySqlTextString(token), "Expected a string but found " + token.image);
+ return stringValue(token);
+ }
+}
+
+/** The SRID, or the identifier an organization gives the system, as written. */
+Expression SpatialReferenceSystemNumber():
+{ Token token; }
+{
+ ( token= | token= )
+ { return spatialReferenceSystemNumber(token); }
+}
+
LockStatement.Target LockTarget():
{ TableReference reference; }
{
@@ -23290,9 +23517,14 @@ void DropTargets(Drop drop):
TransformSignature transform;
OperatorSignature operator;
Table name;
+ Expression srid;
}
{
(
+ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM })
+ srid=SpatialReferenceSystemNumber()
+ { drop.addNames(linkToLastToken(new Table(srid.toString(), false))); }
+ |
LOOKAHEAD({ drop.getObjectType().hasRoutineTargets() })
routine=RoutineReference(aggregate) { drop.addRoutines(routine); }
( "," routine=RoutineReference(aggregate) { drop.addRoutines(routine); } )*
@@ -23432,6 +23664,10 @@ Drop Drop():
| ContextualKeyword("TEMPLATE") { multiWordType = "TEXT SEARCH TEMPLATE"; }
)
|
+ // MySQL DROP SPATIAL REFERENCE SYSTEM [IF EXISTS] srid
+ LOOKAHEAD({ isSpatialReferenceSystemAhead() })
+ ContextualKeyword("REFERENCE") { multiWordType = "SPATIAL REFERENCE SYSTEM"; }
+ |
// PostgreSQL DROP OWNED BY role [, ...]
LOOKAHEAD({ isKeywordAhead("OWNED") && getToken(2).kind == K_BY }) tk=
|
@@ -23548,6 +23784,11 @@ Drop Drop():
|| DropLookahead.isResourceGroupTarget(drop) && dropArgs.isEmpty()
&& indexTable == null && indexOptions.isEmpty() && behavior == null,
"DROP RESOURCE GROUP takes one group name and FORCE");
+ // MySQL's DROP SPATIAL REFERENCE SYSTEM ends with the SRID
+ requireDdlSyntax(drop.getObjectType() != Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM
+ || dropArgs.isEmpty() && indexTable == null && indexOptions.isEmpty()
+ && behavior == null && !drop.isMaterialized(),
+ "Unexpected clause in DROP " + drop.getType());
if (dropArgs.size() > 0) {
drop.setParameters(dropArgs);
}
@@ -25224,6 +25465,8 @@ Statement Alter():
(
(
+ LOOKAHEAD({ isAlterInstanceAhead() }) statement = AlterInstance()
+ |
LOOKAHEAD( ) statement = AlterForeignDataWrapper()
|
LOOKAHEAD({ isKeywordAhead("SERVER") }) statement = AlterServer()
@@ -27345,6 +27588,9 @@ Statement Create():
|
LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics()
|
+ LOOKAHEAD({ isSpatialReferenceSystemAhead() })
+ statement = CreateSpatialReferenceSystem(isUsingOrReplace)
+ |
LOOKAHEAD({ isMySqlCreateRoleAhead() }) statement = MySqlCreateRole()
{ requireAccessSyntax(!isUsingOrReplace, "CREATE ROLE does not accept OR REPLACE"); }
|
diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst
index d949f1d43..86da6b41f 100644
--- a/src/site/sphinx/index.rst
+++ b/src/site/sphinx/index.rst
@@ -180,6 +180,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad
- ``CREATE``/``ALTER``/``RENAME USER`` · ``CREATE ROLE`` · ``SET ROLE`` · ``SET DEFAULT ROLE`` · ``SET PASSWORD``
* - **Tablespaces**
- MySQL and PostgreSQL ``CREATE TABLESPACE`` · ``ALTER TABLESPACE``
+ * - **MySQL spatial reference systems and instance**
+ - ``CREATE``/``DROP SPATIAL REFERENCE SYSTEM`` · ``ALTER INSTANCE``
* - **PostgreSQL RLS**
- ``CREATE POLICY`` · ``ALTER TABLE … ENABLE``/``DISABLE``/``FORCE``/``NO FORCE ROW LEVEL SECURITY``
* - **Routine changes**
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index f514c41f9..adf1f3ff5 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -314,7 +314,9 @@ TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized
object types, and so are PostgreSQL's ACCESS METHOD, CAST, LANGUAGE, OPERATOR,
OPERATOR CLASS, OPERATOR FAMILY, TRANSFORM and TEXT SEARCH CONFIGURATION,
DICTIONARY, PARSER and TEMPLATE.
-``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is
+``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. MySQL's
+``DROP SPATIAL REFERENCE SYSTEM [IF EXISTS] srid`` has the object type
+``SPATIAL_REFERENCE_SYSTEM`` and its name is the SRID as written. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is
parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model.
.. code-block:: java
@@ -2393,6 +2395,54 @@ such as Oracle and NDB data file clauses, still parse as ``UnsupportedStatement`
``createTablespace`` and ``alterTablespace`` features, which the MySQL and PostgreSQL
capabilities enable; feature analysis reports ``MODIFIES_SCHEMA``.
+MySQL spatial reference systems and ALTER INSTANCE
+--------------------------------------------------
+
+MySQL `CREATE SPATIAL REFERENCE SYSTEM
+`_ parses to
+``CreateSpatialReferenceSystem``. ``isOrReplace()`` and ``isIfNotExists()`` exclude each
+other, ``getSrid()`` is the number as written (a ``LongValue``, or a ``HexValue`` for
+``0x...`` and ``X'...'``), and ``getAttributes()`` keeps the ``NAME``, ``DEFINITION``,
+``ORGANIZATION … IDENTIFIED BY`` and ``DESCRIPTION`` attributes in source order as
+``SpatialReferenceSystemAttribute`` values. ``getName()``, ``getDefinition()``,
+``getOrganization()``, ``getOrganizationId()`` and ``getDescription()`` return the first
+attribute of their kind. The parser follows MySQL's grammar, which takes the attributes in
+any order and any number: the server reports a repeated attribute, a missing ``NAME`` or
+``DEFINITION`` and an SRID above 4294967295 when it runs the statement, not as syntax
+errors. `DROP SPATIAL REFERENCE SYSTEM
+`_ parses to
+``Drop`` with the object type ``SPATIAL_REFERENCE_SYSTEM``.
+
+.. code-block:: java
+
+ CreateSpatialReferenceSystem srs = (CreateSpatialReferenceSystem) CCJSqlParserUtil.parse(
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' ORGANIZATION 'EPSG' "
+ + "IDENTIFIED BY 4120 DEFINITION 'GEOGCS[\"Greek\"]'");
+ srs.getSrid(); // 4120
+ srs.getName().getValue(); // Greek
+ srs.getOrganizationId(); // 4120
+
+MySQL `ALTER INSTANCE `_ parses
+to ``AlterInstance``. ``getAction()`` is ``ENABLE_INNODB_REDO_LOG``,
+``DISABLE_INNODB_REDO_LOG``, ``ROTATE_INNODB_MASTER_KEY``, ``ROTATE_BINLOG_MASTER_KEY``,
+``RELOAD_TLS`` or ``RELOAD_KEYRING``. For ``RELOAD TLS``, ``getChannel()`` is the identifier
+after ``FOR CHANNEL`` as written, or null, and ``isNoRollbackOnError()`` records
+``NO ROLLBACK ON ERROR``. MySQL reads ``INNODB``, ``BINLOG`` and ``REDO_LOG`` as
+identifiers, so they may be written in backticks, and the engine of ``ROTATE`` also as a
+string; the statement prints them as plain words. The grammar takes any identifier as the
+channel; the server accepts ``mysql_main`` and ``mysql_admin``.
+
+These statements are read with ``Dialect.MYSQL`` and without a dialect. With
+``Dialect.MYSQL``, a ``CREATE SPATIAL`` statement that is neither this one nor
+``CREATE SPATIAL INDEX``, and an ``ALTER INSTANCE`` with another action, are errors;
+without a dialect they stay ``UnsupportedStatement``, as in the other dialects, where
+``DROP SPATIAL REFERENCE SYSTEM`` is still rejected. ``REFERENCE``, ``INSTANCE``,
+``ROTATE``, ``RELOAD`` and the other words remain ordinary identifiers. Validators report
+the ``createSpatialReferenceSystem`` and ``alterInstance`` features, which the MySQL
+capabilities enable, and ``drop``. Feature analysis reports ``MODIFIES_SCHEMA`` for
+``CREATE SPATIAL REFERENCE SYSTEM`` and ``MODIFIES_SESSION`` for ``ALTER INSTANCE``, a
+server operation like ``ALTER SYSTEM``.
+
SQL Server identity inserts
---------------------------
diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
index ffbc801d6..efe772f7a 100644
--- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
+++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
@@ -139,6 +139,9 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException {
"CREATE RESOURCE GROUP rg TYPE = USER VCPU = 0-3, 5 THREAD_PRIORITY = 10 ENABLE",
"ALTER RESOURCE GROUP rg VCPU = 0 DISABLE FORCE",
"SET RESOURCE GROUP rg FOR 14, 0x1F", "DROP RESOURCE GROUP rg FORCE",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' ORGANIZATION 'EPSG' IDENTIFIED BY 0x1018 DEFINITION 'x' DESCRIPTION 'd'",
+ "DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin NO ROLLBACK ON ERROR",
"INSTALL PLUGIN p SONAME 'p.so'", "UNINSTALL PLUGIN p",
"INSTALL COMPONENT 'file://c', 'file://d' SET GLOBAL c.v = 1 + 2, w = ON",
"UNINSTALL COMPONENT 'file://c'",
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterInstanceTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterInstanceTest.java
new file mode 100644
index 000000000..7c1337c56
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterInstanceTest.java
@@ -0,0 +1,308 @@
+/*-
+ * #%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 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.assertNotNull;
+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.Set;
+import java.util.stream.Stream;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+import net.sf.jsqlparser.statement.StatementVisitorAdapter;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.alter.AlterInstance.Action;
+import net.sf.jsqlparser.statement.lock.BackupLockStatement;
+import net.sf.jsqlparser.statement.select.Select;
+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.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** MySQL ALTER INSTANCE. */
+class AlterInstanceTest {
+ private static final Dialect[] MYSQL_OR_NONE = {Dialect.MYSQL, null};
+
+ private static Statement parse(String sql, Dialect dialect) throws JSQLParserException {
+ return dialect == null ? CCJSqlParserUtil.parse(sql)
+ : CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect));
+ }
+
+ private static void assertSpans(String expected, String source, ASTNodeAccess access) {
+ Node node = access.getASTNode();
+ assertNotNull(node, expected);
+ assertEquals(expected, source.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1));
+ }
+
+ /**
+ * Parses {@code sql}, which must print as {@code expected}, and checks the statement span, the
+ * deparser, the reparse, the feature analysis and the table finder.
+ */
+ private static AlterInstance roundTrip(String sql, String expected, Dialect dialect)
+ throws Exception {
+ AlterInstance statement = assertInstanceOf(AlterInstance.class, parse(sql, dialect));
+ assertEquals(expected, statement.toString());
+ assertSpans(sql, sql, statement);
+ StringBuilder output = new StringBuilder();
+ statement.accept(new StatementDeParser(output), null);
+ assertEquals(expected, output.toString());
+ Statement reparsed = parse(output.toString(), dialect);
+ assertInstanceOf(AlterInstance.class, reparsed);
+ assertEquals(expected, reparsed.toString());
+ assertEquals(Set.of(StmtFeature.MODIFIES_SESSION), statement.getFeatures().getCertain());
+ assertTrue(statement.getFeatures().getUncertain().isEmpty());
+ assertTrue(new TablesNamesFinder<>().getTables(statement).isEmpty());
+ return statement;
+ }
+
+ static Stream actions() {
+ return Stream.of(
+ Arguments.of("ALTER INSTANCE ENABLE INNODB REDO_LOG",
+ Action.ENABLE_INNODB_REDO_LOG, null, false),
+ Arguments.of("ALTER INSTANCE DISABLE INNODB REDO_LOG",
+ Action.DISABLE_INNODB_REDO_LOG, null, false),
+ Arguments.of("ALTER INSTANCE ROTATE INNODB MASTER KEY",
+ Action.ROTATE_INNODB_MASTER_KEY, null, false),
+ Arguments.of("ALTER INSTANCE ROTATE BINLOG MASTER KEY",
+ Action.ROTATE_BINLOG_MASTER_KEY, null, false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS", Action.RELOAD_TLS, null, false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS NO ROLLBACK ON ERROR", Action.RELOAD_TLS,
+ null, true),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_main",
+ Action.RELOAD_TLS, "mysql_main", false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin",
+ Action.RELOAD_TLS, "mysql_admin", false),
+ Arguments.of(
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin NO ROLLBACK ON ERROR",
+ Action.RELOAD_TLS, "mysql_admin", true),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL `mysql_main`",
+ Action.RELOAD_TLS, "`mysql_main`", false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL MYSQL_MAIN",
+ Action.RELOAD_TLS, "MYSQL_MAIN", false),
+ // the grammar takes any identifier; the server checks the name when it runs
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL other_channel",
+ Action.RELOAD_TLS, "other_channel", false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL channel", Action.RELOAD_TLS,
+ "channel", false),
+ Arguments.of("ALTER INSTANCE RELOAD TLS FOR CHANNEL tls NO ROLLBACK ON ERROR",
+ Action.RELOAD_TLS, "tls", true),
+ Arguments.of("ALTER INSTANCE RELOAD KEYRING", Action.RELOAD_KEYRING, null, false));
+ }
+
+ @ParameterizedTest
+ @MethodSource("actions")
+ void everyActionIsStructured(String sql, Action action, String channel,
+ boolean noRollbackOnError) throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ AlterInstance statement = roundTrip(sql, sql, dialect);
+ assertEquals(action, statement.getAction());
+ assertEquals(channel, statement.getChannel());
+ assertEquals(noRollbackOnError, statement.isNoRollbackOnError());
+ assertTrue(sql.startsWith("ALTER INSTANCE " + action.getSql()));
+ }
+ }
+
+ static Stream spellings() {
+ return Stream.of(
+ Arguments.of("alter instance rotate innodb master key",
+ "ALTER INSTANCE ROTATE INNODB MASTER KEY"),
+ Arguments.of("ALTER INSTANCE ROTATE `INNODB` MASTER KEY",
+ "ALTER INSTANCE ROTATE INNODB MASTER KEY"),
+ Arguments.of("ALTER INSTANCE ROTATE `binlog` MASTER KEY",
+ "ALTER INSTANCE ROTATE BINLOG MASTER KEY"),
+ Arguments.of("ALTER INSTANCE ROTATE 'BINLOG' MASTER KEY",
+ "ALTER INSTANCE ROTATE BINLOG MASTER KEY"),
+ Arguments.of("ALTER INSTANCE ROTATE \"binlog\" MASTER KEY",
+ "ALTER INSTANCE ROTATE BINLOG MASTER KEY"),
+ Arguments.of("ALTER INSTANCE DISABLE innodb redo_log",
+ "ALTER INSTANCE DISABLE INNODB REDO_LOG"),
+ Arguments.of("ALTER INSTANCE DISABLE `INNODB` `REDO_LOG`",
+ "ALTER INSTANCE DISABLE INNODB REDO_LOG"),
+ Arguments.of("ALTER INSTANCE ENABLE `innodb` `redo_log`",
+ "ALTER INSTANCE ENABLE INNODB REDO_LOG"),
+ Arguments.of("alter instance reload tls for channel mysql_main "
+ + "no rollback on error",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_main NO ROLLBACK ON ERROR"),
+ Arguments.of("alter instance reload keyring", "ALTER INSTANCE RELOAD KEYRING"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("spellings")
+ void quotedAndLowerCaseWordsPrintAsPlainKeywords(String sql, String expected)
+ throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ roundTrip(sql, expected, dialect);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER INSTANCE ROTATE MASTER KEY",
+ "ALTER INSTANCE ROTATE MYISAM MASTER KEY",
+ "ALTER INSTANCE ROTATE REDO_LOG MASTER KEY",
+ "ALTER INSTANCE ROTATE 'x' MASTER KEY",
+ "ALTER INSTANCE ROTATE N'INNODB' MASTER KEY",
+ "ALTER INSTANCE ROTATE X'494E4E4F4442' MASTER KEY",
+ "ALTER INSTANCE ROTATE INNODB MASTER",
+ "ALTER INSTANCE ROTATE INNODB KEY",
+ "ALTER INSTANCE ROTATE INNODB MASTER KEYS",
+ "ALTER INSTANCE ROTATE INNODB MASTER KEY x",
+ "ALTER INSTANCE ROTATE INNODB, BINLOG MASTER KEY",
+ "ALTER INSTANCE ROTATE INNODB MASTER KEY, ROTATE BINLOG MASTER KEY",
+ "ALTER INSTANCE RELOAD",
+ "ALTER INSTANCE RELOAD SSL",
+ "ALTER INSTANCE RELOAD TLS ROLLBACK ON ERROR",
+ "ALTER INSTANCE RELOAD TLS NO ROLLBACK",
+ "ALTER INSTANCE RELOAD TLS NO ROLLBACK ON ERROR x",
+ "ALTER INSTANCE RELOAD TLS NO ROLLBACK ON ERROR FOR CHANNEL mysql_main",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL 'mysql_main'",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL \"mysql_main\"",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL 1",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL d.mysql_main",
+ "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_main, mysql_admin",
+ "ALTER INSTANCE RELOAD TLS FOR mysql_main",
+ "ALTER INSTANCE RELOAD TLS CHANNEL mysql_main",
+ "ALTER INSTANCE RELOAD TLS, RELOAD KEYRING",
+ "ALTER INSTANCE RELOAD KEYRING NO ROLLBACK ON ERROR",
+ "ALTER INSTANCE RELOAD KEYRING x",
+ "ALTER INSTANCE ENABLE INNODB",
+ "ALTER INSTANCE ENABLE REDO_LOG",
+ "ALTER INSTANCE ENABLE INNODB REDO LOG",
+ "ALTER INSTANCE ENABLE MYISAM REDO_LOG",
+ "ALTER INSTANCE ENABLE BINLOG REDO_LOG",
+ "ALTER INSTANCE ENABLE INNODB UNDO_LOG",
+ "ALTER INSTANCE ENABLE 'INNODB' REDO_LOG",
+ "ALTER INSTANCE ENABLE \"INNODB\" REDO_LOG",
+ "ALTER INSTANCE ENABLE INNODB 'REDO_LOG'",
+ "ALTER INSTANCE ENABLE INNODB REDO_LOG NO ROLLBACK ON ERROR",
+ "ALTER INSTANCE DISABLE INNODB REDO_LOG x",
+ "ALTER INSTANCE ENABLE INNODB REDO_LOG, DISABLE INNODB REDO_LOG"})
+ void rejectsWhatTheServerGrammarRejects(String sql) {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ assertThrows(JSQLParserException.class, () -> parse(sql, dialect), sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ALTER INSTANCE", "ALTER INSTANCE FLUSH TLS",
+ "ALTER INSTANCE mysql ENABLE INNODB REDO_LOG"})
+ void mySqlDialectRejectsAnUnknownAction(String sql) throws Exception {
+ assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL), sql);
+ // without a dialect only a known action selects the statement
+ assertInstanceOf(UnsupportedStatement.class, parse(sql, null));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Dialect.class, names = {"MARIADB", "POSTGRESQL", "ORACLE", "SQLSERVER"})
+ void otherDialectsKeepTheirReading(Dialect dialect) throws Exception {
+ assertInstanceOf(UnsupportedStatement.class,
+ parse("ALTER INSTANCE ROTATE INNODB MASTER KEY", dialect));
+ assertInstanceOf(UnsupportedStatement.class, parse("ALTER INSTANCE RELOAD SSL", dialect));
+ }
+
+ @Test
+ void otherAlterStatementsAndInstanceNamesKeepTheirReading() throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ assertInstanceOf(Alter.class, parse("ALTER TABLE instance ADD COLUMN c INT", dialect));
+ assertInstanceOf(Alter.class,
+ parse("ALTER TABLE t ENABLE KEYS", dialect));
+ assertInstanceOf(Select.class, parse("SELECT instance FROM instance", dialect));
+ assertInstanceOf(BackupLockStatement.class,
+ parse("LOCK INSTANCE FOR BACKUP", dialect));
+ }
+ }
+
+ @Test
+ void statementsEndAtTheBoundary() throws Exception {
+ String script = "ALTER INSTANCE RELOAD TLS;\n"
+ + "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin;\n"
+ + "ALTER INSTANCE DISABLE INNODB REDO_LOG;\n"
+ + "SELECT 1;\n"
+ + "ALTER INSTANCE ROTATE BINLOG MASTER KEY";
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ Statements statements = dialect == null ? CCJSqlParserUtil.parseStatements(script)
+ : CCJSqlParserUtil.parseStatements(script, p -> p.withDialect(dialect));
+ assertEquals(5, statements.size());
+ assertEquals(Action.RELOAD_TLS, ((AlterInstance) statements.get(0)).getAction());
+ assertEquals("mysql_admin", ((AlterInstance) statements.get(1)).getChannel());
+ assertEquals(Action.DISABLE_INNODB_REDO_LOG,
+ ((AlterInstance) statements.get(2)).getAction());
+ assertInstanceOf(Select.class, statements.get(3));
+ assertEquals(Action.ROTATE_BINLOG_MASTER_KEY,
+ ((AlterInstance) statements.get(4)).getAction());
+ assertSpans("ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin", script,
+ (ASTNodeAccess) statements.get(1));
+ }
+ assertEquals("ALTER INSTANCE RELOAD KEYRING",
+ parse("ALTER INSTANCE RELOAD KEYRING;", Dialect.MYSQL).toString());
+ }
+
+ @Test
+ void canBeBuiltAndEdited() {
+ AlterInstance statement = new AlterInstance(Action.RELOAD_TLS).withChannel("mysql_admin")
+ .withNoRollbackOnError(true);
+ assertEquals("ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin NO ROLLBACK ON ERROR",
+ statement.toString());
+ assertThrows(IllegalArgumentException.class,
+ () -> statement.setAction(Action.RELOAD_KEYRING));
+ statement.setChannel(null);
+ assertThrows(IllegalArgumentException.class,
+ () -> statement.setAction(Action.RELOAD_KEYRING));
+ statement.setNoRollbackOnError(false);
+ assertEquals("ALTER INSTANCE RELOAD KEYRING",
+ statement.withAction(Action.RELOAD_KEYRING).toString());
+ assertThrows(IllegalArgumentException.class, () -> statement.setChannel("mysql_main"));
+ assertThrows(IllegalArgumentException.class, () -> statement.setNoRollbackOnError(true));
+ statement.setChannel(null);
+ statement.setNoRollbackOnError(false);
+ assertNull(statement.getChannel());
+ assertFalse(statement.isNoRollbackOnError());
+ assertThrows(NullPointerException.class, () -> statement.setAction(null));
+ assertThrows(NullPointerException.class, () -> new AlterInstance(null));
+ assertEquals("ALTER INSTANCE ENABLE INNODB REDO_LOG",
+ new AlterInstance(Action.ENABLE_INNODB_REDO_LOG).toString());
+ }
+
+ @Test
+ void visitorsReachTheStatement() throws Exception {
+ AlterInstance statement =
+ (AlterInstance) parse("ALTER INSTANCE RELOAD TLS", Dialect.MYSQL);
+ assertNull(statement.accept(new StatementVisitorAdapter(), null));
+
+ StringBuilder seen = new StringBuilder();
+ StatementVisitor visitor = new StatementVisitorAdapter() {
+ @Override
+ public String visit(AlterInstance visited, S context) {
+ seen.append(visited.getAction()).append(context);
+ return "visited";
+ }
+ };
+ assertEquals("visited", statement.accept(visitor, "!"));
+ visitor.visit(statement);
+ assertEquals("RELOAD_TLS!RELOAD_TLSnull", seen.toString());
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemTest.java b/src/test/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemTest.java
new file mode 100644
index 000000000..c0e10347b
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/create/spatial/SpatialReferenceSystemTest.java
@@ -0,0 +1,476 @@
+/*-
+ * #%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.spatial;
+
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.HexValue;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+import net.sf.jsqlparser.statement.StatementVisitorAdapter;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.alter.Alter;
+import net.sf.jsqlparser.statement.create.index.CreateIndex;
+import net.sf.jsqlparser.statement.create.spatial.SpatialReferenceSystemAttribute.Kind;
+import net.sf.jsqlparser.statement.drop.Drop;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.test.TestUtils;
+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.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** MySQL CREATE SPATIAL REFERENCE SYSTEM and DROP SPATIAL REFERENCE SYSTEM. */
+class SpatialReferenceSystemTest {
+ private static final Dialect[] MYSQL_OR_NONE = {Dialect.MYSQL, null};
+ private static final String FULL = "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' "
+ + "ORGANIZATION 'EPSG' IDENTIFIED BY 4120 DEFINITION 'GEOGCS[\"Greek\"]' "
+ + "DESCRIPTION 'd'";
+
+ private static Statement parse(String sql, Dialect dialect) throws JSQLParserException {
+ return dialect == null ? CCJSqlParserUtil.parse(sql)
+ : CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect));
+ }
+
+ private static void assertSpans(String expected, String source, ASTNodeAccess access) {
+ Node node = access.getASTNode();
+ assertNotNull(node, expected);
+ assertEquals(expected, source.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1));
+ }
+
+ /**
+ * Parses {@code sql}, which must print as written, and checks the statement span, the deparser,
+ * the reparse, the feature analysis and the table finder.
+ */
+ private static Statement roundTrip(String sql, Dialect dialect, StmtFeature... features)
+ throws Exception {
+ Statement statement = parse(sql, dialect);
+ assertEquals(sql, statement.toString());
+ assertSpans(sql, sql, (ASTNodeAccess) statement);
+ StringBuilder output = new StringBuilder();
+ statement.accept(new StatementDeParser(output), null);
+ assertEquals(sql, output.toString());
+ Statement reparsed = parse(output.toString(), dialect);
+ assertEquals(statement.getClass(), reparsed.getClass());
+ assertEquals(sql, reparsed.toString());
+ assertEquals(Set.of(features), statement.getFeatures().getCertain());
+ assertTrue(statement.getFeatures().getUncertain().isEmpty());
+ assertTrue(new TablesNamesFinder<>().getTables(statement).isEmpty());
+ return statement;
+ }
+
+ private static CreateSpatialReferenceSystem create(String sql, Dialect dialect)
+ throws Exception {
+ return assertInstanceOf(CreateSpatialReferenceSystem.class,
+ roundTrip(sql, dialect, StmtFeature.MODIFIES_SCHEMA));
+ }
+
+ private static Drop drop(String sql, Dialect dialect) throws Exception {
+ return assertInstanceOf(Drop.class,
+ roundTrip(sql, dialect, StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA));
+ }
+
+ // ---- CREATE --------------------------------------------------------------------------------
+
+ @Test
+ void createKeepsTheSridAndEveryAttribute() throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ CreateSpatialReferenceSystem statement = create(FULL, dialect);
+ assertFalse(statement.isOrReplace());
+ assertFalse(statement.isIfNotExists());
+ assertEquals(4120L, assertInstanceOf(LongValue.class, statement.getSrid()).getValue());
+ assertSpans("4120", FULL, (ASTNodeAccess) statement.getSrid());
+ assertEquals("Greek", statement.getName().getValue());
+ assertEquals("EPSG", statement.getOrganization().getValue());
+ assertEquals(4120L, assertInstanceOf(LongValue.class, statement.getOrganizationId())
+ .getValue());
+ assertEquals("GEOGCS[\"Greek\"]", statement.getDefinition().getValue());
+ assertEquals("d", statement.getDescription().getValue());
+
+ List attributes = statement.getAttributes();
+ assertEquals(Arrays.asList(Kind.NAME, Kind.ORGANIZATION, Kind.DEFINITION,
+ Kind.DESCRIPTION),
+ Arrays.asList(attributes.get(0).getKind(), attributes.get(1).getKind(),
+ attributes.get(2).getKind(), attributes.get(3).getKind()));
+ assertEquals(4, attributes.size());
+ assertSpans("NAME 'Greek'", FULL, attributes.get(0));
+ assertSpans("'Greek'", FULL, attributes.get(0).getValue());
+ assertNull(attributes.get(0).getOrganizationId());
+ assertSpans("ORGANIZATION 'EPSG' IDENTIFIED BY 4120", FULL, attributes.get(1));
+ assertSpans("'EPSG'", FULL, attributes.get(1).getValue());
+ assertSame(statement.getOrganizationId(), attributes.get(1).getOrganizationId());
+ assertSpans("DEFINITION 'GEOGCS[\"Greek\"]'", FULL, attributes.get(2));
+ assertSpans("DESCRIPTION 'd'", FULL, attributes.get(3));
+ assertEquals("DESCRIPTION 'd'", attributes.get(3).toString());
+ assertSame(attributes.get(2), statement.getAttribute(Kind.DEFINITION));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 4120 NAME 'Greek' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 DEFINITION 'x' NAME 'Greek'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 DESCRIPTION 'd' ORGANIZATION 'o' IDENTIFIED BY 1 "
+ + "DEFINITION 'x' NAME 'n'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME \"n\" DEFINITION \"x\"",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' ORGANIZATION \"o\" "
+ + "IDENTIFIED BY 1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'it''s' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME '' DEFINITION ''",
+ "CREATE SPATIAL REFERENCE SYSTEM 04120 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4294967296 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' ORGANIZATION 'o' "
+ + "IDENTIFIED BY 4294967296",
+ // the server reports a missing or repeated attribute itself, not as a syntax error
+ "CREATE SPATIAL REFERENCE SYSTEM 4120",
+ "CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 4120",
+ "CREATE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 4120",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 DESCRIPTION 'd'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 ORGANIZATION 'o' IDENTIFIED BY 0",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'a' NAME 'b' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'a' DEFINITION 'x' DEFINITION 'y'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'a' DEFINITION 'x' DESCRIPTION 'd' "
+ + "DESCRIPTION 'e'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'a' DEFINITION 'x' ORGANIZATION 'o' "
+ + "IDENTIFIED BY 1 ORGANIZATION 'p' IDENTIFIED BY 2"})
+ void createTakesTheAttributesInAnyOrderAndNumber(String sql) throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ CreateSpatialReferenceSystem statement = create(sql, dialect);
+ assertEquals(sql.startsWith("CREATE OR REPLACE"), statement.isOrReplace());
+ assertEquals(sql.contains("IF NOT EXISTS"), statement.isIfNotExists());
+ }
+ }
+
+ @Test
+ void repeatedAttributesStayInTheListAndTheGettersReturnTheFirst() throws Exception {
+ CreateSpatialReferenceSystem statement = create(
+ "CREATE SPATIAL REFERENCE SYSTEM 1 NAME 'a' ORGANIZATION 'o' IDENTIFIED BY 1 "
+ + "NAME 'b' ORGANIZATION 'p' IDENTIFIED BY 2",
+ Dialect.MYSQL);
+ assertEquals(4, statement.getAttributes().size());
+ assertEquals("a", statement.getName().getValue());
+ assertEquals("o", statement.getOrganization().getValue());
+ assertEquals("1", statement.getOrganizationId().toString());
+ assertNull(statement.getDefinition());
+ assertNull(statement.getDescription());
+
+ CreateSpatialReferenceSystem bare =
+ create("CREATE SPATIAL REFERENCE SYSTEM 1", Dialect.MYSQL);
+ assertTrue(bare.getAttributes().isEmpty());
+ assertNull(bare.getName());
+ assertNull(bare.getOrganization());
+ assertNull(bare.getOrganizationId());
+ assertNull(bare.getAttribute(Kind.NAME));
+ }
+
+ @Test
+ void sridAndOrganizationIdAreUnsignedIntegersOrHexadecimalNumbers() throws Exception {
+ String sql = "CREATE SPATIAL REFERENCE SYSTEM 0x3D0978 NAME 'n' DEFINITION 'x' "
+ + "ORGANIZATION 'o' IDENTIFIED BY X'10'";
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ CreateSpatialReferenceSystem statement = create(sql, dialect);
+ assertEquals("0x3D0978",
+ assertInstanceOf(HexValue.class, statement.getSrid()).getValue());
+ assertSpans("0x3D0978", sql, (ASTNodeAccess) statement.getSrid());
+ assertEquals("X'10'",
+ assertInstanceOf(HexValue.class, statement.getOrganizationId()).getValue());
+ assertSpans("X'10'", sql, (ASTNodeAccess) statement.getOrganizationId());
+ }
+ create("CREATE SPATIAL REFERENCE SYSTEM x'3D0978' NAME 'n'", Dialect.MYSQL);
+ create("CREATE SPATIAL REFERENCE SYSTEM X'' NAME 'n'", Dialect.MYSQL);
+ create("CREATE SPATIAL REFERENCE SYSTEM 0x3d0978 NAME 'n'", Dialect.MYSQL);
+
+ // the grammar takes any 64-bit unsigned integer; the server then checks the SRID range
+ String largest = "CREATE SPATIAL REFERENCE SYSTEM 18446744073709551615 NAME 'n'";
+ assertEquals(new BigInteger("18446744073709551615"),
+ assertInstanceOf(LongValue.class, create(largest, Dialect.MYSQL).getSrid())
+ .getBigIntegerValue());
+ create("CREATE SPATIAL REFERENCE SYSTEM 000000000000000000000004000120 NAME 'n'",
+ Dialect.MYSQL);
+ }
+
+ @Test
+ void keywordsAreCaseInsensitiveAndDoubleQuotedStringsAreStrings() throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ Statement statement = parse("create or replace spatial reference system 1 name 'n' "
+ + "definition 'x' organization 'o' identified by 2 description 'd'", dialect);
+ assertEquals("CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 1 NAME 'n' DEFINITION 'x' "
+ + "ORGANIZATION 'o' IDENTIFIED BY 2 DESCRIPTION 'd'", statement.toString());
+
+ CreateSpatialReferenceSystem quoted = create(
+ "CREATE SPATIAL REFERENCE SYSTEM 1 NAME \"a b\" DEFINITION 'x'", dialect);
+ assertEquals("a b", quoted.getName().getValue());
+ assertEquals("\"", quoted.getName().getQuoteStr());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "CREATE OR REPLACE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 4120 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM '4120' NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM `4120` NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM srs1 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM -1 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM +1 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM (1) NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 1.5 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 1e3 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 1_000 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 18446744073709551616 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 0X3D0978 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM X'1' NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM B'1' NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 0b101 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120, 4121 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM IF EXISTS 4120 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 IF NOT EXISTS NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME n DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 1 DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME N'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME _utf8'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME _utf8mb4'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME X'41' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'a' 'b' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME ('n') DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME = 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n', DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' ORGANIZATION 'o'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION 'o' IDENTIFIED BY 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION 'o' IDENTIFIED BY -1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION 'o' IDENTIFIED BY 1.5",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION 'o' IDENTIFIED 1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION 'o' IDENTIFIED WITH 1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' ORGANIZATION IDENTIFIED BY 1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' IDENTIFIED BY 1",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' COMMENT 'c'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 SRID 1 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x' x"})
+ void createRejectsWhatTheServerGrammarRejects(String sql) {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ assertThrows(JSQLParserException.class, () -> parse(sql, dialect), sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"CREATE SPATIAL REFERENCE 4120 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL SYSTEM 4120 NAME 'n' DEFINITION 'x'",
+ "CREATE SPATIAL REFERENCE SYSTEMS 4120 NAME 'n' DEFINITION 'x'"})
+ void mySqlDialectRejectsAMisspelledObjectType(String sql) throws Exception {
+ assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL), sql);
+ // without a dialect only the exact words select the statement
+ assertInstanceOf(UnsupportedStatement.class, parse(sql, null));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Dialect.class, names = {"MARIADB", "POSTGRESQL", "ORACLE", "SQLSERVER"})
+ void otherDialectsKeepTheirReading(Dialect dialect) throws Exception {
+ assertInstanceOf(UnsupportedStatement.class, parse(FULL, dialect));
+ assertThrows(JSQLParserException.class,
+ () -> parse("DROP SPATIAL REFERENCE SYSTEM 4120", dialect));
+ }
+
+ @Test
+ void spatialIndexStatementsKeepTheirReading() throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ assertInstanceOf(CreateIndex.class, parse("CREATE SPATIAL INDEX i ON t (g)", dialect));
+ assertInstanceOf(Alter.class, parse("ALTER TABLE t ADD SPATIAL INDEX i (g)", dialect));
+ assertEquals(Drop.ObjectType.INDEX,
+ ((Drop) parse("DROP INDEX i ON t", dialect)).getObjectType());
+ }
+ TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i ON t (g)", true);
+ }
+
+ @Test
+ void createCanBeBuiltAndEdited() {
+ CreateSpatialReferenceSystem statement =
+ new CreateSpatialReferenceSystem(new LongValue(4120))
+ .addAttributes(
+ new SpatialReferenceSystemAttribute(Kind.NAME,
+ new StringValue("'Greek'")),
+ new SpatialReferenceSystemAttribute(new StringValue("'EPSG'"),
+ new LongValue(4120)))
+ .withIfNotExists(true);
+ assertEquals("CREATE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 4120 NAME 'Greek' "
+ + "ORGANIZATION 'EPSG' IDENTIFIED BY 4120", statement.toString());
+ assertThrows(IllegalArgumentException.class, () -> statement.setOrReplace(true));
+
+ statement.setIfNotExists(false);
+ statement.withOrReplace(true).withSrid(new HexValue("0x10"));
+ assertThrows(IllegalArgumentException.class, () -> statement.setIfNotExists(true));
+ statement.getAttributes().get(0).withValue(new StringValue("'n'"));
+ statement.getAttributes().get(1).withOrganizationId(new LongValue(7));
+ assertEquals("CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 0x10 NAME 'n' "
+ + "ORGANIZATION 'EPSG' IDENTIFIED BY 7", statement.toString());
+
+ statement.withAttributes(Collections.singletonList(
+ new SpatialReferenceSystemAttribute(Kind.DEFINITION, new StringValue("'x'"))));
+ assertEquals("CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 0x10 DEFINITION 'x'",
+ statement.toString());
+ statement.setAttributes(null);
+ assertEquals("CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 0x10", statement.toString());
+
+ assertThrows(NullPointerException.class, () -> new CreateSpatialReferenceSystem(null));
+ assertThrows(IllegalArgumentException.class,
+ () -> new SpatialReferenceSystemAttribute(Kind.ORGANIZATION,
+ new StringValue("'o'")));
+ assertThrows(IllegalArgumentException.class,
+ () -> new SpatialReferenceSystemAttribute(Kind.NAME, new StringValue("'n'"))
+ .setOrganizationId(new LongValue(1)));
+ assertThrows(NullPointerException.class,
+ () -> new SpatialReferenceSystemAttribute(Kind.NAME, null));
+ assertThrows(NullPointerException.class,
+ () -> new SpatialReferenceSystemAttribute(new StringValue("'o'"), null));
+ }
+
+ // ---- DROP ----------------------------------------------------------------------------------
+
+ @Test
+ void dropNamesTheSrid() throws Exception {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ String sql = "DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120";
+ Drop drop = drop(sql, dialect);
+ assertEquals(Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM, drop.getObjectType());
+ assertEquals("SPATIAL REFERENCE SYSTEM", drop.getType());
+ assertTrue(drop.isIfExists());
+ assertEquals(1, drop.getNames().size());
+ assertEquals("4120", drop.getName().getName());
+ assertSpans("4120", sql, drop.getName());
+ assertNull(drop.getParameters());
+ assertNull(drop.getBehavior());
+
+ assertFalse(drop("DROP SPATIAL REFERENCE SYSTEM 4120", dialect).isIfExists());
+ assertEquals("0x3D0978",
+ drop("DROP SPATIAL REFERENCE SYSTEM 0x3D0978", dialect).getName().getName());
+ assertEquals("X'10'",
+ drop("DROP SPATIAL REFERENCE SYSTEM X'10'", dialect).getName().getName());
+ drop("DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4294967296", dialect);
+ assertEquals("DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120",
+ parse("drop spatial reference system if exists 4120", dialect).toString());
+ }
+ }
+
+ @Test
+ void dropCanBeBuilt() {
+ Drop drop = new Drop().withObjectType(Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM)
+ .withIfExists(true).withName(new Table("4120", false));
+ assertEquals("DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120", drop.toString());
+ assertEquals(Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM,
+ new Drop().withType("spatial reference system").getObjectType());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"DROP SPATIAL REFERENCE SYSTEM",
+ "DROP SPATIAL REFERENCE SYSTEM IF EXISTS",
+ "DROP SPATIAL REFERENCE SYSTEM IF NOT EXISTS 4120",
+ "DROP SPATIAL REFERENCE SYSTEM 4120, 4121",
+ "DROP SPATIAL REFERENCE SYSTEM '4120'",
+ "DROP SPATIAL REFERENCE SYSTEM `4120`",
+ "DROP SPATIAL REFERENCE SYSTEM srs1",
+ "DROP SPATIAL REFERENCE SYSTEM -1",
+ "DROP SPATIAL REFERENCE SYSTEM 1.5",
+ "DROP SPATIAL REFERENCE SYSTEM X'1'",
+ "DROP SPATIAL REFERENCE SYSTEM 0X10",
+ "DROP SPATIAL REFERENCE SYSTEM 18446744073709551616",
+ "DROP SPATIAL REFERENCE SYSTEM 4120 CASCADE",
+ "DROP SPATIAL REFERENCE SYSTEM 4120 RESTRICT",
+ "DROP SPATIAL REFERENCE SYSTEM 4120 IF EXISTS",
+ "DROP SPATIAL REFERENCE SYSTEM 4120 x",
+ "DROP SPATIAL REFERENCE SYSTEM 4120 ON t",
+ "DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120 NAME 'n'",
+ "DROP SPATIAL REFERENCE 4120",
+ "DROP SPATIAL SYSTEM 4120",
+ "DROP SPATIAL 4120",
+ "DROP TEMPORARY SPATIAL REFERENCE SYSTEM 4120",
+ "DROP MATERIALIZED SPATIAL REFERENCE SYSTEM 4120"})
+ void dropRejectsWhatTheServerGrammarRejects(String sql) {
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ assertThrows(JSQLParserException.class, () -> parse(sql, dialect), sql);
+ }
+ }
+
+ // ---- statement boundaries and visitors -----------------------------------------------------
+
+ @Test
+ void statementsEndAtTheBoundary() throws Exception {
+ String script = "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x';\n"
+ + "DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120;\n"
+ + "CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 4120;\n"
+ + "SELECT 1;\n"
+ + "CREATE SPATIAL INDEX i ON t (g);\n"
+ + "DROP SPATIAL REFERENCE SYSTEM 4120";
+ for (Dialect dialect : MYSQL_OR_NONE) {
+ Statements statements = dialect == null ? CCJSqlParserUtil.parseStatements(script)
+ : CCJSqlParserUtil.parseStatements(script, p -> p.withDialect(dialect));
+ assertEquals(6, statements.size());
+ assertInstanceOf(CreateSpatialReferenceSystem.class, statements.get(0));
+ assertEquals(Drop.ObjectType.SPATIAL_REFERENCE_SYSTEM,
+ ((Drop) statements.get(1)).getObjectType());
+ assertTrue(((CreateSpatialReferenceSystem) statements.get(2)).getAttributes()
+ .isEmpty());
+ assertInstanceOf(Select.class, statements.get(3));
+ assertInstanceOf(CreateIndex.class, statements.get(4));
+ assertInstanceOf(Drop.class, statements.get(5));
+ assertSpans("CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'n' DEFINITION 'x'", script,
+ (ASTNodeAccess) statements.get(0));
+ assertSpans("DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120", script,
+ (ASTNodeAccess) statements.get(1));
+ }
+ assertEquals("CREATE SPATIAL REFERENCE SYSTEM 1 NAME 'n'",
+ parse("CREATE SPATIAL REFERENCE SYSTEM 1 NAME 'n';", Dialect.MYSQL).toString());
+ }
+
+ @Test
+ void visitorsReachTheStatement() throws Exception {
+ CreateSpatialReferenceSystem statement =
+ (CreateSpatialReferenceSystem) parse(FULL, Dialect.MYSQL);
+ assertNull(statement.accept(new StatementVisitorAdapter(), null));
+
+ StringBuilder seen = new StringBuilder();
+ StatementVisitor visitor = new StatementVisitorAdapter() {
+ @Override
+ public String visit(CreateSpatialReferenceSystem visited, S context) {
+ seen.append(visited.getSrid()).append(context);
+ return "visited";
+ }
+ };
+ assertEquals("visited", statement.accept(visitor, "!"));
+ visitor.visit(statement);
+ assertEquals("4120!4120null", seen.toString());
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlInstanceStatementsValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlInstanceStatementsValidatorTest.java
new file mode 100644
index 000000000..935746fad
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlInstanceStatementsValidatorTest.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.util.validation.validator;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.util.validation.ValidationTestAsserts;
+import net.sf.jsqlparser.util.validation.feature.DatabaseType;
+import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
+import net.sf.jsqlparser.util.validation.feature.MariaDbVersion;
+import net.sf.jsqlparser.util.validation.feature.MySqlVersion;
+import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion;
+import org.junit.jupiter.api.Test;
+
+/** MySQL CREATE and DROP SPATIAL REFERENCE SYSTEM and ALTER INSTANCE. */
+public class MySqlInstanceStatementsValidatorTest extends ValidationTestAsserts {
+ private static final String CREATE =
+ "CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' DEFINITION 'x'";
+ private static final String ALTER = "ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_main";
+
+ @Test
+ public void testAllowedForMySql() throws JSQLParserException {
+ for (String sql : new String[] {CREATE, ALTER,
+ "DROP SPATIAL REFERENCE SYSTEM IF EXISTS 4120"}) {
+ validateNoErrors(sql, 1, MySqlVersion.V8_0, DatabaseType.MYSQL);
+ }
+ }
+
+ @Test
+ public void testNotAllowedForMariaDbAndPostgresql() throws JSQLParserException {
+ for (String sql : new String[] {CREATE, ALTER}) {
+ validate(sql, 1, 1, MariaDbVersion.V10_5_4);
+ validate(sql, 1, 1, PostgresqlVersion.V14);
+ }
+ }
+
+ @Test
+ public void testFeatureNotAllowed() throws JSQLParserException {
+ validateNotAllowed(CREATE, 1, 1, FeaturesAllowed.SELECT,
+ Feature.createSpatialReferenceSystem);
+ validateNotAllowed(ALTER, 1, 1, FeaturesAllowed.SELECT, Feature.alterInstance);
+ validateNotAllowed("DROP SPATIAL REFERENCE SYSTEM 4120", 1, 1, FeaturesAllowed.SELECT,
+ Feature.drop);
+ }
+}