Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ public byte getByte(int columnIndex) throws SQLException {
case ENUM:
return isNull ? (byte) 0 : checkedCastToByte(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? (byte) 0 : checkedCastToByte(spanner.getBigDecimal(spannerIndex));
return isNull
? (byte) 0
: checkedCastToByte(spanner.getBigDecimal(spannerIndex).toBigInteger());
case PG_NUMERIC:
return isNull
? (byte) 0
Expand Down Expand Up @@ -354,7 +356,9 @@ public short getShort(int columnIndex) throws SQLException {
}
return isNull ? 0 : checkedCastToShort(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? 0 : checkedCastToShort(spanner.getBigDecimal(spannerIndex));
return isNull
? (short) 0
: checkedCastToShort(spanner.getBigDecimal(spannerIndex).toBigInteger());
case PG_NUMERIC:
return isNull
? 0
Expand Down Expand Up @@ -395,7 +399,7 @@ public int getInt(int columnIndex) throws SQLException {
case ENUM:
return isNull ? 0 : checkedCastToInt(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? 0 : checkedCastToInt(spanner.getBigDecimal(spannerIndex));
return isNull ? 0 : checkedCastToInt(spanner.getBigDecimal(spannerIndex).toBigInteger());
case PG_NUMERIC:
return isNull
? 0
Expand Down Expand Up @@ -432,7 +436,7 @@ public long getLong(int columnIndex) throws SQLException {
case ENUM:
return isNull ? 0L : spanner.getLong(spannerIndex);
case NUMERIC:
return isNull ? 0 : checkedCastToLong(parseBigDecimal(spanner.getString(spannerIndex)));
return isNull ? 0L : checkedCastToLong(spanner.getBigDecimal(spannerIndex).toBigInteger());
case PG_NUMERIC:
return isNull
? 0L
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,46 +116,65 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
}
if (type.getCode() == Code.FLOAT64) return (Double) value != 0d;
if (type.getCode() == Code.NUMERIC) return !value.equals(BigDecimal.ZERO);
if (type.getCode() == Code.PG_NUMERIC)
return !AbstractJdbcWrapper.parseBigDecimal((String) value).equals(BigDecimal.ZERO);
}
if (targetType.equals(BigDecimal.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? BigDecimal.ONE : BigDecimal.ZERO;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM)
return BigDecimal.valueOf((Long) value);
if (type.getCode() == Code.NUMERIC) return value;
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.parseBigDecimal((String) value);
}
if (targetType.equals(Long.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? 1L : 0L;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM) return value;
if (type.getCode() == Code.NUMERIC)
return AbstractJdbcWrapper.checkedCastToLong((BigDecimal) value);
return AbstractJdbcWrapper.checkedCastToLong(((BigDecimal) value).toBigInteger());
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.checkedCastToLong(
AbstractJdbcWrapper.parseBigDecimal((String) value).toBigInteger());
}
if (targetType.equals(Integer.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM)
return AbstractJdbcWrapper.checkedCastToInt((Long) value);
if (type.getCode() == Code.NUMERIC)
return AbstractJdbcWrapper.checkedCastToInt((BigDecimal) value);
return AbstractJdbcWrapper.checkedCastToInt(((BigDecimal) value).toBigInteger());
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.checkedCastToInt(
AbstractJdbcWrapper.parseBigDecimal((String) value).toBigInteger());
}
if (targetType.equals(Short.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM)
return AbstractJdbcWrapper.checkedCastToShort((Long) value);
if (type.getCode() == Code.NUMERIC)
return AbstractJdbcWrapper.checkedCastToShort((BigDecimal) value);
return AbstractJdbcWrapper.checkedCastToShort(((BigDecimal) value).toBigInteger());
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.checkedCastToShort(
AbstractJdbcWrapper.parseBigDecimal((String) value).toBigInteger());
}
if (targetType.equals(Byte.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM)
return AbstractJdbcWrapper.checkedCastToByte((Long) value);
if (type.getCode() == Code.NUMERIC)
return AbstractJdbcWrapper.checkedCastToByte((BigDecimal) value);
return AbstractJdbcWrapper.checkedCastToByte(((BigDecimal) value).toBigInteger());
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.checkedCastToByte(
AbstractJdbcWrapper.parseBigDecimal((String) value).toBigInteger());
}
if (targetType.equals(BigInteger.class)) {
if (type.getCode() == Code.BOOL) return (Boolean) value ? BigInteger.ONE : BigInteger.ZERO;
if (type.getCode() == Code.INT64 || type.getCode() == Code.ENUM)
return BigInteger.valueOf((Long) value);
if (type.getCode() == Code.NUMERIC)
return AbstractJdbcWrapper.checkedCastToBigInteger((BigDecimal) value);
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.checkedCastToBigInteger(
AbstractJdbcWrapper.parseBigDecimal((String) value));
}
if (targetType.equals(Float.class)) {
if (type.getCode() == Code.BOOL)
Expand All @@ -166,6 +185,8 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
if (type.getCode() == Code.FLOAT64)
return AbstractJdbcWrapper.checkedCastToFloat((Double) value);
if (type.getCode() == Code.NUMERIC) return ((BigDecimal) value).floatValue();
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.parseFloat((String) value);
}
if (targetType.equals(Double.class)) {
if (type.getCode() == Code.BOOL)
Expand All @@ -174,6 +195,8 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
return value;
}
if (type.getCode() == Code.NUMERIC) return ((BigDecimal) value).doubleValue();
if (type.getCode() == Code.PG_NUMERIC)
return AbstractJdbcWrapper.parseDouble((String) value);
}
if (targetType.equals(java.sql.Date.class)) {
if (type.getCode() == Code.DATE) return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public static void startEmulator() {
.withExposedPorts(9010)
.waitingFor(Wait.forListeningPorts(9010));
emulator.start();
try {
Thread.sleep(1500); // Give gRPC server time to fully initialize
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed? I don't remember having seen any issues with this in the past for this repository. But it might be that recent versions of the emulator are more sensitive to this. (Could we maybe make it a bit more dynamic than always sleeping for 1.5 seconds?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

properties = new Properties();
properties.setProperty("autoConfigEmulator", "true");
properties.setProperty(
Expand Down
117 changes: 112 additions & 5 deletions src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ public class JdbcResultSetTest {
private static final BigDecimal NUMERIC_VALUE = new BigDecimal("3.14");
private static final int NUMERIC_COLINDEX_NULL = 25;
private static final int NUMERIC_COLINDEX_NOTNULL = 26;
private static final String PG_NUMERIC_COL_NULL = "PG_NUMERIC_COL_NULL";
private static final String PG_NUMERIC_COL_NOT_NULL = "PG_NUMERIC_COL_NOT_NULL";
private static final String PG_NUMERIC_COL_NAN = "PG_NUMERIC_COL_NAN";
private static final int PG_NUMERIC_COLINDEX_NULL = 44;
private static final int PG_NUMERIC_COLINDEX_NOTNULL = 45;
private static final int PG_NUMERIC_COLINDEX_NAN = 46;
private static final String JSON_COL_NULL = "JSON_COL_NULL";
private static final String JSON_COL_NOT_NULL = "JSON_COL_NOT_NULL";
private static final int JSON_COLINDEX_NULL = 27;
Expand Down Expand Up @@ -237,7 +243,10 @@ static ResultSet getMockResultSet() {
Type.array(Type.proto(SingerInfo.getDescriptor().getFullName()))),
StructField.of(
PROTO_ENUM_ARRAY_COL,
Type.array(Type.protoEnum(Genre.getDescriptor().getFullName())))),
Type.array(Type.protoEnum(Genre.getDescriptor().getFullName()))),
StructField.of(PG_NUMERIC_COL_NULL, Type.pgNumeric()),
StructField.of(PG_NUMERIC_COL_NOT_NULL, Type.pgNumeric()),
StructField.of(PG_NUMERIC_COL_NAN, Type.pgNumeric())),
Collections.singletonList(
Struct.newBuilder()
.set(STRING_COL_NULL)
Expand Down Expand Up @@ -327,6 +336,12 @@ static ResultSet getMockResultSet() {
PROTO_MSG_ARRAY_VALUE, SingerInfo.getDescriptor().getFullName())
.set(PROTO_ENUM_ARRAY_COL)
.toProtoEnumArray(PROTO_ENUM_ARRAY_VALUE, Genre.getDescriptor().getFullName())
.set(PG_NUMERIC_COL_NULL)
.to(Value.pgNumeric((String) null))
.set(PG_NUMERIC_COL_NOT_NULL)
.to(Value.pgNumeric("3.14"))
.set(PG_NUMERIC_COL_NAN)
.to(Value.pgNumeric("NaN"))
.build()));
}

Expand Down Expand Up @@ -594,14 +609,106 @@ public void testGetLongIndexForFloat64() throws SQLException {
assertTrue(subject.wasNull());
}

@Test
public void testGetIntegerTypesOnNumeric() throws SQLException {
assertEquals((byte) 0, subject.getByte(NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals((byte) 3, subject.getByte(NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals((short) 0, subject.getShort(NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals((short) 3, subject.getShort(NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals(0, subject.getInt(NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals(3, subject.getInt(NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals(0L, subject.getLong(NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals(3L, subject.getLong(NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());
}

@Test
public void testGetIntegerTypesOnPgNumeric() throws SQLException {
assertEquals((byte) 0, subject.getByte(PG_NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals((byte) 3, subject.getByte(PG_NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals((short) 0, subject.getShort(PG_NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals((short) 3, subject.getShort(PG_NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals(0, subject.getInt(PG_NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals(3, subject.getInt(PG_NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());

assertEquals(0L, subject.getLong(PG_NUMERIC_COLINDEX_NULL));
assertTrue(subject.wasNull());
assertEquals(3L, subject.getLong(PG_NUMERIC_COLINDEX_NOTNULL));
assertFalse(subject.wasNull());
}

@Test
public void testGetIntegerTypesOnPgNumericNaN() throws SQLException {
assertTrue(Double.isNaN(subject.getDouble(PG_NUMERIC_COLINDEX_NAN)));
assertTrue(Float.isNaN(subject.getFloat(PG_NUMERIC_COLINDEX_NAN)));
assertEquals("NaN", subject.getString(PG_NUMERIC_COLINDEX_NAN));
try {
subject.getByte(PG_NUMERIC_COLINDEX_NAN);
fail("missing expected SQLException");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use assertThrows(...) for these assertions instead of this try-fail construct. I know that there are many of them in this project, but that is because the original JDBC driver was written when Java 7 was the minimum supported version, which did not support assertThrows(..) (as it did not support lambda expressions)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}

try {
subject.getShort(PG_NUMERIC_COLINDEX_NAN);
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}

try {
subject.getInt(PG_NUMERIC_COLINDEX_NAN);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So one interesting thing regarding NaN is that the following assertion actually passes:

assertEquals(0, (int) Float.NaN)

Do you know what the PostgreSQL JDBC driver returns in a case like this:

  1. Does it return zero (which would be consistent with how Java casts a NaN to int)?
  2. Or does it throw an error like here?

Copy link
Copy Markdown
Collaborator Author

@rayudu3745 rayudu3745 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verified pgdriver behaviour, it also throws exception for "NaN" for these methods

fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}

try {
subject.getLong(PG_NUMERIC_COLINDEX_NAN);
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}

try {
subject.getBigDecimal(PG_NUMERIC_COLINDEX_NAN);
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}
}

@Test
public void testGetLongIndexForString() {
try {
subject.getLong(STRING_COLINDEX_NOTNULL);
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(((JdbcSqlException) e).getCode(), Code.INVALID_ARGUMENT);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}
}

Expand All @@ -625,7 +732,7 @@ public void testGetLongIndexForTimestamp() {
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(((JdbcSqlException) e).getCode(), Code.INVALID_ARGUMENT);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}
}

Expand Down Expand Up @@ -669,7 +776,7 @@ public void testGetDoubleIndexFromTimestamp() {
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(((JdbcSqlException) e).getCode(), Code.INVALID_ARGUMENT);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}
}

Expand Down Expand Up @@ -1392,7 +1499,7 @@ public void testGetFloatIndexFromTimestamp() {
fail("missing expected SQLException");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(((JdbcSqlException) e).getCode(), Code.INVALID_ARGUMENT);
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,22 +452,63 @@ public void testConvertNumeric() throws SQLException {
assertThat(convert(d, Type.numeric(), String.class)).isEqualTo(String.valueOf(d));
assertThat(convert(d, Type.numeric(), Boolean.class)).isEqualTo(!d.equals(BigDecimal.ZERO));
if (d.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0
|| d.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0
|| d.scale() > 0) {
|| d.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0) {
assertConvertThrows(d, Type.numeric(), Long.class, Code.OUT_OF_RANGE);
} else {
assertThat(convert(d, Type.numeric(), Long.class)).isEqualTo(d.longValue());
}
if (d.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 0
|| d.compareTo(BigDecimal.valueOf(Integer.MIN_VALUE)) < 0
|| d.scale() > 0) {
|| d.compareTo(BigDecimal.valueOf(Integer.MIN_VALUE)) < 0) {
assertConvertThrows(d, Type.numeric(), Integer.class, Code.OUT_OF_RANGE);
} else {
assertThat(convert(d, Type.numeric(), Integer.class)).isEqualTo(d.intValue());
}
}
}

@Test
public void testConvertPgNumeric() throws SQLException {
BigDecimal[] testValues =
new BigDecimal[] {
BigDecimal.ZERO,
BigDecimal.ONE.negate(),
BigDecimal.ONE,
BigDecimal.valueOf(Double.MIN_VALUE),
BigDecimal.valueOf(Double.MAX_VALUE),
BigDecimal.valueOf(Float.MIN_VALUE),
BigDecimal.valueOf(Float.MAX_VALUE),
BigDecimal.valueOf(Float.MAX_VALUE + 1D)
};
for (BigDecimal d : testValues) {
String strVal = String.valueOf(d);
assertThat(convert(strVal, Type.pgNumeric(), BigDecimal.class)).isEqualTo(d);
assertThat(convert(strVal, Type.pgNumeric(), Double.class)).isEqualTo(d.doubleValue());
assertThat(convert(strVal, Type.pgNumeric(), Float.class)).isEqualTo(d.floatValue());
assertThat(convert(strVal, Type.pgNumeric(), String.class)).isEqualTo(strVal);
assertThat(convert(strVal, Type.pgNumeric(), Boolean.class))
.isEqualTo(!d.equals(BigDecimal.ZERO));
if (d.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0
|| d.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0) {
assertConvertThrows(strVal, Type.pgNumeric(), Long.class, Code.OUT_OF_RANGE);
} else {
assertThat(convert(strVal, Type.pgNumeric(), Long.class)).isEqualTo(d.longValue());
}
if (d.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 0
|| d.compareTo(BigDecimal.valueOf(Integer.MIN_VALUE)) < 0) {
assertConvertThrows(strVal, Type.pgNumeric(), Integer.class, Code.OUT_OF_RANGE);
} else {
assertThat(convert(strVal, Type.pgNumeric(), Integer.class)).isEqualTo(d.intValue());
}
}

assertThat(convert("NaN", Type.pgNumeric(), Float.class)).isEqualTo(Float.NaN);
assertThat(convert("NaN", Type.pgNumeric(), Double.class)).isEqualTo(Double.NaN);
assertThat(convert("NaN", Type.pgNumeric(), String.class)).isEqualTo("NaN");
assertConvertThrows("NaN", Type.pgNumeric(), Long.class, Code.INVALID_ARGUMENT);
assertConvertThrows("NaN", Type.pgNumeric(), Integer.class, Code.INVALID_ARGUMENT);
assertConvertThrows("NaN", Type.pgNumeric(), BigDecimal.class, Code.INVALID_ARGUMENT);
}

private void assertConvertThrows(Object t, Type type, Class<?> destinationType, Code code)
throws SQLException {
try {
Expand Down
Loading
Loading