From 1838efce386aee0ceb670a9d6fabc42218d996b3 Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:06:24 +0800 Subject: [PATCH] Honor Blob ranges and independent read cursors Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- src/main/java/org/duckdb/DuckDBResultSet.java | 33 +++++-- src/test/java/org/duckdb/TestDuckDBJDBC.java | 91 +++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/duckdb/DuckDBResultSet.java b/src/main/java/org/duckdb/DuckDBResultSet.java index 4e4698f65..576cac323 100644 --- a/src/main/java/org/duckdb/DuckDBResultSet.java +++ b/src/main/java/org/duckdb/DuckDBResultSet.java @@ -455,6 +455,15 @@ public int read() throws IOException { } public int read(byte[] bytes, int off, int len) throws IOException { + if (bytes == null) { + throw new NullPointerException(); + } + if (off < 0 || len < 0 || len > bytes.length - off) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } if (!buf.hasRemaining()) { return -1; } @@ -472,21 +481,31 @@ public DuckDBBlobResult(ByteBuffer buffer_p) { } public InputStream getBinaryStream() { - return getBinaryStream(0, length()); + return new ByteBufferBackedInputStream(buffer.duplicate()); } - public InputStream getBinaryStream(long pos, long length) { - return new ByteBufferBackedInputStream(buffer); + public InputStream getBinaryStream(long pos, long length) throws SQLException { + long blobLength = length(); + if (pos < 1 || length < 0 || pos - 1 > blobLength || length > blobLength - (pos - 1)) { + throw createSQLException("Invalid position or length", ErrorCode.RESULT_SET_INVALID_POS_LEN); + } + ByteBuffer slice = buffer.duplicate(); + slice.position((int) (pos - 1)); + slice.limit((int) (pos - 1 + length)); + return new ByteBufferBackedInputStream(slice); } @Override public byte[] getBytes(long pos, int length) throws SQLException { - if (pos < 1 || length < 0) { + long blobLength = length(); + if (pos < 1 || length < 0 || pos - 1 > blobLength) { throw createSQLException("Invalid position or length", ErrorCode.RESULT_SET_INVALID_POS_LEN); } - byte[] bytes = new byte[length]; - buffer.position((int) pos - 1); - buffer.get(bytes, 0, length); + int available = (int) Math.min((long) length, blobLength - (pos - 1)); + byte[] bytes = new byte[available]; + ByteBuffer dup = buffer.duplicate(); + dup.position((int) (pos - 1)); + dup.get(bytes, 0, available); return bytes; } diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index a2aeb750b..2cf997049 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -18,6 +18,8 @@ import static org.duckdb.test.Runner.runTests; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; @@ -2007,6 +2009,95 @@ public static void test_blob_after_rs_next() throws Exception { } } + private static byte[] readAll(InputStream in) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8]; + int n; + while ((n = in.read(buf, 0, buf.length)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + public static void test_blob_binary_stream_slice() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 'ABCDEF'::BLOB")) { + assertTrue(rs.next()); + Blob blob = rs.getBlob(1); + // 1-based pos with an exact bounded length yields just the requested slice + assertEquals(new String(readAll(blob.getBinaryStream(2, 3)), US_ASCII), "BCD"); + // whole-blob stream still sees everything + assertEquals(new String(readAll(blob.getBinaryStream()), US_ASCII), "ABCDEF"); + // bounded slice from the first byte spanning the whole blob + assertEquals(new String(readAll(blob.getBinaryStream(1, blob.length())), US_ASCII), "ABCDEF"); + } + } + + public static void test_blob_streams_independent_cursors() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 'ABCDEF'::BLOB")) { + assertTrue(rs.next()); + Blob blob = rs.getBlob(1); + InputStream first = blob.getBinaryStream(); + InputStream second = blob.getBinaryStream(); + // Advancing one stream must not move the other + assertEquals(first.read(), (int) 'A'); + assertEquals(second.read(), (int) 'A'); + assertEquals(first.read(), (int) 'B'); + assertEquals(second.read(), (int) 'B'); + // getBytes must not disturb either stream's cursor + assertEquals(new String(blob.getBytes(1, (int) blob.length()), US_ASCII), "ABCDEF"); + assertEquals(first.read(), (int) 'C'); + assertEquals(second.read(), (int) 'C'); + assertEquals(new String(readAll(first), US_ASCII), "DEF"); + assertEquals(new String(readAll(second), US_ASCII), "DEF"); + } + } + + public static void test_blob_get_bytes_overlength() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 'ABCDEF'::BLOB")) { + assertTrue(rs.next()); + Blob blob = rs.getBlob(1); + // Over-length is clipped to the remaining bytes per JDBC + assertEquals(new String(blob.getBytes(2, 100), US_ASCII), "BCDEF"); + assertEquals(new String(blob.getBytes(1, 100), US_ASCII), "ABCDEF"); + // Zero-length read returns 0 even at EOF; an empty range yields an empty array/stream + InputStream empty = blob.getBinaryStream(1, 0); + assertEquals(empty.read(new byte[4], 0, 0), 0); + assertEquals(empty.read(new byte[4], 0, 4), -1); + assertEquals(blob.getBytes(1, 0).length, 0); + // Empty blob supports an empty whole-blob stream + assertEquals(new String(readAll(blobOf("").getBinaryStream()), US_ASCII), ""); + } + } + + public static void test_blob_invalid_pos_len() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 'ABCDEF'::BLOB")) { + assertTrue(rs.next()); + Blob blob = rs.getBlob(1); + assertThrows(() -> blob.getBytes(0, 1), SQLException.class); + assertThrows(() -> blob.getBytes(1, -1), SQLException.class); + assertThrows(() -> blob.getBinaryStream(0, 1), SQLException.class); + assertThrows(() -> blob.getBinaryStream(1, -1), SQLException.class); + assertThrows(() -> blob.getBinaryStream(2, 100), SQLException.class); + // Extreme positions/lengths must not silently wrap + assertThrows(() -> blob.getBytes(Long.MAX_VALUE, 1), SQLException.class); + assertThrows(() -> blob.getBinaryStream(Long.MAX_VALUE, 1), SQLException.class); + assertThrows(() -> blob.getBinaryStream(1, Long.MAX_VALUE), SQLException.class); + } + } + + public static void test_blob_stream_read_arg_validation() throws Exception { + // Even at EOF / on a zero-length blob stream, read(byte[], int, int) must + // validate its arguments before reporting -1. + Blob empty = blobOf(""); + InputStream stream = empty.getBinaryStream(); + assertThrows(() -> stream.read(null, 0, 0), NullPointerException.class); + assertThrows(() -> stream.read(new byte[1], 2, 0), IndexOutOfBoundsException.class); + } + public static void test_typed_connection_properties() throws Exception { Properties config = new Properties(); config.put("autoinstall_known_extensions", false); // BOOLEAN