From aafd14ce78dbfdfe909ee0e9c1250feaa7775774 Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 01:36:25 +0200
Subject: [PATCH 1/3] Add type mapping doc
---
vendor/JDBC4-TYPE-MAPPING.md | 244 +++++++++++++++++++++++++++++++++++
1 file changed, 244 insertions(+)
create mode 100644 vendor/JDBC4-TYPE-MAPPING.md
diff --git a/vendor/JDBC4-TYPE-MAPPING.md b/vendor/JDBC4-TYPE-MAPPING.md
new file mode 100644
index 0000000..94b48d1
--- /dev/null
+++ b/vendor/JDBC4-TYPE-MAPPING.md
@@ -0,0 +1,244 @@
+# TABLE B-1 JDBC Types Mapped to Java Types
+
+This table shows the conceptual correspondence between JDBC types and Java types.
+A programmer should write code with this mapping in mind. For example, if a
+value in the database is a SMALLINT, a short should be the data type used in a
+JDBC application.
+All CallableStatement getter methods except for getObject use this mapping. The
+getObject methods for both the CallableStatement and ResultSet interfaces use the
+mapping in TABLE B-3.
+
+| JDBC Type | Java Type |
+|---------------|----------------------------|
+| CHAR | String |
+| VARCHAR | String |
+| LONGVARCHAR | String |
+| NUMERIC | java.math.BigDecimal |
+| DECIMAL | java.math.BigDecimal |
+| BIT | boolean |
+| BOOLEAN | boolean |
+| TINYINT | byte |
+| SMALLINT | short |
+| INTEGER | int |
+| BIGINT | long |
+| REAL | float |
+| FLOAT | double |
+| DOUBLE | double |
+| BINARY | byte[] |
+| VARBINARY | byte[] |
+| LONGVARBINARY | byte[] |
+| DATE | java.sql.Date |
+| TIME | java.sql.Time |
+| TIMESTAMP | java.sql.Timestamp |
+| CLOB | java.sql.Clob |
+| BLOB | java.sql.Blob |
+| ARRAY | java.sql.Array |
+| DISTINCT | mapping of underlying type |
+| STRUCT | java.sql.Struct |
+| REF | java.sql.Ref |
+| DATALINK | java.net.URL |
+| JAVA_OBJECT | underlying Java class |
+| ROWID | java.sql.RowId |
+| NCHAR | String |
+| NVARCHAR | String |
+| LONGNVARCHAR | String |
+| NCLOB | java.sql.NClob |
+| SQLXML | java.sql.SQLXML |
+
+# TABLE B-2 Java Types Mapper to JDBC Types
+
+This table shows the mapping a driver should use for the updater methods in the
+ResultSet interface and for IN parameters. PreparedStatement setter methods and
+RowSet setter methods use this table for mapping an IN parameter, which is a Java
+type, to the JDBC type that will be sent to the database. Note that the setObject
+methods for these two interfaces use the mapping shown in TABLE B-4.
+
+| Java Type | JDBC Type |
+|----------------------|-------------------------------------------------------------|
+| String | CHAR, VARCHAR, LONGVARCHAR, NCHAR, NVARCHAR or LONGNVARCHAR |
+| java.math.BigDecimal | NUMERIC |
+| boolean | BIT or BOOLEAN |
+| byte | TINYINT |
+| short | SMALLINT |
+| int | INTEGER |
+| long | BIGINT |
+| float | REAL |
+| double | DOUBLE |
+| byte[] | BINARY, VARBINARY, or LONGVARBINARY |
+| java.sql.Date | DATE |
+| java.sql.Time | TIME |
+| java.sql.Timestamp | TIMESTAMP |
+| java.sql.Clob | CLOB |
+| java.sql.Blob | BLOB |
+| java.sql.Array | ARRAY |
+| java.sql.Struct | STRUCT |
+| java.sql.Ref | REF |
+| java.net.URL | DATALINK |
+| Java class | JAVA_OBJECT |
+| java.sql.RowId | ROWID |
+| java.sql.NClob | NCLOB |
+| java.sql.SQLXML | SQLXML |
+
+# TABLE B-3 JDBC Types Mapped to Java Object Types
+
+ResultSet.getObject and CallableStatement.getObject use the mapping shown in this
+table for standard mappings.
+
+| JDBC Type | Java Object Type |
+|---------------|-------------------------------------|
+| CHAR | String |
+| VARCHAR | String |
+| LONGVARCHAR | String |
+| NUMERIC | java.math.BigDecimal |
+| DECIMAL | java.math.BigDecimal |
+| BIT | Boolean |
+| BOOLEAN | Boolean |
+| TINYINT | Integer |
+| SMALLINT | Integer |
+| INTEGER | Integer |
+| BIGINT | Long |
+| REAL | Float |
+| FLOAT | Double |
+| DOUBLE | Double |
+| BINARY | byte[] |
+| VARBINARY | byte[] |
+| LONGVARBINARY | byte[] |
+| DATE | java.sql.Date |
+| TIME | java.sql.Time |
+| TIMESTAMP | java.sql.Timestamp |
+| DISTINCT | Object type of underlying type |
+| CLOB | java.sql.Clob |
+| BLOB | java.sql.Blob |
+| ARRAY | java.sql.Array |
+| STRUCT | java.sql.Struct or java.sql.SQLData |
+| REF | java.sql.Ref |
+| DATALINK | java.net.URL |
+| JAVA_OBJECT | underlying Java class |
+| ROWID | java.sql.RowId |
+| NCHAR | String |
+| NVARCHAR | String |
+| LONGNVARCHAR | String |
+| NCLOB | java.sql.NClob |
+| SQLXML | java.sql.SQLXML |
+
+# TABLE B-4 Java Object Types Mapped to JDBC Types
+
+PreparedStatement.setObject, PreparedStatement.setNull, RowSet.setNull and
+RowSet.setObject use the mapping shown in this table when no parameter
+specifying a target JDBC type is provided.
+
+| Java Object Type | JDBC Type |
+|----------------------|-------------------------------------------------------------|
+| String | CHAR, VARCHAR, LONGVARCHAR, NCHAR, NVARCHAR or LONGNVARCHAR |
+| java.math.BigDecimal | NUMERIC |
+| Boolean | BIT or BOOLEAN |
+| Byte | TINYINT |
+| Short | SMALLINT |
+| Integer | INTEGER |
+| Long | BIGINT |
+| Float | REAL |
+| Double | DOUBLE |
+| byte[] | BINARY, VARBINARY, or LONGVARBINARY |
+| java.sql.Date | DATE |
+| java.sql.Time | TIME |
+| java.sql.Timestamp | TIMESTAMP |
+| java.sql.Clob | CLOB |
+| java.sql.Blob | BLOB |
+| java.sql.Array | ARRAY |
+| java.sql.Struct | STRUCT |
+| java.sql.Ref | REF |
+| java.net.URL | DATALINK |
+| Java class | JAVA_OBJECT |
+| java.sql.RowId | ROWID |
+| java.sql.NClob | NCLOB |
+| java.sql.SQLXML | SQLXML |
+
+# TABLE B-5 Conversions by setObject and setNull from Java Object Types to JDBC Types
+
+This table shows which JDBC types may be specified as the target JDBC type to the
+methods PreparedStatement.setObject, PreparedStatement.setNull, RowSet.setNull
+and RowSet.setObject.
+
+| Java object type | TINYINT | SMALLINT | INTEGER | BIGINT | REAL | FLOAT | DOUBLE | DECIMAL | NUMERIC | BIT | BOOLEAN | CHAR | VARCHAR | LONGVARCHAR | BINARY | VARBINARY | LONGVARBINARY | DATE | TIME | TIMESTAMP | ARRAY | BLOB | CLOB | STRUCT | REF | DATALINK | JAVA_OBJECT | ROWID | NCHAR | NVARCHAR | NCLOB | SQLXML |
+|----------------------|---------|----------|---------|--------|------|-------|--------|---------|---------|-----|---------|------|---------|-------------|--------|-----------|---------------|------|------|-----------|-------|------|------|--------|-----|----------|-------------|-------|-------|----------|-------|--------|
+| String | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | x | x | | |
+| java.math.BigDecimal | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Boolean | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Byte | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Short | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Integer | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Long | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Float | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| Double | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
+| byte[] | | | | | | | | | | | | | | | x | x | x | | | | | | | | | | | | | | | |
+| java.sql.Date | | | | | | | | | | | | x | x | x | | | | x | | x | | | | | | | | | | | | |
+| java.sql.Time | | | | | | | | | | | | x | x | x | | | | | x | x | | | | | | | | | | | | |
+| java.sql.Timestamp | | | | | | | | | | | | x | x | x | | | | x | x | x | | | | | | | | | | | | |
+| java.sql.Array | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | | |
+| java.sql.Blob | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | |
+| java.sql.Clob | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | |
+| java.sql.Struct | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | |
+| java.sql.Ref | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | |
+| java.net.URL | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | |
+| Java class | | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | |
+| java.sql.RowId | | | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | |
+| java.sql.NClob | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | x | |
+| java.sql.SQLXML | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | x |
+
+Notes:
+
+- Blank cells mean "not a valid target type for `setObject`/`setNull`"; `x` means the driver must support the
+ conversion.
+- JDBC 4.1+ adds the `LONGNVARCHAR` column (`String` also targets it), plus rows for `BigInteger`, `java.util.Calendar`,
+ and `java.util.Date`; JDBC 4.2+ adds `java.time` rows.
+- `java.sql.Date`/`Time` do not cross-map to each other — only `Timestamp` targets all three of `DATE`, `TIME`,
+ `TIMESTAMP`.
+
+# TABLE B-6 Type Conversions Supported by ResultSet getter Methods
+
+This table shows which JDBC types may be returned by ResultSet getter methods. A
+bold X indicates the method recommended for retrieving a JDBC type. A plain x
+indicates for which JDBC types it is possible to use a getter method.
+
+That image is the JDBC spec's **Table B-6 — Use of `ResultSet` getter Methods to Retrieve JDBC Data Types**. Converted
+(`**X**` = recommended getter, `x` = supported conversion, blank = not supported):
+
+| ResultSet getter | TINYINT | SMALLINT | INTEGER | BIGINT | REAL | FLOAT | DOUBLE | DECIMAL | NUMERIC | BIT | BOOLEAN | CHAR | VARCHAR | LONGVARCHAR | BINARY | VARBINARY | LONGVARBINARY | DATE | TIME | TIMESTAMP | CLOB | BLOB | ARRAY | REF | DATALINK | STRUCT | JAVA_OBJECT | ROWID | NCHAR | NVARCHAR | LONGNVARCHAR | NCLOB | SQLXML |
+|---------------------|---------|----------|---------|--------|-------|-------|--------|---------|---------|-------|---------|-------|---------|-------------|--------|-----------|---------------|-------|-------|-----------|-------|-------|-------|-------|----------|--------|-------------|-------|-------|----------|--------------|-------|--------|
+| getByte | **X** | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | x | | | | | |
+| getShort | x | **X** | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getInt | x | x | **X** | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getLong | x | x | x | **X** | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getFloat | x | x | x | x | **X** | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getDouble | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getBigDecimal | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getBoolean | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | | | | | | | | | | | | | | | | | | | |
+| getString | x | x | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x | x | | | | | x | | | | x | x | x | | |
+| getNString | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | x | | | | **X** | **X** | x | | |
+| getBytes | | | | | | | | | | | | | | | **X** | **X** | x | | | | | | | | | | | | | | | | |
+| getDate | | | | | | | | | | | | x | x | x | | | | **X** | | x | | | | | | | | | | | | | |
+| getTime | | | | | | | | | | | | x | x | x | | | | | **X** | x | | | | | | | | | | | | | |
+| getTimestamp | | | | | | | | | | | | x | x | x | | | | x | x | **X** | | | | | | | | | | | | | |
+| getAsciiStream | | | | | | | | | | | | x | x | **X** | x | x | x | | | | x | | | | | | | | | | | x | |
+| getBinaryStream | | | | | | | | | | | | | | | x | x | **X** | | | | | x | | | | | | | | | | | x |
+| getCharacterStream | | | | | | | | | | | | x | x | **X** | x | x | x | | | | x | | | | | | | | x | x | x | x | x |
+| getNCharacterStream | | | | | | | | | | | | x | x | x | x | x | x | | | | x | | | | | | | | x | x | **X** | x | x |
+| getClob | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | | x | |
+| getNClob | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | | **X** | |
+| getBlob | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | | |
+| getArray | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | |
+| getRef | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | |
+| getURL | | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | |
+| getObject | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x |
+| getRowId | | | | | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | |
+| getSQLXML | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | **X** |
+
+Notes:
+
+- Read a column as: "this JDBC type can be fetched with this getter"; the bold `X` is the JDBC-recommended getter for
+ that SQL type.
+- Worth flagging because it surprises people: `getObject` accepts every listed SQL type; `getString` covers all numeric
+ types plus binary and date/time; and `getBinaryStream` in this revision lists `BLOB` and `SQLXML` (later revisions
+ narrowed it to the binary types).
+- The column order here follows the printed table (`CLOB, BLOB, ARRAY, REF, …`); the current JDBC 4.3 spec presents the
+ same content as prose rather than a matrix.
\ No newline at end of file
From 3e8f582c0563323c91d825ab80b6708eb58f700d Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 02:24:25 +0200
Subject: [PATCH 2/3] Update to 4.3
---
vendor/JDBC4-TYPE-MAPPING.md | 240 ++++++++++++++++++++---------------
1 file changed, 135 insertions(+), 105 deletions(-)
diff --git a/vendor/JDBC4-TYPE-MAPPING.md b/vendor/JDBC4-TYPE-MAPPING.md
index 94b48d1..47fae2e 100644
--- a/vendor/JDBC4-TYPE-MAPPING.md
+++ b/vendor/JDBC4-TYPE-MAPPING.md
@@ -1,13 +1,33 @@
-# TABLE B-1 JDBC Types Mapped to Java Types
+# JDBC 4.3 Appendix B — Data Type Conversion Tables
+
+Source: JDBC 4.3 Specification (JSR 000221, Maintenance Release 3, July 2017),
+Appendix B, pages 189–201.
+
+Reproduced as published, with the typographical errors listed below corrected.
+Tables B-5 and B-6 are prose tables in 4.3; they were matrices in 4.0/4.1.
+
+## Errata corrected in this copy
+
+| Published | Corrected | Location |
+|--------------------------------------------|------------------------------|--------------------|
+| `java.sql.array` | `java.sql.Array` | Table B-1 |
+| `LONVARBINARY` | `LONGVARBINARY` | Tables B-5 and B-6 |
+| `java.time.OffsetDatetime` | `java.time.OffsetDateTime` | Tables B-4 and B-5 |
+| `Java Object Type` (column of getter names) | `ResultSet getter` | Table B-6 |
+
+## B.1 JDBC Types Mapped to Java Types
+
+TABLE B-1 shows the conceptual correspondence between JDBC types and Java types.
+A programmer should write code with this mapping in mind. For example, if a value
+in the database is a SMALLINT, a short should be the data type used in a JDBC
+application.
-This table shows the conceptual correspondence between JDBC types and Java types.
-A programmer should write code with this mapping in mind. For example, if a
-value in the database is a SMALLINT, a short should be the data type used in a
-JDBC application.
All CallableStatement getter methods except for getObject use this mapping. The
getObject methods for both the CallableStatement and ResultSet interfaces use the
mapping in TABLE B-3.
+**TABLE B-1 — JDBC Types Mapped to Java Types**
+
| JDBC Type | Java Type |
|---------------|----------------------------|
| CHAR | String |
@@ -33,11 +53,11 @@ mapping in TABLE B-3.
| CLOB | java.sql.Clob |
| BLOB | java.sql.Blob |
| ARRAY | java.sql.Array |
-| DISTINCT | mapping of underlying type |
+| DISTINCT | Mapping of underlying type |
| STRUCT | java.sql.Struct |
| REF | java.sql.Ref |
| DATALINK | java.net.URL |
-| JAVA_OBJECT | underlying Java class |
+| JAVA_OBJECT | Underlying Java class |
| ROWID | java.sql.RowId |
| NCHAR | String |
| NVARCHAR | String |
@@ -45,14 +65,16 @@ mapping in TABLE B-3.
| NCLOB | java.sql.NClob |
| SQLXML | java.sql.SQLXML |
-# TABLE B-2 Java Types Mapper to JDBC Types
+## B.2 Java Types Mapped to JDBC Types
-This table shows the mapping a driver should use for the updater methods in the
+TABLE B-2 shows the mapping a driver should use for the updater methods in the
ResultSet interface and for IN parameters. PreparedStatement setter methods and
RowSet setter methods use this table for mapping an IN parameter, which is a Java
type, to the JDBC type that will be sent to the database. Note that the setObject
methods for these two interfaces use the mapping shown in TABLE B-4.
+**TABLE B-2 — Standard Mapping from Java Types to JDBC Types**
+
| Java Type | JDBC Type |
|----------------------|-------------------------------------------------------------|
| String | CHAR, VARCHAR, LONGVARCHAR, NCHAR, NVARCHAR or LONGNVARCHAR |
@@ -79,10 +101,18 @@ methods for these two interfaces use the mapping shown in TABLE B-4.
| java.sql.NClob | NCLOB |
| java.sql.SQLXML | SQLXML |
-# TABLE B-3 JDBC Types Mapped to Java Object Types
+## B.3 JDBC Types Mapped to Java Object Types
+
+ResultSet.getObject and CallableStatement.getObject use the mapping shown in
+TABLE B-3 for standard mappings.
-ResultSet.getObject and CallableStatement.getObject use the mapping shown in this
-table for standard mappings.
+> **Note** — The JDBC 1.0 specification defined the Java object mapping for the
+> SMALLINT and TINYINT JDBC types to be Integer. The Java language did not
+> include the Byte and Short data types when the JDBC 1.0 specification was
+> finalized. The mapping of SMALLINT and TINYINT to Integer is maintained to
+> preserve backwards compatibility.
+
+**TABLE B-3 — Mapping from JDBC Types to Java Object Types**
| JDBC Type | Java Object Type |
|---------------|-------------------------------------|
@@ -113,7 +143,7 @@ table for standard mappings.
| STRUCT | java.sql.Struct or java.sql.SQLData |
| REF | java.sql.Ref |
| DATALINK | java.net.URL |
-| JAVA_OBJECT | underlying Java class |
+| JAVA_OBJECT | Underlying Java class |
| ROWID | java.sql.RowId |
| NCHAR | String |
| NVARCHAR | String |
@@ -121,11 +151,13 @@ table for standard mappings.
| NCLOB | java.sql.NClob |
| SQLXML | java.sql.SQLXML |
-# TABLE B-4 Java Object Types Mapped to JDBC Types
+## B.4 Java Object Types Mapped to JDBC Types
PreparedStatement.setObject, PreparedStatement.setNull, RowSet.setNull and
-RowSet.setObject use the mapping shown in this table when no parameter
-specifying a target JDBC type is provided.
+RowSet.setObject use the mapping shown in TABLE B-4 when no parameter specifying
+a target JDBC type is provided.
+
+**TABLE B-4 — Mapping from Java Object Types to JDBC Types**
| Java Object Type | JDBC Type |
|----------------------|-------------------------------------------------------------|
@@ -139,6 +171,7 @@ specifying a target JDBC type is provided.
| Float | REAL |
| Double | DOUBLE |
| byte[] | BINARY, VARBINARY, or LONGVARBINARY |
+| java.math.BigInteger | BIGINT |
| java.sql.Date | DATE |
| java.sql.Time | TIME |
| java.sql.Timestamp | TIMESTAMP |
@@ -152,93 +185,90 @@ specifying a target JDBC type is provided.
| java.sql.RowId | ROWID |
| java.sql.NClob | NCLOB |
| java.sql.SQLXML | SQLXML |
+| java.util.Calendar | TIMESTAMP |
+| java.util.Date | TIMESTAMP |
+| java.time.LocalDate | DATE |
+| java.time.LocalTime | TIME |
+| java.time.LocalDateTime | TIMESTAMP |
+| java.time.OffsetTime | TIME_WITH_TIMEZONE |
+| java.time.OffsetDateTime | TIMESTAMP_WITH_TIMEZONE |
+
+## B.5 Conversions by setObject and setNull from Java Object Types to JDBC Types
+
+TABLE B-5 shows which JDBC types may be specified as the target JDBC type to the
+methods PreparedStatement.setObject, PreparedStatement.setNull,
+RowSet.setNull, and RowSet.setObject.
+
+**TABLE B-5 — Conversions Performed by setObject and setNull Between Java Object Types and Target JDBC Types**
+
+| Java Object Type | Supported JDBC Type |
+|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| String | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP, NCHAR, NVARCHAR, LONGNVARCHAR |
+| java.math.BigDecimal | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Boolean | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Byte | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Short | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Integer | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Long | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Float | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| Double | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| byte[] | BINARY, VARBINARY, or LONGVARBINARY |
+| java.math.BigInteger | BIGINT, CHAR, VARCHAR, LONGVARCHAR |
+| java.sql.Date | CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP |
+| java.sql.Time | CHAR, VARCHAR, LONGVARCHAR, TIME, TIMESTAMP |
+| java.sql.Timestamp | CHAR, VARCHAR, LONGVARCHAR, DATE, TIME, TIMESTAMP |
+| java.sql.Array | ARRAY |
+| java.sql.Blob | BLOB |
+| java.sql.Clob | CLOB |
+| java.sql.Struct | STRUCT |
+| java.sql.Ref | REF |
+| java.net.URL | DATALINK |
+| Java class | JAVA_OBJECT |
+| java.sql.RowId | ROWID |
+| java.sql.NClob | NCLOB |
+| java.sql.SQLXML | SQLXML |
+| java.util.Calendar | CHAR, VARCHAR, LONGVARCHAR, DATE, TIME, TIMESTAMP, ARRAY |
+| java.util.Date | CHAR, VARCHAR, LONGVARCHAR, DATE, TIME, TIMESTAMP, ARRAY |
+| java.time.LocalDate | CHAR, VARCHAR, LONGVARCHAR, DATE |
+| java.time.LocalTime | CHAR, VARCHAR, LONGVARCHAR, TIME |
+| java.time.LocalDateTime | CHAR, VARCHAR, LONGVARCHAR, DATE, TIME, TIMESTAMP |
+| java.time.OffsetTime | CHAR, VARCHAR, LONGVARCHAR, TIME_WITH_TIMEZONE |
+| java.time.OffsetDateTime | CHAR, VARCHAR, LONGVARCHAR, TIME_WITH_TIMEZONE, TIMESTAMP_WITH_TIMEZONE |
+
+## B.6 Type Conversions Supported by ResultSet getter Methods
+
+TABLE B-6 shows which JDBC types may be returned by ResultSet getter methods.
+This table also shows the conversions used by the SQLInput reader methods, except
+that they use only the recommended conversions.
+
+**TABLE B-6 — Use of ResultSet getter Methods to Retrieve JDBC Data Types**
-# TABLE B-5 Conversions by setObject and setNull from Java Object Types to JDBC Types
-
-This table shows which JDBC types may be specified as the target JDBC type to the
-methods PreparedStatement.setObject, PreparedStatement.setNull, RowSet.setNull
-and RowSet.setObject.
-
-| Java object type | TINYINT | SMALLINT | INTEGER | BIGINT | REAL | FLOAT | DOUBLE | DECIMAL | NUMERIC | BIT | BOOLEAN | CHAR | VARCHAR | LONGVARCHAR | BINARY | VARBINARY | LONGVARBINARY | DATE | TIME | TIMESTAMP | ARRAY | BLOB | CLOB | STRUCT | REF | DATALINK | JAVA_OBJECT | ROWID | NCHAR | NVARCHAR | NCLOB | SQLXML |
-|----------------------|---------|----------|---------|--------|------|-------|--------|---------|---------|-----|---------|------|---------|-------------|--------|-----------|---------------|------|------|-----------|-------|------|------|--------|-----|----------|-------------|-------|-------|----------|-------|--------|
-| String | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | x | x | | |
-| java.math.BigDecimal | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Boolean | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Byte | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Short | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Integer | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Long | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Float | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| Double | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | |
-| byte[] | | | | | | | | | | | | | | | x | x | x | | | | | | | | | | | | | | | |
-| java.sql.Date | | | | | | | | | | | | x | x | x | | | | x | | x | | | | | | | | | | | | |
-| java.sql.Time | | | | | | | | | | | | x | x | x | | | | | x | x | | | | | | | | | | | | |
-| java.sql.Timestamp | | | | | | | | | | | | x | x | x | | | | x | x | x | | | | | | | | | | | | |
-| java.sql.Array | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | | |
-| java.sql.Blob | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | |
-| java.sql.Clob | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | |
-| java.sql.Struct | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | |
-| java.sql.Ref | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | | |
-| java.net.URL | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | | |
-| Java class | | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | | |
-| java.sql.RowId | | | | | | | | | | | | | | | | | | | | | | | | | | | | x | | | | |
-| java.sql.NClob | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | x | |
-| java.sql.SQLXML | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | x |
-
-Notes:
-
-- Blank cells mean "not a valid target type for `setObject`/`setNull`"; `x` means the driver must support the
- conversion.
-- JDBC 4.1+ adds the `LONGNVARCHAR` column (`String` also targets it), plus rows for `BigInteger`, `java.util.Calendar`,
- and `java.util.Date`; JDBC 4.2+ adds `java.time` rows.
-- `java.sql.Date`/`Time` do not cross-map to each other — only `Timestamp` targets all three of `DATE`, `TIME`,
- `TIMESTAMP`.
-
-# TABLE B-6 Type Conversions Supported by ResultSet getter Methods
-
-This table shows which JDBC types may be returned by ResultSet getter methods. A
-bold X indicates the method recommended for retrieving a JDBC type. A plain x
-indicates for which JDBC types it is possible to use a getter method.
-
-That image is the JDBC spec's **Table B-6 — Use of `ResultSet` getter Methods to Retrieve JDBC Data Types**. Converted
-(`**X**` = recommended getter, `x` = supported conversion, blank = not supported):
-
-| ResultSet getter | TINYINT | SMALLINT | INTEGER | BIGINT | REAL | FLOAT | DOUBLE | DECIMAL | NUMERIC | BIT | BOOLEAN | CHAR | VARCHAR | LONGVARCHAR | BINARY | VARBINARY | LONGVARBINARY | DATE | TIME | TIMESTAMP | CLOB | BLOB | ARRAY | REF | DATALINK | STRUCT | JAVA_OBJECT | ROWID | NCHAR | NVARCHAR | LONGNVARCHAR | NCLOB | SQLXML |
-|---------------------|---------|----------|---------|--------|-------|-------|--------|---------|---------|-------|---------|-------|---------|-------------|--------|-----------|---------------|-------|-------|-----------|-------|-------|-------|-------|----------|--------|-------------|-------|-------|----------|--------------|-------|--------|
-| getByte | **X** | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | x | | | | | |
-| getShort | x | **X** | x | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getInt | x | x | **X** | x | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getLong | x | x | x | **X** | x | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getFloat | x | x | x | x | **X** | x | x | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getDouble | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getBigDecimal | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getBoolean | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | | | | | | | | | | | | | | | | | | | |
-| getString | x | x | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x | x | | | | | x | | | | x | x | x | | |
-| getNString | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | | | | x | | | | **X** | **X** | x | | |
-| getBytes | | | | | | | | | | | | | | | **X** | **X** | x | | | | | | | | | | | | | | | | |
-| getDate | | | | | | | | | | | | x | x | x | | | | **X** | | x | | | | | | | | | | | | | |
-| getTime | | | | | | | | | | | | x | x | x | | | | | **X** | x | | | | | | | | | | | | | |
-| getTimestamp | | | | | | | | | | | | x | x | x | | | | x | x | **X** | | | | | | | | | | | | | |
-| getAsciiStream | | | | | | | | | | | | x | x | **X** | x | x | x | | | | x | | | | | | | | | | | x | |
-| getBinaryStream | | | | | | | | | | | | | | | x | x | **X** | | | | | x | | | | | | | | | | | x |
-| getCharacterStream | | | | | | | | | | | | x | x | **X** | x | x | x | | | | x | | | | | | | | x | x | x | x | x |
-| getNCharacterStream | | | | | | | | | | | | x | x | x | x | x | x | | | | x | | | | | | | | x | x | **X** | x | x |
-| getClob | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | | x | |
-| getNClob | | | | | | | | | | | | | | | | | | | | | x | | | | | | | | | | | **X** | |
-| getBlob | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | | |
-| getArray | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | | |
-| getRef | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | | |
-| getURL | | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | | | | |
-| getObject | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | **X** | **X** | x | x | x | x | x | x |
-| getRowId | | | | | | | | | | | | | | | | | | | | | | | | | | | | **X** | | | | | |
-| getSQLXML | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | **X** |
-
-Notes:
-
-- Read a column as: "this JDBC type can be fetched with this getter"; the bold `X` is the JDBC-recommended getter for
- that SQL type.
-- Worth flagging because it surprises people: `getObject` accepts every listed SQL type; `getString` covers all numeric
- types plus binary and date/time; and `getBinaryStream` in this revision lists `BLOB` and `SQLXML` (later revisions
- narrowed it to the binary types).
-- The column order here follows the printed table (`CLOB, BLOB, ARRAY, REF, …`); the current JDBC 4.3 spec presents the
- same content as prose rather than a matrix.
\ No newline at end of file
+| ResultSet getter | Recommended JDBC Type | Supported JDBC Type |
+|---------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| getByte | TINYINT | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR, ROWID |
+| getShort | SMALLINT | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getInt | INTEGER | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getLong | BIGINT | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getFloat | REAL | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getDouble | FLOAT, DOUBLE | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getBigDecimal | DECIMAL, NUMERIC | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getBoolean | BIT, BOOLEAN | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR |
+| getString | CHAR, VARCHAR | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP, DATALINK, NCHAR, NVARCHAR, LONGNVARCHAR |
+| getNString | NCHAR, NVARCHAR | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP, DATALINK, NCHAR, NVARCHAR, LONGNVARCHAR |
+| getBytes | BINARY, VARBINARY | BINARY, VARBINARY, LONGVARBINARY |
+| getDate | DATE | CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP |
+| getTime | TIME | CHAR, VARCHAR, LONGVARCHAR, TIME, TIMESTAMP |
+| getTimestamp | TIMESTAMP | CHAR, VARCHAR, LONGVARCHAR, DATE, TIME, TIMESTAMP |
+| getAsciiStream | LONGVARCHAR | CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, CLOB, NCLOB |
+| getBinaryStream | LONGVARBINARY | BINARY, VARBINARY, LONGVARBINARY |
+| getCharacterStream | LONGVARCHAR | CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, CLOB, NCHAR, NVARCHAR, LONGNVARCHAR, NCLOB, SQLXML |
+| getNCharacterStream | LONGNVARCHAR | CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, CLOB, NCHAR, NVARCHAR, LONGNVARCHAR, NCLOB, SQLXML |
+| getClob | CLOB | CLOB, NCLOB |
+| getNClob | NCLOB | CLOB, NCLOB |
+| getBlob | BLOB | BLOB |
+| getArray | ARRAY | ARRAY |
+| getRef | REF | REF |
+| getURL | DATALINK | DATALINK |
+| getObject | STRUCT, JAVA_OBJECT | TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR, BINARY, VARBINARY, LONGVARBINARY, DATE, TIME, TIMESTAMP, CLOB, BLOB, ARRAY, REF, DATALINK, STRUCT, JAVA_OBJECT, ROWID, NCHAR, NVARCHAR, LONGNVARCHAR, NCLOB, SQLXML, TIME_WITH_TIMEZONE, TIMESTAMP_WITH_TIMEZONE |
+| getRowId | ROWID | ROWID |
+| getSQLXML | SQLXML | SQLXML |
From 6a9e74a426dd99cefbb98f3ee57022991afdb322 Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 02:54:37 +0200
Subject: [PATCH 3/3] test(jdbc): add the JDBC 4.3 Appendix B oracle suite
The suite encodes Tables B-1 through B-6 from the JDBC 4.3 specification. It fails on the current driver by design and becomes the evidence for the compliance stack.
---
.../itest/Jdbc4AppendixBTypeMappingIT.java | 1181 +++++++++++++++++
.../libsql/jdbc/Jdbc4AppendixBReadTest.java | 614 +++++++++
.../jdbc/Jdbc4AppendixBSetObjectTest.java | 295 ++++
3 files changed, 2090 insertions(+)
create mode 100644 libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java
create mode 100644 libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java
create mode 100644 libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java
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
new file mode 100644
index 0000000..d3d7223
--- /dev/null
+++ b/libsql-itests/src/test/java/com/github/magnusp/libsql/itest/Jdbc4AppendixBTypeMappingIT.java
@@ -0,0 +1,1181 @@
+package com.github.magnusp.libsql.itest;
+
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.Date;
+import java.sql.DriverManager;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLXML;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * JDBC 4.3 Appendix B, server round trips: Tables B-1, B-2, B-3, B-4, B-5 and B-6.
+ *
+ *
The companion unit suite ({@code Jdbc4AppendixBReadTest} and {@code
+ * Jdbc4AppendixBSetObjectTest}) supplies values through Hrana metadata. This suite creates real
+ * sqld tables so the declared type comes from the server and the binding actually round trips.
+ *
+ *
The oracle is the specification, not sqlite-jdbc. A failure names the table, row and column.
+ * Where the appendix intentionally leaves the value representation open (for example Calendar to
+ * CHAR, or Time to TIMESTAMP), the assertion is limited to the required Java type.
+ */
+public class Jdbc4AppendixBTypeMappingIT extends SqldContainerBase {
+
+ private static String url() {
+ return "jdbc:libsql:" + getHttpUrl();
+ }
+
+ private static Connection connect() throws SQLException {
+ return DriverManager.getConnection(url());
+ }
+
+ private static String table(String prefix) {
+ return prefix + "_" + UUID.randomUUID().toString().replace("-", "");
+ }
+
+ // =====================================================================
+ // Table B-1, B-3: read path over real DDL.
+ // =====================================================================
+
+ record JdbcType(
+ String name,
+ int sqlType,
+ Class> b1JavaType,
+ Class> b3ObjectType,
+ Object sample) {}
+
+ private static final String TEXT_ABC = "abc";
+ private static final String TEXT_DATE = "2024-01-02";
+ private static final String TEXT_TIME = "03:04:05";
+ private static final String TEXT_TIMESTAMP = "2024-01-02 03:04:05";
+ private static final String TEXT_URL = "https://example.com";
+ private static final String TEXT_ROWID = "rowid-1";
+ private static final String TEXT_XML = "";
+ private static final byte[] BYTES_ABC = "abc".getBytes(StandardCharsets.UTF_8);
+
+ private static final List READ_TYPES = List.of(
+ jdbc("CHAR", Types.CHAR, String.class, String.class, TEXT_ABC),
+ jdbc("VARCHAR", Types.VARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("LONGVARCHAR", Types.LONGVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NUMERIC", Types.NUMERIC, BigDecimal.class, BigDecimal.class, 42L),
+ jdbc("DECIMAL", Types.DECIMAL, BigDecimal.class, BigDecimal.class, 42L),
+ jdbc("BIT", Types.BIT, boolean.class, Boolean.class, 42L),
+ jdbc("BOOLEAN", Types.BOOLEAN, boolean.class, Boolean.class, 42L),
+ jdbc("TINYINT", Types.TINYINT, byte.class, Integer.class, 42L),
+ jdbc("SMALLINT", Types.SMALLINT, short.class, Integer.class, 42L),
+ jdbc("INTEGER", Types.INTEGER, int.class, Integer.class, 42L),
+ jdbc("BIGINT", Types.BIGINT, long.class, Long.class, 42L),
+ jdbc("REAL", Types.REAL, float.class, Float.class, 42.5d),
+ jdbc("FLOAT", Types.FLOAT, double.class, Double.class, 42.5d),
+ jdbc("DOUBLE", Types.DOUBLE, double.class, Double.class, 42.5d),
+ jdbc("BINARY", Types.BINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("VARBINARY", Types.VARBINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("LONGVARBINARY", Types.LONGVARBINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("DATE", Types.DATE, Date.class, Date.class, TEXT_DATE),
+ jdbc("TIME", Types.TIME, Time.class, Time.class, TEXT_TIME),
+ jdbc("TIMESTAMP", Types.TIMESTAMP, Timestamp.class, Timestamp.class, TEXT_TIMESTAMP),
+ jdbc("CLOB", Types.CLOB, Clob.class, Clob.class, TEXT_ABC),
+ jdbc("BLOB", Types.BLOB, Blob.class, Blob.class, BYTES_ABC),
+ jdbc("ARRAY", Types.ARRAY, Array.class, Array.class, TEXT_ABC),
+ jdbc("DISTINCT", Types.DISTINCT, String.class, String.class, TEXT_ABC),
+ jdbc("STRUCT", Types.STRUCT, Struct.class, Struct.class, TEXT_ABC),
+ jdbc("REF", Types.REF, Ref.class, Ref.class, TEXT_ABC),
+ jdbc("DATALINK", Types.DATALINK, URL.class, URL.class, TEXT_URL),
+ jdbc("JAVA_OBJECT", Types.JAVA_OBJECT, String.class, String.class, TEXT_ABC),
+ jdbc("ROWID", Types.ROWID, RowId.class, RowId.class, TEXT_ROWID),
+ jdbc("NCHAR", Types.NCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NVARCHAR", Types.NVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("LONGNVARCHAR", Types.LONGNVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NCLOB", Types.NCLOB, NClob.class, NClob.class, TEXT_ABC),
+ jdbc("SQLXML", Types.SQLXML, SQLXML.class, SQLXML.class, TEXT_XML));
+
+ private static JdbcType jdbc(
+ String name, int sqlType, Class> b1, Class> b3, Object sample) {
+ return new JdbcType(name, sqlType, b1, b3, sample);
+ }
+
+ interface SqlRead {
+ Object read(ResultSet resultSet, int column) throws SQLException;
+ }
+
+ private static final Map NATURAL_GETTER = naturalGetters();
+
+ private static Map naturalGetters() {
+ Map getters = new LinkedHashMap<>();
+ for (String name : List.of("CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR", "DISTINCT")) {
+ getters.put(name, ResultSet::getString);
+ }
+ getters.put("NUMERIC", ResultSet::getBigDecimal);
+ getters.put("DECIMAL", ResultSet::getBigDecimal);
+ getters.put("BIT", ResultSet::getBoolean);
+ getters.put("BOOLEAN", ResultSet::getBoolean);
+ getters.put("TINYINT", ResultSet::getByte);
+ getters.put("SMALLINT", ResultSet::getShort);
+ getters.put("INTEGER", ResultSet::getInt);
+ getters.put("BIGINT", ResultSet::getLong);
+ getters.put("REAL", ResultSet::getFloat);
+ getters.put("FLOAT", ResultSet::getDouble);
+ getters.put("DOUBLE", ResultSet::getDouble);
+ getters.put("BINARY", ResultSet::getBytes);
+ getters.put("VARBINARY", ResultSet::getBytes);
+ getters.put("LONGVARBINARY", ResultSet::getBytes);
+ getters.put("DATE", ResultSet::getDate);
+ getters.put("TIME", ResultSet::getTime);
+ getters.put("TIMESTAMP", ResultSet::getTimestamp);
+ getters.put("CLOB", ResultSet::getClob);
+ getters.put("BLOB", ResultSet::getBlob);
+ getters.put("ARRAY", ResultSet::getArray);
+ getters.put("STRUCT", (rs, i) -> rs.getObject(i));
+ getters.put("REF", ResultSet::getRef);
+ getters.put("DATALINK", ResultSet::getURL);
+ getters.put("JAVA_OBJECT", (rs, i) -> rs.getObject(i));
+ getters.put("ROWID", ResultSet::getRowId);
+ getters.put("NCLOB", ResultSet::getNClob);
+ getters.put("SQLXML", ResultSet::getSQLXML);
+ return Map.copyOf(getters);
+ }
+
+ @ParameterizedTest(name = "B-1/B-3 read {0} from a real column")
+ @MethodSource("readTypeRows")
+ void b1AndB3ReadPathAgainstRealDdl(JdbcType type) throws SQLException {
+ String table = table("read");
+ try (Connection connection = connect(); Statement statement = connection.createStatement()) {
+ statement.execute(
+ "CREATE TABLE " + table + " (v \"" + type.name() + "\")");
+ try (PreparedStatement insert =
+ connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) {
+ insert.setObject(1, type.sample());
+ insert.executeUpdate();
+ }
+ try (Statement query = connection.createStatement();
+ ResultSet resultSet = query.executeQuery("SELECT v FROM " + table)) {
+ assertThat(resultSet.next()).isTrue();
+ assertThat(resultSet.getMetaData().getColumnTypeName(1))
+ .as("server reported declared type for %s", type.name())
+ .isEqualTo(type.name());
+
+ List checks = new ArrayList<>();
+ checks.add(() -> {
+ Object natural;
+ try {
+ natural = NATURAL_GETTER.get(type.name()).read(resultSet, 1);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ "B-1: " + type.name() + " via its natural getter threw " + e, e);
+ }
+ assertThat(natural)
+ .as("B-1: %s must read as %s", type.name(), type.b1JavaType())
+ .isInstanceOf(boxed(type.b1JavaType()));
+ });
+ checks.add(() -> assertThat(resultSet.getObject(1))
+ .as("B-3: %s must read back as %s", type.name(), type.b3ObjectType())
+ .isInstanceOf(type.b3ObjectType()));
+ assertAll(type.name(), checks);
+ }
+ }
+ }
+
+ static Stream readTypeRows() {
+ return READ_TYPES.stream();
+ }
+
+ // =====================================================================
+ // Table B-6: each getter reads its recommended JDBC type from real DDL.
+ // =====================================================================
+
+ record GetterCell(
+ String getter, String declaredType, Object sample, SqlRead reader, Class> javaType) {}
+
+ private static final List B6_CELLS = List.of(
+ new GetterCell("getByte", "TINYINT", 42L, ResultSet::getByte, byte.class),
+ new GetterCell("getShort", "SMALLINT", 42L, ResultSet::getShort, short.class),
+ new GetterCell("getInt", "INTEGER", 42L, ResultSet::getInt, int.class),
+ new GetterCell("getLong", "BIGINT", 42L, ResultSet::getLong, long.class),
+ new GetterCell("getFloat", "REAL", 42.5d, ResultSet::getFloat, float.class),
+ new GetterCell("getDouble", "DOUBLE", 42.5d, ResultSet::getDouble, double.class),
+ new GetterCell("getBigDecimal", "DECIMAL", 42L, ResultSet::getBigDecimal, BigDecimal.class),
+ new GetterCell("getBoolean", "BOOLEAN", 42L, ResultSet::getBoolean, boolean.class),
+ new GetterCell("getString", "VARCHAR", TEXT_ABC, ResultSet::getString, String.class),
+ new GetterCell("getNString", "NCHAR", TEXT_ABC, ResultSet::getNString, String.class),
+ new GetterCell("getBytes", "VARBINARY", BYTES_ABC, ResultSet::getBytes, byte[].class),
+ new GetterCell("getDate", "DATE", TEXT_DATE, ResultSet::getDate, Date.class),
+ new GetterCell("getTime", "TIME", TEXT_TIME, ResultSet::getTime, Time.class),
+ new GetterCell("getTimestamp", "TIMESTAMP", TEXT_TIMESTAMP, ResultSet::getTimestamp,
+ Timestamp.class),
+ new GetterCell("getAsciiStream", "LONGVARCHAR", TEXT_ABC, ResultSet::getAsciiStream,
+ java.io.InputStream.class),
+ new GetterCell("getBinaryStream", "LONGVARBINARY", BYTES_ABC,
+ ResultSet::getBinaryStream, java.io.InputStream.class),
+ new GetterCell("getCharacterStream", "LONGVARCHAR", TEXT_ABC,
+ ResultSet::getCharacterStream, java.io.Reader.class),
+ new GetterCell("getNCharacterStream", "LONGNVARCHAR", TEXT_ABC,
+ ResultSet::getNCharacterStream, java.io.Reader.class),
+ new GetterCell("getClob", "CLOB", TEXT_ABC, ResultSet::getClob, Clob.class),
+ new GetterCell("getNClob", "NCLOB", TEXT_ABC, ResultSet::getNClob, NClob.class),
+ new GetterCell("getBlob", "BLOB", BYTES_ABC, ResultSet::getBlob, Blob.class),
+ new GetterCell("getArray", "ARRAY", TEXT_ABC, ResultSet::getArray, Array.class),
+ new GetterCell("getRef", "REF", TEXT_ABC, ResultSet::getRef, Ref.class),
+ new GetterCell("getURL", "DATALINK", TEXT_URL, ResultSet::getURL, URL.class),
+ new GetterCell("getObject", "JAVA_OBJECT", TEXT_ABC, (rs, i) -> rs.getObject(i),
+ Object.class),
+ new GetterCell("getRowId", "ROWID", TEXT_ROWID, ResultSet::getRowId, RowId.class),
+ new GetterCell("getSQLXML", "SQLXML", TEXT_XML, ResultSet::getSQLXML, SQLXML.class));
+
+ @ParameterizedTest(name = "B-6 {0} reads its recommended {1}")
+ @MethodSource("b6Cells")
+ void b6GetterReadsRecommendedTypeFromRealDdl(GetterCell cell) throws SQLException {
+ String table = table("b6");
+ try (Connection connection = connect(); Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE " + table + " (v \"" + cell.declaredType() + "\")");
+ try (PreparedStatement insert =
+ connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) {
+ insert.setObject(1, cell.sample());
+ insert.executeUpdate();
+ }
+ try (Statement query = connection.createStatement();
+ ResultSet resultSet = query.executeQuery("SELECT v FROM " + table)) {
+ assertThat(resultSet.next()).isTrue();
+ Object value;
+ try {
+ value = cell.reader().read(resultSet, 1);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ "B-6: " + cell.getter() + " on " + cell.declaredType() + " threw " + e, e);
+ }
+ assertThat(value)
+ .as("B-6: %s on %s must return a %s", cell.getter(), cell.declaredType(),
+ cell.javaType())
+ .isNotNull();
+ if (cell.javaType() != Object.class && cell.javaType() != byte[].class) {
+ assertThat(value)
+ .as("B-6: %s on %s must return a %s", cell.getter(), cell.declaredType(),
+ cell.javaType())
+ .isInstanceOf(boxed(cell.javaType()));
+ }
+ }
+ }
+ }
+
+ static Stream b6Cells() {
+ return B6_CELLS.stream();
+ }
+
+ // =====================================================================
+ // Table B-2: standard setters map a Java type to a JDBC type.
+ // =====================================================================
+
+ record WriteRow(
+ String name,
+ Object value,
+ String targetName,
+ int targetType,
+ Set storage,
+ SqlRead reader,
+ Expected expected) {}
+
+ record Expected(Object value, Class> anyType) {
+ static Expected of(Object value) {
+ return new Expected(value, null);
+ }
+
+ static Expected any(Class> type) {
+ return new Expected(null, type);
+ }
+ }
+
+ private static final Set INTEGRAL = Set.of("integer");
+ private static final Set REAL = Set.of("real");
+ private static final Set NUMERIC = Set.of("integer", "real");
+ private static final Set TEXT = Set.of("text");
+ private static final Set BLOB = Set.of("blob");
+ private static final Set UNSPECIFIED = Set.of();
+
+ private static final List B2_ROWS = List.of(
+ new WriteRow("String", "abc", "VARCHAR", Types.VARCHAR, TEXT,
+ ResultSet::getString, Expected.of("abc")),
+ new WriteRow("BigDecimal", new BigDecimal("7"), "NUMERIC", Types.NUMERIC, NUMERIC,
+ ResultSet::getBigDecimal, Expected.of(new BigDecimal("7"))),
+ new WriteRow("boolean", true, "BOOLEAN", Types.BOOLEAN, INTEGRAL,
+ ResultSet::getBoolean, Expected.of(true)),
+ new WriteRow("byte", (byte) 7, "TINYINT", Types.TINYINT, INTEGRAL,
+ ResultSet::getByte, Expected.of((byte) 7)),
+ new WriteRow("short", (short) 7, "SMALLINT", Types.SMALLINT, INTEGRAL,
+ ResultSet::getShort, Expected.of((short) 7)),
+ new WriteRow("int", 7, "INTEGER", Types.INTEGER, INTEGRAL,
+ ResultSet::getInt, Expected.of(7)),
+ new WriteRow("long", 7L, "BIGINT", Types.BIGINT, INTEGRAL,
+ ResultSet::getLong, Expected.of(7L)),
+ new WriteRow("float", 7.0f, "REAL", Types.REAL, REAL,
+ ResultSet::getFloat, Expected.of(7.0f)),
+ new WriteRow("double", 7.0d, "DOUBLE", Types.DOUBLE, REAL,
+ ResultSet::getDouble, Expected.of(7.0d)),
+ new WriteRow("byte[]", new byte[] {1, 2, 3}, "VARBINARY", Types.VARBINARY, BLOB,
+ ResultSet::getBytes, Expected.of(new byte[] {1, 2, 3})),
+ new WriteRow("java.sql.Date", Date.valueOf("2024-01-02"), "DATE", Types.DATE, TEXT,
+ ResultSet::getDate, Expected.of(Date.valueOf("2024-01-02"))),
+ new WriteRow("java.sql.Time", Time.valueOf("03:04:05"), "TIME", Types.TIME, TEXT,
+ ResultSet::getTime, Expected.of(Time.valueOf("03:04:05"))),
+ new WriteRow("java.sql.Timestamp", Timestamp.valueOf("2024-01-02 03:04:05"),
+ "TIMESTAMP", Types.TIMESTAMP, TEXT, ResultSet::getTimestamp,
+ Expected.of(Timestamp.valueOf("2024-01-02 03:04:05"))),
+ new WriteRow("java.sql.Clob", clob("abc"), "CLOB", Types.CLOB, TEXT,
+ Jdbc4AppendixBTypeMappingIT::clobText, Expected.of("abc")),
+ new WriteRow("java.sql.Blob", blob(new byte[] {1, 2, 3}), "BLOB", Types.BLOB, BLOB,
+ Jdbc4AppendixBTypeMappingIT::blobBytes, Expected.of(new byte[] {1, 2, 3})),
+ new WriteRow("java.sql.Array", stub(Array.class), "ARRAY", Types.ARRAY, UNSPECIFIED,
+ ResultSet::getArray, Expected.any(Array.class)),
+ new WriteRow("java.sql.Struct", stub(Struct.class), "STRUCT", Types.STRUCT, UNSPECIFIED,
+ (rs, i) -> rs.getObject(i), Expected.any(Struct.class)),
+ new WriteRow("java.sql.Ref", stub(Ref.class), "REF", Types.REF, UNSPECIFIED,
+ ResultSet::getRef, Expected.any(Ref.class)),
+ new WriteRow("java.net.URL", url("https://example.com"), "DATALINK", Types.DATALINK,
+ TEXT, ResultSet::getURL, Expected.any(URL.class)),
+ new WriteRow("Java class", new Marker("abc"), "JAVA_OBJECT", Types.JAVA_OBJECT,
+ UNSPECIFIED, (rs, i) -> rs.getObject(i), Expected.of(new Marker("abc"))),
+ new WriteRow("java.sql.RowId", stub(RowId.class), "ROWID", Types.ROWID, UNSPECIFIED,
+ ResultSet::getRowId, Expected.any(RowId.class)),
+ new WriteRow("java.sql.NClob", nclob("abc"), "NCLOB", Types.NCLOB, TEXT,
+ ResultSet::getNClob, Expected.any(NClob.class)),
+ new WriteRow("java.sql.SQLXML", stub(SQLXML.class), "SQLXML", Types.SQLXML, UNSPECIFIED,
+ ResultSet::getSQLXML, Expected.any(SQLXML.class)));
+
+ @ParameterizedTest(name = "B-2 {0} -> {2}")
+ @MethodSource("b2Rows")
+ void b2StandardSettersMapJavaTypeToJdbcType(WriteRow row) throws SQLException {
+ String table = table("b2");
+ try (Connection connection = connect(); Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE " + table + " (v \"" + row.targetName() + "\")");
+ try (PreparedStatement insert =
+ connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) {
+ try {
+ bindB2(insert, row);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ "B-2: " + row.name() + " -> " + row.targetName() + " bind failed: " + e,
+ e);
+ }
+ insert.executeUpdate();
+ }
+ verifyRoundTrip(connection, table, row, "B-2");
+ }
+ }
+
+ static Stream b2Rows() {
+ return B2_ROWS.stream();
+ }
+
+ private static void bindB2(PreparedStatement statement, WriteRow row) throws SQLException {
+ switch (row.name()) {
+ case "String" -> statement.setString(1, (String) row.value());
+ case "BigDecimal" -> statement.setBigDecimal(1, (BigDecimal) row.value());
+ case "boolean" -> statement.setBoolean(1, (boolean) row.value());
+ case "byte" -> statement.setByte(1, (byte) row.value());
+ case "short" -> statement.setShort(1, (short) row.value());
+ case "int" -> statement.setInt(1, (int) row.value());
+ case "long" -> statement.setLong(1, (long) row.value());
+ case "float" -> statement.setFloat(1, (float) row.value());
+ case "double" -> statement.setDouble(1, (double) row.value());
+ case "byte[]" -> statement.setBytes(1, (byte[]) row.value());
+ case "java.sql.Date" -> statement.setDate(1, (Date) row.value());
+ case "java.sql.Time" -> statement.setTime(1, (Time) row.value());
+ case "java.sql.Timestamp" -> statement.setTimestamp(1, (Timestamp) row.value());
+ case "java.sql.Clob" -> statement.setClob(1, (Clob) row.value());
+ case "java.sql.Blob" -> statement.setBlob(1, (Blob) row.value());
+ case "java.sql.Array" -> statement.setArray(1, (Array) row.value());
+ case "java.sql.Struct", "Java class" -> statement.setObject(1, row.value());
+ case "java.sql.Ref" -> statement.setRef(1, (Ref) row.value());
+ case "java.net.URL" -> statement.setURL(1, (URL) row.value());
+ case "java.sql.RowId" -> statement.setRowId(1, (RowId) row.value());
+ case "java.sql.NClob" -> statement.setNClob(1, (NClob) row.value());
+ case "java.sql.SQLXML" -> statement.setSQLXML(1, (SQLXML) row.value());
+ default -> throw new IllegalArgumentException(row.name());
+ }
+ }
+
+ // =====================================================================
+ // Table B-4: setObject without a target type maps a Java object type.
+ // =====================================================================
+
+ private static final Calendar CALENDAR = calendar2024();
+ private static final java.util.Date UTIL_DATE = new java.util.Date(CALENDAR.getTimeInMillis());
+
+ private static Calendar calendar2024() {
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ calendar.clear();
+ calendar.set(2024, Calendar.JANUARY, 2, 3, 4, 5);
+ return calendar;
+ }
+
+ private static final List B4_ROWS = List.of(
+ new WriteRow("String", "abc", "VARCHAR", Types.VARCHAR, TEXT,
+ ResultSet::getString, Expected.of("abc")),
+ new WriteRow("BigDecimal", new BigDecimal("7"), "NUMERIC", Types.NUMERIC, NUMERIC,
+ ResultSet::getBigDecimal, Expected.of(new BigDecimal("7"))),
+ new WriteRow("Boolean", true, "BOOLEAN", Types.BOOLEAN, INTEGRAL,
+ ResultSet::getBoolean, Expected.of(true)),
+ new WriteRow("Byte", (byte) 7, "TINYINT", Types.TINYINT, INTEGRAL,
+ ResultSet::getByte, Expected.of((byte) 7)),
+ new WriteRow("Short", (short) 7, "SMALLINT", Types.SMALLINT, INTEGRAL,
+ ResultSet::getShort, Expected.of((short) 7)),
+ new WriteRow("Integer", 7, "INTEGER", Types.INTEGER, INTEGRAL,
+ ResultSet::getInt, Expected.of(7)),
+ new WriteRow("Long", 7L, "BIGINT", Types.BIGINT, INTEGRAL,
+ ResultSet::getLong, Expected.of(7L)),
+ new WriteRow("Float", 7.0f, "REAL", Types.REAL, REAL,
+ ResultSet::getFloat, Expected.of(7.0f)),
+ new WriteRow("Double", 7.0d, "DOUBLE", Types.DOUBLE, REAL,
+ ResultSet::getDouble, Expected.of(7.0d)),
+ new WriteRow("byte[]", new byte[] {1, 2, 3}, "VARBINARY", Types.VARBINARY, BLOB,
+ ResultSet::getBytes, Expected.of(new byte[] {1, 2, 3})),
+ new WriteRow("java.math.BigInteger", BigInteger.valueOf(7), "BIGINT", Types.BIGINT,
+ INTEGRAL, ResultSet::getLong, Expected.of(7L)),
+ new WriteRow("java.sql.Date", Date.valueOf("2024-01-02"), "DATE", Types.DATE, TEXT,
+ ResultSet::getDate, Expected.of(Date.valueOf("2024-01-02"))),
+ new WriteRow("java.sql.Time", Time.valueOf("03:04:05"), "TIME", Types.TIME, TEXT,
+ ResultSet::getTime, Expected.of(Time.valueOf("03:04:05"))),
+ new WriteRow("java.sql.Timestamp", Timestamp.valueOf("2024-01-02 03:04:05"),
+ "TIMESTAMP", Types.TIMESTAMP, TEXT, ResultSet::getTimestamp,
+ Expected.of(Timestamp.valueOf("2024-01-02 03:04:05"))),
+ new WriteRow("java.sql.Clob", clob("abc"), "CLOB", Types.CLOB, TEXT,
+ Jdbc4AppendixBTypeMappingIT::clobText, Expected.of("abc")),
+ new WriteRow("java.sql.Blob", blob(new byte[] {1, 2, 3}), "BLOB", Types.BLOB, BLOB,
+ Jdbc4AppendixBTypeMappingIT::blobBytes, Expected.of(new byte[] {1, 2, 3})),
+ new WriteRow("java.sql.Array", stub(Array.class), "ARRAY", Types.ARRAY, UNSPECIFIED,
+ ResultSet::getArray, Expected.any(Array.class)),
+ new WriteRow("java.sql.Struct", stub(Struct.class), "STRUCT", Types.STRUCT, UNSPECIFIED,
+ (rs, i) -> rs.getObject(i), Expected.any(Struct.class)),
+ new WriteRow("java.sql.Ref", stub(Ref.class), "REF", Types.REF, UNSPECIFIED,
+ ResultSet::getRef, Expected.any(Ref.class)),
+ new WriteRow("java.net.URL", url("https://example.com"), "DATALINK", Types.DATALINK,
+ TEXT, ResultSet::getURL, Expected.any(URL.class)),
+ new WriteRow("Java class", new Marker("abc"), "JAVA_OBJECT", Types.JAVA_OBJECT,
+ UNSPECIFIED, (rs, i) -> rs.getObject(i), Expected.of(new Marker("abc"))),
+ new WriteRow("java.sql.RowId", stub(RowId.class), "ROWID", Types.ROWID, UNSPECIFIED,
+ ResultSet::getRowId, Expected.any(RowId.class)),
+ new WriteRow("java.sql.NClob", nclob("abc"), "NCLOB", Types.NCLOB, TEXT,
+ ResultSet::getNClob, Expected.any(NClob.class)),
+ new WriteRow("java.sql.SQLXML", stub(SQLXML.class), "SQLXML", Types.SQLXML, UNSPECIFIED,
+ ResultSet::getSQLXML, Expected.any(SQLXML.class)),
+ new WriteRow("java.util.Calendar", CALENDAR, "TIMESTAMP", Types.TIMESTAMP, TEXT,
+ ResultSet::getTimestamp,
+ Expected.of(Timestamp.valueOf(localDateTime(CALENDAR.toInstant())))),
+ new WriteRow("java.util.Date", UTIL_DATE, "TIMESTAMP", Types.TIMESTAMP, TEXT,
+ ResultSet::getTimestamp,
+ Expected.of(Timestamp.valueOf(localDateTime(UTIL_DATE.toInstant())))),
+ new WriteRow("java.time.LocalDate", LocalDate.of(2024, 1, 2), "DATE", Types.DATE,
+ TEXT, ResultSet::getDate, Expected.of(Date.valueOf("2024-01-02"))),
+ new WriteRow("java.time.LocalTime", LocalTime.of(3, 4, 5), "TIME", Types.TIME, TEXT,
+ ResultSet::getTime, Expected.of(Time.valueOf("03:04:05"))),
+ new WriteRow("java.time.LocalDateTime", LocalDateTime.of(2024, 1, 2, 3, 4, 5),
+ "TIMESTAMP", Types.TIMESTAMP, TEXT, ResultSet::getTimestamp,
+ Expected.of(Timestamp.valueOf("2024-01-02 03:04:05"))),
+ new WriteRow("java.time.OffsetTime", OffsetTime.of(3, 4, 5, 0, ZoneOffset.UTC),
+ "TIME_WITH_TIMEZONE", Types.TIME_WITH_TIMEZONE, TEXT,
+ (rs, i) -> rs.getObject(i, OffsetTime.class),
+ Expected.of(OffsetTime.of(3, 4, 5, 0, ZoneOffset.UTC))),
+ new WriteRow("java.time.OffsetDateTime",
+ OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC),
+ "TIMESTAMP_WITH_TIMEZONE", Types.TIMESTAMP_WITH_TIMEZONE, TEXT,
+ (rs, i) -> rs.getObject(i, OffsetDateTime.class),
+ Expected.of(OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC))));
+
+ @ParameterizedTest(name = "B-4 {0} -> {2}")
+ @MethodSource("b4Rows")
+ void b4SetObjectMapsJavaObjectTypeToJdbcType(WriteRow row) throws SQLException {
+ String table = table("b4");
+ try (Connection connection = connect(); Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE " + table + " (v \"" + row.targetName() + "\")");
+ try (PreparedStatement insert =
+ connection.prepareStatement("INSERT INTO " + table + " (v) VALUES (?)")) {
+ insert.setObject(1, row.value());
+ insert.executeUpdate();
+ }
+ verifyRoundTrip(connection, table, row, "B-4");
+ }
+ }
+
+ static Stream b4Rows() {
+ return B4_ROWS.stream();
+ }
+
+ // =====================================================================
+ // Table B-5: setObject with a target type converts between Java and JDBC types.
+ // =====================================================================
+
+ record Target(String name, int sqlType) {}
+
+ private static final List TARGETS = List.of(
+ target("TINYINT", Types.TINYINT),
+ target("SMALLINT", Types.SMALLINT),
+ target("INTEGER", Types.INTEGER),
+ target("BIGINT", Types.BIGINT),
+ target("REAL", Types.REAL),
+ target("FLOAT", Types.FLOAT),
+ target("DOUBLE", Types.DOUBLE),
+ target("DECIMAL", Types.DECIMAL),
+ target("NUMERIC", Types.NUMERIC),
+ target("BIT", Types.BIT),
+ target("BOOLEAN", Types.BOOLEAN),
+ target("CHAR", Types.CHAR),
+ target("VARCHAR", Types.VARCHAR),
+ target("LONGVARCHAR", Types.LONGVARCHAR),
+ target("BINARY", Types.BINARY),
+ target("VARBINARY", Types.VARBINARY),
+ target("LONGVARBINARY", Types.LONGVARBINARY),
+ target("DATE", Types.DATE),
+ target("TIME", Types.TIME),
+ target("TIMESTAMP", Types.TIMESTAMP),
+ target("NCHAR", Types.NCHAR),
+ target("NVARCHAR", Types.NVARCHAR),
+ target("LONGNVARCHAR", Types.LONGNVARCHAR),
+ target("ARRAY", Types.ARRAY),
+ target("BLOB", Types.BLOB),
+ target("CLOB", Types.CLOB),
+ target("STRUCT", Types.STRUCT),
+ target("REF", Types.REF),
+ target("DATALINK", Types.DATALINK),
+ target("JAVA_OBJECT", Types.JAVA_OBJECT),
+ target("ROWID", Types.ROWID),
+ target("NCLOB", Types.NCLOB),
+ target("SQLXML", Types.SQLXML),
+ target("TIME_WITH_TIMEZONE", Types.TIME_WITH_TIMEZONE),
+ target("TIMESTAMP_WITH_TIMEZONE", Types.TIMESTAMP_WITH_TIMEZONE));
+
+ private static Target target(String name, int sqlType) {
+ return new Target(name, sqlType);
+ }
+
+ record JavaRow(String name, Object value, Set targets) {}
+
+ private static final Set NUMERIC_TEXT_TARGETS = Set.of(
+ Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.BIGINT, Types.REAL, Types.FLOAT,
+ Types.DOUBLE, Types.DECIMAL, Types.NUMERIC, Types.BIT, Types.BOOLEAN, Types.CHAR,
+ Types.VARCHAR, Types.LONGVARCHAR);
+
+ private static Set set(int... values) {
+ return Arrays.stream(values).boxed().collect(Collectors.toUnmodifiableSet());
+ }
+
+ private static Set union(Set base, int... extra) {
+ Set result = new java.util.HashSet<>(base);
+ for (int value : extra) {
+ result.add(value);
+ }
+ return Set.copyOf(result);
+ }
+
+ private static final List B5_ROWS = List.of(
+ new JavaRow("String", "42", union(NUMERIC_TEXT_TARGETS, Types.BINARY, Types.VARBINARY,
+ Types.LONGVARBINARY, Types.DATE, Types.TIME, Types.TIMESTAMP, Types.NCHAR,
+ Types.NVARCHAR, Types.LONGNVARCHAR)),
+ new JavaRow("BigDecimal", new BigDecimal("7"), NUMERIC_TEXT_TARGETS),
+ new JavaRow("Boolean", Boolean.TRUE, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Byte", (byte) 7, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Short", (short) 7, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Integer", 7, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Long", 7L, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Float", 7.0f, NUMERIC_TEXT_TARGETS),
+ new JavaRow("Double", 7.0d, NUMERIC_TEXT_TARGETS),
+ new JavaRow("byte[]", new byte[] {1, 2, 3},
+ set(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY)),
+ new JavaRow("BigInteger", BigInteger.valueOf(7),
+ set(Types.BIGINT, Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR)),
+ new JavaRow("java.sql.Date", Date.valueOf("2024-01-02"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIMESTAMP)),
+ new JavaRow("java.sql.Time", Time.valueOf("03:04:05"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME, Types.TIMESTAMP)),
+ new JavaRow("java.sql.Timestamp", Timestamp.valueOf("2024-01-02 03:04:05"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP)),
+ new JavaRow("java.sql.Array", stub(Array.class), set(Types.ARRAY)),
+ new JavaRow("java.sql.Blob", blob(new byte[] {1, 2, 3}), set(Types.BLOB)),
+ new JavaRow("java.sql.Clob", clob("abc"), set(Types.CLOB)),
+ new JavaRow("java.sql.Struct", stub(Struct.class), set(Types.STRUCT)),
+ new JavaRow("java.sql.Ref", stub(Ref.class), set(Types.REF)),
+ new JavaRow("java.net.URL", url("https://example.com"), set(Types.DATALINK)),
+ new JavaRow("Java class", new Marker("abc"), set(Types.JAVA_OBJECT)),
+ new JavaRow("java.sql.RowId", stub(RowId.class), set(Types.ROWID)),
+ new JavaRow("java.sql.NClob", nclob("abc"), set(Types.NCLOB)),
+ new JavaRow("java.sql.SQLXML", stub(SQLXML.class), set(Types.SQLXML)),
+ new JavaRow("java.util.Calendar", CALENDAR,
+ union(set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP), Types.ARRAY)),
+ new JavaRow("java.util.Date", UTIL_DATE,
+ union(set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP), Types.ARRAY)),
+ new JavaRow("java.time.LocalDate", LocalDate.of(2024, 1, 2),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE)),
+ new JavaRow("java.time.LocalTime", LocalTime.of(3, 4, 5),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME)),
+ new JavaRow("java.time.LocalDateTime", LocalDateTime.of(2024, 1, 2, 3, 4, 5),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP)),
+ new JavaRow("java.time.OffsetTime", OffsetTime.of(3, 4, 5, 0, ZoneOffset.UTC),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME_WITH_TIMEZONE)),
+ new JavaRow("java.time.OffsetDateTime",
+ OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME_WITH_TIMEZONE,
+ Types.TIMESTAMP_WITH_TIMEZONE)));
+
+ @ParameterizedTest(name = "B-5 {0} to every listed target")
+ @MethodSource("b5Rows")
+ void b5SetObjectConvertsToExplicitTargetType(JavaRow row) throws SQLException {
+ String table = table("b5");
+ String columns = TARGETS.stream()
+ .map(target -> "\"" + target.name() + "\" \"" + target.name() + "\"")
+ .collect(Collectors.joining(", "));
+ String placeholders = TARGETS.stream().map(ignored -> "?").collect(Collectors.joining(", "));
+ String select = TARGETS.stream()
+ .map(target -> "typeof(\"" + target.name() + "\"), \"" + target.name() + "\"")
+ .collect(Collectors.joining(", "));
+
+ try (Connection connection = connect(); Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE " + table + " (id INTEGER, " + columns + ")");
+ try (PreparedStatement insert = connection.prepareStatement(
+ "INSERT INTO " + table + " (id, " + String.join(", ", columnNames()) + ") VALUES (1, "
+ + placeholders + ")")) {
+ int parameter = 1;
+ for (Target target : TARGETS) {
+ if (row.targets().contains(target.sqlType())) {
+ insert.setObject(parameter++, sourceFor(row, target), target.sqlType());
+ } else {
+ insert.setNull(parameter++, target.sqlType());
+ }
+ }
+ insert.executeUpdate();
+ }
+
+ try (Statement query = connection.createStatement();
+ ResultSet resultSet =
+ query.executeQuery("SELECT " + select + " FROM " + table + " WHERE id = 1")) {
+ assertThat(resultSet.next()).isTrue();
+ List checks = new ArrayList<>();
+ int targetIndex = 0;
+ for (Target target : TARGETS) {
+ int typeofColumn = targetIndex * 2 + 1;
+ int valueColumn = typeofColumn + 1;
+ targetIndex++;
+ if (!row.targets().contains(target.sqlType())) {
+ continue;
+ }
+ checks.add(() -> {
+ Set storage = storageOf(target.name());
+ if (!storage.isEmpty()) {
+ assertThat(resultSet.getString(typeofColumn))
+ .as("B-5: %s -> %s storage class", row.name(), target.name())
+ .isIn(storage);
+ }
+ Object actual;
+ try {
+ actual = readFor(target).read(resultSet, valueColumn);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ "B-5: " + row.name() + " -> " + target.name()
+ + " read failed: " + e,
+ e);
+ }
+ assertExpected(actual, expectedFor(row, target), "B-5", row.name(), target.name());
+ });
+ }
+ assertAll("B-5 " + row.name(), checks);
+ }
+ }
+ }
+
+ static Stream b5Rows() {
+ return B5_ROWS.stream();
+ }
+
+ private static List columnNames() {
+ return TARGETS.stream().map(target -> "\"" + target.name() + "\"").toList();
+ }
+
+ // =====================================================================
+ // Round-trip helpers.
+ // =====================================================================
+
+ private static void verifyRoundTrip(
+ Connection connection, String table, WriteRow row, String tableName) throws SQLException {
+ try (Statement query = connection.createStatement();
+ ResultSet resultSet = query.executeQuery("SELECT typeof(v), v FROM " + table)) {
+ assertThat(resultSet.next()).isTrue();
+ if (!row.storage().isEmpty()) {
+ assertThat(resultSet.getString(1))
+ .as("%s: %s -> %s storage class", tableName, row.name(), row.targetName())
+ .isIn(row.storage());
+ }
+ Object actual;
+ try {
+ actual = row.reader().read(resultSet, 2);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ tableName + ": " + row.name() + " -> " + row.targetName() + " read failed: " + e,
+ e);
+ }
+ assertExpected(actual, row.expected(), tableName, row.name(), row.targetName());
+ }
+ }
+
+ private static void assertExpected(
+ Object actual, Expected expected, String table, String row, String target) {
+ if (expected.anyType() != null) {
+ assertThat(actual)
+ .as("%s: %s -> %s must be a %s", table, row, target, expected.anyType())
+ .isInstanceOf(expected.anyType());
+ return;
+ }
+ Object value = expected.value();
+ if (value instanceof BigDecimal decimal) {
+ assertThat((BigDecimal) actual)
+ .as("%s: %s -> %s", table, row, target)
+ .isEqualByComparingTo(decimal);
+ return;
+ }
+ if (value != null && value.getClass().isArray()) {
+ assertThat(actual).as("%s: %s -> %s", table, row, target).isEqualTo(value);
+ return;
+ }
+ assertThat(actual).as("%s: %s -> %s", table, row, target).isEqualTo(value);
+ }
+
+ private static Set storageOf(String targetName) {
+ return switch (targetName) {
+ case "TINYINT", "SMALLINT", "INTEGER", "BIGINT", "BIT", "BOOLEAN" -> INTEGRAL;
+ case "REAL", "FLOAT", "DOUBLE" -> REAL;
+ case "DECIMAL", "NUMERIC" -> NUMERIC;
+ case "CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR", "DATE",
+ "TIME", "TIMESTAMP", "CLOB", "NCLOB", "DATALINK", "TIME_WITH_TIMEZONE",
+ "TIMESTAMP_WITH_TIMEZONE" ->
+ TEXT;
+ case "BINARY", "VARBINARY", "LONGVARBINARY", "BLOB" -> BLOB;
+ default -> UNSPECIFIED;
+ };
+ }
+
+ private static SqlRead readFor(Target target) {
+ return switch (target.name()) {
+ case "TINYINT" -> ResultSet::getByte;
+ case "SMALLINT" -> ResultSet::getShort;
+ case "INTEGER" -> ResultSet::getInt;
+ case "BIGINT" -> ResultSet::getLong;
+ case "REAL" -> ResultSet::getFloat;
+ case "FLOAT", "DOUBLE" -> ResultSet::getDouble;
+ case "DECIMAL", "NUMERIC" -> ResultSet::getBigDecimal;
+ case "BIT", "BOOLEAN" -> ResultSet::getBoolean;
+ case "CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR" ->
+ ResultSet::getString;
+ case "BINARY", "VARBINARY", "LONGVARBINARY" -> ResultSet::getBytes;
+ case "DATE" -> ResultSet::getDate;
+ case "TIME" -> ResultSet::getTime;
+ case "TIMESTAMP" -> ResultSet::getTimestamp;
+ case "CLOB" -> Jdbc4AppendixBTypeMappingIT::clobText;
+ case "BLOB" -> Jdbc4AppendixBTypeMappingIT::blobBytes;
+ case "NCLOB" -> (rs, i) -> {
+ NClob nclob = rs.getNClob(i);
+ return nclob == null ? null : nclob.getSubString(1, (int) nclob.length());
+ };
+ case "ARRAY" -> ResultSet::getArray;
+ case "STRUCT" -> (rs, i) -> rs.getObject(i);
+ case "REF" -> ResultSet::getRef;
+ case "DATALINK" -> ResultSet::getURL;
+ case "JAVA_OBJECT" -> (rs, i) -> rs.getObject(i);
+ case "ROWID" -> ResultSet::getRowId;
+ case "SQLXML" -> ResultSet::getSQLXML;
+ case "TIME_WITH_TIMEZONE" -> (rs, i) -> rs.getObject(i, OffsetTime.class);
+ case "TIMESTAMP_WITH_TIMEZONE" -> (rs, i) -> rs.getObject(i, OffsetDateTime.class);
+ default -> throw new IllegalArgumentException(target.name());
+ };
+ }
+
+ private static Object sourceFor(JavaRow row, Target target) {
+ if (row.value() instanceof String) {
+ return switch (target.name()) {
+ case "DATE" -> "2024-01-02";
+ case "TIME" -> "03:04:05";
+ case "TIMESTAMP" -> "2024-01-02 03:04:05";
+ case "BINARY", "VARBINARY", "LONGVARBINARY" -> "abc";
+ case "BIT", "BOOLEAN" -> "true";
+ default -> "42";
+ };
+ }
+ return row.value();
+ }
+
+ private static Expected expectedFor(JavaRow row, Target target) {
+ Object source = sourceFor(row, target);
+ return switch (target.name()) {
+ case "TINYINT" -> Expected.of((byte) number(source));
+ case "SMALLINT" -> Expected.of((short) number(source));
+ case "INTEGER" -> Expected.of((int) number(source));
+ case "BIGINT" -> Expected.of((long) number(source));
+ case "REAL" -> Expected.of((float) number(source));
+ case "FLOAT", "DOUBLE" -> Expected.of(number(source));
+ case "DECIMAL", "NUMERIC" -> Expected.of(BigDecimal.valueOf(number(source)));
+ case "BIT", "BOOLEAN" -> Expected.of(number(source) != 0);
+ case "CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR" ->
+ textExpected(source);
+ case "DATE" -> dateExpected(source);
+ case "TIME" -> timeExpected(source);
+ case "TIMESTAMP" -> timestampExpected(source);
+ case "CLOB" -> Expected.of(clobTextValue(source));
+ case "NCLOB" -> Expected.any(NClob.class);
+ case "BLOB" -> Expected.of(bytesForm(source));
+ case "BINARY", "VARBINARY", "LONGVARBINARY" -> Expected.of(bytesForm(source));
+ case "DATALINK" -> Expected.any(URL.class);
+ case "ARRAY" -> Expected.any(Array.class);
+ case "STRUCT" -> Expected.any(Struct.class);
+ case "REF" -> Expected.any(Ref.class);
+ case "JAVA_OBJECT" -> Expected.of(source);
+ case "ROWID" -> Expected.any(RowId.class);
+ case "SQLXML" -> Expected.any(SQLXML.class);
+ case "TIME_WITH_TIMEZONE" -> offsetTimeExpected(source);
+ case "TIMESTAMP_WITH_TIMEZONE" -> offsetDateTimeExpected(source);
+ default -> throw new IllegalArgumentException(target.name());
+ };
+ }
+
+ private static Expected textExpected(Object source) {
+ // The appendix requires the Java type but leaves the text representation open for
+ // Calendar, java.util.Date and Boolean.
+ if (source instanceof Calendar
+ || source.getClass() == java.util.Date.class
+ || source instanceof Boolean) {
+ return Expected.any(String.class);
+ }
+ return Expected.of(textForm(source));
+ }
+
+ private static Expected dateExpected(Object source) {
+ if (source instanceof Date date) {
+ return Expected.of(date);
+ }
+ if (source instanceof Timestamp timestamp) {
+ return Expected.of(Date.valueOf(timestamp.toLocalDateTime().toLocalDate()));
+ }
+ if (source instanceof LocalDate localDate) {
+ return Expected.of(Date.valueOf(localDate));
+ }
+ if (source instanceof LocalDateTime localDateTime) {
+ return Expected.of(Date.valueOf(localDateTime.toLocalDate()));
+ }
+ if (source instanceof java.util.Date date) {
+ return Expected.of(Date.valueOf(LocalDate.ofInstant(date.toInstant(), ZoneId.systemDefault())));
+ }
+ if (source instanceof Calendar calendar) {
+ return Expected.of(Date.valueOf(LocalDate.ofInstant(calendar.toInstant(), ZoneId.systemDefault())));
+ }
+ if (source instanceof String text) {
+ return Expected.of(Date.valueOf(LocalDate.parse(text.substring(0, 10))));
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static Expected timeExpected(Object source) {
+ if (source instanceof Time time) {
+ return Expected.of(time);
+ }
+ if (source instanceof Timestamp timestamp) {
+ return Expected.of(Time.valueOf(timestamp.toLocalDateTime().toLocalTime()));
+ }
+ if (source instanceof LocalTime localTime) {
+ return Expected.of(Time.valueOf(localTime));
+ }
+ if (source instanceof LocalDateTime localDateTime) {
+ return Expected.of(Time.valueOf(localDateTime.toLocalTime()));
+ }
+ if (source instanceof java.util.Date date) {
+ return Expected.of(Time.valueOf(LocalTime.ofInstant(date.toInstant(), ZoneId.systemDefault())));
+ }
+ if (source instanceof Calendar calendar) {
+ return Expected.of(Time.valueOf(LocalTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault())));
+ }
+ if (source instanceof String text) {
+ return Expected.of(Time.valueOf(LocalTime.parse(text.substring(text.length() - 8))));
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static Expected timestampExpected(Object source) {
+ if (source instanceof Timestamp timestamp) {
+ return Expected.of(timestamp);
+ }
+ if (source instanceof Date date) {
+ return Expected.of(Timestamp.valueOf(date.toLocalDate().atStartOfDay()));
+ }
+ if (source instanceof LocalDate localDate) {
+ return Expected.of(Timestamp.valueOf(localDate.atStartOfDay()));
+ }
+ if (source instanceof LocalDateTime localDateTime) {
+ return Expected.of(Timestamp.valueOf(localDateTime));
+ }
+ // The date part of a Time to TIMESTAMP conversion is implementation defined.
+ if (source instanceof Time) {
+ return Expected.any(Timestamp.class);
+ }
+ if (source instanceof java.util.Date date) {
+ return Expected.of(Timestamp.valueOf(localDateTime(date.toInstant())));
+ }
+ if (source instanceof Calendar calendar) {
+ return Expected.of(Timestamp.valueOf(localDateTime(calendar.toInstant())));
+ }
+ if (source instanceof String text) {
+ return Expected.of(Timestamp.valueOf(text.replace('T', ' ')));
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static Expected offsetTimeExpected(Object source) {
+ if (source instanceof OffsetTime offsetTime) {
+ return Expected.of(offsetTime);
+ }
+ if (source instanceof OffsetDateTime offsetDateTime) {
+ return Expected.of(offsetDateTime.toOffsetTime());
+ }
+ if (source instanceof String text) {
+ return Expected.of(OffsetTime.parse(text));
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static Expected offsetDateTimeExpected(Object source) {
+ if (source instanceof OffsetDateTime offsetDateTime) {
+ return Expected.of(offsetDateTime);
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static String clobTextValue(Object source) {
+ if (source instanceof Clob clob) {
+ try {
+ return clob.getSubString(1, (int) clob.length());
+ } catch (SQLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return textForm(source);
+ }
+
+ private static String textForm(Object source) {
+ return switch (source) {
+ case String text -> text;
+ case BigDecimal decimal -> decimal.toPlainString();
+ case BigInteger integer -> integer.toString();
+ case Boolean bool -> bool.toString();
+ case Number number -> String.valueOf(number);
+ case Date date -> date.toString();
+ case Time time -> time.toString();
+ case Timestamp timestamp -> timestamp.toString();
+ case LocalDate localDate -> localDate.toString();
+ case LocalTime localTime -> localTime.toString();
+ case LocalDateTime localDateTime -> localDateTime.toString();
+ case OffsetTime offsetTime -> offsetTime.toString();
+ case OffsetDateTime offsetDateTime -> offsetDateTime.toString();
+ default -> throw new IllegalArgumentException(source.getClass().getName());
+ };
+ }
+
+ private static byte[] bytesForm(Object source) {
+ if (source instanceof byte[] bytes) {
+ return bytes;
+ }
+ if (source instanceof String text) {
+ return text.getBytes(StandardCharsets.UTF_8);
+ }
+ if (source instanceof Blob blob) {
+ try {
+ return blob.getBytes(1, (int) blob.length());
+ } catch (SQLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static double number(Object source) {
+ if (source instanceof Number number) {
+ return number.doubleValue();
+ }
+ if (source instanceof Boolean bool) {
+ return bool ? 1 : 0;
+ }
+ if (source instanceof String text) {
+ if ("true".equalsIgnoreCase(text)) {
+ return 1;
+ }
+ if ("false".equalsIgnoreCase(text)) {
+ return 0;
+ }
+ return Double.parseDouble(text);
+ }
+ throw new IllegalArgumentException(source.getClass().getName());
+ }
+
+ private static LocalDateTime localDateTime(Instant instant) {
+ return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+ }
+
+ private static String clobText(ResultSet resultSet, int column) throws SQLException {
+ Clob clob = resultSet.getClob(column);
+ return clob == null ? null : clob.getSubString(1, (int) clob.length());
+ }
+
+ private static byte[] blobBytes(ResultSet resultSet, int column) throws SQLException {
+ Blob blob = resultSet.getBlob(column);
+ if (blob == null) {
+ return null;
+ }
+ try {
+ return blob.getBinaryStream().readAllBytes();
+ } catch (java.io.IOException e) {
+ throw new SQLException(e);
+ }
+ }
+
+ private static Class> boxed(Class> type) {
+ if (!type.isPrimitive()) {
+ return type;
+ }
+ return switch (type.getName()) {
+ case "byte" -> Byte.class;
+ case "short" -> Short.class;
+ case "int" -> Integer.class;
+ case "long" -> Long.class;
+ case "float" -> Float.class;
+ case "double" -> Double.class;
+ case "boolean" -> Boolean.class;
+ case "char" -> Character.class;
+ default -> throw new IllegalArgumentException(type.getName());
+ };
+ }
+
+ private record Marker(String value) {}
+
+ @SuppressWarnings("unchecked")
+ private static T stub(Class type) {
+ return (T) Proxy.newProxyInstance(
+ type.getClassLoader(),
+ new Class>[] {type},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "toString" -> "stub";
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ default -> defaultValue(method.getReturnType());
+ });
+ }
+
+ private static Blob blob(byte[] data) {
+ return (Blob) Proxy.newProxyInstance(
+ Blob.class.getClassLoader(),
+ new Class>[] {Blob.class},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "toString" -> "blob";
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ case "length" -> (long) data.length;
+ case "getBytes" -> data;
+ case "getBinaryStream" -> new ByteArrayInputStream(data);
+ default -> defaultValue(method.getReturnType());
+ });
+ }
+
+ private static Clob clob(String text) {
+ return (Clob) Proxy.newProxyInstance(
+ Clob.class.getClassLoader(),
+ new Class>[] {Clob.class},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "toString" -> "clob";
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ case "length" -> (long) text.length();
+ case "getSubString" -> text;
+ case "getCharacterStream" -> new StringReader(text);
+ default -> defaultValue(method.getReturnType());
+ });
+ }
+
+ private static NClob nclob(String text) {
+ return (NClob) Proxy.newProxyInstance(
+ NClob.class.getClassLoader(),
+ new Class>[] {NClob.class},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "toString" -> text;
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ case "length" -> (long) text.length();
+ case "getSubString" -> text;
+ case "getCharacterStream" -> new StringReader(text);
+ default -> defaultValue(method.getReturnType());
+ });
+ }
+
+ private static URL url(String value) {
+ try {
+ return URI.create(value).toURL();
+ } catch (java.net.MalformedURLException e) {
+ throw new IllegalArgumentException(value, e);
+ }
+ }
+
+ private static Object defaultValue(Class> type) {
+ if (!type.isPrimitive()) {
+ return null;
+ }
+ return switch (type.getName()) {
+ case "boolean" -> false;
+ case "char" -> '\0';
+ case "byte" -> (byte) 0;
+ case "short" -> (short) 0;
+ case "int" -> 0;
+ case "long" -> 0L;
+ case "float" -> 0.0f;
+ case "double" -> 0.0d;
+ default -> null;
+ };
+ }
+}
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
new file mode 100644
index 0000000..cce5622
--- /dev/null
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java
@@ -0,0 +1,614 @@
+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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+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.ResultSet;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLXML;
+import java.sql.Struct;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * JDBC 4.3 Appendix B, read side: Tables B-1, B-3 and B-6.
+ *
+ * The oracle is the specification, not sqlite-jdbc. A failure here means the driver disagrees
+ * with Appendix B; the test name names the table row, so the failure list is the gap list. Values
+ * are supplied through {@link StmtResult} metadata rather than a live server, which keeps the
+ * matrix exhaustive and fast.
+ *
+ *
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}.
+ */
+class Jdbc4AppendixBReadTest {
+
+ // ---------------------------------------------------------------------
+ // Model for the JDBC types in Tables B-1, B-3 and B-6.
+ // ---------------------------------------------------------------------
+
+ record JdbcType(
+ String name,
+ int sqlType,
+ Class> b1JavaType,
+ Class> b3ObjectType,
+ Value sample,
+ boolean inB1B3) {}
+
+ private static final Value TEXT_ABC = Value.of("abc");
+ private static final Value TEXT_DATE = Value.of("2024-01-02");
+ private static final Value TEXT_TIME = Value.of("03:04:05");
+ private static final Value TEXT_TIMESTAMP = Value.of("2024-01-02 03:04:05");
+ private static final Value TEXT_URL = Value.of("https://example.com");
+ private static final Value TEXT_ROWID = Value.of("rowid-1");
+ private static final Value TEXT_XML = Value.of("");
+ private static final Value NUMBER_42 = Value.of(42L);
+ private static final Value REAL_42_5 = Value.of(42.5);
+ private static final Value BYTES_ABC = Value.of("abc".getBytes(StandardCharsets.UTF_8));
+
+ private static final List JDBC_TYPES = List.of(
+ jdbc("CHAR", Types.CHAR, String.class, String.class, TEXT_ABC),
+ jdbc("VARCHAR", Types.VARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("LONGVARCHAR", Types.LONGVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NUMERIC", Types.NUMERIC, BigDecimal.class, BigDecimal.class, NUMBER_42),
+ jdbc("DECIMAL", Types.DECIMAL, BigDecimal.class, BigDecimal.class, NUMBER_42),
+ jdbc("BIT", Types.BIT, boolean.class, Boolean.class, NUMBER_42),
+ jdbc("BOOLEAN", Types.BOOLEAN, boolean.class, Boolean.class, NUMBER_42),
+ jdbc("TINYINT", Types.TINYINT, byte.class, Integer.class, NUMBER_42),
+ jdbc("SMALLINT", Types.SMALLINT, short.class, Integer.class, NUMBER_42),
+ jdbc("INTEGER", Types.INTEGER, int.class, Integer.class, NUMBER_42),
+ // A small value: B-3 keys the object type on the declared type, not the magnitude.
+ jdbc("BIGINT", Types.BIGINT, long.class, Long.class, NUMBER_42),
+ jdbc("REAL", Types.REAL, float.class, Float.class, REAL_42_5),
+ jdbc("FLOAT", Types.FLOAT, double.class, Double.class, REAL_42_5),
+ jdbc("DOUBLE", Types.DOUBLE, double.class, Double.class, REAL_42_5),
+ jdbc("BINARY", Types.BINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("VARBINARY", Types.VARBINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("LONGVARBINARY", Types.LONGVARBINARY, byte[].class, byte[].class, BYTES_ABC),
+ jdbc("DATE", Types.DATE, Date.class, Date.class, TEXT_DATE),
+ jdbc("TIME", Types.TIME, Time.class, Time.class, TEXT_TIME),
+ jdbc("TIMESTAMP", Types.TIMESTAMP, Timestamp.class, Timestamp.class, TEXT_TIMESTAMP),
+ jdbc("CLOB", Types.CLOB, Clob.class, Clob.class, TEXT_ABC),
+ jdbc("BLOB", Types.BLOB, Blob.class, Blob.class, BYTES_ABC),
+ jdbc("ARRAY", Types.ARRAY, Array.class, Array.class, TEXT_ABC),
+ jdbc("DISTINCT", Types.DISTINCT, String.class, String.class, TEXT_ABC),
+ jdbc("STRUCT", Types.STRUCT, Struct.class, Struct.class, TEXT_ABC),
+ jdbc("REF", Types.REF, Ref.class, Ref.class, TEXT_ABC),
+ jdbc("DATALINK", Types.DATALINK, URL.class, URL.class, TEXT_URL),
+ jdbc("JAVA_OBJECT", Types.JAVA_OBJECT, String.class, String.class, TEXT_ABC),
+ jdbc("ROWID", Types.ROWID, RowId.class, RowId.class, TEXT_ROWID),
+ jdbc("NCHAR", Types.NCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NVARCHAR", Types.NVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("LONGNVARCHAR", Types.LONGNVARCHAR, String.class, String.class, TEXT_ABC),
+ jdbc("NCLOB", Types.NCLOB, NClob.class, NClob.class, TEXT_ABC),
+ jdbc("SQLXML", Types.SQLXML, SQLXML.class, SQLXML.class, TEXT_XML),
+ new JdbcType(
+ "TIME_WITH_TIMEZONE",
+ Types.TIME_WITH_TIMEZONE,
+ null,
+ null,
+ Value.of("03:04:05+01:00"),
+ false),
+ new JdbcType(
+ "TIMESTAMP_WITH_TIMEZONE",
+ Types.TIMESTAMP_WITH_TIMEZONE,
+ null,
+ null,
+ Value.of("2024-01-02 03:04:05+01:00"),
+ false));
+
+ private static JdbcType jdbc(
+ String name, int sqlType, Class> b1JavaType, Class> b3ObjectType, Value sample) {
+ return new JdbcType(name, sqlType, b1JavaType, b3ObjectType, sample, true);
+ }
+
+ // ---------------------------------------------------------------------
+ // Table B-1: the natural getter for each JDBC type returns the B-1 Java type.
+ // ---------------------------------------------------------------------
+
+ @FunctionalInterface
+ interface SqlReader {
+ Object read(ResultSet resultSet, int column) throws SQLException;
+ }
+
+ record Accessor(String name, Class> javaType, SqlReader reader, Set supported) {}
+
+ private static final Accessor GET_BYTE =
+ new Accessor("getByte", byte.class, ResultSet::getByte, numericTextWithRowId());
+ private static final Accessor GET_SHORT =
+ new Accessor("getShort", short.class, ResultSet::getShort, numericText());
+ private static final Accessor GET_INT =
+ new Accessor("getInt", int.class, ResultSet::getInt, numericText());
+ private static final Accessor GET_LONG =
+ new Accessor("getLong", long.class, ResultSet::getLong, numericText());
+ private static final Accessor GET_FLOAT =
+ new Accessor("getFloat", float.class, ResultSet::getFloat, numericText());
+ private static final Accessor GET_DOUBLE =
+ new Accessor("getDouble", double.class, ResultSet::getDouble, numericText());
+ private static final Accessor GET_BIG_DECIMAL =
+ new Accessor("getBigDecimal", BigDecimal.class, ResultSet::getBigDecimal, numericText());
+ private static final Accessor GET_BOOLEAN =
+ new Accessor("getBoolean", boolean.class, ResultSet::getBoolean, numericText());
+ private static final Accessor GET_STRING =
+ new Accessor("getString", String.class, ResultSet::getString, stringTargets());
+ private static final Accessor GET_N_STRING =
+ new Accessor("getNString", String.class, ResultSet::getNString, stringTargets());
+ private static final Accessor GET_BYTES = new Accessor(
+ "getBytes", byte[].class, ResultSet::getBytes, set("BINARY", "VARBINARY", "LONGVARBINARY"));
+ private static final Accessor GET_DATE = new Accessor(
+ "getDate", Date.class, ResultSet::getDate, set("CHAR", "VARCHAR", "LONGVARCHAR", "DATE", "TIMESTAMP"));
+ private static final Accessor GET_TIME = new Accessor(
+ "getTime", Time.class, ResultSet::getTime, set("CHAR", "VARCHAR", "LONGVARCHAR", "TIME", "TIMESTAMP"));
+ private static final Accessor GET_TIMESTAMP = new Accessor(
+ "getTimestamp",
+ Timestamp.class,
+ ResultSet::getTimestamp,
+ set("CHAR", "VARCHAR", "LONGVARCHAR", "DATE", "TIME", "TIMESTAMP"));
+ private static final Accessor GET_ASCII_STREAM = new Accessor(
+ "getAsciiStream",
+ InputStream.class,
+ ResultSet::getAsciiStream,
+ set("CHAR", "VARCHAR", "LONGVARCHAR", "BINARY", "VARBINARY", "LONGVARBINARY", "CLOB", "NCLOB"));
+ private static final Accessor GET_BINARY_STREAM = new Accessor(
+ "getBinaryStream",
+ InputStream.class,
+ ResultSet::getBinaryStream,
+ set("BINARY", "VARBINARY", "LONGVARBINARY"));
+ private static final Accessor GET_CHARACTER_STREAM = new Accessor(
+ "getCharacterStream",
+ Reader.class,
+ ResultSet::getCharacterStream,
+ set(
+ "CHAR",
+ "VARCHAR",
+ "LONGVARCHAR",
+ "BINARY",
+ "VARBINARY",
+ "LONGVARBINARY",
+ "CLOB",
+ "NCHAR",
+ "NVARCHAR",
+ "LONGNVARCHAR",
+ "NCLOB",
+ "SQLXML"));
+ private static final Accessor GET_N_CHARACTER_STREAM = new Accessor(
+ "getNCharacterStream",
+ Reader.class,
+ ResultSet::getNCharacterStream,
+ set(
+ "CHAR",
+ "VARCHAR",
+ "LONGVARCHAR",
+ "BINARY",
+ "VARBINARY",
+ "LONGVARBINARY",
+ "CLOB",
+ "NCHAR",
+ "NVARCHAR",
+ "LONGNVARCHAR",
+ "NCLOB",
+ "SQLXML"));
+ private static final Accessor GET_CLOB =
+ new Accessor("getClob", Clob.class, ResultSet::getClob, set("CLOB", "NCLOB"));
+ private static final Accessor GET_N_CLOB =
+ new Accessor("getNClob", NClob.class, ResultSet::getNClob, set("CLOB", "NCLOB"));
+ private static final Accessor GET_BLOB =
+ new Accessor("getBlob", Blob.class, ResultSet::getBlob, set("BLOB"));
+ private static final Accessor GET_ARRAY =
+ new Accessor("getArray", Array.class, ResultSet::getArray, set("ARRAY"));
+ private static final Accessor GET_REF =
+ new Accessor("getRef", Ref.class, ResultSet::getRef, set("REF"));
+ private static final Accessor GET_URL =
+ new Accessor("getURL", URL.class, ResultSet::getURL, set("DATALINK"));
+ private static final Accessor GET_OBJECT =
+ new Accessor("getObject", Object.class, (rs, i) -> rs.getObject(i), allJdbcTypes());
+ private static final Accessor GET_ROW_ID =
+ new Accessor("getRowId", RowId.class, ResultSet::getRowId, set("ROWID"));
+ private static final Accessor GET_SQLXML =
+ new Accessor("getSQLXML", SQLXML.class, ResultSet::getSQLXML, set("SQLXML"));
+
+ private static final List ACCESSORS = List.of(
+ GET_BYTE,
+ GET_SHORT,
+ GET_INT,
+ GET_LONG,
+ GET_FLOAT,
+ GET_DOUBLE,
+ GET_BIG_DECIMAL,
+ GET_BOOLEAN,
+ GET_STRING,
+ GET_N_STRING,
+ GET_BYTES,
+ GET_DATE,
+ GET_TIME,
+ GET_TIMESTAMP,
+ GET_ASCII_STREAM,
+ GET_BINARY_STREAM,
+ GET_CHARACTER_STREAM,
+ GET_N_CHARACTER_STREAM,
+ GET_CLOB,
+ GET_N_CLOB,
+ GET_BLOB,
+ GET_ARRAY,
+ GET_REF,
+ GET_URL,
+ GET_OBJECT,
+ GET_ROW_ID,
+ GET_SQLXML);
+
+ private static final Map B1_ACCESSORS = b1Accessors();
+
+ private static Map b1Accessors() {
+ Map accessors = new LinkedHashMap<>();
+ for (String name : set("CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR", "DISTINCT")) {
+ accessors.put(name, GET_STRING);
+ }
+ accessors.put("NUMERIC", GET_BIG_DECIMAL);
+ accessors.put("DECIMAL", GET_BIG_DECIMAL);
+ accessors.put("BIT", GET_BOOLEAN);
+ accessors.put("BOOLEAN", GET_BOOLEAN);
+ accessors.put("TINYINT", GET_BYTE);
+ accessors.put("SMALLINT", GET_SHORT);
+ accessors.put("INTEGER", GET_INT);
+ accessors.put("BIGINT", GET_LONG);
+ accessors.put("REAL", GET_FLOAT);
+ accessors.put("FLOAT", GET_DOUBLE);
+ accessors.put("DOUBLE", GET_DOUBLE);
+ accessors.put("BINARY", GET_BYTES);
+ accessors.put("VARBINARY", GET_BYTES);
+ accessors.put("LONGVARBINARY", GET_BYTES);
+ accessors.put("DATE", GET_DATE);
+ accessors.put("TIME", GET_TIME);
+ accessors.put("TIMESTAMP", GET_TIMESTAMP);
+ accessors.put("CLOB", GET_CLOB);
+ accessors.put("BLOB", GET_BLOB);
+ accessors.put("ARRAY", GET_ARRAY);
+ accessors.put("STRUCT", GET_OBJECT);
+ accessors.put("REF", GET_REF);
+ accessors.put("DATALINK", GET_URL);
+ accessors.put("JAVA_OBJECT", GET_OBJECT);
+ accessors.put("ROWID", GET_ROW_ID);
+ accessors.put("NCLOB", GET_N_CLOB);
+ accessors.put("SQLXML", GET_SQLXML);
+ return Map.copyOf(accessors);
+ }
+
+ static Stream b1Cases() {
+ return JDBC_TYPES.stream()
+ .filter(JdbcType::inB1B3)
+ .map(type -> Arguments.of(type, B1_ACCESSORS.get(type.name())));
+ }
+
+ @ParameterizedTest(name = "B-1 {0} via {1}")
+ @MethodSource("b1Cases")
+ void b1NaturalGetterReturnsDeclaredJavaType(JdbcType type, Accessor accessor) throws SQLException {
+ LibsqlResultSet resultSet = oneRow(type, type.sample());
+ assertThat(resultSet.next()).isTrue();
+
+ Object value;
+ try {
+ value = accessor.reader().read(resultSet, 1);
+ } catch (SQLException e) {
+ throw new AssertionError(
+ "B-1: " + type.name() + " via " + accessor.name() + " threw " + e, e);
+ }
+
+ assertThat(value)
+ .as("B-1: %s read with %s must be a %s", type.name(), accessor.name(), type.b1JavaType())
+ .isInstanceOf(boxed(type.b1JavaType()));
+ }
+
+ // ---------------------------------------------------------------------
+ // Table B-3: getObject maps each JDBC type to its Java object type.
+ // ---------------------------------------------------------------------
+
+ static Stream b3Cases() {
+ return JDBC_TYPES.stream().filter(JdbcType::inB1B3);
+ }
+
+ @ParameterizedTest(name = "B-3 {0} -> {1}")
+ @MethodSource("b3Cases")
+ void b3GetObjectReturnsDeclaredObjectType(JdbcType type) throws SQLException {
+ LibsqlResultSet resultSet = oneRow(type, type.sample());
+ assertThat(resultSet.next()).isTrue();
+
+ assertThat(resultSet.getObject(1))
+ .as("B-3: %s must read back as %s", type.name(), type.b3ObjectType())
+ .isInstanceOf(type.b3ObjectType());
+ }
+
+ @Test
+ void b3GetObjectReturnsNullForNull() throws SQLException {
+ for (JdbcType type : JDBC_TYPES) {
+ if (!type.inB1B3()) {
+ continue;
+ }
+ LibsqlResultSet resultSet = oneRow(type, new Value.Null());
+ assertThat(resultSet.next()).isTrue();
+ 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();
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // Table B-6: which JDBC types each getter may read.
+ // ---------------------------------------------------------------------
+
+ 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())));
+ }
+ }
+ return arguments.stream();
+ }
+
+ @ParameterizedTest(name = "B-6 {0} on {1} ({2})")
+ @MethodSource("b6Cases")
+ void b6GetterHonoursSupportedJdbcTypes(Accessor getter, JdbcType type, boolean supported)
+ throws SQLException {
+ LibsqlResultSet resultSet = oneRow(type, fixtureFor(getter, type));
+ assertThat(resultSet.next()).isTrue();
+
+ if (supported) {
+ Object value;
+ try {
+ value = getter.reader().read(resultSet, 1);
+ } catch (SQLException e) {
+ 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()));
+ }
+ } 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.
+ }
+ }
+ }
+
+ @Test
+ void b6NullIsVisibleThroughWasNull() throws SQLException {
+ for (Accessor getter : ACCESSORS) {
+ LibsqlResultSet resultSet = oneRow(jdbcType("VARCHAR"), new Value.Null());
+ assertThat(resultSet.next()).isTrue();
+ try {
+ getter.reader().read(resultSet, 1);
+ assertThat(resultSet.wasNull())
+ .as("B-6: %s on NULL must report wasNull", getter.name())
+ .isTrue();
+ } catch (SQLException unsupportedOnNull) {
+ // A getter the driver does not implement at all is still allowed to reject NULL.
+ }
+ }
+ }
+
+ @Test
+ void b6NumericGettersRejectNonNumericText() {
+ for (Accessor getter : List.of(
+ GET_BYTE, GET_SHORT, GET_INT, GET_LONG, GET_FLOAT, GET_DOUBLE, GET_BIG_DECIMAL)) {
+ LibsqlResultSet resultSet;
+ try {
+ resultSet = oneRow(jdbcType("VARCHAR"), Value.of("not-a-number"));
+ assertThat(resultSet.next()).isTrue();
+ } catch (SQLException e) {
+ throw new AssertionError(e);
+ }
+ try {
+ Object value = getter.reader().read(resultSet, 1);
+ fail("B-6: " + getter.name()
+ + " must reject non-numeric text instead of coercing to " + value);
+ } catch (SQLException expected) {
+ // Any SQLException is a pass: the spec does not prescribe the exception type.
+ }
+ }
+ }
+
+ @Test
+ void b6TemporalGettersRejectNonDateText() {
+ for (Accessor getter : List.of(GET_DATE, GET_TIME, GET_TIMESTAMP)) {
+ LibsqlResultSet resultSet;
+ try {
+ resultSet = oneRow(jdbcType("VARCHAR"), Value.of("not-a-date"));
+ assertThat(resultSet.next()).isTrue();
+ } catch (SQLException e) {
+ throw new AssertionError(e);
+ }
+ try {
+ Object value = getter.reader().read(resultSet, 1);
+ fail("B-6: " + getter.name() + " must reject non-date text but returned " + value);
+ } catch (SQLException expected) {
+ // Any SQLException is a pass: the spec does not prescribe the exception type.
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // Fixtures and helpers.
+ // ---------------------------------------------------------------------
+
+ private static Value fixtureFor(Accessor getter, JdbcType type) {
+ boolean textual = isTextual(type.name());
+ return switch (getter.name()) {
+ case "getByte", "getShort", "getInt", "getLong", "getFloat", "getDouble", "getBigDecimal",
+ "getBoolean" ->
+ textual ? Value.of("42") : type.sample();
+ case "getDate", "getTime", "getTimestamp" -> textual ? TEXT_TIMESTAMP : type.sample();
+ case "getBytes", "getBinaryStream", "getAsciiStream", "getCharacterStream",
+ "getNCharacterStream", "getClob", "getNClob" ->
+ textual ? TEXT_ABC : type.sample();
+ case "getURL" -> TEXT_URL;
+ case "getRowId" -> TEXT_ROWID;
+ case "getSQLXML" -> TEXT_XML;
+ case "getArray", "getRef" -> TEXT_ABC;
+ default -> type.sample();
+ };
+ }
+
+ private static boolean isTextual(String name) {
+ return switch (name) {
+ case "CHAR", "VARCHAR", "LONGVARCHAR", "NCHAR", "NVARCHAR", "LONGNVARCHAR", "CLOB",
+ "NCLOB", "DATE", "TIME", "TIMESTAMP", "DATALINK", "SQLXML", "ROWID", "ARRAY",
+ "STRUCT", "REF", "JAVA_OBJECT", "DISTINCT", "TIME_WITH_TIMEZONE",
+ "TIMESTAMP_WITH_TIMEZONE" ->
+ true;
+ default -> false;
+ };
+ }
+
+ private static LibsqlResultSet oneRow(JdbcType type, Value value) {
+ return oneRow(type.name(), value);
+ }
+
+ 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));
+ }
+
+ private static JdbcType jdbcType(String name) {
+ return JDBC_TYPES.stream()
+ .filter(type -> type.name().equals(name))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(name));
+ }
+
+ private static Class> boxed(Class> type) {
+ if (!type.isPrimitive()) {
+ return type;
+ }
+ if (type == byte.class) {
+ return Byte.class;
+ }
+ if (type == short.class) {
+ return Short.class;
+ }
+ if (type == int.class) {
+ return Integer.class;
+ }
+ if (type == long.class) {
+ return Long.class;
+ }
+ if (type == float.class) {
+ return Float.class;
+ }
+ if (type == double.class) {
+ return Double.class;
+ }
+ if (type == boolean.class) {
+ return Boolean.class;
+ }
+ if (type == char.class) {
+ return Character.class;
+ }
+ throw new IllegalArgumentException(type.getName());
+ }
+
+ private static Set set(String... names) {
+ return Set.of(names);
+ }
+
+ private static Set numericText() {
+ return set(
+ "TINYINT",
+ "SMALLINT",
+ "INTEGER",
+ "BIGINT",
+ "REAL",
+ "FLOAT",
+ "DOUBLE",
+ "DECIMAL",
+ "NUMERIC",
+ "BIT",
+ "BOOLEAN",
+ "CHAR",
+ "VARCHAR",
+ "LONGVARCHAR");
+ }
+
+ private static Set numericTextWithRowId() {
+ Set names = new java.util.HashSet<>(numericText());
+ names.add("ROWID");
+ return Set.copyOf(names);
+ }
+
+ private static Set stringTargets() {
+ return set(
+ "TINYINT",
+ "SMALLINT",
+ "INTEGER",
+ "BIGINT",
+ "REAL",
+ "FLOAT",
+ "DOUBLE",
+ "DECIMAL",
+ "NUMERIC",
+ "BIT",
+ "BOOLEAN",
+ "CHAR",
+ "VARCHAR",
+ "LONGVARCHAR",
+ "BINARY",
+ "VARBINARY",
+ "LONGVARBINARY",
+ "DATE",
+ "TIME",
+ "TIMESTAMP",
+ "DATALINK",
+ "NCHAR",
+ "NVARCHAR",
+ "LONGNVARCHAR");
+ }
+
+ private static Set allJdbcTypes() {
+ return JDBC_TYPES.stream()
+ .map(JdbcType::name)
+ .collect(java.util.stream.Collectors.toUnmodifiableSet());
+ }
+}
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
new file mode 100644
index 0000000..d64cbc0
--- /dev/null
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBSetObjectTest.java
@@ -0,0 +1,295 @@
+package com.github.magnusp.libsql.jdbc;
+
+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;
+
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+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.SQLException;
+import java.sql.SQLXML;
+import java.sql.Struct;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * JDBC 4.3 Appendix B, Table B-5, negative half: a target JDBC type the table does not list for a
+ * Java object type must not be accepted.
+ *
+ * Table B-5 says which JDBC types {@code setObject} and {@code setNull} "may" be given for each
+ * Java type. This suite takes the strict reading: a listed pair must be accepted (covered by the
+ * integration suite, which performs the round trip), and an unlisted pair must be rejected with a
+ * {@link SQLException}. Any {@link SQLException} is a pass because the specification does not
+ * prescribe the exception type.
+ *
+ *
No connection is needed: binding happens locally before the statement is executed, so a
+ * statement over a {@code null} connection is enough to observe whether the target type is
+ * validated.
+ */
+class Jdbc4AppendixBSetObjectTest {
+
+ record Target(String name, int sqlType) {}
+
+ private static final List TARGETS = List.of(
+ new Target("TINYINT", Types.TINYINT),
+ new Target("SMALLINT", Types.SMALLINT),
+ new Target("INTEGER", Types.INTEGER),
+ new Target("BIGINT", Types.BIGINT),
+ new Target("REAL", Types.REAL),
+ new Target("FLOAT", Types.FLOAT),
+ new Target("DOUBLE", Types.DOUBLE),
+ new Target("DECIMAL", Types.DECIMAL),
+ new Target("NUMERIC", Types.NUMERIC),
+ new Target("BIT", Types.BIT),
+ new Target("BOOLEAN", Types.BOOLEAN),
+ new Target("CHAR", Types.CHAR),
+ new Target("VARCHAR", Types.VARCHAR),
+ new Target("LONGVARCHAR", Types.LONGVARCHAR),
+ new Target("BINARY", Types.BINARY),
+ new Target("VARBINARY", Types.VARBINARY),
+ new Target("LONGVARBINARY", Types.LONGVARBINARY),
+ new Target("DATE", Types.DATE),
+ new Target("TIME", Types.TIME),
+ new Target("TIMESTAMP", Types.TIMESTAMP),
+ new Target("NCHAR", Types.NCHAR),
+ new Target("NVARCHAR", Types.NVARCHAR),
+ new Target("LONGNVARCHAR", Types.LONGNVARCHAR),
+ new Target("ARRAY", Types.ARRAY),
+ new Target("BLOB", Types.BLOB),
+ new Target("CLOB", Types.CLOB),
+ new Target("STRUCT", Types.STRUCT),
+ new Target("REF", Types.REF),
+ new Target("DATALINK", Types.DATALINK),
+ new Target("JAVA_OBJECT", Types.JAVA_OBJECT),
+ new Target("ROWID", Types.ROWID),
+ 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));
+
+ record JavaRow(String name, Object value, Set targets) {}
+
+ private static final Set NUMERIC_TEXT = Set.of(
+ Types.TINYINT,
+ Types.SMALLINT,
+ Types.INTEGER,
+ Types.BIGINT,
+ Types.REAL,
+ Types.FLOAT,
+ Types.DOUBLE,
+ Types.DECIMAL,
+ Types.NUMERIC,
+ Types.BIT,
+ Types.BOOLEAN,
+ Types.CHAR,
+ Types.VARCHAR,
+ Types.LONGVARCHAR);
+
+ private static Set set(int... sqlTypes) {
+ return java.util.Arrays.stream(sqlTypes).boxed().collect(java.util.stream.Collectors.toUnmodifiableSet());
+ }
+
+ private static java.net.URL url(String value) {
+ try {
+ return URI.create(value).toURL();
+ } catch (java.net.MalformedURLException e) {
+ throw new IllegalArgumentException(value, e);
+ }
+ }
+
+ private static Set union(Set base, int... extra) {
+ Set result = new java.util.HashSet<>(base);
+ for (int sqlType : extra) {
+ result.add(sqlType);
+ }
+ return Set.copyOf(result);
+ }
+
+ private static final Calendar CALENDAR = calendar2024();
+
+ private static Calendar calendar2024() {
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ calendar.clear();
+ calendar.set(2024, Calendar.JANUARY, 2, 3, 4, 5);
+ return calendar;
+ }
+
+ private static final java.util.Date UTIL_DATE =
+ new java.util.Date(CALENDAR.getTimeInMillis());
+
+ private static final List ROWS = List.of(
+ new JavaRow(
+ "String",
+ "42",
+ union(NUMERIC_TEXT, Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY,
+ Types.DATE, Types.TIME, Types.TIMESTAMP, Types.NCHAR, Types.NVARCHAR,
+ Types.LONGNVARCHAR)),
+ new JavaRow("BigDecimal", new BigDecimal("7"), NUMERIC_TEXT),
+ new JavaRow("Boolean", Boolean.TRUE, NUMERIC_TEXT),
+ new JavaRow("Byte", (byte) 7, NUMERIC_TEXT),
+ new JavaRow("Short", (short) 7, NUMERIC_TEXT),
+ new JavaRow("Integer", 7, NUMERIC_TEXT),
+ new JavaRow("Long", 7L, NUMERIC_TEXT),
+ new JavaRow("Float", 7.0f, NUMERIC_TEXT),
+ new JavaRow("Double", 7.0d, NUMERIC_TEXT),
+ new JavaRow("byte[]", new byte[] {1, 2, 3},
+ set(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY)),
+ new JavaRow("BigInteger", BigInteger.valueOf(7),
+ set(Types.BIGINT, Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR)),
+ new JavaRow("java.sql.Date", Date.valueOf("2024-01-02"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIMESTAMP)),
+ new JavaRow("java.sql.Time", Time.valueOf("03:04:05"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME, Types.TIMESTAMP)),
+ new JavaRow("java.sql.Timestamp", Timestamp.valueOf("2024-01-02 03:04:05"),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP)),
+ new JavaRow("java.sql.Array", stub(Array.class), set(Types.ARRAY)),
+ new JavaRow("java.sql.Blob", new LibsqlBlob("abc".getBytes(StandardCharsets.UTF_8)),
+ set(Types.BLOB)),
+ new JavaRow("java.sql.Clob", new LibsqlClob("abc"), set(Types.CLOB)),
+ new JavaRow("java.sql.Struct", stub(Struct.class), set(Types.STRUCT)),
+ new JavaRow("java.sql.Ref", stub(Ref.class), set(Types.REF)),
+ new JavaRow("java.net.URL", url("https://example.com"),
+ set(Types.DATALINK)),
+ new JavaRow("Java class", new Object(), set(Types.JAVA_OBJECT)),
+ new JavaRow("java.sql.RowId", stub(RowId.class), set(Types.ROWID)),
+ new JavaRow("java.sql.NClob", stub(NClob.class), set(Types.NCLOB)),
+ new JavaRow("java.sql.SQLXML", stub(SQLXML.class), set(Types.SQLXML)),
+ new JavaRow("java.util.Calendar", CALENDAR,
+ union(set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP), Types.ARRAY)),
+ new JavaRow("java.util.Date", UTIL_DATE,
+ union(set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP), Types.ARRAY)),
+ new JavaRow("java.time.LocalDate", LocalDate.of(2024, 1, 2),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE)),
+ new JavaRow("java.time.LocalTime", LocalTime.of(3, 4, 5),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME)),
+ new JavaRow("java.time.LocalDateTime", LocalDateTime.of(2024, 1, 2, 3, 4, 5),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.DATE, Types.TIME,
+ Types.TIMESTAMP)),
+ new JavaRow("java.time.OffsetTime", OffsetTime.of(3, 4, 5, 0, ZoneOffset.UTC),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME_WITH_TIMEZONE)),
+ new JavaRow("java.time.OffsetDateTime",
+ OffsetDateTime.of(2024, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC),
+ set(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR, Types.TIME_WITH_TIMEZONE,
+ Types.TIMESTAMP_WITH_TIMEZONE)));
+
+ static Stream javaRows() {
+ return ROWS.stream();
+ }
+
+ @ParameterizedTest(name = "B-5 rejects unlisted targets for {0}")
+ @MethodSource("javaRows")
+ void b5SetObjectRejectsUnlistedTargetTypes(JavaRow row) {
+ List checks = new ArrayList<>();
+ for (Target target : TARGETS) {
+ if (row.targets().contains(target.sqlType())) {
+ continue;
+ }
+ checks.add(() -> {
+ LibsqlPreparedStatement statement = new LibsqlPreparedStatement(null, "?");
+ try {
+ statement.setObject(1, row.value(), target.sqlType());
+ fail("B-5: setObject(" + row.name() + ", " + target.name()
+ + ") must be rejected, but it was accepted");
+ } catch (SQLException expected) {
+ // Any SQLException is a pass.
+ }
+ });
+ }
+ assertAll("B-5 " + row.name(), checks);
+ }
+
+ @Test
+ void b5SetObjectAcceptsListedTargetTypes() throws SQLException {
+ for (JavaRow row : ROWS) {
+ for (Target target : TARGETS) {
+ if (!row.targets().contains(target.sqlType())) {
+ continue;
+ }
+ LibsqlPreparedStatement statement = new LibsqlPreparedStatement(null, "?");
+ statement.setObject(1, row.value(), target.sqlType());
+ }
+ }
+ }
+
+ @Test
+ void b5SetNullAcceptsEveryTargetType() throws SQLException {
+ for (Target target : TARGETS) {
+ LibsqlPreparedStatement statement = new LibsqlPreparedStatement(null, "?");
+ statement.setNull(1, target.sqlType());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T stub(Class type) {
+ return (T) Proxy.newProxyInstance(
+ type.getClassLoader(),
+ new Class>[] {type},
+ (proxy, method, args) -> switch (method.getName()) {
+ case "toString" -> "stub";
+ case "hashCode" -> System.identityHashCode(proxy);
+ case "equals" -> proxy == args[0];
+ default -> defaultValue(method.getReturnType());
+ });
+ }
+
+ private static Object defaultValue(Class> type) {
+ if (!type.isPrimitive()) {
+ return null;
+ }
+ if (type == boolean.class) {
+ return false;
+ }
+ if (type == char.class) {
+ return '\0';
+ }
+ if (type == byte.class) {
+ return (byte) 0;
+ }
+ if (type == short.class) {
+ return (short) 0;
+ }
+ if (type == int.class) {
+ return 0;
+ }
+ if (type == long.class) {
+ return 0L;
+ }
+ if (type == float.class) {
+ return 0.0f;
+ }
+ if (type == double.class) {
+ return 0.0d;
+ }
+ return null;
+ }
+}