Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/main/java/org/duckdb/DuckDBResultSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand Down
91 changes: 91 additions & 0 deletions src/test/java/org/duckdb/TestDuckDBJDBC.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading