Skip to content
Closed
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 @@ -264,6 +264,10 @@ record GetterCell(
new GetterCell("getURL", "DATALINK", TEXT_URL, ResultSet::getURL, URL.class),
new GetterCell("getObject", "JAVA_OBJECT", TEXT_ABC, (rs, i) -> rs.getObject(i),
Object.class),
new GetterCell("getObject", "TIME_WITH_TIMEZONE", "03:04:05Z",
(rs, i) -> rs.getObject(i), OffsetTime.class),
new GetterCell("getObject", "TIMESTAMP_WITH_TIMEZONE", "2024-01-02T03:04:05Z",
(rs, i) -> rs.getObject(i), OffsetDateTime.class),
new GetterCell("getRowId", "ROWID", TEXT_ROWID, ResultSet::getRowId, RowId.class),
new GetterCell("getSQLXML", "SQLXML", TEXT_XML, ResultSet::getSQLXML, SQLXML.class));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -26,10 +28,11 @@
* falls back to the storage class of the value and marks the result {@link Support#FALLBACK} so a
* caller can record the exception.
*
* <p>The registry holds the 27 canonical JDBC 4.3 Appendix B type names. A separate alias table
* maps the SQLite spellings that Appendix B does not name, such as {@code INT8} and {@code
* DATETIME}, to the registry entry they stand for, preserving the sqlite-jdbc metadata that
* existing users see. Both tables resolve through the declared type, never through the value.
* <p>The registry holds the canonical JDBC 4.3 Appendix B type names that have a declared-type
* entry. A separate alias table maps the SQLite spellings that Appendix B does not name, such as
* {@code INT8} and {@code DATETIME}, to the registry entry they stand for, preserving the
* sqlite-jdbc metadata that existing users see. Both tables resolve through the declared type,
* never through the value.
*/
final class JdbcTypeMapping {

Expand Down Expand Up @@ -159,6 +162,18 @@ private static Map<String, JdbcType> declaredTypeRegistry() {
entry("DATE", Types.DATE, Date.class, Date.class, StorageClass.TEXT),
entry("TIME", Types.TIME, Time.class, Time.class, StorageClass.TEXT),
entry("TIMESTAMP", Types.TIMESTAMP, Timestamp.class, Timestamp.class, StorageClass.TEXT),
entry(
"TIME_WITH_TIMEZONE",
Types.TIME_WITH_TIMEZONE,
OffsetTime.class,
OffsetTime.class,
StorageClass.TEXT),
entry(
"TIMESTAMP_WITH_TIMEZONE",
Types.TIMESTAMP_WITH_TIMEZONE,
OffsetDateTime.class,
OffsetDateTime.class,
StorageClass.TEXT),
entry("CLOB", Types.CLOB, Clob.class, Clob.class, StorageClass.TEXT),
entry("NCLOB", Types.NCLOB, NClob.class, NClob.class, StorageClass.TEXT),
entry("SQLXML", Types.SQLXML, SQLXML.class, SQLXML.class, StorageClass.TEXT),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -117,6 +120,23 @@ private Value getCurrentValue(int columnIndex) throws SQLException {
return valueAtCursor(columnIndex);
}

/**
* Rejects a declared offset column through a getter that Table B-6 does not list for it. The
* offset types are new in this release and expose only {@code getObject}; the rest of the
* strict Table B-6 matrix lands with the unsupported-type policy.
*/
private void rejectUnlistedOffsetGetter(int columnIndex) throws SQLException {
checkClosed();
if (columnIndex < 1 || columnIndex > declaredTypes.length) {
return;
}
JdbcTypeMapping.JdbcType type = declaredTypes[columnIndex - 1];
if (type != null && (type.objectClass() == OffsetTime.class || type.objectClass() == OffsetDateTime.class)) {
throw new SQLFeatureNotSupportedException(
"Declared " + JdbcTypeMapping.sqlTypeName(type.sqlType()) + " columns support only getObject");
}
}

Value valueForMetadata(int columnIndex) {
int rowIndex = currentRowIndex < 0 ? 0 : currentRowIndex;
if (rowIndex >= rows.size()) {
Expand Down Expand Up @@ -190,6 +210,7 @@ public boolean wasNull() throws SQLException {
*/
@Override
public String getString(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return stringValue(getCurrentValue(columnIndex));
}

Expand All @@ -212,6 +233,7 @@ private static String stringValue(Value val) {
*/
@Override
public boolean getBoolean(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return booleanValue(getCurrentValue(columnIndex));
}

Expand Down Expand Up @@ -240,21 +262,25 @@ public short getShort(int columnIndex) throws SQLException {

@Override
public int getInt(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return (int) toLong(getCurrentValue(columnIndex));
}

@Override
public long getLong(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return toLong(getCurrentValue(columnIndex));
}

@Override
public float getFloat(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return (float) toDouble(getCurrentValue(columnIndex));
}

@Override
public double getDouble(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return toDouble(getCurrentValue(columnIndex));
}

Expand Down Expand Up @@ -289,6 +315,7 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException

@Override
public byte[] getBytes(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return bytesValue(getCurrentValue(columnIndex));
}

Expand All @@ -304,6 +331,7 @@ private static byte[] bytesValue(Value val) {

@Override
public Date getDate(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return dateValue(getCurrentValue(columnIndex));
}

Expand All @@ -314,6 +342,7 @@ private static Date dateValue(Value value) throws SQLException {

@Override
public Time getTime(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return timeValue(getCurrentValue(columnIndex));
}

Expand All @@ -324,6 +353,7 @@ private static Time timeValue(Value value) throws SQLException {

@Override
public Timestamp getTimestamp(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return timestampValue(getCurrentValue(columnIndex));
}

Expand All @@ -332,8 +362,44 @@ private static Timestamp timestampValue(Value value) throws SQLException {
return dateTime == null ? null : Timestamp.valueOf(dateTime);
}

private static OffsetTime offsetTimeValue(Value value) throws SQLException {
String text = stringValue(value);
if (text == null) {
return null;
}
try {
return OffsetTime.parse(text);
} catch (DateTimeParseException e) {
throw new SQLException("Bad value for type OffsetTime: " + text, e);
}
}

private static OffsetDateTime offsetDateTimeValue(Value value) throws SQLException {
String text = stringValue(value);
if (text == null) {
return null;
}
try {
return OffsetDateTime.parse(sqlTimestampSeparator(text));
} catch (DateTimeParseException e) {
throw new SQLException("Bad value for type OffsetDateTime: " + text, e);
}
}

/**
* Accepts the SQLite text form with a space between the date and the time as well as the
* ISO-8601 form with {@code T}. {@link OffsetDateTime#parse(CharSequence)} accepts only the
* latter, and SQLite columns are commonly written with the former.
*/
private static String sqlTimestampSeparator(String text) {
return text.length() > 10 && text.charAt(10) == ' '
? text.substring(0, 10) + 'T' + text.substring(11)
: text;
}

@Override
public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return bigDecimalValue(getCurrentValue(columnIndex));
}

Expand Down Expand Up @@ -423,6 +489,12 @@ private static Object convertedObject(Value value, JdbcTypeMapping.JdbcType type
if (objectClass == Timestamp.class) {
return timestampValue(value);
}
if (objectClass == OffsetTime.class) {
return offsetTimeValue(value);
}
if (objectClass == OffsetDateTime.class) {
return offsetDateTimeValue(value);
}
if (objectClass == Clob.class) {
return clobValue(value);
}
Expand Down Expand Up @@ -738,13 +810,15 @@ public void setFetchSize(int rows) throws SQLException {
}
@Override public Ref getRef(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public Blob getBlob(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return blobValue(getCurrentValue(columnIndex));
}
private static Blob blobValue(Value value) {
byte[] bytes = bytesValue(value);
return bytes == null ? null : new LibsqlBlob(bytes);
}
@Override public Clob getClob(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return clobValue(getCurrentValue(columnIndex));
}
private static Clob clobValue(Value value) {
Expand All @@ -768,6 +842,7 @@ private static Clob clobValue(Value value) {
@Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(columnLabel); }
@Override
public URL getURL(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return urlValue(getCurrentValue(columnIndex));
}

Expand All @@ -794,6 +869,7 @@ private static URL urlValue(Value value) throws SQLException {
@Override public void updateArray(int columnIndex, Array x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateArray(String columnLabel, Array x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public RowId getRowId(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return rowIdValue(getCurrentValue(columnIndex));
}
private static RowId rowIdValue(Value value) {
Expand All @@ -809,6 +885,7 @@ private static RowId rowIdValue(Value value) {
@Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public NClob getNClob(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return nclobValue(getCurrentValue(columnIndex));
}
private static NClob nclobValue(Value value) {
Expand All @@ -817,6 +894,7 @@ private static NClob nclobValue(Value value) {
}
@Override public NClob getNClob(String columnLabel) throws SQLException { return getNClob(findColumn(columnLabel)); }
@Override public SQLXML getSQLXML(int columnIndex) throws SQLException {
rejectUnlistedOffsetGetter(columnIndex);
return sqlxmlValue(getCurrentValue(columnIndex));
}
private static SQLXML sqlxmlValue(Value value) {
Expand Down Expand Up @@ -902,7 +980,7 @@ private static SQLXML sqlxmlValue(Value value) {
* </tr>
* <tr>
* <td>{@code Date}, {@code Time}, {@code Timestamp}, {@code LocalDate}, {@code LocalTime},
* {@code LocalDateTime}</td>
* {@code LocalDateTime}, {@code OffsetTime}, {@code OffsetDateTime}</td>
* <td>epoch millis</td><td>Julian day</td><td>date/time text</td><td>bad value</td>
* <td>{@code null}</td>
* </tr>
Expand All @@ -916,8 +994,10 @@ private static SQLXML sqlxmlValue(Value value) {
* Julian days, and TEXT values use the SQLite date and time formats, including a bare date and
* a bare time. A bare time uses SQLite's 2000-01-01 default date. The SQLite TEXT format is
* {@code yyyy-MM-dd HH:mm:ss.SSS}, with optional fractional digits and an optional time zone
* suffix. All six date and time targets are built from the same storage-class value, so
* {@code getObject(column, LocalDate.class)} and {@code getDate(column)} agree.
* suffix. {@code OffsetTime} and {@code OffsetDateTime} read ISO-8601 offset text, accepting
* either a {@code T} or a space between the date and the time. All eight date and time targets
* are built from the same storage-class value, so {@code getObject(column, LocalDate.class)}
* and {@code getDate(column)} agree.
*
* <p>{@code Short} and {@code Byte} are rejected rather than narrowed from the numeric
* targets, preserving the sqlite-jdbc 3.53.4.0 contract.
Expand Down Expand Up @@ -962,6 +1042,12 @@ public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
if (type == LocalDateTime.class) {
return type.cast(getLocalDateTime(columnIndex));
}
if (type == OffsetTime.class) {
return type.cast(offsetTimeValue(getCurrentValue(columnIndex)));
}
if (type == OffsetDateTime.class) {
return type.cast(offsetDateTimeValue(getCurrentValue(columnIndex)));
}

Value stored = getCurrentValue(columnIndex);
boolean numeric = stored instanceof Value.Integer || stored instanceof Value.Float;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -568,6 +571,51 @@ void b3RowIdComparesAndCopiesItsBytes() {
.isNotEqualTo(new LibsqlRowId(new byte[] {1, 3}));
}

// ---------------------------------------------------------------------
// PR-4 offset types: TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE.
// ---------------------------------------------------------------------

@Test
void b6DeclaredOffsetTypesReturnOffsetJavaTypes() throws SQLException {
LibsqlResultSet timeRow = oneRow(jdbcType("TIME_WITH_TIMEZONE"), Value.of("03:04:05+01:00"));
assertThat(timeRow.next()).isTrue();
assertThat(timeRow.getObject(1))
.isEqualTo(OffsetTime.of(3, 4, 5, 0, ZoneOffset.ofHours(1)));
assertThat(timeRow.getMetaData().getColumnType(1)).isEqualTo(Types.TIME_WITH_TIMEZONE);
assertThat(timeRow.getMetaData().getColumnClassName(1))
.isEqualTo(OffsetTime.class.getName());

LibsqlResultSet timestampRow = oneRow(
jdbcType("TIMESTAMP_WITH_TIMEZONE"), Value.of("2024-01-02 03:04:05+01:00"));
assertThat(timestampRow.next()).isTrue();
assertThat(timestampRow.getObject(1))
.isEqualTo(OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(1)));
assertThat(timestampRow.getMetaData().getColumnType(1))
.isEqualTo(Types.TIMESTAMP_WITH_TIMEZONE);
assertThat(timestampRow.getMetaData().getColumnClassName(1))
.isEqualTo(OffsetDateTime.class.getName());
}

@Test
void offsetTextParsesZuluAndPlusOneAndRejectsMalformedText() throws SQLException {
LibsqlResultSet timeRow = oneRow(jdbcType("TIME_WITH_TIMEZONE"), Value.of("03:04:05Z"));
assertThat(timeRow.next()).isTrue();
assertThat(timeRow.getObject(1, OffsetTime.class))
.isEqualTo(OffsetTime.of(3, 4, 5, 0, ZoneOffset.UTC));

LibsqlResultSet timestampRow = oneRow(
jdbcType("TIMESTAMP_WITH_TIMEZONE"), Value.of("2024-01-02T03:04:05+01:00"));
assertThat(timestampRow.next()).isTrue();
assertThat(timestampRow.getObject(1, OffsetDateTime.class))
.isEqualTo(OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(1)));

LibsqlResultSet malformed = oneRow(jdbcType("TIME_WITH_TIMEZONE"), Value.of("not-a-time"));
assertThat(malformed.next()).isTrue();
assertThatThrownBy(() -> malformed.getObject(1, OffsetTime.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("OffsetTime");
}

// ---------------------------------------------------------------------
// Fixtures and helpers.
// ---------------------------------------------------------------------
Expand Down
Loading