diff --git a/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/BaseRule.g4 b/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/BaseRule.g4 index 81e18359d0749..d6757b9885e62 100644 --- a/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/BaseRule.g4 +++ b/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/BaseRule.g4 @@ -76,6 +76,7 @@ literals | bitValueLiterals | booleanLiterals | nullValueLiterals + | arrayLiterals ; string_ @@ -110,6 +111,10 @@ nullValueLiterals : NULL ; +arrayLiterals + : LBT_ (expr (COMMA_ expr)*)? RBT_ + ; + collationName : textOrIdentifier | BINARY ; diff --git a/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/DALStatement.g4 b/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/DALStatement.g4 index 38badae1a1053..4b884e6a54f7a 100644 --- a/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/DALStatement.g4 +++ b/parser/sql/engine/dialect/doris/src/main/antlr4/imports/doris/DALStatement.g4 @@ -254,6 +254,10 @@ showCharset : SHOW CHARSET ; +showTransaction + : SHOW TRANSACTION fromDatabase? showWhereClause? + ; + setCharacter : SET (CHARACTER SET | CHARSET) (charsetName | DEFAULT) ; @@ -707,4 +711,5 @@ show | showVariables | showReplicas | showReplicaStatus + | showTransaction ; diff --git a/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/DorisStatementVisitor.java b/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/DorisStatementVisitor.java index e4a4f98fa35db..ebcd10180c5ee 100644 --- a/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/DorisStatementVisitor.java +++ b/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/DorisStatementVisitor.java @@ -30,6 +30,7 @@ import org.apache.shardingsphere.sql.parser.autogen.DorisStatementBaseVisitor; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.AggregationFunctionContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.AliasContext; +import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ArrayLiteralsContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.AssignmentContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.AssignmentValueContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.AssignmentValuesContext; @@ -290,7 +291,10 @@ public final ASTNode visitLiterals(final LiteralsContext ctx) { if (null != ctx.nullValueLiterals()) { return visit(ctx.nullValueLiterals()); } - throw new IllegalStateException("Literals must have string, number, dateTime, hex, bit, boolean or null."); + if (null != ctx.arrayLiterals()) { + return visit(ctx.arrayLiterals()); + } + throw new IllegalStateException("Literals must have string, number, dateTime, hex, bit, boolean, null or array."); } @Override @@ -336,6 +340,15 @@ public final ASTNode visitNullValueLiterals(final NullValueLiteralsContext ctx) return new NullLiteralValue(ctx.getText()); } + @Override + public final ASTNode visitArrayLiterals(final ArrayLiteralsContext ctx) { + ListExpression result = new ListExpression(ctx.getStart().getStartIndex(), ctx.getStop().getStopIndex()); + for (ExprContext each : ctx.expr()) { + result.getItems().add((ExpressionSegment) visit(each)); + } + return result; + } + @Override public final ASTNode visitIdentifier(final IdentifierContext ctx) { return new IdentifierValue(ctx.getText()); @@ -622,7 +635,11 @@ public final ASTNode visitSimpleExpr(final SimpleExprContext ctx) { return result; } if (null != ctx.literals()) { - return SQLUtils.createLiteralExpression(visit(ctx.literals()), startIndex, stopIndex, ctx.literals().start.getInputStream().getText(new Interval(startIndex, stopIndex))); + ASTNode astNode = visit(ctx.literals()); + if (astNode instanceof ExpressionSegment) { + return astNode; + } + return SQLUtils.createLiteralExpression(astNode, startIndex, stopIndex, ctx.literals().start.getInputStream().getText(new Interval(startIndex, stopIndex))); } if (null != ctx.intervalExpression()) { return visit(ctx.intervalExpression()); diff --git a/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/type/DorisDALStatementVisitor.java b/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/type/DorisDALStatementVisitor.java index 11cb7e4f1ed6b..cdfbeeaa03ada 100644 --- a/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/type/DorisDALStatementVisitor.java +++ b/parser/sql/engine/dialect/doris/src/main/java/org/apache/shardingsphere/sql/parser/engine/doris/visitor/statement/type/DorisDALStatementVisitor.java @@ -109,6 +109,7 @@ import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowStatusContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowTableStatusContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowTablesContext; +import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowTransactionContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowTriggersContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowVariablesContext; import org.apache.shardingsphere.sql.parser.autogen.DorisStatementParser.ShowWarningsContext; @@ -232,6 +233,7 @@ import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.process.MySQLShowProcessListStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.profile.MySQLShowProfileStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.profile.MySQLShowProfilesStatement; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.table.MySQLShowCreateTableStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.table.MySQLShowOpenTablesStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.table.MySQLShowTableStatusStatement; @@ -815,6 +817,19 @@ public ASTNode visitShowPrivileges(final ShowPrivilegesContext ctx) { return new MySQLShowPrivilegesStatement(getDatabaseType()); } + @Override + public ASTNode visitShowTransaction(final ShowTransactionContext ctx) { + MySQLShowTransactionStatement result = new MySQLShowTransactionStatement(getDatabaseType()); + if (null != ctx.fromDatabase()) { + result.setFromDatabase((FromDatabaseSegment) visit(ctx.fromDatabase())); + } + if (null != ctx.showWhereClause()) { + result.setWhere((WhereSegment) visit(ctx.showWhereClause())); + } + result.addParameterMarkers(getParameterMarkerSegments()); + return result; + } + @Override public ASTNode visitShutdown(final ShutdownContext ctx) { return new MySQLShutdownStatement(getDatabaseType()); diff --git a/parser/sql/statement/dialect/mysql/src/main/java/org/apache/shardingsphere/sql/parser/statement/mysql/dal/show/MySQLShowTransactionStatement.java b/parser/sql/statement/dialect/mysql/src/main/java/org/apache/shardingsphere/sql/parser/statement/mysql/dal/show/MySQLShowTransactionStatement.java new file mode 100644 index 0000000000000..abe2c1ae289e8 --- /dev/null +++ b/parser/sql/statement/dialect/mysql/src/main/java/org/apache/shardingsphere/sql/parser/statement/mysql/dal/show/MySQLShowTransactionStatement.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.sql.parser.statement.mysql.dal.show; + +import lombok.Getter; +import lombok.Setter; +import org.apache.shardingsphere.database.connector.core.type.DatabaseType; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dal.FromDatabaseSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.predicate.WhereSegment; +import org.apache.shardingsphere.sql.parser.statement.core.statement.attribute.SQLStatementAttributes; +import org.apache.shardingsphere.sql.parser.statement.core.statement.attribute.type.AllowNotUseDatabaseSQLStatementAttribute; +import org.apache.shardingsphere.sql.parser.statement.core.statement.attribute.type.DatabaseSelectRequiredSQLStatementAttribute; +import org.apache.shardingsphere.sql.parser.statement.core.statement.attribute.type.FromDatabaseSQLStatementAttribute; +import org.apache.shardingsphere.sql.parser.statement.core.statement.attribute.type.TablelessDataSourceBroadcastRouteSQLStatementAttribute; +import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dal.DALStatement; + +/** + * Show transaction statement for MySQL. + */ +@Getter +@Setter +public final class MySQLShowTransactionStatement extends DALStatement { + + private FromDatabaseSegment fromDatabase; + + private WhereSegment where; + + public MySQLShowTransactionStatement(final DatabaseType databaseType) { + super(databaseType); + } + + @Override + public SQLStatementAttributes getAttributes() { + String databaseName = null == fromDatabase ? null : fromDatabase.getDatabase().getIdentifier().getValue(); + return new SQLStatementAttributes(new DatabaseSelectRequiredSQLStatementAttribute(), new FromDatabaseSQLStatementAttribute(fromDatabase), + new TablelessDataSourceBroadcastRouteSQLStatementAttribute(), new AllowNotUseDatabaseSQLStatementAttribute(true, databaseName)); + } +} diff --git a/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutor.java b/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutor.java new file mode 100644 index 0000000000000..bb9bb19787fd4 --- /dev/null +++ b/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutor.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.shardingsphere.database.exception.core.exception.syntax.database.UnknownDatabaseException; +import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions; +import org.apache.shardingsphere.infra.executor.sql.execute.result.query.QueryResultMetaData; +import org.apache.shardingsphere.infra.executor.sql.execute.result.query.impl.raw.metadata.RawQueryResultColumnMetaData; +import org.apache.shardingsphere.infra.executor.sql.execute.result.query.impl.raw.metadata.RawQueryResultMetaData; +import org.apache.shardingsphere.infra.merge.result.MergedResult; +import org.apache.shardingsphere.infra.merge.result.impl.local.LocalDataMergedResult; +import org.apache.shardingsphere.infra.merge.result.impl.local.LocalDataQueryResultRow; +import org.apache.shardingsphere.infra.metadata.ShardingSphereMetaData; +import org.apache.shardingsphere.infra.metadata.database.ShardingSphereDatabase; +import org.apache.shardingsphere.proxy.backend.handler.admin.executor.DatabaseAdminQueryExecutor; +import org.apache.shardingsphere.proxy.backend.session.ConnectionSession; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.column.ColumnSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.expr.BinaryOperationExpression; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.expr.simple.LiteralExpressionSegment; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; + +import java.sql.Types; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Show transaction executor for MySQL. + */ +@RequiredArgsConstructor +public class MySQLShowTransactionExecutor implements DatabaseAdminQueryExecutor { + + private final MySQLShowTransactionStatement sqlStatement; + + @Getter + private QueryResultMetaData queryResultMetaData; + + @Getter + private MergedResult mergedResult; + + private Optional filterTransactionId = Optional.empty(); + + private Optional filterLabel = Optional.empty(); + + @Override + public void execute(final ConnectionSession connectionSession, final ShardingSphereMetaData metaData) { + String databaseName = getDatabaseName(connectionSession); + if (null != databaseName) { + ShardingSpherePreconditions.checkState(metaData.containsDatabase(databaseName), () -> new UnknownDatabaseException(databaseName)); + } + queryResultMetaData = createQueryResultMetaData(); + mergedResult = new LocalDataMergedResult(getQueryResultRows(databaseName, metaData)); + } + + private String getDatabaseName(final ConnectionSession connectionSession) { + return null == sqlStatement.getFromDatabase() ? connectionSession.getUsedDatabaseName() : sqlStatement.getFromDatabase().getDatabase().getIdentifier().getValue(); + } + + private QueryResultMetaData createQueryResultMetaData() { + List columns = new ArrayList<>(15); + columns.add(new RawQueryResultColumnMetaData("", "TransactionId", "TransactionId", Types.BIGINT, "BIGINT", 20, 0)); + columns.add(new RawQueryResultColumnMetaData("", "Label", "Label", Types.VARCHAR, "VARCHAR", 255, 0)); + columns.add(new RawQueryResultColumnMetaData("", "Coordinator", "Coordinator", Types.VARCHAR, "VARCHAR", 255, 0)); + columns.add(new RawQueryResultColumnMetaData("", "TransactionStatus", "TransactionStatus", Types.VARCHAR, "VARCHAR", 50, 0)); + columns.add(new RawQueryResultColumnMetaData("", "LoadJobSourceType", "LoadJobSourceType", Types.VARCHAR, "VARCHAR", 50, 0)); + columns.add(new RawQueryResultColumnMetaData("", "PrepareTime", "PrepareTime", Types.VARCHAR, "VARCHAR", 50, 0)); + columns.add(new RawQueryResultColumnMetaData("", "CommitTime", "CommitTime", Types.VARCHAR, "VARCHAR", 50, 0)); + columns.add(new RawQueryResultColumnMetaData("", "FinishTime", "FinishTime", Types.VARCHAR, "VARCHAR", 50, 0)); + columns.add(new RawQueryResultColumnMetaData("", "Reason", "Reason", Types.VARCHAR, "VARCHAR", 1000, 0)); + columns.add(new RawQueryResultColumnMetaData("", "ErrorReplicasCount", "ErrorReplicasCount", Types.INTEGER, "INTEGER", 10, 0)); + columns.add(new RawQueryResultColumnMetaData("", "ListenerId", "ListenerId", Types.BIGINT, "BIGINT", 20, 0)); + columns.add(new RawQueryResultColumnMetaData("", "TimeoutMs", "TimeoutMs", Types.BIGINT, "BIGINT", 20, 0)); + return new RawQueryResultMetaData(columns); + } + + private Collection getQueryResultRows(final String databaseName, final ShardingSphereMetaData metaData) { + if (null == databaseName) { + return Collections.emptyList(); + } + ShardingSphereDatabase database = metaData.getDatabase(databaseName); + if (!database.isComplete()) { + return Collections.emptyList(); + } + extractWhereFilter(); + Collection transactions = loadTransactions(); + return transactions.stream() + .filter(this::matchesFilter) + .map(this::buildTransactionRow) + .collect(Collectors.toList()); + } + + private void extractWhereFilter() { + ShardingSpherePreconditions.checkState(null != sqlStatement.getWhere(), + () -> new IllegalArgumentException("SHOW TRANSACTION requires WHERE clause with 'id = ' or 'label = '")); + ShardingSpherePreconditions.checkState(sqlStatement.getWhere().getExpr() instanceof BinaryOperationExpression, + () -> new IllegalArgumentException("WHERE clause must be in the form: id = or label = ")); + BinaryOperationExpression binaryExpr = (BinaryOperationExpression) sqlStatement.getWhere().getExpr(); + extractFilterFromBinaryExpression(binaryExpr); + ShardingSpherePreconditions.checkState(filterTransactionId.isPresent() || filterLabel.isPresent(), + () -> new IllegalArgumentException("WHERE clause must specify either 'id = ' or 'label = '")); + } + + private void extractFilterFromBinaryExpression(final BinaryOperationExpression expression) { + ShardingSpherePreconditions.checkState(expression.getLeft() instanceof ColumnSegment && expression.getRight() instanceof LiteralExpressionSegment, + () -> new IllegalArgumentException("WHERE clause must be in the form: column = literal")); + ShardingSpherePreconditions.checkState("=".equals(expression.getOperator()), + () -> new IllegalArgumentException("WHERE clause only supports '=' operator, got: " + expression.getOperator())); + ColumnSegment column = (ColumnSegment) expression.getLeft(); + LiteralExpressionSegment literal = (LiteralExpressionSegment) expression.getRight(); + String columnName = column.getIdentifier().getValue().toLowerCase(); + Object literalValue = literal.getLiterals(); + if ("id".equalsIgnoreCase(columnName)) { + filterTransactionId = extractLongValue(literalValue); + ShardingSpherePreconditions.checkState(filterTransactionId.isPresent(), + () -> new IllegalArgumentException("Invalid transaction id value: " + literalValue)); + } else if ("label".equalsIgnoreCase(columnName)) { + filterLabel = Optional.of(String.valueOf(literalValue)); + } else { + throw new IllegalArgumentException("WHERE clause only supports 'id' or 'label' columns, got: " + columnName); + } + } + + private Optional extractLongValue(final Object value) { + if (value instanceof Number) { + return Optional.of(((Number) value).longValue()); + } + if (value instanceof String) { + try { + return Optional.of(Long.parseLong((String) value)); + } catch (final NumberFormatException ignored) { + return Optional.empty(); + } + } + return Optional.empty(); + } + + private boolean matchesFilter(final TransactionInfo transaction) { + if (filterTransactionId.isPresent() && filterTransactionId.get() != transaction.getTransactionId()) { + return false; + } + if (filterLabel.isPresent() && !filterLabel.get().equals(transaction.getLabel())) { + return false; + } + return true; + } + + private LocalDataQueryResultRow buildTransactionRow(final TransactionInfo transaction) { + return new LocalDataQueryResultRow( + transaction.getTransactionId(), + transaction.getLabel(), + transaction.getCoordinator(), + transaction.getTransactionStatus(), + transaction.getLoadJobSourceType(), + transaction.getPrepareTime(), + transaction.getCommitTime(), + transaction.getFinishTime(), + transaction.getReason(), + transaction.getErrorReplicasCount(), + transaction.getListenerId(), + transaction.getTimeoutMs()); + } + + protected Collection loadTransactions() { + throw new UnsupportedOperationException("SHOW TRANSACTION is not supported for the moment. "); + } + + /** + * Transaction information holder. + */ + @Getter + @RequiredArgsConstructor + static class TransactionInfo { + + private final long transactionId; + + private final String label; + + private final String coordinator; + + private final String transactionStatus; + + private final String loadJobSourceType; + + private final String prepareTime; + + private final String commitTime; + + private final String finishTime; + + private final String reason; + + private final int errorReplicasCount; + + private final long listenerId; + + private final long timeoutMs; + } +} diff --git a/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/factory/MySQLShowAdminExecutorFactory.java b/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/factory/MySQLShowAdminExecutorFactory.java index 6865ae5b3a6f1..9d7afb323be06 100644 --- a/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/factory/MySQLShowAdminExecutorFactory.java +++ b/proxy/backend/dialect/mysql/src/main/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/factory/MySQLShowAdminExecutorFactory.java @@ -26,12 +26,14 @@ import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowProcedureStatusExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowProcessListExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowTablesExecutor; +import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowTransactionExecutor; import org.apache.shardingsphere.sql.parser.statement.core.statement.SQLStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.database.MySQLShowCreateDatabaseStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.database.MySQLShowDatabasesStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.function.MySQLShowFunctionStatusStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.procedure.MySQLShowProcedureStatusStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.process.MySQLShowProcessListStatement; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.table.MySQLShowTablesStatement; import java.util.Optional; @@ -67,6 +69,9 @@ public static Optional newInstance(final SQLStatement sql if (sqlStatement instanceof MySQLShowProcessListStatement) { return Optional.of(new MySQLShowProcessListExecutor((MySQLShowProcessListStatement) sqlStatement)); } + if (sqlStatement instanceof MySQLShowTransactionStatement) { + return Optional.of(new MySQLShowTransactionExecutor((MySQLShowTransactionStatement) sqlStatement)); + } return Optional.empty(); } } diff --git a/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/MySQLAdminExecutorCreatorTest.java b/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/MySQLAdminExecutorCreatorTest.java index 7f69967391c98..ec72fb3fe4dac 100644 --- a/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/MySQLAdminExecutorCreatorTest.java +++ b/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/MySQLAdminExecutorCreatorTest.java @@ -42,6 +42,7 @@ import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowProcedureStatusExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowProcessListExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowTablesExecutor; +import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show.MySQLShowTransactionExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.MySQLUseDatabaseExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.select.NoResourceShowExecutor; import org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.select.SelectInformationSchemataExecutor; @@ -66,6 +67,7 @@ import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.function.MySQLShowFunctionStatusStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.procedure.MySQLShowProcedureStatusStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.process.MySQLShowProcessListStatement; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.table.MySQLShowTablesStatement; import org.apache.shardingsphere.test.infra.fixture.jdbc.MockedDataSource; import org.apache.shardingsphere.test.infra.framework.extension.mock.AutoMockExtension; @@ -118,6 +120,14 @@ void assertCreateWithShowTables() { assertThat(actual.get(), isA(MySQLShowTablesExecutor.class)); } + @Test + void assertCreateWithShowTransaction() { + SQLStatementContext sqlStatementContext = new CommonSQLStatementContext(new MySQLShowTransactionStatement(databaseType)); + Optional actual = new MySQLAdminExecutorCreator().create(sqlStatementContext, "", "", Collections.emptyList()); + assertTrue(actual.isPresent()); + assertThat(actual.get(), isA(MySQLShowTransactionExecutor.class)); + } + @Test void assertCreateWithUse() { SQLStatementContext sqlStatementContext = new CommonSQLStatementContext(new MySQLUseStatement(databaseType, null)); diff --git a/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutorTest.java b/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutorTest.java new file mode 100644 index 0000000000000..09dff5fba64d8 --- /dev/null +++ b/proxy/backend/dialect/mysql/src/test/java/org/apache/shardingsphere/proxy/backend/mysql/handler/admin/executor/show/MySQLShowTransactionExecutorTest.java @@ -0,0 +1,478 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.show; + +import org.apache.shardingsphere.authority.rule.AuthorityRule; +import org.apache.shardingsphere.database.connector.core.type.DatabaseType; +import org.apache.shardingsphere.database.exception.core.exception.syntax.database.UnknownDatabaseException; +import org.apache.shardingsphere.infra.config.props.ConfigurationProperties; +import org.apache.shardingsphere.infra.executor.sql.execute.result.query.QueryResultMetaData; +import org.apache.shardingsphere.infra.merge.result.MergedResult; +import org.apache.shardingsphere.infra.metadata.ShardingSphereMetaData; +import org.apache.shardingsphere.infra.metadata.database.ShardingSphereDatabase; +import org.apache.shardingsphere.infra.metadata.database.resource.ResourceMetaData; +import org.apache.shardingsphere.infra.metadata.database.rule.RuleMetaData; +import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader; +import org.apache.shardingsphere.proxy.backend.session.ConnectionSession; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dal.FromDatabaseSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.column.ColumnSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.expr.BinaryOperationExpression; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.expr.simple.LiteralExpressionSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.predicate.WhereSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.generic.DatabaseSegment; +import org.apache.shardingsphere.sql.parser.statement.core.value.identifier.IdentifierValue; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MySQLShowTransactionExecutorTest { + + private static final String DATABASE_NAME = "test_db"; + + private final DatabaseType databaseType = TypedSPILoader.getService(DatabaseType.class, "MySQL"); + + @Test + void assertExecuteWithoutFromAndWhere() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + assertThat(exception.getMessage(), is("SHOW TRANSACTION requires WHERE clause with 'id = ' or 'label = '")); + } + + @Test + void assertExecuteWithFromDatabase() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + FromDatabaseSegment fromDatabaseSegment = new FromDatabaseSegment(0, new DatabaseSegment(0, 0, new IdentifierValue(DATABASE_NAME))); + sqlStatement.setFromDatabase(fromDatabaseSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession("other_db"), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithWhereId() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("ID", 4005); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithWhereLabel() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("label", "test_label"); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithFromAndWhereId() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + FromDatabaseSegment fromDatabaseSegment = new FromDatabaseSegment(0, new DatabaseSegment(0, 0, new IdentifierValue(DATABASE_NAME))); + sqlStatement.setFromDatabase(fromDatabaseSegment); + WhereSegment whereSegment = createWhereSegment("ID", 4005); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession("other_db"), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithFromAndWhereLabel() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + FromDatabaseSegment fromDatabaseSegment = new FromDatabaseSegment(0, new DatabaseSegment(0, 0, new IdentifierValue(DATABASE_NAME))); + sqlStatement.setFromDatabase(fromDatabaseSegment); + WhereSegment whereSegment = createWhereSegment("label", "test_label"); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession("other_db"), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithUnknownDatabase() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + FromDatabaseSegment fromDatabaseSegment = new FromDatabaseSegment(0, new DatabaseSegment(0, 0, new IdentifierValue("unknown_db"))); + sqlStatement.setFromDatabase(fromDatabaseSegment); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnknownDatabaseException.class, () -> executor.execute(mockConnectionSession("other_db"), mockMetaData(createDatabases()))); + } + + @Test + void assertExecuteWithUncompletedDatabase() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + FromDatabaseSegment fromDatabaseSegment = new FromDatabaseSegment(0, new DatabaseSegment(0, 0, new IdentifierValue("uncompleted"))); + sqlStatement.setFromDatabase(fromDatabaseSegment); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + executor.execute(mockConnectionSession("uncompleted"), mockMetaData(createDatabases())); + } + + @Test + void assertTransactionStatusConstants() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertWhereIdFilterWithNumberLiteral() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertWhereIdFilterWithStringLiteral() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", "1001"); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertWhereIdFilterCaseInsensitive() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("ID", 1001L); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertWhereLabelFilterCaseInsensitive() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("LABEL", "test_label"); + sqlStatement.setWhere(whereSegment); + MySQLShowTransactionExecutor executor = new MySQLShowTransactionExecutor(sqlStatement); + assertThrows(UnsupportedOperationException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + } + + @Test + void assertQueryResultMetaDataWithFullColumnSet() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(Collections.emptyList()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + QueryResultMetaData metaData = executor.getQueryResultMetaData(); + assertThat(metaData.getColumnCount(), is(12)); + assertThat(metaData.getColumnLabel(1), is("TransactionId")); + assertThat(metaData.getColumnType(1), is(Types.BIGINT)); + assertThat(metaData.getColumnLabel(2), is("Label")); + assertThat(metaData.getColumnType(2), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(3), is("Coordinator")); + assertThat(metaData.getColumnType(3), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(4), is("TransactionStatus")); + assertThat(metaData.getColumnType(4), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(5), is("LoadJobSourceType")); + assertThat(metaData.getColumnType(5), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(6), is("PrepareTime")); + assertThat(metaData.getColumnType(6), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(7), is("CommitTime")); + assertThat(metaData.getColumnType(7), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(8), is("FinishTime")); + assertThat(metaData.getColumnType(8), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(9), is("Reason")); + assertThat(metaData.getColumnType(9), is(Types.VARCHAR)); + assertThat(metaData.getColumnLabel(10), is("ErrorReplicasCount")); + assertThat(metaData.getColumnType(10), is(Types.INTEGER)); + assertThat(metaData.getColumnLabel(11), is("ListenerId")); + assertThat(metaData.getColumnType(11), is(Types.BIGINT)); + assertThat(metaData.getColumnLabel(12), is("TimeoutMs")); + assertThat(metaData.getColumnType(12), is(Types.BIGINT)); + } + + @Test + void assertExecuteWithDataPresent() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1001")); + assertThat(mergedResult.getValue(2, String.class), is("load_job_1")); + assertThat(mergedResult.getValue(3, String.class), is("coordinator_node_1")); + assertThat(mergedResult.getValue(4, String.class), is("VISIBLE")); + assertFalse(mergedResult.next()); + } + + @Test + void assertExecuteWithDataAbsent() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(Collections.emptyList()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereIdFilterHit() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 1001L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1001")); + assertThat(mergedResult.getValue(2, String.class), is("load_job_1")); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereIdFilterMiss() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", 9999L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereLabelFilterHit() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("label", "load_job_2"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1002")); + assertThat(mergedResult.getValue(2, String.class), is("load_job_2")); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereLabelFilterMiss() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("label", "non_existent_label"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereIdWithStringLiteralFilterHit() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", "1002"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1002")); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereIdCaseInsensitiveFilterHit() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("ID", 1001L); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1001")); + assertFalse(mergedResult.next()); + } + + @Test + void assertWhereLabelCaseInsensitiveFilterHit() throws SQLException { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("LABEL", "load_job_1"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases())); + MergedResult mergedResult = executor.getMergedResult(); + assertTrue(mergedResult.next()); + assertThat(mergedResult.getValue(1, String.class), is("1001")); + assertThat(mergedResult.getValue(2, String.class), is("load_job_1")); + assertFalse(mergedResult.next()); + } + + @Test + void assertMissingWhereClause() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + assertThat(exception.getMessage(), is("SHOW TRANSACTION requires WHERE clause with 'id = ' or 'label = '")); + } + + @Test + void assertWhereWithUnsupportedColumn() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("status", "VISIBLE"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + assertThat(exception.getMessage(), is("WHERE clause only supports 'id' or 'label' columns, got: status")); + } + + @Test + void assertWhereWithInvalidIdValue() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + WhereSegment whereSegment = createWhereSegment("id", "not_a_number"); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + assertThat(exception.getMessage(), is("Invalid transaction id value: not_a_number")); + } + + @Test + void assertWhereWithNonEqualityOperator() { + MySQLShowTransactionStatement sqlStatement = new MySQLShowTransactionStatement(databaseType); + ColumnSegment columnSegment = new ColumnSegment(0, 0, new IdentifierValue("id")); + LiteralExpressionSegment literalSegment = new LiteralExpressionSegment(0, 0, 1000L); + BinaryOperationExpression expression = new BinaryOperationExpression(0, 0, columnSegment, literalSegment, ">", ""); + WhereSegment whereSegment = new WhereSegment(0, 0, expression); + sqlStatement.setWhere(whereSegment); + TestableShowTransactionExecutor executor = new TestableShowTransactionExecutor(sqlStatement); + executor.setTestData(createMockTransactions()); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> executor.execute(mockConnectionSession(DATABASE_NAME), mockMetaData(createDatabases()))); + assertThat(exception.getMessage(), is("WHERE clause only supports '=' operator, got: >")); + } + + private WhereSegment createWhereSegment(final String columnName, final Object value) { + ColumnSegment columnSegment = new ColumnSegment(0, 0, new IdentifierValue(columnName)); + LiteralExpressionSegment literalSegment = new LiteralExpressionSegment(0, 0, value); + BinaryOperationExpression expression = new BinaryOperationExpression(0, 0, columnSegment, literalSegment, "=", ""); + return new WhereSegment(0, 0, expression); + } + + private ShardingSphereMetaData mockMetaData(final Collection databases) { + return new ShardingSphereMetaData(databases, mock(ResourceMetaData.class), new RuleMetaData(Collections.singleton(mock(AuthorityRule.class))), new ConfigurationProperties(new Properties())); + } + + private Collection createDatabases() { + ShardingSphereDatabase database = mock(ShardingSphereDatabase.class, RETURNS_DEEP_STUBS); + when(database.getName()).thenReturn(DATABASE_NAME); + when(database.getProtocolType()).thenReturn(databaseType); + when(database.isComplete()).thenReturn(true); + ShardingSphereDatabase uncompletedDatabase = new ShardingSphereDatabase("uncompleted", mock(), mock(), mock(), Collections.emptyList()); + return Arrays.asList(database, uncompletedDatabase); + } + + private ConnectionSession mockConnectionSession(final String usedDatabaseName) { + ConnectionSession result = mock(ConnectionSession.class, RETURNS_DEEP_STUBS); + when(result.getUsedDatabaseName()).thenReturn(usedDatabaseName); + return result; + } + + private Collection createMockTransactions() { + List mockTransactions = new ArrayList<>(2); + mockTransactions.add(new MySQLShowTransactionExecutor.TransactionInfo( + 1001L, "load_job_1", "coordinator_node_1", "VISIBLE", "ROUTINE_LOAD", + "2025-01-01 10:00:00", "2025-01-01 10:01:00", "2025-01-01 10:02:00", + "", 0, 10001L, 60000L)); + mockTransactions.add(new MySQLShowTransactionExecutor.TransactionInfo( + 1002L, "load_job_2", "coordinator_node_2", "COMMITTED", "BROKER_LOAD", + "2025-01-01 11:00:00", "2025-01-01 11:01:00", "", + "", 0, 10002L, 120000L)); + return mockTransactions; + } + + /** + * Testable executor for testing data flow and filtering logic. + */ + static class TestableShowTransactionExecutor extends MySQLShowTransactionExecutor { + + private Collection testData; + + TestableShowTransactionExecutor(final MySQLShowTransactionStatement sqlStatement) { + super(sqlStatement); + } + + void setTestData(final Collection data) { + this.testData = data; + } + + @Override + protected Collection loadTransactions() { + if (null != testData) { + return testData; + } + return super.loadTransactions(); + } + } +} diff --git a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/MySQLDALStatementAssert.java b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/MySQLDALStatementAssert.java index 5f63a968529a2..11c6c8380fef1 100644 --- a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/MySQLDALStatementAssert.java +++ b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/MySQLDALStatementAssert.java @@ -47,6 +47,7 @@ import org.apache.shardingsphere.sql.parser.statement.mysql.dal.resource.MySQLCreateResourceGroupStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.resource.MySQLDropResourceGroupStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.resource.MySQLSetResourceGroupStatement; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.character.MySQLShowCollationStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.column.MySQLDescribeStatement; import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.column.MySQLShowColumnsStatement; @@ -111,6 +112,7 @@ import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowStatusStatementAssert; import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowTableStatusStatementAssert; import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowTablesStatementAssert; +import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowTransactionStatementAssert; import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowTriggersStatementAssert; import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShowVariablesStatementAssert; import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type.MySQLShutdownStatementAssert; @@ -152,6 +154,7 @@ import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.event.MySQLShowEventsStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.function.MySQLShowFunctionStatusStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.index.MySQLShowIndexStatementTestCase; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.MySQLShowTransactionStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.privilege.MySQLShowCreateUserStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.procedure.MySQLShowProcedureCodeStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.procedure.MySQLShowProcedureStatusStatementTestCase; @@ -277,6 +280,8 @@ public static void assertIs(final SQLCaseAssertContext assertContext, final DALS MySQLDelimiterStatementAssert.assertIs(assertContext, (MySQLDelimiterStatement) actual, (MySQLDelimiterStatementTestCase) expected); } else if (actual instanceof MySQLShowBinlogEventsStatement) { MySQLShowBinlogEventsStatementAssert.assertIs(assertContext, (MySQLShowBinlogEventsStatement) actual, (MySQLShowBinlogEventsStatementTestCase) expected); + } else if (actual instanceof MySQLShowTransactionStatement) { + MySQLShowTransactionStatementAssert.assertIs(assertContext, (MySQLShowTransactionStatement) actual, (MySQLShowTransactionStatementTestCase) expected); } } } diff --git a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/type/MySQLShowTransactionStatementAssert.java b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/type/MySQLShowTransactionStatementAssert.java new file mode 100644 index 0000000000000..ca3ae7e7202c9 --- /dev/null +++ b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/asserts/statement/dal/dialect/mysql/type/MySQLShowTransactionStatementAssert.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.test.it.sql.parser.internal.asserts.statement.dal.dialect.mysql.type; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.shardingsphere.sql.parser.statement.mysql.dal.show.MySQLShowTransactionStatement; +import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.SQLCaseAssertContext; +import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.segment.SQLSegmentAssert; +import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.segment.database.DatabaseAssert; +import org.apache.shardingsphere.test.it.sql.parser.internal.asserts.segment.where.WhereClauseAssert; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.MySQLShowTransactionStatementTestCase; + +/** + * Show transaction statement assert for MySQL. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class MySQLShowTransactionStatementAssert { + + /** + * Assert show transaction statement is correct with expected parser result. + * + * @param assertContext assert context + * @param actual actual show transaction statement + * @param expected expected show transaction statement test case + */ + public static void assertIs(final SQLCaseAssertContext assertContext, final MySQLShowTransactionStatement actual, final MySQLShowTransactionStatementTestCase expected) { + if (null != actual.getFromDatabase()) { + DatabaseAssert.assertIs(assertContext, actual.getFromDatabase().getDatabase(), expected.getFromDatabase().getDatabase()); + SQLSegmentAssert.assertIs(assertContext, actual.getFromDatabase(), expected.getFromDatabase()); + } + if (null != actual.getWhere()) { + WhereClauseAssert.assertIs(assertContext, actual.getWhere(), expected.getWhere()); + } + } +} diff --git a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/RootSQLParserTestCases.java b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/RootSQLParserTestCases.java index e76a915e849f9..95a4cc56c4f3e 100644 --- a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/RootSQLParserTestCases.java +++ b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/RootSQLParserTestCases.java @@ -68,6 +68,7 @@ import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.trigger.MySQLShowCreateTriggerStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.trigger.MySQLShowTriggersStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.variable.MySQLShowVariablesStatementTestCase; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show.MySQLShowTransactionStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.table.MySQLCheckTableStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.table.MySQLChecksumTableStatementTestCase; import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.table.MySQLOptimizeTableStatementTestCase; @@ -671,6 +672,9 @@ public final class RootSQLParserTestCases { @XmlElement(name = "show-index") private final List showIndexTestCases = new LinkedList<>(); + @XmlElement(name = "show-transaction") + private final List showTransactionTestCases = new LinkedList<>(); + @XmlElement(name = "show") private final List showTestCases = new LinkedList<>(); diff --git a/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/statement/dal/dialect/mysql/show/MySQLShowTransactionStatementTestCase.java b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/statement/dal/dialect/mysql/show/MySQLShowTransactionStatementTestCase.java new file mode 100644 index 0000000000000..58d6b3872a37a --- /dev/null +++ b/test/it/parser/src/main/java/org/apache/shardingsphere/test/it/sql/parser/internal/cases/parser/jaxb/statement/dal/dialect/mysql/show/MySQLShowTransactionStatementTestCase.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.statement.dal.dialect.mysql.show; + +import lombok.Getter; +import lombok.Setter; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.SQLParserTestCase; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.segment.impl.from.ExpectedFromDatabase; +import org.apache.shardingsphere.test.it.sql.parser.internal.cases.parser.jaxb.segment.impl.where.ExpectedWhereClause; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Show transaction statement test case for MySQL. + */ +@Getter +@Setter +public final class MySQLShowTransactionStatementTestCase extends SQLParserTestCase { + + @XmlElement(name = "from") + private ExpectedFromDatabase fromDatabase; + + @XmlElement(name = "where") + private ExpectedWhereClause where; +} diff --git a/test/it/parser/src/main/resources/case/dal/show.xml b/test/it/parser/src/main/resources/case/dal/show.xml index 158f7b6b9a743..72cb17c1ab193 100644 --- a/test/it/parser/src/main/resources/case/dal/show.xml +++ b/test/it/parser/src/main/resources/case/dal/show.xml @@ -1098,4 +1098,82 @@ + + + + + + + + + + + + + + + + + = + + + + + + + + + + + + + + + + = + + + + + + + + + + + + + + + + + + + = + + + + + + + + + + + + + + + + + + + = + + + + + + + diff --git a/test/it/parser/src/main/resources/case/dml/select-special-function.xml b/test/it/parser/src/main/resources/case/dml/select-special-function.xml index 980c53490c4f3..102437193bce8 100644 --- a/test/it/parser/src/main/resources/case/dml/select-special-function.xml +++ b/test/it/parser/src/main/resources/case/dml/select-special-function.xml @@ -5304,4 +5304,92 @@ + + + + diff --git a/test/it/parser/src/main/resources/sql/supported/dal/show.xml b/test/it/parser/src/main/resources/sql/supported/dal/show.xml index 0ebdfa0697bd5..0c86e33f4d2da 100644 --- a/test/it/parser/src/main/resources/sql/supported/dal/show.xml +++ b/test/it/parser/src/main/resources/sql/supported/dal/show.xml @@ -235,4 +235,10 @@ + + + + + + diff --git a/test/it/parser/src/main/resources/sql/supported/dml/select-special-function.xml b/test/it/parser/src/main/resources/sql/supported/dml/select-special-function.xml index 3944d08c6e2ee..2105eacefdb10 100644 --- a/test/it/parser/src/main/resources/sql/supported/dml/select-special-function.xml +++ b/test/it/parser/src/main/resources/sql/supported/dml/select-special-function.xml @@ -305,4 +305,6 @@ + +