From c9906e96b3c4a0657a58b48f6b7d45c9f9a4aed4 Mon Sep 17 00:00:00 2001 From: Zhengqiang Duan Date: Tue, 5 Dec 2023 20:25:42 +0800 Subject: [PATCH] Throw exception when placeholders exceed 65535 in MySQL prepared statement (#29296) * Throw exception when placeholders exceed 65535 in MySQL prepared statement * Update license --- .../TooManyPlaceholdersException.java | 28 +++++++++++++++++++ .../mapper/MySQLDialectExceptionMapper.java | 8 ++++-- .../mysql/vendor/MySQLVendorError.java | 2 ++ .../prepare/MySQLComStmtPrepareExecutor.java | 5 ++++ .../MySQLComStmtPrepareExecutorTest.java | 23 +++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/exception/TooManyPlaceholdersException.java diff --git a/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/exception/TooManyPlaceholdersException.java b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/exception/TooManyPlaceholdersException.java new file mode 100644 index 0000000000000..5115a9901e7e1 --- /dev/null +++ b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/exception/TooManyPlaceholdersException.java @@ -0,0 +1,28 @@ +/* + * 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.infra.exception.mysql.exception; + +import org.apache.shardingsphere.infra.exception.dialect.exception.SQLDialectException; + +/** + * Too many placeholders exception. + */ +public final class TooManyPlaceholdersException extends SQLDialectException { + + private static final long serialVersionUID = -220866708278903418L; +} diff --git a/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/mapper/MySQLDialectExceptionMapper.java b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/mapper/MySQLDialectExceptionMapper.java index ecd3b7132cedb..0b61b5152658c 100644 --- a/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/mapper/MySQLDialectExceptionMapper.java +++ b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/mapper/MySQLDialectExceptionMapper.java @@ -17,6 +17,8 @@ package org.apache.shardingsphere.infra.exception.mysql.mapper; +import org.apache.shardingsphere.infra.exception.core.external.sql.type.generic.UnknownSQLException; +import org.apache.shardingsphere.infra.exception.core.external.sql.vendor.VendorError; import org.apache.shardingsphere.infra.exception.dialect.exception.SQLDialectException; import org.apache.shardingsphere.infra.exception.dialect.exception.connection.TooManyConnectionsException; import org.apache.shardingsphere.infra.exception.dialect.exception.data.InsertColumnsAndValuesMismatchedException; @@ -38,9 +40,8 @@ import org.apache.shardingsphere.infra.exception.mysql.exception.UnknownCollationException; import org.apache.shardingsphere.infra.exception.mysql.exception.UnknownSystemVariableException; import org.apache.shardingsphere.infra.exception.mysql.exception.UnsupportedPreparedStatementException; +import org.apache.shardingsphere.infra.exception.mysql.exception.TooManyPlaceholdersException; import org.apache.shardingsphere.infra.exception.mysql.vendor.MySQLVendorError; -import org.apache.shardingsphere.infra.exception.core.external.sql.type.generic.UnknownSQLException; -import org.apache.shardingsphere.infra.exception.core.external.sql.vendor.VendorError; import java.sql.SQLException; @@ -83,6 +84,9 @@ public SQLException convert(final SQLDialectException sqlDialectException) { if (sqlDialectException instanceof UnsupportedPreparedStatementException) { return toSQLException(MySQLVendorError.ER_UNSUPPORTED_PS); } + if (sqlDialectException instanceof TooManyPlaceholdersException) { + return toSQLException(MySQLVendorError.ER_PS_MANY_PARAM); + } if (sqlDialectException instanceof UnknownCharsetException) { return toSQLException(MySQLVendorError.ER_UNKNOWN_CHARACTER_SET, ((UnknownCharsetException) sqlDialectException).getCharset()); } diff --git a/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/vendor/MySQLVendorError.java b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/vendor/MySQLVendorError.java index ace2d4f5f9a0b..67a9489bc8111 100644 --- a/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/vendor/MySQLVendorError.java +++ b/infra/exception/dialect/type/mysql/src/main/java/org/apache/shardingsphere/infra/exception/mysql/vendor/MySQLVendorError.java @@ -70,6 +70,8 @@ public enum MySQLVendorError implements VendorError { ER_UNKNOWN_COLLATION(XOpenSQLState.GENERAL_ERROR, 1273, "Unknown collation: '%s'"), + ER_PS_MANY_PARAM(XOpenSQLState.GENERAL_ERROR, 1390, "Prepared statement contains too many placeholders"), + ER_ERROR_ON_MODIFYING_GTID_EXECUTED_TABLE(XOpenSQLState.GENERAL_ERROR, 3176, "Please do not modify the %s table with an XA transaction. This is an internal system table used to store GTIDs for committed transactions. " + "Although modifying it can lead to an inconsistent GTID state, if necessary you can modify it with a non-XA transaction."); diff --git a/proxy/frontend/type/mysql/src/main/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutor.java b/proxy/frontend/type/mysql/src/main/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutor.java index 8e63f755af3d8..ee174ed68571b 100644 --- a/proxy/frontend/type/mysql/src/main/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutor.java +++ b/proxy/frontend/type/mysql/src/main/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutor.java @@ -35,6 +35,8 @@ import org.apache.shardingsphere.infra.binder.engine.SQLBindEngine; import org.apache.shardingsphere.infra.database.core.type.DatabaseType; import org.apache.shardingsphere.infra.database.core.type.DatabaseTypeRegistry; +import org.apache.shardingsphere.infra.exception.core.ShardingSpherePreconditions; +import org.apache.shardingsphere.infra.exception.mysql.exception.TooManyPlaceholdersException; import org.apache.shardingsphere.infra.exception.mysql.exception.UnsupportedPreparedStatementException; import org.apache.shardingsphere.infra.metadata.database.ShardingSphereDatabase; import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereColumn; @@ -66,6 +68,8 @@ @RequiredArgsConstructor public final class MySQLComStmtPrepareExecutor implements CommandExecutor { + private static final int MAX_PARAMETER_COUNT = 65535; + private final MySQLComStmtPreparePacket packet; private final ConnectionSession connectionSession; @@ -101,6 +105,7 @@ private Collection createPackets(final SQLStatementContext sqlSt Collection result = new LinkedList<>(); Collection projections = getProjections(sqlStatementContext); int parameterCount = sqlStatementContext.getSqlStatement().getParameterCount(); + ShardingSpherePreconditions.checkState(parameterCount <= MAX_PARAMETER_COUNT, TooManyPlaceholdersException::new); result.add(new MySQLComStmtPrepareOKPacket(statementId, projections.size(), parameterCount, 0)); int characterSet = connectionSession.getAttributeMap().attr(MySQLConstants.MYSQL_CHARACTER_SET_ATTRIBUTE_KEY).get().getId(); int statusFlags = ServerStatusFlagCalculator.calculateFor(connectionSession); diff --git a/proxy/frontend/type/mysql/src/test/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutorTest.java b/proxy/frontend/type/mysql/src/test/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutorTest.java index 6b1a36ed78d3b..dd011c7bee02a 100644 --- a/proxy/frontend/type/mysql/src/test/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutorTest.java +++ b/proxy/frontend/type/mysql/src/test/java/org/apache/shardingsphere/proxy/frontend/mysql/command/query/binary/prepare/MySQLComStmtPrepareExecutorTest.java @@ -32,6 +32,7 @@ import org.apache.shardingsphere.infra.binder.context.statement.dml.SelectStatementContext; import org.apache.shardingsphere.infra.binder.context.statement.dml.UpdateStatementContext; import org.apache.shardingsphere.infra.database.core.type.DatabaseType; +import org.apache.shardingsphere.infra.exception.mysql.exception.TooManyPlaceholdersException; import org.apache.shardingsphere.infra.exception.mysql.exception.UnsupportedPreparedStatementException; import org.apache.shardingsphere.infra.hint.HintValueContext; import org.apache.shardingsphere.infra.metadata.database.ShardingSphereDatabase; @@ -165,6 +166,28 @@ private int getColumnDefinitionFlag(final MySQLColumnDefinition41Packet packet) return byteBuf.getUnsignedShortLE(17); } + @Test + void assertPrepareInsertStatementWithTooManyPlaceholders() { + String sql = createTooManyPlaceholdersSQL(); + when(packet.getSQL()).thenReturn(sql); + when(packet.getHintValueContext()).thenReturn(new HintValueContext()); + int connectionId = 2; + when(connectionSession.getConnectionId()).thenReturn(connectionId); + when(connectionSession.getDefaultDatabaseName()).thenReturn("foo_db"); + MySQLStatementIdGenerator.getInstance().registerConnection(connectionId); + ContextManager contextManager = mockContextManager(); + when(ProxyContext.getInstance().getContextManager()).thenReturn(contextManager); + assertThrows(TooManyPlaceholdersException.class, () -> new MySQLComStmtPrepareExecutor(packet, connectionSession).execute()); + } + + private String createTooManyPlaceholdersSQL() { + StringBuilder builder = new StringBuilder("INSERT INTO USER (ID, NAME, AGE) VALUES (?, ?, ?)"); + for (int index = 0; index < Short.MAX_VALUE; index++) { + builder.append(", (?, ?, ?)"); + } + return builder.toString(); + } + @Test void assertPrepareUpdateStatement() { String sql = "update user set name = ?, age = ? where id = ?";