Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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();
}
Comment on lines +62 to +66
Copy link
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?)

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
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)

} 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
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?

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