Repository: xerial/sqlite-jdbc
Pull request #1301 (2025-07-08 11:32:38)

Support JDBC4 CLOB and BLOB in PreparedStatement parameters
There are TODO markers in the JDBC4 Prepared Statement implementation relating to BLOB and CLOB data. These can easily be implemented by delegating to the existing JDBC implementations. This may not be optimised, but it will allow the use of CLOB and BLOB, which is a significant improvement.


Patch:
diff --git a/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java b/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
index 6c2c3434..542f0b95 100644
--- a/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
+++ b/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
@@ -390,13 +390,14 @@ public abstract class JDBC3PreparedStatement extends CorePreparedStatement {
     /** @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int) */
     public void setCharacterStream(int pos, Reader reader, int length) throws SQLException {
         try {
-            // copy chars from reader to StringBuffer
-            StringBuffer sb = new StringBuffer();
+            // copy chars from reader to StringBuilder
+            StringBuilder sb = new StringBuilder();
             char[] cbuf = new char[8192];
             int cnt;
 
-            while ((cnt = reader.read(cbuf)) > 0) {
+            while ((cnt = reader.read(cbuf, 0, Math.min(length, cbuf.length))) > 0) {
                 sb.append(cbuf, 0, cnt);
+                length -= cnt;
             }
 
             // set as string
diff --git a/src/main/java/org/sqlite/jdbc4/JDBC4PreparedStatement.java b/src/main/java/org/sqlite/jdbc4/JDBC4PreparedStatement.java
index b4221e71..ee3ef3d8 100644
--- a/src/main/java/org/sqlite/jdbc4/JDBC4PreparedStatement.java
+++ b/src/main/java/org/sqlite/jdbc4/JDBC4PreparedStatement.java
@@ -1,5 +1,8 @@
 package org.sqlite.jdbc4;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.sql.NClob;
@@ -48,14 +51,21 @@ public class JDBC4PreparedStatement extends JDBC3PreparedStatement
     }
 
     public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        requireLengthIsPositiveInt(length);
+        setCharacterStream(parameterIndex, reader, (int) length);
+    }
+
+    private void requireLengthIsPositiveInt(long length) throws SQLFeatureNotSupportedException {
+        if (length > Integer.MAX_VALUE || length < 0) {
+            throw new SQLFeatureNotSupportedException(
+                    "Data must have a length between 0 and Integer.MAX_VALUE");
+        }
     }
 
     public void setBlob(int parameterIndex, InputStream inputStream, long length)
             throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        requireLengthIsPositiveInt(length);
+        setBinaryStream(parameterIndex, inputStream, (int) length);
     }
 
     public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
@@ -69,35 +79,59 @@ public class JDBC4PreparedStatement extends JDBC3PreparedStatement
     }
 
     public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        requireLengthIsPositiveInt(length);
+        setAsciiStream(parameterIndex, x, (int) length);
     }
 
     public void setBinaryStream(int parameterIndex, InputStream x, long length)
             throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        requireLengthIsPositiveInt(length);
+        setBinaryStream(parameterIndex, x, (int) length);
     }
 
     public void setCharacterStream(int parameterIndex, Reader reader, long length)
             throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        requireLengthIsPositiveInt(length);
+        setCharacterStream(parameterIndex, reader, (int) length);
     }
 
     public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        byte[] bytes = readBytes(x);
+        setAsciiStream(parameterIndex, new ByteArrayInputStream(bytes), bytes.length);
+    }
+
+    /**
+     * Reads given number of bytes from an input stream.
+     *
+     * @param istream The input stream.
+     * @param length The number of bytes to read.
+     * @return byte array.
+     * @throws SQLException
+     */
+    private byte[] readBytes(InputStream istream) throws SQLException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] bytes = new byte[8192];
+
+        try {
+            int bytesRead;
+            while ((bytesRead = istream.read(bytes)) > 0) {
+                baos.write(bytes, 0, bytesRead);
+            }
+            return baos.toByteArray();
+        } catch (IOException cause) {
+            SQLException exception = new SQLException("Error reading stream");
+
+            exception.initCause(cause);
+            throw exception;
+        }
     }
 
     public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        setBytes(parameterIndex, readBytes(x));
     }
 
     public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        setCharacterStream(parameterIndex, reader, Integer.MAX_VALUE);
     }
 
     public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
@@ -106,13 +140,11 @@ public class JDBC4PreparedStatement extends JDBC3PreparedStatement
     }
 
     public void setClob(int parameterIndex, Reader reader) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        setCharacterStream(parameterIndex, reader, Integer.MAX_VALUE);
     }
 
     public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
-        // TODO Support this
-        throw new SQLFeatureNotSupportedException();
+        setBytes(parameterIndex, readBytes(inputStream));
     }
 
     public void setNClob(int parameterIndex, Reader reader) throws SQLException {


Test patch:
diff --git a/src/test/java/org/sqlite/PrepStmtTest.java b/src/test/java/org/sqlite/PrepStmtTest.java
index 1bf609d9..3dca80ed 100644
--- a/src/test/java/org/sqlite/PrepStmtTest.java
+++ b/src/test/java/org/sqlite/PrepStmtTest.java
@@ -7,6 +7,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.data.Offset.offset;
 
 import java.io.ByteArrayInputStream;
+import java.io.StringReader;
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.sql.Connection;
@@ -177,6 +178,32 @@ public class PrepStmtTest {
         rs.close();
     }
 
+    @Test
+    public void clobRS() throws SQLException {
+        String name = "Gandhi";
+        PreparedStatement prep = conn.prepareStatement("select ?;");
+        prep.setClob(1, new StringReader(name));
+        ResultSet rs = prep.executeQuery();
+        assertThat(prep.getUpdateCount()).isEqualTo(-1);
+        assertThat(rs.next()).isTrue();
+        assertThat(rs.getString(1)).isEqualTo(name);
+        assertThat(rs.next()).isFalse();
+        rs.close();
+    }
+
+    @Test
+    public void blobRS() throws SQLException {
+        String name = "Gandhi";
+        PreparedStatement prep = conn.prepareStatement("select ?;");
+        prep.setBlob(1, new ByteArrayInputStream(name.getBytes()));
+        ResultSet rs = prep.executeQuery();
+        assertThat(prep.getUpdateCount()).isEqualTo(-1);
+        assertThat(rs.next()).isTrue();
+        assertThat(rs.getBytes(1)).isEqualTo(name.getBytes());
+        assertThat(rs.next()).isFalse();
+        rs.close();
+    }
+
     @Test
     public void finalizePrep() throws SQLException {
         conn.prepareStatement("select null;");