From 6362141193916f0ffed657e35b53332fc5383ab2 Mon Sep 17 00:00:00 2001 From: minleejae Date: Mon, 9 Nov 2026 10:30:05 +0900 Subject: [PATCH] feat: parse the MySQL replication statements CHANGE REPLICATION SOURCE TO, CHANGE REPLICATION FILTER, START REPLICA, STOP REPLICA, RESET REPLICA, RESET BINARY LOGS AND GTIDS and PURGE BINARY LOGS failed to parse, so a replica setup script failed at its first statement. The new statements of net.sf.jsqlparser.statement.replication follow the MySQL 8.4 grammar. ReplicationSourceOption extends ReplicationOption, and each option kind says which value it takes: a string, a number, NULL, 0 or 1, a keyword, the server ID list or an account. ReplicationFilter keeps database names, tables, patterns or rewrite pairs, and the statements share one FOR CHANNEL production. RESET reads MySQL's comma list into ResetReplicationStatement, apart from ResetStatement. PurgeStatement gains BINARY LOGS TO and BEFORE, which MariaDB also reads, with its MASTER LOGS; as in MySQL, BEFORE takes no subquery. Without a dialect a bare RESET REPLICA stays a ResetStatement, and other dialects keep their readings. STOP becomes a non-reserved keyword and remains usable as a name, also in Oracle's PURGE TABLESPACE. Signed-off-by: minleejae --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 36 + .../jsqlparser/statement/PurgeObjectType.java | 6 +- .../jsqlparser/statement/PurgeStatement.java | 56 +- .../statement/StatementFeatureVisitor.java | 35 + .../statement/StatementVisitor.java | 45 ++ .../statement/StatementVisitorAdapter.java | 30 + .../ChangeReplicationFilterStatement.java | 71 ++ .../ChangeReplicationSourceStatement.java | 74 ++ .../statement/replication/ReplicaThread.java | 30 + .../replication/ReplicaUntilOption.java | 60 ++ .../replication/ReplicationFilter.java | 153 +++++ .../replication/ReplicationSourceOption.java | 239 +++++++ .../ResetReplicationStatement.java | 142 ++++ .../replication/StartReplicaStatement.java | 131 ++++ .../replication/StopReplicaStatement.java | 66 ++ .../sf/jsqlparser/util/TablesNamesFinder.java | 35 + .../util/deparser/StatementDeParser.java | 38 +- .../validation/feature/MariaDbVersion.java | 3 + .../util/validation/feature/MySqlVersion.java | 5 + .../validator/StatementValidator.java | 38 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 366 +++++++++- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 61 ++ .../parser/ASTNodeCoverageTest.java | 14 +- .../MySqlReplicationStatementsTest.java | 647 ++++++++++++++++++ .../ReplicationStatementValidatorTest.java | 64 ++ 27 files changed, 2435 insertions(+), 13 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationFilterStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ChangeReplicationSourceStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ReplicaThread.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ReplicaUntilOption.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ReplicationFilter.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ReplicationSourceOption.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/ResetReplicationStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/StartReplicaStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/replication/StopReplicaStatement.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/replication/MySqlReplicationStatementsTest.java create mode 100644 src/test/java/net/sf/jsqlparser/util/validation/validator/ReplicationStatementValidatorTest.java 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