Skip to content

Commit

Permalink
FIR-32017: PreparedStatement: set date/time/timestamp with calendar (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexradzin authored Apr 30, 2024
1 parent f6772ab commit 60cedd5
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -340,6 +343,34 @@ void shouldInsertAndSelectStreams() throws SQLException, IOException {
}
}

@Test
void shouldInsertAndSelectDateTime() throws SQLException {
Car car1 = Car.builder().make("Ford").sales(12345).ts(new Timestamp(2)).d(new Date(3)).build();
try (Connection connection = createConnection()) {

try (PreparedStatement statement = connection
.prepareStatement("INSERT INTO prepared_statement_test (sales, make, ts, d) VALUES (?,?,?,?)")) {
statement.setLong(1, car1.getSales());
statement.setString(2, car1.getMake());
statement.setTimestamp(3, car1.getTs());
statement.setDate(4, car1.getD());
statement.executeUpdate();
}

try (Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("SELECT sales, make, ts, d FROM prepared_statement_test")) {
assertTrue(rs.next());
Car actual = Car.builder().sales(rs.getInt(1)).make(rs.getString(2)).ts(rs.getTimestamp(3)).d(rs.getDate(4)).build();
assertFalse(rs.next());
// Date type in DB does not really hold the time, so the time part is unpredictable and cannot be compared.
// This is the reason to compare string representation of the object: Date.toString() returns the date only
// without hours, minutes and seconds.
assertEquals(car1.toString(), actual.toString());
}
}
}


private QueryResult createExpectedResult(List<List<?>> expectedRows) {
return QueryResult.builder().databaseName(ConnectionInfo.getInstance().getDatabase())
.tableName("prepared_statement_test")
Expand Down Expand Up @@ -385,6 +416,8 @@ private static class Car {
Integer sales;
String make;
byte[] signature;
Timestamp ts;
Date d;
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
DROP TABLE IF EXISTS prepared_statement_test;
CREATE
FACT TABLE IF NOT EXISTS prepared_statement_test (
make STRING null,
sales bigint not null,
signature bytea null
make STRING null,
sales bigint not null,
ts timestamp NULL,
d date NULL,
signature bytea null
)
PRIMARY INDEX make;
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,8 @@ public void setClob(int parameterIndex, Clob clob) throws SQLException {
}

@Override
@NotImplemented
public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
throw new FireboltUnsupportedOperationException();
public void setDate(int parameterIndex, Date date, Calendar calendar) throws SQLException {
setDateTime(parameterIndex, date, calendar, JavaTypeToFireboltSQLString.DATE);
}

@Override
Expand All @@ -331,9 +330,18 @@ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLExceptio
}

@Override
@NotImplemented
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
throw new FireboltUnsupportedOperationException();
public void setTimestamp(int parameterIndex, Timestamp timestamp, Calendar calendar) throws SQLException {
setDateTime(parameterIndex, timestamp, calendar, JavaTypeToFireboltSQLString.TIMESTAMP);
}

private <T extends java.util.Date> void setDateTime(int parameterIndex, T datetime, Calendar calendar, JavaTypeToFireboltSQLString type) throws SQLException {
validateStatementIsNotClosed();
validateParamIndex(parameterIndex);
if (datetime == null || calendar == null) {
providedParameters.put(parameterIndex, type.transform(datetime));
} else {
providedParameters.put(parameterIndex, type.transform(datetime, calendar.getTimeZone().getID()));
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.firebolt.jdbc.type;

import com.firebolt.jdbc.CheckedBiFunction;
import com.firebolt.jdbc.CheckedFunction;
import com.firebolt.jdbc.exception.FireboltException;
import com.firebolt.jdbc.type.array.SqlArrayUtil;
Expand All @@ -18,6 +19,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.TimeZone;
import java.util.function.Supplier;
import java.util.stream.Stream;

Expand All @@ -39,8 +41,8 @@ public enum JavaTypeToFireboltSQLString {
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)),
DATE(Date.class, date -> SqlDateUtil.transformFromDateToSQLStringFunction.apply((Date) date), (date, tz) -> SqlDateUtil.transformFromDateWithTimezoneToSQLStringFunction.apply((Date) date, toTimeZone(tz))),
TIMESTAMP(Timestamp.class, time -> SqlDateUtil.transformFromTimestampToSQLStringFunction.apply((Timestamp) time), (ts, tz) -> SqlDateUtil.transformFromTimestampWithTimezoneToSQLStringFunction.apply((Timestamp) ts, toTimeZone(tz))),
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)),
Expand Down Expand Up @@ -80,13 +82,21 @@ public enum JavaTypeToFireboltSQLString {

private final Class<?> sourceType;
private final CheckedFunction<Object, String> transformToJavaTypeFunction;
private final CheckedBiFunction<Object, Object, String> transformToJavaTypeFunctionWithParameter;
public static final String NULL_VALUE = "NULL";
private static final Map<Class<?>, JavaTypeToFireboltSQLString> classToType = Stream.of(JavaTypeToFireboltSQLString.values())
.collect(toMap(type -> type.sourceType, type -> type));

JavaTypeToFireboltSQLString(Class<?> sourceType, CheckedFunction<Object, String> transformToSqlStringFunction) {
this(sourceType, transformToSqlStringFunction, null);
}

JavaTypeToFireboltSQLString(Class<?> sourceType,
CheckedFunction<Object, String> transformToSqlStringFunction,
CheckedBiFunction<Object, Object, String> transformToJavaTypeFunctionWithParameter) {
this.sourceType = sourceType;
this.transformToJavaTypeFunction = transformToSqlStringFunction;
this.transformToJavaTypeFunctionWithParameter = transformToJavaTypeFunctionWithParameter;
}

public static String transformAny(Object object) throws FireboltException {
Expand Down Expand Up @@ -131,15 +141,29 @@ public Class<?> getSourceType() {
return sourceType;
}

public String transform(Object object) throws FireboltException {
public String transform(Object object, Object ... more) throws FireboltException {
if (object == null) {
return NULL_VALUE;
} else {
try {
if (more.length > 0) {
return transformToJavaTypeFunctionWithParameter.apply(object, more[0]);
}
return transformToJavaTypeFunction.apply(object);
} catch (Exception e) {
throw new FireboltException("Could not convert object to a String ", e, TYPE_TRANSFORMATION_ERROR);
}
}
}

@SuppressWarnings("java:S6201") // Pattern Matching for "instanceof" was introduced in java 16 while we still try to be compliant with java 11
private static TimeZone toTimeZone(Object tz) {
if (tz instanceof TimeZone) {
return (TimeZone)tz;
}
if (tz instanceof String) {
return TimeZone.getTimeZone((String)tz);
}
throw new IllegalArgumentException(format("Cannot convert %s to TimeZone", tz));
}
}
15 changes: 10 additions & 5 deletions src/main/java/com/firebolt/jdbc/type/date/SqlDateUtil.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.firebolt.jdbc.type.date;

import com.firebolt.jdbc.CheckedBiFunction;
import lombok.CustomLog;
import lombok.experimental.UtilityClass;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.TimeZone;
import java.util.function.BiFunction;
import java.util.function.Function;

import com.firebolt.jdbc.CheckedBiFunction;

import lombok.CustomLog;
import lombok.experimental.UtilityClass;

@UtilityClass
@CustomLog
public class SqlDateUtil {
Expand All @@ -28,12 +29,16 @@ public class SqlDateUtil {

public static final Function<Timestamp, String> transformFromTimestampToSQLStringFunction = value -> String
.format("'%s'", dateTimeFormatter.format(value.toLocalDateTime()));
public static final BiFunction<Timestamp, TimeZone, String> transformFromTimestampWithTimezoneToSQLStringFunction = (ts, tz) -> String
.format("'%s'", dateTimeFormatter.format(ts.toInstant().atZone(tz.toZoneId()).toLocalDateTime()));
private static final TimeZone DEFAULT_SERVER_TZ = TimeZone.getTimeZone("UTC");
private static final DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4).parseDefaulting(ChronoField.YEAR, 0).appendPattern("[-]MM-dd")
.toFormatter();
public static final Function<Date, String> transformFromDateToSQLStringFunction = value -> String.format("'%s'",
dateFormatter.format(value.toLocalDate()));
public static final BiFunction<Date, TimeZone, String> transformFromDateWithTimezoneToSQLStringFunction = (date, tz) -> String.format("'%s'",
dateFormatter.format(Instant.ofEpochMilli(date.getTime()).atZone(tz.toZoneId()).toLocalDateTime()));
public static final CheckedBiFunction<String, TimeZone, Timestamp> transformToTimestampFunction = TimestampUtil::toTimestamp;

public static final Function<Timestamp, OffsetDateTime> transformFromTimestampToOffsetDateTime = timestamp -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Optional;
import java.util.TimeZone;
import java.util.stream.Stream;

import static java.lang.String.format;
Expand Down Expand Up @@ -96,10 +99,8 @@ public SerialNClob(char[] ch) throws SQLException {
private static Stream<Arguments> unsupported() {
return Stream.of(
Arguments.of("setRef", (Executable) () -> statement.setRef(1, mock(Ref.class))),
Arguments.of("setDate", (Executable) () -> statement.setDate(1, new Date(System.currentTimeMillis()), Calendar.getInstance())),
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("setSQLXML", (Executable) () -> statement.setSQLXML(1, mock(SQLXML.class))),

Expand Down Expand Up @@ -423,6 +424,45 @@ void shouldSetDate() throws SQLException {
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
void shouldSetDateWithCalendar() throws SQLException, ParseException {
statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)");
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST"));
calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:00:00 GMT"));
statement.setDate(1, new Date(calendar.getTimeInMillis()), calendar);
statement.execute();

verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), anyBoolean(), any());
assertEquals("INSERT INTO cars(release_date) VALUES ('2024-04-19')",
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
@DefaultTimeZone("Europe/London")
void shouldSetDateWithNullCalendar() throws SQLException, ParseException {
statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)");

statement.setDate(1, new Date(1564527600000L), null);
statement.execute();

verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), anyBoolean(), any());
assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31')",
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
void shouldSetTimeStampWithCalendar() throws SQLException, ParseException {
statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)");
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST"));
calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:11:01 GMT"));
statement.setTimestamp(1, new Timestamp(calendar.getTimeInMillis()), calendar);
statement.execute();

verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), anyBoolean(), any());
assertEquals("INSERT INTO cars(release_date) VALUES ('2024-04-19 05:11:01')",
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
@DefaultTimeZone("Europe/London")
void shouldSetTimeStamp() throws SQLException {
Expand All @@ -436,6 +476,19 @@ void shouldSetTimeStamp() throws SQLException {
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
void shouldSetNullTimeStampWithCalendar() throws SQLException, ParseException {
statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)");
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST"));
calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:11:01 GMT"));
statement.setTimestamp(1, null, calendar);
statement.execute();

verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), anyBoolean(), any());
assertEquals("INSERT INTO cars(release_date) VALUES (NULL)",
queryInfoWrapperArgumentCaptor.getValue().getSql());
}

@Test
@DefaultTimeZone("Europe/London")
void shouldSetAllObjects() throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;

import static com.firebolt.jdbc.exception.ExceptionType.TYPE_NOT_SUPPORTED;
Expand Down Expand Up @@ -123,7 +124,23 @@ void shouldTransformDateToString() throws FireboltException {
Date d = Date.valueOf(LocalDate.of(2022, 5, 23));
String expectedDateString = "'2022-05-23'";
assertEquals(expectedDateString, JavaTypeToFireboltSQLString.DATE.transform(d));
assertEquals(expectedDateString, JavaTypeToFireboltSQLString.transformAny((d)));
assertEquals(expectedDateString, JavaTypeToFireboltSQLString.transformAny(d));
}

@Test
void shouldTransformDateWithDefaultTimezoneToString() throws FireboltException {
assertEquals("'2022-05-23'", JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), TimeZone.getDefault()));
}

@Test
void shouldTransformDateWithDefaultTimezoneIdToString() throws FireboltException {
assertEquals("'2022-05-23'", JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), TimeZone.getDefault().getID()));
}

@Test
void shouldThrowExceptionWhenTransformingDateToStringWithWrongParameter() {
assertEquals(IllegalArgumentException.class,
assertThrows(SQLException.class, () -> JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), new Object())).getCause().getClass());
}

@Test
Expand Down

0 comments on commit 60cedd5

Please sign in to comment.