diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java index 575f97636..4070f1445 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java @@ -1,5 +1,6 @@ package tech.turso.jdbc4; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; @@ -32,6 +33,7 @@ import tech.turso.core.TursoResultSet; public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { private final TursoResultSet resultSet; + private boolean wasNull = false; /** * Creates a new JDBC4ResultSet. @@ -54,13 +56,14 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public boolean wasNull() throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return wasNull; } @Override @Nullable public String getString(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -70,6 +73,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public boolean getBoolean(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return false; } @@ -79,6 +83,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public byte getByte(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -88,6 +93,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public short getShort(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -97,6 +103,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public int getInt(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -106,6 +113,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public long getLong(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -115,6 +123,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public float getFloat(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -124,6 +133,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public double getDouble(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return 0; } @@ -135,6 +145,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Nullable public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -147,6 +158,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Nullable public byte[] getBytes(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -157,6 +169,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Nullable public Date getDate(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -177,6 +190,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @SkipNullableCheck public Time getTime(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -197,6 +211,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @SkipNullableCheck public Timestamp getTimestamp(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -216,81 +231,116 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override @SkipNullableCheck public InputStream getAsciiStream(int columnIndex) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + final Object result = resultSet.get(columnIndex); + wasNull = result == null; + if (result == null) { + return null; + } + return wrapTypeConversion( + () -> { + if (result instanceof String) { + return new ByteArrayInputStream(((String) result).getBytes("US-ASCII")); + } else if (result instanceof byte[]) { + return new ByteArrayInputStream((byte[]) result); + } + throw new SQLException("Cannot convert to ASCII stream: " + result.getClass()); + }); } @Override @SkipNullableCheck public InputStream getUnicodeStream(int columnIndex) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + final Object result = resultSet.get(columnIndex); + wasNull = result == null; + if (result == null) { + return null; + } + return wrapTypeConversion( + () -> { + if (result instanceof String) { + return new ByteArrayInputStream(((String) result).getBytes("UTF-8")); + } else if (result instanceof byte[]) { + return new ByteArrayInputStream((byte[]) result); + } + throw new SQLException("Cannot convert to Unicode stream: " + result.getClass()); + }); } @Override @SkipNullableCheck public InputStream getBinaryStream(int columnIndex) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + final Object result = resultSet.get(columnIndex); + wasNull = result == null; + if (result == null) { + return null; + } + return wrapTypeConversion( + () -> { + if (result instanceof byte[]) { + return new ByteArrayInputStream((byte[]) result); + } + throw new SQLException("Cannot convert to binary stream: " + result.getClass()); + }); } @Override + @Nullable public String getString(String columnLabel) throws SQLException { - final Object result = this.resultSet.get(columnLabel); - if (result == null) { - return ""; - } - - return wrapTypeConversion(() -> (String) result); + return getString(findColumn(columnLabel)); } @Override public boolean getBoolean(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getBoolean(findColumn(columnLabel)); } @Override public byte getByte(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getByte(findColumn(columnLabel)); } @Override public short getShort(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getShort(findColumn(columnLabel)); } @Override public int getInt(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getInt(findColumn(columnLabel)); } @Override public long getLong(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getLong(findColumn(columnLabel)); } @Override public float getFloat(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getFloat(findColumn(columnLabel)); } @Override public double getDouble(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getDouble(findColumn(columnLabel)); } @Override @SkipNullableCheck public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getBigDecimal(findColumn(columnLabel), scale); } @Override + @Nullable public byte[] getBytes(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getBytes(findColumn(columnLabel)); } @Override @Nullable public Date getDate(String columnLabel) throws SQLException { final Object result = resultSet.get(columnLabel); + wasNull = result == null; if (result == null) { return null; } @@ -314,7 +364,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override @SkipNullableCheck public Time getTime(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getTime(findColumn(columnLabel)); } @Override @@ -326,19 +376,19 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override @SkipNullableCheck public InputStream getAsciiStream(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getAsciiStream(findColumn(columnLabel)); } @Override @SkipNullableCheck public InputStream getUnicodeStream(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getUnicodeStream(findColumn(columnLabel)); } @Override @SkipNullableCheck public InputStream getBinaryStream(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getBinaryStream(findColumn(columnLabel)); } @Override @@ -364,13 +414,15 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Override public Object getObject(int columnIndex) throws SQLException { - return resultSet.get(columnIndex); + final Object result = resultSet.get(columnIndex); + wasNull = result == null; + return result; } @Override @SkipNullableCheck public Object getObject(String columnLabel) throws SQLException { - throw new UnsupportedOperationException("not implemented"); + return getObject(findColumn(columnLabel)); } @Override @@ -392,6 +444,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @SkipNullableCheck public Reader getCharacterStream(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -408,6 +461,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { @Nullable public BigDecimal getBigDecimal(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); + wasNull = result == null; if (result == null) { return null; } @@ -830,15 +884,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { if (date == null || cal == null) { return date; } - - final Calendar localCal = Calendar.getInstance(); - localCal.setTime(date); - - final long offset = - cal.getTimeZone().getOffset(date.getTime()) - - localCal.getTimeZone().getOffset(date.getTime()); - - return new Date(date.getTime() + offset); + return new Date(date.getTime() + calculateTimezoneOffset(date.getTime(), cal)); } @Override @@ -854,15 +900,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { if (time == null || cal == null) { return time; } - - final Calendar localCal = Calendar.getInstance(); - localCal.setTime(time); - - final long offset = - cal.getTimeZone().getOffset(time.getTime()) - - localCal.getTimeZone().getOffset(time.getTime()); - - return new Time(time.getTime() + offset); + return new Time(time.getTime() + calculateTimezoneOffset(time.getTime(), cal)); } @Override @@ -878,15 +916,7 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { if (timestamp == null || cal == null) { return timestamp; } - - final Calendar localCal = Calendar.getInstance(); - localCal.setTime(timestamp); - - final long offset = - cal.getTimeZone().getOffset(timestamp.getTime()) - - localCal.getTimeZone().getOffset(timestamp.getTime()); - - return new Timestamp(timestamp.getTime() + offset); + return new Timestamp(timestamp.getTime() + calculateTimezoneOffset(timestamp.getTime(), cal)); } @Override @@ -1334,6 +1364,12 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { throw new UnsupportedOperationException("not implemented"); } + private long calculateTimezoneOffset(long timeMillis, Calendar targetCal) { + Calendar localCal = Calendar.getInstance(); + return targetCal.getTimeZone().getOffset(timeMillis) + - localCal.getTimeZone().getOffset(timeMillis); + } + /** * Functional interface for result set value suppliers. * diff --git a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ResultSetTest.java index 45fc0f1ee..25d993064 100644 --- a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ResultSetTest.java @@ -2,6 +2,7 @@ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.*; +import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; import java.math.RoundingMode; @@ -869,4 +870,285 @@ class JDBC4ResultSetTest { assertNotNull(timestamp); } + + @Test + void test_wasNull() throws Exception { + stmt.executeUpdate("CREATE TABLE test_was_null (id INTEGER, name TEXT);"); + stmt.executeUpdate("INSERT INTO test_was_null VALUES (1, 'test');"); + stmt.executeUpdate("INSERT INTO test_was_null VALUES (NULL, NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_was_null"); + + // First row - non-null values + assertTrue(resultSet.next()); + int id = resultSet.getInt(1); + assertFalse(resultSet.wasNull()); + String name = resultSet.getString(2); + assertFalse(resultSet.wasNull()); + + // Second row - null values + assertTrue(resultSet.next()); + int nullInt = resultSet.getInt(1); + assertTrue(resultSet.wasNull()); + assertEquals(0, nullInt); + String nullString = resultSet.getString(2); + assertTrue(resultSet.wasNull()); + assertNull(nullString); + } + + @Test + void test_columnLabel_getters() throws Exception { + stmt.executeUpdate( + "CREATE TABLE test_column_label (" + + "bool_col INTEGER, " + + "byte_col INTEGER, " + + "short_col INTEGER, " + + "int_col INTEGER, " + + "long_col BIGINT, " + + "float_col REAL, " + + "double_col REAL, " + + "bytes_col BLOB);"); + + stmt.executeUpdate( + "INSERT INTO test_column_label VALUES (" + + "1, 127, 32767, 2147483647, 9223372036854775807, 3.14, 2.718281828, X'48656C6C6F');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_column_label"); + assertTrue(resultSet.next()); + + // Test columnLabel-based getters + assertTrue(resultSet.getBoolean("bool_col")); + assertEquals(127, resultSet.getByte("byte_col")); + assertEquals(32767, resultSet.getShort("short_col")); + assertEquals(2147483647, resultSet.getInt("int_col")); + assertEquals(9223372036854775807L, resultSet.getLong("long_col")); + assertEquals(3.14f, resultSet.getFloat("float_col"), 0.001); + assertEquals(2.718281828, resultSet.getDouble("double_col"), 0.000001); + assertArrayEquals("Hello".getBytes(), resultSet.getBytes("bytes_col")); + } + + @Test + void test_getObject_with_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_object (id INTEGER, name TEXT);"); + stmt.executeUpdate("INSERT INTO test_object VALUES (42, 'test');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_object"); + assertTrue(resultSet.next()); + + Object idObj = resultSet.getObject("id"); + assertEquals(42L, idObj); + assertFalse(resultSet.wasNull()); + + Object nameObj = resultSet.getObject("name"); + assertEquals("test", nameObj); + assertFalse(resultSet.wasNull()); + } + + @Test + void test_getBigDecimal_with_scale_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_decimal (amount REAL);"); + stmt.executeUpdate("INSERT INTO test_decimal VALUES (123.456789);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_decimal"); + assertTrue(resultSet.next()); + + BigDecimal result = resultSet.getBigDecimal("amount", 2); + assertEquals(new BigDecimal("123.46"), result); // Should be rounded to 2 decimal places + } + + @Test + void test_getTime_with_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_time (time_col BLOB);"); + + long timeMillis = System.currentTimeMillis(); + byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(timeMillis).array(); + StringBuilder hexString = new StringBuilder(); + for (byte b : timeBytes) { + hexString.append(String.format("%02X", b)); + } + stmt.executeUpdate("INSERT INTO test_time VALUES (X'" + hexString + "');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_time"); + assertTrue(resultSet.next()); + + Time time = resultSet.getTime("time_col"); + assertNotNull(time); + assertEquals(timeMillis, time.getTime()); + } + + @Test + void test_getAsciiStream() throws Exception { + stmt.executeUpdate("CREATE TABLE test_ascii (text_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_ascii VALUES ('Hello ASCII');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_ascii"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getAsciiStream(1); + assertNotNull(stream); + byte[] buffer = new byte[11]; + int bytesRead = stream.read(buffer); + assertEquals(11, bytesRead); + assertEquals("Hello ASCII", new String(buffer, "US-ASCII")); + assertFalse(resultSet.wasNull()); + } + + @Test + void test_getAsciiStream_with_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_ascii (text_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_ascii VALUES ('Test');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_ascii"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getAsciiStream("text_col"); + assertNotNull(stream); + byte[] buffer = new byte[4]; + stream.read(buffer); + assertEquals("Test", new String(buffer, "US-ASCII")); + } + + @Test + void test_getBinaryStream() throws Exception { + stmt.executeUpdate("CREATE TABLE test_binary (binary_col BLOB);"); + byte[] data = {0x01, 0x02, 0x03, 0x04, 0x05}; + + StringBuilder hexString = new StringBuilder(); + for (byte b : data) { + hexString.append(String.format("%02X", b)); + } + stmt.executeUpdate("INSERT INTO test_binary VALUES (X'" + hexString + "');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_binary"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getBinaryStream(1); + assertNotNull(stream); + byte[] buffer = new byte[5]; + int bytesRead = stream.read(buffer); + assertEquals(5, bytesRead); + assertArrayEquals(data, buffer); + assertFalse(resultSet.wasNull()); + } + + @Test + void test_getBinaryStream_with_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_binary (data BLOB);"); + byte[] data = {0x0A, 0x0B, 0x0C}; + + StringBuilder hexString = new StringBuilder(); + for (byte b : data) { + hexString.append(String.format("%02X", b)); + } + stmt.executeUpdate("INSERT INTO test_binary VALUES (X'" + hexString + "');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_binary"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getBinaryStream("data"); + assertNotNull(stream); + byte[] buffer = new byte[3]; + stream.read(buffer); + assertArrayEquals(data, buffer); + } + + @Test + void test_getUnicodeStream() throws Exception { + stmt.executeUpdate("CREATE TABLE test_unicode (text_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_unicode VALUES ('Hello minseok');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_unicode"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getUnicodeStream(1); + assertNotNull(stream); + byte[] buffer = new byte[1024]; + int bytesRead = stream.read(buffer); + String result = new String(buffer, 0, bytesRead, "UTF-8"); + assertEquals("Hello minseok", result); + assertFalse(resultSet.wasNull()); + } + + @Test + void test_getUnicodeStream_with_columnLabel() throws Exception { + stmt.executeUpdate("CREATE TABLE test_unicode (text_col TEXT);"); + stmt.executeUpdate("INSERT INTO test_unicode VALUES ('Unicode 테스트');"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_unicode"); + assertTrue(resultSet.next()); + + InputStream stream = resultSet.getUnicodeStream("text_col"); + assertNotNull(stream); + byte[] buffer = new byte[1024]; + int bytesRead = stream.read(buffer); + String result = new String(buffer, 0, bytesRead, "UTF-8"); + assertEquals("Unicode 테스트", result); + } + + @Test + void test_stream_methods_return_null_on_null() throws Exception { + stmt.executeUpdate("CREATE TABLE test_null_stream (text_col TEXT, binary_col BLOB);"); + stmt.executeUpdate("INSERT INTO test_null_stream VALUES (NULL, NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null_stream"); + assertTrue(resultSet.next()); + + assertNull(resultSet.getAsciiStream(1)); + assertTrue(resultSet.wasNull()); + + assertNull(resultSet.getUnicodeStream(1)); + assertTrue(resultSet.wasNull()); + + assertNull(resultSet.getBinaryStream(2)); + assertTrue(resultSet.wasNull()); + } + + @Test + void test_getMetaData_column_count() throws Exception { + stmt.executeUpdate("CREATE TABLE test_meta (col1 INTEGER, col2 TEXT, col3 REAL);"); + stmt.executeUpdate("INSERT INTO test_meta VALUES (1, 'test', 3.14);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_meta"); + ResultSetMetaData metaData = resultSet.getMetaData(); + + assertEquals(3, metaData.getColumnCount()); + assertEquals("col1", metaData.getColumnName(1)); + assertEquals("col2", metaData.getColumnName(2)); + assertEquals("col3", metaData.getColumnName(3)); + assertEquals("col1", metaData.getColumnLabel(1)); + assertEquals(Integer.MAX_VALUE, metaData.getColumnDisplaySize(1)); + } + + @Test + void test_wasNull_consistency_across_types() throws Exception { + stmt.executeUpdate( + "CREATE TABLE test_null_types (" + + "int_col INTEGER, " + + "text_col TEXT, " + + "real_col REAL, " + + "blob_col BLOB);"); + stmt.executeUpdate("INSERT INTO test_null_types VALUES (NULL, NULL, NULL, NULL);"); + + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null_types"); + assertTrue(resultSet.next()); + + // Test wasNull for various getter methods + resultSet.getInt(1); + assertTrue(resultSet.wasNull()); + + resultSet.getString(2); + assertTrue(resultSet.wasNull()); + + resultSet.getDouble(3); + assertTrue(resultSet.wasNull()); + + resultSet.getBytes(4); + assertTrue(resultSet.wasNull()); + + resultSet.getObject(1); + assertTrue(resultSet.wasNull()); + + resultSet.getBigDecimal(3); + assertTrue(resultSet.wasNull()); + } }