diff --git a/libsql-hrana-codec/src/main/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodec.java b/libsql-hrana-codec/src/main/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodec.java
index d0c0654..d46042b 100644
--- a/libsql-hrana-codec/src/main/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodec.java
+++ b/libsql-hrana-codec/src/main/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodec.java
@@ -694,7 +694,16 @@ public void writeString(String s) {
} else if (c < 0x80) {
writeByte((byte) c);
} else {
- byte[] utf8 = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
+ String chunk;
+ if (Character.isHighSurrogate(c)
+ && i + 1 < len
+ && Character.isLowSurrogate(s.charAt(i + 1))) {
+ chunk = s.substring(i, i + 2);
+ i++;
+ } else {
+ chunk = String.valueOf(c);
+ }
+ byte[] utf8 = chunk.getBytes(StandardCharsets.UTF_8);
ensureCapacity(count + utf8.length);
System.arraycopy(utf8, 0, buf, count, utf8.length);
count += utf8.length;
diff --git a/libsql-hrana-codec/src/test/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java b/libsql-hrana-codec/src/test/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java
index 588043b..c393009 100644
--- a/libsql-hrana-codec/src/test/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java
+++ b/libsql-hrana-codec/src/test/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java
@@ -34,6 +34,20 @@ void testSerializePipelineRequestOneShot() throws IOException {
assertThat(json).contains("\"value\":\"hello \\\"world\\\"\\nnewline\"");
}
+ @Test
+ void testSerializeSupplementaryCharactersAsUtf8() throws IOException {
+ String supplementary = "\uD840\uDC40 and \uD880\uDC99";
+ Stmt stmt = new Stmt("SELECT '" + supplementary + "'", List.of(Value.of(supplementary)));
+ PipelineReqBody req = new PipelineReqBody(null, List.of(new StreamRequest.Execute(stmt)));
+
+ byte[] jsonBytes = codec.serializePipelineRequest(req);
+ String json = new String(jsonBytes, StandardCharsets.UTF_8);
+
+ assertThat(json).contains("\"sql\":\"SELECT '" + supplementary + "'\"");
+ assertThat(json).contains("\"value\":\"" + supplementary + "\"");
+ assertThat(json).doesNotContain("?");
+ }
+
@Test
void testDeserializePipelineResponse() throws IOException {
String json = """
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 2dcb12d..9ce6b7b 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
@@ -4,10 +4,13 @@
import com.github.magnusp.libsql.hrana.codec.StmtResult;
import com.github.magnusp.libsql.hrana.codec.Value;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
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;
@@ -20,9 +23,11 @@
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
@@ -35,11 +40,17 @@
* #getGeneratedKeys()} throw after {@link #close()}. A prepared execute already throws once closed,
* so there is no post-close execute that could expose a result set. sqlite-jdbc 3.53.4.0 instead
* returns generated keys, and rejects {@code addBatch()} with a different message; see issue #87.
+ *
+ *
Parameter metadata is derived locally. The placeholder count comes from the SQL text, but the
+ * parameter type, precision, and scale stay unknown because Hrana has no describe request. The
+ * stream and {@link Blob}/{@link Clob} setters convert to a TEXT or BLOB value locally. An unset
+ * parameter, including one released by {@link #clearParameters()}, binds SQL NULL.
*/
-public class LibsqlPreparedStatement extends LibsqlStatement implements PreparedStatement {
+public class LibsqlPreparedStatement extends LibsqlStatement implements PreparedStatement, ParameterMetaData {
private final String sql;
private final boolean returnsResultSet;
+ private final int parameterCount;
// Mitigation for Bug 7: Sequential ArrayList instead of Map to eliminate Integer autoboxing overhead
private final List parameters = new ArrayList<>();
@@ -47,6 +58,7 @@ public LibsqlPreparedStatement(LibsqlConnection connection, String sql) {
super(connection);
this.sql = sql;
this.returnsResultSet = returnsResultSet(sql);
+ this.parameterCount = SqlTokenizer.placeholderCount(sql);
}
private void setParam(int parameterIndex, Value value) throws SQLException {
@@ -59,7 +71,12 @@ private void setParam(int parameterIndex, Value value) throws SQLException {
}
private Stmt buildCurrentStmt() {
- return new Stmt(sql, new ArrayList<>(parameters));
+ List args = new ArrayList<>(parameters);
+ while (args.size() < parameterCount) {
+ // An unset parameter, including one released by clearParameters, binds SQL NULL.
+ args.add(Value.ofNull());
+ }
+ return new Stmt(sql, args);
}
@Override
@@ -352,37 +369,324 @@ public void setObject(int parameterIndex, Object x, int targetSqlType, int scale
setObject(parameterIndex, x);
}
- @Override public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override @Deprecated @SuppressWarnings("deprecation") public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setRef(int parameterIndex, Ref x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setClob(int parameterIndex, Clob x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setArray(int parameterIndex, Array x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public ResultSetMetaData getMetaData() throws SQLException { return currentResultSet != null ? currentResultSet.getMetaData() : null; }
- @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { setDate(parameterIndex, x); }
- @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { setTime(parameterIndex, x); }
- @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { setTimestamp(parameterIndex, x); }
- @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { setNull(parameterIndex, sqlType); }
- @Override public void setURL(int parameterIndex, URL x) throws SQLException { setString(parameterIndex, x != null ? x.toString() : null); }
- @Override public ParameterMetaData getParameterMetaData() throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setNString(int parameterIndex, String value) throws SQLException { setString(parameterIndex, value); }
- @Override public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setNClob(int parameterIndex, NClob value) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setClob(int parameterIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { throw new UnsupportedOperationException(); }
- @Override public void setNClob(int parameterIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException(); }
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ setParam(parameterIndex, asciiText(readBytes(x, length)));
+ }
+
+ @Override
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ setParam(parameterIndex, utf8Text(readBytes(x, length)));
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
+ setParam(parameterIndex, Value.of(readBytes(x, length)));
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
+ setParam(parameterIndex, Value.of(readCharacters(reader, length)));
+ }
+
+ @Override
+ public void setRef(int parameterIndex, Ref x) throws SQLException {
+ throw new SQLFeatureNotSupportedException("Ref parameters are not supported");
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, Blob x) throws SQLException {
+ setParam(parameterIndex, Value.of(x == null ? null : readAllBytes(x.getBinaryStream())));
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Clob x) throws SQLException {
+ setParam(parameterIndex, Value.of(x == null ? null : readAllCharacters(x.getCharacterStream())));
+ }
+
+ @Override
+ public void setArray(int parameterIndex, Array x) throws SQLException {
+ throw new SQLFeatureNotSupportedException("Array parameters are not supported");
+ }
+
+ @Override
+ public ResultSetMetaData getMetaData() throws SQLException {
+ return currentResultSet != null ? currentResultSet.getMetaData() : null;
+ }
+
+ @Override
+ public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
+ setDate(parameterIndex, x);
+ }
+
+ @Override
+ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
+ setTime(parameterIndex, x);
+ }
+
+ @Override
+ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
+ setTimestamp(parameterIndex, x);
+ }
+
+ @Override
+ public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
+ setNull(parameterIndex, sqlType);
+ }
+
+ @Override
+ public void setURL(int parameterIndex, URL x) throws SQLException {
+ setString(parameterIndex, x != null ? x.toString() : null);
+ }
+
+ @Override
+ public ParameterMetaData getParameterMetaData() throws SQLException {
+ checkClosed();
+ return this;
+ }
+
+ @Override
+ public int getParameterCount() throws SQLException {
+ checkClosed();
+ return parameterCount;
+ }
+
+ @Override
+ public int isNullable(int param) throws SQLException {
+ checkParameterIndex(param);
+ return ParameterMetaData.parameterNullableUnknown;
+ }
+
+ @Override
+ public boolean isSigned(int param) throws SQLException {
+ checkParameterIndex(param);
+ return true;
+ }
+
+ @Override
+ public int getPrecision(int param) throws SQLException {
+ checkParameterIndex(param);
+ return 0;
+ }
+
+ @Override
+ public int getScale(int param) throws SQLException {
+ checkParameterIndex(param);
+ return 0;
+ }
+
+ @Override
+ public int getParameterType(int param) throws SQLException {
+ checkParameterIndex(param);
+ // Hrana has no describe statement, so a parameter type is unknown until it is executed.
+ return Types.OTHER;
+ }
+
+ @Override
+ public String getParameterTypeName(int param) throws SQLException {
+ checkParameterIndex(param);
+ return "OTHER";
+ }
+
+ @Override
+ public String getParameterClassName(int param) throws SQLException {
+ checkParameterIndex(param);
+ return Object.class.getName();
+ }
+
+ @Override
+ public int getParameterMode(int param) throws SQLException {
+ checkParameterIndex(param);
+ return ParameterMetaData.parameterModeIn;
+ }
+
+ private void checkParameterIndex(int param) throws SQLException {
+ checkClosed();
+ if (param < 1 || param > parameterCount) {
+ throw new SQLException("Parameter index out of range: " + param);
+ }
+ }
+
+ @Override
+ public void setRowId(int parameterIndex, RowId x) throws SQLException {
+ throw new SQLFeatureNotSupportedException("RowId parameters are not supported");
+ }
+
+ @Override
+ public void setNString(int parameterIndex, String value) throws SQLException {
+ setString(parameterIndex, value);
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
+ setCharacterStream(parameterIndex, value, length);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, NClob value) throws SQLException {
+ setClob(parameterIndex, (Clob) value);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ setParam(parameterIndex, Value.of(readCharacters(reader, length)));
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
+ setParam(parameterIndex, Value.of(readBytes(inputStream, length)));
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
+ setClob(parameterIndex, reader, length);
+ }
+
+ @Override
+ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
+ throw new SQLFeatureNotSupportedException("SQLXML parameters are not supported");
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ setParam(parameterIndex, asciiText(readBytes(x, length)));
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
+ setParam(parameterIndex, Value.of(readBytes(x, length)));
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
+ setParam(parameterIndex, Value.of(readCharacters(reader, length)));
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
+ setParam(parameterIndex, asciiText(readAllBytes(x)));
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
+ setParam(parameterIndex, Value.of(readAllBytes(x)));
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
+ setParam(parameterIndex, Value.of(readAllCharacters(reader)));
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
+ setCharacterStream(parameterIndex, value);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader) throws SQLException {
+ setParam(parameterIndex, Value.of(readAllCharacters(reader)));
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
+ setParam(parameterIndex, Value.of(readAllBytes(inputStream)));
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader) throws SQLException {
+ setClob(parameterIndex, reader);
+ }
+
+ private static Value asciiText(byte[] bytes) {
+ return Value.of(bytes == null ? null : new String(bytes, StandardCharsets.US_ASCII));
+ }
+
+ private static Value utf8Text(byte[] bytes) {
+ return Value.of(bytes == null ? null : new String(bytes, StandardCharsets.UTF_8));
+ }
+
+ private static byte[] readAllBytes(InputStream in) throws SQLException {
+ if (in == null) {
+ return null;
+ }
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ return out.toByteArray();
+ } catch (IOException e) {
+ throw new SQLException("Cannot read from binary stream: " + e.getMessage(), e);
+ }
+ }
+
+ private static byte[] readBytes(InputStream in, long length) throws SQLException {
+ if (in == null) {
+ return null;
+ }
+ if (length < 0 || length > Integer.MAX_VALUE) {
+ throw new SQLException("Data must have a length between 0 and Integer.MAX_VALUE");
+ }
+ byte[] bytes = new byte[(int) length];
+ int total = 0;
+ try {
+ while (total < bytes.length) {
+ int read = in.read(bytes, total, bytes.length - total);
+ if (read < 0) {
+ throw new SQLException("End of stream reached before " + length + " bytes were read");
+ }
+ total += read;
+ }
+ } catch (IOException e) {
+ throw new SQLException("Cannot read from binary stream: " + e.getMessage(), e);
+ }
+ return bytes;
+ }
+
+ private static String readAllCharacters(Reader reader) throws SQLException {
+ if (reader == null) {
+ return null;
+ }
+ try {
+ StringBuilder text = new StringBuilder();
+ char[] buffer = new char[8192];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ text.append(buffer, 0, read);
+ }
+ return text.toString();
+ } catch (IOException e) {
+ throw new SQLException("Cannot read from character stream: " + e.getMessage(), e);
+ }
+ }
+
+ private static String readCharacters(Reader reader, long length) throws SQLException {
+ if (reader == null) {
+ return null;
+ }
+ if (length < 0 || length > Integer.MAX_VALUE) {
+ throw new SQLException("Data must have a length between 0 and Integer.MAX_VALUE");
+ }
+ StringBuilder text = new StringBuilder((int) Math.min(length, 8192));
+ char[] buffer = new char[8192];
+ long remaining = length;
+ try {
+ while (remaining > 0) {
+ int read = reader.read(buffer, 0, (int) Math.min(remaining, buffer.length));
+ if (read < 0) {
+ throw new SQLException(
+ "End of stream reached before " + length + " characters were read");
+ }
+ text.append(buffer, 0, read);
+ remaining -= read;
+ }
+ } catch (IOException e) {
+ throw new SQLException("Cannot read from character stream: " + e.getMessage(), e);
+ }
+ return text.toString();
+ }
}
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java
index 1492170..ea60206 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java
@@ -180,7 +180,27 @@ protected boolean executeResult(String sql, StmtResult result) throws SQLExcepti
private StmtResult executeSql(String sql) throws SQLException {
checkEscapeProcessing(sql);
beginExecution();
- return connection.executeStatement(new Stmt(sql));
+ return connection.executeStatement(statement(sql));
+ }
+
+ /**
+ * Builds a statement with SQL NULL for every placeholder a plain {@code Statement} leaves
+ * unbound, matching SQLite, where an unset parameter reads as NULL. Hrana rejects an argument
+ * list shorter than the SQL placeholders, so the nulls must be sent explicitly.
+ */
+ static Stmt statement(String sql) {
+ if (sql == null || sql.indexOf('?') < 0) {
+ return new Stmt(sql);
+ }
+ int placeholders = SqlTokenizer.placeholderCount(sql);
+ if (placeholders == 0) {
+ return new Stmt(sql);
+ }
+ List args = new ArrayList<>(placeholders);
+ for (int i = 0; i < placeholders; i++) {
+ args.add(Value.ofNull());
+ }
+ return new Stmt(sql, args);
}
/**
@@ -521,7 +541,7 @@ public boolean getMoreResults() throws SQLException {
public void addBatch(String sql) throws SQLException {
checkClosed();
checkEscapeProcessing(sql);
- batchStatements.add(new Stmt(sql));
+ batchStatements.add(statement(sql));
}
@Override
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTokenizer.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTokenizer.java
index 0792c78..6c5bb0b 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTokenizer.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTokenizer.java
@@ -147,6 +147,20 @@ static List tokenize(String sql) {
return tokens;
}
+ static int placeholderCount(String sql) {
+ List tokens = tokenize(sql);
+ if (tokens == null) {
+ return 0;
+ }
+ int count = 0;
+ for (Token token : tokens) {
+ if (token.isSymbol("?")) {
+ count++;
+ }
+ }
+ return count;
+ }
+
static boolean isWordAt(List tokens, int index, int depth, String word) {
if (index < 0 || index >= tokens.size()) {
return false;
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 ad08f45..aa2626e 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
@@ -7,9 +7,14 @@
import com.github.magnusp.libsql.hrana.codec.Value;
import org.junit.jupiter.api.Test;
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.sql.ParameterMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
@@ -148,9 +153,115 @@ void escapeProcessingAppliesToPreparedSql() throws SQLException {
}
}
+ @Test
+ void parameterCountComesFromThePlaceholders() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ try (LibsqlPreparedStatement statement =
+ new LibsqlPreparedStatement(connection, "select ?, ?, ?")) {
+ assertThat(statement.getParameterMetaData().getParameterCount()).isEqualTo(3);
+ }
+ try (LibsqlPreparedStatement statement = new LibsqlPreparedStatement(
+ connection, "select '?', \"?\", ? -- trailing ?\n")) {
+ assertThat(statement.getParameterMetaData().getParameterCount()).isEqualTo(1);
+ }
+ }
+
+ @Test
+ void parameterTypesAreUnknownWithoutADescription() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ try (LibsqlPreparedStatement statement =
+ new LibsqlPreparedStatement(connection, "insert into t values (?)")) {
+ ParameterMetaData meta = statement.getParameterMetaData();
+
+ assertThat(meta.getParameterType(1)).isEqualTo(Types.OTHER);
+ assertThat(meta.getParameterTypeName(1)).isEqualTo("OTHER");
+ assertThat(meta.getParameterMode(1)).isEqualTo(ParameterMetaData.parameterModeIn);
+ }
+ }
+
+ @Test
+ void clearParametersBindsNullArguments() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ try (LibsqlPreparedStatement statement =
+ new LibsqlPreparedStatement(connection, "insert into t values (?)")) {
+ statement.setString(1, "value");
+ statement.clearParameters();
+
+ assertThat(statement.executeUpdate()).isEqualTo(1);
+ }
+
+ assertThat(connection.executed.get(0).args()).containsExactly(Value.ofNull());
+ }
+
+ @Test
+ void unsetParametersBindNullArguments() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ try (LibsqlPreparedStatement statement =
+ new LibsqlPreparedStatement(connection, "insert into t values (?, ?)")) {
+ statement.setInt(1, 7);
+
+ statement.executeUpdate();
+ }
+
+ assertThat(connection.executed.get(0).args())
+ .containsExactly(Value.of(7L), Value.ofNull());
+ }
+
+ @Test
+ void streamSettersBindLocalConversions() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ byte[] bytes = {1, 2, 3};
+ String supplementary = "\uD840\uDC40 stream";
+ try (LibsqlPreparedStatement statement = new LibsqlPreparedStatement(
+ connection, "insert into t values (?, ?, ?, ?, ?)")) {
+ statement.setClob(1, new StringReader("clob"));
+ statement.setBlob(2, new ByteArrayInputStream(bytes));
+ statement.setBinaryStream(3, new ByteArrayInputStream(bytes), bytes.length);
+ statement.setAsciiStream(
+ 4,
+ new ByteArrayInputStream("ascii".getBytes(StandardCharsets.US_ASCII)),
+ "ascii".length());
+ statement.setUnicodeStream(
+ 5,
+ new ByteArrayInputStream(supplementary.getBytes(StandardCharsets.UTF_8)),
+ supplementary.getBytes(StandardCharsets.UTF_8).length);
+
+ statement.executeUpdate();
+ }
+
+ assertThat(connection.executed.get(0).args())
+ .containsExactly(
+ Value.of("clob"),
+ Value.of(bytes),
+ Value.of(bytes),
+ Value.of("ascii"),
+ Value.of(supplementary));
+ }
+
+ @Test
+ void streamSettersWithoutALengthReadToEndOfStream() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(NO_ROWS);
+ byte[] bytes = {4, 5, 6};
+ try (LibsqlPreparedStatement statement = new LibsqlPreparedStatement(
+ connection, "insert into t values (?, ?, ?, ?)")) {
+ statement.setClob(1, new StringReader("clob"));
+ statement.setBlob(2, new ByteArrayInputStream(bytes));
+ statement.setBinaryStream(3, new ByteArrayInputStream(bytes));
+ statement.setAsciiStream(
+ 4, new ByteArrayInputStream("ascii".getBytes(StandardCharsets.US_ASCII)));
+
+ statement.executeUpdate();
+ }
+
+ assertThat(connection.executed.get(0).args())
+ .containsExactly(Value.of("clob"), Value.of(bytes), Value.of(bytes),
+ Value.of("ascii"));
+ }
+
private static final class RecordingConnection extends LibsqlConnection {
private final StmtResult result;
private final List executedSql = new ArrayList<>();
+ private final List executed = new ArrayList<>();
private int executeCalls;
private RecordingConnection(StmtResult result) {
@@ -162,6 +273,7 @@ private RecordingConnection(StmtResult result) {
public StmtResult executeStatement(Stmt stmt) {
executeCalls++;
executedSql.add(stmt.sql());
+ executed.add(stmt);
return result;
}
}
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java
index 64306a3..e430881 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java
@@ -181,10 +181,21 @@ void batchThatReturnsResultsThrowsBatchUpdateException() throws SQLException {
.containsExactly(1, 0));
}
+ @Test
+ void plainStatementBindsNullForUnboundPlaceholders() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(THREE_ROWS);
+ LibsqlStatement statement = new LibsqlStatement(connection);
+
+ statement.execute("insert into t values (?)");
+
+ assertThat(connection.executedStatements.get(0).args()).containsExactly(Value.ofNull());
+ }
+
private static final class RecordingConnection extends LibsqlConnection {
private final StmtResult result;
private final List batchResults;
private final List executedSql = new ArrayList<>();
+ private final List executedStatements = new ArrayList<>();
private RecordingConnection(StmtResult result) {
this(result, List.of());
@@ -199,6 +210,7 @@ private RecordingConnection(StmtResult result, List batchResults) {
@Override
public StmtResult executeStatement(Stmt stmt) {
executedSql.add(stmt.sql());
+ executedStatements.add(stmt);
return result;
}