diff --git a/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java b/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java index b8d6c1b2..c44fd832 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java @@ -103,6 +103,7 @@ public final class YdbConst { public static final String UNABLE_TO_CONVERT = "Cannot cast [%s] with value [%s] to [%s]"; public static final String UNABLE_TO_CONVERT_AS_URL = "Cannot cast as URL: "; public static final String UNABLE_TO_CAST_TO_CLASS = "Cannot cast [%s] to class [%s]"; + public static final String UNABLE_TO_CAST_TO_DECIMAL = "Cannot cast to decimal type %s: [%s] is %s"; public static final String MISSING_VALUE_FOR_PARAMETER = "Missing value for parameter: "; public static final String MISSING_REQUIRED_VALUE = "Missing required value for parameter: "; diff --git a/jdbc/src/main/java/tech/ydb/jdbc/common/MappingGetters.java b/jdbc/src/main/java/tech/ydb/jdbc/common/MappingGetters.java index e0b166ae..9f2ba02a 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/common/MappingGetters.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/common/MappingGetters.java @@ -30,32 +30,6 @@ import tech.ydb.table.values.Type; import tech.ydb.table.values.Value; -import static tech.ydb.table.values.PrimitiveType.Bool; -import static tech.ydb.table.values.PrimitiveType.Bytes; -import static tech.ydb.table.values.PrimitiveType.Date; -import static tech.ydb.table.values.PrimitiveType.Datetime; -import static tech.ydb.table.values.PrimitiveType.Double; -import static tech.ydb.table.values.PrimitiveType.Float; -import static tech.ydb.table.values.PrimitiveType.Int16; -import static tech.ydb.table.values.PrimitiveType.Int32; -import static tech.ydb.table.values.PrimitiveType.Int64; -import static tech.ydb.table.values.PrimitiveType.Int8; -import static tech.ydb.table.values.PrimitiveType.Interval; -import static tech.ydb.table.values.PrimitiveType.Json; -import static tech.ydb.table.values.PrimitiveType.JsonDocument; -import static tech.ydb.table.values.PrimitiveType.Text; -import static tech.ydb.table.values.PrimitiveType.Timestamp; -import static tech.ydb.table.values.PrimitiveType.TzDate; -import static tech.ydb.table.values.PrimitiveType.TzDatetime; -import static tech.ydb.table.values.PrimitiveType.TzTimestamp; -import static tech.ydb.table.values.PrimitiveType.Uint16; -import static tech.ydb.table.values.PrimitiveType.Uint32; -import static tech.ydb.table.values.PrimitiveType.Uint64; -import static tech.ydb.table.values.PrimitiveType.Uint8; -import static tech.ydb.table.values.PrimitiveType.Uuid; -import static tech.ydb.table.values.PrimitiveType.Yson; -import static tech.ydb.table.values.Type.Kind.PRIMITIVE; - public class MappingGetters { private MappingGetters() { } @@ -95,7 +69,7 @@ static Getters buildGetters(Type type) { value -> value.getDecimal().toBigDecimal().floatValue(), value -> value.getDecimal().toBigDecimal().doubleValue(), castToBytesNotSupported(clazz), - PrimitiveReader::getDecimal, + value -> value.getDecimal().toBigDecimal(), castToClassNotSupported(clazz), castToInstantNotSupported(clazz), castToNStringNotSupported(clazz), diff --git a/jdbc/src/main/java/tech/ydb/jdbc/common/MappingSetters.java b/jdbc/src/main/java/tech/ydb/jdbc/common/MappingSetters.java index 750006e3..a068ea5c 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/common/MappingSetters.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/common/MappingSetters.java @@ -466,23 +466,36 @@ private static PrimitiveValue castToTimestamp(PrimitiveType type, Object x) thro throw castNotSupported(type, x); } + private static DecimalValue validateValue(DecimalType type, DecimalValue value, Object x) throws SQLException { + if (value.isInf()) { + throw new SQLException(String.format(YdbConst.UNABLE_TO_CAST_TO_DECIMAL, type, toString(x), "Infinite")); + } + if (value.isNegativeInf()) { + throw new SQLException(String.format(YdbConst.UNABLE_TO_CAST_TO_DECIMAL, type, toString(x), "-Infinite")); + } + if (value.isNan()) { + throw new SQLException(String.format(YdbConst.UNABLE_TO_CAST_TO_DECIMAL, type, toString(x), "NaN")); + } + return value; + } + private static DecimalValue castToDecimalValue(DecimalType type, Object x) throws SQLException { if (x instanceof DecimalValue) { - return (DecimalValue) x; + return validateValue(type, (DecimalValue) x, x); } else if (x instanceof BigDecimal) { - return type.newValue((BigDecimal) x); + return validateValue(type, type.newValue((BigDecimal) x), x); } else if (x instanceof BigInteger) { - return type.newValue((BigInteger) x); + return validateValue(type, type.newValue((BigInteger) x), x); } else if (x instanceof Long) { - return type.newValue((Long) x); + return validateValue(type, type.newValue((Long) x), x); } else if (x instanceof Integer) { - return type.newValue((Integer) x); + return validateValue(type, type.newValue((Integer) x), x); } else if (x instanceof Short) { - return type.newValue((Short) x); + return validateValue(type, type.newValue((Short) x), x); } else if (x instanceof Byte) { - return type.newValue((Byte) x); + return validateValue(type, type.newValue((Byte) x), x); } else if (x instanceof String) { - return type.newValue((String) x); + return validateValue(type, type.newValue((String) x), x); } throw castNotSupported(type.getKind(), x); } diff --git a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbLazyResultSetImplTest.java b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbLazyResultSetImplTest.java index 0c026e27..8a5e3ccf 100644 --- a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbLazyResultSetImplTest.java +++ b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbLazyResultSetImplTest.java @@ -1636,7 +1636,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(311111156, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(311111, 223342000)) .typedValue(21, "c_Interval", Duration.parse("PT3.111113S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("3.335000000")); + .typedValue(22, "c_Decimal", new BigDecimal("3.335000000")); checker.nextRow() .typedValue(1, "key", 2) @@ -1660,7 +1660,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(211211100, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(111111, 223342000)) .typedValue(21, "c_Interval", Duration.parse("PT3.112113S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("-3.335000000")); + .typedValue(22, "c_Decimal", new BigDecimal("-3.335000000")); checker.nextRow() .typedValue(1, "key", 3) @@ -1684,7 +1684,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(0, 0)) .typedValue(21, "c_Interval", Duration.parse("PT0.000000S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("0.00000000")); + .typedValue(22, "c_Decimal", new BigDecimal("0.000000000")); checker.nextRow() .typedValue(1, "key", 4) @@ -1708,7 +1708,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(0, 1000)) .typedValue(21, "c_Interval", Duration.parse("PT0.000001S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("1.00000000")); + .typedValue(22, "c_Decimal", new BigDecimal("1.000000000")); checker.nextRow() .value(1, "key", 5) diff --git a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java index 724048bb..6fe32391 100644 --- a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java +++ b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java @@ -1,6 +1,7 @@ package tech.ydb.jdbc.impl; import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -982,4 +983,74 @@ private void assertNextDate(ResultSet rs, int key, LocalDate ld) throws SQLExcep Assertions.assertEquals(ld.atStartOfDay(), rs.getObject("c_Date", LocalDateTime.class)); Assertions.assertEquals(ld.atStartOfDay(ZoneId.systemDefault()).toInstant(), rs.getObject("c_Date", Instant.class)); } + + @ParameterizedTest(name = "with {0}") + @EnumSource(SqlQueries.JdbcQuery.class) + public void decimalTest(SqlQueries.JdbcQuery query) throws SQLException { + String upsert = TEST_TABLE.upsertOne(query, "c_Decimal", "Decimal(22, 9)"); + + // YDB partially ignores Decimal(22, 9) limit, but have hard limit to 35 digits + String maxValue = "9999999999" + "9999999999" + "9999999999" + "99999"; + BigDecimal closeToInf = new BigDecimal(new BigInteger(maxValue), 9); + BigDecimal closeToNegInf = new BigDecimal(new BigInteger(maxValue).negate(), 9); + try (PreparedStatement ps = jdbc.connection().prepareStatement(upsert)) { + ps.setInt(1, 1); + ps.setBigDecimal(2, BigDecimal.valueOf(1.5d)); + ps.execute(); + + ps.setInt(1, 2); + ps.setBigDecimal(2, BigDecimal.valueOf(-12345, 10)); // will be rounded to -0.000001234 + ps.execute(); + + ps.setInt(1, 3); + ps.setBigDecimal(2, closeToInf); + ps.execute(); + + ps.setInt(1, 4); + ps.setBigDecimal(2, closeToNegInf); + ps.execute(); + + ps.setInt(1, 5); + ExceptionAssert.sqlException("" + + "Cannot cast to decimal type Decimal(22, 9): " + + "[class java.math.BigDecimal: 100000000000000000000000000.000000000] is Infinite", + () -> ps.setBigDecimal(2, closeToInf.add(BigDecimal.valueOf(1, 9))) + ); + + ExceptionAssert.sqlException("" + + "Cannot cast to decimal type Decimal(22, 9): " + + "[class java.math.BigDecimal: -100000000000000000000000000.000000000] is -Infinite", + () -> ps.setBigDecimal(2, closeToNegInf.subtract(BigDecimal.valueOf(1, 9))) + ); + + ExceptionAssert.sqlException("" + + "Cannot cast to decimal type Decimal(22, 9): " + + "[class java.math.BigDecimal: 100000000000000000000000000.000000001] is NaN", + () -> ps.setBigDecimal(2, closeToInf.add(BigDecimal.valueOf(2, 9))) + ); + } + + try (Statement st = jdbc.connection().createStatement()) { + try (ResultSet rs = st.executeQuery(TEST_TABLE.selectColumn("c_Decimal"))) { + assertNextDecimal(rs, 1, BigDecimal.valueOf(1.5d).setScale(9)); + assertNextDecimal(rs, 2, BigDecimal.valueOf(-1234, 9)); + assertNextDecimal(rs, 3, closeToInf); + assertNextDecimal(rs, 4, closeToNegInf); + + Assertions.assertFalse(rs.next()); + } + } + }; + + private void assertNextDecimal(ResultSet rs, int key, BigDecimal bg) throws SQLException { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(key, rs.getInt("key")); + + Object obj = rs.getObject("c_Decimal"); + Assertions.assertTrue(obj instanceof BigDecimal); + Assertions.assertEquals(bg, obj); + + BigDecimal decimal = rs.getBigDecimal("c_Decimal"); + Assertions.assertEquals(bg, decimal); + } } diff --git a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbResultSetImplTest.java b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbResultSetImplTest.java index aee1c983..921168ec 100644 --- a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbResultSetImplTest.java +++ b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbResultSetImplTest.java @@ -1797,7 +1797,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(311111156, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(311111, 223342000)) .typedValue(21, "c_Interval", Duration.parse("PT3.111113S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("3.335000000")); + .typedValue(22, "c_Decimal", new BigDecimal("3.335000000")); checker.nextRow() .typedValue(1, "key", 2) @@ -1821,7 +1821,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(211211100, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(111111, 223342000)) .typedValue(21, "c_Interval", Duration.parse("PT3.112113S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("-3.335000000")); + .typedValue(22, "c_Decimal", new BigDecimal("-3.335000000")); checker.nextRow() .typedValue(1, "key", 3) @@ -1845,7 +1845,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(0, 0)) .typedValue(21, "c_Interval", Duration.parse("PT0.000000S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("0.00000000")); + .typedValue(22, "c_Decimal", new BigDecimal("0.000000000")); checker.nextRow() .typedValue(1, "key", 4) @@ -1869,7 +1869,7 @@ public void getObject() throws SQLException { .typedValue(19, "c_Datetime", LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC)) .typedValue(20, "c_Timestamp", Instant.ofEpochSecond(0, 1000)) .typedValue(21, "c_Interval", Duration.parse("PT0.000001S")) - .typedValue(22, "c_Decimal", DecimalType.getDefault().newValue("1.00000000")); + .typedValue(22, "c_Decimal", new BigDecimal("1.000000000")); checker.nextRow() .value(1, "key", 5)