Merge 'Implement wasNull tracking in ResultSet getter methods' from 김민석

## Summary
Implemented comprehensive wasNull tracking and refactored getter methods
in JDBC4ResultSet to ensure JDBC specification compliance and improve
code maintainability.
### Changes
Added wasNull tracking to all getter methods: Covers primitive types,
objects, dates/times, streams, and BigDecimal
Refactored columnLabel getters to use delegation pattern: Eliminates
code duplication and ensures consistent wasNull behavior
### Bug Fixes & Code Quality
- Fixed getString(String) to return null instead of empty string for
null values
- Added @Nullable annotation to getBytes(String) to fix NullAway error
- Preserved String parsing in getDate(String) for TEXT-formatted dates
- Extracted timezone offset calculation to helper method
### Testing
Added comprehensive tests for wasNull tracking, columnLabel getters,
stream methods, and null handling

Closes #3838
This commit is contained in:
Pekka Enberg
2025-10-29 18:10:42 +02:00
committed by GitHub
2 changed files with 370 additions and 52 deletions

View File

@@ -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.
*

View File

@@ -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());
}
}