From 98a509227ffcab7e41b5d4c50cb5f7a32221bd75 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Fri, 30 Jun 2023 11:40:05 +0530 Subject: [PATCH] fix: fix MySQL stale connection error (#24429) ## Description - Add changes to address the`StaleConnection` exception caused by MySQL plugin. - Update MySQL driver version. - Other refactor changes not related to the main issue: - Explicit empty constructor definition is replaced with Lombok annotation for all error messages class. - A base class is created for plugin error messages class to store all common error messages. - Fix Indentation. --- .../external/constants/PluginConstants.java | 20 + .../BasePluginErrorMessages.java | 13 + .../StaleConnectionException.java | 12 +- .../external/helpers/PluginUtilsTest.java | 2 - .../com/external/plugins/AmazonS3Plugin.java | 10 +- .../plugins/exceptions/S3ErrorMessages.java | 12 +- .../com/external/plugins/ArangoDBPlugin.java | 4 +- .../exceptions/ArangoDBErrorMessages.java | 11 +- .../exceptions/DynamoErrorMessages.java | 9 +- .../ElasticSearchErrorMessages.java | 9 +- .../exceptions/FirestoreErrorMessages.java | 9 +- .../com/external/constants/ErrorMessages.java | 7 +- .../exceptions/GraphQLErrorMessages.java | 10 +- .../com/external/plugins/MongoPlugin.java | 12 +- .../exceptions/MongoPluginErrorMessages.java | 2 + .../appsmith-plugins/mssqlPlugin/pom.xml | 1 - .../com/external/plugins/MssqlPlugin.java | 30 +- .../exceptions/MssqlErrorMessages.java | 9 +- .../plugins/utils/MssqlDatasourceUtils.java | 61 +- .../plugins/MssqlTestDBContainerManager.java | 8 +- .../appsmith-plugins/mysqlPlugin/pom.xml | 2 +- .../com/external/plugins/MySqlPlugin.java | 112 +- .../exceptions/MySQLErrorMessages.java | 15 +- .../external/utils/MySqlDatasourceUtils.java | 34 +- .../com/external/plugins/MySqlPluginTest.java | 2844 ++++++++--------- .../MySqlStaleConnectionErrorMessageTest.java | 117 + .../com/external/plugins/OraclePlugin.java | 10 +- .../exceptions/OracleErrorMessages.java | 12 +- .../plugins/utils/OracleDatasourceUtils.java | 59 +- .../plugins/OraclePluginErrorsTest.java | 71 + .../plugins/OracleTestDBContainerManager.java | 9 +- .../com/external/plugins/PostgresPlugin.java | 42 +- .../exceptions/PostgresErrorMessages.java | 10 +- .../utils/PostgresDatasourceUtils.java | 47 + .../exceptions/RedisErrorMessages.java | 9 +- .../appsmith-plugins/redshiftPlugin/pom.xml | 1 - .../com/external/plugins/RedshiftPlugin.java | 20 +- .../exceptions/RedshiftErrorMessages.java | 11 +- .../utils/RedshiftDatasourceUtils.java | 38 +- .../exceptions/RestApiErrorMessages.java | 10 +- .../plugins/exceptions/SaaSErrorMessages.java | 9 +- .../plugins/exceptions/SMTPErrorMessages.java | 8 +- .../com/external/plugins/SnowflakePlugin.java | 49 +- .../exceptions/SnowflakeErrorMessages.java | 14 +- .../com/external/utils/ExecutionUtils.java | 6 +- .../utils/SnowflakeDatasourceUtils.java | 47 + .../ce/ActionExecutionSolutionCEImpl.java | 2 +- 47 files changed, 2162 insertions(+), 1697 deletions(-) create mode 100644 app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/BasePluginErrorMessages.java create mode 100644 app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlStaleConnectionErrorMessageTest.java create mode 100644 app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/utils/PostgresDatasourceUtils.java create mode 100644 app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/SnowflakeDatasourceUtils.java diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java index bc67040cf0..fbf25c10d2 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/PluginConstants.java @@ -11,4 +11,24 @@ public interface PluginConstants { String AMAZON_S3_PLUGIN = "amazons3-plugin"; String GOOGLE_SHEETS_PLUGIN = "google-sheets-plugin"; } + + interface PluginName { + public static final String S3_PLUGIN_NAME = "S3"; + public static final String ARANGO_PLUGIN_NAME = "Arango"; + public static final String DYNAMO_PLUGIN_NAME = "Dynamo"; + public static final String ELASTIC_SEARCH_PLUGIN_NAME = "ElasticSearch"; + public static final String FIRESTORE_PLUGIN_NAME = "Firestore"; + public static final String GOOGLE_SHEETS_PLUGIN_NAME = "GoogleSheets"; + public static final String GRAPHQL_PLUGIN_NAME = "Graphql"; + public static final String MSSQL_PLUGIN_NAME = "Mssql"; + public static final String MYSQL_PLUGIN_NAME = "Mysql"; + public static final String ORACLE_PLUGIN_NAME = "Oracle"; + public static final String POSTGRES_PLUGIN_NAME = "Postgres"; + public static final String REDIS_PLUGIN_NAME = "Redis"; + public static final String REDSHIFT_PLUGIN_NAME = "Redshift"; + public static final String REST_API_PLUGIN_NAME = "RestApi"; + public static final String SAAS_PLUGIN_NAME = "Saas"; + public static final String SMTP_PLUGIN_NAME = "Smtp"; + public static final String SNOWFLAKE_PLUGIN_NAME = "Snowflake"; + } } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/BasePluginErrorMessages.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/BasePluginErrorMessages.java new file mode 100644 index 0000000000..30561b4a20 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/BasePluginErrorMessages.java @@ -0,0 +1,13 @@ +package com.appsmith.external.exceptions.pluginExceptions; + +public abstract class BasePluginErrorMessages { + public static final String CONNECTION_INVALID_ERROR_MSG = "Connection object is invalid."; + public static final String CONNECTION_NULL_ERROR_MSG = "Connection object is null."; + public static final String CONNECTION_CLOSED_ERROR_MSG = "Connection object is closed."; + public static final String CONNECTION_POOL_NULL_ERROR_MSG = "Connection pool is null."; + public static final String CONNECTION_POOL_CLOSED_ERROR_MSG = "Connection pool is closed."; + public static final String CONNECTION_POOL_NOT_RUNNING_ERROR_MSG = "Connection pool is not running."; + public static final String UNKNOWN_CONNECTION_ERROR_MSG = "Unknown connection error. Please reach out to Appsmith " + + "customer support to resolve this."; + public static final String JDBC_DRIVER_LOADING_ERROR_MSG = "Error loading JDBC Driver class."; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/StaleConnectionException.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/StaleConnectionException.java index eeb65da733..bf11c450a6 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/StaleConnectionException.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/StaleConnectionException.java @@ -1,14 +1,22 @@ package com.appsmith.external.exceptions.pluginExceptions; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor public class StaleConnectionException extends RuntimeException { - public StaleConnectionException() { - } + String message = ""; public StaleConnectionException(String message) { super(message); + this.message = message; } public StaleConnectionException(String message, Throwable cause) { super(message, cause); + this.message = message; } } diff --git a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/PluginUtilsTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/PluginUtilsTest.java index 10552d1491..83f0504b86 100644 --- a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/PluginUtilsTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/PluginUtilsTest.java @@ -5,9 +5,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.models.Condition; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.extern.slf4j.Slf4j; - import org.junit.jupiter.api.Test; import java.io.IOException; diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java index d862a695d2..c6e75a9303 100644 --- a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java +++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java @@ -71,10 +71,10 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import java.util.LinkedHashMap; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_PATH; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_NULL_ERROR_MSG; import static com.appsmith.external.helpers.PluginUtils.OBJECT_TYPE; import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE; import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData; @@ -459,7 +459,7 @@ public class AmazonS3Plugin extends BasePlugin { * - If connection object is null, then assume stale connection. */ if (connection == null) { - return Mono.error(new StaleConnectionException()); + return Mono.error(new StaleConnectionException(CONNECTION_NULL_ERROR_MSG)); } if (actionConfiguration == null) { @@ -787,7 +787,7 @@ public class AmazonS3Plugin extends BasePlugin { } return Mono.just(actionResult); }) - .onErrorMap(IllegalStateException.class, error -> new StaleConnectionException()) + .onErrorMap(IllegalStateException.class, error -> new StaleConnectionException(error.getMessage())) .flatMap(obj -> obj) .flatMap(result -> { ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); @@ -986,8 +986,8 @@ public class AmazonS3Plugin extends BasePlugin { S3ErrorMessages.LIST_OF_BUCKET_FETCHING_ERROR_MSG, e.getMessage() ); - } catch (IllegalStateException s) { - throw new StaleConnectionException(); + } catch (IllegalStateException e) { + throw new StaleConnectionException(e.getMessage()); } return new DatasourceStructure(tableList); diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/exceptions/S3ErrorMessages.java b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/exceptions/S3ErrorMessages.java index 2ea84387fb..a980544788 100644 --- a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/exceptions/S3ErrorMessages.java +++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/exceptions/S3ErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class S3ErrorMessages { - private S3ErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class S3ErrorMessages extends BasePluginErrorMessages { public static final String FILE_CONTENT_FETCHING_ERROR_MSG = "Appsmith server has encountered an unexpected error when fetching file " + "content from AWS S3 server. Please reach out to Appsmith customer support to resolve this."; @@ -99,6 +101,4 @@ public class S3ErrorMessages { public static final String DS_MANDATORY_PARAMETER_ENDPOINT_URL_MISSING_ERROR_MSG = "Required parameter 'Endpoint URL' is empty. Did you forget to edit the 'Endpoint" + " URL' field in the datasource creation form ? You need to fill it with " + "the endpoint URL of your S3 instance."; - - } diff --git a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/ArangoDBPlugin.java b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/ArangoDBPlugin.java index b5881cfbb1..b9037bc200 100644 --- a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/ArangoDBPlugin.java +++ b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/ArangoDBPlugin.java @@ -26,7 +26,6 @@ import com.arangodb.model.CollectionsReadOptions; import com.external.plugins.exceptions.ArangoDBErrorMessages; import com.external.plugins.exceptions.ArangoDBPluginError; import com.external.utils.ArangoDBErrorUtils; -import java.util.Properties; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ObjectUtils; import org.pf4j.Extension; @@ -51,6 +50,7 @@ import java.util.stream.Collectors; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; import static com.appsmith.external.helpers.PluginUtils.MATCH_QUOTED_WORDS_REGEX; +import static com.external.plugins.exceptions.ArangoDBErrorMessages.CONNECTION_INVALID_ERROR_MSG; import static com.external.plugins.exceptions.ArangoDBErrorMessages.DS_HOSTNAME_MISSING_OR_INVALID_ERROR_MSG; import static com.external.utils.SSLUtils.isCaCertificateAvailable; import static com.external.utils.SSLUtils.setSSLContext; @@ -84,7 +84,7 @@ public class ArangoDBPlugin extends BasePlugin { ActionConfiguration actionConfiguration) { if (!isConnectionValid(db)) { - return Mono.error(new StaleConnectionException()); + return Mono.error(new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG)); } String query = actionConfiguration.getBody(); diff --git a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/exceptions/ArangoDBErrorMessages.java b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/exceptions/ArangoDBErrorMessages.java index 8a0ecd3957..5ec02396df 100644 --- a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/exceptions/ArangoDBErrorMessages.java +++ b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/plugins/exceptions/ArangoDBErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class ArangoDBErrorMessages { - private ArangoDBErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class ArangoDBErrorMessages extends BasePluginErrorMessages { public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query."; public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details."; @@ -20,7 +22,6 @@ public class ArangoDBErrorMessages { public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch list of collections from database. Please check " + "if the database credentials are valid and/or you have the required permissions."; - /* ************************************************************************************************************************************************ Error messages related to validation of datasource. diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/exceptions/DynamoErrorMessages.java b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/exceptions/DynamoErrorMessages.java index b0f4ea815c..7be8f9a2da 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/exceptions/DynamoErrorMessages.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/exceptions/DynamoErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class DynamoErrorMessages { - private DynamoErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class DynamoErrorMessages extends BasePluginErrorMessages { public static final String MISSING_ACTION_NAME_ERROR_MSG = "Missing action name (like `ListTables`, `GetItem` etc.)."; public static final String UNKNOWN_ACTION_NAME_ERROR_MSG = "Unknown action: `%s`. Note that action names are case-sensitive."; diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/exceptions/ElasticSearchErrorMessages.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/exceptions/ElasticSearchErrorMessages.java index fd7991e7af..a15923a69a 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/exceptions/ElasticSearchErrorMessages.java +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/exceptions/ElasticSearchErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class ElasticSearchErrorMessages { - private ElasticSearchErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class ElasticSearchErrorMessages extends BasePluginErrorMessages { public static final String ARRAY_TO_ND_JSON_ARRAY_CONVERSION_ERROR_MSG = "Error occurred while converting array to ND-JSON"; public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Error occurred while executing Elasticsearch query."; diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/exceptions/FirestoreErrorMessages.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/exceptions/FirestoreErrorMessages.java index a1979e0d08..9ebdae4824 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/exceptions/FirestoreErrorMessages.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/exceptions/FirestoreErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class FirestoreErrorMessages { - private FirestoreErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class FirestoreErrorMessages extends BasePluginErrorMessages { public static final String MANDATORY_PARAM_COMMAND_MISSING_ERROR_MSG = "Mandatory parameter 'Command' is missing. Did you forget to select one of the commands" + " from the Command dropdown ?"; diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/constants/ErrorMessages.java b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/constants/ErrorMessages.java index 4b7235ecb2..db991bcf78 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/constants/ErrorMessages.java +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/constants/ErrorMessages.java @@ -1,6 +1,11 @@ package com.external.constants; -public class ErrorMessages { +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class ErrorMessages extends BasePluginErrorMessages { public static final String EMPTY_ROW_OBJECT_MESSAGE = "Row object(s) cannot be empty."; diff --git a/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/exceptions/GraphQLErrorMessages.java b/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/exceptions/GraphQLErrorMessages.java index f7022b5f51..61365a37d7 100644 --- a/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/exceptions/GraphQLErrorMessages.java +++ b/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/exceptions/GraphQLErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class GraphQLErrorMessages { - private GraphQLErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class GraphQLErrorMessages extends BasePluginErrorMessages { public static final String URI_SYNTAX_WRONG_ERROR_MSG = "URI is invalid. Please rectify the URI and try again."; public static final String INVALID_CONTENT_TYPE_ERROR_MSG = "Invalid value for Content-Type."; public static final String NO_HTTP_METHOD_ERROR_MSG = "HTTPMethod must be set."; diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index 0359bf67d0..50eafa6d99 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -116,6 +116,7 @@ import static com.external.plugins.constants.FieldName.SMART_SUBSTITUTION; import static com.external.plugins.constants.FieldName.SUCCESS; import static com.external.plugins.constants.FieldName.UPDATE_OPERATION; import static com.external.plugins.constants.FieldName.UPDATE_QUERY; +import static com.external.plugins.exceptions.MongoPluginErrorMessages.MONGO_CLIENT_NULL_ERROR_MSG; import static com.external.plugins.utils.DatasourceUtils.KEY_PASSWORD; import static com.external.plugins.utils.DatasourceUtils.KEY_URI_DEFAULT_DBNAME; import static com.external.plugins.utils.DatasourceUtils.KEY_USERNAME; @@ -134,7 +135,6 @@ import static com.external.plugins.utils.MongoPluginUtils.isRawCommand; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; import static org.apache.logging.log4j.util.Strings.isBlank; -import static org.apache.logging.log4j.util.Strings.isEmpty; public class MongoPlugin extends BasePlugin { @@ -310,7 +310,7 @@ public class MongoPlugin extends BasePlugin { if (mongoClient == null) { log.info("Encountered null connection in MongoDB plugin. Reporting back."); - throw new StaleConnectionException(); + throw new StaleConnectionException(MONGO_CLIENT_NULL_ERROR_MSG); } Mono mongoOutputMono; ActionExecutionResult result = new ActionExecutionResult(); @@ -354,13 +354,13 @@ public class MongoPlugin extends BasePlugin { */ .onErrorMap( IllegalStateException.class, - error -> new StaleConnectionException() + error -> new StaleConnectionException(error.getMessage()) ) // This is an experimental fix to handle the scenario where after a period of inactivity, the mongo // database drops the connection which makes the client throw the following exception. .onErrorMap( MongoSocketWriteException.class, - error -> new StaleConnectionException() + error -> new StaleConnectionException(error.getMessage()) ) .flatMap(mongoOutput -> { try { @@ -917,13 +917,13 @@ public class MongoPlugin extends BasePlugin { */ .onErrorMap( IllegalStateException.class, - error -> new StaleConnectionException() + error -> new StaleConnectionException(error.getMessage()) ) // This is an experimental fix to handle the scenario where after a period of inactivity, the mongo // database drops the connection which makes the client throw the following exception. .onErrorMap( MongoSocketWriteException.class, - error -> new StaleConnectionException() + error -> new StaleConnectionException(error.getMessage()) ) .onErrorMap( MongoCommandException.class, diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/exceptions/MongoPluginErrorMessages.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/exceptions/MongoPluginErrorMessages.java index d913aa6647..b44026acfe 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/exceptions/MongoPluginErrorMessages.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/exceptions/MongoPluginErrorMessages.java @@ -33,6 +33,8 @@ public class MongoPluginErrorMessages { public static final String QUERY_INVALID_ERROR_MSG = "Your query is invalid"; + public static final String MONGO_CLIENT_NULL_ERROR_MSG = "Mongo client object is null."; + /* ************************************************************************************************************************************************ Error messages related to validation of datasource. diff --git a/app/server/appsmith-plugins/mssqlPlugin/pom.xml b/app/server/appsmith-plugins/mssqlPlugin/pom.xml index e5ab9c5e4a..3ed26981c7 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/pom.xml +++ b/app/server/appsmith-plugins/mssqlPlugin/pom.xml @@ -27,7 +27,6 @@ com.zaxxer HikariCP 5.0.1 - compile org.slf4j diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java index 21ea029b5a..e67a194ded 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -66,10 +66,13 @@ import java.util.Set; import java.util.stream.IntStream; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME; import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel; import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex; -import static com.external.plugins.utils.MssqlDatasourceUtils.getConnectionFromConnectionPool; +import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_CLOSED_ERROR_MSG; +import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_INVALID_ERROR_MSG; +import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_NULL_ERROR_MSG; import static com.external.plugins.utils.MssqlDatasourceUtils.logHikariCPStatus; import static com.external.plugins.utils.MssqlExecuteUtils.closeConnectionPostExecution; import static java.lang.Boolean.FALSE; @@ -90,6 +93,8 @@ public class MssqlPlugin extends BasePlugin { private static final long MS_SQL_DEFAULT_PORT = 1433L; + public static final MssqlDatasourceUtils mssqlDatasourceUtils = new MssqlDatasourceUtils(); + public MssqlPlugin(PluginWrapper wrapper) { super(wrapper); } @@ -190,19 +195,36 @@ public class MssqlPlugin extends BasePlugin { final List columnsList = new ArrayList<>(); try { - sqlConnectionFromPool = getConnectionFromConnectionPool(hikariDSConnection); + sqlConnectionFromPool = + mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(hikariDSConnection, + MSSQL_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The underlying hikari // library throws SQLException in case the pool is closed or there is an issue initializing // the connection pool which can also be translated in our world to StaleConnectionException // and should then trigger the destruction and recreation of the pool. - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : + new StaleConnectionException(e.getMessage())); } try { if (sqlConnectionFromPool == null || sqlConnectionFromPool.isClosed() || !sqlConnectionFromPool.isValid(VALIDITY_CHECK_TIMEOUT)) { log.info("Encountered stale connection in MsSQL plugin. Reporting back."); - return Mono.error(new StaleConnectionException()); + + if (sqlConnectionFromPool == null) { + return Mono.error(new StaleConnectionException(CONNECTION_NULL_ERROR_MSG)); + } + else if (sqlConnectionFromPool.isClosed()) { + return Mono.error(new StaleConnectionException(CONNECTION_CLOSED_ERROR_MSG)); + } + else { + /** + * Not adding explicit `!sqlConnectionFromPool.isValid(VALIDITY_CHECK_TIMEOUT)` + * check here because this check may take few seconds to complete hence adding + * extra time delay. + */ + return Mono.error(new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG)); + } } } catch (SQLException error) { // This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case, diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/exceptions/MssqlErrorMessages.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/exceptions/MssqlErrorMessages.java index 2cb5d2f878..8c39e16b4b 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/exceptions/MssqlErrorMessages.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/exceptions/MssqlErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class MssqlErrorMessages { +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; - private MssqlErrorMessages() { - //Prevents instantiation - } +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class MssqlErrorMessages extends BasePluginErrorMessages { public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query."; public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details."; diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/utils/MssqlDatasourceUtils.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/utils/MssqlDatasourceUtils.java index d02e7874ba..51ae861a09 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/utils/MssqlDatasourceUtils.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/utils/MssqlDatasourceUtils.java @@ -8,8 +8,6 @@ import com.appsmith.external.models.DatasourceStructure; import com.external.plugins.exceptions.MssqlErrorMessages; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariPoolMXBean; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; @@ -24,11 +22,16 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP; import static com.external.plugins.MssqlPlugin.MssqlPluginExecutor.scheduler; +import static com.external.plugins.MssqlPlugin.mssqlDatasourceUtils; @Slf4j -@NoArgsConstructor(access = AccessLevel.PRIVATE) public class MssqlDatasourceUtils { public static final String PRIMARY_KEY_INDICATOR = "PRIMARY KEY"; @@ -94,13 +97,15 @@ public class MssqlDatasourceUtils { return Mono.fromSupplier(() -> { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connection); + connectionFromPool = mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(connection, + MSSQL_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The // underlying hikari library throws SQLException in case the pool is closed or there is an issue // initializing the connection pool which can also be translated in our world to // StaleConnectionException and should then trigger the destruction and recreation of the pool. - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : + new StaleConnectionException(e.getMessage())); } logHikariCPStatus("Before getting Mssql DB schema", connection); @@ -137,21 +142,9 @@ public class MssqlDatasourceUtils { * First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return * In case a connection is not available in the pool, SQL Exception is thrown * - * @param hikariDSConnectionPool + * @param connectionPool * @return SQL Connection */ - public static Connection getConnectionFromConnectionPool(HikariDataSource hikariDSConnectionPool) throws SQLException { - - if (hikariDSConnectionPool == null || hikariDSConnectionPool.isClosed() || !hikariDSConnectionPool.isRunning()) { - log.debug("Encountered stale connection pool in SQL Server plugin. Reporting back."); - throw new StaleConnectionException(); - } - - Connection sqlDataSourceConnection = hikariDSConnectionPool.getConnection(); - - return sqlDataSourceConnection; - } - public static void logHikariCPStatus(String logPrefix, HikariDataSource connectionPool) { HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean(); int idleConnections = poolProxy.getIdleConnections(); @@ -305,4 +298,36 @@ public class MssqlDatasourceUtils { return MessageFormat.format("{0}={1}", columnNameToSampleColumnDataMap.keySet().stream().findFirst().orElse( "id"), columnNameToSampleColumnDataMap.values().stream().findFirst().orElse("'uid'")); } + + public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException { + if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { + String printMessage = MessageFormat.format(Thread.currentThread().getName() + + ": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName); + System.out.println(printMessage); + + if (connectionPool == null) { + throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG); + } + else if (connectionPool.isClosed()) { + throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG); + } + else if (!connectionPool.isRunning()) { + throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG); + } + else { + /** + * Ideally, code flow is never expected to reach here. However, this section has been added to catch + * those cases wherein a developer updates the parent if condition but does not update the nested + * if else conditions. + */ + throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG); + } + } + } + + public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool, + String pluginName) throws SQLException { + checkHikariCPConnectionPoolValidity(connectionPool, pluginName); + return connectionPool.getConnection(); + } } diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlTestDBContainerManager.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlTestDBContainerManager.java index 2d6fd3a0ef..ac8c02a596 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlTestDBContainerManager.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlTestDBContainerManager.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.SSLDetails; +import com.external.plugins.utils.MssqlDatasourceUtils; import com.zaxxer.hikari.HikariDataSource; import org.testcontainers.containers.MSSQLServerContainer; import org.testcontainers.utility.DockerImageName; @@ -12,13 +13,15 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.List; -import static com.external.plugins.utils.MssqlDatasourceUtils.getConnectionFromConnectionPool; +import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME; import static com.external.plugins.utils.MssqlExecuteUtils.closeConnectionPostExecution; public class MssqlTestDBContainerManager { static MssqlPlugin.MssqlPluginExecutor mssqlPluginExecutor = new MssqlPlugin.MssqlPluginExecutor(); + public static MssqlDatasourceUtils mssqlDatasourceUtils = new MssqlDatasourceUtils(); + @SuppressWarnings("rawtypes") public static MSSQLServerContainer getMssqlDBForTest() { return new MSSQLServerContainer<>( @@ -56,7 +59,8 @@ public class MssqlTestDBContainerManager { } static void runSQLQueryOnMssqlTestDB(String sqlQuery, HikariDataSource sharedConnectionPool) throws SQLException { - java.sql.Connection connectionFromPool = getConnectionFromConnectionPool(sharedConnectionPool); + java.sql.Connection connectionFromPool = + mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(sharedConnectionPool, MSSQL_PLUGIN_NAME); Statement statement = connectionFromPool.createStatement(); statement.execute(sqlQuery); closeConnectionPostExecution(null, statement, null, connectionFromPool); diff --git a/app/server/appsmith-plugins/mysqlPlugin/pom.xml b/app/server/appsmith-plugins/mysqlPlugin/pom.xml index 4ceb731e8f..725c77bf9e 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/pom.xml +++ b/app/server/appsmith-plugins/mysqlPlugin/pom.xml @@ -19,7 +19,7 @@ org.mariadb r2dbc-mariadb - 1.1.3 + 1.1.4 io.netty diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index 9dd521008c..b2909a4380 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -66,6 +66,7 @@ import static com.appsmith.external.helpers.PluginUtils.MATCH_QUOTED_WORDS_REGEX import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel; import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex; +import static com.external.plugins.exceptions.MySQLErrorMessages.CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG; import static com.external.utils.MySqlDatasourceUtils.getNewConnectionPool; import static com.external.utils.MySqlGetStructureUtils.getKeyInfo; import static com.external.utils.MySqlGetStructureUtils.getTableInfo; @@ -256,9 +257,9 @@ public class MySqlPlugin extends BasePlugin { connectionPool.create(), connection -> { // TODO: add JUnit TC for the `connection.validate` check. Not sure how to do it at the moment. - Flux resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE)) + Flux resultFlux = Mono.from(connection.validate(ValidationDepth.LOCAL)) .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) - .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage())) .flatMapMany(isValid -> { if (isValid) { return createAndExecuteQueryFromConnection(finalQuery, @@ -269,7 +270,7 @@ public class MySqlPlugin extends BasePlugin { requestData, psParams); } - return Flux.error(new StaleConnectionException()); + return Flux.error(new StaleConnectionException(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG)); }); Mono>> resultMono; @@ -348,9 +349,10 @@ public class MySqlPlugin extends BasePlugin { }, Connection::close ) - .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) - .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException()) - .onErrorMap(R2dbcNonTransientResourceException.class, error -> new StaleConnectionException()) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage())) + .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException(error.getMessage())) + .onErrorMap(R2dbcNonTransientResourceException.class, error -> new StaleConnectionException(error.getMessage())) + .onErrorMap(IllegalStateException.class, error -> new StaleConnectionException(error.getMessage())) .subscribeOn(scheduler); } @@ -579,60 +581,58 @@ public class MySqlPlugin extends BasePlugin { return Mono.usingWhen( connectionPool.create(), - connection -> { - return Mono.from(connection.validate(ValidationDepth.REMOTE)) - .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) - .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) - .flatMapMany(isValid -> { - if (isValid) { - return connection.createStatement(COLUMNS_QUERY).execute(); - } else { - return Flux.error(new StaleConnectionException()); - } - }) - .flatMap(result -> { - return result.map((row, meta) -> { - getTableInfo(row, meta, tablesByName); + connection -> Mono.from(connection.validate(ValidationDepth.REMOTE)) + .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage())) + .flatMapMany(isValid -> { + if (isValid) { + return connection.createStatement(COLUMNS_QUERY).execute(); + } else { + return Flux.error(new StaleConnectionException(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG)); + } + }) + .flatMap(result -> { + return result.map((row, meta) -> { + getTableInfo(row, meta, tablesByName); - return result; - }); - }) - .collectList() - .thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute())) - .flatMap(result -> { - return result.map((row, meta) -> { - getKeyInfo(row, meta, tablesByName, keyRegistry); - - return result; - }); - }) - .collectList() - .map(list -> { - /* Get templates for each table and put those in. */ - getTemplates(tablesByName); - structure.setTables(new ArrayList<>(tablesByName.values())); - for (DatasourceStructure.Table table : structure.getTables()) { - table.getKeys().sort(Comparator.naturalOrder()); - } - - return structure; - }) - .onErrorMap(e -> { - if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) { - return new AppsmithPluginException( - AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR, - MySQLErrorMessages.GET_STRUCTURE_ERROR_MSG, - e.getMessage() - ); - } - - return e; + return result; }); - }, + }) + .collectList() + .thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute())) + .flatMap(result -> { + return result.map((row, meta) -> { + getKeyInfo(row, meta, tablesByName, keyRegistry); + + return result; + }); + }) + .collectList() + .map(list -> { + /* Get templates for each table and put those in. */ + getTemplates(tablesByName); + structure.setTables(new ArrayList<>(tablesByName.values())); + for (DatasourceStructure.Table table : structure.getTables()) { + table.getKeys().sort(Comparator.naturalOrder()); + } + + return structure; + }) + .onErrorMap(e -> { + if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) { + return new AppsmithPluginException( + AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR, + MySQLErrorMessages.GET_STRUCTURE_ERROR_MSG, + e.getMessage() + ); + } + + return e; + }), Connection::close ) - .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) - .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException()) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage())) + .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException(error.getMessage())) .subscribeOn(scheduler); } } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/exceptions/MySQLErrorMessages.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/exceptions/MySQLErrorMessages.java index 8022b058e6..293fb32ec1 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/exceptions/MySQLErrorMessages.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/exceptions/MySQLErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class MySQLErrorMessages { - private MySQLErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class MySQLErrorMessages extends BasePluginErrorMessages { public static final String MISSING_PARAMETER_QUERY_ERROR_MSG = "Missing required parameter: Query."; public static final String IS_KEYWORD_NOT_SUPPORTED_IN_PS_ERROR_MSG = "Appsmith currently does not support the IS keyword with the prepared statement " + @@ -32,7 +34,6 @@ public class MySQLErrorMessages { public static final String DS_MISSING_DATABASE_NAME_ERROR_MSG = "Missing database name."; public static final String DS_SSL_CONFIGURATION_FETCHING_FAILED_ERROR_MSG = "Appsmith server has failed to fetch SSL configuration from datasource configuration form. " + "Please reach out to Appsmith customer support to resolve this."; - - - + public static final String CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG = "Connection obtained from connection pool" + + " is invalid."; } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.java index 96b1695e3f..f518283af6 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.java @@ -8,7 +8,6 @@ import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.Property; import com.appsmith.external.models.SSLDetails; import com.external.plugins.exceptions.MySQLErrorMessages; -import com.external.plugins.exceptions.MySQLPluginError; import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.ConnectionPoolConfiguration; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -25,14 +24,40 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import static io.r2dbc.pool.PoolingConnectionFactoryProvider.MAX_SIZE; import static io.r2dbc.spi.ConnectionFactoryOptions.SSL; public class MySqlDatasourceUtils { public static int MAX_CONNECTION_POOL_SIZE = 5; - private static final Duration MAX_IDLE_TIME = Duration.ofMinutes(10); + /** + * 1 sec is the recommended value as shown in the example here: + * https://mariadb.com/docs/xpand/connect/programming-languages/java-r2dbc/native/connection-pools/ + * + * Current understanding is that the issue mentioned in #17324 is because of at least one of the connections + * malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks. + * Setting max idle time value to 1 sec could also be seen as a precaution move to make sure that we don't land + * into a situation where an idle thread can malfunction. + */ + private static final Duration MAX_IDLE_TIME = Duration.ofSeconds(1); + + /** + * Current understanding is that the issue mentioned in #17324 is because of at least one of the connections + * malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks. + * Setting max lifetime value to 5 min is a precaution move to make sure that we don't land into a situation + * where an older connection can malfunction. + * To understand what this config means please check here: https://github.com/r2dbc/r2dbc-pool + */ + private static final Duration MAX_LIFE_TIME = Duration.ofMinutes(5); + + /** + * Current understanding is that the issue mentioned in #17324 is because of at least one of the connections + * malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks. + * Setting eviction time value to 5 min is a precaution move to make sure that we don't land into a situation + * where an older connection can malfunction. + * To understand what this config means please check here: https://github.com/r2dbc/r2dbc-pool + */ + public static final Duration BACKGROUND_EVICTION_TIME = Duration.ofMinutes(5); public static ConnectionFactoryOptions.Builder getBuilder(DatasourceConfiguration datasourceConfiguration) { DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); @@ -188,7 +213,10 @@ public class MySqlDatasourceUtils { ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration.builder(connectionFactory) .maxIdleTime(MAX_IDLE_TIME) .maxSize(MAX_CONNECTION_POOL_SIZE) + .backgroundEvictionInterval(BACKGROUND_EVICTION_TIME) + .maxLifeTime(MAX_LIFE_TIME) .build(); + return new ConnectionPool(configuration); } } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index 06f57835fe..d8744563f4 100755 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -72,140 +72,140 @@ import static reactor.core.publisher.Mono.zip; @Testcontainers public class MySqlPluginTest { - static MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); + static MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); - @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is - // pseudo-optional. - @Container - public static MySQLContainer mySQLContainer = new MySQLContainer( - DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) - .withUsername("mysql") - .withPassword("password") - .withDatabaseName("test_db"); + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is + // pseudo-optional. + @Container + public static MySQLContainer mySQLContainer = new MySQLContainer( + DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) + .withUsername("mysql") + .withPassword("password") + .withDatabaseName("test_db"); - @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is - // pseudo-optional. - @Container - public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer( - DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) - .withUsername("root") - .withPassword("") - .withDatabaseName("test_db") - .withEnv("TZ", "PDT") - .withEnv("MYSQL_ROOT_HOST", "%"); + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is + // pseudo-optional. + @Container + public static MySQLContainer mySQLContainerWithInvalidTimezone = (MySQLContainer) new MySQLContainer( + DockerImageName.parse("mysql/mysql-server:8.0.25").asCompatibleSubstituteFor("mysql")) + .withUsername("root") + .withPassword("") + .withDatabaseName("test_db") + .withEnv("TZ", "PDT") + .withEnv("MYSQL_ROOT_HOST", "%"); - private static String address; - private static Integer port; - private static String username; - private static String password; - private static String database; - private static DatasourceConfiguration dsConfig; + private static String address; + private static Integer port; + private static String username; + private static String password; + private static String database; + private static DatasourceConfiguration dsConfig; - private static Mono getConnectionMonoFromContainer(MySQLContainer mySQLContainer) { - ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); - MariadbConnectionConfiguration conf = MariadbConnectionConfiguration.fromOptions(ob.build()) - .allowPublicKeyRetrieval(true) - .build(); - MariadbConnectionFactory connFactory = new MariadbConnectionFactory(conf); - return connFactory.create(); - } + private static Mono getConnectionMonoFromContainer(MySQLContainer mySQLContainer) { + ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); + MariadbConnectionConfiguration conf = MariadbConnectionConfiguration.fromOptions(ob.build()) + .allowPublicKeyRetrieval(true) + .build(); + MariadbConnectionFactory connFactory = new MariadbConnectionFactory(conf); + return connFactory.create(); + } - @BeforeAll - public static void setUp() { - address = mySQLContainer.getContainerIpAddress(); - port = mySQLContainer.getFirstMappedPort(); - username = mySQLContainer.getUsername(); - password = mySQLContainer.getPassword(); - database = mySQLContainer.getDatabaseName(); - dsConfig = createDatasourceConfiguration(); + @BeforeAll + public static void setUp() { + address = mySQLContainer.getContainerIpAddress(); + port = mySQLContainer.getFirstMappedPort(); + username = mySQLContainer.getUsername(); + password = mySQLContainer.getPassword(); + database = mySQLContainer.getDatabaseName(); + dsConfig = createDatasourceConfiguration(); - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> { - return connection.createBatch() - .add("DROP TABLE IF EXISTS possessions") - .add("DROP TABLE IF EXISTS users") - .add("create table users (\n" + - " id int auto_increment primary key,\n" + - " username varchar (250) unique not null,\n" - + - " password varchar (250) not null,\n" + - " email varchar (250) unique not null,\n" + - " spouse_dob date,\n" + - " dob date not null,\n" + - " yob year not null,\n" + - " time1 time not null,\n" + - " created_on timestamp not null,\n" + - " updated_on datetime not null,\n" + - " constraint unique index (username, email)\n" - + - ")") - .add("create table possessions (\n" + - " id int primary key,\n" + - " title varchar (250) not null,\n" + - " user_id int not null,\n" + - " username varchar (250) not null,\n" + - " email varchar (250) not null\n" + - ")") - .add("alter table possessions add foreign key (username, email) \n" - + - "references users (username, email)") - .add("SET SESSION sql_mode = '';\n") - .add("INSERT INTO users VALUES (" + - "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31', 2018," - + - " '18:32:45'," + - " '2018-11-30 20:45:15', '0000-00-00 00:00:00'" - + - ")") - .add("INSERT INTO users VALUES (" + - "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31', 2019," - + - " '15:45:30'," + - " '2019-11-30 23:59:59', '2019-11-30 23:59:59'" - + - ")"); - }) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> { + return connection.createBatch() + .add("DROP TABLE IF EXISTS possessions") + .add("DROP TABLE IF EXISTS users") + .add("create table users (\n" + + " id int auto_increment primary key,\n" + + " username varchar (250) unique not null,\n" + + + " password varchar (250) not null,\n" + + " email varchar (250) unique not null,\n" + + " spouse_dob date,\n" + + " dob date not null,\n" + + " yob year not null,\n" + + " time1 time not null,\n" + + " created_on timestamp not null,\n" + + " updated_on datetime not null,\n" + + " constraint unique index (username, email)\n" + + + ")") + .add("create table possessions (\n" + + " id int primary key,\n" + + " title varchar (250) not null,\n" + + " user_id int not null,\n" + + " username varchar (250) not null,\n" + + " email varchar (250) not null\n" + + ")") + .add("alter table possessions add foreign key (username, email) \n" + + + "references users (username, email)") + .add("SET SESSION sql_mode = '';\n") + .add("INSERT INTO users VALUES (" + + "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31', 2018," + + + " '18:32:45'," + + " '2018-11-30 20:45:15', '0000-00-00 00:00:00'" + + + ")") + .add("INSERT INTO users VALUES (" + + "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31', 2019," + + + " '15:45:30'," + + " '2019-11-30 23:59:59', '2019-11-30 23:59:59'" + + + ")"); + }) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries - return; - } + return; + } - private static DatasourceConfiguration createDatasourceConfiguration() { - DBAuth authDTO = new DBAuth(); - authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); - authDTO.setUsername(username); - authDTO.setPassword(password); - authDTO.setDatabaseName(database); + private static DatasourceConfiguration createDatasourceConfiguration() { + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + authDTO.setDatabaseName(database); - Endpoint endpoint = new Endpoint(); - endpoint.setHost(address); - endpoint.setPort(port.longValue()); + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); - DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); - /* set endpoint */ - datasourceConfiguration.setAuthentication(authDTO); - datasourceConfiguration.setEndpoints(List.of(endpoint)); + /* set endpoint */ + datasourceConfiguration.setAuthentication(authDTO); + datasourceConfiguration.setEndpoints(List.of(endpoint)); - /* set ssl mode */ - datasourceConfiguration.setConnection(new com.appsmith.external.models.Connection()); - datasourceConfiguration.getConnection().setSsl(new SSLDetails()); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + /* set ssl mode */ + datasourceConfiguration.setConnection(new com.appsmith.external.models.Connection()); + datasourceConfiguration.getConnection().setSsl(new SSLDetails()); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - return datasourceConfiguration; - } + return datasourceConfiguration; + } - @Test - public void testConnectMySQLContainer() { + @Test + public void testConnectMySQLContainer() { - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - } + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } @Test public void testMySqlNoPasswordExceptionMessage() { @@ -232,220 +232,220 @@ public class MySqlPluginTest { .verifyComplete(); } - @Test - public void testConnectMySQLContainerWithInvalidTimezone() { + @Test + public void testConnectMySQLContainerWithInvalidTimezone() { - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - dsConfig.setProperties(List.of( - new Property("serverTimezone", "UTC"))); + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + dsConfig.setProperties(List.of( + new Property("serverTimezone", "UTC"))); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); - } + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } - @Test - public void testTestDatasource() { - dsConfig = createDatasourceConfiguration(); + @Test + public void testTestDatasource() { + dsConfig = createDatasourceConfiguration(); - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> { - assertEquals(0, datasourceTestResult.getInvalids().size()); - }) - .verifyComplete(); + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> { + assertEquals(0, datasourceTestResult.getInvalids().size()); + }) + .verifyComplete(); - /* Create bad datasource configuration and expect error */ - dsConfig.getEndpoints().get(0).setHost("badHost"); - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> { - assertNotEquals(0, datasourceTestResult.getInvalids().size()); - }) - .verifyComplete(); + /* Create bad datasource configuration and expect error */ + dsConfig.getEndpoints().get(0).setHost("badHost"); + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> { + assertNotEquals(0, datasourceTestResult.getInvalids().size()); + }) + .verifyComplete(); - /* Reset dsConfig */ - dsConfig = createDatasourceConfiguration(); - } + /* Reset dsConfig */ + dsConfig = createDatasourceConfiguration(); + } - public DatasourceConfiguration createDatasourceConfigForContainerWithInvalidTZ() { - final DBAuth authDTO = new DBAuth(); - authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); - authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername()); - authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword()); - authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName()); + public DatasourceConfiguration createDatasourceConfigForContainerWithInvalidTZ() { + final DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); + authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername()); + authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword()); + authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName()); - final Endpoint endpoint = new Endpoint(); - endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); - endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); + final Endpoint endpoint = new Endpoint(); + endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); + endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); - final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); - /* set endpoint */ - dsConfig.setAuthentication(authDTO); - dsConfig.setEndpoints(List.of(endpoint)); + /* set endpoint */ + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); - /* set ssl mode */ + /* set ssl mode */ - dsConfig.setConnection(new com.appsmith.external.models.Connection()); - dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_WRITE); - dsConfig.getConnection().setSsl(new SSLDetails()); - dsConfig.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + dsConfig.setConnection(new com.appsmith.external.models.Connection()); + dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_WRITE); + dsConfig.getConnection().setSsl(new SSLDetails()); + dsConfig.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - return dsConfig; - } + return dsConfig; + } - @Test - public void testDatasourceWithNullPassword() { - // adding a user with empty password - String sqlCmd = "CREATE USER 'mysql'@'%' IDENTIFIED BY '';" + - "GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;" + - "FLUSH PRIVILEGES;"; - Mono.from(getConnectionMonoFromContainer(mySQLContainerWithInvalidTimezone)) - .map(connection -> connection.createBatch() - .add("CREATE USER 'mysql'@'%' IDENTIFIED BY '';") - .add("GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;") - .add("FLUSH PRIVILEGES;") - ) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries + @Test + public void testDatasourceWithNullPassword() { + // adding a user with empty password + String sqlCmd = "CREATE USER 'mysql'@'%' IDENTIFIED BY '';" + + "GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;" + + "FLUSH PRIVILEGES;"; + Mono.from(getConnectionMonoFromContainer(mySQLContainerWithInvalidTimezone)) + .map(connection -> connection.createBatch() + .add("CREATE USER 'mysql'@'%' IDENTIFIED BY '';") + .add("GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;") + .add("FLUSH PRIVILEGES;") + ) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - // change to ordinary user - DBAuth auth = ((DBAuth) dsConfig.getAuthentication()); - auth.setPassword(""); - auth.setUsername("mysql"); + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + // change to ordinary user + DBAuth auth = ((DBAuth) dsConfig.getAuthentication()); + auth.setPassword(""); + auth.setUsername("mysql"); - // check user pass - assertEquals("mysql", auth.getUsername()); - assertEquals("", auth.getPassword()); + // check user pass + assertEquals("mysql", auth.getUsername()); + assertEquals("", auth.getPassword()); - // Validate datastore - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.isEmpty()); - // test connect - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + // Validate datastore + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.isEmpty()); + // test connect + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> assertEquals(0, - datasourceTestResult.getInvalids().size())) - .verifyComplete(); - } + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> assertEquals(0, + datasourceTestResult.getInvalids().size())) + .verifyComplete(); + } - @Test - public void testDatasourceWithRootUserAndNullPassword() { + @Test + public void testDatasourceWithRootUserAndNullPassword() { - final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); - // check user pass - assertEquals("root", mySQLContainerWithInvalidTimezone.getUsername()); - assertEquals("", mySQLContainerWithInvalidTimezone.getPassword()); + // check user pass + assertEquals("root", mySQLContainerWithInvalidTimezone.getUsername()); + assertEquals("", mySQLContainerWithInvalidTimezone.getPassword()); - // Validate datastore - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.isEmpty()); - // test connect - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + // Validate datastore + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.isEmpty()); + // test connect + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - StepVerifier.create(dsConnectionMono) - .assertNext(Assertions::assertNotNull) - .verifyComplete(); + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); - /* Expect no error */ - StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) - .assertNext(datasourceTestResult -> assertEquals(0, - datasourceTestResult.getInvalids().size())) - .verifyComplete(); + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> assertEquals(0, + datasourceTestResult.getInvalids().size())) + .verifyComplete(); - } + } - @Test - public void testExecute() { - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + @Test + public void testExecute() { + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show databases"); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - }) - .verifyComplete(); - } + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + }) + .verifyComplete(); + } - @Test - public void testExecuteWithFormattingWithShowCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + @Test + public void testExecuteWithFormattingWithShowCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show\n\tdatabases"); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show\n\tdatabases"); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Database\":\"information_schema\"},{\"Database\":\"test_db\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Database\":\"information_schema\"},{\"Database\":\"test_db\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } - @Test - public void testExecuteWithFormattingWithSelectCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + @Test + public void testExecuteWithFormattingWithSelectCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select\n\t*\nfrom\nusers where id=1"); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select\n\t*\nfrom\nusers where id=1"); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertEquals("2018-12-31", node.get("dob").asText()); - assertEquals("2018", node.get("yob").asText()); - assertEquals("Jack", node.get("username").asText()); - assertEquals("jill", node.get("password").asText()); - assertEquals("1", node.get("id").asText()); - assertEquals("jack@exemplars.com", node.get("email").asText()); - assertEquals("18:32:45", node.get("time1").asText()); - assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("2018", node.get("yob").asText()); + assertEquals("Jack", node.get("username").asText()); + assertEquals("jill", node.get("password").asText()); + assertEquals("1", node.get("id").asText()); + assertEquals("jack@exemplars.com", node.get("email").asText()); + assertEquals("18:32:45", node.get("time1").asText()); + assertEquals("2018-11-30T20:45:15Z", node.get("created_on").asText()); - /* - * - RequestParamDTO object only have attributes configProperty and value at - * this point. - * - The other two RequestParamDTO attributes - label and type are null at this - * point. - */ - List expectedRequestParams = new ArrayList<>(); - expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY, - actionConfiguration.getBody(), null, null, new HashMap<>())); - assertEquals(result.getRequest().getRequestParams().toString(), - expectedRequestParams.toString()); - }) - .verifyComplete(); - } + /* + * - RequestParamDTO object only have attributes configProperty and value at + * this point. + * - The other two RequestParamDTO attributes - label and type are null at this + * point. + */ + List expectedRequestParams = new ArrayList<>(); + expectedRequestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_BODY, + actionConfiguration.getBody(), null, null, new HashMap<>())); + assertEquals(result.getRequest().getRequestParams().toString(), + expectedRequestParams.toString()); + }) + .verifyComplete(); + } @Test public void testExecuteWithLongRunningQuery() { @@ -466,1147 +466,1147 @@ public class MySqlPluginTest { .verifyComplete(); } - @Test - public void testStaleConnectionCheck() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show databases"); - ConnectionPool connectionPool = pluginExecutor.datasourceCreate(dsConfig).block(); - Flux resultFlux = Mono.from(connectionPool.disposeLater()) - .thenMany(pluginExecutor.executeParameterized(connectionPool, new ExecuteActionDTO(), - dsConfig, actionConfiguration)); + @Test + public void testStaleConnectionCheck() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show databases"); + ConnectionPool connectionPool = pluginExecutor.datasourceCreate(dsConfig).block(); + Flux resultFlux = Mono.from(connectionPool.disposeLater()) + .thenMany(pluginExecutor.executeParameterized(connectionPool, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); - StepVerifier.create(resultFlux) - .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) - .verify(); - } + StepVerifier.create(resultFlux) + .expectErrorMatches(throwable -> throwable instanceof StaleConnectionException) + .verify(); + } - @Test - public void testValidateDatasourceNullCredentials() { - dsConfig.setConnection(new com.appsmith.external.models.Connection()); - DBAuth auth = (DBAuth) dsConfig.getAuthentication(); - auth.setUsername(null); - auth.setPassword(null); - auth.setDatabaseName("someDbName"); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.contains("Missing username for authentication.")); - assertTrue(output.contains("Missing password for authentication.")); - } + @Test + public void testValidateDatasourceNullCredentials() { + dsConfig.setConnection(new com.appsmith.external.models.Connection()); + DBAuth auth = (DBAuth) dsConfig.getAuthentication(); + auth.setUsername(null); + auth.setPassword(null); + auth.setDatabaseName("someDbName"); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains("Missing username for authentication.")); + assertTrue(output.contains("Missing password for authentication.")); + } - @Test - public void testValidateDatasourceMissingDBName() { - ((DBAuth) dsConfig.getAuthentication()).setDatabaseName(""); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Missing database name."))); - } + @Test + public void testValidateDatasourceMissingDBName() { + ((DBAuth) dsConfig.getAuthentication()).setDatabaseName(""); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Missing database name."))); + } - @Test - public void testValidateDatasourceNullEndpoint() { - dsConfig.setEndpoints(null); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Missing endpoint and url"))); - } + @Test + public void testValidateDatasourceNullEndpoint() { + dsConfig.setEndpoints(null); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Missing endpoint and url"))); + } - @Test - public void testValidateDatasource_NullHost() { - dsConfig.setEndpoints(List.of(new Endpoint())); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output - .stream() - .anyMatch(error -> error.contains("Host value cannot be empty"))); + @Test + public void testValidateDatasource_NullHost() { + dsConfig.setEndpoints(List.of(new Endpoint())); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output + .stream() + .anyMatch(error -> error.contains("Host value cannot be empty"))); - Endpoint endpoint = new Endpoint(); - endpoint.setHost(address); - endpoint.setPort(port.longValue()); - dsConfig.setEndpoints(List.of(endpoint)); - } + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + dsConfig.setEndpoints(List.of(endpoint)); + } - @Test - public void testValidateDatasourceInvalidEndpoint() { - String hostname = "r2dbc:mysql://localhost"; - dsConfig.getEndpoints().get(0).setHost(hostname); - Set output = pluginExecutor.validateDatasource(dsConfig); - assertTrue(output.contains( - "Host value cannot contain `/` or `:` characters. Found `" + hostname + "`.")); - } + @Test + public void testValidateDatasourceInvalidEndpoint() { + String hostname = "r2dbc:mysql://localhost"; + dsConfig.getEndpoints().get(0).setHost(hostname); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains( + "Host value cannot contain `/` or `:` characters. Found `" + hostname + "`.")); + } - @Test - public void testAliasColumnNames() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, actionConfiguration)); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "user_id" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - }) - .verifyComplete(); + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + }) + .verifyComplete(); - return; - } + return; + } - @Test - public void testPreparedStatementErrorWithIsKeyword() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - /** - * - MySQL r2dbc driver is not able to substitute the `True/False` value - * properly after the IS keyword. - * Converting `True/False` to integer 1 or 0 also does not work in this case as - * MySQL syntax does not support - * integers with IS keyword. - * - I have raised an issue with r2dbc to track it: - * https://github.com/mirromutth/r2dbc-mysql/issues/200 - */ - actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean IS {{binding1}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("True"); - param1.setClientDataType(ClientDataType.BOOLEAN); - params.add(param1); - - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .verifyErrorSatisfies(error -> { - assertTrue(error instanceof AppsmithPluginException); - String expectedMessage = MySQLErrorMessages.IS_KEYWORD_NOT_SUPPORTED_IN_PS_ERROR_MSG; - assertTrue(expectedMessage.equals(error.getMessage())); - }); - } - - @Test - public void testPreparedStatementWithRealTypes() { - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> connection.createBatch() - .add("create table test_real_types(id int, c_float float, c_double double, c_real real)") - .add("insert into test_real_types values (1, 1.123, 3.123, 5.123)") - .add("insert into test_real_types values (2, 11.123, 13.123, 15.123)")) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries - - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - /** - * - For mysql float / double / real types the actual values that are stored in - * the db my differ by a very - * thin margin as long as they are approximately same. Hence adding comparison - * based check instead of direct - * equality. - * - Ref: https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html - */ - actionConfiguration.setBody( - "SELECT id FROM test_real_types WHERE ABS(c_float - {{binding1}}) < 0.1 AND ABS" + - "(c_double - {{binding2}}) < 0.1 AND ABS(c_real - {{binding3}}) < 0.1;"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("1.123"); - param1.setClientDataType(ClientDataType.NUMBER); - params.add(param1); - - Param param2 = new Param(); - param2.setKey("binding2"); - param2.setValue("3.123"); - param2.setClientDataType(ClientDataType.NUMBER); - params.add(param2); - - Param param3 = new Param(); - param3.setKey("binding3"); - param3.setValue("5.123"); - param3.setClientDataType(ClientDataType.NUMBER); - params.add(param3); - - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()); - assertEquals(1, node.size()); - // Verify selected row id. - assertEquals(1, node.get(0).get("id").asInt()); - }) - .verifyComplete(); - - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> connection.createBatch() - .add("drop table test_real_types")) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries - } - - private Publisher getConnectionFromBuilder(ConnectionFactoryOptions.Builder builder) { - return ConnectionFactories.get(builder.build()).create(); - } - - @Test - public void testPreparedStatementWithBooleanType() { - // Create a new table with boolean type - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> connection.createBatch() - .add("create table test_boolean_type(id int, c_boolean boolean)") - .add("insert into test_boolean_type values (1, True)") - .add("insert into test_boolean_type values (2, True)") - .add("insert into test_boolean_type values (3, False)")) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries - - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean={{binding1}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("True"); - param1.setClientDataType(ClientDataType.BOOLEAN); - params.add(param1); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()); - assertEquals(2, node.size()); - // Verify selected row id. - assertEquals(1, node.get(0).get("id").asInt()); - assertEquals(2, node.get(1).get("id").asInt()); - }) - .verifyComplete(); - - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> connection.createBatch() - .add("drop table test_boolean_type")) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries - } - - @Test - public void testExecuteWithPreparedStatement() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration - .setBody("SELECT id FROM users WHERE id = {{binding1}} limit 1 offset {{binding2}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("1"); - param1.setClientDataType(ClientDataType.NUMBER); - params.add(param1); - Param param2 = new Param(); - param2.setKey("binding2"); - param2.setValue("0"); - param2.setClientDataType(ClientDataType.NUMBER); - params.add(param2); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "id" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - - // Verify value - assertEquals(1, node.get("id").asInt()); - - /* - * - Check if request params are sent back properly. - * - Not replicating the same to other tests as the overall flow remains the - * same w.r.t. request - * params. - */ - - // Check if '?' is replaced by $i. - assertEquals("SELECT id FROM users WHERE id = $1 limit 1 offset $2;", - ((RequestParamDTO) (((List) result.getRequest() - .getRequestParams())).get(0)).getValue()); - - // Check 1st prepared statement parameter - PsParameterDTO expectedPsParam1 = new PsParameterDTO("1", "INTEGER"); - PsParameterDTO returnedPsParam1 = (PsParameterDTO) ((RequestParamDTO) (((List) result - .getRequest().getRequestParams())).get(0)) - .getSubstitutedParams().get("$1"); - // Check if prepared stmt param value is correctly sent back. - assertEquals(expectedPsParam1.getValue(), returnedPsParam1.getValue()); - // Check if prepared stmt param type is correctly sent back. - assertEquals(expectedPsParam1.getType(), returnedPsParam1.getType()); - - // Check 2nd prepared statement parameter - PsParameterDTO expectedPsParam2 = new PsParameterDTO("0", "INTEGER"); - PsParameterDTO returnedPsParam2 = (PsParameterDTO) ((RequestParamDTO) (((List) result - .getRequest().getRequestParams())).get(0)) - .getSubstitutedParams().get("$2"); - // Check if prepared stmt param value is correctly sent back. - assertEquals(expectedPsParam2.getValue(), returnedPsParam2.getValue()); - // Check if prepared stmt param type is correctly sent back. - assertEquals(expectedPsParam2.getType(), returnedPsParam2.getType()); - }) - .verifyComplete(); - - return; - } - - @Test - public void testExecuteDataTypes() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertEquals("2018-12-31", node.get("dob").asText()); - assertEquals("2018", node.get("yob").asText()); - assertTrue(node.get("time1").asText().matches("\\d{2}:\\d{2}:\\d{2}")); - assertTrue(node.get("created_on").asText() - .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); - assertTrue(node.get("updated_on").isNull()); - - assertArrayEquals( - new String[]{ - "id", - "username", - "password", - "email", - "spouse_dob", - "dob", - "yob", - "time1", - "created_on", - "updated_on" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - }) - .verifyComplete(); - } + @Test + public void testPreparedStatementErrorWithIsKeyword() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + ActionConfiguration actionConfiguration = new ActionConfiguration(); /** - * 1. Add a test to check that mysql driver can interpret and read all the - * regular data types used in mysql. - * 2. List of the data types is taken is from - * https://dev.mysql.com/doc/refman/8.0/en/data-types.html - * 3. Data types tested here are: INTEGER, SMALLINT, TINYINT, MEDIUMINT, BIGINT, - * DECIMAL, FLOAT, DOUBLE, BIT, - * DATE, DATETIME, TIMESTAMP, TIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, - * TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, - * TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, JSON, GEOMETRY, POINT + * - MySQL r2dbc driver is not able to substitute the `True/False` value + * properly after the IS keyword. + * Converting `True/False` to integer 1 or 0 also does not work in this case as + * MySQL syntax does not support + * integers with IS keyword. + * - I have raised an issue with r2dbc to track it: + * https://github.com/mirromutth/r2dbc-mysql/issues/200 */ - @Test - public void testExecuteDataTypesExtensive() throws AppsmithPluginException { - String query_create_table_numeric_types = "create table test_numeric_types (c_integer INTEGER, c_smallint " - + - "SMALLINT, c_tinyint TINYINT, c_mediumint MEDIUMINT, c_bigint BIGINT, c_decimal DECIMAL, c_float " - + - "FLOAT, c_double DOUBLE, c_bit BIT(10));"; - String query_insert_into_table_numeric_types = "insert into test_numeric_types values (-1, 1, 1, 10, 2000, 1" - + - ".02345, 0.1234, 1.0102344, b'0101010');"; - - String query_create_table_date_time_types = "create table test_date_time_types (c_date DATE, c_datetime " - + - "DATETIME DEFAULT CURRENT_TIMESTAMP, c_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, c_time TIME, " - + - "c_year YEAR);"; - String query_insert_into_table_date_time_types = "insert into test_date_time_types values ('2020-12-01', " - + - "'2020-12-01 20:20:20', '2020-12-01 20:20:20', '20:20:20', 2020);"; - - String query_create_table_data_types = "create table test_data_types (c_char CHAR(50), c_varchar VARCHAR(50)," - + - " c_binary BINARY(20), c_varbinary VARBINARY(20), c_tinyblob TINYBLOB, c_blob BLOB, c_mediumblob " - + - "MEDIUMBLOB, c_longblob LONGBLOB, c_tinytext TINYTEXT, c_text TEXT, c_mediumtext MEDIUMTEXT, " - + - "c_longtext LONGTEXT, c_enum ENUM('ONE'), c_set SET('a'));"; - String query_insert_data_types = "insert into test_data_types values ('test', 'test', 'a\\0\\t', 'a\\0\\t', " - + - "'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'ONE', 'a');"; - - String query_create_table_json_data_type = "create table test_json_type (c_json JSON);"; - String query_insert_json_data_type = "insert into test_json_type values ('{\"key1\": \"value1\", \"key2\": " - + - "\"value2\"}');"; - - String query_create_table_geometry_types = "create table test_geometry_types (c_geometry GEOMETRY, c_point " - + - "POINT);"; - String query_insert_geometry_types = "insert into test_geometry_types values (ST_GeomFromText('POINT(1 1)'), " - + - "ST_PointFromText('POINT(1 100)'));"; - - String query_select_from_test_numeric_types = "select * from test_numeric_types;"; - String query_select_from_test_date_time_types = "select * from test_date_time_types;"; - String query_select_from_test_json_data_type = "select * from test_json_type;"; - String query_select_from_test_data_types = "select * from test_data_types;"; - String query_select_from_test_geometry_types = "select * from test_geometry_types;"; - - String expected_numeric_types_result = "[{\"c_integer\":-1,\"c_smallint\":1,\"c_tinyint\":1,\"" - + - "c_mediumint\":10,\"c_bigint\":2000,\"c_decimal\":1,\"c_float\":0.1234,\"c_double\":1.0102344," - + - "\"c_bit\":{\"empty\":false}}]"; - - String expected_date_time_types_result = "[{\"c_date\":\"2020-12-01\",\"c_datetime\":\"2020-12-01T20:20:20Z\"," - + - "\"c_timestamp\":\"2020-12-01T20:20:20Z\",\"c_time\":\"20:20:20\",\"c_year\":2020}]"; - - String expected_data_types_result = "[{\"c_char\":\"test\",\"c_varchar\":\"test\"," - + - "\"c_binary\":\"YQAJAAAAAAAAAAAAAAAAAAAAAAA=\",\"c_varbinary\":\"YQAJ\",\"c_tinyblob\":\"dGVzdA==\"," - + - "\"c_blob\":\"dGVzdA==\",\"c_mediumblob\":\"dGVzdA==\",\"c_longblob\":\"dGVzdA==\",\"c_tinytext\":\"test\"," - + - "\"c_text\":\"test\",\"c_mediumtext\":\"test\",\"c_longtext\":\"test\",\"c_enum\":\"ONE\",\"c_set\":\"a\"}]"; - - String expected_json_result = "[{\"c_json\":\"{\\\"key1\\\": \\\"value1\\\", \\\"key2\\\": \\\"value2\\\"}\"}]"; - - String expected_geometry_types_result = "[{\"c_geometry\":\"AAAAAAEBAAAAAAAAAAAA8D8AAAAAAADwPw==\"," - + - "\"c_point\":\"AAAAAAEBAAAAAAAAAAAA8D8AAAAAAABZQA==\"}]"; - - - - Mono.from(getConnectionMonoFromContainer(mySQLContainer)) - .map(connection -> { - return connection.createBatch() - .add(query_create_table_numeric_types) - .add(query_insert_into_table_numeric_types) - .add(query_create_table_date_time_types) - .add(query_insert_into_table_date_time_types) - .add(query_create_table_json_data_type) - .add(query_insert_json_data_type) - .add(query_create_table_data_types) - .add(query_insert_data_types) - .add(query_create_table_geometry_types) - .add(query_insert_geometry_types); - }) - .flatMapMany(batch -> Flux.from(batch.execute())) - .blockLast(); // wait until completion of all the queries - - /* Test numeric types */ - testExecute(query_select_from_test_numeric_types, expected_numeric_types_result); - /* Test date time types */ - testExecute(query_select_from_test_date_time_types, expected_date_time_types_result); - /* Test data types */ - testExecute(query_select_from_test_data_types, expected_data_types_result); - /* Test json type */ - testExecute(query_select_from_test_json_data_type, expected_json_result); - /* Test geometry types */ - testExecute(query_select_from_test_geometry_types, expected_geometry_types_result); - - return; - } - - private void testExecute(String query, String expectedResult) { - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody(query); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - if (expectedResult != null) { - assertEquals(expectedResult, result.getBody().toString()); - } - }) - .verifyComplete(); - } - - @Test - public void testStructure() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono structureMono = pluginExecutor.datasourceCreate(dsConfig) - .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); - - StepVerifier.create(structureMono) - .assertNext(structure -> { - assertNotNull(structure); - assertEquals(2, structure.getTables().size()); - - Optional possessionsTableOptional = structure - .getTables() - .stream() - .filter(table -> table.getName() - .equalsIgnoreCase("possessions")) - .findFirst(); - assertTrue(possessionsTableOptional.isPresent()); - final DatasourceStructure.Table possessionsTable = possessionsTableOptional - .get(); - assertEquals(DatasourceStructure.TableType.TABLE, possessionsTable.getType()); - assertArrayEquals( - new DatasourceStructure.Column[]{ - new DatasourceStructure.Column("id", "int", - null, false), - new DatasourceStructure.Column("title", - "varchar", null, false), - new DatasourceStructure.Column("user_id", "int", - null, false), - new DatasourceStructure.Column("username", - "varchar", null, false), - new DatasourceStructure.Column("email", - "varchar", null, false), - }, - possessionsTable.getColumns().toArray()); - - final DatasourceStructure.PrimaryKey possessionsPrimaryKey = new DatasourceStructure.PrimaryKey( - "PRIMARY", List.of("id")); - final DatasourceStructure.ForeignKey possessionsUserForeignKey = new DatasourceStructure.ForeignKey( - "possessions_ibfk_1", - List.of("username", "email"), - List.of("users.username", "users.email")); - assertArrayEquals( - new DatasourceStructure.Key[]{possessionsPrimaryKey, - possessionsUserForeignKey}, - possessionsTable.getKeys().toArray()); - - assertArrayEquals( - new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", - "SELECT * FROM possessions LIMIT 10;"), - new DatasourceStructure.Template("INSERT", - "INSERT INTO possessions (id, title, user_id, username, email)\n" - + - " VALUES (1, '', 1, '', '');"), - new DatasourceStructure.Template("UPDATE", - "UPDATE possessions SET\n" + - " id = 1,\n" - + - " title = '',\n" - + - " user_id = 1,\n" - + - " username = '',\n" - + - " email = ''\n" - + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), - new DatasourceStructure.Template("DELETE", - "DELETE FROM possessions\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), - }, - possessionsTable.getTemplates().toArray()); - - Optional usersTableOptional = structure.getTables() - .stream() - .filter(table -> table.getName().equalsIgnoreCase("users")) - .findFirst(); - assertTrue(usersTableOptional.isPresent()); - final DatasourceStructure.Table usersTable = usersTableOptional.get(); - assertEquals(DatasourceStructure.TableType.TABLE, usersTable.getType()); - assertArrayEquals( - new DatasourceStructure.Column[]{ - new DatasourceStructure.Column("id", "int", - null, true), - new DatasourceStructure.Column("username", - "varchar", null, false), - new DatasourceStructure.Column("password", - "varchar", null, false), - new DatasourceStructure.Column("email", - "varchar", null, false), - new DatasourceStructure.Column("spouse_dob", - "date", null, false), - new DatasourceStructure.Column("dob", "date", - null, false), - new DatasourceStructure.Column("yob", "year", - null, false), - new DatasourceStructure.Column("time1", "time", - null, false), - new DatasourceStructure.Column("created_on", - "timestamp", null, false), - new DatasourceStructure.Column("updated_on", - "datetime", null, false) - }, - usersTable.getColumns().toArray()); - - final DatasourceStructure.PrimaryKey usersPrimaryKey = new DatasourceStructure.PrimaryKey( - "PRIMARY", List.of("id")); - assertArrayEquals( - new DatasourceStructure.Key[]{usersPrimaryKey}, - usersTable.getKeys().toArray()); - - assertArrayEquals( - new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", - "SELECT * FROM users LIMIT 10;"), - new DatasourceStructure.Template("INSERT", - "INSERT INTO users (id, username, password, email, spouse_dob, dob, yob, time1, created_on, updated_on)\n" - + - " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');"), - new DatasourceStructure.Template("UPDATE", - "UPDATE users SET\n" + - " id = 1,\n" - + - " username = '',\n" - + - " password = '',\n" - + - " email = '',\n" - + - " spouse_dob = '2019-07-01',\n" - + - " dob = '2019-07-01',\n" - + - " yob = '',\n" - + - " time1 = '',\n" - + - " created_on = '2019-07-01 10:00:00',\n" - + - " updated_on = '2019-07-01 10:00:00'\n" - + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), - new DatasourceStructure.Template("DELETE", - "DELETE FROM users\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), - }, - usersTable.getTemplates().toArray()); - }) - .verifyComplete(); - } - - @Test - public void testSslToggleMissingError() { - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(null); - - Mono> invalidsMono = Mono.just(pluginExecutor) - .map(executor -> executor.validateDatasource(datasourceConfiguration)); - - StepVerifier.create(invalidsMono) - .assertNext(invalids -> { - String expectedError = "Appsmith server has failed to fetch SSL configuration from datasource " - + - "configuration form. Please reach out to Appsmith customer support to resolve this."; - assertTrue(invalids - .stream() - .anyMatch(error -> expectedError.equals(error))); - }) - .verifyComplete(); - } - - @Test - public void testSslDisabled() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DISABLED); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testSslRequired() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.REQUIRED); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"TLS_AES_128_GCM_SHA256\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testSslDefault() { - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("show session status like 'Ssl_cipher'"); - - DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); - datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, - actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - Object body = result.getBody(); - assertNotNull(body); - assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", - body.toString()); - }) - .verifyComplete(); - } - - @Test - public void testDuplicateColumnNames() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody( - "SELECT id, username as id, password, email as password FROM users WHERE id = 1"); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), - dsConfig, actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertNotEquals(0, result.getMessages().size()); - - String expectedMessage = "Your MySQL query result may not have all the columns because duplicate column names " - + - "were found for the column(s)"; - assertTrue( - result.getMessages().stream() - .anyMatch(message -> message - .contains(expectedMessage))); - - /* - * - Check if all of the duplicate column names are reported. - */ - Set expectedColumnNames = Stream.of("id", "password") - .collect(Collectors.toCollection(HashSet::new)); - Set foundColumnNames = new HashSet<>(); - result.getMessages().stream() - .filter(message -> message.contains(expectedMessage)) - .forEach(message -> { - Arrays.stream(message.split(":")[1].split("\\.")[0] - .split(",")) - .forEach(columnName -> foundColumnNames - .add(columnName.trim())); - }); - assertTrue(expectedColumnNames.equals(foundColumnNames)); - }) - .verifyComplete(); - } - - @Test - public void testExecuteDescribeTableCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("describe users"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteDescTableCmd() { - dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("desc users"); - - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, - new ExecuteActionDTO(), dsConfig, actionConfiguration)); - StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - assertNotNull(result); - assertTrue(result.getIsExecutionSuccess()); - assertNotNull(result.getBody()); - String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; - assertEquals(expectedBody, result.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testNullObjectWithPreparedStatement() { - pluginExecutor = spy(new MySqlPlugin.MySqlPluginExecutor()); - doReturn(false).when(pluginExecutor).isIsOperatorUsed(any()); - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT * from (\n" + - "\tselect 'Appsmith' as company_name, true as open_source\n" + - "\tunion\n" + - "\tselect 'Retool' as company_name, false as open_source\n" + - "\tunion\n" + - "\tselect 'XYZ' as company_name, null as open_source\n" + - ") t\n" + - "where t.open_source IS {{binding1}}"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue(null); - param1.setClientDataType(ClientDataType.NULL); - params.add(param1); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertTrue(result.getIsExecutionSuccess()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "company_name", - "open_source" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - - // Verify value - assertEquals(JsonNodeType.NULL, node.get("open_source").getNodeType()); - - }) - .verifyComplete(); - } - - @Test - public void testNullAsStringWithPreparedStatement() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT * from (\n" + - "\tselect 'Appsmith' as company_name, true as open_source\n" + - "\tunion\n" + - "\tselect 'Retool' as company_name, false as open_source\n" + - "\tunion\n" + - "\tselect 'XYZ' as company_name, 'null' as open_source\n" + - ") t\n" + - "where t.open_source = {{binding1}};"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("null"); - param1.setClientDataType(ClientDataType.STRING); - params.add(param1); - - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertTrue(result.getIsExecutionSuccess()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "company_name", - "open_source" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - - // Verify value - assertEquals(JsonNodeType.STRING, node.get("open_source").getNodeType()); - - }) - .verifyComplete(); - } - - @Test - public void testNumericValuesHavingLeadingZeroWithPreparedStatement() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("SELECT {{binding1}} as numeric_string;"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("098765"); - param1.setClientDataType(ClientDataType.STRING); - params.add(param1); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertTrue(result.getIsExecutionSuccess()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "numeric_string" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - - // Verify value - assertEquals(JsonNodeType.STRING, node.get("numeric_string").getNodeType()); - assertEquals(param1.getValue(), node.get("numeric_string").asText()); - - }) - .verifyComplete(); - } - - @Test - public void testLongValueWithPreparedStatement() { - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); - - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select id from users LIMIT {{binding1}}"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - List params = new ArrayList<>(); - Param param1 = new Param(); - param1.setKey("binding1"); - param1.setValue("2147483648"); - param1.setClientDataType(ClientDataType.NUMBER); - params.add(param1); - executeActionDTO.setParams(params); - - Mono executeMono = dsConnectionMono - .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, - actionConfiguration)); - - StepVerifier.create(executeMono) - .assertNext(result -> { - assertTrue(result.getIsExecutionSuccess()); - final JsonNode node = ((ArrayNode) result.getBody()).get(0); - assertArrayEquals( - new String[]{ - "id" - }, - new ObjectMapper() - .convertValue(node, LinkedHashMap.class) - .keySet() - .toArray()); - - // Verify value - assertEquals(JsonNodeType.NUMBER, node.get("id").getNodeType()); - - }) - .verifyComplete(); - } - - @Test - public void testDatasourceDestroy() { - dsConfig = createDatasourceConfiguration(); - Mono connPoolMonoCache = pluginExecutor.datasourceCreate(dsConfig).cache(); - Mono testConnResultMono = connPoolMonoCache - .flatMap(conn -> pluginExecutor.testDatasource(conn)); - Mono> zipMono = zip(connPoolMonoCache, testConnResultMono); - StepVerifier.create(zipMono) - .assertNext(tuple2 -> { - DatasourceTestResult testDsResult = tuple2.getT2(); - assertEquals(0, testDsResult.getInvalids().size()); - - ConnectionPool conn = tuple2.getT1(); - pluginExecutor.datasourceDestroy(conn); - try { - /** - * We need to wait a few seconds before the next check because - * `datasourceDestroy` for MySQL Plugin is a non-blocking operation scheduled on - * a separate thread. We are hoping that by the time sleep ends, the other - * thread has finished execution. - */ - sleep(5000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - assertTrue(conn.isDisposed()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteCommon_queryWithComments_callValidationCallsAfterRemovingComments(){ - MySqlPlugin.MySqlPluginExecutor spyPlugin = spy(pluginExecutor); - - DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - ConnectionPool dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig).block(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration - .setBody("SELECT id FROM users WHERE -- IS operator\nid = 1 limit 1;"); - - List pluginSpecifiedTemplates = new ArrayList<>(); - pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); - actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); - HashMap requestData = new HashMap<>(); - - Mono resultMono = spyPlugin.executeCommon( - dsConnectionMono, - actionConfiguration, - TRUE, - null, - null, - requestData - ); - - StepVerifier.create(resultMono) - .assertNext(result -> { - assertTrue(result.getIsExecutionSuccess()); - - verify(spyPlugin).isIsOperatorUsed("SELECT id FROM users WHERE \nid = 1 limit 1;"); - - verify(spyPlugin).getIsSelectOrShowOrDescQuery("SELECT id FROM users WHERE \nid = 1 limit 1;"); - - }) - .verifyComplete(); - } - - @Test - public void verifyUniquenessOfMySQLPluginErrorCode() { - assert (Arrays.stream(MySQLPluginError.values()).map(MySQLPluginError::getAppErrorCode).distinct().count() == MySQLPluginError.values().length); - - assert (Arrays.stream(MySQLPluginError.values()).map(MySQLPluginError::getAppErrorCode) - .filter(appErrorCode-> appErrorCode.length() != 11 || !appErrorCode.startsWith("PE-MYS")) - .collect(Collectors.toList()).size() == 0); - - } + actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean IS {{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("True"); + param1.setClientDataType(ClientDataType.BOOLEAN); + params.add(param1); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .verifyErrorSatisfies(error -> { + assertTrue(error instanceof AppsmithPluginException); + String expectedMessage = MySQLErrorMessages.IS_KEYWORD_NOT_SUPPORTED_IN_PS_ERROR_MSG; + assertTrue(expectedMessage.equals(error.getMessage())); + }); + } + + @Test + public void testPreparedStatementWithRealTypes() { + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> connection.createBatch() + .add("create table test_real_types(id int, c_float float, c_double double, c_real real)") + .add("insert into test_real_types values (1, 1.123, 3.123, 5.123)") + .add("insert into test_real_types values (2, 11.123, 13.123, 15.123)")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + /** + * - For mysql float / double / real types the actual values that are stored in + * the db my differ by a very + * thin margin as long as they are approximately same. Hence adding comparison + * based check instead of direct + * equality. + * - Ref: https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html + */ + actionConfiguration.setBody( + "SELECT id FROM test_real_types WHERE ABS(c_float - {{binding1}}) < 0.1 AND ABS" + + "(c_double - {{binding2}}) < 0.1 AND ABS(c_real - {{binding3}}) < 0.1;"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("1.123"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + + Param param2 = new Param(); + param2.setKey("binding2"); + param2.setValue("3.123"); + param2.setClientDataType(ClientDataType.NUMBER); + params.add(param2); + + Param param3 = new Param(); + param3.setKey("binding3"); + param3.setValue("5.123"); + param3.setClientDataType(ClientDataType.NUMBER); + params.add(param3); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()); + assertEquals(1, node.size()); + // Verify selected row id. + assertEquals(1, node.get(0).get("id").asInt()); + }) + .verifyComplete(); + + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> connection.createBatch() + .add("drop table test_real_types")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + } + + private Publisher getConnectionFromBuilder(ConnectionFactoryOptions.Builder builder) { + return ConnectionFactories.get(builder.build()).create(); + } + + @Test + public void testPreparedStatementWithBooleanType() { + // Create a new table with boolean type + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> connection.createBatch() + .add("create table test_boolean_type(id int, c_boolean boolean)") + .add("insert into test_boolean_type values (1, True)") + .add("insert into test_boolean_type values (2, True)") + .add("insert into test_boolean_type values (3, False)")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id FROM test_boolean_type WHERE c_boolean={{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("True"); + param1.setClientDataType(ClientDataType.BOOLEAN); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()); + assertEquals(2, node.size()); + // Verify selected row id. + assertEquals(1, node.get(0).get("id").asInt()); + assertEquals(2, node.get(1).get("id").asInt()); + }) + .verifyComplete(); + + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> connection.createBatch() + .add("drop table test_boolean_type")) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + } + + @Test + public void testExecuteWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration + .setBody("SELECT id FROM users WHERE id = {{binding1}} limit 1 offset {{binding2}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("1"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + Param param2 = new Param(); + param2.setKey("binding2"); + param2.setValue("0"); + param2.setClientDataType(ClientDataType.NUMBER); + params.add(param2); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(1, node.get("id").asInt()); + + /* + * - Check if request params are sent back properly. + * - Not replicating the same to other tests as the overall flow remains the + * same w.r.t. request + * params. + */ + + // Check if '?' is replaced by $i. + assertEquals("SELECT id FROM users WHERE id = $1 limit 1 offset $2;", + ((RequestParamDTO) (((List) result.getRequest() + .getRequestParams())).get(0)).getValue()); + + // Check 1st prepared statement parameter + PsParameterDTO expectedPsParam1 = new PsParameterDTO("1", "INTEGER"); + PsParameterDTO returnedPsParam1 = (PsParameterDTO) ((RequestParamDTO) (((List) result + .getRequest().getRequestParams())).get(0)) + .getSubstitutedParams().get("$1"); + // Check if prepared stmt param value is correctly sent back. + assertEquals(expectedPsParam1.getValue(), returnedPsParam1.getValue()); + // Check if prepared stmt param type is correctly sent back. + assertEquals(expectedPsParam1.getType(), returnedPsParam1.getType()); + + // Check 2nd prepared statement parameter + PsParameterDTO expectedPsParam2 = new PsParameterDTO("0", "INTEGER"); + PsParameterDTO returnedPsParam2 = (PsParameterDTO) ((RequestParamDTO) (((List) result + .getRequest().getRequestParams())).get(0)) + .getSubstitutedParams().get("$2"); + // Check if prepared stmt param value is correctly sent back. + assertEquals(expectedPsParam2.getValue(), returnedPsParam2.getValue()); + // Check if prepared stmt param type is correctly sent back. + assertEquals(expectedPsParam2.getType(), returnedPsParam2.getType()); + }) + .verifyComplete(); + + return; + } + + @Test + public void testExecuteDataTypes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("2018", node.get("yob").asText()); + assertTrue(node.get("time1").asText().matches("\\d{2}:\\d{2}:\\d{2}")); + assertTrue(node.get("created_on").asText() + .matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + assertTrue(node.get("updated_on").isNull()); + + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "yob", + "time1", + "created_on", + "updated_on" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + }) + .verifyComplete(); + } + + /** + * 1. Add a test to check that mysql driver can interpret and read all the + * regular data types used in mysql. + * 2. List of the data types is taken is from + * https://dev.mysql.com/doc/refman/8.0/en/data-types.html + * 3. Data types tested here are: INTEGER, SMALLINT, TINYINT, MEDIUMINT, BIGINT, + * DECIMAL, FLOAT, DOUBLE, BIT, + * DATE, DATETIME, TIMESTAMP, TIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, + * TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, + * TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, JSON, GEOMETRY, POINT + */ + @Test + public void testExecuteDataTypesExtensive() throws AppsmithPluginException { + String query_create_table_numeric_types = "create table test_numeric_types (c_integer INTEGER, c_smallint " + + + "SMALLINT, c_tinyint TINYINT, c_mediumint MEDIUMINT, c_bigint BIGINT, c_decimal DECIMAL, c_float " + + + "FLOAT, c_double DOUBLE, c_bit BIT(10));"; + String query_insert_into_table_numeric_types = "insert into test_numeric_types values (-1, 1, 1, 10, 2000, 1" + + + ".02345, 0.1234, 1.0102344, b'0101010');"; + + String query_create_table_date_time_types = "create table test_date_time_types (c_date DATE, c_datetime " + + + "DATETIME DEFAULT CURRENT_TIMESTAMP, c_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, c_time TIME, " + + + "c_year YEAR);"; + String query_insert_into_table_date_time_types = "insert into test_date_time_types values ('2020-12-01', " + + + "'2020-12-01 20:20:20', '2020-12-01 20:20:20', '20:20:20', 2020);"; + + String query_create_table_data_types = "create table test_data_types (c_char CHAR(50), c_varchar VARCHAR(50)," + + + " c_binary BINARY(20), c_varbinary VARBINARY(20), c_tinyblob TINYBLOB, c_blob BLOB, c_mediumblob " + + + "MEDIUMBLOB, c_longblob LONGBLOB, c_tinytext TINYTEXT, c_text TEXT, c_mediumtext MEDIUMTEXT, " + + + "c_longtext LONGTEXT, c_enum ENUM('ONE'), c_set SET('a'));"; + String query_insert_data_types = "insert into test_data_types values ('test', 'test', 'a\\0\\t', 'a\\0\\t', " + + + "'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'ONE', 'a');"; + + String query_create_table_json_data_type = "create table test_json_type (c_json JSON);"; + String query_insert_json_data_type = "insert into test_json_type values ('{\"key1\": \"value1\", \"key2\": " + + + "\"value2\"}');"; + + String query_create_table_geometry_types = "create table test_geometry_types (c_geometry GEOMETRY, c_point " + + + "POINT);"; + String query_insert_geometry_types = "insert into test_geometry_types values (ST_GeomFromText('POINT(1 1)'), " + + + "ST_PointFromText('POINT(1 100)'));"; + + String query_select_from_test_numeric_types = "select * from test_numeric_types;"; + String query_select_from_test_date_time_types = "select * from test_date_time_types;"; + String query_select_from_test_json_data_type = "select * from test_json_type;"; + String query_select_from_test_data_types = "select * from test_data_types;"; + String query_select_from_test_geometry_types = "select * from test_geometry_types;"; + + String expected_numeric_types_result = "[{\"c_integer\":-1,\"c_smallint\":1,\"c_tinyint\":1,\"" + + + "c_mediumint\":10,\"c_bigint\":2000,\"c_decimal\":1,\"c_float\":0.1234,\"c_double\":1.0102344," + + + "\"c_bit\":{\"empty\":false}}]"; + + String expected_date_time_types_result = "[{\"c_date\":\"2020-12-01\",\"c_datetime\":\"2020-12-01T20:20:20Z\"," + + + "\"c_timestamp\":\"2020-12-01T20:20:20Z\",\"c_time\":\"20:20:20\",\"c_year\":2020}]"; + + String expected_data_types_result = "[{\"c_char\":\"test\",\"c_varchar\":\"test\"," + + + "\"c_binary\":\"YQAJAAAAAAAAAAAAAAAAAAAAAAA=\",\"c_varbinary\":\"YQAJ\",\"c_tinyblob\":\"dGVzdA==\"," + + + "\"c_blob\":\"dGVzdA==\",\"c_mediumblob\":\"dGVzdA==\",\"c_longblob\":\"dGVzdA==\",\"c_tinytext\":\"test\"," + + + "\"c_text\":\"test\",\"c_mediumtext\":\"test\",\"c_longtext\":\"test\",\"c_enum\":\"ONE\",\"c_set\":\"a\"}]"; + + String expected_json_result = "[{\"c_json\":\"{\\\"key1\\\": \\\"value1\\\", \\\"key2\\\": \\\"value2\\\"}\"}]"; + + String expected_geometry_types_result = "[{\"c_geometry\":\"AAAAAAEBAAAAAAAAAAAA8D8AAAAAAADwPw==\"," + + + "\"c_point\":\"AAAAAAEBAAAAAAAAAAAA8D8AAAAAAABZQA==\"}]"; + + + + Mono.from(getConnectionMonoFromContainer(mySQLContainer)) + .map(connection -> { + return connection.createBatch() + .add(query_create_table_numeric_types) + .add(query_insert_into_table_numeric_types) + .add(query_create_table_date_time_types) + .add(query_insert_into_table_date_time_types) + .add(query_create_table_json_data_type) + .add(query_insert_json_data_type) + .add(query_create_table_data_types) + .add(query_insert_data_types) + .add(query_create_table_geometry_types) + .add(query_insert_geometry_types); + }) + .flatMapMany(batch -> Flux.from(batch.execute())) + .blockLast(); // wait until completion of all the queries + + /* Test numeric types */ + testExecute(query_select_from_test_numeric_types, expected_numeric_types_result); + /* Test date time types */ + testExecute(query_select_from_test_date_time_types, expected_date_time_types_result); + /* Test data types */ + testExecute(query_select_from_test_data_types, expected_data_types_result); + /* Test json type */ + testExecute(query_select_from_test_json_data_type, expected_json_result); + /* Test geometry types */ + testExecute(query_select_from_test_geometry_types, expected_geometry_types_result); + + return; + } + + private void testExecute(String query, String expectedResult) { + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(query); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + if (expectedResult != null) { + assertEquals(expectedResult, result.getBody().toString()); + } + }) + .verifyComplete(); + } + + @Test + public void testStructure() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono structureMono = pluginExecutor.datasourceCreate(dsConfig) + .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); + + StepVerifier.create(structureMono) + .assertNext(structure -> { + assertNotNull(structure); + assertEquals(2, structure.getTables().size()); + + Optional possessionsTableOptional = structure + .getTables() + .stream() + .filter(table -> table.getName() + .equalsIgnoreCase("possessions")) + .findFirst(); + assertTrue(possessionsTableOptional.isPresent()); + final DatasourceStructure.Table possessionsTable = possessionsTableOptional + .get(); + assertEquals(DatasourceStructure.TableType.TABLE, possessionsTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("id", "int", + null, false), + new DatasourceStructure.Column("title", + "varchar", null, false), + new DatasourceStructure.Column("user_id", "int", + null, false), + new DatasourceStructure.Column("username", + "varchar", null, false), + new DatasourceStructure.Column("email", + "varchar", null, false), + }, + possessionsTable.getColumns().toArray()); + + final DatasourceStructure.PrimaryKey possessionsPrimaryKey = new DatasourceStructure.PrimaryKey( + "PRIMARY", List.of("id")); + final DatasourceStructure.ForeignKey possessionsUserForeignKey = new DatasourceStructure.ForeignKey( + "possessions_ibfk_1", + List.of("username", "email"), + List.of("users.username", "users.email")); + assertArrayEquals( + new DatasourceStructure.Key[]{possessionsPrimaryKey, + possessionsUserForeignKey}, + possessionsTable.getKeys().toArray()); + + assertArrayEquals( + new DatasourceStructure.Template[]{ + new DatasourceStructure.Template("SELECT", + "SELECT * FROM possessions LIMIT 10;"), + new DatasourceStructure.Template("INSERT", + "INSERT INTO possessions (id, title, user_id, username, email)\n" + + + " VALUES (1, '', 1, '', '');"), + new DatasourceStructure.Template("UPDATE", + "UPDATE possessions SET\n" + + " id = 1,\n" + + + " title = '',\n" + + + " user_id = 1,\n" + + + " username = '',\n" + + + " email = ''\n" + + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", + "DELETE FROM possessions\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + possessionsTable.getTemplates().toArray()); + + Optional usersTableOptional = structure.getTables() + .stream() + .filter(table -> table.getName().equalsIgnoreCase("users")) + .findFirst(); + assertTrue(usersTableOptional.isPresent()); + final DatasourceStructure.Table usersTable = usersTableOptional.get(); + assertEquals(DatasourceStructure.TableType.TABLE, usersTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("id", "int", + null, true), + new DatasourceStructure.Column("username", + "varchar", null, false), + new DatasourceStructure.Column("password", + "varchar", null, false), + new DatasourceStructure.Column("email", + "varchar", null, false), + new DatasourceStructure.Column("spouse_dob", + "date", null, false), + new DatasourceStructure.Column("dob", "date", + null, false), + new DatasourceStructure.Column("yob", "year", + null, false), + new DatasourceStructure.Column("time1", "time", + null, false), + new DatasourceStructure.Column("created_on", + "timestamp", null, false), + new DatasourceStructure.Column("updated_on", + "datetime", null, false) + }, + usersTable.getColumns().toArray()); + + final DatasourceStructure.PrimaryKey usersPrimaryKey = new DatasourceStructure.PrimaryKey( + "PRIMARY", List.of("id")); + assertArrayEquals( + new DatasourceStructure.Key[]{usersPrimaryKey}, + usersTable.getKeys().toArray()); + + assertArrayEquals( + new DatasourceStructure.Template[]{ + new DatasourceStructure.Template("SELECT", + "SELECT * FROM users LIMIT 10;"), + new DatasourceStructure.Template("INSERT", + "INSERT INTO users (id, username, password, email, spouse_dob, dob, yob, time1, created_on, updated_on)\n" + + + " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');"), + new DatasourceStructure.Template("UPDATE", + "UPDATE users SET\n" + + " id = 1,\n" + + + " username = '',\n" + + + " password = '',\n" + + + " email = '',\n" + + + " spouse_dob = '2019-07-01',\n" + + + " dob = '2019-07-01',\n" + + + " yob = '',\n" + + + " time1 = '',\n" + + + " created_on = '2019-07-01 10:00:00',\n" + + + " updated_on = '2019-07-01 10:00:00'\n" + + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + new DatasourceStructure.Template("DELETE", + "DELETE FROM users\n" + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + }, + usersTable.getTemplates().toArray()); + }) + .verifyComplete(); + } + + @Test + public void testSslToggleMissingError() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(null); + + Mono> invalidsMono = Mono.just(pluginExecutor) + .map(executor -> executor.validateDatasource(datasourceConfiguration)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + String expectedError = "Appsmith server has failed to fetch SSL configuration from datasource " + + + "configuration form. Please reach out to Appsmith customer support to resolve this."; + assertTrue(invalids + .stream() + .anyMatch(error -> expectedError.equals(error))); + }) + .verifyComplete(); + } + + @Test + public void testSslDisabled() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DISABLED); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testSslRequired() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.REQUIRED); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"TLS_AES_128_GCM_SHA256\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testSslDefault() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("show session status like 'Ssl_cipher'"); + + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DEFAULT); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, + actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + Object body = result.getBody(); + assertNotNull(body); + assertEquals("[{\"Variable_name\":\"Ssl_cipher\",\"Value\":\"\"}]", + body.toString()); + }) + .verifyComplete(); + } + + @Test + public void testDuplicateColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody( + "SELECT id, username as id, password, email as password FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), + dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotEquals(0, result.getMessages().size()); + + String expectedMessage = "Your MySQL query result may not have all the columns because duplicate column names " + + + "were found for the column(s)"; + assertTrue( + result.getMessages().stream() + .anyMatch(message -> message + .contains(expectedMessage))); + + /* + * - Check if all of the duplicate column names are reported. + */ + Set expectedColumnNames = Stream.of("id", "password") + .collect(Collectors.toCollection(HashSet::new)); + Set foundColumnNames = new HashSet<>(); + result.getMessages().stream() + .filter(message -> message.contains(expectedMessage)) + .forEach(message -> { + Arrays.stream(message.split(":")[1].split("\\.")[0] + .split(",")) + .forEach(columnName -> foundColumnNames + .add(columnName.trim())); + }); + assertTrue(expectedColumnNames.equals(foundColumnNames)); + }) + .verifyComplete(); + } + + @Test + public void testExecuteDescribeTableCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("describe users"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteDescTableCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("desc users"); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + String expectedBody = "[{\"Field\":\"id\",\"Type\":\"int\",\"Null\":\"NO\",\"Key\":\"PRI\",\"Default\":null,\"Extra\":\"auto_increment\"},{\"Field\":\"username\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"password\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"email\",\"Type\":\"varchar(250)\",\"Null\":\"NO\",\"Key\":\"UNI\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"spouse_dob\",\"Type\":\"date\",\"Null\":\"YES\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"dob\",\"Type\":\"date\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"yob\",\"Type\":\"year\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"time1\",\"Type\":\"time\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"created_on\",\"Type\":\"timestamp\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"},{\"Field\":\"updated_on\",\"Type\":\"datetime\",\"Null\":\"NO\",\"Key\":\"\",\"Default\":null,\"Extra\":\"\"}]"; + assertEquals(expectedBody, result.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testNullObjectWithPreparedStatement() { + pluginExecutor = spy(new MySqlPlugin.MySqlPluginExecutor()); + doReturn(false).when(pluginExecutor).isIsOperatorUsed(any()); + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * from (\n" + + "\tselect 'Appsmith' as company_name, true as open_source\n" + + "\tunion\n" + + "\tselect 'Retool' as company_name, false as open_source\n" + + "\tunion\n" + + "\tselect 'XYZ' as company_name, null as open_source\n" + + ") t\n" + + "where t.open_source IS {{binding1}}"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue(null); + param1.setClientDataType(ClientDataType.NULL); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "company_name", + "open_source" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.NULL, node.get("open_source").getNodeType()); + + }) + .verifyComplete(); + } + + @Test + public void testNullAsStringWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * from (\n" + + "\tselect 'Appsmith' as company_name, true as open_source\n" + + "\tunion\n" + + "\tselect 'Retool' as company_name, false as open_source\n" + + "\tunion\n" + + "\tselect 'XYZ' as company_name, 'null' as open_source\n" + + ") t\n" + + "where t.open_source = {{binding1}};"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("null"); + param1.setClientDataType(ClientDataType.STRING); + params.add(param1); + + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "company_name", + "open_source" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.STRING, node.get("open_source").getNodeType()); + + }) + .verifyComplete(); + } + + @Test + public void testNumericValuesHavingLeadingZeroWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT {{binding1}} as numeric_string;"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("098765"); + param1.setClientDataType(ClientDataType.STRING); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "numeric_string" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.STRING, node.get("numeric_string").getNodeType()); + assertEquals(param1.getValue(), node.get("numeric_string").asText()); + + }) + .verifyComplete(); + } + + @Test + public void testLongValueWithPreparedStatement() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select id from users LIMIT {{binding1}}"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("binding1"); + param1.setValue("2147483648"); + param1.setClientDataType(ClientDataType.NUMBER); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.executeParameterized(conn, executeActionDTO, dsConfig, + actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray()); + + // Verify value + assertEquals(JsonNodeType.NUMBER, node.get("id").getNodeType()); + + }) + .verifyComplete(); + } + + @Test + public void testDatasourceDestroy() { + dsConfig = createDatasourceConfiguration(); + Mono connPoolMonoCache = pluginExecutor.datasourceCreate(dsConfig).cache(); + Mono testConnResultMono = connPoolMonoCache + .flatMap(conn -> pluginExecutor.testDatasource(conn)); + Mono> zipMono = zip(connPoolMonoCache, testConnResultMono); + StepVerifier.create(zipMono) + .assertNext(tuple2 -> { + DatasourceTestResult testDsResult = tuple2.getT2(); + assertEquals(0, testDsResult.getInvalids().size()); + + ConnectionPool conn = tuple2.getT1(); + pluginExecutor.datasourceDestroy(conn); + try { + /** + * We need to wait a few seconds before the next check because + * `datasourceDestroy` for MySQL Plugin is a non-blocking operation scheduled on + * a separate thread. We are hoping that by the time sleep ends, the other + * thread has finished execution. + */ + sleep(5000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertTrue(conn.isDisposed()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteCommon_queryWithComments_callValidationCallsAfterRemovingComments(){ + MySqlPlugin.MySqlPluginExecutor spyPlugin = spy(pluginExecutor); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + ConnectionPool dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig).block(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration + .setBody("SELECT id FROM users WHERE -- IS operator\nid = 1 limit 1;"); + + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("preparedStatement", "true")); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + HashMap requestData = new HashMap<>(); + + Mono resultMono = spyPlugin.executeCommon( + dsConnectionMono, + actionConfiguration, + TRUE, + null, + null, + requestData + ); + + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + + verify(spyPlugin).isIsOperatorUsed("SELECT id FROM users WHERE \nid = 1 limit 1;"); + + verify(spyPlugin).getIsSelectOrShowOrDescQuery("SELECT id FROM users WHERE \nid = 1 limit 1;"); + + }) + .verifyComplete(); + } + + @Test + public void verifyUniquenessOfMySQLPluginErrorCode() { + assert (Arrays.stream(MySQLPluginError.values()).map(MySQLPluginError::getAppErrorCode).distinct().count() == MySQLPluginError.values().length); + + assert (Arrays.stream(MySQLPluginError.values()).map(MySQLPluginError::getAppErrorCode) + .filter(appErrorCode-> appErrorCode.length() != 11 || !appErrorCode.startsWith("PE-MYS")) + .collect(Collectors.toList()).size() == 0); + + } } \ No newline at end of file diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlStaleConnectionErrorMessageTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlStaleConnectionErrorMessageTest.java new file mode 100644 index 0000000000..f956cad738 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlStaleConnectionErrorMessageTest.java @@ -0,0 +1,117 @@ +package com.external.plugins; + +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.external.utils.MySqlDatasourceUtils; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.ValidationDepth; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.pool.PoolShutdownException; +import reactor.test.StepVerifier; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static com.external.plugins.exceptions.MySQLErrorMessages.CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MySqlStaleConnectionErrorMessageTest { + static MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); + static MySqlDatasourceUtils mysqlDatasourceUtils = new MySqlDatasourceUtils(); + + @Test + public void testStaleConnectionExceptionReturnsUpstreamErrorOnTimeoutError() throws TimeoutException { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select 1;"); + ConnectionPool mockConnectionPool = mock(ConnectionPool.class); + String expectedErrorMessage = "Timeout exception from MockConnectionPool"; + when(mockConnectionPool.create()).thenReturn(Mono.error(new TimeoutException(expectedErrorMessage))); + Mono actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(), + new ExecuteActionDTO(), new HashMap<>()); + StepVerifier.create(actionExecutionResultMono) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof StaleConnectionException); + assertEquals(expectedErrorMessage, error.getMessage()); + }) + .verify(); + } + + @Test + public void testStaleConnectionExceptionReturnsUpstreamErrorOnPoolShutdownError() throws TimeoutException { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select 1;"); + ConnectionPool mockConnectionPool = mock(ConnectionPool.class); + String expectedErrorMessage = "Timeout exception from MockConnectionPool"; + when(mockConnectionPool.create()).thenReturn(Mono.error(new PoolShutdownException(expectedErrorMessage))); + Mono actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(), + new ExecuteActionDTO(), new HashMap<>()); + StepVerifier.create(actionExecutionResultMono) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof StaleConnectionException); + assertEquals(expectedErrorMessage, error.getMessage()); + }) + .verify(); + } + + @Test + public void testStaleConnectionExceptionReturnsUpstreamErrorOnIllegalStateError() throws TimeoutException { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select 1;"); + ConnectionPool mockConnectionPool = mock(ConnectionPool.class); + String expectedErrorMessage = "Timeout exception from MockConnectionPool"; + when(mockConnectionPool.create()).thenReturn(Mono.error(new IllegalStateException(expectedErrorMessage))); + Mono actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(), + new ExecuteActionDTO(), new HashMap<>()); + StepVerifier.create(actionExecutionResultMono) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof StaleConnectionException); + assertEquals(expectedErrorMessage, error.getMessage()); + }) + .verify(); + } + + @Test + public void testStaleConnectionExceptionReturnsUpstreamErrorOnResourceError() throws TimeoutException { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select 1;"); + ConnectionPool mockConnectionPool = mock(ConnectionPool.class); + String expectedErrorMessage = "Timeout exception from MockConnectionPool"; + when(mockConnectionPool.create()).thenReturn(Mono.error(new R2dbcNonTransientResourceException(expectedErrorMessage))); + Mono actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(), + new ExecuteActionDTO(), new HashMap<>()); + StepVerifier.create(actionExecutionResultMono) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof StaleConnectionException); + assertEquals(expectedErrorMessage, error.getMessage()); + }) + .verify(); + } + + @Test + public void testStaleConnectionExceptionReturnsUpstreamErrorOnInvalidConnection() throws TimeoutException { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select 1;"); + ConnectionPool mockConnectionPool = mock(ConnectionPool.class); + Connection mockConnection = mock(Connection.class); + when(mockConnectionPool.create()).thenReturn(Mono.just(mockConnection)); + when(mockConnection.validate(ValidationDepth.LOCAL)).thenReturn(Mono.just(false)); + when(mockConnection.close()).thenReturn(Mono.empty()); + Mono actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(), + new ExecuteActionDTO(), new HashMap<>()); + StepVerifier.create(actionExecutionResultMono) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof StaleConnectionException); + assertEquals(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG, error.getMessage()); + }) + .verify(); + } +} diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java index b08c22e832..b836ac5ec4 100644 --- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java +++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java @@ -58,6 +58,7 @@ import java.util.stream.IntStream; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; import static com.appsmith.external.constants.CommonFieldName.BODY; import static com.appsmith.external.constants.CommonFieldName.PREPARED_STATEMENT; +import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME; import static com.appsmith.external.helpers.PluginUtils.OBJECT_TYPE; import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE; import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData; @@ -67,7 +68,6 @@ import static com.appsmith.external.helpers.PluginUtils.setDataValueSafelyInForm import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex; import static com.external.plugins.utils.OracleDatasourceUtils.JDBC_DRIVER; import static com.external.plugins.utils.OracleDatasourceUtils.createConnectionPool; -import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool; import static com.external.plugins.utils.OracleDatasourceUtils.logHikariCPStatus; import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution; import static com.external.plugins.utils.OracleExecuteUtils.isPLSQL; @@ -79,6 +79,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; @Slf4j public class OraclePlugin extends BasePlugin { + public static final OracleDatasourceUtils oracleDatasourceUtils = new OracleDatasourceUtils(); public OraclePlugin(PluginWrapper wrapper) { super(wrapper); @@ -197,7 +198,9 @@ public class OraclePlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connectionPool); + connectionFromPool = + oracleDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool, + ORACLE_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The underlying hikari // library throws SQLException in case the pool is closed or there is an issue initializing @@ -205,7 +208,8 @@ public class OraclePlugin extends BasePlugin { // and should then trigger the destruction and recreation of the pool. log.debug("Exception Occurred while getting connection from pool" + e.getMessage()); e.printStackTrace(System.out); - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : + new StaleConnectionException(e.getMessage())); } List> rowsList = new ArrayList<>(50); diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java index 54fb8dbf26..54618753d7 100644 --- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java +++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class OracleErrorMessages { - private OracleErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class OracleErrorMessages extends BasePluginErrorMessages { public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query."; public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your Oracle query failed to execute."; @@ -34,8 +36,6 @@ public class OracleErrorMessages { public static final String DS_INVALID_HOSTNAME_ERROR_MSG = "Host value cannot contain `/` or `:` characters. Found `%s`."; - public static final String DS_MISSING_CONNECTION_MODE_ERROR_MSG = "Missing connection mode."; - public static final String DS_MISSING_AUTHENTICATION_DETAILS_ERROR_MSG = "Missing authentication details."; public static final String DS_MISSING_USERNAME_ERROR_MSG = "Missing username for authentication."; diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java index de6cb82b45..8254eeb618 100644 --- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java +++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java @@ -32,8 +32,14 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP; import static com.external.plugins.OraclePlugin.OraclePluginExecutor.scheduler; +import static com.external.plugins.OraclePlugin.oracleDatasourceUtils; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.springframework.util.CollectionUtils.isEmpty; @@ -155,13 +161,15 @@ public class OracleDatasourceUtils { return Mono.fromSupplier(() -> { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connectionPool); + connectionFromPool = + oracleDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool, ORACLE_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The // underlying hikari library throws SQLException in case the pool is closed or there is an issue // initializing the connection pool which can also be translated in our world to // StaleConnectionException and should then trigger the destruction and recreation of the pool. - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : + new StaleConnectionException(e.getMessage())); } logHikariCPStatus("Before getting Oracle DB schema", connectionPool); @@ -439,21 +447,6 @@ public class OracleDatasourceUtils { return datasource; } - /** - * First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return - * In case a connection is not available in the pool, SQL Exception is thrown - */ - public static java.sql.Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException { - - if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { - System.out.println(Thread.currentThread().getName() + - ": Encountered stale connection pool in Oracle plugin. Reporting back."); - throw new StaleConnectionException(); - } - - return connectionPool.getConnection(); - } - public static void logHikariCPStatus(String logPrefix, HikariDataSource connectionPool) { HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean(); int idleConnections = poolProxy.getIdleConnections(); @@ -463,4 +456,36 @@ public class OracleDatasourceUtils { log.debug(MessageFormat.format("{0}: Hikari Pool stats : active - {1} , idle - {2}, awaiting - {3} , total - {4}", logPrefix, activeConnections, idleConnections, threadsAwaitingConnection, totalConnections)); } + + public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException { + if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { + String printMessage = MessageFormat.format(Thread.currentThread().getName() + + ": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName); + System.out.println(printMessage); + + if (connectionPool == null) { + throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG); + } + else if (connectionPool.isClosed()) { + throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG); + } + else if (!connectionPool.isRunning()) { + throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG); + } + else { + /** + * Ideally, code flow is never expected to reach here. However, this section has been added to catch + * those cases wherein a developer updates the parent if condition but does not update the nested + * if else conditions. + */ + throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG); + } + } + } + + public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool, + String pluginName) throws SQLException { + checkHikariCPConnectionPoolValidity(connectionPool, pluginName); + return connectionPool.getConnection(); + } } diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java index 37b9df1e47..ce479766f2 100644 --- a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java +++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java @@ -1,11 +1,23 @@ package com.external.plugins; +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.external.plugins.exceptions.OraclePluginError; +import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.stream.Collectors; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; +import static com.external.plugins.OracleTestDBContainerManager.oracleDatasourceUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class OraclePluginErrorsTest { @Test public void verifyUniquenessOfOraclePluginErrorCode() { @@ -15,4 +27,63 @@ public class OraclePluginErrorsTest { .filter(appErrorCode -> appErrorCode.length() != 11 || !appErrorCode.startsWith("PE-ORC")) .collect(Collectors.toList()).size() == 0); } + + /** + * Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We + * want to re-factor this identical code into a common method in future (#24763) - could not be done right now because + * of some package dependency issues. + */ + @Test + public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsNull() { + Exception exception = assertThrows(StaleConnectionException.class, + () -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(null + , "pluginName")); + String expectedErrorMessage = CONNECTION_POOL_NULL_ERROR_MSG; + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + /** + * Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We + * want to re-factor this identical code into a common method in future (#24763) - could not be done right now because + * of some package dependency issues. + */ + @Test + public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsClosed() { + HikariDataSource mockConnectionPool = mock(HikariDataSource.class); + when(mockConnectionPool.isClosed()).thenReturn(true).thenReturn(true); + Exception exception = assertThrows(StaleConnectionException.class, + () -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName")); + String expectedErrorMessage = CONNECTION_POOL_CLOSED_ERROR_MSG; + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + /** + * Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We + * want to re-factor this identical code into a common method in future (#24763) - could not be done right now because + * of some package dependency issues. + */ + @Test + public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsRunning() { + HikariDataSource mockConnectionPool = mock(HikariDataSource.class); + when(mockConnectionPool.isRunning()).thenReturn(false).thenReturn(false); + Exception exception = assertThrows(StaleConnectionException.class, + () -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName")); + String expectedErrorMessage = CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + /** + * Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We + * want to re-factor this identical code into a common method in future (#24763) - could not be done right now because + * of some package dependency issues. + */ + @Test + public void testStaleConnectionErrorHasDefaultUpstreamError() { + HikariDataSource mockConnectionPool = mock(HikariDataSource.class); + when(mockConnectionPool.isRunning()).thenReturn(false).thenReturn(true); + Exception exception = assertThrows(StaleConnectionException.class, + () -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName")); + String expectedErrorMessage = UNKNOWN_CONNECTION_ERROR_MSG; + assertEquals(expectedErrorMessage, exception.getMessage()); + } } diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java index 9cb628d848..7b128702a7 100644 --- a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java +++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java @@ -5,6 +5,7 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.SSLDetails; +import com.external.plugins.utils.OracleDatasourceUtils; import com.zaxxer.hikari.HikariDataSource; import org.testcontainers.containers.OracleContainer; @@ -12,7 +13,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; -import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool; +import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME; import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution; public class OracleTestDBContainerManager { @@ -20,6 +21,8 @@ public class OracleTestDBContainerManager { public static final String ORACLE_PASSWORD = "testPassword"; public static final String ORACLE_DB_NAME = "testDB"; public static final String ORACLE_DOCKER_HUB_CONTAINER = "gvenzl/oracle-xe:21-slim-faststart"; + + public static OracleDatasourceUtils oracleDatasourceUtils = new OracleDatasourceUtils(); static OraclePlugin.OraclePluginExecutor oraclePluginExecutor = new OraclePlugin.OraclePluginExecutor(); public static OracleContainer getOracleDBForTest() { @@ -49,7 +52,9 @@ public class OracleTestDBContainerManager { } static void runSQLQueryOnOracleTestDB(String sqlQuery, HikariDataSource sharedConnectionPool) throws SQLException { - java.sql.Connection connectionFromPool = getConnectionFromConnectionPool(sharedConnectionPool); + java.sql.Connection connectionFromPool = + oracleDatasourceUtils.getConnectionFromHikariConnectionPool(sharedConnectionPool, + ORACLE_PLUGIN_NAME); Statement statement = connectionFromPool.createStatement(); statement.execute(sqlQuery); closeConnectionPostExecution(null, statement, null, connectionFromPool); diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index f1d56ac4a8..594902cd8f 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -28,6 +28,7 @@ import com.appsmith.external.services.SharedConfig; import com.external.plugins.datatypes.PostgresSpecificDataTypes; import com.external.plugins.exceptions.PostgresErrorMessages; import com.external.plugins.exceptions.PostgresPluginError; +import com.external.plugins.utils.PostgresDatasourceUtils; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariPoolMXBean; @@ -76,6 +77,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.appsmith.external.constants.PluginConstants.PluginName.POSTGRES_PLUGIN_NAME; import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel; @@ -124,13 +126,14 @@ public class PostgresPlugin extends BasePlugin { private static int MAX_SIZE_SUPPORTED; + public static PostgresDatasourceUtils postgresDatasourceUtils = new PostgresDatasourceUtils(); + public PostgresPlugin(PluginWrapper wrapper) { super(wrapper); } @Extension public static class PostgresPluginExecutor implements SmartSubstitutionInterface, PluginExecutor { - private final Scheduler scheduler = Schedulers.boundedElastic(); private static final String TABLES_QUERY = "select a.attname as name,\n" @@ -281,7 +284,8 @@ public class PostgresPlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connection, datasourceConfiguration); + connectionFromPool = postgresDatasourceUtils.getConnectionFromHikariConnectionPool(connection, + POSTGRES_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The // underlying hikari @@ -290,7 +294,7 @@ public class PostgresPlugin extends BasePlugin { // the connection pool which can also be translated in our world to // StaleConnectionException // and should then trigger the destruction and recreation of the pool. - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException(e.getMessage())); } List> rowsList = new ArrayList<>(50); @@ -631,7 +635,8 @@ public class PostgresPlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connection, datasourceConfiguration); + connectionFromPool = postgresDatasourceUtils.getConnectionFromHikariConnectionPool(connection, + POSTGRES_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The // underlying hikari @@ -640,7 +645,8 @@ public class PostgresPlugin extends BasePlugin { // the connection pool which can also be translated in our world to // StaleConnectionException // and should then trigger the destruction and recreation of the pool. - return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException()); + return Mono.error(e instanceof StaleConnectionException ? e : + new StaleConnectionException(e.getMessage())); } HikariPoolMXBean poolProxy = connection.getHikariPoolMXBean(); @@ -1101,30 +1107,4 @@ public class PostgresPlugin extends BasePlugin { return datasource; } - - /** - * First checks if the connection pool is still valid. If yes, we fetch a - * connection from the pool and return - * In case a connection is not available in the pool, SQL Exception is thrown - * - * @param connectionPool - * @return SQL Connection - */ - private static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool, - DatasourceConfiguration datasourceConfiguration) throws SQLException { - - if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { - log.debug("Encountered stale connection pool in Postgres plugin. Reporting back."); - throw new StaleConnectionException(); - } - - Connection connection = connectionPool.getConnection(); - - com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); - if (configurationConnection == null) { - return connection; - } - - return connection; - } } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java index 0ddd94d341..5f3eb6a0af 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/exceptions/PostgresErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class PostgresErrorMessages { - private PostgresErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class PostgresErrorMessages extends BasePluginErrorMessages { public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query."; public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your PostgreSQL query failed to execute."; diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/utils/PostgresDatasourceUtils.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/utils/PostgresDatasourceUtils.java new file mode 100644 index 0000000000..31e606b0b1 --- /dev/null +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/utils/PostgresDatasourceUtils.java @@ -0,0 +1,47 @@ +package com.external.plugins.utils; + +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.text.MessageFormat; + +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; + +public class PostgresDatasourceUtils { + public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException { + if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { + String printMessage = MessageFormat.format(Thread.currentThread().getName() + + ": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName); + System.out.println(printMessage); + + if (connectionPool == null) { + throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG); + } + else if (connectionPool.isClosed()) { + throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG); + } + else if (!connectionPool.isRunning()) { + throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG); + } + else { + /** + * Ideally, code flow is never expected to reach here. However, this section has been added to catch + * those cases wherein a developer updates the parent if condition but does not update the nested + * if else conditions. + */ + throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG); + } + } + } + + public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool, + String pluginName) throws SQLException { + checkHikariCPConnectionPoolValidity(connectionPool, pluginName); + return connectionPool.getConnection(); + } +} diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java index 5021c4a592..bd464ff4ba 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class RedisErrorMessages { - private RedisErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class RedisErrorMessages extends BasePluginErrorMessages { public static final String BODY_IS_NULL_OR_EMPTY_ERROR_MSG = "Body is null or empty [%s]"; public static final String QUERY_PARSING_FAILED_ERROR_MSG = "Appsmith server has failed to parse your Redis query. Are you sure it's" + diff --git a/app/server/appsmith-plugins/redshiftPlugin/pom.xml b/app/server/appsmith-plugins/redshiftPlugin/pom.xml index c451138a21..88a7b439b1 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/pom.xml +++ b/app/server/appsmith-plugins/redshiftPlugin/pom.xml @@ -28,7 +28,6 @@ com.amazon.redshift redshift-jdbc42 2.1.0.9 - runtime com.zaxxer diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java index b329d85113..98a83b5905 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java @@ -15,6 +15,7 @@ import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.external.plugins.exceptions.RedshiftErrorMessages; import com.external.plugins.exceptions.RedshiftPluginError; +import com.external.utils.RedshiftDatasourceUtils; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariPoolMXBean; import lombok.NonNull; @@ -47,15 +48,17 @@ import java.util.Set; import java.util.stream.Collectors; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.appsmith.external.constants.PluginConstants.PluginName.REDSHIFT_PLUGIN_NAME; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.JDBC_DRIVER_LOADING_ERROR_MSG; import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin; import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns; import static com.external.utils.RedshiftDatasourceUtils.createConnectionPool; -import static com.external.utils.RedshiftDatasourceUtils.getConnectionFromConnectionPool; @Slf4j public class RedshiftPlugin extends BasePlugin { public static final String JDBC_DRIVER = "com.amazon.redshift.jdbc.Driver"; private static final String DATE_COLUMN_TYPE_NAME = "date"; + public static RedshiftDatasourceUtils redshiftDatasourceUtils = new RedshiftDatasourceUtils(); public RedshiftPlugin(PluginWrapper wrapper) { super(wrapper); @@ -212,7 +215,7 @@ public class RedshiftPlugin extends BasePlugin { return Mono.fromCallable(() -> { Connection connection = null; try { - connection = getConnectionFromConnectionPool(connectionPool); + connection = redshiftDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool, REDSHIFT_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { e.printStackTrace(); @@ -230,13 +233,13 @@ public class RedshiftPlugin extends BasePlugin { // library throws SQLException in case the pool is closed or there is an issue initializing // the connection pool which can also be translated in our world to StaleConnectionException // and should then trigger the destruction and recreation of the pool. - return Mono.error(new StaleConnectionException()); + return Mono.error(new StaleConnectionException(e.getMessage())); } /** - * Keeping this print statement post call to getConnectionFromConnectionPool because it checks for - * stale connection pool. + * Keeping this print statement post call to getConnectionFromHikariConnectionPool because it + * checks for stale connection pool. */ printConnectionPoolStatus(connectionPool, false); @@ -359,7 +362,7 @@ public class RedshiftPlugin extends BasePlugin { try { Class.forName(JDBC_DRIVER); } catch (ClassNotFoundException e) { - return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, RedshiftErrorMessages.JDBC_DRIVER_LOADING_ERROR_MSG, e.getMessage())); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, JDBC_DRIVER_LOADING_ERROR_MSG, e.getMessage())); } return Mono @@ -569,7 +572,8 @@ public class RedshiftPlugin extends BasePlugin { return Mono.fromSupplier(() -> { Connection connection = null; try { - connection = getConnectionFromConnectionPool(connectionPool); + connection = redshiftDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool + , REDSHIFT_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { e.printStackTrace(); @@ -587,7 +591,7 @@ public class RedshiftPlugin extends BasePlugin { // library throws SQLException in case the pool is closed or there is an issue initializing // the connection pool which can also be translated in our world to StaleConnectionException // and should then trigger the destruction and recreation of the pool. - return Mono.error(new StaleConnectionException()); + return Mono.error(new StaleConnectionException(e.getMessage())); } /** diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/exceptions/RedshiftErrorMessages.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/exceptions/RedshiftErrorMessages.java index 82e0fe7bcb..13d02be5e6 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/exceptions/RedshiftErrorMessages.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/exceptions/RedshiftErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class RedshiftErrorMessages { - private RedshiftErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class RedshiftErrorMessages extends BasePluginErrorMessages { public static final String QUERY_PARAMETER_MISSING_ERROR_MSG = "Missing required parameter: Query."; public static final String NULL_RESULTSET_ERROR_MSG = "Redshift driver failed to fetch result: resultSet is null."; @@ -18,7 +19,5 @@ public class RedshiftErrorMessages { public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch the structure of the database. " + "Please check if the database credentials are valid and/or you have the required permissions."; - public static final String JDBC_DRIVER_LOADING_ERROR_MSG = "Error loading Redshift JDBC Driver class."; - public static final String CONNECTION_POOL_CREATION_FAILED_ERROR_MSG = "Exception occurred while creating connection pool. One or more arguments in the datasource configuration may be invalid. Please check your datasource configuration."; } diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/utils/RedshiftDatasourceUtils.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/utils/RedshiftDatasourceUtils.java index 1d3231ed63..a1a0203344 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/utils/RedshiftDatasourceUtils.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/utils/RedshiftDatasourceUtils.java @@ -14,9 +14,14 @@ import org.springframework.util.StringUtils; import java.sql.Connection; import java.sql.SQLException; +import java.text.MessageFormat; import java.util.List; import java.util.stream.Collectors; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; import static com.external.plugins.RedshiftPlugin.JDBC_DRIVER; public class RedshiftDatasourceUtils { @@ -95,14 +100,35 @@ public class RedshiftDatasourceUtils { return datasource; } - public static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException { - + public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException { if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { - System.out.println(Thread.currentThread().getName() + - ": Encountered stale connection pool in Redshift plugin. Reporting back."); - throw new StaleConnectionException(); - } + String printMessage = MessageFormat.format(Thread.currentThread().getName() + + ": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName); + System.out.println(printMessage); + if (connectionPool == null) { + throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG); + } + else if (connectionPool.isClosed()) { + throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG); + } + else if (!connectionPool.isRunning()) { + throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG); + } + else { + /** + * Ideally, code flow is never expected to reach here. However, this section has been added to catch + * those cases wherein a developer updates the parent if condition but does not update the nested + * if else conditions. + */ + throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG); + } + } + } + + public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool, + String pluginName) throws SQLException { + checkHikariCPConnectionPoolValidity(connectionPool, pluginName); return connectionPool.getConnection(); } } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/exceptions/RestApiErrorMessages.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/exceptions/RestApiErrorMessages.java index 0230f989c5..406975b4e5 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/exceptions/RestApiErrorMessages.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/exceptions/RestApiErrorMessages.java @@ -1,9 +1,11 @@ package com.external.plugins.exceptions; -public class RestApiErrorMessages { - private RestApiErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class RestApiErrorMessages extends BasePluginErrorMessages { public static final String URI_SYNTAX_WRONG_ERROR_MSG = "Invalid value of URI."; public static final String INVALID_CONTENT_TYPE_ERROR_MSG = "Invalid value for Content-Type."; public static final String NO_HTTP_METHOD_ERROR_MSG = "HTTPMethod must be set."; diff --git a/app/server/appsmith-plugins/saasPlugin/src/main/java/com/external/plugins/exceptions/SaaSErrorMessages.java b/app/server/appsmith-plugins/saasPlugin/src/main/java/com/external/plugins/exceptions/SaaSErrorMessages.java index 1a7ef0491a..05bc7028ea 100644 --- a/app/server/appsmith-plugins/saasPlugin/src/main/java/com/external/plugins/exceptions/SaaSErrorMessages.java +++ b/app/server/appsmith-plugins/saasPlugin/src/main/java/com/external/plugins/exceptions/SaaSErrorMessages.java @@ -1,10 +1,11 @@ package com.external.plugins.exceptions; -public class SaaSErrorMessages { - private SaaSErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class SaaSErrorMessages extends BasePluginErrorMessages { public static final String MISSING_DATASOURCE_TEMPLATE_NAME_ERROR_MSG = "Missing template name for datasource"; public static final String MISSING_ACTION_TEMPLATE_NAME_ERROR_MSG = "Missing template name for action"; diff --git a/app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/exceptions/SMTPErrorMessages.java b/app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/exceptions/SMTPErrorMessages.java index fa4b4b6942..82030e18af 100644 --- a/app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/exceptions/SMTPErrorMessages.java +++ b/app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/exceptions/SMTPErrorMessages.java @@ -1,10 +1,10 @@ package com.external.plugins.exceptions; -public class SMTPErrorMessages { - private SMTPErrorMessages() { - //Prevents instantiation - } +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class SMTPErrorMessages { public static final String RECIPIENT_ADDRESS_NOT_FOUND_ERROR_MSG = "Couldn't find a valid recipient address. Please check your action configuration."; public static final String SENDER_ADDRESS_NOT_FOUND_ERROR_MSG = "Couldn't find a valid sender address. Please check your action configuration."; diff --git a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java index 6352962990..8337d5ec5b 100644 --- a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java +++ b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java @@ -13,7 +13,6 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.external.plugins.exceptions.SnowflakeErrorMessages; -import com.external.plugins.exceptions.SnowflakePluginError; import com.external.utils.SqlUtils; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @@ -22,7 +21,6 @@ import com.zaxxer.hikari.pool.HikariPool; import lombok.extern.slf4j.Slf4j; import org.pf4j.Extension; import org.pf4j.PluginWrapper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; @@ -41,7 +39,9 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import static com.appsmith.external.constants.PluginConstants.PluginName.SNOWFLAKE_PLUGIN_NAME; import static com.external.utils.ExecutionUtils.getRowsFromQueryResult; +import static com.external.utils.SnowflakeDatasourceUtils.getConnectionFromHikariConnectionPool; import static com.external.utils.ValidationUtils.validateWarehouseDatabaseSchema; @Slf4j @@ -83,12 +83,19 @@ public class SnowflakePlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connection); + /** + * The getConnectionFromHikariConnectionPool method used here is the duplicate of + * method defined in PluginUtils.java and not the same one. Please check the comment on + * the method definition to understand more. + */ + connectionFromPool = + getConnectionFromHikariConnectionPool(connection, + SNOWFLAKE_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { if (e instanceof StaleConnectionException) { throw e; } else { - throw new StaleConnectionException(); + throw new StaleConnectionException(e.getMessage()); } } @@ -275,7 +282,14 @@ public class SnowflakePlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connectionPool); + /** + * The getConnectionFromHikariConnectionPool method used here is the duplicate of + * method defined in PluginUtils.java and not the same one. Please check the comment on + * the method definition to understand more. + */ + connectionFromPool = + getConnectionFromHikariConnectionPool(connectionPool, + SNOWFLAKE_PLUGIN_NAME); return Mono.just(validateWarehouseDatabaseSchema(connectionFromPool)); } catch (SQLException e) { // The function can throw either StaleConnectionException or SQLException. The underlying hikari @@ -313,7 +327,13 @@ public class SnowflakePlugin extends BasePlugin { Connection connectionFromPool; try { - connectionFromPool = getConnectionFromConnectionPool(connection); + /** + * The getConnectionFromHikariConnectionPool method used here is the duplicate of + * method defined in PluginUtils.java and not the same one. Please check the comment on + * the method definition to understand more. + */ + connectionFromPool = + getConnectionFromHikariConnectionPool(connection, SNOWFLAKE_PLUGIN_NAME); } catch (SQLException | StaleConnectionException e) { // The function can throw either StaleConnectionException or SQLException. The underlying hikari // library throws SQLException in case the pool is closed or there is an issue initializing @@ -391,22 +411,5 @@ public class SnowflakePlugin extends BasePlugin { }) .subscribeOn(scheduler); } - - /** - * First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return - * In case a connection is not available in the pool, SQL Exception is thrown - * - * @param connectionPool - * @return SQL Connection - */ - private static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException { - - if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { - log.debug("Encountered stale connection pool in Snowflake plugin. Reporting back."); - throw new StaleConnectionException(); - } - - return connectionPool.getConnection(); - } } } \ No newline at end of file diff --git a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/exceptions/SnowflakeErrorMessages.java b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/exceptions/SnowflakeErrorMessages.java index 6ff02c01e2..68516db8fb 100644 --- a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/exceptions/SnowflakeErrorMessages.java +++ b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/exceptions/SnowflakeErrorMessages.java @@ -1,17 +1,15 @@ package com.external.plugins.exceptions; -public class SnowflakeErrorMessages { - private SnowflakeErrorMessages() { - //Prevents instantiation - } +import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation +public class SnowflakeErrorMessages extends BasePluginErrorMessages { public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query."; - public static final String DRIVER_NOT_FOUND_ERROR_MSG = "Snowflake driver not found. Please reach out to Appsmith support to resolve this issue."; - public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details."; - public static final String CONNECTION_CREATION_FAILED_ERROR_MSG = "Error occurred while connecting to Snowflake endpoint: %s"; - public static final String UNABLE_TO_CREATE_CONNECTION_ERROR_MSG = "Unable to create connection to Snowflake URL"; public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch the structure of your schema. Please check more information in the error details."; diff --git a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/ExecutionUtils.java b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/ExecutionUtils.java index c1cf43b629..3a9bafb129 100644 --- a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/ExecutionUtils.java +++ b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/ExecutionUtils.java @@ -17,6 +17,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static com.external.plugins.exceptions.SnowflakeErrorMessages.CONNECTION_INVALID_ERROR_MSG; + @Slf4j public class ExecutionUtils { /** @@ -38,7 +40,7 @@ public class ExecutionUtils { // Instead for every execution, we check for connection validity, // and reset the connection if required if (!connection.isValid(30)) { - throw new StaleConnectionException(); + throw new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG); } statement = connection.createStatement(); @@ -58,7 +60,7 @@ public class ExecutionUtils { } } catch (SQLException e) { if (e instanceof SnowflakeReauthenticationRequest) { - throw new StaleConnectionException(); + throw new StaleConnectionException(e.getMessage()); } log.error("Exception caught when executing Snowflake query. Cause: ", e); throw new AppsmithPluginException(SnowflakePluginError.QUERY_EXECUTION_FAILED, SnowflakeErrorMessages.QUERY_EXECUTION_FAILED_ERROR_MSG, e.getMessage(), "SQLSTATE: " + e.getSQLState() ); diff --git a/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/SnowflakeDatasourceUtils.java b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/SnowflakeDatasourceUtils.java new file mode 100644 index 0000000000..f3e5f94054 --- /dev/null +++ b/app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/utils/SnowflakeDatasourceUtils.java @@ -0,0 +1,47 @@ +package com.external.utils; + +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.text.MessageFormat; + +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG; +import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG; + +public class SnowflakeDatasourceUtils { + public static void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException { + if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) { + String printMessage = MessageFormat.format(Thread.currentThread().getName() + + ": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName); + System.out.println(printMessage); + + if (connectionPool == null) { + throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG); + } + else if (connectionPool.isClosed()) { + throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG); + } + else if (!connectionPool.isRunning()) { + throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG); + } + else { + /** + * Ideally, code flow is never expected to reach here. However, this section has been added to catch + * those cases wherein a developer updates the parent if condition but does not update the nested + * if else conditions. + */ + throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG); + } + } + } + + public static Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool, + String pluginName) throws SQLException { + checkHikariCPConnectionPoolValidity(connectionPool, pluginName); + return connectionPool.getConnection(); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java index 1bca048661..57f28f6209 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java @@ -644,7 +644,7 @@ public class ActionExecutionSolutionCEImpl implements ActionExecutionSolutionCE return new AppsmithPluginException(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR, actionDTO.getName(), timeoutDuration); } else if (error instanceof StaleConnectionException e) { - return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR); + return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR, e.getMessage()); } else { return error; }