fix: fix query failure on simultaneous execution of multiple queries (#15458)
* update driver remove connection closure when not required * add connection pool remove ssl options * got working with postgres driver * use Redshift driver instead of postgres * updated JUnit TC added comments minor refactor * add comment cleanup * update default port
This commit is contained in:
parent
bae0b75583
commit
02f1451443
|
|
@ -1344,6 +1344,7 @@ public class PostgresPluginTest {
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testReadOnlyMode() {
|
public void testReadOnlyMode() {
|
||||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||||
dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_ONLY);
|
dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_ONLY);
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,21 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.amazon.redshift</groupId>
|
<groupId>com.amazon.redshift</groupId>
|
||||||
<artifactId>redshift-jdbc42</artifactId>
|
<artifactId>redshift-jdbc42</artifactId>
|
||||||
<version>2.1.0.1</version>
|
<version>2.1.0.9</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>3.4.5</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<!-- ******************* Test Dependencies ******************* -->
|
<!-- ******************* Test Dependencies ******************* -->
|
||||||
|
|
||||||
|
|
@ -51,7 +63,6 @@
|
||||||
<version>1.3</version>
|
<version>1.3</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ import com.appsmith.external.models.DatasourceStructure;
|
||||||
import com.appsmith.external.models.DatasourceTestResult;
|
import com.appsmith.external.models.DatasourceTestResult;
|
||||||
import com.appsmith.external.models.Endpoint;
|
import com.appsmith.external.models.Endpoint;
|
||||||
import com.appsmith.external.models.RequestParamDTO;
|
import com.appsmith.external.models.RequestParamDTO;
|
||||||
import com.appsmith.external.models.SSLDetails;
|
|
||||||
import com.appsmith.external.plugins.BasePlugin;
|
import com.appsmith.external.plugins.BasePlugin;
|
||||||
import com.appsmith.external.plugins.PluginExecutor;
|
import com.appsmith.external.plugins.PluginExecutor;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import com.zaxxer.hikari.HikariPoolMXBean;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang.ObjectUtils;
|
import org.apache.commons.lang.ObjectUtils;
|
||||||
|
|
@ -27,7 +28,6 @@ import reactor.core.scheduler.Scheduler;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.ResultSetMetaData;
|
import java.sql.ResultSetMetaData;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
@ -42,23 +42,18 @@ import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
|
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
|
||||||
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
|
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
|
||||||
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
|
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
|
||||||
import static com.appsmith.external.models.Connection.Mode.READ_ONLY;
|
import static com.external.utils.RedshiftDatasourceUtils.createConnectionPool;
|
||||||
|
import static com.external.utils.RedshiftDatasourceUtils.getConnectionFromConnectionPool;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RedshiftPlugin extends BasePlugin {
|
public class RedshiftPlugin extends BasePlugin {
|
||||||
static final String JDBC_DRIVER = "com.amazon.redshift.jdbc.Driver";
|
public static final String JDBC_DRIVER = "com.amazon.redshift.jdbc.Driver";
|
||||||
private static final String JDBC_PROTOCOL = "jdbc:redshift://";
|
|
||||||
private static final String USER = "user";
|
|
||||||
private static final String PASSWORD = "password";
|
|
||||||
private static final String SSL = "ssl";
|
|
||||||
private static final int VALIDITY_CHECK_TIMEOUT = 5; /* must be positive, otherwise may receive exception */
|
|
||||||
private static final String DATE_COLUMN_TYPE_NAME = "date";
|
private static final String DATE_COLUMN_TYPE_NAME = "date";
|
||||||
|
|
||||||
public RedshiftPlugin(PluginWrapper wrapper) {
|
public RedshiftPlugin(PluginWrapper wrapper) {
|
||||||
|
|
@ -66,7 +61,7 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Extension
|
@Extension
|
||||||
public static class RedshiftPluginExecutor implements PluginExecutor<Connection> {
|
public static class RedshiftPluginExecutor implements PluginExecutor<HikariDataSource> {
|
||||||
|
|
||||||
private final Scheduler scheduler = Schedulers.elastic();
|
private final Scheduler scheduler = Schedulers.elastic();
|
||||||
|
|
||||||
|
|
@ -197,19 +192,8 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. This method can throw SQLException via connection.isClosed() or connection.isValid(...)
|
|
||||||
* 2. StaleConnectionException thrown by this method needs to be propagated to upper layers so that a retry
|
|
||||||
* can be triggered.
|
|
||||||
*/
|
|
||||||
private void checkConnectionValidity(Connection connection) throws SQLException {
|
|
||||||
if (connection == null || connection.isClosed()) {
|
|
||||||
throw new StaleConnectionException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ActionExecutionResult> execute(Connection connection,
|
public Mono<ActionExecutionResult> execute(HikariDataSource connectionPool,
|
||||||
DatasourceConfiguration datasourceConfiguration,
|
DatasourceConfiguration datasourceConfiguration,
|
||||||
ActionConfiguration actionConfiguration) {
|
ActionConfiguration actionConfiguration) {
|
||||||
|
|
||||||
|
|
@ -227,104 +211,139 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono.fromCallable(() -> {
|
return Mono.fromCallable(() -> {
|
||||||
/*
|
Connection connection = null;
|
||||||
* 1. If there is any issue with checking connection validity then assume that the connection is stale.
|
try {
|
||||||
*/
|
connection = getConnectionFromConnectionPool(connectionPool);
|
||||||
|
} catch (SQLException | StaleConnectionException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user configured time limit for the query execution is over, and the query is still
|
||||||
|
* queued in the connectionPool then InterruptedException is thrown as the execution thread is
|
||||||
|
* prepared for termination. This exception is wrapped inside SQLException and hence needs to be
|
||||||
|
* checked via getCause method. This exception does not indicate a Stale connection.
|
||||||
|
*/
|
||||||
|
if (e.getCause() != null && e.getCause().getClass().equals(InterruptedException.class)) {
|
||||||
|
return Mono.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
|
||||||
|
// library throws SQLException in case the pool is closed or there is an issue initializing
|
||||||
|
// the connection pool which can also be translated in our world to StaleConnectionException
|
||||||
|
// and should then trigger the destruction and recreation of the pool.
|
||||||
|
return Mono.error(new StaleConnectionException());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeping this print statement post call to getConnectionFromConnectionPool because it checks for
|
||||||
|
* stale connection pool.
|
||||||
|
*/
|
||||||
|
printConnectionPoolStatus(connectionPool, false);
|
||||||
|
|
||||||
|
List<Map<String, Object>> rowsList = new ArrayList<>(50);
|
||||||
|
final List<String> columnsList = new ArrayList<>();
|
||||||
|
Statement statement = null;
|
||||||
|
ResultSet resultSet = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
statement = connection.createStatement();
|
||||||
|
boolean isResultSet = statement.execute(query);
|
||||||
|
|
||||||
|
if (isResultSet) {
|
||||||
|
resultSet = statement.getResultSet();
|
||||||
|
ResultSetMetaData metaData = resultSet.getMetaData();
|
||||||
|
columnsList.addAll(getColumnsListForJdbcPlugin(metaData));
|
||||||
|
|
||||||
|
while (resultSet.next()) {
|
||||||
|
Map<String, Object> row = getRow(resultSet);
|
||||||
|
rowsList.add(row);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowsList.add(Map.of(
|
||||||
|
"affectedRows",
|
||||||
|
ObjectUtils.defaultIfNull(statement.getUpdateCount(), 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
|
||||||
|
} finally {
|
||||||
|
if (resultSet != null) {
|
||||||
try {
|
try {
|
||||||
checkConnectionValidity(connection);
|
resultSet.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
return Mono.error(new StaleConnectionException());
|
log.warn("Error closing Redshift ResultSet", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<Map<String, Object>> rowsList = new ArrayList<>(50);
|
if (statement != null) {
|
||||||
final List<String> columnsList = new ArrayList<>();
|
|
||||||
Statement statement = null;
|
|
||||||
ResultSet resultSet = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
statement = connection.createStatement();
|
statement.close();
|
||||||
boolean isResultSet = statement.execute(query);
|
|
||||||
|
|
||||||
if (isResultSet) {
|
|
||||||
resultSet = statement.getResultSet();
|
|
||||||
ResultSetMetaData metaData = resultSet.getMetaData();
|
|
||||||
columnsList.addAll(getColumnsListForJdbcPlugin(metaData));
|
|
||||||
|
|
||||||
while (resultSet.next()) {
|
|
||||||
Map<String, Object> row = getRow(resultSet);
|
|
||||||
rowsList.add(row);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rowsList.add(Map.of(
|
|
||||||
"affectedRows",
|
|
||||||
ObjectUtils.defaultIfNull(statement.getUpdateCount(), 0))
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
log.warn("Error closing Redshift Statement", e);
|
||||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
|
|
||||||
} finally {
|
|
||||||
if (resultSet != null) {
|
|
||||||
try {
|
|
||||||
resultSet.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.warn("Error closing Redshift ResultSet", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statement != null) {
|
|
||||||
try {
|
|
||||||
statement.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.warn("Error closing Redshift Statement", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
connection.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.warn("Error closing Redshift Connection", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ActionExecutionResult result = new ActionExecutionResult();
|
try {
|
||||||
result.setBody(objectMapper.valueToTree(rowsList));
|
connection.close();
|
||||||
result.setMessages(populateHintMessages(columnsList));
|
} catch (SQLException e) {
|
||||||
result.setIsExecutionSuccess(true);
|
log.warn("Error closing Redshift Connection", e);
|
||||||
log.debug("In RedshiftPlugin, got action execution result");
|
}
|
||||||
return Mono.just(result);
|
|
||||||
})
|
|
||||||
.flatMap(obj -> obj)
|
|
||||||
.map(obj -> (ActionExecutionResult) obj)
|
|
||||||
.onErrorMap(e -> {
|
|
||||||
if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
|
|
||||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return e;
|
}
|
||||||
})
|
|
||||||
.onErrorResume(error -> {
|
ActionExecutionResult result = new ActionExecutionResult();
|
||||||
error.printStackTrace();
|
result.setBody(objectMapper.valueToTree(rowsList));
|
||||||
if (error instanceof StaleConnectionException) {
|
result.setMessages(populateHintMessages(columnsList));
|
||||||
return Mono.error(error);
|
result.setIsExecutionSuccess(true);
|
||||||
}
|
log.debug("In RedshiftPlugin, got action execution result");
|
||||||
ActionExecutionResult result = new ActionExecutionResult();
|
return Mono.just(result);
|
||||||
result.setIsExecutionSuccess(false);
|
})
|
||||||
result.setErrorInfo(error);
|
.flatMap(obj -> obj)
|
||||||
return Mono.just(result);
|
.map(obj -> (ActionExecutionResult) obj)
|
||||||
})
|
.onErrorMap(e -> {
|
||||||
// Now set the request in the result to be returned back to the server
|
if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
|
||||||
.map(actionExecutionResult -> {
|
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
||||||
ActionExecutionRequest request = new ActionExecutionRequest();
|
}
|
||||||
request.setQuery(query);
|
|
||||||
request.setRequestParams(requestParams);
|
return e;
|
||||||
ActionExecutionResult result = actionExecutionResult;
|
})
|
||||||
result.setRequest(request);
|
.onErrorResume(error -> {
|
||||||
return result;
|
error.printStackTrace();
|
||||||
})
|
if (error instanceof StaleConnectionException) {
|
||||||
.subscribeOn(scheduler);
|
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(query);
|
||||||
|
request.setRequestParams(requestParams);
|
||||||
|
ActionExecutionResult result = actionExecutionResult;
|
||||||
|
result.setRequest(request);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.subscribeOn(scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printConnectionPoolStatus(HikariDataSource connectionPool, boolean isFetchingStructure) {
|
||||||
|
HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean();
|
||||||
|
int idleConnections = poolProxy.getIdleConnections();
|
||||||
|
int activeConnections = poolProxy.getActiveConnections();
|
||||||
|
int totalConnections = poolProxy.getTotalConnections();
|
||||||
|
int threadsAwaitingConnection = poolProxy.getThreadsAwaitingConnection();
|
||||||
|
log.debug(Thread.currentThread().getName() + (isFetchingStructure ? "Before fetching Redshift db" +
|
||||||
|
" structure." : "Before executing Redshift query.") + " Hikari Pool stats : " +
|
||||||
|
" active - " + activeConnections +
|
||||||
|
", idle - " + idleConnections +
|
||||||
|
", awaiting - " + threadsAwaitingConnection +
|
||||||
|
", total - " + totalConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> populateHintMessages(List<String> columnNames) {
|
private Set<String> populateHintMessages(List<String> columnNames) {
|
||||||
|
|
@ -342,80 +361,26 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Connection> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
|
public Mono<HikariDataSource> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
|
||||||
try {
|
try {
|
||||||
Class.forName(JDBC_DRIVER);
|
Class.forName(JDBC_DRIVER);
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading Redshift JDBC Driver class."));
|
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading " +
|
||||||
|
"Redshift JDBC Driver class."));
|
||||||
}
|
}
|
||||||
|
|
||||||
String url;
|
return Mono
|
||||||
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
|
.fromCallable(() -> {
|
||||||
|
log.debug(Thread.currentThread().getName() + ": Connecting to Redshift db");
|
||||||
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
|
return createConnectionPool(datasourceConfiguration);
|
||||||
|
|
||||||
final boolean isSslEnabled = configurationConnection != null
|
|
||||||
&& configurationConnection.getSsl() != null
|
|
||||||
&& !SSLDetails.AuthType.NO_SSL.equals(configurationConnection.getSsl().getAuthType());
|
|
||||||
|
|
||||||
Properties properties = new Properties();
|
|
||||||
properties.put(SSL, isSslEnabled);
|
|
||||||
if (authentication.getUsername() != null) {
|
|
||||||
properties.put(USER, authentication.getUsername());
|
|
||||||
}
|
|
||||||
if (authentication.getPassword() != null) {
|
|
||||||
properties.put(PASSWORD, authentication.getPassword());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) {
|
|
||||||
url = datasourceConfiguration.getUrl();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
StringBuilder urlBuilder = new StringBuilder(JDBC_PROTOCOL);
|
|
||||||
for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) {
|
|
||||||
urlBuilder
|
|
||||||
.append(endpoint.getHost())
|
|
||||||
.append(':')
|
|
||||||
.append(ObjectUtils.defaultIfNull(endpoint.getPort(), 5439L))
|
|
||||||
.append('/');
|
|
||||||
|
|
||||||
if (!StringUtils.isEmpty(authentication.getDatabaseName())) {
|
|
||||||
urlBuilder.append(authentication.getDatabaseName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url = urlBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
try {
|
|
||||||
log.debug("Connecting to Redshift db");
|
|
||||||
Connection connection = DriverManager.getConnection(url, properties);
|
|
||||||
connection.setReadOnly(
|
|
||||||
configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode()));
|
|
||||||
return Mono.just(connection);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return Mono.error(
|
|
||||||
new AppsmithPluginException(
|
|
||||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
|
||||||
e.getMessage()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.flatMap(obj -> obj)
|
|
||||||
.map(conn -> (Connection) conn)
|
|
||||||
.subscribeOn(scheduler);
|
.subscribeOn(scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void datasourceDestroy(Connection connection) {
|
public void datasourceDestroy(HikariDataSource connectionPool) {
|
||||||
try {
|
if (connectionPool != null) {
|
||||||
if (connection != null) {
|
connectionPool.close();
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Error closing Redshift Connection.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,12 +430,8 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
public Mono<DatasourceTestResult> testDatasource(DatasourceConfiguration datasourceConfiguration) {
|
public Mono<DatasourceTestResult> testDatasource(DatasourceConfiguration datasourceConfiguration) {
|
||||||
return datasourceCreate(datasourceConfiguration)
|
return datasourceCreate(datasourceConfiguration)
|
||||||
.map(connection -> {
|
.map(connection -> {
|
||||||
try {
|
if (connection != null) {
|
||||||
if (connection != null) {
|
connection.close();
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.warn("Error closing Redshift connection that was made for testing.", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DatasourceTestResult();
|
return new DatasourceTestResult();
|
||||||
|
|
@ -619,77 +580,99 @@ public class RedshiftPlugin extends BasePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<DatasourceStructure> getStructure(Connection connection, DatasourceConfiguration datasourceConfiguration) {
|
public Mono<DatasourceStructure> getStructure(HikariDataSource connectionPool,
|
||||||
/*
|
DatasourceConfiguration datasourceConfiguration) {
|
||||||
* 1. If there is any issue with checking connection validity then assume that the connection is stale.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
checkConnectionValidity(connection);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return Mono.error(new StaleConnectionException());
|
|
||||||
}
|
|
||||||
|
|
||||||
final DatasourceStructure structure = new DatasourceStructure();
|
final DatasourceStructure structure = new DatasourceStructure();
|
||||||
final Map<String, DatasourceStructure.Table> tablesByName = new LinkedHashMap<>();
|
final Map<String, DatasourceStructure.Table> tablesByName = new LinkedHashMap<>();
|
||||||
final Map<String, DatasourceStructure.Key> keyRegistry = new HashMap<>();
|
final Map<String, DatasourceStructure.Key> keyRegistry = new HashMap<>();
|
||||||
|
|
||||||
return Mono.fromSupplier(() -> {
|
return Mono.fromSupplier(() -> {
|
||||||
// Ref: <https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/DatabaseMetaData.html>.
|
Connection connection = null;
|
||||||
log.debug("Getting Redshift Db structure");
|
try {
|
||||||
try (Statement statement = connection.createStatement()) {
|
connection = getConnectionFromConnectionPool(connectionPool);
|
||||||
|
} catch (SQLException | StaleConnectionException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
// Get tables' schema and fill up their columns.
|
/**
|
||||||
ResultSet columnsResultSet = statement.executeQuery(TABLES_QUERY);
|
* When the user configured time limit for the query execution is over, and the query is still
|
||||||
getTablesInfo(columnsResultSet, tablesByName);
|
* queued in the connectionPool then InterruptedException is thrown as the execution thread is
|
||||||
|
* prepared for termination. This exception is wrapped inside SQLException and hence needs to be
|
||||||
|
* checked via getCause method. This exception does not indicate a Stale connection.
|
||||||
|
*/
|
||||||
|
if (e.getCause() != null && e.getCause().getClass().equals(InterruptedException.class)) {
|
||||||
|
return Mono.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Get tables' primary key constraints and fill those up.
|
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
|
||||||
ResultSet primaryKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_PRIMARY_KEY);
|
// library throws SQLException in case the pool is closed or there is an issue initializing
|
||||||
getKeysInfo(primaryKeyConstraintsResultSet, tablesByName, keyRegistry);
|
// the connection pool which can also be translated in our world to StaleConnectionException
|
||||||
|
// and should then trigger the destruction and recreation of the pool.
|
||||||
|
return Mono.error(new StaleConnectionException());
|
||||||
|
}
|
||||||
|
|
||||||
// Get tables' foreign key constraints and fill those up.
|
/**
|
||||||
ResultSet foreignKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_FOREIGN_KEY);
|
* Keeping this print statement post call to getConnectionFromConnectionPool because it checks for
|
||||||
getKeysInfo(foreignKeyConstraintsResultSet, tablesByName, keyRegistry);
|
* stale connection pool.
|
||||||
|
*/
|
||||||
|
printConnectionPoolStatus(connectionPool, true);
|
||||||
|
|
||||||
// Get templates for each table and put those in.
|
// Ref: <https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/DatabaseMetaData.html>.
|
||||||
getTemplates(tablesByName);
|
log.debug(Thread.currentThread().getName() + ": Getting Redshift Db structure");
|
||||||
} catch (SQLException e) {
|
try (Statement statement = connection.createStatement()) {
|
||||||
e.printStackTrace();
|
|
||||||
return Mono.error(
|
|
||||||
new AppsmithPluginException(
|
|
||||||
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
|
|
||||||
e.getMessage()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (AppsmithPluginException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return Mono.error(e);
|
|
||||||
|
|
||||||
} finally {
|
// Get tables' schema and fill up their columns.
|
||||||
try {
|
ResultSet columnsResultSet = statement.executeQuery(TABLES_QUERY);
|
||||||
connection.close();
|
getTablesInfo(columnsResultSet, tablesByName);
|
||||||
} catch (SQLException e) {
|
|
||||||
log.warn("Error closing Redshift Connection", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// Get tables' primary key constraints and fill those up.
|
||||||
|
ResultSet primaryKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_PRIMARY_KEY);
|
||||||
|
getKeysInfo(primaryKeyConstraintsResultSet, tablesByName, keyRegistry);
|
||||||
|
|
||||||
structure.setTables(new ArrayList<>(tablesByName.values()));
|
// Get tables' foreign key constraints and fill those up.
|
||||||
|
ResultSet foreignKeyConstraintsResultSet = statement.executeQuery(KEYS_QUERY_FOREIGN_KEY);
|
||||||
|
getKeysInfo(foreignKeyConstraintsResultSet, tablesByName, keyRegistry);
|
||||||
|
|
||||||
for (DatasourceStructure.Table table : structure.getTables()) {
|
// Get templates for each table and put those in.
|
||||||
table.getKeys().sort(Comparator.naturalOrder());
|
getTemplates(tablesByName);
|
||||||
}
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Mono.error(
|
||||||
|
new AppsmithPluginException(
|
||||||
|
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
|
||||||
|
e.getMessage()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (AppsmithPluginException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Mono.error(e);
|
||||||
|
|
||||||
return structure;
|
}
|
||||||
})
|
finally {
|
||||||
.map(resultStructure -> (DatasourceStructure) resultStructure)
|
try {
|
||||||
.onErrorMap(e -> {
|
connection.close();
|
||||||
if (!(e instanceof AppsmithPluginException)) {
|
} catch (SQLException e) {
|
||||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
log.warn("Error closing Redshift Connection", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return e;
|
}
|
||||||
})
|
|
||||||
.subscribeOn(scheduler);
|
structure.setTables(new ArrayList<>(tablesByName.values()));
|
||||||
|
|
||||||
|
for (DatasourceStructure.Table table : structure.getTables()) {
|
||||||
|
table.getKeys().sort(Comparator.naturalOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
return structure;
|
||||||
|
})
|
||||||
|
.map(resultStructure -> (DatasourceStructure) resultStructure)
|
||||||
|
.onErrorMap(e -> {
|
||||||
|
if ((e instanceof AppsmithPluginException) || (e instanceof StaleConnectionException)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
||||||
|
})
|
||||||
|
.subscribeOn(scheduler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.external.utils;
|
||||||
|
|
||||||
|
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||||
|
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||||
|
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||||
|
import com.appsmith.external.models.DBAuth;
|
||||||
|
import com.appsmith.external.models.DatasourceConfiguration;
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import com.zaxxer.hikari.pool.HikariPool;
|
||||||
|
import org.apache.commons.lang.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.external.plugins.RedshiftPlugin.JDBC_DRIVER;
|
||||||
|
|
||||||
|
public class RedshiftDatasourceUtils {
|
||||||
|
|
||||||
|
private static final int MINIMUM_POOL_SIZE = 1;
|
||||||
|
private static final int MAXIMUM_POOL_SIZE = 5;
|
||||||
|
private static final long LEAK_DETECTION_TIME_MS = 60 * 1000;
|
||||||
|
private static final String JDBC_PROTOCOL = "jdbc:redshift://";
|
||||||
|
|
||||||
|
|
||||||
|
public static HikariDataSource createConnectionPool(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException {
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
|
||||||
|
config.setDriverClassName(JDBC_DRIVER);
|
||||||
|
config.setMinimumIdle(MINIMUM_POOL_SIZE);
|
||||||
|
config.setMaximumPoolSize(MAXIMUM_POOL_SIZE);
|
||||||
|
|
||||||
|
// Set authentication properties
|
||||||
|
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
|
||||||
|
if (authentication.getUsername() != null) {
|
||||||
|
config.setUsername(authentication.getUsername());
|
||||||
|
}
|
||||||
|
if (authentication.getPassword() != null) {
|
||||||
|
config.setPassword(authentication.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the connection URL
|
||||||
|
StringBuilder urlBuilder = new StringBuilder(JDBC_PROTOCOL);
|
||||||
|
|
||||||
|
List<String> hosts = datasourceConfiguration
|
||||||
|
.getEndpoints()
|
||||||
|
.stream()
|
||||||
|
.map(endpoint -> endpoint.getHost() + ":" + ObjectUtils.defaultIfNull(endpoint.getPort(), 5439L))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
urlBuilder.append(String.join(",", hosts)).append("/");
|
||||||
|
|
||||||
|
if (!StringUtils.isEmpty(authentication.getDatabaseName())) {
|
||||||
|
urlBuilder.append(authentication.getDatabaseName());
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = urlBuilder.toString();
|
||||||
|
config.setJdbcUrl(url);
|
||||||
|
|
||||||
|
// Configuring leak detection threshold for 60 seconds. Any connection which hasn't been released in 60 seconds
|
||||||
|
// should get tracked (may be falsely for long running queries) as leaked connection
|
||||||
|
config.setLeakDetectionThreshold(LEAK_DETECTION_TIME_MS);
|
||||||
|
config.setConnectionTimeout(60 * 1000);
|
||||||
|
|
||||||
|
// Set read only mode if applicable
|
||||||
|
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
|
||||||
|
switch (configurationConnection.getMode()) {
|
||||||
|
case READ_WRITE: {
|
||||||
|
config.setReadOnly(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case READ_ONLY: {
|
||||||
|
config.setReadOnly(true);
|
||||||
|
config.addDataSourceProperty("readOnlyMode", "always");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create the connection pool from the configuration
|
||||||
|
HikariDataSource datasource = null;
|
||||||
|
try {
|
||||||
|
datasource = new HikariDataSource(config);
|
||||||
|
} catch (HikariPool.PoolInitializationException e) {
|
||||||
|
throw new AppsmithPluginException(
|
||||||
|
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||||
|
e.getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException {
|
||||||
|
|
||||||
|
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
|
||||||
|
System.out.println(Thread.currentThread().getName() +
|
||||||
|
": Encountered stale connection pool in Redshift plugin. Reporting back.");
|
||||||
|
throw new StaleConnectionException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionPool.getConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,6 +76,7 @@
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"sectionName": "SSL (optional)",
|
"sectionName": "SSL (optional)",
|
||||||
|
"hidden": true,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"label": "SSL Mode",
|
"label": "SSL Mode",
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@ import com.appsmith.external.models.DBAuth;
|
||||||
import com.appsmith.external.models.DatasourceConfiguration;
|
import com.appsmith.external.models.DatasourceConfiguration;
|
||||||
import com.appsmith.external.models.DatasourceStructure;
|
import com.appsmith.external.models.DatasourceStructure;
|
||||||
import com.appsmith.external.models.Endpoint;
|
import com.appsmith.external.models.Endpoint;
|
||||||
|
import com.appsmith.external.models.Property;
|
||||||
import com.appsmith.external.models.RequestParamDTO;
|
import com.appsmith.external.models.RequestParamDTO;
|
||||||
import com.appsmith.external.plugins.PluginExecutor;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
|
@ -45,8 +46,11 @@ import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,7 +58,7 @@ import static org.mockito.Mockito.when;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RedshiftPluginTest {
|
public class RedshiftPluginTest {
|
||||||
PluginExecutor pluginExecutor = new RedshiftPlugin.RedshiftPluginExecutor();
|
RedshiftPlugin.RedshiftPluginExecutor pluginExecutor = new RedshiftPlugin.RedshiftPluginExecutor();
|
||||||
|
|
||||||
private static String address;
|
private static String address;
|
||||||
private static Integer port;
|
private static Integer port;
|
||||||
|
|
@ -91,7 +95,14 @@ public class RedshiftPluginTest {
|
||||||
@Test
|
@Test
|
||||||
public void testDatasourceCreateConnectionFailure() {
|
public void testDatasourceCreateConnectionFailure() {
|
||||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||||
Mono<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
|
DBAuth authentication = (DBAuth) dsConfig.getAuthentication();
|
||||||
|
authentication.setUsername("user");
|
||||||
|
authentication.setPassword("pass");
|
||||||
|
authentication.setDatabaseName("dbName");
|
||||||
|
dsConfig.setEndpoints(List.of(new Endpoint("host", 1234L)));
|
||||||
|
dsConfig.setConnection(new com.appsmith.external.models.Connection());
|
||||||
|
dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_ONLY);
|
||||||
|
Mono<HikariDataSource> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
|
||||||
|
|
||||||
StepVerifier.create(dsConnectionMono)
|
StepVerifier.create(dsConnectionMono)
|
||||||
.expectErrorMatches(throwable ->
|
.expectErrorMatches(throwable ->
|
||||||
|
|
@ -99,7 +110,7 @@ public class RedshiftPluginTest {
|
||||||
throwable.getMessage().equals(
|
throwable.getMessage().equals(
|
||||||
new AppsmithPluginException(
|
new AppsmithPluginException(
|
||||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||||
"The connection attempt failed."
|
"Failed to initialize pool: The connection attempt failed."
|
||||||
)
|
)
|
||||||
.getMessage()
|
.getMessage()
|
||||||
)
|
)
|
||||||
|
|
@ -117,9 +128,9 @@ public class RedshiftPluginTest {
|
||||||
* a. isClosed(): return true
|
* a. isClosed(): return true
|
||||||
* b. isValid() : return false
|
* b. isValid() : return false
|
||||||
*/
|
*/
|
||||||
Connection mockConnection = mock(Connection.class);
|
HikariDataSource mockConnection = mock(HikariDataSource.class);
|
||||||
when(mockConnection.isClosed()).thenReturn(true);
|
when(mockConnection.isClosed()).thenReturn(true);
|
||||||
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(false);
|
when(mockConnection.isRunning()).thenReturn(false);
|
||||||
|
|
||||||
Mono<ActionExecutionResult> resultMono = pluginExecutor.execute(mockConnection, dsConfig, actionConfiguration);
|
Mono<ActionExecutionResult> resultMono = pluginExecutor.execute(mockConnection, dsConfig, actionConfiguration);
|
||||||
|
|
||||||
|
|
@ -184,13 +195,18 @@ public class RedshiftPluginTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testExecute() throws SQLException {
|
public void testExecute() throws SQLException {
|
||||||
/* Mock java.sql.Connection:
|
/* Mock Hikari connection pool object:
|
||||||
* a. isClosed()
|
* a. isClosed()
|
||||||
* b. isValid()
|
* b. isRunning()
|
||||||
*/
|
*/
|
||||||
|
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
|
||||||
|
when(mockConnectionPool.isClosed()).thenReturn(false);
|
||||||
|
when(mockConnectionPool.isRunning()).thenReturn(true);
|
||||||
|
|
||||||
Connection mockConnection = mock(Connection.class);
|
Connection mockConnection = mock(Connection.class);
|
||||||
when(mockConnection.isClosed()).thenReturn(false);
|
when(mockConnection.isClosed()).thenReturn(false);
|
||||||
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
||||||
|
when(mockConnectionPool.getConnection()).thenReturn(mockConnection);
|
||||||
|
|
||||||
/* Mock java.sql.Statement:
|
/* Mock java.sql.Statement:
|
||||||
* a. execute(...)
|
* a. execute(...)
|
||||||
|
|
@ -198,7 +214,7 @@ public class RedshiftPluginTest {
|
||||||
*/
|
*/
|
||||||
Statement mockStatement = mock(Statement.class);
|
Statement mockStatement = mock(Statement.class);
|
||||||
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
||||||
when(mockStatement.execute(Mockito.any())).thenReturn(true);
|
when(mockStatement.execute(any())).thenReturn(true);
|
||||||
doNothing().when(mockStatement).close();
|
doNothing().when(mockStatement).close();
|
||||||
|
|
||||||
/* Mock java.sql.ResultSet:
|
/* Mock java.sql.ResultSet:
|
||||||
|
|
@ -217,7 +233,7 @@ public class RedshiftPluginTest {
|
||||||
when(mockResultSet.getDate(Mockito.anyInt())).thenReturn(Date.valueOf("2018-12-31"), Date.valueOf("2018-11-30"));
|
when(mockResultSet.getDate(Mockito.anyInt())).thenReturn(Date.valueOf("2018-12-31"), Date.valueOf("2018-11-30"));
|
||||||
when(mockResultSet.getString(Mockito.anyInt())).thenReturn("18:32:45", "12:05:06+00");
|
when(mockResultSet.getString(Mockito.anyInt())).thenReturn("18:32:45", "12:05:06+00");
|
||||||
when(mockResultSet.getTime(Mockito.anyInt())).thenReturn(Time.valueOf("20:45:15"));
|
when(mockResultSet.getTime(Mockito.anyInt())).thenReturn(Time.valueOf("20:45:15"));
|
||||||
when(mockResultSet.getObject(Mockito.anyInt(), Mockito.any(Class.class))).thenReturn(OffsetDateTime.parse(
|
when(mockResultSet.getObject(Mockito.anyInt(), any(Class.class))).thenReturn(OffsetDateTime.parse(
|
||||||
"2018-11-30T19:45:15+00"));
|
"2018-11-30T19:45:15+00"));
|
||||||
when(mockResultSet.next()).thenReturn(true).thenReturn(false);
|
when(mockResultSet.next()).thenReturn(true).thenReturn(false);
|
||||||
doNothing().when(mockResultSet).close();
|
doNothing().when(mockResultSet).close();
|
||||||
|
|
@ -238,10 +254,13 @@ public class RedshiftPluginTest {
|
||||||
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
||||||
actionConfiguration.setBody("SELECT * FROM users WHERE id = 1");
|
actionConfiguration.setBody("SELECT * FROM users WHERE id = 1");
|
||||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||||
Mono<Connection> dsConnectionMono = Mono.just(mockConnection);
|
Mono<HikariDataSource> dsConnectionMono = Mono.just(mockConnectionPool);
|
||||||
|
|
||||||
|
RedshiftPlugin.RedshiftPluginExecutor spyPluginExecutor = spy(new RedshiftPlugin.RedshiftPluginExecutor());
|
||||||
|
doNothing().when(spyPluginExecutor).printConnectionPoolStatus(mockConnectionPool, false);
|
||||||
|
|
||||||
Mono<ActionExecutionResult> executeMono = dsConnectionMono
|
Mono<ActionExecutionResult> executeMono = dsConnectionMono
|
||||||
.flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration));
|
.flatMap(connPool -> spyPluginExecutor.execute(connPool, dsConfig, actionConfiguration));
|
||||||
|
|
||||||
StepVerifier.create(executeMono)
|
StepVerifier.create(executeMono)
|
||||||
.assertNext(result -> {
|
.assertNext(result -> {
|
||||||
|
|
@ -308,13 +327,24 @@ public class RedshiftPluginTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testStructure() throws SQLException {
|
public void testStructure() throws SQLException {
|
||||||
|
|
||||||
|
/* Mock Hikari connection pool object:
|
||||||
|
* a. isClosed()
|
||||||
|
* b. isRunning()
|
||||||
|
*/
|
||||||
|
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
|
||||||
|
when(mockConnectionPool.isClosed()).thenReturn(false);
|
||||||
|
when(mockConnectionPool.isRunning()).thenReturn(true);
|
||||||
|
|
||||||
/* Mock java.sql.Connection:
|
/* Mock java.sql.Connection:
|
||||||
* a. isClosed()
|
* a. isClosed()
|
||||||
* b. isValid()
|
* b. isValid()
|
||||||
|
* Also, return mock connection object from mock connection pool
|
||||||
*/
|
*/
|
||||||
Connection mockConnection = mock(Connection.class);
|
Connection mockConnection = mock(Connection.class);
|
||||||
when(mockConnection.isClosed()).thenReturn(false);
|
when(mockConnection.isClosed()).thenReturn(false);
|
||||||
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
||||||
|
when(mockConnectionPool.getConnection()).thenReturn(mockConnection);
|
||||||
|
|
||||||
/* Mock java.sql.Statement:
|
/* Mock java.sql.Statement:
|
||||||
* a. execute(...)
|
* a. execute(...)
|
||||||
|
|
@ -322,7 +352,7 @@ public class RedshiftPluginTest {
|
||||||
*/
|
*/
|
||||||
Statement mockStatement = mock(Statement.class);
|
Statement mockStatement = mock(Statement.class);
|
||||||
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
||||||
when(mockStatement.execute(Mockito.any())).thenReturn(true);
|
when(mockStatement.execute(any())).thenReturn(true);
|
||||||
doNothing().when(mockStatement).close();
|
doNothing().when(mockStatement).close();
|
||||||
|
|
||||||
/* Mock java.sql.ResultSet:
|
/* Mock java.sql.ResultSet:
|
||||||
|
|
@ -367,10 +397,13 @@ public class RedshiftPluginTest {
|
||||||
when(mockResultSet.getString("foreign_column")).thenReturn("id"); // KEYS_QUERY_FOREIGN_KEY
|
when(mockResultSet.getString("foreign_column")).thenReturn("id"); // KEYS_QUERY_FOREIGN_KEY
|
||||||
doNothing().when(mockResultSet).close();
|
doNothing().when(mockResultSet).close();
|
||||||
|
|
||||||
|
RedshiftPlugin.RedshiftPluginExecutor spyPluginExecutor = spy(new RedshiftPlugin.RedshiftPluginExecutor());
|
||||||
|
doNothing().when(spyPluginExecutor).printConnectionPoolStatus(mockConnectionPool, true);
|
||||||
|
|
||||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||||
Mono<Connection> dsConnectionMono = Mono.just(mockConnection);
|
Mono<HikariDataSource> dsConnectionMono = Mono.just(mockConnectionPool);
|
||||||
Mono<DatasourceStructure> structureMono = dsConnectionMono
|
Mono<DatasourceStructure> structureMono = dsConnectionMono
|
||||||
.flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig));
|
.flatMap(connectionPool -> spyPluginExecutor.getStructure(connectionPool, dsConfig));
|
||||||
|
|
||||||
StepVerifier.create(structureMono)
|
StepVerifier.create(structureMono)
|
||||||
.assertNext(structure -> {
|
.assertNext(structure -> {
|
||||||
|
|
@ -469,6 +502,14 @@ public class RedshiftPluginTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDuplicateColumnNames() throws SQLException {
|
public void testDuplicateColumnNames() throws SQLException {
|
||||||
|
/* Mock Hikari connection pool object:
|
||||||
|
* a. isClosed()
|
||||||
|
* b. isRunning()
|
||||||
|
*/
|
||||||
|
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
|
||||||
|
when(mockConnectionPool.isClosed()).thenReturn(false);
|
||||||
|
when(mockConnectionPool.isRunning()).thenReturn(true);
|
||||||
|
|
||||||
/* Mock java.sql.Connection:
|
/* Mock java.sql.Connection:
|
||||||
* a. isClosed()
|
* a. isClosed()
|
||||||
* b. isValid()
|
* b. isValid()
|
||||||
|
|
@ -476,14 +517,11 @@ public class RedshiftPluginTest {
|
||||||
Connection mockConnection = mock(Connection.class);
|
Connection mockConnection = mock(Connection.class);
|
||||||
when(mockConnection.isClosed()).thenReturn(false);
|
when(mockConnection.isClosed()).thenReturn(false);
|
||||||
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
when(mockConnection.isValid(Mockito.anyInt())).thenReturn(true);
|
||||||
|
when(mockConnectionPool.getConnection()).thenReturn(mockConnection);
|
||||||
|
|
||||||
/* Mock java.sql.Statement:
|
|
||||||
* a. execute(...)
|
|
||||||
* b. close()
|
|
||||||
*/
|
|
||||||
Statement mockStatement = mock(Statement.class);
|
Statement mockStatement = mock(Statement.class);
|
||||||
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
when(mockConnection.createStatement()).thenReturn(mockStatement);
|
||||||
when(mockStatement.execute(Mockito.any())).thenReturn(true);
|
when(mockStatement.execute(any())).thenReturn(true);
|
||||||
doNothing().when(mockStatement).close();
|
doNothing().when(mockStatement).close();
|
||||||
|
|
||||||
/* Mock java.sql.ResultSet:
|
/* Mock java.sql.ResultSet:
|
||||||
|
|
@ -512,10 +550,13 @@ public class RedshiftPluginTest {
|
||||||
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
||||||
actionConfiguration.setBody("SELECT id, id, username, username FROM users WHERE id = 1");
|
actionConfiguration.setBody("SELECT id, id, username, username FROM users WHERE id = 1");
|
||||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||||
Mono<Connection> dsConnectionMono = Mono.just(mockConnection);
|
Mono<HikariDataSource> dsConnectionMono = Mono.just(mockConnectionPool);
|
||||||
|
|
||||||
|
RedshiftPlugin.RedshiftPluginExecutor spyPluginExecutor = spy(new RedshiftPlugin.RedshiftPluginExecutor());
|
||||||
|
doNothing().when(spyPluginExecutor).printConnectionPoolStatus(mockConnectionPool, false);
|
||||||
|
|
||||||
Mono<ActionExecutionResult> executeMono = dsConnectionMono
|
Mono<ActionExecutionResult> executeMono = dsConnectionMono
|
||||||
.flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration));
|
.flatMap(connPool -> spyPluginExecutor.execute(connPool, dsConfig, actionConfiguration));
|
||||||
|
|
||||||
StepVerifier.create(executeMono)
|
StepVerifier.create(executeMono)
|
||||||
.assertNext(result -> {
|
.assertNext(result -> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user