Merge 'bindings/java: Implement methods in JDBC4ResultSet' from Kim Seon Woo

## Purpose of this PR
- Implement `JDBC4ResultSet`'s `getXXX(columnIndex)` methods
## Changes
- `JDBC4ResultSet`'s `getXXX(columnIndex)` methods are implemented
- Add some docs for clarification(+ leaving history)
## Reference
- [Issue](https://github.com/tursodatabase/limbo/issues/615)

Closes #813
This commit is contained in:
Pekka Enberg
2025-01-29 09:45:26 +02:00
6 changed files with 567 additions and 28 deletions

View File

@@ -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 || columnIndex < 0) {
throw new SQLException("columnIndex out of bound");
}
return resultSet[columnIndex - 1];
}
@Override
public String toString() {
return "LimboResultSet{"

View File

@@ -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;

View File

@@ -47,6 +47,11 @@ public class LimboStepResult {
|| stepResultId == STEP_RESULT_ID_ERROR;
}
@Nullable
public Object[] getResult() {
return result;
}
@Override
public String toString() {
return "LimboStepResult{"

View File

@@ -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> {
T get() throws Exception;
}
private <T> T wrapTypeConversion(ResultSetSupplier<T> supplier) throws SQLException {
try {
return supplier.get();
} catch (Exception e) {
throw new SQLException("Type conversion failed: " + e);
}
}
}

View File

@@ -53,9 +53,21 @@ 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());

View File

@@ -1,16 +1,25 @@
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;
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 +88,429 @@ 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");
assertTrue(resultSet.next());
assertEquals("test", resultSet.getString(1));
}
@Test
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);");
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);");
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.next());
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");
assertTrue(resultSet.next());
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);");
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
assertTrue(resultSet.next());
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_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 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
ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_short");
// Test typical short value
assertTrue(resultSet.next());
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_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);");
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
assertTrue(resultSet.next());
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
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 {
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_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);");
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
assertTrue(resultSet.next());
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_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 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
// 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
assertTrue(resultSet.next());
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_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);");
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
assertTrue(resultSet.next());
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));
}
@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 {
stmt.executeUpdate("CREATE TABLE test_bytes (bytes_col BLOB);");
executeDMLAndAssert(data);
}
private static Stream<byte[]> 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");
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));
}
@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));
}
}