diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MySQLNoiseTest_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MySQLNoiseTest_spec.js index 7479016c4a..7bdf9db30e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MySQLNoiseTest_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MySQLNoiseTest_spec.js @@ -63,10 +63,8 @@ describe("MySQL noise test", function() { expect(response.body.data.statusCode).to.eq("200 OK"); }); cy.wait("@postExecute").then(({ response }) => { - expect(response.body.data.statusCode).to.eq("5004"); - expect(response.body.data.title).to.eq( - "Datasource configuration is invalid", - ); + expect(response.body.data.statusCode).to.eq("5000"); + expect(response.body.data.title).to.eq("Query execution error"); }); }); }); diff --git a/app/server/appsmith-plugins/mysqlPlugin/pom.xml b/app/server/appsmith-plugins/mysqlPlugin/pom.xml index f89aac55e0..ccf36e41f9 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/pom.xml +++ b/app/server/appsmith-plugins/mysqlPlugin/pom.xml @@ -24,27 +24,6 @@ - - - org.springframework.data - spring-data-r2dbc - 1.1.5.RELEASE - - - org.slf4j - slf4j-api - - - io.projectreactor - reactor-core - - - org.reactivestreams - reactive-streams - - - - org.mariadb r2dbc-mariadb @@ -85,6 +64,35 @@ 4.1.75.Final + + io.r2dbc + r2dbc-pool + + 0.8.8.RELEASE + + + io.netty + * + + + org.slf4j + slf4j-api + + + io.projectreactor + reactor-core + + + org.reactivestreams + reactive-streams + + + + mysql 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 82f6152d1a..cf5ae754b0 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 @@ -10,26 +10,24 @@ import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; -import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; import com.appsmith.external.models.RequestParamDTO; -import com.appsmith.external.models.SSLDetails; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.external.plugins.SmartSubstitutionInterface; import com.external.plugins.datatypes.MySQLSpecificDataTypes; +import com.external.utils.MySqlDatasourceUtils; import com.external.utils.QueryUtils; +import io.r2dbc.pool.ConnectionPool; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.ConnectionFactoryOptions; -import io.r2dbc.spi.Option; +import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -37,15 +35,14 @@ import io.r2dbc.spi.Statement; import io.r2dbc.spi.ValidationDepth; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ObjectUtils; -import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; import org.pf4j.Extension; import org.pf4j.PluginWrapper; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import reactor.pool.PoolShutdownException; import java.time.Duration; import java.time.LocalDate; @@ -64,7 +61,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; import java.util.stream.IntStream; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; @@ -72,16 +68,16 @@ 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 io.r2dbc.spi.ConnectionFactoryOptions.SSL; +import static com.external.utils.MySqlDatasourceUtils.getNewConnectionPool; +import static com.external.utils.MySqlGetStructureUtils.getKeyInfo; +import static com.external.utils.MySqlGetStructureUtils.getTableInfo; +import static com.external.utils.MySqlGetStructureUtils.getTemplates; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @Slf4j public class MySqlPlugin extends BasePlugin { - private static final String DATE_COLUMN_TYPE_NAME = "date"; - private static final String DATETIME_COLUMN_TYPE_NAME = "datetime"; - private static final String TIMESTAMP_COLUMN_TYPE_NAME = "timestamp"; private static final int VALIDATION_CHECK_TIMEOUT = 4; // seconds private static final String IS_KEY = "is"; @@ -142,7 +138,7 @@ public class MySqlPlugin extends BasePlugin { } @Extension - public static class MySqlPluginExecutor implements PluginExecutor, SmartSubstitutionInterface { + public static class MySqlPluginExecutor implements PluginExecutor, SmartSubstitutionInterface { private static final int PREPARED_STATEMENT_INDEX = 0; private final Scheduler scheduler = Schedulers.boundedElastic(); @@ -162,7 +158,7 @@ public class MySqlPlugin extends BasePlugin { * @return */ @Override - public Mono executeParameterized(Connection connection, + public Mono executeParameterized(ConnectionPool connection, ExecuteActionDTO executeActionDTO, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { @@ -223,7 +219,7 @@ public class MySqlPlugin extends BasePlugin { return executeCommon(connection, actionConfiguration, TRUE, mustacheKeysInOrder, executeActionDTO, requestData); } - public Mono executeCommon(Connection connection, + public Mono executeCommon(ConnectionPool connectionPool, ActionConfiguration actionConfiguration, Boolean preparedStatement, List mustacheValuesInOrder, @@ -233,6 +229,7 @@ public class MySqlPlugin extends BasePlugin { String query = actionConfiguration.getBody(); /** + * TBD: check if this comment is resolved with the new MariaDB driver. * - 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. @@ -260,87 +257,96 @@ public class MySqlPlugin extends BasePlugin { List requestParams = List.of(new RequestParamDTO(ACTION_CONFIGURATION_BODY, transformedQuery, null, null, psParams)); - // TODO: need to write a JUnit TC for VALIDATION_CHECK_TIMEOUT - Flux resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE)) - .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) - .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) - .flatMapMany(isValid -> { - if (isValid) { - return createAndExecuteQueryFromConnection(finalQuery, - connection, - preparedStatement, - mustacheValuesInOrder, - executeActionDTO, - requestData, - psParams); - } - return Flux.error(new StaleConnectionException()); - }); + return Mono.usingWhen( + 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)) + .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) + .flatMapMany(isValid -> { + if (isValid) { + return createAndExecuteQueryFromConnection(finalQuery, + connection, + preparedStatement, + mustacheValuesInOrder, + executeActionDTO, + requestData, + psParams); + } + return Flux.error(new StaleConnectionException()); + }); - Mono>> resultMono; + Mono>> resultMono; + if (isSelectOrShowOrDescQuery) { + resultMono = resultFlux + .flatMap(result -> + result.map((row, meta) -> { + rowsList.add(getRow(row, meta)); - if (isSelectOrShowOrDescQuery) { - resultMono = resultFlux - .flatMap(result -> - result.map((row, meta) -> { - rowsList.add(getRow(row, meta)); + if (columnsList.isEmpty()) { + meta.getColumnMetadatas().stream().forEach(columnMetadata -> columnsList.add(columnMetadata.getName())); + } - if (columnsList.isEmpty()) { - meta.getColumnMetadatas().stream().forEach(columnMetadata -> columnsList.add(columnMetadata.getName())); - } - - return result; - } - ) - ) - .collectList() - .thenReturn(rowsList); - } else { - resultMono = resultFlux - .flatMap(Result::getRowsUpdated) - .collectList() - .map(list -> list.get(list.size() - 1)) - .map(rowsUpdated -> { - rowsList.add( - Map.of( - "affectedRows", - ObjectUtils.defaultIfNull(rowsUpdated, 0) + return result; + } + ) ) - ); - return rowsList; - }); - } - - return resultMono - .map(res -> { - ActionExecutionResult result = new ActionExecutionResult(); - result.setBody(objectMapper.valueToTree(rowsList)); - result.setMessages(populateHintMessages(columnsList)); - result.setIsExecutionSuccess(true); - log.debug("In the MySqlPlugin, got action execution result"); - return result; - }) - .onErrorResume(error -> { - if (error instanceof StaleConnectionException) { - return Mono.error(error); + .collectList() + .thenReturn(rowsList); + } else { + resultMono = resultFlux + .flatMap(Result::getRowsUpdated) + .collectList() + .map(list -> list.get(list.size() - 1)) + .map(rowsUpdated -> { + rowsList.add( + Map.of( + "affectedRows", + ObjectUtils.defaultIfNull(rowsUpdated, 0) + ) + ); + return rowsList; + }); } - ActionExecutionResult result = new ActionExecutionResult(); - result.setIsExecutionSuccess(false); - result.setErrorInfo(error); - return Mono.just(result); - }) - // Now set the request in the result to be returned back to the server - .map(actionExecutionResult -> { - ActionExecutionRequest request = new ActionExecutionRequest(); - request.setQuery(finalQuery); - request.setProperties(requestData); - request.setRequestParams(requestParams); - ActionExecutionResult result = actionExecutionResult; - result.setRequest(request); - return result; - }) - .subscribeOn(scheduler); + return resultMono + .map(res -> { + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(rowsList)); + result.setMessages(populateHintMessages(columnsList)); + result.setIsExecutionSuccess(true); + log.debug("In the MySqlPlugin, got action execution result"); + return result; + }) + .onErrorResume(error -> { + if (error instanceof StaleConnectionException) { + return Mono.error(error); + } + ActionExecutionResult result = new ActionExecutionResult(); + result.setIsExecutionSuccess(false); + result.setErrorInfo(error); + return Mono.just(result); + }) + // Now set the request in the result to be returned back to the server + .map(actionExecutionResult -> { + ActionExecutionRequest request = new ActionExecutionRequest(); + request.setQuery(finalQuery); + request.setProperties(requestData); + request.setRequestParams(requestParams); + ActionExecutionResult result = actionExecutionResult; + result.setRequest(request); + + return result; + }); + }, + Connection::close + ) + .timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT)) + .onErrorMap(TimeoutException.class, error -> new StaleConnectionException()) + .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException()) + .onErrorMap(R2dbcNonTransientResourceException.class, error -> new StaleConnectionException()) + .subscribeOn(scheduler); } boolean isIsOperatorUsed(String query) { @@ -388,6 +394,14 @@ public class MySqlPlugin extends BasePlugin { } + @Override + public Mono testDatasource(ConnectionPool pool) { + return Mono.just(pool) + .flatMap(p -> p.create()) + .flatMap(conn -> Mono.from(conn.close())) + .then(Mono.just(new DatasourceTestResult())); + } + @Override public Object substituteValueInInput(int index, String binding, @@ -500,112 +514,26 @@ public class MySqlPlugin extends BasePlugin { } @Override - public Mono execute(Connection connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { + public Mono execute(ConnectionPool connection, + DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { // Unused function return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation")); } @Override - public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { - DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); - - StringBuilder urlBuilder = new StringBuilder(); - if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { - urlBuilder.append(datasourceConfiguration.getUrl()); - } else { - urlBuilder.append("r2dbc:mysql://"); - final List hosts = new ArrayList<>(); - - for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { - hosts.add(endpoint.getHost() + ":" + ObjectUtils.defaultIfNull(endpoint.getPort(), 3306L)); - } - - urlBuilder.append(String.join(",", hosts)).append("/"); - - if (StringUtils.hasLength(authentication.getDatabaseName())) { - urlBuilder.append(authentication.getDatabaseName()); - } - + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + ConnectionPool pool = null; + try { + pool = getNewConnectionPool(datasourceConfiguration); + } catch (AppsmithPluginException e) { + return Mono.error(e); } - - urlBuilder.append("?zeroDateTimeBehavior=convertToNull"); - final List dsProperties = datasourceConfiguration.getProperties(); - - if (dsProperties != null) { - for (Property property : dsProperties) { - if ("serverTimezone".equals(property.getKey()) && !StringUtils.isEmpty(property.getValue())) { - urlBuilder.append("&serverTimezone=").append(property.getValue()); - break; - } - } - } - - - ConnectionFactoryOptions baseOptions = ConnectionFactoryOptions.parse(urlBuilder.toString()); - ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions) - .option(ConnectionFactoryOptions.DRIVER, MariadbConnectionFactoryProvider.MARIADB_DRIVER) - .option(MariadbConnectionFactoryProvider.ALLOW_MULTI_QUERIES, true) - .option(ConnectionFactoryOptions.USER, authentication.getUsername()) - .option(ConnectionFactoryOptions.PASSWORD, authentication.getPassword()); - - /* - * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. - */ - if (datasourceConfiguration.getConnection() == null - || datasourceConfiguration.getConnection().getSsl() == null - || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { - return Mono.error( - new AppsmithPluginException( - AppsmithPluginError.PLUGIN_ERROR, - "Appsmith server has failed to fetch SSL configuration from datasource configuration form. " + - "Please reach out to Appsmith customer support to resolve this." - ) - ); - } - - /* - * - By default, the driver configures SSL in the preferred mode. - */ - SSLDetails.AuthType sslAuthType = datasourceConfiguration.getConnection().getSsl().getAuthType(); - switch (sslAuthType) { - case PREFERRED: - case REQUIRED: - ob = ob - .option(SSL, true) - .option(Option.valueOf("sslMode"), sslAuthType.toString().toLowerCase()); - - break; - case DISABLED: - ob = ob.option(SSL, false); - - break; - case DEFAULT: - /* do nothing - accept default driver setting*/ - - break; - default: - return Mono.error( - new AppsmithPluginException( - AppsmithPluginError.PLUGIN_ERROR, - "Appsmith server has found an unexpected SSL option: " + sslAuthType + ". Please reach out to" + - " Appsmith customer support to resolve this." - ) - ); - } - - return (Mono) Mono.from(ConnectionFactories.get(ob.build()).create()) - .onErrorResume(exception -> Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, - exception - ))) - .subscribeOn(scheduler); + return Mono.just(pool); } - @Override - public void datasourceDestroy(Connection connection) { - - if (connection != null) { - Mono.from(connection.close()) + public void datasourceDestroy(ConnectionPool connectionPool) { + if (connectionPool != null) { + Mono.just(connectionPool.disposeLater()) .onErrorResume(exception -> { log.debug("In datasourceDestroy function error mode.", exception); return Mono.empty(); @@ -617,258 +545,72 @@ public class MySqlPlugin extends BasePlugin { @Override public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { - - Set invalids = new HashSet<>(); - - if (datasourceConfiguration.getConnection() != null - && datasourceConfiguration.getConnection().getMode() == null) { - invalids.add("Missing Connection Mode."); - } - - if (StringUtils.isEmpty(datasourceConfiguration.getUrl()) && - CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { - invalids.add("Missing endpoint and url"); - } else if (!CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { - for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) { - if (endpoint.getHost() == null || endpoint.getHost().isBlank()) { - invalids.add("Host value cannot be empty"); - } else if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { - invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); - } - } - } - - if (datasourceConfiguration.getAuthentication() == null) { - invalids.add("Missing authentication details."); - } else { - DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); - if (StringUtils.isEmpty(authentication.getUsername())) { - invalids.add("Missing username for authentication."); - } - - if (StringUtils.isEmpty(authentication.getPassword()) && StringUtils.isEmpty(authentication.getUsername())) { - invalids.add("Missing password for authentication."); - } else if (StringUtils.isEmpty(authentication.getPassword())) { - // it is valid if it has the username but not the password - authentication.setPassword(""); - } - - if (StringUtils.isEmpty(authentication.getDatabaseName())) { - invalids.add("Missing database name."); - } - } - - /* - * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. - */ - if (datasourceConfiguration.getConnection() == null - || datasourceConfiguration.getConnection().getSsl() == null - || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { - invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration form. " + - "Please reach out to Appsmith customer support to resolve this."); - } - - return invalids; - } - - /** - * 1. Parse results obtained by running COLUMNS_QUERY defined on top of the page. - * 2. A sample mysql output for the query is also given near COLUMNS_QUERY definition on top of the page. - */ - private void getTableInfo(Row row, RowMetadata meta, Map tablesByName) { - final String tableName = row.get("table_name", String.class); - - if (!tablesByName.containsKey(tableName)) { - tablesByName.put(tableName, new DatasourceStructure.Table( - DatasourceStructure.TableType.TABLE, - null, - tableName, - new ArrayList<>(), - new ArrayList<>(), - new ArrayList<>() - )); - } - - final DatasourceStructure.Table table = tablesByName.get(tableName); - table.getColumns().add(new DatasourceStructure.Column( - row.get("column_name", String.class), - row.get("column_type", String.class), - null, - row.get("extra", String.class).contains("auto_increment") - )); - - return; - } - - /** - * 1. Parse results obtained by running KEYS_QUERY defined on top of the page. - * 2. A sample mysql output for the query is also given near KEYS_QUERY definition on top of the page. - */ - private void getKeyInfo(Row row, RowMetadata meta, Map tablesByName, - Map keyRegistry) { - final String constraintName = row.get("constraint_name", String.class); - final char constraintType = row.get("constraint_type", String.class).charAt(0); - final String selfSchema = row.get("self_schema", String.class); - final String tableName = row.get("self_table", String.class); - - - if (!tablesByName.containsKey(tableName)) { - /* do nothing */ - return; - } - - final DatasourceStructure.Table table = tablesByName.get(tableName); - final String keyFullName = tableName + "." + row.get("constraint_name", String.class); - - if (constraintType == 'p') { - if (!keyRegistry.containsKey(keyFullName)) { - final DatasourceStructure.PrimaryKey key = new DatasourceStructure.PrimaryKey( - constraintName, - new ArrayList<>() - ); - keyRegistry.put(keyFullName, key); - table.getKeys().add(key); - } - ((DatasourceStructure.PrimaryKey) keyRegistry.get(keyFullName)).getColumnNames() - .add(row.get("self_column", String.class)); - } else if (constraintType == 'f') { - final String foreignSchema = row.get("foreign_schema", String.class); - final String prefix = (foreignSchema.equalsIgnoreCase(selfSchema) ? "" : foreignSchema + ".") - + row.get("foreign_table", String.class) + "."; - - if (!keyRegistry.containsKey(keyFullName)) { - final DatasourceStructure.ForeignKey key = new DatasourceStructure.ForeignKey( - constraintName, - new ArrayList<>(), - new ArrayList<>() - ); - keyRegistry.put(keyFullName, key); - table.getKeys().add(key); - } - - ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getFromColumns() - .add(row.get("self_column", String.class)); - ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getToColumns() - .add(prefix + row.get("foreign_column", String.class)); - } - - return; - } - - /** - * 1. Generate template for all tables in the database. - */ - private void getTemplates(Map tablesByName) { - for (DatasourceStructure.Table table : tablesByName.values()) { - final List columnsWithoutDefault = table.getColumns() - .stream() - .filter(column -> column.getDefaultValue() == null) - .collect(Collectors.toList()); - - final List columnNames = new ArrayList<>(); - final List columnValues = new ArrayList<>(); - final StringBuilder setFragments = new StringBuilder(); - - for (DatasourceStructure.Column column : columnsWithoutDefault) { - final String name = column.getName(); - final String type = column.getType(); - String value; - - if (type == null) { - value = "null"; - } else if ("text".equals(type) || "varchar".equals(type)) { - value = "''"; - } else if (type.startsWith("int")) { - value = "1"; - } else if (type.startsWith("double")) { - value = "1.0"; - } else if (DATE_COLUMN_TYPE_NAME.equals(type)) { - value = "'2019-07-01'"; - } else if (DATETIME_COLUMN_TYPE_NAME.equals(type) - || TIMESTAMP_COLUMN_TYPE_NAME.equals(type)) { - value = "'2019-07-01 10:00:00'"; - } else { - value = "''"; - } - - columnNames.add(name); - columnValues.add(value); - setFragments.append("\n ").append(name).append(" = ").append(value).append(","); - } - - // Delete the last comma - if (setFragments.length() > 0) { - setFragments.deleteCharAt(setFragments.length() - 1); - } - - final String tableName = table.getName(); - table.getTemplates().addAll(List.of( - new DatasourceStructure.Template("SELECT", "SELECT * FROM " + tableName + " LIMIT 10;"), - new DatasourceStructure.Template("INSERT", "INSERT INTO " + tableName - + " (" + String.join(", ", columnNames) + ")\n" - + " VALUES (" + String.join(", ", columnValues) + ");"), - new DatasourceStructure.Template("UPDATE", "UPDATE " + tableName + " SET" - + setFragments + "\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 " + tableName - + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") - )); - } + return MySqlDatasourceUtils.validateDatasource(datasourceConfiguration); } @Override - public Mono getStructure(Connection connection, DatasourceConfiguration datasourceConfiguration) { + public Mono getStructure(ConnectionPool connectionPool, + DatasourceConfiguration datasourceConfiguration) { final DatasourceStructure structure = new DatasourceStructure(); final Map tablesByName = new LinkedHashMap<>(); final Map keyRegistry = new HashMap<>(); - return Mono.from(connection.validate(ValidationDepth.REMOTE)) + 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); + + 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_ERROR, + e.getMessage() + ); + } + + return e; + }); + }, + Connection::close + ) .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); - - 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_ERROR, - e.getMessage() - ); - } - - return e; - }) + .onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException()) .subscribeOn(scheduler); } } 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 new file mode 100644 index 0000000000..029b64ca8d --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.java @@ -0,0 +1,193 @@ +package com.external.utils; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.Property; +import com.appsmith.external.models.SSLDetails; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; +import org.apache.commons.lang.ObjectUtils; +import org.mariadb.r2dbc.MariadbConnectionConfiguration; +import org.mariadb.r2dbc.MariadbConnectionFactory; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +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); + + public static ConnectionFactoryOptions.Builder getBuilder(DatasourceConfiguration datasourceConfiguration) { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + + StringBuilder urlBuilder = new StringBuilder(); + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + urlBuilder.append(datasourceConfiguration.getUrl()); + } else { + urlBuilder.append("r2dbc:pool:mariadb://"); + final List hosts = new ArrayList<>(); + + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + hosts.add(endpoint.getHost() + ":" + ObjectUtils.defaultIfNull(endpoint.getPort(), 3306L)); + } + + urlBuilder.append(String.join(",", hosts)).append("/"); + + if (!StringUtils.isEmpty(authentication.getDatabaseName())) { + urlBuilder.append(authentication.getDatabaseName()); + } + + } + + urlBuilder.append("?zeroDateTimeBehavior=convertToNull&allowMultiQueries=true"); + final List dsProperties = datasourceConfiguration.getProperties(); + + if (dsProperties != null) { + for (Property property : dsProperties) { + if ("serverTimezone".equals(property.getKey()) && !StringUtils.isEmpty(property.getValue())) { + urlBuilder.append("&serverTimezone=").append(property.getValue()); + break; + } + } + } + + ConnectionFactoryOptions baseOptions = ConnectionFactoryOptions.parse(urlBuilder.toString()); + ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions) + .option(ConnectionFactoryOptions.USER, authentication.getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, authentication.getPassword()); + + return ob; + } + + public static ConnectionFactoryOptions.Builder addSslOptionsToBuilder(DatasourceConfiguration datasourceConfiguration, + ConnectionFactoryOptions.Builder ob) throws AppsmithPluginException { + /* + * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. + */ + if (datasourceConfiguration.getConnection() == null + || datasourceConfiguration.getConnection().getSsl() == null + || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith server has failed to fetch SSL configuration from datasource configuration form. " + + "Please reach out to Appsmith customer support to resolve this."); + } + + /* + * - By default, the driver configures SSL in the preferred mode. + */ + SSLDetails.AuthType sslAuthType = datasourceConfiguration.getConnection().getSsl().getAuthType(); + switch (sslAuthType) { + case REQUIRED: + ob = ob + .option(SSL, true) + .option(Option.valueOf("sslMode"), sslAuthType.toString().toLowerCase()); + + break; + case DISABLED: + ob = ob.option(SSL, false); + + break; + case DEFAULT: + /* do nothing - accept default driver setting*/ + + break; + default: + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith server has found an unexpected SSL option: " + sslAuthType + ". Please reach out to" + + " Appsmith customer support to resolve this."); + } + + return ob; + } + + public static Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (datasourceConfiguration.getConnection() != null + && datasourceConfiguration.getConnection().getMode() == null) { + invalids.add("Missing Connection Mode."); + } + + if (StringUtils.isEmpty(datasourceConfiguration.getUrl()) && + CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint and url"); + } else if (!CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + if (endpoint.getHost() == null || endpoint.getHost().isBlank()) { + invalids.add("Host value cannot be empty"); + } else if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { + invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); + } + } + } + + if (datasourceConfiguration.getAuthentication() == null) { + invalids.add("Missing authentication details."); + } else { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + if (StringUtils.isEmpty(authentication.getUsername())) { + invalids.add("Missing username for authentication."); + } + + if (StringUtils.isEmpty(authentication.getPassword()) && StringUtils.isEmpty(authentication.getUsername())) { + invalids.add("Missing password for authentication."); + } else if (StringUtils.isEmpty(authentication.getPassword())) { + // it is valid if it has the username but not the password + authentication.setPassword(""); + } + + if (StringUtils.isEmpty(authentication.getDatabaseName())) { + invalids.add("Missing database name."); + } + } + + /* + * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. + */ + if (datasourceConfiguration.getConnection() == null + || datasourceConfiguration.getConnection().getSsl() == null + || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { + invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration form. " + + "Please reach out to Appsmith customer support to resolve this."); + } + + return invalids; + } + + public static ConnectionPool getNewConnectionPool(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException { + ConnectionFactoryOptions.Builder ob = getBuilder(datasourceConfiguration); + ob = addSslOptionsToBuilder(datasourceConfiguration, ob); + MariadbConnectionFactory connectionFactory = + MariadbConnectionFactory.from( + MariadbConnectionConfiguration.fromOptions(ob.build()) + .allowPublicKeyRetrieval(true).build() + ); + + /** + * The pool configuration object does not seem to have any option to set the minimum pool size, hence could + * not configure the minimum pool size. + */ + ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration.builder(connectionFactory) + .maxIdleTime(MAX_IDLE_TIME) + .maxSize(MAX_CONNECTION_POOL_SIZE) + .build(); + return new ConnectionPool(configuration); + } +} diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlGetStructureUtils.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlGetStructureUtils.java new file mode 100644 index 0000000000..7bd5c6f403 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlGetStructureUtils.java @@ -0,0 +1,162 @@ +package com.external.utils; + +import com.appsmith.external.models.DatasourceStructure; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MySqlGetStructureUtils { + + private static final String DATE_COLUMN_TYPE_NAME = "date"; + private static final String DATETIME_COLUMN_TYPE_NAME = "datetime"; + private static final String TIMESTAMP_COLUMN_TYPE_NAME = "timestamp"; + + /** + * 1. Parse results obtained by running COLUMNS_QUERY defined on top of the page. + * 2. A sample mysql output for the query is also given near COLUMNS_QUERY definition on top of the page. + */ + public static void getTableInfo(Row row, RowMetadata meta, Map tablesByName) { + final String tableName = row.get("table_name", String.class); + + if (!tablesByName.containsKey(tableName)) { + tablesByName.put(tableName, new DatasourceStructure.Table( + DatasourceStructure.TableType.TABLE, + null, + tableName, + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>() + )); + } + + final DatasourceStructure.Table table = tablesByName.get(tableName); + table.getColumns().add(new DatasourceStructure.Column( + row.get("column_name", String.class), + row.get("column_type", String.class), + null, + row.get("extra", String.class).contains("auto_increment") + )); + + return; + } + + /** + * 1. Parse results obtained by running KEYS_QUERY defined on top of the page. + * 2. A sample mysql output for the query is also given near KEYS_QUERY definition on top of the page. + */ + public static void getKeyInfo(Row row, RowMetadata meta, Map tablesByName, + Map keyRegistry) { + final String constraintName = row.get("constraint_name", String.class); + final char constraintType = row.get("constraint_type", String.class).charAt(0); + final String selfSchema = row.get("self_schema", String.class); + final String tableName = row.get("self_table", String.class); + + + if (!tablesByName.containsKey(tableName)) { + /* do nothing */ + return; + } + + final DatasourceStructure.Table table = tablesByName.get(tableName); + final String keyFullName = tableName + "." + row.get("constraint_name", String.class); + + if (constraintType == 'p') { + if (!keyRegistry.containsKey(keyFullName)) { + final DatasourceStructure.PrimaryKey key = new DatasourceStructure.PrimaryKey( + constraintName, + new ArrayList<>() + ); + keyRegistry.put(keyFullName, key); + table.getKeys().add(key); + } + ((DatasourceStructure.PrimaryKey) keyRegistry.get(keyFullName)).getColumnNames() + .add(row.get("self_column", String.class)); + } else if (constraintType == 'f') { + final String foreignSchema = row.get("foreign_schema", String.class); + final String prefix = (foreignSchema.equalsIgnoreCase(selfSchema) ? "" : foreignSchema + ".") + + row.get("foreign_table", String.class) + "."; + + if (!keyRegistry.containsKey(keyFullName)) { + final DatasourceStructure.ForeignKey key = new DatasourceStructure.ForeignKey( + constraintName, + new ArrayList<>(), + new ArrayList<>() + ); + keyRegistry.put(keyFullName, key); + table.getKeys().add(key); + } + + ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getFromColumns() + .add(row.get("self_column", String.class)); + ((DatasourceStructure.ForeignKey) keyRegistry.get(keyFullName)).getToColumns() + .add(prefix + row.get("foreign_column", String.class)); + } + + return; + } + + /** + * 1. Generate template for all tables in the database. + */ + public static void getTemplates(Map tablesByName) { + for (DatasourceStructure.Table table : tablesByName.values()) { + final List columnsWithoutDefault = table.getColumns() + .stream() + .filter(column -> column.getDefaultValue() == null) + .collect(Collectors.toList()); + + final List columnNames = new ArrayList<>(); + final List columnValues = new ArrayList<>(); + final StringBuilder setFragments = new StringBuilder(); + + for (DatasourceStructure.Column column : columnsWithoutDefault) { + final String name = column.getName(); + final String type = column.getType(); + String value; + + if (type == null) { + value = "null"; + } else if ("text".equals(type) || "varchar".equals(type)) { + value = "''"; + } else if (type.startsWith("int")) { + value = "1"; + } else if (type.startsWith("double")) { + value = "1.0"; + } else if (DATE_COLUMN_TYPE_NAME.equals(type)) { + value = "'2019-07-01'"; + } else if (DATETIME_COLUMN_TYPE_NAME.equals(type) + || TIMESTAMP_COLUMN_TYPE_NAME.equals(type)) { + value = "'2019-07-01 10:00:00'"; + } else { + value = "''"; + } + + columnNames.add(name); + columnValues.add(value); + setFragments.append("\n ").append(name).append(" = ").append(value).append(","); + } + + // Delete the last comma + if (setFragments.length() > 0) { + setFragments.deleteCharAt(setFragments.length() - 1); + } + + final String tableName = table.getName(); + table.getTemplates().addAll(List.of( + new DatasourceStructure.Template("SELECT", "SELECT * FROM " + tableName + " LIMIT 10;"), + new DatasourceStructure.Template("INSERT", "INSERT INTO " + tableName + + " (" + String.join(", ", columnNames) + ")\n" + + " VALUES (" + String.join(", ", columnValues) + ");"), + new DatasourceStructure.Template("UPDATE", "UPDATE " + tableName + " SET" + + setFragments + "\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 " + tableName + + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") + )); + } + } +} diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json index b99c0a5ddc..f666e9a371 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json @@ -87,10 +87,6 @@ "label": "Default", "value": "DEFAULT" }, - { - "label": "Preferred", - "value": "PREFERRED" - }, { "label": "Required", "value": "REQUIRED" 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 3b93d075b2..56ca2b702d 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 @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeType; +import io.r2dbc.pool.ConnectionPool; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -28,7 +29,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mariadb.r2dbc.MariadbConnectionConfiguration; import org.mariadb.r2dbc.MariadbConnectionFactory; -import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; +import org.reactivestreams.Publisher; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.containers.MySQLR2DBCDatabaseContainer; import org.testcontainers.junit.jupiter.Container; @@ -63,1427 +64,1396 @@ import static org.mockito.Mockito.spy; @Testcontainers public class MySqlPluginTest { -// 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 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; -// -// @BeforeAll -// public static void setUp() { -// address = mySQLContainer.getContainerIpAddress(); -// port = mySQLContainer.getFirstMappedPort(); -// username = mySQLContainer.getUsername(); -// password = mySQLContainer.getPassword(); -// database = mySQLContainer.getDatabaseName(); -// dsConfig = createDatasourceConfiguration(); -// -// ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); -// ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions) -// .option(ConnectionFactoryOptions.DRIVER, MariadbConnectionFactoryProvider.MARIADB_DRIVER) -// .option(ConnectionFactoryOptions.SSL, false) -// .option(MariadbConnectionFactoryProvider.ALLOW_MULTI_QUERIES, true); -// -// MariadbConnectionConfiguration.Builder connectionConfigurationBuilder = MariadbConnectionConfiguration.fromOptions(ob.build()); -// connectionConfigurationBuilder.allowPublicKeyRetrieval(true); -// -// MariadbConnectionFactory connectionFactory = MariadbConnectionFactory.from(connectionConfigurationBuilder.build()); -// Mono.from(connectionFactory.create()) -// .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; -// } -// -// 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()); -// -// DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); -// -// /* 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); -// -// return datasourceConfiguration; -// } -// -// @Test -// public void testConnectMySQLContainer() { -// -// Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); -// -// StepVerifier.create(dsConnectionMono) -// .assertNext(Assertions::assertNotNull) -// .verifyComplete(); -// } -// -// @Test -// public void testConnectMySQLContainerWithInvalidTimezone() { -// -// final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); -// dsConfig.setProperties(List.of( -// new Property("serverTimezone", "UTC"))); -// -// Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); -// -// StepVerifier.create(dsConnectionMono) -// .assertNext(Assertions::assertNotNull) -// .verifyComplete(); -// } -// -// @Test -// public void testTestDatasource() { -// dsConfig = createDatasourceConfiguration(); -// -// /* 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(); -// -// /* 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()); -// -// final Endpoint endpoint = new Endpoint(); -// endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); -// endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); -// -// final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); -// -// /* set endpoint */ -// dsConfig.setAuthentication(authDTO); -// dsConfig.setEndpoints(List.of(endpoint)); -// -// /* 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); -// -// return dsConfig; -// } -// -// @Test -// public void testDatasourceWithNullPassword() { -// final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); -// -// // adding a user with empty password -// ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer -// .getOptions(mySQLContainerWithInvalidTimezone); -// ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); -// -// Mono.from(ConnectionFactories.get(ob.build()).create()) -// .map(connection -> connection.createBatch() -// // adding a new user called 'mysql' with empty password -// .add("CREATE USER 'mysql'@'%';\n" + -// "GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'%' WITH GRANT OPTION;\n" -// + -// "FLUSH PRIVILEGES;")) -// .flatMapMany(batch -> Flux.from(batch.execute())) -// .blockLast(); // wait until completion of all the queries -// -// // change to ordinary user -// DBAuth auth = ((DBAuth) dsConfig.getAuthentication()); -// auth.setPassword(""); -// auth.setUsername("mysql"); -// -// // 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); -// -// StepVerifier.create(dsConnectionMono) -// .assertNext(Assertions::assertNotNull) -// .verifyComplete(); -// -// /* Expect no error */ -// StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) -// .assertNext(datasourceTestResult -> assertEquals(0, -// datasourceTestResult.getInvalids().size())) -// .verifyComplete(); -// } -// -// @Test -// public void testDatasourceWithRootUserAndNullPassword() { -// -// final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); -// -// // 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); -// -// StepVerifier.create(dsConnectionMono) -// .assertNext(Assertions::assertNotNull) -// .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); -// -// 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(); -// } -// -// @Test -// public void testExecuteWithFormattingWithShowCmd() { -// dsConfig = createDatasourceConfiguration(); -// Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); -// -// 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(); -// } -// -// @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"); -// -// 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(); -// } -// -// @Test -// public void testStaleConnectionCheck() { -// ActionConfiguration actionConfiguration = new ActionConfiguration(); -// actionConfiguration.setBody("show databases"); -// Connection connection = pluginExecutor.datasourceCreate(dsConfig).block(); -// -// Flux resultFlux = Mono.from(connection.close()) -// .thenMany(pluginExecutor.executeParameterized(connection, new ExecuteActionDTO(), -// dsConfig, actionConfiguration)); -// -// 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 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 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)); -// } -// -// @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); -// -// 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)); -// -// 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; -// } -// -// @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 = "Appsmith currently does not support the IS keyword with the prepared " -// + -// "statement setting turned ON. Please re-write your SQL query without the IS keyword or " -// + -// "turn OFF (unsafe) the 'Use prepared statement' knob from the settings tab."; -// assertTrue(expectedMessage.equals(error.getMessage())); -// }); -// } -// -// @Test -// public void testPreparedStatementWithRealTypes() { -// ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); -// ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); -// Mono.from(ConnectionFactories.get(ob.build()).create()) -// .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(ConnectionFactories.get(ob.build()).create()) -// .map(connection -> connection.createBatch() -// .add("drop table test_real_types")) -// .flatMapMany(batch -> Flux.from(batch.execute())) -// .blockLast(); // wait until completion of all the queries -// } -// -// @Test -// public void testPreparedStatementWithBooleanType() { -// // Create a new table with boolean type -// ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); -// ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); -// Mono.from(ConnectionFactories.get(ob.build()).create()) -// .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(ConnectionFactories.get(ob.build()).create()) -// .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;"; -// -// ConnectionFactoryOptions baseOptions = MySQLR2DBCDatabaseContainer.getOptions(mySQLContainer); -// ConnectionFactoryOptions.Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); -// Mono.from(ConnectionFactories.get(ob.build()).create()) -// .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); -// /* Test date time types */ -// testExecute(query_select_from_test_date_time_types); -// /* Test data types */ -// testExecute(query_select_from_test_data_types); -// /* Test data types */ -// testExecute(query_select_from_test_json_data_type); -// /* Test data types */ -// testExecute(query_select_from_test_geometry_types); -// -// return; -// } -// -// private void testExecute(String query) { -// 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()); -// }) -// .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\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", -// body.toString()); -// }) -// .verifyComplete(); -// } -// -// @Test -// public void testSslPreferred() { -// ActionConfiguration actionConfiguration = new ActionConfiguration(); -// actionConfiguration.setBody("show session status like 'Ssl_cipher'"); -// -// DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); -// datasourceConfiguration.getConnection().getSsl().setAuthType(SSLDetails.AuthType.PREFERRED); -// 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\":\"ECDHE-RSA-AES128-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\":\"ECDHE-RSA-AES128-GCM-SHA256\"}]", -// 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(); -// } + 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 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 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(); + + 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; + } + + 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()); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + + /* 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); + + return datasourceConfiguration; + } + + @Test + public void testConnectMySQLContainer() { + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } + + @Test + public void testConnectMySQLContainerWithInvalidTimezone() { + + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + dsConfig.setProperties(List.of( + new Property("serverTimezone", "UTC"))); + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } + + @Test + public void testTestDatasource() { + dsConfig = createDatasourceConfiguration(); + + /* 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(); + + /* 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()); + + final Endpoint endpoint = new Endpoint(); + endpoint.setHost(mySQLContainerWithInvalidTimezone.getContainerIpAddress()); + endpoint.setPort(mySQLContainerWithInvalidTimezone.getFirstMappedPort().longValue()); + + final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + + /* set endpoint */ + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + + /* 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); + + 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 + + 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()); + + // Validate datastore + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.isEmpty()); + // test connect + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + + /* Expect no error */ + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(datasourceTestResult -> assertEquals(0, + datasourceTestResult.getInvalids().size())) + .verifyComplete(); + } + + @Test + public void testDatasourceWithRootUserAndNullPassword() { + + final DatasourceConfiguration dsConfig = createDatasourceConfigForContainerWithInvalidTZ(); + + // 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); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assertions::assertNotNull) + .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); + + 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(); + } + + @Test + public void testExecuteWithFormattingWithShowCmd() { + dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + 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(); + } + + @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"); + + 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(); + } + + @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(); + } + + @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 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"))); + + 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 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"); + + 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(); + + 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 = "Appsmith currently does not support the IS keyword with the prepared " + + + "statement setting turned ON. Please re-write your SQL query without the IS keyword or " + + + "turn OFF (unsafe) the 'Use prepared statement' knob from the settings tab."; + 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;"; + + 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); + /* Test date time types */ + testExecute(query_select_from_test_date_time_types); + /* Test data types */ + testExecute(query_select_from_test_data_types); + /* Test data types */ + testExecute(query_select_from_test_json_data_type); + /* Test data types */ + testExecute(query_select_from_test_geometry_types); + + return; + } + + private void testExecute(String query) { + 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()); + }) + .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(); + } } \ No newline at end of file diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml index 1c367e77aa..70ac59b0af 100644 --- a/app/server/appsmith-server/pom.xml +++ b/app/server/appsmith-server/pom.xml @@ -41,12 +41,13 @@ + + Ideally this dependency should have been added in the pom.xml file of GraphQLPlugin module, but it is + causing 'java.lang.NoClassDefFoundError' error. Hence, adding it here after many attempts of fixing it the right + way. Somehow adding it here makes it work. GraphQLPlugin module's pom.xml file also has this dependency + defined with 'provided' scope + --> com.graphql-java graphql-java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog2.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog2.java index 43b797cbfa..8372099ea2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog2.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog2.java @@ -66,6 +66,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.querydsl.core.types.Path; import io.changock.migration.api.annotations.NonLockGuarded; +import io.mongock.api.annotations.ChangeUnit; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; import org.bson.types.ObjectId; @@ -2774,154 +2775,20 @@ public class DatabaseChangelog2 { ensureIndexes(mongoTemplate, CustomJSLib.class, uidStringUniqueness); } - // @ChangeSet(order = "039", id = "deprecate-queryabletext-encryption", author = "") - // public void deprecateQueryableTextEncryption(MongockTemplate mongockTemplate, - // @NonLockGuarded EncryptionConfig encryptionConfig, - // EncryptionService encryptionService) { - // Stopwatch stopwatch = new Stopwatch("Instance Schema migration to v2"); + /** + * Since MySQL plugin's underlying driver has been changed to MariaDB driver, the `ssl-mode=preferred` is no + * longer supported. Hence, any such usage is being updated to `ssl-mode=default` by this method. + */ + @ChangeSet(order = "039", id = "remove-preferred-ssl-mode-from-mysql", author = "") + public void changeSSLModeFromPreferredToDefaultForMySQLPlugin(MongoTemplate mongoTemplate) { + Plugin mySQLPlugin = mongoTemplate.findOne(query(where("packageName").is("mysql-plugin")), + Plugin.class); + Query queryToGetDatasources = getQueryToFetchAllDomainObjectsWhichAreNotDeletedUsingPluginId(mySQLPlugin); + queryToGetDatasources.addCriteria(Criteria.where("datasourceConfiguration.connection.ssl.authType").is( + "PREFERRED")); - // Config encryptionVersion = mongockTemplate.findOne( - // query(where(fieldName(QConfig.config1.name)).is(Appsmith.INSTANCE_SCHEMA_VERSION)), - // Config.class); - - // if (encryptionVersion != null && (Integer) encryptionVersion.getConfig().get("value") < 2) { - // String saltInHex = Hex.encodeHexString(encryptionConfig.getSalt().getBytes()); - // TextEncryptor textEncryptor = Encryptors.queryableText(encryptionConfig.getPassword(), saltInHex); - - // /** - // * - List of attributes in datasources that need to be encoded. - // * - Each path represents where the attribute exists in mongo db document. - // */ - // List datasourcePathList = new ArrayList<>(); - // datasourcePathList.add("datasourceConfiguration.connection.ssl.keyFile.base64Content"); - // datasourcePathList.add("datasourceConfiguration.connection.ssl.certificateFile.base64Content"); - // datasourcePathList.add("datasourceConfiguration.connection.ssl.caCertificateFile.base64Content"); - // datasourcePathList.add("datasourceConfiguration.connection.ssl.pemCertificate.file.base64Content"); - // datasourcePathList.add("datasourceConfiguration.connection.ssl.pemCertificate.password"); - // datasourcePathList.add("datasourceConfiguration.sshProxy.privateKey.keyFile.base64Content"); - // datasourcePathList.add("datasourceConfiguration.sshProxy.privateKey.password"); - // datasourcePathList.add("datasourceConfiguration.authentication.value"); - // datasourcePathList.add("datasourceConfiguration.authentication.password"); - // datasourcePathList.add("datasourceConfiguration.authentication.bearerToken"); - // datasourcePathList.add("datasourceConfiguration.authentication.clientSecret"); - // datasourcePathList.add("datasourceConfiguration.authentication.authenticationResponse.token"); - // datasourcePathList.add("datasourceConfiguration.authentication.authenticationResponse.refreshToken"); - // datasourcePathList.add("datasourceConfiguration.authentication.authenticationResponse.tokenResponse"); - // List datasourcePathListExists = datasourcePathList - // .stream() - // .map(Filters::exists) - // .collect(Collectors.toList()); - - // List gitDeployKeysPathListExists = new ArrayList<>(); - // ArrayList gitDeployKeysPathList = new ArrayList<>(); - // gitDeployKeysPathList.add("gitAuth.privateKey"); - // gitDeployKeysPathListExists.add(Filters.exists("gitAuth.privateKey")); - - // List applicationPathListExists = new ArrayList<>(); - // ArrayList applicationPathList = new ArrayList<>(); - // applicationPathList.add("gitApplicationMetadata.gitAuth.privateKey"); - // applicationPathListExists.add(Filters.exists("gitApplicationMetadata.gitAuth.privateKey")); - - // mongockTemplate.execute("datasource", getNewEncryptionCallback(textEncryptor, encryptionService, datasourcePathListExists, datasourcePathList, stopwatch)); - // mongockTemplate.execute("gitDeployKeys", getNewEncryptionCallback(textEncryptor, encryptionService, gitDeployKeysPathListExists, gitDeployKeysPathList, stopwatch)); - // mongockTemplate.execute("application", getNewEncryptionCallback(textEncryptor, encryptionService, applicationPathListExists, applicationPathList, stopwatch)); - - // mongockTemplate.upsert( - // query(where(fieldName(QConfig.config1.name)).is(Appsmith.INSTANCE_SCHEMA_VERSION)), - // update("config.value", 2), - // Config.class); - // } - // stopwatch.stopAndLogTimeInMillis(); - // } - - // private CollectionCallback getNewEncryptionCallback( - // TextEncryptor textEncryptor, - // EncryptionService encryptionService, - // Iterable collectionFilterIterable, - // List pathList, - // Stopwatch stopwatch) { - // return new CollectionCallback() { - // @Override - // public String doInCollection(MongoCollection collection) { - // MongoCursor cursor = collection - // .find( - // Filters.and( - // Filters.or(collectionFilterIterable), - // Filters.not(Filters.exists("encryptionVersion")))) - // .cursor(); - - // log.debug("collection callback start: {}ms", stopwatch.getExecutionTime()); - - // List> documentPairList = new ArrayList<>(); - // while (cursor.hasNext()) { - // Document old = cursor.next(); - // BasicDBObject query = new BasicDBObject(); - // query.put("_id", old.getObjectId("_id")); - // // This document will have the encrypted values. - // BasicDBObject updated = new BasicDBObject(); - // updated.put("$set", new BasicDBObject("encryptionVersion", 2)); - // updated.put("$unset", new BasicDBObject()); - // // Encrypt attributes - // pathList.stream() - // .forEach(path -> reapplyNewEncryptionToPathValueIfExists(old, updated, path, encryptionService, textEncryptor)); - // // Since empty unset values are only allowed since Mongo v5+, - // // Remove the operation if there is nothing to unset - // if (((BasicDBObject) updated.get("$unset")).isEmpty()) { - // updated.remove("$unset"); - // } - // documentPairList.add(List.of(query, updated)); - // } - - // log.debug("collection callback processing end: {}ms", stopwatch.getExecutionTime()); - // log.debug("update will be run for {} documents", documentPairList.size()); - - // /** - // * - Replace old document with the updated document that has encrypted values. - // * - Replacing here instead of the while loop above makes sure that we attempt replacement only if - // * the encryption step succeeded without error for each selected document. - // */ - // documentPairList.stream().parallel() - // .forEach(docPair -> collection.updateOne(docPair.get(0), docPair.get(1))); - - // log.debug("collection callback update end: {}ms", stopwatch.getExecutionTime()); - - // return null; - // } - // }; - // } - - // private void reapplyNewEncryptionToPathValueIfExists(Document document, BasicDBObject update, String path, - // EncryptionService encryptionService, - // TextEncryptor textEncryptor) { - // String[] pathKeys = path.split("\\."); - // /** - // * - For attribute path "datasourceConfiguration.connection.ssl.keyFile.base64Content", first get the parent - // * document that contains the attribute 'base64Content' i.e. fetch the document corresponding to path - // * "datasourceConfiguration.connection.ssl.keyFile" - // */ - // String parentDocumentPath = org.apache.commons.lang.StringUtils.join(ArrayUtils.subarray(pathKeys, 0, pathKeys.length - 1), "."); - // Document parentDocument = DatabaseChangelog1.getDocumentFromPath(document, parentDocumentPath); - - // if (parentDocument != null) { - // if (parentDocument.containsKey(pathKeys[pathKeys.length - 1])) { - // String oldEncryptedValue = parentDocument.getString(pathKeys[pathKeys.length - 1]); - // if (StringUtils.hasLength(String.valueOf(oldEncryptedValue))) { - // String decryptedValue = null; - // try { - // decryptedValue = textEncryptor.decrypt(String.valueOf(oldEncryptedValue)); - // } catch (IllegalArgumentException e) { - // // This happens on release DB for some creds that are malformed - // if ("Hex-encoded string must have an even number of characters".equals(e.getMessage())) { - // decryptedValue = String.valueOf(oldEncryptedValue); - // } - // } - // String newEncryptedValue = encryptionService.encryptString(decryptedValue); - // ((BasicDBObject) update.get("$set")).put(path, newEncryptedValue); - // if (path.startsWith("datasourceConfiguration.authentication")) { - // ((BasicDBObject) update.get("$unset")).put("datasourceConfiguration.authentication.isEncrypted", 1); - // } - // } - // } - // } - // } + Update update = new Update(); + update.set("datasourceConfiguration.connection.ssl.authType", "DEFAULT"); + mongoTemplate.updateMulti(queryToGetDatasources, update, Datasource.class); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseAppsmithRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseAppsmithRepositoryImpl.java index b542201af8..c3f7a3a5f2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseAppsmithRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseAppsmithRepositoryImpl.java @@ -15,4 +15,4 @@ public abstract class BaseAppsmithRepositoryImpl extends B super(mongoOperations, mongoConverter, cacheableRepositoryHelper); } -} +} \ No newline at end of file