diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java index d9bfe818b..da0620ee6 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java index 46a35c1a4..8aee7692d 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java @@ -116,39 +116,55 @@ 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; @@ -156,6 +172,9 @@ static Object convert(Object value, Type type, Class targetType) throws SQLEx 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) @@ -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) @@ -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; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConcurrentTransactionOnEmulatorTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConcurrentTransactionOnEmulatorTest.java index 74a005038..a2565d8b7 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConcurrentTransactionOnEmulatorTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/ConcurrentTransactionOnEmulatorTest.java @@ -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(); + } properties = new Properties(); properties.setProperty("autoConfigEmulator", "true"); properties.setProperty( diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java index 67b0cfd23..795d5155f 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java @@ -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; @@ -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) @@ -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())); } @@ -594,6 +609,98 @@ 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"); + } 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); + 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 { @@ -601,7 +708,7 @@ public void testGetLongIndexForString() { 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()); } } @@ -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()); } } @@ -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()); } } @@ -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()); } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java index db36b47fa..2d3727970 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java @@ -452,15 +452,13 @@ 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()); @@ -468,6 +466,49 @@ public void testConvertNumeric() throws SQLException { } } + @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 { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcNumericTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcNumericTest.java new file mode 100644 index 000000000..9706348ff --- /dev/null +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcNumericTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.jdbc.it; + +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITJdbcNumericTest { + + @ClassRule public static JdbcIntegrationTestEnv env = new JdbcIntegrationTestEnv(); + + private static final Duration OPERATION_TIMEOUT = Duration.ofMinutes(10); + private static RemoteSpannerHelper testHelper; + private static Database database; + private static String url; + + @BeforeClass + public static void setup() throws Exception { + testHelper = env.getTestHelper(); + final String projectId = testHelper.getInstanceId().getProject(); + final String instanceId = testHelper.getInstanceId().getInstance(); + final String databaseId = testHelper.getUniqueDatabaseId(); + final DatabaseAdminClient databaseAdminClient = testHelper.getClient().getDatabaseAdminClient(); + final Database databaseToCreate = + databaseAdminClient + .newDatabaseBuilder(DatabaseId.of(projectId, instanceId, databaseId)) + .setDialect(Dialect.GOOGLE_STANDARD_SQL) + .build(); + final String host = SpannerTestHost.getHost(); + + database = + databaseAdminClient + .createDatabase(databaseToCreate, Collections.emptyList()) + .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + url = "jdbc:cloudspanner://" + host + "/" + database.getId(); + if (isUsingEmulator()) { + url += "?usePlainText=true"; + } + } + + @AfterClass + public static void teardown() { + if (database != null) { + database.drop(); + } + ConnectionOptions.closeSpanner(); + } + + @Test + public void testResultSet() throws SQLException { + final String table = testHelper.getUniqueDatabaseId(); + final String positiveBigNumeric = "99999999999999999999999999999.999999999"; + final String negativeBigNumeric = "-99999999999999999999999999999.999999999"; + try (Connection connection = DriverManager.getConnection(url); + Statement statement = connection.createStatement()) { + statement.executeUpdate( + "CREATE TABLE " + table + " (id INT64, col1 NUMERIC) PRIMARY KEY (id)"); + statement.executeUpdate( + "INSERT INTO " + + table + + " (id, col1) VALUES" + + " (1, NUMERIC '1.23')," + + " (2, null)," + + " (3, NUMERIC '" + + positiveBigNumeric + + "')," + + " (4, NUMERIC '" + + negativeBigNumeric + + "')"); + } + + try (Connection connection = DriverManager.getConnection(url); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT * FROM " + table + " ORDER BY id")) { + + resultSet.next(); + assertEquals("1.23", resultSet.getString("col1")); + assertEquals((byte) 1, resultSet.getByte("col1")); + assertEquals((short) 1, resultSet.getShort("col1")); + assertEquals(1, resultSet.getInt("col1")); + assertEquals(1L, resultSet.getLong("col1")); + assertEquals(1.23F, resultSet.getFloat("col1"), 0.001F); + assertEquals(1.23D, resultSet.getDouble("col1"), 0.001D); + assertEquals(new BigDecimal("1.23"), resultSet.getBigDecimal("col1")); + assertEquals(new BigDecimal("1.23"), resultSet.getObject("col1")); + assertEquals(Value.numeric(new BigDecimal("1.23")), resultSet.getObject("col1", Value.class)); + + resultSet.next(); + assertNull(resultSet.getString("col1")); + assertEquals((byte) 0, resultSet.getByte("col1")); + assertEquals((short) 0, resultSet.getShort("col1")); + assertEquals(0, resultSet.getInt("col1")); + assertEquals(0L, resultSet.getLong("col1")); + assertEquals(0F, resultSet.getFloat("col1"), 0F); + assertEquals(0D, resultSet.getDouble("col1"), 0D); + assertNull(resultSet.getBigDecimal("col1")); + assertNull(resultSet.getObject("col1")); + assertNull(resultSet.getObject("col1", Value.class)); + + resultSet.next(); + assertEquals(positiveBigNumeric, resultSet.getString("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getByte("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getShort("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getInt("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getLong("col1")); + assertEquals(Float.parseFloat(positiveBigNumeric), resultSet.getFloat("col1"), 0F); + assertEquals(Double.parseDouble(positiveBigNumeric), resultSet.getDouble("col1"), 0D); + assertEquals(new BigDecimal(positiveBigNumeric), resultSet.getBigDecimal("col1")); + assertEquals(new BigDecimal(positiveBigNumeric), resultSet.getObject("col1")); + assertEquals( + Value.numeric(new BigDecimal(positiveBigNumeric)), + resultSet.getObject("col1", Value.class)); + + resultSet.next(); + assertEquals(negativeBigNumeric, resultSet.getString("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getByte("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getShort("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getInt("col1")); + assertThrows(JdbcSqlExceptionImpl.class, () -> resultSet.getLong("col1")); + assertEquals(Float.parseFloat(negativeBigNumeric), resultSet.getFloat("col1"), 0F); + assertEquals(Double.parseDouble(negativeBigNumeric), resultSet.getDouble("col1"), 0D); + assertEquals(new BigDecimal(negativeBigNumeric), resultSet.getBigDecimal("col1")); + assertEquals(new BigDecimal(negativeBigNumeric), resultSet.getObject("col1")); + assertEquals( + Value.numeric(new BigDecimal(negativeBigNumeric)), + resultSet.getObject("col1", Value.class)); + + // Just verify that the getColumns method works + try (ResultSet columns = connection.getMetaData().getColumns(null, null, null, null)) { + //noinspection StatementWithEmptyBody + while (columns.next()) { + // ignore + } + } + } + } + + @Test + public void testPreparedStatement() throws Exception { + final String table = testHelper.getUniqueDatabaseId(); + try (Connection connection = DriverManager.getConnection(url); + Statement statement = connection.createStatement(); ) { + statement.executeUpdate( + "CREATE TABLE " + table + " (id INT64, col1 NUMERIC) PRIMARY KEY (id)"); + } + + try (Connection connection = DriverManager.getConnection(url); + PreparedStatement preparedStatement = + connection.prepareStatement( + "INSERT INTO " + + table + + " (id, col1) VALUES" + + " (1, ?)," + + " (2, ?)," + + " (3, ?)," + + " (4, ?)," + + " (5, ?)," + + " (8, ?)," + + " (9, ?)," + + " (10, ?)," + + " (11, ?)," + + " (12, ?)," + + " (15, ?)," + + " (16, ?)")) { + + preparedStatement.setNull(1, Types.NUMERIC); + + preparedStatement.setByte(2, (byte) 1); + preparedStatement.setShort(3, (short) 1); + preparedStatement.setInt(4, 1); + preparedStatement.setLong(5, 1L); + preparedStatement.setBigDecimal(6, new BigDecimal("1")); + preparedStatement.setObject(7, (byte) 1); + preparedStatement.setObject(8, (short) 1); + preparedStatement.setObject(9, 1); + preparedStatement.setObject(10, 1L); + preparedStatement.setObject(11, new BigDecimal("1")); + preparedStatement.setObject(12, Value.numeric(new BigDecimal("1"))); + + final int updateCount = preparedStatement.executeUpdate(); + assertEquals(12, updateCount); + } + + try (Connection connection = DriverManager.getConnection(url); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT * FROM " + table + " ORDER BY id")) { + resultSet.next(); + assertNull(resultSet.getObject("col1")); + + for (int i = 2; i <= 12; i++) { + resultSet.next(); + assertEquals(Value.numeric(new BigDecimal("1")), resultSet.getObject("col1", Value.class)); + } + } + } +}