Adding support for explicit data type casting in postgres prepared statement (#5842)
* Adding support for explicit data type casting in postgres prepared statement * Added text and int psql data types for support in explicit typecasting * Documenting the code
This commit is contained in:
parent
fad7874613
commit
b8eb2f1aa5
|
|
@ -9,6 +9,8 @@ import lombok.ToString;
|
|||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Param {
|
||||
|
||||
String key;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import com.appsmith.external.models.DatasourceConfiguration;
|
|||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.PsParameterDTO;
|
||||
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;
|
||||
|
|
@ -25,8 +25,8 @@ import com.appsmith.external.plugins.SmartSubstitutionInterface;
|
|||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import com.zaxxer.hikari.HikariPoolMXBean;
|
||||
import com.zaxxer.hikari.pool.HikariProxyConnection;
|
||||
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
|
||||
import com.zaxxer.hikari.pool.HikariProxyConnection;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.pf4j.Extension;
|
||||
|
|
@ -70,6 +70,15 @@ import static com.appsmith.external.helpers.MustacheHelper.replaceQuestionMarkWi
|
|||
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
|
||||
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
|
||||
import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.BOOL;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DATE;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DECIMAL;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.FLOAT8;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.INT4;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.INT8;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.TIME;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.VARCHAR;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.extractExplicitCasting;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
|
|
@ -201,7 +210,7 @@ public class PostgresPlugin extends BasePlugin {
|
|||
// In case of non prepared statement, simply do binding replacement and execute
|
||||
if (FALSE.equals(isPreparedStatement)) {
|
||||
prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration);
|
||||
return executeCommon(connection, datasourceConfiguration, actionConfiguration, FALSE, null, null);
|
||||
return executeCommon(connection, datasourceConfiguration, actionConfiguration, FALSE, null, null, null);
|
||||
}
|
||||
|
||||
// Prepared Statement
|
||||
|
|
@ -210,8 +219,10 @@ public class PostgresPlugin extends BasePlugin {
|
|||
List<String> mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query);
|
||||
// Replace all the bindings with a ? as expected in a prepared statement.
|
||||
String updatedQuery = MustacheHelper.replaceMustacheWithQuestionMark(query, mustacheKeysInOrder);
|
||||
List<DataType> explicitCastDataTypes = extractExplicitCasting(updatedQuery);
|
||||
actionConfiguration.setBody(updatedQuery);
|
||||
return executeCommon(connection, datasourceConfiguration, actionConfiguration, TRUE, mustacheKeysInOrder, executeActionDTO);
|
||||
return executeCommon(connection, datasourceConfiguration, actionConfiguration, TRUE,
|
||||
mustacheKeysInOrder, executeActionDTO, explicitCastDataTypes);
|
||||
}
|
||||
|
||||
private Mono<ActionExecutionResult> executeCommon(HikariDataSource connection,
|
||||
|
|
@ -219,7 +230,8 @@ public class PostgresPlugin extends BasePlugin {
|
|||
ActionConfiguration actionConfiguration,
|
||||
Boolean preparedStatement,
|
||||
List<String> mustacheValuesInOrder,
|
||||
ExecuteActionDTO executeActionDTO) {
|
||||
ExecuteActionDTO executeActionDTO,
|
||||
List<DataType> explicitCastDataTypes) {
|
||||
|
||||
final Map<String, Object> requestData = new HashMap<>();
|
||||
requestData.put("preparedStatement", TRUE.equals(preparedStatement) ? true : false);
|
||||
|
|
@ -277,7 +289,8 @@ public class PostgresPlugin extends BasePlugin {
|
|||
mustacheValuesInOrder,
|
||||
executeActionDTO.getParams(),
|
||||
parameters,
|
||||
connectionFromPool);
|
||||
connectionFromPool,
|
||||
explicitCastDataTypes);
|
||||
|
||||
IntStream.range(0, parameters.size())
|
||||
.forEachOrdered(i ->
|
||||
|
|
@ -759,7 +772,14 @@ public class PostgresPlugin extends BasePlugin {
|
|||
|
||||
PreparedStatement preparedStatement = (PreparedStatement) input;
|
||||
HikariProxyConnection connection = (HikariProxyConnection) args[0];
|
||||
DataType valueType = DataTypeStringUtils.stringToKnownDataTypeConverter(value);
|
||||
List<DataType> explicitCastDataTypes = (List<DataType>) args[1];
|
||||
DataType valueType;
|
||||
// If explicitly cast, set the user specified data type
|
||||
if (explicitCastDataTypes != null && explicitCastDataTypes.get(index - 1) != null) {
|
||||
valueType = explicitCastDataTypes.get(index - 1);
|
||||
} else {
|
||||
valueType = DataTypeStringUtils.stringToKnownDataTypeConverter(value);
|
||||
}
|
||||
|
||||
Map.Entry<String, String> parameter = new SimpleEntry<>(value, valueType.toString());
|
||||
insertedParams.add(parameter);
|
||||
|
|
@ -851,21 +871,21 @@ public class PostgresPlugin extends BasePlugin {
|
|||
private static String toPostgresqlPrimitiveTypeName(DataType type) {
|
||||
switch (type) {
|
||||
case LONG:
|
||||
return "int8";
|
||||
return INT8;
|
||||
case INTEGER:
|
||||
return "int4";
|
||||
return INT4;
|
||||
case FLOAT:
|
||||
return "decimal";
|
||||
return DECIMAL;
|
||||
case STRING:
|
||||
return "varchar";
|
||||
return VARCHAR;
|
||||
case BOOLEAN:
|
||||
return "bool";
|
||||
return BOOL;
|
||||
case DATE:
|
||||
return "date";
|
||||
return DATE;
|
||||
case TIME:
|
||||
return "time";
|
||||
return TIME;
|
||||
case DOUBLE:
|
||||
return "float8";
|
||||
return FLOAT8;
|
||||
case ARRAY:
|
||||
throw new IllegalArgumentException("Array of Array datatype is not supported.");
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
package com.external.plugins.utils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.appsmith.external.constants.DataType.BOOLEAN;
|
||||
import static com.appsmith.external.constants.DataType.DOUBLE;
|
||||
import static com.appsmith.external.constants.DataType.FLOAT;
|
||||
import static com.appsmith.external.constants.DataType.INTEGER;
|
||||
import static com.appsmith.external.constants.DataType.LONG;
|
||||
import static com.appsmith.external.constants.DataType.STRING;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.BOOL;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DATE;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DECIMAL;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.FLOAT8;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.INT;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.INT4;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.INT8;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.TEXT;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.TIME;
|
||||
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.VARCHAR;
|
||||
|
||||
public class PostgresDataTypeUtils {
|
||||
|
||||
/**
|
||||
* questionWithCast will match the following sample strings in a query
|
||||
* - "?"
|
||||
* - "?::text"
|
||||
*
|
||||
* Capturing only the words post "::" so that the explict data type to which the parameter must be cast can be read
|
||||
* and ignoring the group "::" from getting captured by using regex "?:" which ignores the subsequent string
|
||||
*/
|
||||
private static String questionWithCast = "\\?(?:::)*([a-zA-Z]+)*";
|
||||
private static Pattern questionWithCastPattern = Pattern.compile(questionWithCast);
|
||||
|
||||
public static DataType dataType = new DataType();
|
||||
|
||||
public static class DataType {
|
||||
/**
|
||||
* Declare all the explicitly castable postgresql types below. These would be automatically added to the
|
||||
* dataTypes set automatically.
|
||||
*
|
||||
* !!! WARNING !!!
|
||||
* When adding a new data type to support for explicit casting, please ensure to add an entry in the Map
|
||||
* dataTypeMapper which maps the postgres data types to Appsmith data types.
|
||||
*/
|
||||
public static final String INT8 = "int8";
|
||||
public static final String INT4 = "int4";
|
||||
public static final String DECIMAL = "decimal";
|
||||
public static final String VARCHAR = "varchar";
|
||||
public static final String BOOL = "bool";
|
||||
public static final String DATE = "date";
|
||||
public static final String TIME = "time";
|
||||
public static final String FLOAT8 = "float8";
|
||||
public static final String TEXT = "text";
|
||||
public static final String INT = "int";
|
||||
|
||||
public Set dataTypes = null;
|
||||
|
||||
public Set getDataTypes() {
|
||||
// if data types hasn't been initialized, read and set all the supported data types for postgres
|
||||
if (dataTypes == null || dataTypes.isEmpty()) {
|
||||
dataTypes = new HashSet<>();
|
||||
|
||||
Field[] fields = this.getClass().getDeclaredFields();
|
||||
|
||||
for (Field field : fields) {
|
||||
if (field.getType().equals(String.class)) { // if it is a String field
|
||||
try {
|
||||
dataTypes.add(field.get(dataType));
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
// We weren't able to read the value of the field. Ignore this field and continue
|
||||
// Still print the stack trace for posterity.
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We are assured that data types has been set.
|
||||
return dataTypes;
|
||||
}
|
||||
}
|
||||
|
||||
// Stores the mapping between postgres data types and appsmith data types
|
||||
public static Map dataTypeMapper;
|
||||
|
||||
private static Map getDataTypeMapper() {
|
||||
if (dataTypeMapper == null) {
|
||||
dataTypeMapper = new HashMap<String, com.appsmith.external.constants.DataType>();
|
||||
dataTypeMapper.put(INT8, LONG);
|
||||
dataTypeMapper.put(INT4, INTEGER);
|
||||
dataTypeMapper.put(DECIMAL, FLOAT);
|
||||
dataTypeMapper.put(VARCHAR, STRING);
|
||||
dataTypeMapper.put(BOOL, BOOLEAN);
|
||||
dataTypeMapper.put(DATE, com.appsmith.external.constants.DataType.DATE);
|
||||
dataTypeMapper.put(TIME, com.appsmith.external.constants.DataType.TIME);
|
||||
dataTypeMapper.put(FLOAT8, DOUBLE);
|
||||
dataTypeMapper.put(TEXT, STRING);
|
||||
dataTypeMapper.put(INT, INTEGER);
|
||||
|
||||
// Must ensure that all the declared postgres data types have a mapping to appsmith data types
|
||||
assert(dataTypeMapper.size() == dataType.getDataTypes().size());
|
||||
}
|
||||
|
||||
return dataTypeMapper;
|
||||
}
|
||||
|
||||
public static List<com.appsmith.external.constants.DataType> extractExplicitCasting(String query) {
|
||||
Matcher matcher = questionWithCastPattern.matcher(query);
|
||||
List<com.appsmith.external.constants.DataType> inputDataTypes = new ArrayList<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
String prospectiveDataType = matcher.group(1);
|
||||
|
||||
if (prospectiveDataType != null) {
|
||||
String dataTypeFromInput = prospectiveDataType.trim().toLowerCase();
|
||||
if (dataType.getDataTypes().contains(dataTypeFromInput)) {
|
||||
com.appsmith.external.constants.DataType appsmithDataType
|
||||
= (com.appsmith.external.constants.DataType) getDataTypeMapper().get(dataTypeFromInput);
|
||||
inputDataTypes.add(appsmithDataType);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Either no external casting exists or unsupported data type is being used. Do not use external casting for this
|
||||
// and instead default to implicit type casting (default behaviour) by setting the entry to null.
|
||||
inputDataTypes.add(null);
|
||||
}
|
||||
|
||||
return inputDataTypes;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,9 @@ 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.PsParameterDTO;
|
||||
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.fasterxml.jackson.databind.JsonNode;
|
||||
|
|
@ -1117,6 +1117,71 @@ public class PostgresPluginTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testPreparedStatementWithExplicitTypeCasting() {
|
||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||
|
||||
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
||||
|
||||
String query = "INSERT INTO users (id, username, password, email, dob) VALUES ({{id}}, {{firstName}}::varchar, {{lastName}}, {{email}}, {{date}}::date)";
|
||||
actionConfiguration.setBody(query);
|
||||
|
||||
List<Property> pluginSpecifiedTemplates = new ArrayList<>();
|
||||
pluginSpecifiedTemplates.add(new Property("preparedStatement", "true"));
|
||||
actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates);
|
||||
|
||||
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
|
||||
List<Param> params = new ArrayList<>();
|
||||
params.add(new Param("id", "10"));
|
||||
params.add(new Param("firstName", "1001"));
|
||||
params.add(new Param("lastName", "LastName"));
|
||||
params.add(new Param("email", "email@email.com"));
|
||||
params.add(new Param("date", "2018-12-31"));
|
||||
executeActionDTO.setParams(params);
|
||||
|
||||
Mono<HikariDataSource> connectionCreateMono = pluginExecutor.datasourceCreate(dsConfig).cache();
|
||||
|
||||
Mono<ActionExecutionResult> resultMono = connectionCreateMono
|
||||
.flatMap(pool -> pluginExecutor.executeParameterized(pool, executeActionDTO, dsConfig, actionConfiguration));
|
||||
|
||||
StepVerifier.create(resultMono)
|
||||
.assertNext(result -> {
|
||||
|
||||
assertTrue(result.getIsExecutionSuccess());
|
||||
final JsonNode node = ((ArrayNode) result.getBody()).get(0);
|
||||
assertEquals(node.get("affectedRows").asText(), "1");
|
||||
|
||||
List<RequestParamDTO> requestParams = (List<RequestParamDTO>) result.getRequest().getRequestParams();
|
||||
RequestParamDTO requestParamDTO = requestParams.get(0);
|
||||
Map<String, Object> substitutedParams = requestParamDTO.getSubstitutedParams();
|
||||
for (Map.Entry<String, Object> substitutedParam : substitutedParams.entrySet()) {
|
||||
PsParameterDTO psParameter = (PsParameterDTO) substitutedParam.getValue();
|
||||
switch (psParameter.getValue()) {
|
||||
case "10" :
|
||||
assertEquals(psParameter.getType(), "INTEGER");
|
||||
break;
|
||||
case "1001" :
|
||||
|
||||
case "LastName" :
|
||||
|
||||
case "email@email.com" :
|
||||
assertEquals(psParameter.getType(), "STRING");
|
||||
break;
|
||||
case "2018-12-31" :
|
||||
assertEquals(psParameter.getType(), "DATE");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// Delete the newly added row to not affect any other test case
|
||||
actionConfiguration.setBody("DELETE FROM users WHERE id = 10");
|
||||
connectionCreateMono
|
||||
.flatMap(pool -> pluginExecutor.executeParameterized(pool, executeActionDTO, dsConfig, actionConfiguration)).block();
|
||||
|
||||
}
|
||||
|
||||
public void testReadOnlyMode() {
|
||||
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
|
||||
dsConfig.getConnection().setMode(com.appsmith.external.models.Connection.Mode.READ_ONLY);
|
||||
|
|
@ -1138,4 +1203,4 @@ public class PostgresPluginTest {
|
|||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue