From bc1039e41b524e615013b4ba265d1f2bb3dce2be Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 03:33:02 +0100
Subject: [PATCH 1/4] feat(jdbc): add NClob, SQLXML, and RowId wrappers
Each wrapper holds the materialized value of one result column and
rejects every read once freed. NClob reuses the Clob text holder because
SQLite stores national and regular text identically; SQLXML and RowId are
read-only views over the same text and bytes.
---
.../magnusp/libsql/jdbc/LibsqlClob.java | 2 +-
.../magnusp/libsql/jdbc/LibsqlNClob.java | 16 ++++
.../magnusp/libsql/jdbc/LibsqlRowId.java | 34 ++++++++
.../magnusp/libsql/jdbc/LibsqlSQLXML.java | 79 +++++++++++++++++++
4 files changed, 130 insertions(+), 1 deletion(-)
create mode 100644 libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlNClob.java
create mode 100644 libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlRowId.java
create mode 100644 libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlSQLXML.java
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlClob.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlClob.java
index b1b917b..5c145f8 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlClob.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlClob.java
@@ -12,7 +12,7 @@
import java.sql.SQLFeatureNotSupportedException;
/** Read-only {@link Clob} over the materialized text of one result column. */
-final class LibsqlClob implements Clob {
+class LibsqlClob implements Clob {
private String text;
private boolean freed;
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlNClob.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlNClob.java
new file mode 100644
index 0000000..97f0333
--- /dev/null
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlNClob.java
@@ -0,0 +1,16 @@
+package com.github.magnusp.libsql.jdbc;
+
+import java.sql.NClob;
+
+/**
+ * Read-only {@link NClob} over the materialized text of one result column.
+ *
+ *
SQLite stores national and regular character data as the same UTF-8 text, so the national
+ * variant only adds the {@link NClob} interface to {@link LibsqlClob}.
+ */
+final class LibsqlNClob extends LibsqlClob implements NClob {
+
+ LibsqlNClob(String text) {
+ super(text);
+ }
+}
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlRowId.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlRowId.java
new file mode 100644
index 0000000..ee51178
--- /dev/null
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlRowId.java
@@ -0,0 +1,34 @@
+package com.github.magnusp.libsql.jdbc;
+
+import java.sql.RowId;
+import java.util.Arrays;
+
+/** {@link RowId} over the materialized bytes of one result column. */
+final class LibsqlRowId implements RowId {
+
+ private final byte[] bytes;
+
+ LibsqlRowId(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return bytes.clone();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof LibsqlRowId rowId && Arrays.equals(bytes, rowId.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+
+ @Override
+ public String toString() {
+ return "LibsqlRowId" + Arrays.toString(bytes);
+ }
+}
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlSQLXML.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlSQLXML.java
new file mode 100644
index 0000000..387fef1
--- /dev/null
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlSQLXML.java
@@ -0,0 +1,79 @@
+package com.github.magnusp.libsql.jdbc;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLXML;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+
+/** Read-only {@link SQLXML} over the materialized text of one result column. */
+final class LibsqlSQLXML implements SQLXML {
+
+ private String text;
+ private boolean freed;
+
+ LibsqlSQLXML(String text) {
+ this.text = text;
+ }
+
+ private String data() throws SQLException {
+ if (freed) {
+ throw new SQLException("SQLXML has been freed");
+ }
+ return text;
+ }
+
+ @Override
+ public void free() throws SQLException {
+ freed = true;
+ text = null;
+ }
+
+ @Override
+ public String getString() throws SQLException {
+ return data();
+ }
+
+ @Override
+ public InputStream getBinaryStream() throws SQLException {
+ return new ByteArrayInputStream(data().getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public Reader getCharacterStream() throws SQLException {
+ return new StringReader(data());
+ }
+
+ @Override
+ public T getSource(Class sourceClass) throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML sources are not supported");
+ }
+
+ @Override
+ public OutputStream setBinaryStream() throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML is read-only");
+ }
+
+ @Override
+ public Writer setCharacterStream() throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML is read-only");
+ }
+
+ @Override
+ public void setString(String value) throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML is read-only");
+ }
+
+ @Override
+ public T setResult(Class resultClass) throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML is read-only");
+ }
+}
From d9bbb220c580a4d2c532b6533eafb2c3da3078cf Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 03:33:05 +0100
Subject: [PATCH 2/4] feat(jdbc): read URL, LOB, row id, and NCHAR result
columns
getURL parses DATALINK text and throws on text that is not a URL.
getNClob, getSQLXML, and getRowId wrap the stored text or bytes, and
getObject routes NCLOB, SQLXML, ROWID, and DATALINK to the same wrappers.
getAsciiStream reads text as US-ASCII and getNCharacterStream delegates to
getNString.
SQLXML and ROWID join the declared-type registry so the metadata and
getObject agree on their object type.
---
.../magnusp/libsql/jdbc/JdbcTypeMapping.java | 4 +
.../magnusp/libsql/jdbc/LibsqlResultSet.java | 84 +++++++++++++++----
2 files changed, 74 insertions(+), 14 deletions(-)
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java
index 83ddc7f..1244c07 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java
@@ -9,6 +9,8 @@
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
+import java.sql.RowId;
+import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
@@ -159,6 +161,8 @@ private static Map declaredTypeRegistry() {
entry("TIMESTAMP", Types.TIMESTAMP, Timestamp.class, Timestamp.class, StorageClass.TEXT),
entry("CLOB", Types.CLOB, Clob.class, Clob.class, StorageClass.TEXT),
entry("NCLOB", Types.NCLOB, NClob.class, NClob.class, StorageClass.TEXT),
+ entry("SQLXML", Types.SQLXML, SQLXML.class, SQLXML.class, StorageClass.TEXT),
+ entry("ROWID", Types.ROWID, RowId.class, RowId.class, StorageClass.TEXT),
entry("BLOB", Types.BLOB, Blob.class, Blob.class, StorageClass.BLOB),
entry("DATALINK", Types.DATALINK, URL.class, URL.class, StorageClass.TEXT));
}
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java
index 32098ab..cb9b721 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java
@@ -10,6 +10,8 @@
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
+import java.net.MalformedURLException;
+import java.net.URI;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
@@ -424,11 +426,21 @@ private static Object convertedObject(Value value, JdbcTypeMapping.JdbcType type
if (objectClass == Clob.class) {
return clobValue(value);
}
+ if (objectClass == NClob.class) {
+ return nclobValue(value);
+ }
if (objectClass == Blob.class) {
return blobValue(value);
}
- // NCLOB and DATALINK resolve to their declared wrapper types, but the wrapper getters
- // arrive in PR-3, so the value keeps its storage-class representation until then.
+ if (objectClass == RowId.class) {
+ return rowIdValue(value);
+ }
+ if (objectClass == SQLXML.class) {
+ return sqlxmlValue(value);
+ }
+ if (objectClass == URL.class) {
+ return urlValue(value);
+ }
return storedObject(value);
}
@@ -624,13 +636,19 @@ public boolean isLast() throws SQLException {
throw new SQLFeatureNotSupportedException("not supported by sqlite");
}
- @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override
+ public InputStream getAsciiStream(int columnIndex) throws SQLException {
+ String text = getString(columnIndex);
+ return text == null
+ ? null
+ : new ByteArrayInputStream(text.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
+ }
@Override @Deprecated @SuppressWarnings("deprecation") public InputStream getUnicodeStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public InputStream getBinaryStream(int columnIndex) throws SQLException {
byte[] bytes = getBytes(columnIndex);
return bytes == null ? null : new ByteArrayInputStream(bytes);
}
- @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { return getAsciiStream(findColumn(columnLabel)); }
@Override @Deprecated @SuppressWarnings("deprecation") public InputStream getUnicodeStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public InputStream getBinaryStream(String columnLabel) throws SQLException { return getBinaryStream(findColumn(columnLabel)); }
@Override public SQLWarning getWarnings() throws SQLException { return null; }
@@ -748,8 +766,25 @@ private static Clob clobValue(Value value) {
@Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { return getTime(columnLabel); }
@Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { return getTimestamp(columnIndex); }
@Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(columnLabel); }
- @Override public URL getURL(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public URL getURL(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override
+ public URL getURL(int columnIndex) throws SQLException {
+ return urlValue(getCurrentValue(columnIndex));
+ }
+
+ private static URL urlValue(Value value) throws SQLException {
+ String text = stringValue(value);
+ if (text == null) {
+ return null;
+ }
+ try {
+ // URI.toURL replaces the deprecated URL(String) constructor and rejects the same text.
+ return URI.create(text).toURL();
+ } catch (IllegalArgumentException | MalformedURLException e) {
+ throw new SQLException("Bad value for type URL: " + text, e);
+ }
+ }
+
+ @Override public URL getURL(String columnLabel) throws SQLException { return getURL(findColumn(columnLabel)); }
@Override public void updateRef(int columnIndex, Ref x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateRef(String columnLabel, Ref x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateBlob(int columnIndex, Blob x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@@ -758,8 +793,14 @@ private static Clob clobValue(Value value) {
@Override public void updateClob(String columnLabel, Clob x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateArray(int columnIndex, Array x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateArray(String columnLabel, Array x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public RowId getRowId(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public RowId getRowId(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public RowId getRowId(int columnIndex) throws SQLException {
+ return rowIdValue(getCurrentValue(columnIndex));
+ }
+ private static RowId rowIdValue(Value value) {
+ byte[] bytes = bytesValue(value);
+ return bytes == null ? null : new LibsqlRowId(bytes);
+ }
+ @Override public RowId getRowId(String columnLabel) throws SQLException { return getRowId(findColumn(columnLabel)); }
@Override public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public int getHoldability() throws SQLException { return CLOSE_CURSORS_AT_COMMIT; }
@@ -767,16 +808,31 @@ private static Clob clobValue(Value value) {
@Override public void updateNString(String columnLabel, String nString) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public NClob getNClob(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public NClob getNClob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public SQLXML getSQLXML(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public NClob getNClob(int columnIndex) throws SQLException {
+ return nclobValue(getCurrentValue(columnIndex));
+ }
+ private static NClob nclobValue(Value value) {
+ String text = stringValue(value);
+ return text == null ? null : new LibsqlNClob(text);
+ }
+ @Override public NClob getNClob(String columnLabel) throws SQLException { return getNClob(findColumn(columnLabel)); }
+ @Override public SQLXML getSQLXML(int columnIndex) throws SQLException {
+ return sqlxmlValue(getCurrentValue(columnIndex));
+ }
+ private static SQLXML sqlxmlValue(Value value) {
+ String text = stringValue(value);
+ return text == null ? null : new LibsqlSQLXML(text);
+ }
+ @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { return getSQLXML(findColumn(columnLabel)); }
@Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public String getNString(int columnIndex) throws SQLException { return getString(columnIndex); }
@Override public String getNString(String columnLabel) throws SQLException { return getString(columnLabel); }
- @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public Reader getNCharacterStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public Reader getNCharacterStream(int columnIndex) throws SQLException {
+ String text = getNString(columnIndex);
+ return text == null ? null : new StringReader(text);
+ }
+ @Override public Reader getNCharacterStream(String columnLabel) throws SQLException { return getNCharacterStream(findColumn(columnLabel)); }
@Override public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
@Override public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
From 58ef0e368e575a45b25e37e1962f1b63da7ea7cd Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 03:33:07 +0100
Subject: [PATCH 3/4] feat(jdbc): bind SQLXML and RowId parameters
setSQLXML binds the SQLXML content as text. setRowId binds the bytes as a
BLOB, which is the only representation getRowId reads back unchanged.
setArray and setRef still throw.
---
.../libsql/jdbc/LibsqlPreparedStatement.java | 6 ++-
.../jdbc/LibsqlPreparedStatementTest.java | 46 +++++++++++++++----
2 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
index 64ad113..a954611 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
@@ -359,7 +359,9 @@ public void setURL(int parameterIndex, URL x) throws SQLException {
@Override
public void setRowId(int parameterIndex, RowId x) throws SQLException {
- throw new SQLFeatureNotSupportedException("RowId parameters are not supported");
+ // SQLite has no ROWID parameter type; BLOB binds the bytes unchanged, so getRowId reads
+ // back exactly the bytes the caller supplied.
+ setParam(parameterIndex, Value.of(x == null ? null : x.getBytes()));
}
@Override
@@ -394,7 +396,7 @@ public void setNClob(int parameterIndex, Reader reader, long length) throws SQLE
@Override
public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
- throw new SQLFeatureNotSupportedException("SQLXML parameters are not supported");
+ setParam(parameterIndex, Value.of(xmlObject == null ? null : xmlObject.getString()));
}
@Override
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
index 8446d83..f574042 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
@@ -559,21 +559,51 @@ void setObjectWithoutATargetFollowsTableB4() throws SQLException {
.isEqualTo(Value.of("2024-01-02 03:04:05"));
}
+ @Test
+ void rowIdAndSqlXmlParametersBindTheirContent() throws SQLException {
+ byte[] bytes = {1, 2, 3};
+ assertThat(bindParameter(statement -> statement.setRowId(1, new LibsqlRowId(bytes))))
+ .isEqualTo(Value.of(bytes));
+ assertThat(bindParameter(statement -> statement.setRowId(1, null)))
+ .isEqualTo(Value.ofNull());
+ assertThat(bindParameter(statement -> statement.setSQLXML(1, new LibsqlSQLXML(""))))
+ .isEqualTo(Value.of(""));
+ assertThat(bindParameter(statement -> statement.setSQLXML(1, null)))
+ .isEqualTo(Value.ofNull());
+ }
+
+ @Test
+ void rowIdBoundAsBlobReadsBackTheSameBytes() throws SQLException {
+ byte[] bytes = "rowid-1".getBytes(StandardCharsets.UTF_8);
+ Value bound = bindParameter(statement -> statement.setRowId(1, new LibsqlRowId(bytes)));
+
+ LibsqlResultSet rows = new LibsqlResultSet(
+ new LibsqlStatement(null),
+ new StmtResult(
+ List.of(new Col("c", "ROWID")), List.of(new Row(List.of(bound))), 0, null));
+
+ assertThat(rows.next()).isTrue();
+ assertThat(rows.getRowId(1).getBytes()).isEqualTo(bytes);
+ }
+
private static Value bind(Object value) throws SQLException {
- RecordingConnection connection = new RecordingConnection(NO_ROWS);
- try (LibsqlPreparedStatement statement =
- new LibsqlPreparedStatement(connection, "insert into t values (?)")) {
- statement.setObject(1, value);
- statement.executeUpdate();
- }
- return connection.executed.get(0).args().get(0);
+ return bindParameter(statement -> statement.setObject(1, value));
}
private static Value bind(Object value, int targetSqlType) throws SQLException {
+ return bindParameter(statement -> statement.setObject(1, value, targetSqlType));
+ }
+
+ @FunctionalInterface
+ private interface ParameterBinder {
+ void bind(LibsqlPreparedStatement statement) throws SQLException;
+ }
+
+ private static Value bindParameter(ParameterBinder binder) throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "insert into t values (?)")) {
- statement.setObject(1, value, targetSqlType);
+ binder.bind(statement);
statement.executeUpdate();
}
return connection.executed.get(0).args().get(0);
From 4067d4257148eb54863d2340495d64ff6f0dabb7 Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Sun, 25 Oct 2026 03:33:10 +0100
Subject: [PATCH 4/4] test(jdbc): cover wrapper free, length, and stream reads
B-1 and B-3 now check the URL, NClob, SQLXML, RowId, ASCII, and NCHAR
getters end to end. A freed wrapper must reject every further read.
---
.../libsql/jdbc/Jdbc4AppendixBReadTest.java | 117 ++++++++++++++++++
.../libsql/jdbc/LibsqlResultSetTest.java | 12 --
2 files changed, 117 insertions(+), 12 deletions(-)
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java
index cce5622..a6e8feb 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/Jdbc4AppendixBReadTest.java
@@ -9,6 +9,7 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
@@ -36,6 +37,7 @@
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.fail;
/**
@@ -465,6 +467,107 @@ void b6TemporalGettersRejectNonDateText() {
}
}
+ // ---------------------------------------------------------------------
+ // PR-3 wrappers: URL, NClob, SQLXML, RowId, and the ASCII/Unicode streams.
+ // ---------------------------------------------------------------------
+
+ @Test
+ void b1UrlGetterParsesAbsoluteTextAndRejectsTheRest() throws SQLException {
+ LibsqlResultSet absolute = oneRow(jdbcType("DATALINK"), TEXT_URL);
+ assertThat(absolute.next()).isTrue();
+ assertThat(absolute.getURL(1).toExternalForm()).isEqualTo("https://example.com");
+ assertThat(absolute.getURL("c").toExternalForm()).isEqualTo("https://example.com");
+
+ LibsqlResultSet malformed = oneRow(jdbcType("DATALINK"), Value.of("not a url"));
+ assertThat(malformed.next()).isTrue();
+ assertThatThrownBy(() -> malformed.getURL(1))
+ .isInstanceOf(SQLException.class)
+ .hasMessageContaining("URL");
+ }
+
+ @Test
+ void b3NewWrappersReadBackTheirContent() throws Exception {
+ LibsqlResultSet nclobRow = oneRow(jdbcType("NCLOB"), TEXT_ABC);
+ assertThat(nclobRow.next()).isTrue();
+ NClob nclob = nclobRow.getNClob(1);
+ assertThat(nclob.length()).isEqualTo(3);
+ assertThat(nclob.getSubString(1, 2)).isEqualTo("ab");
+ assertThat(readAll(nclob.getCharacterStream())).isEqualTo("abc");
+ assertThat(nclobRow.getNClob("c").getSubString(2, 1)).isEqualTo("b");
+
+ LibsqlResultSet xmlRow = oneRow(jdbcType("SQLXML"), TEXT_XML);
+ assertThat(xmlRow.next()).isTrue();
+ SQLXML xml = xmlRow.getSQLXML(1);
+ assertThat(xml.getString()).isEqualTo("");
+ assertThat(readAll(xml.getCharacterStream())).isEqualTo("");
+ assertThat(readAll(xml.getBinaryStream())).isEqualTo("".getBytes(StandardCharsets.UTF_8));
+ assertThat(xmlRow.getSQLXML("c").getString()).isEqualTo("");
+
+ LibsqlResultSet rowIdRow = oneRow(jdbcType("ROWID"), TEXT_ROWID);
+ assertThat(rowIdRow.next()).isTrue();
+ assertThat(rowIdRow.getRowId(1).getBytes())
+ .isEqualTo("rowid-1".getBytes(StandardCharsets.UTF_8));
+ assertThat(rowIdRow.getRowId("c").getBytes())
+ .isEqualTo("rowid-1".getBytes(StandardCharsets.UTF_8));
+
+ LibsqlResultSet textRow = oneRow(jdbcType("VARCHAR"), TEXT_ABC);
+ assertThat(textRow.next()).isTrue();
+ assertThat(readAll(textRow.getAsciiStream(1)))
+ .isEqualTo("abc".getBytes(StandardCharsets.US_ASCII));
+ assertThat(readAll(textRow.getAsciiStream("c")))
+ .isEqualTo("abc".getBytes(StandardCharsets.US_ASCII));
+ assertThat(readAll(textRow.getNCharacterStream(1))).isEqualTo("abc");
+ assertThat(readAll(textRow.getNCharacterStream("c"))).isEqualTo("abc");
+ }
+
+ @Test
+ void b3FreedWrappersRejectFurtherReads() throws SQLException {
+ LibsqlResultSet nclobRow = oneRow(jdbcType("NCLOB"), TEXT_ABC);
+ assertThat(nclobRow.next()).isTrue();
+ NClob nclob = nclobRow.getNClob(1);
+ nclob.free();
+ assertThatThrownBy(nclob::length).isInstanceOf(SQLException.class);
+ assertThatThrownBy(() -> nclob.getSubString(1, 1)).isInstanceOf(SQLException.class);
+ assertThatThrownBy(nclob::getCharacterStream).isInstanceOf(SQLException.class);
+
+ LibsqlResultSet xmlRow = oneRow(jdbcType("SQLXML"), TEXT_XML);
+ assertThat(xmlRow.next()).isTrue();
+ SQLXML xml = xmlRow.getSQLXML(1);
+ xml.free();
+ assertThatThrownBy(xml::getString).isInstanceOf(SQLException.class);
+ assertThatThrownBy(xml::getCharacterStream).isInstanceOf(SQLException.class);
+ assertThatThrownBy(xml::getBinaryStream).isInstanceOf(SQLException.class);
+
+ LibsqlResultSet clobRow = oneRow(jdbcType("CLOB"), TEXT_ABC);
+ assertThat(clobRow.next()).isTrue();
+ Clob clob = clobRow.getClob(1);
+ clob.free();
+ assertThatThrownBy(clob::length).isInstanceOf(SQLException.class);
+ assertThatThrownBy(clob::getCharacterStream).isInstanceOf(SQLException.class);
+ assertThatThrownBy(clob::getAsciiStream).isInstanceOf(SQLException.class);
+
+ LibsqlResultSet blobRow = oneRow(jdbcType("BLOB"), BYTES_ABC);
+ assertThat(blobRow.next()).isTrue();
+ Blob blob = blobRow.getBlob(1);
+ blob.free();
+ assertThatThrownBy(blob::length).isInstanceOf(SQLException.class);
+ assertThatThrownBy(blob::getBinaryStream).isInstanceOf(SQLException.class);
+ assertThatThrownBy(() -> blob.getBytes(1, 1)).isInstanceOf(SQLException.class);
+ }
+
+ @Test
+ void b3RowIdComparesAndCopiesItsBytes() {
+ LibsqlRowId rowId = new LibsqlRowId(new byte[] {1, 2});
+ byte[] returned = rowId.getBytes();
+ returned[0] = 9;
+
+ assertThat(rowId.getBytes()).containsExactly(1, 2);
+ assertThat(rowId)
+ .isEqualTo(new LibsqlRowId(new byte[] {1, 2}))
+ .hasSameHashCodeAs(new LibsqlRowId(new byte[] {1, 2}))
+ .isNotEqualTo(new LibsqlRowId(new byte[] {1, 3}));
+ }
+
// ---------------------------------------------------------------------
// Fixtures and helpers.
// ---------------------------------------------------------------------
@@ -502,6 +605,20 @@ private static LibsqlResultSet oneRow(JdbcType type, Value value) {
return oneRow(type.name(), value);
}
+ private static String readAll(Reader reader) throws IOException {
+ StringBuilder text = new StringBuilder();
+ char[] buffer = new char[256];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ text.append(buffer, 0, count);
+ }
+ return text.toString();
+ }
+
+ private static byte[] readAll(InputStream stream) throws IOException {
+ return stream.readAllBytes();
+ }
+
private static LibsqlResultSet oneRow(String declaredType, Value value) {
return new LibsqlResultSet(
new LibsqlStatement(null),
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
index fae61d1..41cb4d7 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
@@ -472,24 +472,12 @@ void everyUnsupportedMethodThrowsSqlFeatureNotSupportedException() throws SQLExc
Reader reader = null;
List calls = List.of(
- new UnsupportedCall("getAsciiStream(int)", () -> rs.getAsciiStream(1)),
- new UnsupportedCall("getAsciiStream(String)", () -> rs.getAsciiStream("c1")),
new UnsupportedCall("getUnicodeStream(int)", () -> rs.getUnicodeStream(1)),
new UnsupportedCall("getUnicodeStream(String)", () -> rs.getUnicodeStream("c1")),
new UnsupportedCall("getRef(int)", () -> rs.getRef(1)),
new UnsupportedCall("getRef(String)", () -> rs.getRef("c1")),
new UnsupportedCall("getArray(int)", () -> rs.getArray(1)),
new UnsupportedCall("getArray(String)", () -> rs.getArray("c1")),
- new UnsupportedCall("getURL(int)", () -> rs.getURL(1)),
- new UnsupportedCall("getURL(String)", () -> rs.getURL("c1")),
- new UnsupportedCall("getRowId(int)", () -> rs.getRowId(1)),
- new UnsupportedCall("getRowId(String)", () -> rs.getRowId("c1")),
- new UnsupportedCall("getNClob(int)", () -> rs.getNClob(1)),
- new UnsupportedCall("getNClob(String)", () -> rs.getNClob("c1")),
- new UnsupportedCall("getSQLXML(int)", () -> rs.getSQLXML(1)),
- new UnsupportedCall("getSQLXML(String)", () -> rs.getSQLXML("c1")),
- new UnsupportedCall("getNCharacterStream(int)", () -> rs.getNCharacterStream(1)),
- new UnsupportedCall("getNCharacterStream(String)", () -> rs.getNCharacterStream("c1")),
new UnsupportedCall("updateNull(int)", () -> rs.updateNull(1)),
new UnsupportedCall("updateBoolean(int)", () -> rs.updateBoolean(1, true)),
new UnsupportedCall("updateByte(int)", () -> rs.updateByte(1, (byte) 1)),