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] 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)); + } }