From 4a359b0149e602db585b73d99cff53ca2496cc57 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:35:29 +0100 Subject: [PATCH] fix(jdbc): read declared columns through implemented getters Table B-6 was enforced as a rejection list, so getBytes on a declared BLOB threw SQLFeatureNotSupportedException. Treat the table as the required support matrix and let the stored value decide the conversion, matching sqlite-jdbc. Only the five N/A declared types remain rejected. The getter mask, Getter enum, and getterMask field were used only by the rejection, so remove them. --- AGENTS.md | 3 +- docs/jdbc-appendix-b-compliance.md | 65 ++++--- libsql-conformance/README.md | 8 +- .../libsql/conformance/XerialQuarantine.java | 5 - .../itest/Jdbc4AppendixBTypeMappingIT.java | 22 +++ .../magnusp/libsql/jdbc/JdbcTypeMapping.java | 160 ++---------------- .../magnusp/libsql/jdbc/LibsqlResultSet.java | 76 ++++----- .../libsql/jdbc/Jdbc4AppendixBReadTest.java | 97 ++++++++--- 8 files changed, 185 insertions(+), 251 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b4964c4..890d15b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,8 +138,9 @@ Use the absolute path shown. A relative `DOCKER_CONFIG` is resolved against the JDBC 4.3 Appendix B is the oracle for type mapping. The source is [`vendor/JDBC4-TYPE-MAPPING.md`](vendor/JDBC4-TYPE-MAPPING.md). -- When Appendix B and sqlite-jdbc disagree, Appendix B wins. A conformance test that encodes the xerial behavior is quarantined in `libsql-conformance`, not copied. +- When Appendix B defines a conversion or mapping and sqlite-jdbc disagrees, Appendix B wins. A conformance test that encodes the xerial behavior is quarantined in `libsql-conformance`, not copied. - Keep sqlite-jdbc parity only where Appendix B is silent. Parity never overrides a defined Appendix B conversion. +- Table B-6 is a required-support matrix, not a rejection list. A representable declared column is not rejected because of its declared type; the stored value decides the conversion, matching sqlite-jdbc. Reject a declared type only for the five N/A types. Unimplemented getters still throw for any column. - `ARRAY`, `STRUCT`, `REF`, `JAVA_OBJECT`, and `DISTINCT` are N/A. The driver throws `SQLFeatureNotSupportedException` and does not emulate them. - An expression column has no declared type. Its storage-class fallback is a documented exception in [`docs/jdbc-appendix-b-compliance.md`](docs/jdbc-appendix-b-compliance.md), not a supported Appendix B row. diff --git a/docs/jdbc-appendix-b-compliance.md b/docs/jdbc-appendix-b-compliance.md index e24bcc4..ee2ec81 100644 --- a/docs/jdbc-appendix-b-compliance.md +++ b/docs/jdbc-appendix-b-compliance.md @@ -202,37 +202,48 @@ Table B-5 is observed through `setObject(int, Object, int)`. A listed pair must ## Table B-6, ResultSet getter conversions -Table B-6 is observed through each getter against its recommended declared type. +Table B-6 is the matrix of conversions the driver must support, observed through each getter against its recommended declared type. The driver does not reject a conversion because the table omits it. A representable declared column is not rejected because of its declared type, and the stored value decides whether the conversion succeeds, matching sqlite-jdbc. The five N/A types are still rejected before the getter runs. | Appendix B row | Status | Reason | Test | |---|---|---|---| -| `getByte` | supported | Reads a numeric, boolean or text value as a byte. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getShort` | supported | Reads a numeric, boolean or text value as a short. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getInt` | supported | Reads a numeric, boolean or text value as an int. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getLong` | supported | Reads a numeric, boolean or text value as a long. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getFloat` | supported | Reads a numeric, boolean or text value as a float. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getDouble` | supported | Reads a numeric, boolean or text value as a double. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getBigDecimal` | supported | Reads a numeric, boolean or text value as BigDecimal. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getBoolean` | emulated | Reads integer 0 or 1, and the text true or false, as Boolean. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getString` | supported | Reads text and the listed scalar types as String. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getNString` | supported | Reads text and the listed scalar types as String. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getBytes` | supported | Reads binary values and the listed types as byte[]. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getDate` | emulated | Parses the supported text forms into java.sql.Date. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getTime` | emulated | Parses the supported text forms into java.sql.Time. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getTimestamp` | emulated | Parses the supported text forms into java.sql.Timestamp. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getAsciiStream` | emulated | Returns the text bytes as a US-ASCII stream. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getBinaryStream` | supported | Returns the blob bytes as a stream. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getCharacterStream` | emulated | Returns a reader over the stored text or bytes. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getNCharacterStream` | emulated | Returns a reader over the stored text or bytes. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getClob` | emulated | Wraps stored text in java.sql.Clob. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getNClob` | emulated | Wraps stored text in java.sql.NClob. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getBlob` | supported | Wraps stored bytes in java.sql.Blob. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getByte` | supported | Reads a numeric, boolean or text value as a byte. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getShort` | supported | Reads a numeric, boolean or text value as a short. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getInt` | supported | Reads a numeric, boolean or text value as an int. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getLong` | supported | Reads a numeric, boolean or text value as a long. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getFloat` | supported | Reads a numeric, boolean or text value as a float. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getDouble` | supported | Reads a numeric, boolean or text value as a double. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getBigDecimal` | supported | Reads a numeric, boolean or text value as BigDecimal. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getBoolean` | emulated | Reads integer 0 or 1, and the text true or false, as Boolean. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getString` | supported | Reads any stored value as String; blobs decode as UTF-8. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getNString` | supported | Reads any stored value as String; blobs decode as UTF-8. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getBytes` | supported | Reads any stored value as byte[]; text encodes as UTF-8. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getDate` | emulated | Parses the supported text forms into java.sql.Date. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getTime` | emulated | Parses the supported text forms into java.sql.Time. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getTimestamp` | emulated | Parses the supported text forms into java.sql.Timestamp. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getAsciiStream` | emulated | Returns any stored value as a US-ASCII stream. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getBinaryStream` | supported | Returns any stored value as a byte stream. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getCharacterStream` | emulated | Returns a reader over the stored text or bytes. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getNCharacterStream` | emulated | Returns a reader over the stored text or bytes. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getClob` | emulated | Wraps any stored value in java.sql.Clob. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getNClob` | emulated | Wraps any stored value in java.sql.NClob. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getBlob` | supported | Wraps any stored value in java.sql.Blob. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | | `getArray` | N/A | SQLite has no interoperable representation. The driver throws SQLFeatureNotSupportedException. | `Jdbc4AppendixBProfileTest#everyUnsupportedDeclaredTypeThrowsFeatureNotSupported` | | `getRef` | N/A | SQLite has no interoperable representation. The driver throws SQLFeatureNotSupportedException. | `Jdbc4AppendixBProfileTest#everyUnsupportedDeclaredTypeThrowsFeatureNotSupported` | -| `getURL` | emulated | Parses stored text into java.net.URL. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getObject` | supported | Returns the Table B-3 object type for each supported declared type. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getRowId` | emulated | Wraps stored text or bytes in java.sql.RowId. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | -| `getSQLXML` | emulated | Wraps stored text in java.sql.SQLXML. | `Jdbc4AppendixBReadTest#b6GetterHonoursSupportedJdbcTypes`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getURL` | emulated | Parses stored text into java.net.URL. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getObject` | supported | Returns the Table B-3 object type for each supported declared type. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getRowId` | emulated | Wraps any stored value in java.sql.RowId. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | +| `getSQLXML` | emulated | Wraps any stored value in java.sql.SQLXML. | `Jdbc4AppendixBReadTest#b6RequiredConversionsAndPermissiveReads`, `Jdbc4AppendixBTypeMappingIT#b6GetterReadsRecommendedTypeFromRealDdl` | + +### Read conversions beyond Table B-6 + +Table B-6 defines the conversions a driver must support; it does not require the driver to reject +the rest. The driver therefore does not reject a representable declared column because of its +declared type, and the stored value decides the conversion. A declared `BLOB` reads through +`getBytes`, `getString` and `getBinaryStream`, as it does in sqlite-jdbc. A conversion the stored +value cannot satisfy throws `SQLException`. Only the five N/A declared types are rejected because of +the declared type; unimplemented getters (`getArray`, `getRef`, `getUnicodeStream`) still throw +`SQLFeatureNotSupportedException` for any column. Regression coverage is +`Jdbc4AppendixBReadTest#b6UnlistedConversionsReadTheStoredValue`. ## Documented exception @@ -242,4 +253,4 @@ The fallback is covered by `Jdbc4AppendixBProfileTest#expressionFallbackStaysPer ## Conformance quarantines -The xerial conformance suite keeps its selected tests except for four methods whose expectations Appendix B replaces. The list and the reason for each one are in [`../libsql-conformance/README.md`](../libsql-conformance/README.md). They still run in the `xerial-control` profile. +The xerial conformance suite keeps its selected tests except for three methods whose expectations Appendix B replaces. The list and the reason for each one are in [`../libsql-conformance/README.md`](../libsql-conformance/README.md). They still run in the `xerial-control` profile. diff --git a/libsql-conformance/README.md b/libsql-conformance/README.md index 8546c45..4b67036 100644 --- a/libsql-conformance/README.md +++ b/libsql-conformance/README.md @@ -47,13 +47,13 @@ with: ./mvnw -pl libsql-conformance -am verify ``` -Last green result: 270 tests, 7 skipped, with `SavepointTest` included. The -count excludes the four Appendix B quarantines below and includes the two +Last green result: 271 tests, 7 skipped, with `SavepointTest` included. The +count excludes the three Appendix B quarantines below and includes the two harness tests for the quarantine. ### Appendix B quarantines -JDBC 4.3 Appendix B is the oracle. Four selected xerial test methods assert +JDBC 4.3 Appendix B is the oracle. Three selected xerial test methods assert behavior that Appendix B replaces. They stay in the vendored tree and still run in the `xerial-control` profile, but `XerialQuarantine` excludes them from the libSQL run. The full type profile is in @@ -61,7 +61,6 @@ the libSQL run. The full type profile is in | Test | Why the xerial expectation is quarantined | |---|---| -| `org.sqlite.StatementTest#bytesTest` | xerial allows `getBytes` on a declared `BLOB`. Table B-6 lists `getBytes` only for `BINARY`, `VARBINARY`, and `LONGVARBINARY`. | | `org.sqlite.RSMetaDataTest#columnTypes` | xerial reports a declared `NCHAR` as `Types.CHAR`. Appendix B defines `NCHAR` as its own JDBC type, so the driver reports `Types.NCHAR`. | | `org.sqlite.RSMetaDataTest#testGetColumnClassName` | xerial reports a declared `BLOB` as `java.lang.Object`. Table B-3 maps `BLOB` to `java.sql.Blob`. | | `org.sqlite.PrepStmtTest#getObject` | xerial returns an `Integer` for `getObject` on a declared `BIT`. Table B-3 maps `BIT` to `Boolean`. | @@ -69,7 +68,6 @@ the libSQL run. The full type profile is in The exclusion is method-level, so each method also loses checks that do not conflict with Appendix B: -- `StatementTest#bytesTest` also loses its BLOB `setBytes`/`getBytes` round trip. - `RSMetaDataTest#columnTypes` also checks other column types that do not conflict, including the INTEGER family, text, numeric, boolean, date and timestamp types, plus DECIMAL precision and scale. - `RSMetaDataTest#testGetColumnClassName` also checks the INTEGER, VARCHAR, and DOUBLE class names. - `PrepStmtTest#getObject` also checks other `getObject` rows (INTEGER, FLOAT, untyped, VARCHAR and BLOB columns) and the typed getters above them. diff --git a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/XerialQuarantine.java b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/XerialQuarantine.java index 2ff7981..a097588 100644 --- a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/XerialQuarantine.java +++ b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/XerialQuarantine.java @@ -21,11 +21,6 @@ record Quarantined(String className, String methodName, String reason) {} static final List QUARANTINED = List.of( - new Quarantined( - "org.sqlite.StatementTest", - "bytesTest", - "xerial allows getBytes on a declared BLOB; Table B-6 lists getBytes" - + " only for BINARY, VARBINARY and LONGVARBINARY"), new Quarantined( "org.sqlite.RSMetaDataTest", "columnTypes", diff --git a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java index 2e353d7..1071f38 100644 --- a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java +++ b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java @@ -1,5 +1,6 @@ package io.github.magnusp.libsql.itest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -298,6 +299,27 @@ record GetterCell( new GetterCell("getRowId", "ROWID", TEXT_ROWID, ResultSet::getRowId, RowId.class), new GetterCell("getSQLXML", "SQLXML", TEXT_XML, ResultSet::getSQLXML, SQLXML.class)); + @Test + void b6DeclaredBlobReadsThroughByteAndTextGetters() throws Exception { + String table = table("b6blob"); + try (Connection connection = connect(); Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE " + table + " (v BLOB)"); + try (PreparedStatement insert = + connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) { + insert.setBytes(1, BYTES_ABC); + insert.executeUpdate(); + } + try (Statement query = connection.createStatement(); + ResultSet resultSet = query.executeQuery("SELECT v FROM " + table)) { + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBytes(1)).isEqualTo(BYTES_ABC); + assertThat(resultSet.getString(1)).isEqualTo("abc"); + assertThat(resultSet.getBinaryStream(1).readAllBytes()).isEqualTo(BYTES_ABC); + assertThat(resultSet.getBlob(1).getBytes(1, BYTES_ABC.length)).isEqualTo(BYTES_ABC); + } + } + } + @ParameterizedTest(name = "B-6 {0} reads its recommended {1}") @MethodSource("b6Cells") void b6GetterReadsRecommendedTypeFromRealDdl(GetterCell cell) throws SQLException { diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/JdbcTypeMapping.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/JdbcTypeMapping.java index 6a34bad..f5b0625 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/JdbcTypeMapping.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/JdbcTypeMapping.java @@ -19,11 +19,9 @@ 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; @@ -43,9 +41,12 @@ * *

{@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. + * the driver rejects the declared type instead of returning a value of the wrong Java type. + * + *

Reads are not restricted to the Table B-6 matrix. A representable declared column is not + * rejected because of its declared type, and the stored value decides whether the conversion + * succeeds, matching sqlite-jdbc. Rejection on declared-type grounds is limited to the five + * {@link Support#UNSUPPORTED} types; unimplemented getters still throw for any column. */ final class JdbcTypeMapping { @@ -63,58 +64,12 @@ enum StorageClass { NULL } - /** 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) {} + Support support) {} private static final JdbcType INTEGER_FALLBACK = fallback(Types.INTEGER, int.class, Integer.class, StorageClass.INTEGER); @@ -129,7 +84,6 @@ record JdbcType( 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(); @@ -167,10 +121,6 @@ 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); @@ -227,91 +177,7 @@ 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, 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; + return new JdbcType(sqlType, javaClass, objectClass, storage, Support.FALLBACK); } private static Map declaredTypeRegistry() { @@ -368,20 +234,14 @@ 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, - getterMask(name))); + new JdbcType(sqlType, javaClass, objectClass, storage, Support.SUPPORTED)); } 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)); + new JdbcType(sqlType, javaClass, objectClass, storage, Support.UNSUPPORTED)); } /** diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java index 11443ac..0469ac1 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -121,28 +121,22 @@ private Value getCurrentValue(int columnIndex) throws SQLException { } /** - * 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. + * Rejects a declared type that SQLite cannot represent in JDBC. Every other declared type + * passes this check, and the getter's value conversion decides whether the read succeeds. A + * column with no {@code decltype} always reads through the permissive fallback. Getters that + * are not implemented, such as {@code getArray} and {@code getRef}, still throw + * {@link SQLFeatureNotSupportedException} for any column. */ - private void checkGetter(int columnIndex, JdbcTypeMapping.Getter getter) throws SQLException { + private void checkReadable(int columnIndex) throws SQLException { checkClosed(); if (columnIndex < 1 || columnIndex > declaredTypes.length) { return; } JdbcTypeMapping.JdbcType type = declaredTypes[columnIndex - 1]; - if (type == null) { - return; - } - if (JdbcTypeMapping.isUnsupported(type)) { + if (type != null && JdbcTypeMapping.isUnsupported(type)) { throw new SQLFeatureNotSupportedException( "Declared " + JdbcTypeMapping.sqlTypeName(type.sqlType()) + " is not supported"); } - if (!JdbcTypeMapping.supports(type, getter)) { - throw new SQLFeatureNotSupportedException( - getter.methodName() + " does not support declared " - + JdbcTypeMapping.sqlTypeName(type.sqlType())); - } } Value valueForMetadata(int columnIndex) { @@ -218,7 +212,7 @@ public boolean wasNull() throws SQLException { */ @Override public String getString(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_STRING); + checkReadable(columnIndex); return stringValue(getCurrentValue(columnIndex)); } @@ -241,7 +235,7 @@ private static String stringValue(Value val) { */ @Override public boolean getBoolean(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BOOLEAN); + checkReadable(columnIndex); return booleanValue(getCurrentValue(columnIndex)); } @@ -260,37 +254,37 @@ private static boolean booleanValue(Value value) { @Override public byte getByte(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BYTE); + checkReadable(columnIndex); return (byte) longValue(columnIndex); } @Override public short getShort(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_SHORT); + checkReadable(columnIndex); return (short) longValue(columnIndex); } @Override public int getInt(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_INT); + checkReadable(columnIndex); return (int) longValue(columnIndex); } @Override public long getLong(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_LONG); + checkReadable(columnIndex); return longValue(columnIndex); } @Override public float getFloat(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_FLOAT); + checkReadable(columnIndex); return (float) doubleValue(columnIndex); } @Override public double getDouble(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_DOUBLE); + checkReadable(columnIndex); return doubleValue(columnIndex); } @@ -370,7 +364,7 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException @Override public byte[] getBytes(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BYTES); + checkReadable(columnIndex); return bytesValue(getCurrentValue(columnIndex)); } @@ -386,7 +380,7 @@ private static byte[] bytesValue(Value val) { @Override public Date getDate(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_DATE); + checkReadable(columnIndex); return dateValue(getCurrentValue(columnIndex)); } @@ -397,7 +391,7 @@ private static Date dateValue(Value value) throws SQLException { @Override public Time getTime(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_TIME); + checkReadable(columnIndex); return timeValue(getCurrentValue(columnIndex)); } @@ -408,7 +402,7 @@ private static Time timeValue(Value value) throws SQLException { @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_TIMESTAMP); + checkReadable(columnIndex); return timestampValue(getCurrentValue(columnIndex)); } @@ -454,7 +448,7 @@ private static String sqlTimestampSeparator(String text) { @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BIG_DECIMAL); + checkReadable(columnIndex); return bigDecimalValue(getCurrentValue(columnIndex)); } @@ -472,7 +466,7 @@ private static BigDecimal bigDecimalValue(Value value) throws SQLException { @Override public Object getObject(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_OBJECT); + checkReadable(columnIndex); Value value = getCurrentValue(columnIndex); if (value instanceof Value.Null) { return null; @@ -766,7 +760,7 @@ public boolean isLast() throws SQLException { @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ASCII_STREAM); + checkReadable(columnIndex); String text = stringValue(getCurrentValue(columnIndex)); return text == null ? null @@ -774,7 +768,7 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { } @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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BINARY_STREAM); + checkReadable(columnIndex); byte[] bytes = bytesValue(getCurrentValue(columnIndex)); return bytes == null ? null : new ByteArrayInputStream(bytes); } @@ -785,7 +779,7 @@ 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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_CHARACTER_STREAM); + checkReadable(columnIndex); String string = stringValue(getCurrentValue(columnIndex)); return string == null ? null : new StringReader(string); } @@ -868,11 +862,11 @@ public void setFetchSize(int rows) throws SQLException { return getObject(columnIndex); } @Override public Ref getRef(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_REF); + checkReadable(columnIndex); throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } @Override public Blob getBlob(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_BLOB); + checkReadable(columnIndex); return blobValue(getCurrentValue(columnIndex)); } private static Blob blobValue(Value value) { @@ -880,7 +874,7 @@ private static Blob blobValue(Value value) { return bytes == null ? null : new LibsqlBlob(bytes); } @Override public Clob getClob(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_CLOB); + checkReadable(columnIndex); return clobValue(getCurrentValue(columnIndex)); } private static Clob clobValue(Value value) { @@ -888,7 +882,7 @@ private static Clob clobValue(Value value) { return string == null ? null : new LibsqlClob(string); } @Override public Array getArray(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ARRAY); + checkReadable(columnIndex); throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } @Override public Object getObject(String columnLabel, Map> map) throws SQLException { @@ -907,7 +901,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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_URL); + checkReadable(columnIndex); return urlValue(getCurrentValue(columnIndex)); } @@ -934,7 +928,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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_ROW_ID); + checkReadable(columnIndex); return rowIdValue(getCurrentValue(columnIndex)); } private static RowId rowIdValue(Value value) { @@ -950,7 +944,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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_CLOB); + checkReadable(columnIndex); return nclobValue(getCurrentValue(columnIndex)); } private static NClob nclobValue(Value value) { @@ -959,7 +953,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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_SQLXML); + checkReadable(columnIndex); return sqlxmlValue(getCurrentValue(columnIndex)); } private static SQLXML sqlxmlValue(Value value) { @@ -970,12 +964,12 @@ private static SQLXML sqlxmlValue(Value value) { @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 { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_STRING); + checkReadable(columnIndex); return stringValue(getCurrentValue(columnIndex)); } @Override public String getNString(String columnLabel) throws SQLException { return getNString(findColumn(columnLabel)); } @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { - checkGetter(columnIndex, JdbcTypeMapping.Getter.GET_N_CHARACTER_STREAM); + checkReadable(columnIndex); String text = stringValue(getCurrentValue(columnIndex)); return text == null ? null : new StringReader(text); } @@ -1081,7 +1075,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); + checkReadable(columnIndex); if (type == String.class) { return type.cast(getString(columnIndex)); } diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java index 22493cd..94f9be7 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java @@ -55,8 +55,9 @@ *

B-1 is defined for CallableStatement getters, which this driver rejects. It is therefore * exercised through the natural ResultSet getter for each JDBC type (the same Java type the * CallableStatement getter would return). B-3 is exercised through {@code getObject(int)}. B-6 is - * exercised cell by cell: a listed conversion must succeed and return a non-null value of the - * getter's Java type, and an unlisted conversion must reject the value with a {@link SQLException}. + * exercised through its required cells and through every representable declared type. B-6's + * required conversions must succeed. A declared type is never rejected because B-6 omits a cell; + * the stored value decides whether the conversion succeeds. */ class Jdbc4AppendixBReadTest { @@ -156,7 +157,7 @@ interface SqlReader { Object read(ResultSet resultSet, int column) throws SQLException; } - record Accessor(String name, Class javaType, SqlReader reader, Set supported) {} + record Accessor(String name, Class javaType, SqlReader reader, Set required) {} private static final Accessor GET_BYTE = new Accessor("getByte", byte.class, ResultSet::getByte, numericTextWithRowId()); @@ -281,6 +282,23 @@ record Accessor(String name, Class javaType, SqlReader reader, Set su GET_ROW_ID, GET_SQLXML); + /** + * Getters whose conversion can fail for a value the declared type permits. Every other + * implemented getter converts all five storage classes. + */ + private static final Set VALUE_GATED = set( + "getByte", + "getShort", + "getInt", + "getLong", + "getFloat", + "getDouble", + "getBigDecimal", + "getDate", + "getTime", + "getTimestamp", + "getURL"); + private static final Map B1_ACCESSORS = b1Accessors(); private static Map b1Accessors() { @@ -397,22 +415,22 @@ void b3GetObjectReturnsNullForNull() throws SQLException { } // --------------------------------------------------------------------- - // Table B-6: which JDBC types each getter may read. + // Table B-6: the conversions the driver must support. // --------------------------------------------------------------------- static Stream b6Cases() { List arguments = new ArrayList<>(); for (Accessor accessor : ACCESSORS) { for (JdbcType type : JDBC_TYPES) { - arguments.add(Arguments.of(accessor, type, accessor.supported().contains(type.name()))); + arguments.add(Arguments.of(accessor, type, accessor.required().contains(type.name()))); } } return arguments.stream(); } - @ParameterizedTest(name = "B-6 {0} on {1} ({2})") + @ParameterizedTest(name = "B-6 {0} on {1} (required={2})") @MethodSource("b6Cases") - void b6GetterHonoursSupportedJdbcTypes(Accessor getter, JdbcType type, boolean supported) + void b6RequiredConversionsAndPermissiveReads(Accessor getter, JdbcType type, boolean required) throws SQLException { LibsqlResultSet resultSet = oneRow(type, fixtureFor(getter, type)); assertThat(resultSet.next()).isTrue(); @@ -426,7 +444,7 @@ void b6GetterHonoursSupportedJdbcTypes(Accessor getter, JdbcType type, boolean s return; } - if (supported) { + if (required) { Object value; try { value = getter.reader().read(resultSet, 1); @@ -434,25 +452,60 @@ void b6GetterHonoursSupportedJdbcTypes(Accessor getter, JdbcType type, boolean s throw new AssertionError( "B-6: " + getter.name() + " must support " + type.name() + " but threw " + e, e); } - assertThat(value) - .as("B-6: %s must support %s", getter.name(), type.name()) - .isNotNull(); - if (getter.javaType() != Object.class && getter.javaType() != byte[].class) { - assertThat(value) - .as("B-6: %s on %s must return %s", getter.name(), type.name(), getter.javaType()) - .isInstanceOf(boxed(getter.javaType())); + assertGetterResult(getter, type, value); + return; + } + + try { + assertGetterResult(getter, type, getter.reader().read(resultSet, 1)); + } catch (SQLFeatureNotSupportedException e) { + if (getter == GET_ARRAY || getter == GET_REF) { + // These optional getters are unimplemented for every declared type. + return; } - } else { - try { - Object value = getter.reader().read(resultSet, 1); - fail("B-6: " + getter.name() + " must reject " + type.name() - + " but returned " + value); - } catch (SQLException expected) { - // The specification lists no conversion, so rejection is the contract. + fail("B-6: " + getter.name() + " must not reject declared " + type.name() + + " on type grounds but threw " + e); + } catch (SQLException conversionFailure) { + if (!VALUE_GATED.contains(getter.name())) { + throw new AssertionError("B-6: " + getter.name() + " must read declared " + + type.name() + " but the conversion threw " + conversionFailure, conversionFailure); } + // The stored value, not the declared type, decided the conversion is not defined. + } + } + + private static void assertGetterResult(Accessor getter, JdbcType type, Object value) { + assertThat(value) + .as("B-6: %s must support %s", getter.name(), type.name()) + .isNotNull(); + if (getter.javaType() != Object.class && getter.javaType() != byte[].class) { + assertThat(value) + .as("B-6: %s on %s must return %s", getter.name(), type.name(), getter.javaType()) + .isInstanceOf(boxed(getter.javaType())); } } + @Test + void b6UnlistedConversionsReadTheStoredValue() throws Exception { + LibsqlResultSet blobRow = oneRow(jdbcType("BLOB"), BYTES_ABC); + assertThat(blobRow.next()).isTrue(); + assertThat(blobRow.getBytes(1)).isEqualTo("abc".getBytes(StandardCharsets.UTF_8)); + assertThat(blobRow.getObject(1, byte[].class)).isEqualTo("abc".getBytes(StandardCharsets.UTF_8)); + assertThat(blobRow.getBinaryStream(1).readAllBytes()) + .isEqualTo("abc".getBytes(StandardCharsets.UTF_8)); + assertThat(blobRow.getString(1)).isEqualTo("abc"); + + LibsqlResultSet textRow = oneRow(jdbcType("VARCHAR"), TEXT_ABC); + assertThat(textRow.next()).isTrue(); + assertThat(textRow.getBytes(1)).isEqualTo("abc".getBytes(StandardCharsets.UTF_8)); + assertThat(textRow.getBlob(1).getBytes(1, 3)).isEqualTo("abc".getBytes(StandardCharsets.UTF_8)); + + LibsqlResultSet nullBlob = oneRow(jdbcType("BLOB"), new Value.Null()); + assertThat(nullBlob.next()).isTrue(); + assertThat(nullBlob.getBytes(1)).isNull(); + assertThat(nullBlob.wasNull()).isTrue(); + } + @Test void b6NullIsVisibleThroughWasNull() throws SQLException { for (Accessor getter : ACCESSORS) {