Skip to content

Commit

Permalink
FIR-32019: PreparedStatement.setObject with all parameters (#391)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexradzin authored Apr 17, 2024
1 parent e6b7661 commit 1667fc8
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.firebolt.jdbc.annotation.NotImplemented;
import com.firebolt.jdbc.connection.FireboltConnection;
import com.firebolt.jdbc.connection.settings.FireboltProperties;
import com.firebolt.jdbc.exception.ExceptionType;
import com.firebolt.jdbc.exception.FireboltException;
import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException;
import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException;
Expand Down Expand Up @@ -43,6 +44,9 @@

import static com.firebolt.jdbc.statement.StatementUtil.replaceParameterMarksWithValues;
import static com.firebolt.jdbc.statement.rawstatement.StatementValidatorFactory.createValidator;
import static java.lang.String.format;
import static java.sql.Types.DECIMAL;
import static java.sql.Types.NUMERIC;
import static java.sql.Types.VARBINARY;

@CustomLog
Expand Down Expand Up @@ -189,7 +193,14 @@ public void clearParameters() throws SQLException {
public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
validateStatementIsNotClosed();
validateParamIndex(parameterIndex);
setObject(parameterIndex, x);
try {
providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.transformAny(x, targetSqlType));
} catch (FireboltException fbe) {
if (ExceptionType.TYPE_NOT_SUPPORTED.equals(fbe.getType())) {
throw new SQLFeatureNotSupportedException(fbe.getMessage(), fbe);
}
throw fbe;
}
}

@Override
Expand Down Expand Up @@ -269,7 +280,7 @@ public int executeUpdate(String sql) throws SQLException {
private void validateParamIndex(int paramIndex) throws FireboltException {
if (rawStatement.getTotalParams() < paramIndex) {
throw new FireboltException(
String.format("Cannot set parameter as there is no parameter at index: %d for statement: %s",
format("Cannot set parameter as there is no parameter at index: %d for statement: %s",
paramIndex, rawStatement));
}
}
Expand Down Expand Up @@ -370,9 +381,24 @@ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException
}

@Override
@NotImplemented
public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {
throw new FireboltSQLFeatureNotSupportedException();
validateStatementIsNotClosed();
validateParamIndex(parameterIndex);
try {
// scaleOfLength should affect only DECIMAL and NUMERIC types
boolean isNumber = (DECIMAL == targetSqlType || NUMERIC == targetSqlType) && x instanceof Number;
String str = isNumber ? formatDecimalNumber(x, scaleOrLength) : JavaTypeToFireboltSQLString.transformAny(x, targetSqlType);
providedParameters.put(parameterIndex, str);
} catch (FireboltException fbe) {
if (ExceptionType.TYPE_NOT_SUPPORTED.equals(fbe.getType())) {
throw new SQLFeatureNotSupportedException(fbe.getMessage(), fbe);
}
}
}

private String formatDecimalNumber(Object x, int scaleOrLength) {
String format = format("%%.%df", scaleOrLength);
return format(format, ((Number)x).doubleValue());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.JDBCType;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.function.Supplier;

import static com.firebolt.jdbc.exception.ExceptionType.TYPE_NOT_SUPPORTED;
import static com.firebolt.jdbc.exception.ExceptionType.TYPE_TRANSFORMATION_ERROR;
Expand All @@ -24,23 +28,53 @@

public enum JavaTypeToFireboltSQLString {
BOOLEAN(Boolean.class, value -> Boolean.TRUE.equals(value) ? "1" : "0"),
UUID(java.util.UUID.class, value -> ((UUID) value).toString()),
SHORT(Short.class, value -> Short.toString((short) value)),
UUID(java.util.UUID.class, Object::toString),
SHORT(Short.class, value -> Short.toString(((Number) value).shortValue())),
STRING(String.class, getSQLStringValueOfString()),
LONG(Long.class, String::valueOf),
INTEGER(Integer.class, String::valueOf),
BIG_INTEGER(BigInteger.class, String::valueOf),
FLOAT(Float.class, String::valueOf),
DOUBLE(Double.class, String::valueOf),
LONG(Long.class, value -> Long.toString(((Number)value).longValue())),
INTEGER(Integer.class, value -> Integer.toString(((Number)value).intValue())),
BIG_INTEGER(BigInteger.class, value -> value instanceof BigInteger ? value.toString() : Long.toString(((Number)value).longValue())),
FLOAT(Float.class, value -> Float.toString(((Number)value).floatValue())),
DOUBLE(Double.class, value -> Double.toString(((Number)value).doubleValue())),
DATE(Date.class, date -> SqlDateUtil.transformFromDateToSQLStringFunction.apply((Date) date)),
TIMESTAMP(Timestamp.class, time -> SqlDateUtil.transformFromTimestampToSQLStringFunction.apply((Timestamp) time)),
BIG_DECIMAL(BigDecimal.class, value -> value == null ? BaseType.NULL_VALUE : ((BigDecimal) value).toPlainString()),
ARRAY(Array.class, SqlArrayUtil::arrayToString),
BYTE_ARRAY(byte[].class, value -> ofNullable(byteArrayToHexString((byte[])value, true)).map(x -> format("E'%s'::BYTEA", x)).orElse(null)),
;

private static final List<Entry<String, String>> characterToEscapedCharacterPairs = List.of(
Map.entry("\0", "\\0"), Map.entry("\\", "\\\\"), Map.entry("'", "''"));
//https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html
private static final Map<JDBCType, Class<?>> jdbcTypeToClass = Map.ofEntries(
Map.entry(JDBCType.CHAR, String.class),
Map.entry(JDBCType.VARCHAR, String.class),
Map.entry(JDBCType.LONGVARCHAR,String.class),
Map.entry(JDBCType.NUMERIC, java.math.BigDecimal.class),
Map.entry(JDBCType.DECIMAL, java.math.BigDecimal.class),
Map.entry(JDBCType.BIT, Boolean.class),
Map.entry(JDBCType.BOOLEAN, Boolean.class),
Map.entry(JDBCType.TINYINT, Short.class),
Map.entry(JDBCType.SMALLINT, Short.class),
Map.entry(JDBCType.INTEGER, Integer.class),
Map.entry(JDBCType.BIGINT, Long.class),
Map.entry(JDBCType.REAL, Float.class),
Map.entry(JDBCType.FLOAT, Double.class),
Map.entry(JDBCType.DOUBLE, Double.class),
Map.entry(JDBCType.BINARY, byte[].class),
Map.entry(JDBCType.VARBINARY, byte[].class),
Map.entry(JDBCType.LONGVARBINARY, byte[].class),
Map.entry(JDBCType.DATE, java.sql.Date.class),
Map.entry(JDBCType.TIME, java.sql.Time.class),
Map.entry(JDBCType.TIMESTAMP, java.sql.Timestamp.class),
//DISTINCT Object type of underlying type
Map.entry(JDBCType.CLOB, Clob.class),
Map.entry(JDBCType.BLOB, Blob.class),
Map.entry(JDBCType.ARRAY, Array.class)
//STRUCT, Struct or SQLData
//Map.entry(JDBCType.REF, Ref.class)
//Map.entry(JDBCType.JAVA_OBJECT, Object.class)
);

private final Class<?> sourceType;
private final CheckedFunction<Object, String> transformToJavaTypeFunction;
public static final String NULL_VALUE = "NULL";
Expand All @@ -51,14 +85,19 @@ public enum JavaTypeToFireboltSQLString {
}

public static String transformAny(Object object) throws FireboltException {
Class<?> objectType;
if (object == null) {
return NULL_VALUE;
} else if (object.getClass().isArray() && !byte[].class.equals(object.getClass())) {
objectType = Array.class;
} else {
objectType = object.getClass();
}
return transformAny(object, () -> getType(object));
}

public static String transformAny(Object object, int sqlType) throws SQLException {
return transformAny(object, () -> getType(sqlType));
}

private static String transformAny(Object object, Supplier<Class<?>> classSupplier) throws FireboltException {
return object == null ? NULL_VALUE : transformAny(object, classSupplier.get());
}


private static String transformAny(Object object, Class<?> objectType) throws FireboltException {
JavaTypeToFireboltSQLString converter = Arrays.stream(JavaTypeToFireboltSQLString.values())
.filter(c -> c.getSourceType().equals(objectType)).findAny()
.orElseThrow(() -> new FireboltException(
Expand All @@ -67,6 +106,14 @@ public static String transformAny(Object object) throws FireboltException {
return converter.transform(object);
}

private static Class<?> getType(Object object) {
return object.getClass().isArray() && !byte[].class.equals(object.getClass()) ? Array.class : object.getClass();
}

private static Class<?> getType(int sqlType) {
return jdbcTypeToClass.get(JDBCType.valueOf(sqlType));
}

private static CheckedFunction<Object, String> getSQLStringValueOfString() {
return value -> {
String escaped = (String) value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.firebolt.jdbc.statement.preparedstatement;

import com.firebolt.jdbc.client.query.StatementClient;
import com.firebolt.jdbc.connection.FireboltConnection;
import com.firebolt.jdbc.connection.settings.FireboltProperties;
import com.firebolt.jdbc.exception.FireboltException;
Expand All @@ -14,6 +15,7 @@
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.DefaultTimeZone;
Expand All @@ -23,6 +25,7 @@
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.math.BigDecimal;
Expand All @@ -44,7 +47,10 @@
import java.util.Optional;
import java.util.stream.Stream;

import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -103,12 +109,10 @@ private static Stream<Arguments> unsupported() {
Arguments.of("setTime", (Executable) () -> statement.setTime(1, new Time(System.currentTimeMillis()))),
Arguments.of("setTime(calendar)", (Executable) () -> statement.setTime(1, new Time(System.currentTimeMillis()), Calendar.getInstance())),
Arguments.of("setTimestamp", (Executable) () -> statement.setTimestamp(1, new Timestamp(System.currentTimeMillis()), Calendar.getInstance())),
Arguments.of("setRowId", (Executable) () -> statement.setRowId(1, mock(RowId.class)),
Arguments.of("setRowId", (Executable) () -> statement.setRowId(1, mock(RowId.class))),
Arguments.of("setSQLXML", (Executable) () -> statement.setSQLXML(1, mock(SQLXML.class))),

// TODO: add support of these methods
Arguments.of("getParameterMetaData", (Executable) () -> statement.getParameterMetaData())),
Arguments.of("setObject", (Executable) () -> statement.setObject(1, mock(SQLXML.class), Types.VARCHAR, 0)),
Arguments.of("getParameterMetaData", (Executable) () -> statement.getParameterMetaData())
);
}
Expand All @@ -128,6 +132,26 @@ void afterEach() throws SQLException {
}
}

@ParameterizedTest
@CsvSource(value = {
"INSERT INTO data (field) VALUES (?),false",
"SELECT * FROM data WHERE field=?,true",
})
void getMetadata(String query, boolean expectedResultSet) throws SQLException {
StatementClient statementClient = mock(StatementClient.class);
when(statementClient.executeSqlStatement(any(), any(), anyBoolean(), anyInt(), anyBoolean())).thenReturn(new ByteArrayInputStream(new byte[0]));
statement = new FireboltPreparedStatement(new FireboltStatementService(statementClient), connection, query);
assertNull(statement.getMetaData());
statement.setObject(1, null);
boolean shouldHaveResultSet = statement.execute();
assertEquals(expectedResultSet, shouldHaveResultSet);
if (shouldHaveResultSet) {
assertNotNull(statement.getMetaData());
} else {
assertNull(statement.getMetaData());
}
}

@Test
void shouldExecute() throws SQLException {
statement = createStatementWithSql("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (?,?,?,?,?,?,?,?)");
Expand All @@ -152,7 +176,7 @@ void shouldExecute() throws SQLException {
void setNullByteArray() throws SQLException {
statement = createStatementWithSql("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (?,?,?,?,?,?,?,?)");

statement.setInt(1, 500);
statement.setShort(1, (short)500);
statement.setString(2, "Ford");
statement.setObject(3, "FOCUS", Types.VARCHAR);
statement.setNull(4, Types.VARCHAR);
Expand Down Expand Up @@ -393,6 +417,112 @@ void shouldSetAllObjects() throws SQLException {
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
@DefaultTimeZone("Europe/London")
void shouldSetAllObjectsWithCorrectSqlType() throws SQLException {
statement = createStatementWithSql(
"INSERT INTO cars(timestamp, date, float, long, big_decimal, null, boolean, int) "
+ "VALUES (?,?,?,?,?,?,?,?)");

statement.setObject(1, new Timestamp(1564571713000L), Types.TIMESTAMP);
statement.setObject(2, new Date(1564527600000L), Types.DATE);
statement.setObject(3, 5.5F, Types.FLOAT);
statement.setObject(4, 5L, Types.BIGINT);
statement.setObject(5, new BigDecimal("555555555555.55555555"), Types.NUMERIC);
statement.setObject(6, null, Types.JAVA_OBJECT);
statement.setObject(7, true, Types.BOOLEAN);
statement.setObject(8, 5, Types.INTEGER);

statement.execute();

verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties),
anyInt(), anyInt(), anyInt(), anyBoolean(), anyBoolean(), any());

assertEquals(
"INSERT INTO cars(timestamp, date, float, long, big_decimal, null, boolean, int) VALUES ('2019-07-31 12:15:13','2019-07-31',5.5,5,555555555555.55555555,NULL,1,5)",
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@ParameterizedTest
@CsvSource(value = {
"123," + Types.TINYINT + ",3",
"123," + Types.SMALLINT + ",1",
"123," + Types.INTEGER + ",",
"123," + Types.BIGINT + ",",
})
// scale is ignored for these types
void shouldSetIntegerObjectWithCorrectSqlType(int value, int type, Integer scale) throws SQLException {
shouldSetObjectWithCorrectSqlType(value, type, scale, String.valueOf(value));
}

@ParameterizedTest
@CsvSource(value = {
"3.14," + Types.DECIMAL + ",2,3.14",
"3.1415926," + Types.DECIMAL + ",2,3.14",
"2.7," + Types.NUMERIC + ",2,2.70",
"2.718281828," + Types.NUMERIC + ",1,2.7",
"2.718281828," + Types.NUMERIC + ",5,2.71828",
})
void shouldSetFloatObjectWithCorrectScalableSqlTypeAndScale(float value, int type, int scale, String expected) throws SQLException {
shouldSetObjectWithCorrectSqlType(value, type, scale, expected);
}

@ParameterizedTest
@CsvSource(value = {
"3.14," + Types.DECIMAL + ",2,3.14",
"3.1415926," + Types.DECIMAL + ",2,3.14",
"2.7," + Types.NUMERIC + ",2,2.70",
"2.718281828," + Types.NUMERIC + ",1,2.7",
"2.718281828," + Types.NUMERIC + ",5,2.71828",
})
void shouldSetDoubleObjectWithCorrectScalableSqlTypeAndScale(double value, int type, int scale, String expected) throws SQLException {
shouldSetObjectWithCorrectSqlType(value, type, scale, expected);
}

@ParameterizedTest
@CsvSource(value = {
"3.14," + Types.FLOAT + ",",
"3.1415926," + Types.DOUBLE + ",",
"3.1415926," + Types.DOUBLE + ",3", // scale is ignored for this type
})
void shouldSetDoubleObjectWithCorrectSqlTypeAndScale(double value, int type, Integer scale) throws SQLException {
shouldSetObjectWithCorrectSqlType(value, type, scale, Double.toString(value));
}

@Test
void unsupportedType() {
statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)");
assertThrows(SQLException.class, () -> statement.setObject(1, this));
// STRUCT is not supported now, so it can be used as an example of unsupported type
assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setObject(1, this, Types.STRUCT));
assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setObject(1, this, Types.STRUCT, 5));
}

private void shouldSetObjectWithCorrectSqlType(Object value, int type, Integer scale, String expected) throws SQLException {
statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)");
if (scale == null) {
statement.setObject(1, value, type);
} else {
statement.setObject(1, value, type, scale);
}
statement.execute();
verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties),
anyInt(), anyInt(), anyInt(), anyBoolean(), anyBoolean(), any());

assertEquals(format("INSERT INTO data (column) VALUES (%s)", expected), queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
void clearParameters() throws SQLException {
statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)");
statement.setObject(1, ""); // set parameter
statement.execute(); // execute statement - should work because all parameters are set
statement.clearParameters(); // clear parameters; now there are no parameters
assertThrows(IllegalArgumentException.class, () -> statement.execute()); // execution fails because parameters are not set
statement.setObject(1, ""); // set parameter again
statement.execute(); // now execution is successful
}

private FireboltPreparedStatement createStatementWithSql(String sql) {
return new FireboltPreparedStatement(fireboltStatementService, connection, sql);
}
Expand Down
Loading

0 comments on commit 1667fc8

Please sign in to comment.