From 2e62abe6df43a5604d5efc92981a95fede98dcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Wed, 29 Jan 2025 11:41:56 +0900 Subject: [PATCH 1/5] Implement basic getXX methods for JDBC4ResultSet --- .../tursodatabase/core/LimboResultSet.java | 19 ++ .../tursodatabase/core/LimboStatement.java | 5 + .../tursodatabase/core/LimboStepResult.java | 5 + .../tursodatabase/jdbc4/JDBC4ResultSet.java | 118 ++++++-- .../jdbc4/JDBC4ResultSetTest.java | 255 ++++++++++++++++++ 5 files changed, 375 insertions(+), 27 deletions(-) diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java index b226c53d0..6fe1ef1a3 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java @@ -117,6 +117,25 @@ public class LimboResultSet { this.open = false; } + // Note that columnIndex starts from 1 + @Nullable + public Object get(int columnIndex) throws SQLException { + if (!this.isOpen()) { + throw new SQLException("ResultSet is not open"); + } + + if (this.lastStepResult == null || this.lastStepResult.getResult() == null) { + throw new SQLException("ResultSet is null"); + } + + final Object[] resultSet = this.lastStepResult.getResult(); + if (columnIndex > resultSet.length) { + throw new SQLException("columnIndex out of bound"); + } + + return resultSet[columnIndex - 1]; + } + @Override public String toString() { return "LimboResultSet{" diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java index 8566c403e..fa660b67c 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java @@ -55,6 +55,11 @@ public class LimboStatement { return result; } + /** + * Because Limbo supports async I/O, it is possible to return a {@link LimboStepResult} with + * {@link LimboStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you + * can expect that this method will not return a {@link LimboStepResult#STEP_RESULT_ID_ROW}. + */ @Nullable private native LimboStepResult step(long stmtPointer) throws SQLException; diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java index 93a1878aa..b82750b9a 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java @@ -47,6 +47,11 @@ public class LimboStepResult { || stepResultId == STEP_RESULT_ID_ERROR; } + @Nullable + public Object[] getResult() { + return result; + } + @Override public String toString() { return "LimboStepResult{" diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java index 092bf4d44..ad3720b8c 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java @@ -3,10 +3,26 @@ package org.github.tursodatabase.jdbc4; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.URL; -import java.sql.*; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; import java.util.Calendar; import java.util.Map; +import org.github.tursodatabase.annotations.Nullable; import org.github.tursodatabase.annotations.SkipNullableCheck; import org.github.tursodatabase.core.LimboResultSet; @@ -35,64 +51,99 @@ public class JDBC4ResultSet implements ResultSet { } @Override + @Nullable public String getString(int columnIndex) throws SQLException { - // TODO - return ""; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return null; + } + return wrapTypeConversion(() -> (String) result); } @Override public boolean getBoolean(int columnIndex) throws SQLException { - // TODO - return false; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return false; + } + return wrapTypeConversion(() -> (Long) result != 0); } @Override public byte getByte(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> ((Long) result).byteValue()); } @Override public short getShort(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> ((Long) result).shortValue()); } @Override public int getInt(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> ((Long) result).intValue()); } @Override public long getLong(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> (long) result); } @Override public float getFloat(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> ((Double) result).floatValue()); } @Override public double getDouble(int columnIndex) throws SQLException { - // TODO - return 0; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return 0; + } + return wrapTypeConversion(() -> (double) result); } + // TODO: customize rounding mode? @Override - @SkipNullableCheck + @Nullable public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { - // TODO - return null; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return null; + } + final double doubleResult = wrapTypeConversion(() -> (double) result); + final BigDecimal bigDecimalResult = BigDecimal.valueOf(doubleResult); + return bigDecimalResult.setScale(scale, RoundingMode.HALF_UP); } @Override + @Nullable public byte[] getBytes(int columnIndex) throws SQLException { - // TODO - return new byte[0]; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return null; + } + return wrapTypeConversion(() -> (byte[]) result); } @Override @@ -300,10 +351,14 @@ public class JDBC4ResultSet implements ResultSet { } @Override - @SkipNullableCheck + @Nullable public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - // TODO - return null; + final Object result = resultSet.get(columnIndex); + if (result == null) { + return null; + } + final double doubleResult = wrapTypeConversion(() -> (double) result); + return BigDecimal.valueOf(doubleResult); } @Override @@ -1126,7 +1181,16 @@ public class JDBC4ResultSet implements ResultSet { return false; } - private SQLException throwNotSupportedException() { - return new SQLFeatureNotSupportedException("Not implemented by the driver"); + @FunctionalInterface + public interface ResultSetSupplier { + T get() throws Exception; + } + + private T wrapTypeConversion(ResultSetSupplier supplier) throws SQLException { + try { + return supplier.get(); + } catch (Exception e) { + throw new SQLException("Type conversion failed: " + e); + } } } diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index a16c096c9..ddd447b9d 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -1,16 +1,24 @@ package org.github.tursodatabase.jdbc4; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; +import java.util.stream.Stream; import org.github.tursodatabase.TestUtils; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class JDBC4ResultSetTest { @@ -79,4 +87,251 @@ class JDBC4ResultSetTest { assertThrows(SQLException.class, resultSet::next); } + + @Test + void test_getString() throws Exception { + stmt.executeUpdate("CREATE TABLE test_string (string_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_string (string_col) VALUES ('test');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_string"); + assertEquals("test", resultSet.getString(1)); + } + + @Test + void test_getBoolean_true() throws Exception { + stmt.executeUpdate("CREATE TABLE test_boolean (boolean_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (1);"); + stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (2);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); + + assertTrue(resultSet.getBoolean(1)); + + resultSet.next(); + assertTrue(resultSet.getBoolean(1)); + } + + @Test + void test_getBoolean_false() throws Exception { + stmt.executeUpdate("CREATE TABLE test_boolean (boolean_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (0);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); + assertFalse(resultSet.getBoolean(1)); + } + + @Test + void test_getByte() throws Exception { + stmt.executeUpdate("CREATE TABLE test_byte (byte_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (1);"); + stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (128);"); // Exceeds byte size + stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (-129);"); // Exceeds byte size + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_byte"); + + // Test value that fits within byte size + assertEquals(1, resultSet.getByte(1)); + + // Test value that exceeds byte size (positive overflow) + assertTrue(resultSet.next()); + assertEquals(-128, resultSet.getByte(1)); // 128 overflows to -128 + + // Test value that exceeds byte size (negative overflow) + assertTrue(resultSet.next()); + assertEquals(127, resultSet.getByte(1)); // -129 overflows to 127 + } + + @Test + void test_getShort() throws Exception { + stmt.executeUpdate("CREATE TABLE test_short (short_col SMALLINT);"); + stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (123);"); + stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (32767);"); // Max short value + stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (-32768);"); // Min short value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_short"); + + // Test typical short value + assertEquals(123, resultSet.getShort(1)); + + // Test maximum short value + assertTrue(resultSet.next()); + assertEquals(32767, resultSet.getShort(1)); + + // Test minimum short value + assertTrue(resultSet.next()); + assertEquals(-32768, resultSet.getShort(1)); + } + + @Test + void test_getInt() throws Exception { + stmt.executeUpdate("CREATE TABLE test_int (int_col INT);"); + stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (12345);"); + stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (2147483647);"); // Max int value + stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (-2147483648);"); // Min int value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_int"); + + // Test typical int value + assertEquals(12345, resultSet.getInt(1)); + + // Test maximum int value + assertTrue(resultSet.next()); + assertEquals(2147483647, resultSet.getInt(1)); + + // Test minimum int value + assertTrue(resultSet.next()); + assertEquals(-2147483648, resultSet.getInt(1)); + } + + @Test + @Disabled("limbo has a bug which sees -9223372036854775808 as double") + void test_getLong() throws Exception { + Long l1 = Long.MIN_VALUE; + Long l2 = Long.MAX_VALUE; + stmt.executeUpdate("CREATE TABLE test_long (long_col BIGINT);"); + stmt.executeUpdate("INSERT INTO test_long (long_col) VALUES (1234567890);"); + stmt.executeUpdate( + "INSERT INTO test_long (long_col) VALUES (9223372036854775807);"); // Max long value + stmt.executeUpdate( + "INSERT INTO test_long (long_col) VALUES (-9223372036854775808);"); // Min long value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_long"); + + // Test typical long value + assertEquals(1234567890L, resultSet.getLong(1)); + + // Test maximum long value + assertTrue(resultSet.next()); + assertEquals(9223372036854775807L, resultSet.getLong(1)); + + // Test minimum long value + assertTrue(resultSet.next()); + assertEquals(-9223372036854775808L, resultSet.getLong(1)); + } + + @Test + void test_getFloat() throws Exception { + stmt.executeUpdate("CREATE TABLE test_float (float_col REAL);"); + stmt.executeUpdate("INSERT INTO test_float (float_col) VALUES (1.23);"); + stmt.executeUpdate( + "INSERT INTO test_float (float_col) VALUES (3.4028235E38);"); // Max float value + stmt.executeUpdate( + "INSERT INTO test_float (float_col) VALUES (1.4E-45);"); // Min positive float value + stmt.executeUpdate( + "INSERT INTO test_float (float_col) VALUES (-3.4028235E38);"); // Min negative float value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_float"); + + // Test typical float value + assertEquals(1.23f, resultSet.getFloat(1), 0.0001); + + // Test maximum float value + assertTrue(resultSet.next()); + assertEquals(3.4028235E38f, resultSet.getFloat(1), 0.0001); + + // Test minimum positive float value + assertTrue(resultSet.next()); + assertEquals(1.4E-45f, resultSet.getFloat(1), 0.0001); + + // Test minimum negative float value + assertTrue(resultSet.next()); + assertEquals(-3.4028235E38f, resultSet.getFloat(1), 0.0001); + } + + @Test + void test_getDouble() throws Exception { + stmt.executeUpdate("CREATE TABLE test_double (double_col DOUBLE);"); + stmt.executeUpdate("INSERT INTO test_double (double_col) VALUES (1.234567);"); + stmt.executeUpdate( + "INSERT INTO test_double (double_col) VALUES (1.7976931348623157E308);"); // Max double + // value + stmt.executeUpdate( + "INSERT INTO test_double (double_col) VALUES (4.9E-324);"); // Min positive double value + stmt.executeUpdate( + "INSERT INTO test_double (double_col) VALUES (-1.7976931348623157E308);"); // Min negative + // double value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_double"); + + // Test typical double value + assertEquals(1.234567, resultSet.getDouble(1), 0.0001); + + // Test maximum double value + assertTrue(resultSet.next()); + assertEquals(1.7976931348623157E308, resultSet.getDouble(1), 0.0001); + + // Test minimum positive double value + assertTrue(resultSet.next()); + assertEquals(4.9E-324, resultSet.getDouble(1), 0.0001); + + // Test minimum negative double value + assertTrue(resultSet.next()); + assertEquals(-1.7976931348623157E308, resultSet.getDouble(1), 0.0001); + } + + @Test + void test_getBigDecimal() throws Exception { + stmt.executeUpdate("CREATE TABLE test_bigdecimal (bigdecimal_col REAL);"); + stmt.executeUpdate("INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (12345.67);"); + stmt.executeUpdate( + "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (1.7976931348623157E308);"); // Max + // double + // value + stmt.executeUpdate( + "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (4.9E-324);"); // Min positive double + // value + stmt.executeUpdate( + "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (-12345.67);"); // Negative value + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_bigdecimal"); + + // Test typical BigDecimal value + assertEquals( + new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), + resultSet.getBigDecimal(1, 2)); + + // Test maximum double value + assertTrue(resultSet.next()); + assertEquals( + new BigDecimal("1.7976931348623157E308").setScale(10, RoundingMode.HALF_UP), + resultSet.getBigDecimal(1, 10)); + + // Test minimum positive double value + assertTrue(resultSet.next()); + assertEquals( + new BigDecimal("4.9E-324").setScale(10, RoundingMode.HALF_UP), + resultSet.getBigDecimal(1, 10)); + + // Test negative BigDecimal value + assertTrue(resultSet.next()); + assertEquals( + new BigDecimal("-12345.67").setScale(2, RoundingMode.HALF_UP), + resultSet.getBigDecimal(1, 2)); + } + + @ParameterizedTest + @MethodSource("byteArrayProvider") + void test_getBytes(byte[] data) throws Exception { + stmt.executeUpdate("CREATE TABLE test_bytes (bytes_col BLOB);"); + executeDMLAndAssert(data); + } + + private static Stream byteArrayProvider() { + return Stream.of( + "Hello".getBytes(), "world".getBytes(), new byte[0], new byte[] {0x00, (byte) 0xFF}); + } + + private void executeDMLAndAssert(byte[] data) throws SQLException { + // Convert byte array to hexadecimal string + StringBuilder hexString = new StringBuilder(); + for (byte b : data) { + hexString.append(String.format("%02X", b)); + } + // Execute DML statement + stmt.executeUpdate("INSERT INTO test_bytes (bytes_col) VALUES (X'" + hexString + "');"); + + // Assert the inserted data + ResultSet resultSet = stmt.executeQuery("SELECT bytes_col FROM test_bytes"); + assertArrayEquals(data, resultSet.getBytes(1)); + } } From 041c8fbddcb5d503284d95abe9aacd70e2032be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Wed, 29 Jan 2025 11:53:44 +0900 Subject: [PATCH 2/5] Fix `executeQuery` to not run statement.execute() --- .../tursodatabase/jdbc4/JDBC4Statement.java | 13 +++++++++++- .../jdbc4/JDBC4ResultSetTest.java | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java index 7cbf6f69d..af3652d7e 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java @@ -53,9 +53,20 @@ public class JDBC4Statement implements Statement { this.resultSetHoldability = resultSetHoldability; } + // TODO: should executeQuery run execute right after preparing the statement? @Override public ResultSet executeQuery(String sql) throws SQLException { - execute(sql); + ensureOpen(); + statement = this.withConnectionTimeout( + () -> { + try { + // TODO: if sql is a readOnly query, do we still need the locks? + connectionLock.lock(); + return connection.prepare(sql); + } finally { + connectionLock.unlock(); + } + }); requireNonNull(statement, "statement should not be null after running execute method"); return new JDBC4ResultSet(statement.getResultSet()); diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index ddd447b9d..60d4c5d62 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -3,6 +3,7 @@ package org.github.tursodatabase.jdbc4; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -94,9 +95,20 @@ class JDBC4ResultSetTest { stmt.executeUpdate("INSERT INTO test_string (string_col) VALUES ('test');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_string"); + assertTrue(resultSet.next()); assertEquals("test", resultSet.getString(1)); } + @Test + void test_getString_returnsNull() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (string_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_null (string_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertNull(resultSet.getString(1)); + } + @Test void test_getBoolean_true() throws Exception { stmt.executeUpdate("CREATE TABLE test_boolean (boolean_col INTEGER);"); @@ -105,6 +117,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); + assertTrue(resultSet.next()); assertTrue(resultSet.getBoolean(1)); resultSet.next(); @@ -117,6 +130,7 @@ class JDBC4ResultSetTest { stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (0);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); + assertTrue(resultSet.next()); assertFalse(resultSet.getBoolean(1)); } @@ -130,6 +144,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_byte"); // Test value that fits within byte size + assertTrue(resultSet.next()); assertEquals(1, resultSet.getByte(1)); // Test value that exceeds byte size (positive overflow) @@ -151,6 +166,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_short"); // Test typical short value + assertTrue(resultSet.next()); assertEquals(123, resultSet.getShort(1)); // Test maximum short value @@ -172,6 +188,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_int"); // Test typical int value + assertTrue(resultSet.next()); assertEquals(12345, resultSet.getInt(1)); // Test maximum int value @@ -223,6 +240,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_float"); // Test typical float value + assertTrue(resultSet.next()); assertEquals(1.23f, resultSet.getFloat(1), 0.0001); // Test maximum float value @@ -254,6 +272,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_double"); // Test typical double value + assertTrue(resultSet.next()); assertEquals(1.234567, resultSet.getDouble(1), 0.0001); // Test maximum double value @@ -286,6 +305,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_bigdecimal"); // Test typical BigDecimal value + assertTrue(resultSet.next()); assertEquals( new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 2)); @@ -332,6 +352,7 @@ class JDBC4ResultSetTest { // Assert the inserted data ResultSet resultSet = stmt.executeQuery("SELECT bytes_col FROM test_bytes"); + assertTrue(resultSet.next()); assertArrayEquals(data, resultSet.getBytes(1)); } } From 3649e8f67c139ca981900fd5e8f5586350c174f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Wed, 29 Jan 2025 11:56:49 +0900 Subject: [PATCH 3/5] Add test to verify behavior on null columns --- .../jdbc4/JDBC4ResultSetTest.java | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index 60d4c5d62..536ed9964 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -100,7 +100,7 @@ class JDBC4ResultSetTest { } @Test - void test_getString_returnsNull() throws Exception { + void test_getString_returns_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (string_col TEXT);"); stmt.executeUpdate("INSERT INTO test_null (string_col) VALUES (NULL);"); @@ -134,6 +134,16 @@ class JDBC4ResultSetTest { assertFalse(resultSet.getBoolean(1)); } + @Test + void test_getBoolean_returns_false_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (boolean_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_null (boolean_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertFalse(resultSet.getBoolean(1)); + } + @Test void test_getByte() throws Exception { stmt.executeUpdate("CREATE TABLE test_byte (byte_col INTEGER);"); @@ -156,6 +166,16 @@ class JDBC4ResultSetTest { assertEquals(127, resultSet.getByte(1)); // -129 overflows to 127 } + @Test + void test_getByte_returns_zero_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (byte_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_null (byte_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0, resultSet.getByte(1)); + } + @Test void test_getShort() throws Exception { stmt.executeUpdate("CREATE TABLE test_short (short_col SMALLINT);"); @@ -178,6 +198,16 @@ class JDBC4ResultSetTest { assertEquals(-32768, resultSet.getShort(1)); } + @Test + void test_getShort_returns_zero_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (short_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_null (short_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0, resultSet.getShort(1)); + } + @Test void test_getInt() throws Exception { stmt.executeUpdate("CREATE TABLE test_int (int_col INT);"); @@ -200,6 +230,16 @@ class JDBC4ResultSetTest { assertEquals(-2147483648, resultSet.getInt(1)); } + @Test + void test_getInt_returns_zero_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (int_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_null (int_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0, resultSet.getInt(1)); + } + @Test @Disabled("limbo has a bug which sees -9223372036854775808 as double") void test_getLong() throws Exception { @@ -226,6 +266,16 @@ class JDBC4ResultSetTest { assertEquals(-9223372036854775808L, resultSet.getLong(1)); } + @Test + void test_getLong_returns_zero_no_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (long_col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_null (long_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0L, resultSet.getLong(1)); + } + @Test void test_getFloat() throws Exception { stmt.executeUpdate("CREATE TABLE test_float (float_col REAL);"); @@ -256,6 +306,16 @@ class JDBC4ResultSetTest { assertEquals(-3.4028235E38f, resultSet.getFloat(1), 0.0001); } + @Test + void test_getFloat_returns_zero_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (float_col REAL);"); + stmt.executeUpdate("INSERT INTO test_null (float_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0.0f, resultSet.getFloat(1), 0.0001); + } + @Test void test_getDouble() throws Exception { stmt.executeUpdate("CREATE TABLE test_double (double_col DOUBLE);"); @@ -288,6 +348,16 @@ class JDBC4ResultSetTest { assertEquals(-1.7976931348623157E308, resultSet.getDouble(1), 0.0001); } + @Test + void test_getDouble_returns_zero_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (double_col REAL);"); + stmt.executeUpdate("INSERT INTO test_null (double_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertEquals(0.0, resultSet.getDouble(1), 0.0001); + } + @Test void test_getBigDecimal() throws Exception { stmt.executeUpdate("CREATE TABLE test_bigdecimal (bigdecimal_col REAL);"); @@ -329,6 +399,16 @@ class JDBC4ResultSetTest { resultSet.getBigDecimal(1, 2)); } + @Test + void test_getBigDecimal_returns_null_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (bigdecimal_col REAL);"); + stmt.executeUpdate("INSERT INTO test_null (bigdecimal_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertNull(resultSet.getBigDecimal(1, 2)); + } + @ParameterizedTest @MethodSource("byteArrayProvider") void test_getBytes(byte[] data) throws Exception { @@ -355,4 +435,14 @@ class JDBC4ResultSetTest { assertTrue(resultSet.next()); assertArrayEquals(data, resultSet.getBytes(1)); } + + @Test + void test_getBytes_returns_null_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null (bytes_col BLOB);"); + stmt.executeUpdate("INSERT INTO test_null (bytes_col) VALUES (NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); + assertTrue(resultSet.next()); + assertNull(resultSet.getBytes(1)); + } } From 7fe5ba84c734d8098b630d4b0e39c4155d8fc510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Wed, 29 Jan 2025 11:59:42 +0900 Subject: [PATCH 4/5] Fix test that uses wrong data types --- .../org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index 536ed9964..285ab1506 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -178,7 +178,7 @@ class JDBC4ResultSetTest { @Test void test_getShort() throws Exception { - stmt.executeUpdate("CREATE TABLE test_short (short_col SMALLINT);"); + stmt.executeUpdate("CREATE TABLE test_short (short_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (123);"); stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (32767);"); // Max short value stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (-32768);"); // Min short value @@ -243,8 +243,6 @@ class JDBC4ResultSetTest { @Test @Disabled("limbo has a bug which sees -9223372036854775808 as double") void test_getLong() throws Exception { - Long l1 = Long.MIN_VALUE; - Long l2 = Long.MAX_VALUE; stmt.executeUpdate("CREATE TABLE test_long (long_col BIGINT);"); stmt.executeUpdate("INSERT INTO test_long (long_col) VALUES (1234567890);"); stmt.executeUpdate( @@ -318,7 +316,7 @@ class JDBC4ResultSetTest { @Test void test_getDouble() throws Exception { - stmt.executeUpdate("CREATE TABLE test_double (double_col DOUBLE);"); + stmt.executeUpdate("CREATE TABLE test_double (double_col REAL);"); stmt.executeUpdate("INSERT INTO test_double (double_col) VALUES (1.234567);"); stmt.executeUpdate( "INSERT INTO test_double (double_col) VALUES (1.7976931348623157E308);"); // Max double From 9fd2c67b4db95b3b825dec5a6932ebe3160b8b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Wed, 29 Jan 2025 12:04:07 +0900 Subject: [PATCH 5/5] Add tests for out of bound access to arrays --- .../tursodatabase/core/LimboResultSet.java | 2 +- .../tursodatabase/jdbc4/JDBC4Statement.java | 3 +- .../jdbc4/JDBC4ResultSetTest.java | 70 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java index 6fe1ef1a3..1884d4786 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java @@ -129,7 +129,7 @@ public class LimboResultSet { } final Object[] resultSet = this.lastStepResult.getResult(); - if (columnIndex > resultSet.length) { + if (columnIndex > resultSet.length || columnIndex < 0) { throw new SQLException("columnIndex out of bound"); } diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java index af3652d7e..3965e3cae 100644 --- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java +++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Statement.java @@ -57,7 +57,8 @@ public class JDBC4Statement implements Statement { @Override public ResultSet executeQuery(String sql) throws SQLException { ensureOpen(); - statement = this.withConnectionTimeout( + statement = + this.withConnectionTimeout( () -> { try { // TODO: if sql is a readOnly query, do we still need the locks? diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index 285ab1506..ec79ce1eb 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -443,4 +443,74 @@ class JDBC4ResultSetTest { assertTrue(resultSet.next()); assertNull(resultSet.getBytes(1)); } + + @Test + void test_getXXX_methods_on_multiple_columns() throws Exception { + stmt.executeUpdate( + "CREATE TABLE test_integration (" + + "string_col TEXT, " + + "boolean_col INTEGER, " + + "byte_col INTEGER, " + + "short_col INTEGER, " + + "int_col INTEGER, " + + "long_col BIGINT, " + + "float_col REAL, " + + "double_col REAL, " + + "bigdecimal_col REAL, " + + "bytes_col BLOB);"); + + stmt.executeUpdate( + "INSERT INTO test_integration VALUES (" + + "'test', " + + "1, " + + "1, " + + "123, " + + "12345, " + + "1234567890, " + + "1.23, " + + "1.234567, " + + "12345.67, " + + "X'48656C6C6F');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_integration"); + assertTrue(resultSet.next()); + + // Verify each column + assertEquals("test", resultSet.getString(1)); + assertTrue(resultSet.getBoolean(2)); + assertEquals(1, resultSet.getByte(3)); + assertEquals(123, resultSet.getShort(4)); + assertEquals(12345, resultSet.getInt(5)); + assertEquals(1234567890L, resultSet.getLong(6)); + assertEquals(1.23f, resultSet.getFloat(7), 0.0001); + assertEquals(1.234567, resultSet.getDouble(8), 0.0001); + assertEquals( + new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), + resultSet.getBigDecimal(9, 2)); + assertArrayEquals("Hello".getBytes(), resultSet.getBytes(10)); + } + + @Test + void test_invalidColumnIndex_outOfBounds() throws Exception { + stmt.executeUpdate("CREATE TABLE test_invalid (col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_invalid (col) VALUES (1);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_invalid"); + assertTrue(resultSet.next()); + + // Test out-of-bounds column index + assertThrows(SQLException.class, () -> resultSet.getInt(2)); + } + + @Test + void test_invalidColumnIndex_negative() throws Exception { + stmt.executeUpdate("CREATE TABLE test_invalid (col INTEGER);"); + stmt.executeUpdate("INSERT INTO test_invalid (col) VALUES (1);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_invalid"); + assertTrue(resultSet.next()); + + // Test negative column index + assertThrows(SQLException.class, () -> resultSet.getInt(-1)); + } }