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/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/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/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"); } 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"); + } +} 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/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); 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)),