From bb72ef6c06d65aa836837da32b82009678ed1edd Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:58:00 +0100 Subject: [PATCH 1/4] feat(jdbc): record n/a types and table b-6 getter support --- .../magnusp/libsql/jdbc/JdbcTypeMapping.java | 205 +++++++++++++++++- 1 file changed, 201 insertions(+), 4 deletions(-) diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java index 214369c..798e127 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java @@ -5,20 +5,27 @@ import java.math.BigDecimal; import java.net.URL; +import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; import java.sql.NClob; +import java.sql.Ref; import java.sql.RowId; import java.sql.SQLXML; +import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.time.OffsetDateTime; import java.time.OffsetTime; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; /** * Resolves the JDBC type of a result-set column from its declared type. @@ -33,6 +40,12 @@ * {@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. + * + *

{@code ARRAY}, {@code STRUCT}, {@code REF}, {@code JAVA_OBJECT}, and {@code DISTINCT} are + * recorded as {@link Support#UNSUPPORTED}. SQLite has no interoperable representation for them, so + * the driver rejects the declared type instead of returning a value of the wrong Java type. The + * Table B-6 getter matrix is stored on each supported entry as a bitmask, which keeps a declared + * column's getter check to one field read and one mask test. */ final class JdbcTypeMapping { @@ -50,7 +63,58 @@ enum StorageClass { NULL } - record JdbcType(int sqlType, Class javaClass, Class objectClass, StorageClass storage, Support support) {} + /** One ResultSet getter, named for Table B-6 and for an error message. */ + enum Getter { + GET_BYTE("getByte"), + GET_SHORT("getShort"), + GET_INT("getInt"), + GET_LONG("getLong"), + GET_FLOAT("getFloat"), + GET_DOUBLE("getDouble"), + GET_BIG_DECIMAL("getBigDecimal"), + GET_BOOLEAN("getBoolean"), + GET_STRING("getString"), + GET_N_STRING("getNString"), + GET_BYTES("getBytes"), + GET_DATE("getDate"), + GET_TIME("getTime"), + GET_TIMESTAMP("getTimestamp"), + GET_ASCII_STREAM("getAsciiStream"), + GET_BINARY_STREAM("getBinaryStream"), + GET_CHARACTER_STREAM("getCharacterStream"), + GET_N_CHARACTER_STREAM("getNCharacterStream"), + GET_CLOB("getClob"), + GET_N_CLOB("getNClob"), + GET_BLOB("getBlob"), + GET_ARRAY("getArray"), + GET_REF("getRef"), + GET_URL("getURL"), + GET_OBJECT("getObject"), + GET_ROW_ID("getRowId"), + GET_SQLXML("getSQLXML"); + + private final String methodName; + + Getter(String methodName) { + this.methodName = methodName; + } + + String methodName() { + return methodName; + } + + int mask() { + return 1 << ordinal(); + } + } + + record JdbcType( + int sqlType, + Class javaClass, + Class objectClass, + StorageClass storage, + Support support, + int getterMask) {} private static final JdbcType INTEGER_FALLBACK = fallback(Types.INTEGER, int.class, Integer.class, StorageClass.INTEGER); @@ -65,6 +129,7 @@ record JdbcType(int sqlType, Class javaClass, Class objectClass, StorageCl private static final JdbcType NULL_FALLBACK = fallback(Types.NUMERIC, Object.class, Object.class, StorageClass.NULL); + private static final Map> GETTER_SUPPORT = getterSupport(); private static final Map DECLARED = declaredTypeRegistry(); private static final Map ALIASES = aliasTypes(); @@ -98,6 +163,34 @@ private static JdbcType declared(Col col) { return type != null ? type : ALIASES.get(name); } + static boolean isUnsupported(JdbcType type) { + return type.support() == Support.UNSUPPORTED; + } + + static boolean supports(JdbcType type, Getter getter) { + return (type.getterMask() & getter.mask()) != 0; + } + + /** The JDBC type names this driver reads, for the compliance profile. */ + static SortedSet supportedTypeNames() { + return typeNames(Support.SUPPORTED); + } + + /** The JDBC type names this driver records as N/A, for the compliance profile. */ + static SortedSet unsupportedTypeNames() { + return typeNames(Support.UNSUPPORTED); + } + + private static SortedSet typeNames(Support support) { + SortedSet names = new TreeSet<>(); + DECLARED.forEach((name, type) -> { + if (type.support() == support) { + names.add(name); + } + }); + return java.util.Collections.unmodifiableSortedSet(names); + } + static JdbcType fallback(Value value) { return switch (storageClass(value)) { case INTEGER -> integerFallback(value); @@ -134,7 +227,91 @@ static long integer(Value value) { } private static JdbcType fallback(int sqlType, Class javaClass, Class objectClass, StorageClass storage) { - return new JdbcType(sqlType, javaClass, objectClass, storage, Support.FALLBACK); + return new JdbcType(sqlType, javaClass, objectClass, storage, Support.FALLBACK, 0); + } + + /** + * The Table B-6 supported JDBC types for each getter. A declared column resolves through this + * table; an expression column has no declared type and keeps the permissive storage fallback. + */ + private static Map> getterSupport() { + Set numeric = Set.of( + "TINYINT", + "SMALLINT", + "INTEGER", + "BIGINT", + "REAL", + "FLOAT", + "DOUBLE", + "DECIMAL", + "NUMERIC", + "BIT", + "BOOLEAN", + "CHAR", + "VARCHAR", + "LONGVARCHAR"); + Set numericWithRowId = union(numeric, "ROWID"); + Set strings = union(numeric, "BINARY", "VARBINARY", "LONGVARBINARY", "DATE", "TIME", + "TIMESTAMP", "DATALINK", "NCHAR", "NVARCHAR", "LONGNVARCHAR"); + Set binary = Set.of("BINARY", "VARBINARY", "LONGVARBINARY"); + Set date = Set.of("CHAR", "VARCHAR", "LONGVARCHAR", "DATE", "TIMESTAMP"); + Set time = Set.of("CHAR", "VARCHAR", "LONGVARCHAR", "TIME", "TIMESTAMP"); + Set timestamp = Set.of("CHAR", "VARCHAR", "LONGVARCHAR", "DATE", "TIME", "TIMESTAMP"); + Set asciiStream = union(binary, "CHAR", "VARCHAR", "LONGVARCHAR", "CLOB", "NCLOB"); + Set characterStream = union( + binary, "CHAR", "VARCHAR", "LONGVARCHAR", "CLOB", "NCHAR", "NVARCHAR", "LONGNVARCHAR", + "NCLOB", "SQLXML"); + Set clob = Set.of("CLOB", "NCLOB"); + Set all = union( + strings, "CLOB", "BLOB", "ARRAY", "STRUCT", "REF", "DATALINK", "JAVA_OBJECT", + "ROWID", "NCLOB", "SQLXML", "TIME_WITH_TIMEZONE", "TIMESTAMP_WITH_TIMEZONE"); + + Map> support = new EnumMap<>(Getter.class); + support.put(Getter.GET_BYTE, numericWithRowId); + support.put(Getter.GET_SHORT, numeric); + support.put(Getter.GET_INT, numeric); + support.put(Getter.GET_LONG, numeric); + support.put(Getter.GET_FLOAT, numeric); + support.put(Getter.GET_DOUBLE, numeric); + support.put(Getter.GET_BIG_DECIMAL, numeric); + support.put(Getter.GET_BOOLEAN, numeric); + support.put(Getter.GET_STRING, strings); + support.put(Getter.GET_N_STRING, strings); + support.put(Getter.GET_BYTES, binary); + support.put(Getter.GET_DATE, date); + support.put(Getter.GET_TIME, time); + support.put(Getter.GET_TIMESTAMP, timestamp); + support.put(Getter.GET_ASCII_STREAM, asciiStream); + support.put(Getter.GET_BINARY_STREAM, binary); + support.put(Getter.GET_CHARACTER_STREAM, characterStream); + support.put(Getter.GET_N_CHARACTER_STREAM, characterStream); + support.put(Getter.GET_CLOB, clob); + support.put(Getter.GET_N_CLOB, clob); + support.put(Getter.GET_BLOB, Set.of("BLOB")); + support.put(Getter.GET_ARRAY, Set.of("ARRAY")); + support.put(Getter.GET_REF, Set.of("REF")); + support.put(Getter.GET_URL, Set.of("DATALINK")); + // B-6 omits DISTINCT from getObject; the other four N/A types are rejected before the mask. + support.put(Getter.GET_OBJECT, all); + support.put(Getter.GET_ROW_ID, Set.of("ROWID")); + support.put(Getter.GET_SQLXML, Set.of("SQLXML")); + return Map.copyOf(support); + } + + private static Set union(Set base, String... extra) { + Set result = new java.util.HashSet<>(base); + result.addAll(List.of(extra)); + return Set.copyOf(result); + } + + private static int getterMask(String typeName) { + int mask = 0; + for (Getter getter : Getter.values()) { + if (GETTER_SUPPORT.get(getter).contains(typeName)) { + mask |= getter.mask(); + } + } + return mask; } private static Map declaredTypeRegistry() { @@ -179,12 +356,32 @@ private static Map declaredTypeRegistry() { entry("SQLXML", Types.SQLXML, SQLXML.class, SQLXML.class, StorageClass.TEXT), entry("ROWID", Types.ROWID, RowId.class, RowId.class, StorageClass.TEXT), entry("BLOB", Types.BLOB, Blob.class, Blob.class, StorageClass.BLOB), - entry("DATALINK", Types.DATALINK, URL.class, URL.class, StorageClass.TEXT)); + entry("DATALINK", Types.DATALINK, URL.class, URL.class, StorageClass.TEXT), + unsupported("ARRAY", Types.ARRAY, Array.class, Array.class, StorageClass.TEXT), + unsupported("STRUCT", Types.STRUCT, Struct.class, Struct.class, StorageClass.TEXT), + unsupported("REF", Types.REF, Ref.class, Ref.class, StorageClass.TEXT), + unsupported("JAVA_OBJECT", Types.JAVA_OBJECT, Object.class, Object.class, StorageClass.TEXT), + unsupported("DISTINCT", Types.DISTINCT, Object.class, Object.class, StorageClass.TEXT)); } private static Map.Entry entry( String name, int sqlType, Class javaClass, Class objectClass, StorageClass storage) { - return Map.entry(name, new JdbcType(sqlType, javaClass, objectClass, storage, Support.SUPPORTED)); + return Map.entry( + name, + new JdbcType( + sqlType, + javaClass, + objectClass, + storage, + Support.SUPPORTED, + getterMask(name))); + } + + private static Map.Entry unsupported( + String name, int sqlType, Class javaClass, Class objectClass, StorageClass storage) { + return Map.entry( + name, + new JdbcType(sqlType, javaClass, objectClass, storage, Support.UNSUPPORTED, 0)); } /** From ec28549231fee7b33c670aa34c66054cde390384 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:58:00 +0100 Subject: [PATCH 2/4] feat(jdbc): reject unsupported reads and unlisted getter conversions --- .../magnusp/libsql/jdbc/LibsqlResultSet.java | 144 +++++++++++++----- .../libsql/jdbc/Jdbc4AppendixBReadTest.java | 39 +++++ 2 files changed, 146 insertions(+), 37 deletions(-) diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java index ee0c239..0197a48 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -121,19 +121,27 @@ private Value getCurrentValue(int columnIndex) throws SQLException { } /** - * 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. + * Enforces the Table B-6 matrix for a declared column. A column with no {@code decltype} keeps + * the permissive storage fallback. An N/A declared type is rejected through every getter, and a + * getter that Table B-6 does not list for the declared type is rejected as well. */ - private void rejectUnlistedOffsetGetter(int columnIndex) throws SQLException { + private void checkGetter(int columnIndex, JdbcTypeMapping.Getter getter) 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)) { + if (type == null) { + return; + } + if (JdbcTypeMapping.isUnsupported(type)) { + throw new SQLFeatureNotSupportedException( + "Declared " + JdbcTypeMapping.sqlTypeName(type.sqlType()) + " is not supported"); + } + if (!JdbcTypeMapping.supports(type, getter)) { throw new SQLFeatureNotSupportedException( - "Declared " + JdbcTypeMapping.sqlTypeName(type.sqlType()) + " columns support only getObject"); + getter.methodName() + " does not support declared " + + JdbcTypeMapping.sqlTypeName(type.sqlType())); } } @@ -210,7 +218,7 @@ public boolean wasNull() throws SQLException { */ @Override public String getString(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_STRING); return stringValue(getCurrentValue(columnIndex)); } @@ -233,7 +241,7 @@ private static String stringValue(Value val) { */ @Override public boolean getBoolean(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BOOLEAN); return booleanValue(getCurrentValue(columnIndex)); } @@ -252,36 +260,83 @@ private static boolean booleanValue(Value value) { @Override public byte getByte(int columnIndex) throws SQLException { - return (byte) getLong(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BYTE); + return (byte) longValue(columnIndex); } @Override public short getShort(int columnIndex) throws SQLException { - return (short) getLong(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_SHORT); + return (short) longValue(columnIndex); } @Override public int getInt(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); - return (int) toLong(getCurrentValue(columnIndex)); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_INT); + return (int) longValue(columnIndex); } @Override public long getLong(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); - return toLong(getCurrentValue(columnIndex)); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_LONG); + return longValue(columnIndex); } @Override public float getFloat(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); - return (float) toDouble(getCurrentValue(columnIndex)); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_FLOAT); + return (float) doubleValue(columnIndex); } @Override public double getDouble(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); - return toDouble(getCurrentValue(columnIndex)); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_DOUBLE); + return doubleValue(columnIndex); + } + + /** + * Reads a declared column strictly: a text or blob value that is not a number is an error + * rather than SQLite's zero coercion. An expression column has no declared type and keeps the + * permissive storage fallback that xerial users rely on. + */ + private long longValue(int columnIndex) throws SQLException { + Value value = getCurrentValue(columnIndex); + if (declaredTypes[columnIndex - 1] == null) { + return toLong(value); + } + return switch (value) { + case Value.Text text -> strictLong(text.value()); + case Value.Blob blob -> strictLong(SqliteNumericText.blobAsText(blob.bytes())); + default -> toLong(value); + }; + } + + private double doubleValue(int columnIndex) throws SQLException { + Value value = getCurrentValue(columnIndex); + if (declaredTypes[columnIndex - 1] == null) { + return toDouble(value); + } + return switch (value) { + case Value.Text text -> strictDouble(text.value()); + case Value.Blob blob -> strictDouble(SqliteNumericText.blobAsText(blob.bytes())); + default -> toDouble(value); + }; + } + + private static long strictLong(String text) throws SQLException { + try { + return new BigDecimal(text.trim()).longValue(); + } catch (NumberFormatException e) { + throw new SQLException("Bad value for type numeric: " + text, e); + } + } + + private static double strictDouble(String text) throws SQLException { + try { + return Double.parseDouble(text.trim()); + } catch (NumberFormatException e) { + throw new SQLException("Bad value for type numeric: " + text, e); + } } private static long toLong(Value val) { @@ -315,7 +370,7 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException @Override public byte[] getBytes(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BYTES); return bytesValue(getCurrentValue(columnIndex)); } @@ -331,7 +386,7 @@ private static byte[] bytesValue(Value val) { @Override public Date getDate(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_DATE); return dateValue(getCurrentValue(columnIndex)); } @@ -342,7 +397,7 @@ private static Date dateValue(Value value) throws SQLException { @Override public Time getTime(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_TIME); return timeValue(getCurrentValue(columnIndex)); } @@ -353,7 +408,7 @@ private static Time timeValue(Value value) throws SQLException { @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_TIMESTAMP); return timestampValue(getCurrentValue(columnIndex)); } @@ -399,7 +454,7 @@ private static String sqlTimestampSeparator(String text) { @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BIG_DECIMAL); return bigDecimalValue(getCurrentValue(columnIndex)); } @@ -417,6 +472,7 @@ private static BigDecimal bigDecimalValue(Value value) throws SQLException { @Override public Object getObject(int columnIndex) throws SQLException { + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_OBJECT); Value value = getCurrentValue(columnIndex); if (value instanceof Value.Null) { return null; @@ -710,14 +766,16 @@ public boolean isLast() throws SQLException { @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { - String text = getString(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ASCII_STREAM); + String text = stringValue(getCurrentValue(columnIndex)); return text == null ? null : new ByteArrayInputStream(text.getBytes(java.nio.charset.StandardCharsets.US_ASCII)); } @Override @Deprecated @SuppressWarnings("deprecation") public InputStream getUnicodeStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } @Override public InputStream getBinaryStream(int columnIndex) throws SQLException { - byte[] bytes = getBytes(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BINARY_STREAM); + byte[] bytes = bytesValue(getCurrentValue(columnIndex)); return bytes == null ? null : new ByteArrayInputStream(bytes); } @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { return getAsciiStream(findColumn(columnLabel)); } @@ -727,7 +785,8 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { @Override public void clearWarnings() throws SQLException {} @Override public String getCursorName() throws SQLException { return null; } @Override public Reader getCharacterStream(int columnIndex) throws SQLException { - String string = getString(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_CHARACTER_STREAM); + String string = stringValue(getCurrentValue(columnIndex)); return string == null ? null : new StringReader(string); } @Override public Reader getCharacterStream(String columnLabel) throws SQLException { return getCharacterStream(findColumn(columnLabel)); } @@ -808,9 +867,12 @@ public void setFetchSize(int rows) throws SQLException { rejectTypeMap(map); return getObject(columnIndex); } - @Override public Ref getRef(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } + @Override public Ref getRef(int columnIndex) throws SQLException { + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_REF); + throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); + } @Override public Blob getBlob(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BLOB); return blobValue(getCurrentValue(columnIndex)); } private static Blob blobValue(Value value) { @@ -818,14 +880,17 @@ private static Blob blobValue(Value value) { return bytes == null ? null : new LibsqlBlob(bytes); } @Override public Clob getClob(int columnIndex) throws SQLException { - rejectUnlistedOffsetGetter(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_CLOB); return clobValue(getCurrentValue(columnIndex)); } private static Clob clobValue(Value value) { String string = stringValue(value); return string == null ? null : new LibsqlClob(string); } - @Override public Array getArray(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } + @Override public Array getArray(int columnIndex) throws SQLException { + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ARRAY); + throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); + } @Override public Object getObject(String columnLabel, Map> map) throws SQLException { rejectTypeMap(map); return getObject(columnLabel); @@ -842,7 +907,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); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_URL); return urlValue(getCurrentValue(columnIndex)); } @@ -869,7 +934,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); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ROW_ID); return rowIdValue(getCurrentValue(columnIndex)); } private static RowId rowIdValue(Value value) { @@ -885,7 +950,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); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_CLOB); return nclobValue(getCurrentValue(columnIndex)); } private static NClob nclobValue(Value value) { @@ -894,7 +959,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); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_SQLXML); return sqlxmlValue(getCurrentValue(columnIndex)); } private static SQLXML sqlxmlValue(Value value) { @@ -904,10 +969,14 @@ private static SQLXML sqlxmlValue(Value value) { @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { return getSQLXML(findColumn(columnLabel)); } @Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } @Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } - @Override public String getNString(int columnIndex) throws SQLException { return getString(columnIndex); } - @Override public String getNString(String columnLabel) throws SQLException { return getString(columnLabel); } + @Override public String getNString(int columnIndex) throws SQLException { + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_STRING); + return stringValue(getCurrentValue(columnIndex)); + } + @Override public String getNString(String columnLabel) throws SQLException { return getNString(findColumn(columnLabel)); } @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { - String text = getNString(columnIndex); + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_CHARACTER_STREAM); + String text = stringValue(getCurrentValue(columnIndex)); return text == null ? null : new StringReader(text); } @Override public Reader getNCharacterStream(String columnLabel) throws SQLException { return getNCharacterStream(findColumn(columnLabel)); } @@ -1012,6 +1081,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { if (type == null) { throw new SQLException("requested type cannot be null"); } + checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_OBJECT); if (type == String.class) { return type.cast(getString(columnIndex)); } diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java index 8c3208e..e91bef3 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java @@ -24,6 +24,7 @@ import java.sql.ResultSet; import java.sql.RowId; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import java.sql.Struct; import java.sql.Time; @@ -138,6 +139,14 @@ private static JdbcType jdbc( return new JdbcType(name, sqlType, b1JavaType, b3ObjectType, sample, true); } + /** The N/A policy: these declared types throw SQLFeatureNotSupportedException. */ + private static final Set NA_TYPES = + set("ARRAY", "STRUCT", "REF", "JAVA_OBJECT", "DISTINCT"); + + private static boolean isNa(JdbcType type) { + return NA_TYPES.contains(type.name()); + } + // --------------------------------------------------------------------- // Table B-1: the natural getter for each JDBC type returns the B-1 Java type. // --------------------------------------------------------------------- @@ -321,6 +330,13 @@ void b1NaturalGetterReturnsDeclaredJavaType(JdbcType type, Accessor accessor) th LibsqlResultSet resultSet = oneRow(type, type.sample()); assertThat(resultSet.next()).isTrue(); + if (isNa(type)) { + assertThatThrownBy(() -> accessor.reader().read(resultSet, 1)) + .as("B-1: %s is N/A and must throw SQLFeatureNotSupportedException", type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } + Object value; try { value = accessor.reader().read(resultSet, 1); @@ -348,6 +364,13 @@ void b3GetObjectReturnsDeclaredObjectType(JdbcType type) throws SQLException { LibsqlResultSet resultSet = oneRow(type, type.sample()); assertThat(resultSet.next()).isTrue(); + if (isNa(type)) { + assertThatThrownBy(() -> resultSet.getObject(1)) + .as("B-3: %s is N/A and must throw SQLFeatureNotSupportedException", type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } + assertThat(resultSet.getObject(1)) .as("B-3: %s must read back as %s", type.name(), type.b3ObjectType()) .isInstanceOf(type.b3ObjectType()); @@ -361,6 +384,13 @@ void b3GetObjectReturnsNullForNull() throws SQLException { } LibsqlResultSet resultSet = oneRow(type, new Value.Null()); assertThat(resultSet.next()).isTrue(); + if (isNa(type)) { + assertThatThrownBy(() -> resultSet.getObject(1)) + .as("B-3: NULL %s is N/A and must throw SQLFeatureNotSupportedException", + type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + continue; + } assertThat(resultSet.getObject(1)).as("B-3: NULL %s", type.name()).isNull(); assertThat(resultSet.wasNull()).as("B-3: wasNull for NULL %s", type.name()).isTrue(); } @@ -387,6 +417,15 @@ void b6GetterHonoursSupportedJdbcTypes(Accessor getter, JdbcType type, boolean s LibsqlResultSet resultSet = oneRow(type, fixtureFor(getter, type)); assertThat(resultSet.next()).isTrue(); + if (isNa(type)) { + // The policy answer for an unrepresentable type is an N/A exception, not a wrong Java + // type. This holds whether Table B-6 lists the cell or not. + assertThatThrownBy(() -> getter.reader().read(resultSet, 1)) + .as("B-6: %s is N/A and must throw SQLFeatureNotSupportedException", type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } + if (supported) { Object value; try { From 2677afb18120c3b5ab19bd83c5fd34f843947609 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:58:00 +0100 Subject: [PATCH 3/4] feat(jdbc): reject unsupported setobject targets --- .../libsql/jdbc/JdbcValueConverter.java | 83 ++++--------- .../jdbc/Jdbc4AppendixBProfileTest.java | 112 ++++++++++++++++++ .../jdbc/Jdbc4AppendixBSetObjectTest.java | 37 +++++- 3 files changed, 167 insertions(+), 65 deletions(-) create mode 100644 libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBProfileTest.java diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcValueConverter.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcValueConverter.java index d639305..5a7c2de 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcValueConverter.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcValueConverter.java @@ -122,9 +122,25 @@ static Value toValue(Object value) throws SQLException { if (value instanceof Blob blob) { return Value.of(binaryBytes(blob)); } - // Table B-4 maps every other Java class to JAVA_OBJECT. PR-5 decides whether those types - // are supported; until then they keep the sqlite-jdbc text binding. - return Value.of(value.toString()); + if (value instanceof URL || value instanceof RowId || value instanceof SQLXML) { + // Table B-4 maps these to DATALINK, ROWID and SQLXML, which the driver supports. + return Value.of(value.toString()); + } + // Table B-4 maps Array, Struct, Ref and every other class to an N/A JDBC type. + throw unsupported(value, unsupportedSourceType(value)); + } + + private static int unsupportedSourceType(Object value) { + if (value instanceof Array) { + return Types.ARRAY; + } + if (value instanceof Struct) { + return Types.STRUCT; + } + if (value instanceof Ref) { + return Types.REF; + } + return Types.JAVA_OBJECT; } /** Converts a value to the target JDBC type, then to the Hrana value of that type. */ @@ -153,15 +169,12 @@ static Value toValue(Object value, int targetSqlType) throws SQLException { case Types.TIME -> Value.of(timeText(value, targetSqlType)); case Types.TIMESTAMP -> Value.of(timestampText(value, targetSqlType)); case Types.DATALINK -> Value.of(datalinkText(value)); - case Types.ARRAY -> Value.of(arrayText(value)); - case Types.STRUCT -> Value.of(structText(value)); - case Types.REF -> Value.of(refText(value)); - case Types.JAVA_OBJECT -> Value.of(javaObjectText(value)); + case Types.ARRAY, Types.STRUCT, Types.REF, Types.JAVA_OBJECT, Types.DISTINCT -> + throw unsupported(value, targetSqlType); case Types.ROWID -> Value.of(rowIdText(value)); case Types.SQLXML -> Value.of(sqlXmlText(value)); case Types.TIME_WITH_TIMEZONE -> Value.of(offsetTimeText(value)); case Types.TIMESTAMP_WITH_TIMEZONE -> Value.of(offsetDateTimeText(value)); - case Types.DISTINCT -> Value.of(value.toString()); default -> throw unsupported(value, targetSqlType); }; } @@ -368,31 +381,6 @@ private static String datalinkText(Object value) throws SQLException { throw unsupported(value, Types.DATALINK); } - private static String arrayText(Object value) throws SQLException { - if (value instanceof Array array) { - return array.toString(); - } - // Table B-5 lists ARRAY for Calendar and java.util.Date, but not for their SQL subclasses. - if (value instanceof Calendar || value.getClass() == java.util.Date.class) { - return value.toString(); - } - throw unsupported(value, Types.ARRAY); - } - - private static String structText(Object value) throws SQLException { - if (value instanceof Struct struct) { - return struct.toString(); - } - throw unsupported(value, Types.STRUCT); - } - - private static String refText(Object value) throws SQLException { - if (value instanceof Ref ref) { - return ref.toString(); - } - throw unsupported(value, Types.REF); - } - private static String rowIdText(Object value) throws SQLException { if (value instanceof RowId rowId) { return rowId.toString(); @@ -407,13 +395,6 @@ private static String sqlXmlText(Object value) throws SQLException { throw unsupported(value, Types.SQLXML); } - private static String javaObjectText(Object value) throws SQLException { - if (knownSourceFamily(value)) { - throw unsupported(value, Types.JAVA_OBJECT); - } - return value.toString(); - } - private static String offsetTimeText(Object value) throws SQLException { if (value instanceof OffsetTime time) { return time.toString(); @@ -431,28 +412,6 @@ private static String offsetDateTimeText(Object value) throws SQLException { throw unsupported(value, Types.TIMESTAMP_WITH_TIMEZONE); } - private static boolean knownSourceFamily(Object value) { - return value instanceof String - || value instanceof Number - || value instanceof Boolean - || value instanceof byte[] - || value instanceof java.util.Date - || value instanceof Calendar - || value instanceof LocalDate - || value instanceof LocalTime - || value instanceof LocalDateTime - || value instanceof OffsetTime - || value instanceof OffsetDateTime - || value instanceof Clob - || value instanceof Blob - || value instanceof Array - || value instanceof Struct - || value instanceof Ref - || value instanceof URL - || value instanceof RowId - || value instanceof SQLXML; - } - private static LocalDateTime localDateTime(java.time.Instant instant) { return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); } diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBProfileTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBProfileTest.java new file mode 100644 index 0000000..8daa230 --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBProfileTest.java @@ -0,0 +1,112 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.Row; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import com.github.magnusp.libsql.hrana.codec.Value; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * The JDBC 4.3 Appendix B compliance profile for the unrepresentable types. + * + *

This is the policy claim, stated once: {@code ARRAY}, {@code STRUCT}, {@code REF}, {@code + * JAVA_OBJECT}, and {@code DISTINCT} are N/A and throw {@link SQLFeatureNotSupportedException}. + * Every other declared type the driver resolves is supported. An expression column has no declared + * type and keeps the permissive storage fallback. + */ +class Jdbc4AppendixBProfileTest { + + private static final Set NA_TYPES = + Set.of("ARRAY", "STRUCT", "REF", "JAVA_OBJECT", "DISTINCT"); + + private static final Set SUPPORTED_TYPES = Set.of( + "TINYINT", + "SMALLINT", + "INTEGER", + "BIGINT", + "REAL", + "FLOAT", + "DOUBLE", + "NUMERIC", + "DECIMAL", + "BIT", + "BOOLEAN", + "CHAR", + "VARCHAR", + "LONGVARCHAR", + "NCHAR", + "NVARCHAR", + "LONGNVARCHAR", + "BINARY", + "VARBINARY", + "LONGVARBINARY", + "DATE", + "TIME", + "TIMESTAMP", + "TIME_WITH_TIMEZONE", + "TIMESTAMP_WITH_TIMEZONE", + "CLOB", + "NCLOB", + "SQLXML", + "ROWID", + "BLOB", + "DATALINK"); + + @Test + void profileListsExactlyTheUnsupportedTypes() { + assertThat(JdbcTypeMapping.unsupportedTypeNames()) + .as("N/A JDBC types") + .containsExactlyInAnyOrderElementsOf(NA_TYPES); + } + + @Test + void profileListsExactlyTheSupportedTypes() { + assertThat(JdbcTypeMapping.supportedTypeNames()) + .as("supported JDBC types") + .containsExactlyInAnyOrderElementsOf(SUPPORTED_TYPES); + } + + @Test + void everyUnsupportedDeclaredTypeThrowsFeatureNotSupported() throws SQLException { + for (String type : NA_TYPES) { + LibsqlResultSet resultSet = oneRow(type, Value.of("42")); + assertThat(resultSet.next()).isTrue(); + assertThatThrownBy(() -> resultSet.getObject(1)) + .as("getObject on declared %s", type) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> resultSet.getArray(1)) + .as("getArray on declared %s", type) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> resultSet.getRef(1)) + .as("getRef on declared %s", type) + .isInstanceOf(SQLFeatureNotSupportedException.class); + } + } + + @Test + void expressionFallbackStaysPermissive() throws Exception { + LibsqlResultSet resultSet = oneRow(null, Value.of("not-a-number")); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getInt(1)) + .as("an expression column keeps SQLite's zero coercion") + .isZero(); + } + + private static LibsqlResultSet oneRow(String declaredType, Value value) { + return new LibsqlResultSet( + new LibsqlStatement(null), + new StmtResult( + List.of(new Col("c", declaredType)), + List.of(new Row(List.of(value))), + 0, + null)); + } +} diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java index d64cbc0..0ac7c6a 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java @@ -18,6 +18,7 @@ import java.sql.Ref; import java.sql.RowId; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import java.sql.Struct; import java.sql.Time; @@ -36,6 +37,7 @@ import java.util.TimeZone; import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.fail; @@ -92,7 +94,12 @@ record Target(String name, int sqlType) {} new Target("NCLOB", Types.NCLOB), new Target("SQLXML", Types.SQLXML), new Target("TIME_WITH_TIMEZONE", Types.TIME_WITH_TIMEZONE), - new Target("TIMESTAMP_WITH_TIMEZONE", Types.TIMESTAMP_WITH_TIMEZONE)); + new Target("TIMESTAMP_WITH_TIMEZONE", Types.TIMESTAMP_WITH_TIMEZONE), + new Target("DISTINCT", Types.DISTINCT)); + + /** The N/A policy: these target JDBC types are rejected by setObject. */ + private static final Set NA_TARGETS = Set.of( + Types.ARRAY, Types.STRUCT, Types.REF, Types.JAVA_OBJECT, Types.DISTINCT); record JavaRow(String name, Object value, Set targets) {} @@ -211,7 +218,7 @@ static Stream javaRows() { void b5SetObjectRejectsUnlistedTargetTypes(JavaRow row) { List checks = new ArrayList<>(); for (Target target : TARGETS) { - if (row.targets().contains(target.sqlType())) { + if (row.targets().contains(target.sqlType()) || NA_TARGETS.contains(target.sqlType())) { continue; } checks.add(() -> { @@ -232,7 +239,8 @@ void b5SetObjectRejectsUnlistedTargetTypes(JavaRow row) { void b5SetObjectAcceptsListedTargetTypes() throws SQLException { for (JavaRow row : ROWS) { for (Target target : TARGETS) { - if (!row.targets().contains(target.sqlType())) { + if (!row.targets().contains(target.sqlType()) + || NA_TARGETS.contains(target.sqlType())) { continue; } LibsqlPreparedStatement statement = new LibsqlPreparedStatement(null, "?"); @@ -241,6 +249,29 @@ void b5SetObjectAcceptsListedTargetTypes() throws SQLException { } } + @Test + void b5SetObjectRejectsUnsupportedTypesAndValues() { + for (Target target : TARGETS) { + if (!NA_TARGETS.contains(target.sqlType())) { + continue; + } + assertThatThrownBy(() -> new LibsqlPreparedStatement(null, "?") + .setObject(1, "42", target.sqlType())) + .as("B-5: setObject(%s) is N/A", target.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + } + + // Table B-4 maps these Java object types to the N/A JDBC types. + Object[] unsupportedValues = { + stub(Array.class), stub(Struct.class), stub(Ref.class), new Object() + }; + for (Object value : unsupportedValues) { + assertThatThrownBy(() -> new LibsqlPreparedStatement(null, "?").setObject(1, value)) + .as("B-4: setObject(%s) is N/A", value.getClass().getName()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + } + } + @Test void b5SetNullAcceptsEveryTargetType() throws SQLException { for (Target target : TARGETS) { From 068e91d9cfc87d18b4fe73d5c3506eabee484d52 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:58:00 +0100 Subject: [PATCH 4/4] test(jdbc): assert n/a rows against sqld --- .../itest/Jdbc4AppendixBTypeMappingIT.java | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java b/libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java index ba0e29c..49ab0c5 100644 --- a/libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java +++ b/libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java @@ -24,6 +24,7 @@ import java.sql.ResultSet; import java.sql.RowId; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import java.sql.Statement; import java.sql.Struct; @@ -51,6 +52,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.fail; @@ -140,6 +142,17 @@ private static JdbcType jdbc( return new JdbcType(name, sqlType, b1, b3, sample); } + /** The N/A policy: these declared and target types throw SQLFeatureNotSupportedException. */ + private static final Set NA_TYPES = + Set.of("ARRAY", "STRUCT", "REF", "JAVA_OBJECT", "DISTINCT"); + + private static final Set NA_TARGETS = + Set.of(Types.ARRAY, Types.STRUCT, Types.REF, Types.JAVA_OBJECT, Types.DISTINCT); + + /** The Table B-2/B-4 Java object types that map to an N/A JDBC type. */ + private static final Set NA_VALUES = + Set.of("java.sql.Array", "java.sql.Struct", "java.sql.Ref", "Java class"); + interface SqlRead { Object read(ResultSet resultSet, int column) throws SQLException; } @@ -190,7 +203,11 @@ void b1AndB3ReadPathAgainstRealDdl(JdbcType type) throws SQLException { "CREATE TABLE " + table + " (v \"" + type.name() + "\")"); try (PreparedStatement insert = connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) { - insert.setObject(1, type.sample()); + if (NA_TYPES.contains(type.name())) { + insert.setString(1, String.valueOf(type.sample())); + } else { + insert.setObject(1, type.sample()); + } insert.executeUpdate(); } try (Statement query = connection.createStatement(); @@ -200,6 +217,16 @@ void b1AndB3ReadPathAgainstRealDdl(JdbcType type) throws SQLException { .as("server reported declared type for %s", type.name()) .isEqualTo(type.name()); + if (NA_TYPES.contains(type.name())) { + assertThatThrownBy(() -> NATURAL_GETTER.get(type.name()).read(resultSet, 1)) + .as("B-1: %s is N/A", type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> resultSet.getObject(1)) + .as("B-3: %s is N/A", type.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } + List checks = new ArrayList<>(); checks.add(() -> { Object natural; @@ -285,6 +312,12 @@ void b6GetterReadsRecommendedTypeFromRealDdl(GetterCell cell) throws SQLExceptio try (Statement query = connection.createStatement(); ResultSet resultSet = query.executeQuery("SELECT v FROM " + table)) { assertThat(resultSet.next()).isTrue(); + if (NA_TYPES.contains(cell.declaredType())) { + assertThatThrownBy(() -> cell.reader().read(resultSet, 1)) + .as("B-6: %s on %s is N/A", cell.getter(), cell.declaredType()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } Object value; try { value = cell.reader().read(resultSet, 1); @@ -397,6 +430,12 @@ void b2StandardSettersMapJavaTypeToJdbcType(WriteRow row) throws SQLException { statement.execute("CREATE TABLE " + table + " (v \"" + row.targetName() + "\")"); try (PreparedStatement insert = connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) { + if (NA_VALUES.contains(row.name())) { + assertThatThrownBy(() -> bindB2(insert, row)) + .as("B-2: %s -> %s is N/A", row.name(), row.targetName()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } try { bindB2(insert, row); } catch (SQLException e) { @@ -537,6 +576,12 @@ void b4SetObjectMapsJavaObjectTypeToJdbcType(WriteRow row) throws SQLException { statement.execute("CREATE TABLE " + table + " (v \"" + row.targetName() + "\")"); try (PreparedStatement insert = connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) { + if (NA_VALUES.contains(row.name())) { + assertThatThrownBy(() -> insert.setObject(1, row.value())) + .as("B-4: %s -> %s is N/A", row.name(), row.targetName()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + return; + } insert.setObject(1, row.value()); insert.executeUpdate(); } @@ -686,7 +731,16 @@ void b5SetObjectConvertsToExplicitTargetType(JavaRow row) throws SQLException { + placeholders + ")")) { int parameter = 1; for (Target target : TARGETS) { - if (row.targets().contains(target.sqlType())) { + if (row.targets().contains(target.sqlType()) + && NA_TARGETS.contains(target.sqlType())) { + int index = parameter; + assertThatThrownBy( + () -> insert.setObject( + index, sourceFor(row, target), target.sqlType())) + .as("B-5: %s -> %s is N/A", row.name(), target.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class); + insert.setNull(parameter++, target.sqlType()); + } else if (row.targets().contains(target.sqlType())) { insert.setObject(parameter++, sourceFor(row, target), target.sqlType()); } else { insert.setNull(parameter++, target.sqlType()); @@ -708,6 +762,13 @@ void b5SetObjectConvertsToExplicitTargetType(JavaRow row) throws SQLException { if (!row.targets().contains(target.sqlType())) { continue; } + if (NA_TARGETS.contains(target.sqlType())) { + checks.add(() -> assertThatThrownBy( + () -> readFor(target).read(resultSet, valueColumn)) + .as("B-5: reading %s for %s is N/A", target.name(), row.name()) + .isInstanceOf(SQLFeatureNotSupportedException.class)); + continue; + } checks.add(() -> { Set storage = storageOf(target.name()); if (!storage.isEmpty()) {