diff --git a/README.md b/README.md index e7a8db4521..dcc7744a27 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` | | **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` | +| **MySQL replication** | `CHANGE REPLICATION SOURCE TO` · `CHANGE REPLICATION FILTER` · `START`/`STOP REPLICA` · `RESET REPLICA` · `RESET BINARY LOGS AND GTIDS` · `PURGE BINARY LOGS` | | **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` | | **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | 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 447245f4aa..7af53cc489 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -562,6 +562,12 @@ public enum Feature { * @see net.sf.jsqlparser.statement.PurgeStatement */ purge, + /** + * MySQL/MariaDB "PURGE BINARY LOGS" and MariaDB "PURGE MASTER LOGS" statements are allowed + * + * @see net.sf.jsqlparser.statement.PurgeStatement + */ + purgeBinaryLogs, /** * PostgreSQL "REASSIGN OWNED BY ... TO ..." statement is allowed * @@ -782,6 +788,36 @@ public enum Feature { * @see BackupLockStatement */ backupLock, + /** + * MySQL "CHANGE REPLICATION SOURCE TO" statement is allowed + * + * @see net.sf.jsqlparser.statement.replication.ChangeReplicationSourceStatement + */ + changeReplicationSource, + /** + * MySQL "CHANGE REPLICATION FILTER" statement is allowed + * + * @see net.sf.jsqlparser.statement.replication.ChangeReplicationFilterStatement + */ + changeReplicationFilter, + /** + * MySQL "START REPLICA" statement is allowed + * + * @see net.sf.jsqlparser.statement.replication.StartReplicaStatement + */ + startReplica, + /** + * MySQL "STOP REPLICA" statement is allowed + * + * @see net.sf.jsqlparser.statement.replication.StopReplicaStatement + */ + stopReplica, + /** + * MySQL "RESET BINARY LOGS AND GTIDS" and "RESET REPLICA" statements are allowed + * + * @see net.sf.jsqlparser.statement.replication.ResetReplicationStatement + */ + resetReplication, /** * SQL "COMMENT ON" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/PurgeObjectType.java b/src/main/java/net/sf/jsqlparser/statement/PurgeObjectType.java index 7535d34675..8e7612f233 100644 --- a/src/main/java/net/sf/jsqlparser/statement/PurgeObjectType.java +++ b/src/main/java/net/sf/jsqlparser/statement/PurgeObjectType.java @@ -15,7 +15,11 @@ * @author Andreas Reichel */ public enum PurgeObjectType { - TABLE, INDEX, RECYCLEBIN, DBA_RECYCLEBIN, TABLESPACE; + TABLE, INDEX, RECYCLEBIN, DBA_RECYCLEBIN, TABLESPACE, + /** MySQL and MariaDB {@code PURGE BINARY LOGS} */ + BINARY_LOGS, + /** MariaDB {@code PURGE MASTER LOGS}, which MySQL 8.4 removed */ + MASTER_LOGS; public static PurgeObjectType from(String type) { return Enum.valueOf(PurgeObjectType.class, type.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/PurgeStatement.java b/src/main/java/net/sf/jsqlparser/statement/PurgeStatement.java index a1b1f634a4..f2c736b617 100644 --- a/src/main/java/net/sf/jsqlparser/statement/PurgeStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/PurgeStatement.java @@ -11,7 +11,9 @@ package net.sf.jsqlparser.statement; import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.create.table.Index; @@ -20,12 +22,23 @@ * @author Andreas Reichel * @see Purge + * @see MySQL PURGE BINARY + * LOGS */ public class PurgeStatement extends ASTNodeAccessImpl implements Statement { private final PurgeObjectType purgeObjectType; private final Object object; private String userName; + private LogBound logBound; + + /** How {@code PURGE BINARY LOGS} gives the first log it keeps. */ + public enum LogBound { + /** {@code TO 'log_name'}: the logs before the named one */ + TO, + /** {@code BEFORE datetime_expr}: the logs older than a point in time */ + BEFORE + } public PurgeStatement(Table table) { this.purgeObjectType = PurgeObjectType.TABLE; @@ -51,15 +64,43 @@ public PurgeStatement(PurgeObjectType purgeObjectType, String tableSpaceName, St this.userName = userName; } + /** + * MySQL {@code PURGE {BINARY | MASTER} LOGS {TO 'log_name' | BEFORE expr}}; for {@code TO} the + * expression is the string literal of the log name. + */ + public PurgeStatement(PurgeObjectType purgeObjectType, LogBound logBound, + Expression logExpression) { + if (purgeObjectType != PurgeObjectType.BINARY_LOGS + && purgeObjectType != PurgeObjectType.MASTER_LOGS) { + throw new IllegalArgumentException( + "Only BINARY LOGS and MASTER LOGS take TO or BEFORE: " + purgeObjectType); + } + this.purgeObjectType = purgeObjectType; + this.logBound = Objects.requireNonNull(logBound, + "The TO or BEFORE of the PURGE LOGS statement must not be null."); + this.object = Objects.requireNonNull(logExpression, + "The log name or point in time of the PURGE LOGS statement must not be null."); + } + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + @SuppressWarnings({"PMD.MissingBreakInSwitch", "PMD.SwitchStmtsShouldHaveDefault", "PMD.CyclomaticComplexity"}) - public StringBuilder appendTo(StringBuilder builder) { + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { builder.append("PURGE "); + if (logBound != null) { + builder.append(purgeObjectType == PurgeObjectType.BINARY_LOGS ? "BINARY" : "MASTER") + .append(" LOGS ").append(logBound).append(' '); + expressions.accept((Expression) object); + return builder; + } switch (purgeObjectType) { case RECYCLEBIN: @@ -106,4 +147,17 @@ public PurgeObjectType getPurgeObjectType() { public Object getObject() { return object; } + + /** TO or BEFORE of {@code PURGE BINARY LOGS}, null for the other object types. */ + public LogBound getLogBound() { + return logBound; + } + + /** + * The log name literal of {@code TO} or the expression of {@code BEFORE}, null for the other + * object types. It is the same object as {@link #getObject()}. + */ + public Expression getLogExpression() { + return logBound != null ? (Expression) object : null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index f91ea876a6..1bf7ec0cf3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -101,6 +101,11 @@ import net.sf.jsqlparser.statement.lock.BackupLockStatement; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.lock.UnlockStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationFilterStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationSourceStatement; +import net.sf.jsqlparser.statement.replication.ResetReplicationStatement; +import net.sf.jsqlparser.statement.replication.StartReplicaStatement; +import net.sf.jsqlparser.statement.replication.StopReplicaStatement; import net.sf.jsqlparser.statement.maintenance.CheckpointStatement; import net.sf.jsqlparser.statement.maintenance.ClusterStatement; import net.sf.jsqlparser.statement.maintenance.VacuumStatement; @@ -682,6 +687,10 @@ public Void visit(LoadDataStatement loadDataStatement, S context) { @Override public Void visit(PurgeStatement purgeStatement, S context) { + if (purgeStatement.getLogBound() != null) { + // removes binary log files, a server operation like the replication statements + return sessionOnly(); + } analysis.claimTopLevel(); analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA); return null; @@ -945,6 +954,32 @@ public Void visit(BackupLockStatement backupLock, S context) { return transactionOnly(); } + // the replication statements change server state, like ALTER SYSTEM, and no data or schema + @Override + public Void visit(ChangeReplicationSourceStatement changeReplicationSource, S context) { + return sessionOnly(); + } + + @Override + public Void visit(ChangeReplicationFilterStatement changeReplicationFilter, S context) { + return sessionOnly(); + } + + @Override + public Void visit(StartReplicaStatement startReplica, S context) { + return sessionOnly(); + } + + @Override + public Void visit(StopReplicaStatement stopReplica, S context) { + return sessionOnly(); + } + + @Override + public Void visit(ResetReplicationStatement resetReplication, S context) { + return sessionOnly(); + } + private Void transactionOnly() { analysis.claimTopLevel(); analysis.certain(StmtFeature.MODIFIES_TRANSACTION); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index bd9119530e..a99a031045 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -98,6 +98,11 @@ import net.sf.jsqlparser.statement.maintenance.VacuumStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationFilterStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationSourceStatement; +import net.sf.jsqlparser.statement.replication.ResetReplicationStatement; +import net.sf.jsqlparser.statement.replication.StartReplicaStatement; +import net.sf.jsqlparser.statement.replication.StopReplicaStatement; import net.sf.jsqlparser.statement.reindex.Reindex; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; @@ -736,6 +741,46 @@ default void visit(BackupLockStatement backupLock) { visit(backupLock, null); } + default T visit(ChangeReplicationSourceStatement changeReplicationSource, S context) { + return null; + } + + default void visit(ChangeReplicationSourceStatement changeReplicationSource) { + visit(changeReplicationSource, null); + } + + default T visit(ChangeReplicationFilterStatement changeReplicationFilter, S context) { + return null; + } + + default void visit(ChangeReplicationFilterStatement changeReplicationFilter) { + visit(changeReplicationFilter, null); + } + + default T visit(StartReplicaStatement startReplica, S context) { + return null; + } + + default void visit(StartReplicaStatement startReplica) { + visit(startReplica, null); + } + + default T visit(StopReplicaStatement stopReplica, S context) { + return null; + } + + default void visit(StopReplicaStatement stopReplica) { + visit(stopReplica, null); + } + + default T visit(ResetReplicationStatement resetReplication, S context) { + return null; + } + + default void visit(ResetReplicationStatement resetReplication) { + visit(resetReplication, null); + } + T visit(CreatePolicy createPolicy, S context); default void visit(CreatePolicy createPolicy) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 32a947382e..46f433763c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -103,6 +103,11 @@ import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; import net.sf.jsqlparser.statement.merge.MergeOperationVisitorAdapter; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationFilterStatement; +import net.sf.jsqlparser.statement.replication.ChangeReplicationSourceStatement; +import net.sf.jsqlparser.statement.replication.ResetReplicationStatement; +import net.sf.jsqlparser.statement.replication.StartReplicaStatement; +import net.sf.jsqlparser.statement.replication.StopReplicaStatement; import net.sf.jsqlparser.statement.reindex.Reindex; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.FromItemVisitor; @@ -569,6 +574,31 @@ public T visit(BackupLockStatement backupLock, S context) { return null; } + @Override + public T visit(ChangeReplicationSourceStatement changeReplicationSource, S context) { + return null; + } + + @Override + public T visit(ChangeReplicationFilterStatement changeReplicationFilter, S context) { + return null; + } + + @Override + public T visit(StartReplicaStatement startReplica, S context) { + return null; + } + + @Override + public T visit(StopReplicaStatement stopReplica, S context) { + return null; + } + + @Override + public T visit(ResetReplicationStatement resetReplication, S context) { + return null; + } + @Override public T visit(CreatePolicy createPolicy, S context) { fromItemVisitor.visitFromItem(createPolicy.getTable(), context); diff --git a/src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationFilterStatement.java b/src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationFilterStatement.java new file mode 100644 index 0000000000..37ea85b41b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationFilterStatement.java @@ -0,0 +1,71 @@ +/*- + * #%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.replication; + +import java.util.ArrayList; +import java.util.List; +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 CHANGE REPLICATION FILTER filter [, filter] ... [FOR CHANNEL 'channel']}.
+ * Example:
+ * CHANGE REPLICATION FILTER REPLICATE_DO_DB = (d1, d2), REPLICATE_REWRITE_DB = ((a, b))
+ * + * @see MySQL + * CHANGE REPLICATION FILTER + */ +public class ChangeReplicationFilterStatement extends ASTNodeAccessImpl implements Statement { + private List filters = new ArrayList<>(); + private StringValue channel; + + public List getFilters() { + return filters; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + /** The channel of FOR CHANNEL, or null when the filters apply to every channel. */ + public StringValue getChannel() { + return channel; + } + + public void setChannel(StringValue channel) { + this.channel = channel; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CHANGE REPLICATION FILTER "); + for (int i = 0; i < filters.size(); i++) { + if (i > 0) { + builder.append(", "); + } + filters.get(i).appendTo(builder); + } + if (channel != null) { + builder.append(" FOR CHANNEL ").append(channel); + } + 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/replication/ChangeReplicationSourceStatement.java b/src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationSourceStatement.java new file mode 100644 index 0000000000..d2afb55de9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationSourceStatement.java @@ -0,0 +1,74 @@ +/*- + * #%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.replication; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +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 CHANGE REPLICATION SOURCE TO option [, option] ... [FOR CHANNEL 'channel']}.
+ * Example:
+ * CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'h', SOURCE_PORT = 3306 FOR CHANNEL 'c1'
+ * + * @see MySQL + * CHANGE REPLICATION SOURCE TO + */ +public class ChangeReplicationSourceStatement extends ASTNodeAccessImpl implements Statement { + private List options = new ArrayList<>(); + private StringValue channel; + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + /** The channel of FOR CHANNEL, or null for the default channel. */ + public StringValue getChannel() { + return channel; + } + + public void setChannel(StringValue channel) { + this.channel = channel; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressions) { + builder.append("CHANGE REPLICATION SOURCE TO "); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + options.get(i).appendTo(builder, expressions); + } + if (channel != null) { + builder.append(" FOR CHANNEL ").append(channel); + } + return builder; + } + + @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/replication/ReplicaThread.java b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicaThread.java new file mode 100644 index 0000000000..f66006ebc7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicaThread.java @@ -0,0 +1,30 @@ +/*- + * #%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.replication; + +import java.util.List; + +/** + * A replication thread that MySQL's {@code START REPLICA} and {@code STOP REPLICA} name. + * + * @see MySQL START REPLICA + */ +public enum ReplicaThread { + /** The receiver thread, which reads the source's binary log into the relay log. */ + IO_THREAD, + /** The applier thread, which executes the events of the relay log. */ + SQL_THREAD; + + static void appendTo(StringBuilder builder, List threads) { + for (int i = 0; i < threads.size(); i++) { + builder.append(i == 0 ? " " : ", ").append(threads.get(i)); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/replication/ReplicaUntilOption.java b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicaUntilOption.java new file mode 100644 index 0000000000..2cbcd68416 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicaUntilOption.java @@ -0,0 +1,60 @@ +/*- + * #%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.replication; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.replication.ReplicationSourceOption.ValueType; + +/** + * A condition of the {@code UNTIL} clause of MySQL's {@code START REPLICA}. The first condition may + * be any kind; the ones after it give further log coordinates. + * + * @see MySQL START REPLICA + */ +public class ReplicaUntilOption extends ReplicationOption { + + public enum Kind { + /** A binary log file of the source */ + SOURCE_LOG_FILE(ValueType.STRING, true), + /** A position in that binary log file */ + SOURCE_LOG_POS(ValueType.NUMBER, true), + /** A relay log file */ + RELAY_LOG_FILE(ValueType.STRING, true), + /** A position in that relay log file */ + RELAY_LOG_POS(ValueType.NUMBER, true), + /** Stop before the first transaction of a GTID set */ + SQL_BEFORE_GTIDS(ValueType.STRING, false), + /** Stop after the last transaction of a GTID set */ + SQL_AFTER_GTIDS(ValueType.STRING, false), + /** Stop once a multithreaded applier has no gaps */ + SQL_AFTER_MTS_GAPS(ValueType.NONE, false); + + private final ValueType valueType; + private final boolean logCoordinate; + + Kind(ValueType valueType, boolean logCoordinate) { + this.valueType = valueType; + this.logCoordinate = logCoordinate; + } + + public ValueType getValueType() { + return valueType; + } + + /** Whether this is a log file name or position, which may follow another condition. */ + public boolean isLogCoordinate() { + return logCoordinate; + } + } + + public ReplicaUntilOption(String name, Kind kind, Expression value) { + super(name, kind, value); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationFilter.java b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationFilter.java new file mode 100644 index 0000000000..4f883d5260 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationFilter.java @@ -0,0 +1,153 @@ +/*- + * #%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.replication; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.schema.Table; + +/** + * A rule of MySQL's {@code CHANGE REPLICATION FILTER}: a filter name and a parenthesized, possibly + * empty list. The list holds database names, qualified table names, quoted wildcard patterns or + * pairs of database names, depending on {@link Kind#getValueType()}; the list of the other value + * types stays empty. + * + * @see MySQL + * CHANGE REPLICATION FILTER + */ +public class ReplicationFilter implements Serializable { + private final Kind kind; + private final List databases = new ArrayList<>(); + private final List tables = new ArrayList<>(); + private final List patterns = new ArrayList<>(); + private final List rewrites = new ArrayList<>(); + + /** What the list of a filter holds. */ + public enum ValueType { + /** Database names: {@code (d1, d2)} */ + DATABASES, + /** Database-qualified table names: {@code (d1.t1)} */ + TABLES, + /** Quoted patterns of database and table names: {@code ('d%.t%')} */ + PATTERNS, + /** Pairs of database names: {@code ((from_db, to_db))} */ + DATABASE_PAIRS + } + + public enum Kind { + /** Replicate only these databases */ + REPLICATE_DO_DB(ValueType.DATABASES), + /** Skip these databases */ + REPLICATE_IGNORE_DB(ValueType.DATABASES), + /** Replicate only these tables */ + REPLICATE_DO_TABLE(ValueType.TABLES), + /** Skip these tables */ + REPLICATE_IGNORE_TABLE(ValueType.TABLES), + /** Replicate only tables matching these patterns */ + REPLICATE_WILD_DO_TABLE(ValueType.PATTERNS), + /** Skip tables matching these patterns */ + REPLICATE_WILD_IGNORE_TABLE(ValueType.PATTERNS), + /** Apply the changes of one database to another */ + REPLICATE_REWRITE_DB(ValueType.DATABASE_PAIRS); + + private final ValueType valueType; + + Kind(ValueType valueType) { + this.valueType = valueType; + } + + public ValueType getValueType() { + return valueType; + } + } + + /** A pair of {@code REPLICATE_REWRITE_DB}: the source database and its replacement. */ + public static class DatabaseRewrite implements Serializable { + private final String from; + private final String to; + + public DatabaseRewrite(String from, String to) { + this.from = Objects.requireNonNull(from); + this.to = Objects.requireNonNull(to); + } + + public String getFrom() { + return from; + } + + public String getTo() { + return to; + } + + @Override + public String toString() { + return "(" + from + ", " + to + ")"; + } + } + + public ReplicationFilter(Kind kind) { + this.kind = Objects.requireNonNull(kind); + } + + public Kind getKind() { + return kind; + } + + /** The database names of REPLICATE_DO_DB and REPLICATE_IGNORE_DB. */ + public List getDatabases() { + return databases; + } + + /** The tables of REPLICATE_DO_TABLE and REPLICATE_IGNORE_TABLE. */ + public List
getTables() { + return tables; + } + + /** The patterns of REPLICATE_WILD_DO_TABLE and REPLICATE_WILD_IGNORE_TABLE. */ + public List getPatterns() { + return patterns; + } + + /** The pairs of REPLICATE_REWRITE_DB. */ + public List getRewrites() { + return rewrites; + } + + /** The list that {@link Kind#getValueType()} selects. */ + public List getValues() { + switch (kind.getValueType()) { + case DATABASES: + return databases; + case TABLES: + return tables; + case PATTERNS: + return patterns; + default: + return rewrites; + } + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(kind).append(" = ("); + List values = getValues(); + for (int i = 0; i < values.size(); i++) { + builder.append(i == 0 ? "" : ", ").append(values.get(i)); + } + return builder.append(')'); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationSourceOption.java b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationSourceOption.java new file mode 100644 index 0000000000..d94cb451ff --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ReplicationSourceOption.java @@ -0,0 +1,239 @@ +/*- + * #%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.replication; + +import java.math.BigInteger; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.user.UserAccount; + +/** + * An option of MySQL's {@code CHANGE REPLICATION SOURCE TO}. Its value is a string, a number, + * {@code NULL}, a keyword such as {@code OFF} (held as a {@link Column}) or, for + * {@code IGNORE_SERVER_IDS}, a parenthesized list of server IDs. {@code PRIVILEGE_CHECKS_USER} + * names an account, which {@link #getAccount()} returns, or {@code NULL}. + * + * @see MySQL + * CHANGE REPLICATION SOURCE TO + */ +public class ReplicationSourceOption extends ReplicationOption { + private final UserAccount account; + + /** The values an option takes in the MySQL 8.4 grammar. */ + public enum ValueType { + /** A string literal. */ + STRING, + /** A string literal or {@code NULL}. */ + STRING_OR_NULL, + /** An unsigned integer or decimal literal. */ + NUMBER, + /** The integer literal 0 or 1. */ + ZERO_OR_ONE, + /** A parenthesized, possibly empty list of numbers. */ + NUMBER_LIST, + /** An account or {@code NULL}. */ + ACCOUNT_OR_NULL, + /** {@code STREAM}, {@code ON}, {@code OFF} or {@code GENERATE}. */ + PRIMARY_KEY_CHECK, + /** {@code OFF}, {@code LOCAL} or a UUID string. */ + GTID_ASSIGNMENT, + /** No value at all. */ + NONE; + + /** + * Whether MySQL accepts {@code value} for an option of this type. An account is not an + * expression, so for {@link #ACCOUNT_OR_NULL} only {@code NULL} is checked here. + */ + public boolean accepts(Expression value) { + switch (this) { + case STRING: + return value instanceof StringValue; + case STRING_OR_NULL: + return value instanceof StringValue || value instanceof NullValue; + case NUMBER: + return value instanceof LongValue || value instanceof DoubleValue; + case ZERO_OR_ONE: + return value instanceof LongValue && BigInteger.ONE + .compareTo(((LongValue) value).getBigIntegerValue()) >= 0; + case NUMBER_LIST: + return value instanceof ParenthesedExpressionList; + case ACCOUNT_OR_NULL: + return value instanceof NullValue; + case PRIMARY_KEY_CHECK: + return isKeyword(value, "STREAM", "ON", "OFF", "GENERATE"); + case GTID_ASSIGNMENT: + return value instanceof StringValue || isKeyword(value, "OFF", "LOCAL"); + default: + return value == null; + } + } + + private static boolean isKeyword(Expression value, String... keywords) { + if (!(value instanceof Column)) { + return false; + } + String name = ((Column) value).getColumnName(); + for (String keyword : keywords) { + if (keyword.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + } + + /** The options of MySQL 8.4; the MASTER_ spellings were removed in 8.4. */ + public enum Kind { + /** The network interface of the replica */ + SOURCE_BIND(ValueType.STRING), + /** The host name or IP address of the source */ + SOURCE_HOST(ValueType.STRING), + /** The replication user account */ + SOURCE_USER(ValueType.STRING), + /** The password of the replication user */ + SOURCE_PASSWORD(ValueType.STRING), + /** The TCP/IP port of the source */ + SOURCE_PORT(ValueType.NUMBER), + /** The account that privilege checks of the applier use, or NULL */ + PRIVILEGE_CHECKS_USER(ValueType.ACCOUNT_OR_NULL), + /** Whether the channel accepts only row-based events */ + REQUIRE_ROW_FORMAT(ValueType.NUMBER), + /** The primary key check of the channel */ + REQUIRE_TABLE_PRIMARY_KEY_CHECK(ValueType.PRIMARY_KEY_CHECK), + /** How anonymous transactions get a GTID */ + ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS(ValueType.GTID_ASSIGNMENT), + /** The binary log file to read from */ + SOURCE_LOG_FILE(ValueType.STRING), + /** The position to read from in the binary log file */ + SOURCE_LOG_POS(ValueType.NUMBER), + /** Whether the replica finds its position by GTID */ + SOURCE_AUTO_POSITION(ValueType.NUMBER), + /** The relay log file the applier starts from */ + RELAY_LOG_FILE(ValueType.STRING), + /** The position the applier starts from in the relay log file */ + RELAY_LOG_POS(ValueType.NUMBER), + /** The heartbeat interval in seconds */ + SOURCE_HEARTBEAT_PERIOD(ValueType.NUMBER), + /** The seconds between reconnection attempts */ + SOURCE_CONNECT_RETRY(ValueType.NUMBER), + /** The number of reconnection attempts */ + SOURCE_RETRY_COUNT(ValueType.NUMBER), + /** Whether the channel fails over to another source */ + SOURCE_CONNECTION_AUTO_FAILOVER(ValueType.ZERO_OR_ONE), + /** The seconds the replica lags behind the source */ + SOURCE_DELAY(ValueType.NUMBER), + /** The permitted compression algorithms */ + SOURCE_COMPRESSION_ALGORITHMS(ValueType.STRING), + /** The zstd compression level */ + SOURCE_ZSTD_COMPRESSION_LEVEL(ValueType.NUMBER), + /** Whether the connection uses SSL */ + SOURCE_SSL(ValueType.NUMBER), + /** The certificate authority file */ + SOURCE_SSL_CA(ValueType.STRING), + /** The directory of certificate authority files */ + SOURCE_SSL_CAPATH(ValueType.STRING), + /** The certificate file */ + SOURCE_SSL_CERT(ValueType.STRING), + /** The certificate revocation list file */ + SOURCE_SSL_CRL(ValueType.STRING), + /** The directory of certificate revocation list files */ + SOURCE_SSL_CRLPATH(ValueType.STRING), + /** The private key file */ + SOURCE_SSL_KEY(ValueType.STRING), + /** The permitted ciphers */ + SOURCE_SSL_CIPHER(ValueType.STRING), + /** Whether the replica verifies the certificate of the source */ + SOURCE_SSL_VERIFY_SERVER_CERT(ValueType.NUMBER), + /** The permitted TLS protocols */ + SOURCE_TLS_VERSION(ValueType.STRING), + /** The permitted TLSv1.3 cipher suites, or NULL */ + SOURCE_TLS_CIPHERSUITES(ValueType.STRING_OR_NULL), + /** The file of the RSA public key of the source */ + SOURCE_PUBLIC_KEY_PATH(ValueType.STRING), + /** Whether the replica requests the RSA public key of the source */ + GET_SOURCE_PUBLIC_KEY(ValueType.NUMBER), + /** The network namespace of the connection */ + NETWORK_NAMESPACE(ValueType.STRING), + /** The server IDs whose events the replica ignores */ + IGNORE_SERVER_IDS(ValueType.NUMBER_LIST), + /** Whether the channel keeps no file names and positions */ + GTID_ONLY(ValueType.ZERO_OR_ONE); + + private final ValueType valueType; + + Kind(ValueType valueType) { + this.valueType = valueType; + } + + public ValueType getValueType() { + return valueType; + } + } + + /** The values of {@code REQUIRE_TABLE_PRIMARY_KEY_CHECK}. */ + public enum PrimaryKeyCheck { + STREAM, ON, OFF, GENERATE + } + + /** The values of {@code ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS}; UUID is a quoted UUID. */ + public enum GtidAssignment { + OFF, LOCAL, UUID + } + + public ReplicationSourceOption(String name, Kind kind, Expression value) { + super(name, kind, value); + this.account = null; + } + + /** {@code PRIVILEGE_CHECKS_USER = account}. */ + public ReplicationSourceOption(String name, UserAccount account) { + super(name, Kind.PRIVILEGE_CHECKS_USER, null); + this.account = account; + } + + /** The account of {@code PRIVILEGE_CHECKS_USER}, null for {@code NULL} and other options. */ + public UserAccount getAccount() { + return account; + } + + public PrimaryKeyCheck getPrimaryKeyCheck() { + return getKind() == Kind.REQUIRE_TABLE_PRIMARY_KEY_CHECK + ? enumValue(PrimaryKeyCheck.class) + : null; + } + + public GtidAssignment getGtidAssignment() { + if (getKind() != Kind.ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS) { + return null; + } + return getValue() instanceof StringValue ? GtidAssignment.UUID + : enumValue(GtidAssignment.class); + } + + @Override + public String getValueText() { + return account != null ? account.toString() : super.getValueText(); + } + + @Override + public void appendTo(StringBuilder sql, Consumer expressions) { + if (account == null) { + super.appendTo(sql, expressions); + } else { + sql.append(getName()).append(" = ").append(account); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/replication/ResetReplicationStatement.java b/src/main/java/net/sf/jsqlparser/statement/replication/ResetReplicationStatement.java new file mode 100644 index 0000000000..4a4d316ba9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/replication/ResetReplicationStatement.java @@ -0,0 +1,142 @@ +/*- + * #%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.replication; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.expression.LongValue; +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 RESET reset_option [, reset_option] ...} with the replication options + * {@code BINARY LOGS AND GTIDS [TO n]} and {@code REPLICA [ALL] [FOR CHANNEL 'channel']}. The + * PostgreSQL {@code RESET} of a configuration parameter remains a + * {@link net.sf.jsqlparser.statement.ResetStatement}.
+ * Example:
+ * RESET REPLICA ALL FOR CHANNEL 'c1', BINARY LOGS AND GTIDS TO 1234
+ * + * @see MySQL RESET + * @see MySQL + * RESET BINARY LOGS AND GTIDS + * @see MySQL RESET REPLICA + */ +public class ResetReplicationStatement extends ASTNodeAccessImpl implements Statement { + private List