Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;

/**
* Prepared statement over the Hrana transport.
Expand All @@ -45,6 +52,9 @@
*/
public class LibsqlPreparedStatement extends LibsqlStatement implements PreparedStatement, ParameterMetaData {

private static final DateTimeFormatter DATE_TEXT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter TIME_TEXT = DateTimeFormatter.ofPattern("HH:mm:ss");

private final String sql;
private final boolean returnsResultSet;
private final int parameterCount;
Expand Down Expand Up @@ -324,17 +334,55 @@ public ResultSetMetaData getMetaData() throws SQLException {

@Override
public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
setDate(parameterIndex, x);
if (cal == null || x == null) {
setDate(parameterIndex, x);
return;
}
setParam(parameterIndex, Value.of(DATE_TEXT.format(Instant.ofEpochMilli(x.getTime()).atZone(zone(cal)))));
}

@Override
public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
setTime(parameterIndex, x);
if (cal == null || x == null) {
setTime(parameterIndex, x);
return;
}
LocalTime time = Instant.ofEpochMilli(x.getTime())
.atZone(zone(cal))
.toLocalTime()
.truncatedTo(ChronoUnit.SECONDS);
setParam(parameterIndex, Value.of(TIME_TEXT.format(time)));
}

@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
setTimestamp(parameterIndex, x);
if (cal == null || x == null) {
setTimestamp(parameterIndex, x);
return;
}
setParam(parameterIndex, Value.of(timestampText(x.toInstant().atZone(zone(cal)))));
}

private static ZoneId zone(Calendar cal) {
return cal.getTimeZone().toZoneId();
}

/**
* The {@link Timestamp#toString()} text form: {@code yyyy-MM-dd HH:mm:ss}, a fraction with
* trailing zeros removed when nanos are non-zero, and {@code .0} when nanos are zero.
*/
private static String timestampText(ZonedDateTime value) {
String dateTime = DATE_TEXT.format(value) + " " + TIME_TEXT.format(value);
int nanos = value.getNano();
if (nanos == 0) {
return dateTime + ".0";
}
String fraction = String.format(Locale.ROOT, "%09d", nanos);
int end = fraction.length();
while (end > 1 && fraction.charAt(end - 1) == '0') {
end--;
}
return dateTime + "." + fraction.substring(0, end);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.HashMap;
Expand Down Expand Up @@ -389,6 +390,14 @@ private static Date dateValue(Value value) throws SQLException {
return dateTime == null ? null : Date.valueOf(dateTime.toLocalDate());
}

private static Date dateValue(Value value, ZoneId zone) throws SQLException {
LocalDateTime dateTime = dateTimeValue(value, "Date", zone);
if (dateTime == null) {
return null;
}
return new Date(dateTime.toLocalDate().atStartOfDay(zone).toInstant().toEpochMilli());
}

@Override
public Time getTime(int columnIndex) throws SQLException {
checkReadable(columnIndex);
Expand All @@ -400,6 +409,18 @@ private static Time timeValue(Value value) throws SQLException {
return dateTime == null ? null : Time.valueOf(dateTime.toLocalTime());
}

private static Time timeValue(Value value, ZoneId zone) throws SQLException {
LocalDateTime dateTime = dateTimeValue(value, "Time", zone);
if (dateTime == null) {
return null;
}
return new Time(LocalDate.of(1970, 1, 1)
.atTime(dateTime.toLocalTime())
.atZone(zone)
.toInstant()
.toEpochMilli());
}

@Override
public Timestamp getTimestamp(int columnIndex) throws SQLException {
checkReadable(columnIndex);
Expand All @@ -411,6 +432,11 @@ private static Timestamp timestampValue(Value value) throws SQLException {
return dateTime == null ? null : Timestamp.valueOf(dateTime);
}

private static Timestamp timestampValue(Value value, ZoneId zone) throws SQLException {
LocalDateTime dateTime = dateTimeValue(value, "Timestamp", zone);
return dateTime == null ? null : Timestamp.from(dateTime.atZone(zone).toInstant());
}

private static OffsetTime offsetTimeValue(Value value) throws SQLException {
String text = stringValue(value);
if (text == null) {
Expand Down Expand Up @@ -893,12 +919,42 @@ private static Clob clobValue(Value value) {
@Override public Blob getBlob(String columnLabel) throws SQLException { return getBlob(findColumn(columnLabel)); }
@Override public Clob getClob(String columnLabel) throws SQLException { return getClob(findColumn(columnLabel)); }
@Override public Array getArray(String columnLabel) throws SQLException { return getArray(findColumn(columnLabel)); }
@Override public Date getDate(int columnIndex, Calendar cal) throws SQLException { return getDate(columnIndex); }
@Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { return getDate(columnLabel); }
@Override public Time getTime(int columnIndex, Calendar cal) throws SQLException { return getTime(columnIndex); }
@Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { return getTime(columnLabel); }
@Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { return getTimestamp(columnIndex); }
@Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(columnLabel); }
@Override
public Date getDate(int columnIndex, Calendar cal) throws SQLException {
if (cal == null) {
return getDate(columnIndex);
}
checkReadable(columnIndex);
return dateValue(getCurrentValue(columnIndex), zone(cal));
}

@Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { return getDate(findColumn(columnLabel), cal); }

@Override
public Time getTime(int columnIndex, Calendar cal) throws SQLException {
if (cal == null) {
return getTime(columnIndex);
}
checkReadable(columnIndex);
return timeValue(getCurrentValue(columnIndex), zone(cal));
}

@Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { return getTime(findColumn(columnLabel), cal); }

@Override
public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
if (cal == null) {
return getTimestamp(columnIndex);
}
checkReadable(columnIndex);
return timestampValue(getCurrentValue(columnIndex), zone(cal));
}

@Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(findColumn(columnLabel), cal); }

private static ZoneId zone(Calendar cal) {
return cal.getTimeZone().toZoneId();
}
@Override
public URL getURL(int columnIndex) throws SQLException {
checkReadable(columnIndex);
Expand Down Expand Up @@ -1165,6 +1221,11 @@ private static LocalDateTime dateTimeValue(Value value, String targetType) throw
return SqliteDateTimeValues.from(value, targetType);
}

private static LocalDateTime dateTimeValue(Value value, String targetType, ZoneId zone)
throws SQLException {
return SqliteDateTimeValues.from(value, targetType, zone);
}

@Override
public <T> T getObject(String columnLabel, Class<T> type) throws SQLException {
return getObject(findColumn(columnLabel), type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,35 @@ final class SqliteDateTimeValues {
private SqliteDateTimeValues() {}

static LocalDateTime from(Value value, String targetType) throws SQLException {
return from(value, targetType, ZoneId.systemDefault());
}

/**
* Converts a value into the wall-clock fields of {@code zone}. An INTEGER or FLOAT value is a
* fixed instant, and text with an explicit offset is too; both are projected into {@code zone}.
* Naive text is wall-clock time in {@code zone}.
*/
static LocalDateTime from(Value value, String targetType, ZoneId zone) throws SQLException {
return switch (value) {
case Value.Null n -> null;
case Value.Integer i -> epochMillisToLocalDateTime(i.value(), targetType);
case Value.Integer i -> epochMillisToLocalDateTime(i.value(), targetType, zone);
case Value.Float f ->
epochMillisToLocalDateTime(julianDayToEpochMillis(f.value()), targetType);
epochMillisToLocalDateTime(julianDayToEpochMillis(f.value()), targetType, zone);
case Value.Text t ->
t.value().isBlank() ? null : parseSqliteDateTime(t.value(), targetType);
t.value().isBlank() ? null : parseSqliteDateTime(t.value(), targetType, zone);
case Value.Blob b -> throw new SQLException("Bad value for type " + targetType);
};
}

static LocalDateTime epochMillisToLocalDateTime(long epochMillis, String targetType)
throws SQLException {
return epochMillisToLocalDateTime(epochMillis, targetType, ZoneId.systemDefault());
}

static LocalDateTime epochMillisToLocalDateTime(
long epochMillis, String targetType, ZoneId zone) throws SQLException {
try {
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault());
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), zone);
} catch (DateTimeException e) {
throw new SQLException("Bad value for type " + targetType, e);
}
Expand All @@ -47,6 +60,11 @@ static long julianDayToEpochMillis(double julianDay) {

static LocalDateTime parseSqliteDateTime(String text, String targetType)
throws SQLException {
return parseSqliteDateTime(text, targetType, ZoneId.systemDefault());
}

static LocalDateTime parseSqliteDateTime(String text, String targetType, ZoneId zone)
throws SQLException {
String value = text.trim();
ZoneOffset offset = null;
if (value.endsWith("Z") || value.endsWith("z")) {
Expand Down Expand Up @@ -91,9 +109,7 @@ static LocalDateTime parseSqliteDateTime(String text, String targetType)
LocalDateTime local = LocalDateTime.of(date, time);
return offset == null
? local
: local.atOffset(offset)
.atZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime();
: local.atOffset(offset).atZoneSameInstant(zone).toLocalDateTime();
} catch (DateTimeException | NumberFormatException e) {
throw new SQLException("Bad value for type " + targetType + ": " + text, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -532,6 +534,65 @@ void temporalTargetsBindSqliteText() throws SQLException {
assertThat(bind(utilDate, Types.TIMESTAMP)).isEqualTo(Value.of(expectedTimestamp));
}

@Test
void calendarSettersRenderTheInstantInTheCalendarZone() throws SQLException {
Instant instant = Instant.parse("2024-01-02T23:30:00Z");
Timestamp timestamp = Timestamp.from(instant);
Date date = new Date(instant.toEpochMilli());
Time time = new Time(instant.toEpochMilli());

assertThat(bindParameter(s -> s.setTimestamp(1, timestamp, calendar("Asia/Tokyo"))))
.isEqualTo(Value.of("2024-01-03 08:30:00.0"));
assertThat(bindParameter(s -> s.setTimestamp(1, timestamp, calendar("UTC"))))
.isEqualTo(Value.of("2024-01-02 23:30:00.0"));
assertThat(bindParameter(s -> s.setDate(1, date, calendar("Asia/Tokyo"))))
.isEqualTo(Value.of("2024-01-03"));
assertThat(bindParameter(s -> s.setDate(1, date, calendar("UTC"))))
.isEqualTo(Value.of("2024-01-02"));
assertThat(bindParameter(s -> s.setTime(1, time, calendar("Asia/Tokyo"))))
.isEqualTo(Value.of("08:30:00"));
assertThat(bindParameter(s -> s.setTime(1, time, calendar("UTC"))))
.isEqualTo(Value.of("23:30:00"));

Calendar buddhistTokyo = Calendar.getInstance(
TimeZone.getTimeZone("Asia/Tokyo"), Locale.forLanguageTag("th-TH-u-ca-buddhist"));
assertThat(bindParameter(s -> s.setTimestamp(1, timestamp, buddhistTokyo)))
.as("only the calendar zone matters, not its field system")
.isEqualTo(Value.of("2024-01-03 08:30:00.0"));
}

@Test
void calendarTimestampSetterKeepsTheTimestampTextForm() throws SQLException {
Timestamp withNanos = Timestamp.from(Instant.parse("2024-01-02T23:30:00.123456789Z"));
Timestamp withMillis = Timestamp.from(Instant.parse("2024-01-02T23:30:00.123Z"));

assertThat(bindParameter(s -> s.setTimestamp(1, withNanos, calendar("Asia/Tokyo"))))
.isEqualTo(Value.of("2024-01-03 08:30:00.123456789"));
assertThat(bindParameter(s -> s.setTimestamp(1, withMillis, calendar("Asia/Tokyo"))))
.isEqualTo(Value.of("2024-01-03 08:30:00.123"));
}

@Test
void nullCalendarSetterEqualsTheDefaultOverload() throws SQLException {
Instant instant = Instant.parse("2024-01-02T23:30:00Z");

assertThat(bindParameter(s -> s.setTimestamp(1, Timestamp.from(instant), null)))
.isEqualTo(bindParameter(s -> s.setTimestamp(1, Timestamp.from(instant))));
assertThat(bindParameter(s -> s.setDate(1, new Date(instant.toEpochMilli()), null)))
.isEqualTo(bindParameter(s -> s.setDate(1, new Date(instant.toEpochMilli()))));
assertThat(bindParameter(s -> s.setTime(1, new Time(instant.toEpochMilli()), null)))
.isEqualTo(bindParameter(s -> s.setTime(1, new Time(instant.toEpochMilli()))));
}

@Test
void calendarSettersBindNullForNullValues() throws SQLException {
Calendar tokyo = calendar("Asia/Tokyo");

assertThat(bindParameter(s -> s.setTimestamp(1, null, tokyo))).isEqualTo(Value.ofNull());
assertThat(bindParameter(s -> s.setDate(1, null, tokyo))).isEqualTo(Value.ofNull());
assertThat(bindParameter(s -> s.setTime(1, null, tokyo))).isEqualTo(Value.ofNull());
}

@Test
void lobTargetsReadTheirStreams() throws SQLException {
byte[] bytes = {1, 2, 3};
Expand Down Expand Up @@ -611,6 +672,10 @@ private static Value bindParameter(ParameterBinder binder) throws SQLException {
return connection.executed.get(0).args().get(0);
}

private static Calendar calendar(String zoneId) {
return Calendar.getInstance(TimeZone.getTimeZone(zoneId));
}

private static Calendar calendar2024() {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.clear();
Expand Down
Loading
Loading