This is an automated email from the ASF dual-hosted git repository. ppa pushed a commit to branch ignite-27612-tuple-implicit-casts in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit 67536d8914b28f509ae5527ce14caa6e14f2100c Author: Pavel Pereslegin <[email protected]> AuthorDate: Thu Jan 22 19:51:45 2026 +0300 IGNITE-27552 Add support for writing numeric primitive values with allowed type casting (#7439) --- .../client/proto/ClientBinaryTupleUtils.java | 15 +- .../ignite/internal/util/TupleTypeCastUtils.java | 48 ++- .../org/apache/ignite/internal/schema/Column.java | 5 + .../ignite/internal/schema/row/RowAssembler.java | 15 +- .../table/ItKeyValueBinaryViewApiTest.java | 427 +++++++++++++++++++- .../internal/table/ItRecordBinaryViewApiTest.java | 433 ++++++++++++++++++++- .../table/ItTableViewApiUnifiedBaseTest.java | 15 + 7 files changed, 939 insertions(+), 19 deletions(-) diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java index 73bc9cb2c33..5e9e38e1932 100644 --- a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java +++ b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java @@ -32,6 +32,7 @@ import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder; import org.apache.ignite.internal.binarytuple.BinaryTupleReader; import org.apache.ignite.internal.type.NativeType; import org.apache.ignite.internal.type.NativeTypes; +import org.apache.ignite.internal.util.TupleTypeCastUtils; import org.apache.ignite.lang.IgniteException; import org.apache.ignite.lang.MarshallerException; import org.apache.ignite.sql.ColumnType; @@ -204,27 +205,27 @@ public class ClientBinaryTupleUtils { return; case INT8: - builder.appendByte((byte) v); + builder.appendByte(TupleTypeCastUtils.castToByte(v)); return; case INT16: - builder.appendShort((short) v); + builder.appendShort(TupleTypeCastUtils.castToShort(v)); return; case INT32: - builder.appendInt((int) v); + builder.appendInt(TupleTypeCastUtils.castToInt(v)); return; case INT64: - builder.appendLong((long) v); + builder.appendLong(TupleTypeCastUtils.castToLong(v)); return; case FLOAT: - builder.appendFloat((float) v); + builder.appendFloat(TupleTypeCastUtils.castToFloat(v)); return; case DOUBLE: - builder.appendDouble((double) v); + builder.appendDouble(TupleTypeCastUtils.castToDouble(v)); return; case DECIMAL: @@ -262,7 +263,7 @@ public class ClientBinaryTupleUtils { default: throw new IllegalArgumentException("Unsupported type: " + type); } - } catch (ClassCastException e) { + } catch (ArithmeticException | ClassCastException e) { NativeType nativeType = NativeTypes.fromObject(v); if (nativeType == null) { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java index 845a69990ac..d816bc03627 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java @@ -23,8 +23,8 @@ import org.apache.ignite.sql.ColumnType; import org.apache.ignite.table.Tuple; /** - * Helper methods that perform conversions between numeric types. These methods are used when reading - * and writing the primitive numeric values of a {@link Tuple tuple}. + * Helper methods that perform conversions between numeric types. These methods are + * used when reading and writing the primitive numeric values to a {@link Tuple tuple}. * * <p>The following conversions are supported: * <ul> @@ -40,8 +40,52 @@ public class TupleTypeCastUtils { private static final String TYPE_CAST_ERROR_COLUMN_INDEX = "Column with index {} has type {} but {} was requested"; + /** Integer column types bitmask. */ private static final int INT_COLUMN_TYPES_BITMASK = buildIntegerTypesBitMask(); + /** + * Checks whether a cast is possible between two types for the given value. + * + * <p>Widening casts between integer types and between floating-point types are always allowed. + * + * <p>Narrowing casts between integer types and between floating-point types are allowed only + * when the provided value can be represented in the target type. + * + * @param from Source column type + * @param to Target column type + * @param val The value to be cast + * @return {@code True} if the cast is possible without data loss, {@code false} otherwise. + */ + public static boolean isCastAllowed(ColumnType from, ColumnType to, Object val) { + if (!(val instanceof Number)) { + return false; + } + + Number number = (Number) val; + + switch (to) { + case INT8: + return integerType(from) && number.byteValue() == number.longValue(); + case INT16: + return integerType(from) && number.shortValue() == number.longValue(); + case INT32: + return integerType(from) && number.intValue() == number.longValue(); + case INT64: + return integerType(from); + case FLOAT: + if (from == ColumnType.DOUBLE) { + double doubleValue = number.doubleValue(); + return number.floatValue() == doubleValue || Double.isNaN(doubleValue); + } + return false; + case DOUBLE: + return from == ColumnType.FLOAT; + + default: + return false; + } + } + /** Reads a value from the tuple and converts it to a byte if possible. */ public static byte readByteValue(InternalTuple binaryTuple, int binaryTupleIndex, ColumnType actualType, int columnIndex) { if (!integerType(actualType)) { diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java index 8b1fd49948a..18029a59397 100644 --- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java +++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java @@ -27,6 +27,7 @@ import org.apache.ignite.internal.tostring.S; import org.apache.ignite.internal.type.NativeType; import org.apache.ignite.internal.type.NativeTypes; import org.apache.ignite.internal.type.VarlenNativeType; +import org.apache.ignite.internal.util.TupleTypeCastUtils; import org.apache.ignite.sql.ColumnType; import org.jetbrains.annotations.Nullable; @@ -222,6 +223,10 @@ public class Column { String error = format("Value too long [column='{}', type={}]", name, type.displayName()); throw new InvalidTypeException(error); } else { + if (TupleTypeCastUtils.isCastAllowed(objType.spec(), type.spec(), val)) { + return; + } + String error = format( "Value type does not match [column='{}', expected={}, actual={}]", name, type.displayName(), objType.displayName() diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java index 5dbb941cd70..83c34317776 100644 --- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java +++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java @@ -109,6 +109,9 @@ public class RowAssembler { /** * Helper method. * + * <p>This method performs implicit conversion of numeric types without range checking, + * it assumes that the check has already been performed (see {@link Column#validate(Object)}). + * * @param val Value. * @throws SchemaMismatchException If a value doesn't match the current column type. */ @@ -124,22 +127,22 @@ public class RowAssembler { return appendBoolean((boolean) val); } case INT8: { - return appendByte((byte) val); + return appendByte(((Number) val).byteValue()); } case INT16: { - return appendShort((short) val); + return appendShort(((Number) val).shortValue()); } case INT32: { - return appendInt((int) val); + return appendInt(((Number) val).intValue()); } case INT64: { - return appendLong((long) val); + return appendLong(((Number) val).longValue()); } case FLOAT: { - return appendFloat((float) val); + return appendFloat(((Number) val).floatValue()); } case DOUBLE: { - return appendDouble((double) val); + return appendDouble(((Number) val).doubleValue()); } case UUID: { return appendUuid((UUID) val); diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java index 5bc8cc1f272..e9b1d4addbb 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java @@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -39,10 +40,12 @@ import org.apache.ignite.internal.schema.InvalidTypeException; import org.apache.ignite.internal.schema.SchemaDescriptor; import org.apache.ignite.internal.schema.SchemaMismatchException; import org.apache.ignite.internal.testframework.IgniteTestUtils; +import org.apache.ignite.internal.type.NativeType; import org.apache.ignite.internal.type.NativeTypes; import org.apache.ignite.lang.ErrorGroups.Marshalling; import org.apache.ignite.lang.IgniteException; import org.apache.ignite.lang.MarshallerException; +import org.apache.ignite.sql.ColumnType; import org.apache.ignite.table.KeyValueView; import org.apache.ignite.table.Tuple; import org.apache.ignite.tx.Transaction; @@ -65,6 +68,8 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { private static final String TABLE_NAME_FOR_SCHEMA_VALIDATION = "test_schema"; + private static final String TABLE_NAME_FOR_TYPE_CAST = "test_type_cast"; + Map<String, TestTableDefinition> createdTables; @BeforeAll @@ -96,6 +101,18 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { new Column("STR", NativeTypes.stringOf(3), true), new Column("BLOB", NativeTypes.blobOf(3), true) } + ), + new TestTableDefinition( + TABLE_NAME_FOR_TYPE_CAST, + simpleKey, + new Column[]{ + new Column("C_BYTE", NativeTypes.INT8, true), + new Column("C_SHORT", NativeTypes.INT16, true), + new Column("C_INT", NativeTypes.INT32, true), + new Column("C_LONG", NativeTypes.INT64, true), + new Column("C_FLOAT", NativeTypes.FLOAT, true), + new Column("C_DOUBLE", NativeTypes.DOUBLE, true) + } ) ); @@ -441,7 +458,7 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { public void validateSchema(TestCase testCase) { KeyValueView<Tuple, Tuple> tbl = testCase.view(); - Tuple keyTuple0 = Tuple.create().set("id", 0).set("id1", 0); + Tuple keyTuple0 = Tuple.create().set("id", 0.0d).set("id1", 0); Tuple keyTuple1 = Tuple.create().set("id1", 0); Tuple key = Tuple.create().set("id", 1L); @@ -582,6 +599,408 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { ); } + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsByte(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(0); + Tuple key = Tuple.create().set(keyName, 1L); + ColumnType targetType = ColumnType.INT8; + + // Put short value. + { + Tuple val = Tuple.create().set(valName, (short) Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (short) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT16, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (short) Byte.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (short) (Byte.MIN_VALUE - 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT16, testCase.thin); + } + + // Put int value. + { + Tuple val = Tuple.create().set(valName, (int) Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Byte.MAX_VALUE + 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (int) Byte.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Byte.MIN_VALUE - 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + // Put long value. + { + Tuple val = Tuple.create().set(valName, (long) Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (long) Byte.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Byte.MIN_VALUE - 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsShort(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(1); + Tuple key = Tuple.create().set(keyName, 1L); + ColumnType targetType = ColumnType.INT16; + + // Put byte value. + { + Tuple val = Tuple.create().set(valName, Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).shortValue(valName), is((short) Byte.MAX_VALUE)); + } + + // Put int value. + { + Tuple val = Tuple.create().set(valName, (int) Short.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).shortValue(valName), is(Short.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Short.MAX_VALUE + 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (int) Short.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).shortValue(valName), is(Short.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Short.MIN_VALUE - 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + // Put long value. + { + Tuple val = Tuple.create().set(valName, (long) Short.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).shortValue(valName), is(Short.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Short.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (long) Short.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).shortValue(valName), is(Short.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Short.MIN_VALUE - 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsInt(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(2); + Tuple key = Tuple.create().set(keyName, 1L); + ColumnType targetType = ColumnType.INT32; + + // Put byte value. + { + Tuple val = Tuple.create().set(valName, Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).intValue(valName), is((int) Byte.MAX_VALUE)); + } + + // Put short value. + { + Tuple val = Tuple.create().set(valName, Short.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).intValue(valName), is((int) Short.MAX_VALUE)); + } + + // Put long value. + { + Tuple val = Tuple.create().set(valName, (long) Integer.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).intValue(valName), is(Integer.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, ((long) Integer.MAX_VALUE) + 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (long) Integer.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).intValue(valName), is(Integer.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, ((long) Integer.MIN_VALUE) - 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsLong(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(3); + Tuple key = Tuple.create().set(keyName, 1L); + + // Put byte value. + { + Tuple val = Tuple.create().set(valName, Byte.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).longValue(valName), is((long) Byte.MAX_VALUE)); + } + + // Put short value. + { + Tuple val = Tuple.create().set(valName, Short.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).longValue(valName), is((long) Short.MAX_VALUE)); + } + + // Put int value. + { + Tuple val = Tuple.create().set(valName, Integer.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).longValue(valName), is((long) Integer.MAX_VALUE)); + } + + ColumnType targetType = ColumnType.INT64; + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsFloat(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(4); + Tuple key = Tuple.create().set(keyName, 1L); + ColumnType targetType = ColumnType.FLOAT; + + // Put double value. + { + Tuple val = Tuple.create().set(valName, (double) Float.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).floatValue(valName), is(Float.MAX_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + { + Tuple val = Tuple.create().set(valName, (double) Float.MIN_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).floatValue(valName), is(Float.MIN_VALUE)); + + Tuple outOfRange = Tuple.create().set(valName, Double.MIN_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // NaN value. + { + Tuple val = Tuple.create().set(valName, Double.NaN); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).floatValue(valName), is(Float.NaN)); + } + + // Positive infinity value. + { + Tuple val = Tuple.create().set(valName, Double.POSITIVE_INFINITY); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).floatValue(valName), is(Float.POSITIVE_INFINITY)); + } + + // Negative infinity value. + { + Tuple val = Tuple.create().set(valName, Double.NEGATIVE_INFINITY); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).floatValue(valName), is(Float.NEGATIVE_INFINITY)); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.create().set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, byteValue), valName, targetType, NativeTypes.INT8, testCase.thin); + + Tuple shortValue = Tuple.create().set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, shortValue), valName, targetType, NativeTypes.INT16, testCase.thin); + + Tuple intValue = Tuple.create().set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, intValue), valName, targetType, NativeTypes.INT32, testCase.thin); + + Tuple longValue = Tuple.create().set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, longValue), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsDouble(TestCase testCase) { + KeyValueView<Tuple, Tuple> tbl = testCase.view(); + String keyName = testCase.keyColumnName(0); + String valName = testCase.valColumnName(5); + Tuple key = Tuple.create().set(keyName, 1L); + ColumnType targetType = ColumnType.DOUBLE; + + // Put float value. + { + Tuple val = Tuple.create().set(valName, Float.MAX_VALUE); + + tbl.put(null, key, val); + assertThat(tbl.get(null, key).doubleValue(valName), is((double) Float.MAX_VALUE)); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.create().set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, byteValue), valName, targetType, NativeTypes.INT8, testCase.thin); + + Tuple shortValue = Tuple.create().set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, shortValue), valName, targetType, NativeTypes.INT16, testCase.thin); + + Tuple intValue = Tuple.create().set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, intValue), valName, targetType, NativeTypes.INT32, testCase.thin); + + Tuple longValue = Tuple.create().set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, longValue), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.create().set(valName, decimal); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + private List<Arguments> testCases() { List<Arguments> args1 = generateKeyValueTestArguments(TABLE_NAME_API_TEST, Tuple.class, Tuple.class); List<Arguments> args2 = generateKeyValueTestArguments(TABLE_NAME_API_TEST_QUOTED, Tuple.class, Tuple.class, " (quoted names)"); @@ -599,6 +1018,10 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { return generateKeyValueTestArguments(TABLE_NAME_FOR_SCHEMA_VALIDATION, Tuple.class, Tuple.class); } + private List<Arguments> typeCastTestCases() { + return generateKeyValueTestArguments(TABLE_NAME_FOR_TYPE_CAST, Tuple.class, Tuple.class); + } + @Override TestCaseFactory getFactory(String name) { return new TestCaseFactory(name) { @@ -682,7 +1105,7 @@ public class ItKeyValueBinaryViewApiTest extends ItKeyValueViewApiBaseTest { } void checkValueTypeDoesNotMatchError(Executable run) { - String expectedMessage = "Value type does not match [column='ID', expected=INT64, actual=INT32]"; + String expectedMessage = "Value type does not match [column='ID', expected=INT64, actual=DOUBLE]"; if (thin) { MarshallerException ex = (MarshallerException) assertThrows(MarshallerException.class, run, expectedMessage); diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java index 94cd7865261..34375033c68 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java @@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -34,11 +35,13 @@ import org.apache.ignite.internal.schema.InvalidTypeException; import org.apache.ignite.internal.schema.SchemaDescriptor; import org.apache.ignite.internal.schema.SchemaMismatchException; import org.apache.ignite.internal.testframework.IgniteTestUtils; +import org.apache.ignite.internal.type.NativeType; import org.apache.ignite.internal.type.NativeTypes; import org.apache.ignite.lang.ErrorGroups.Marshalling; import org.apache.ignite.lang.IgniteException; import org.apache.ignite.lang.MarshallerException; import org.apache.ignite.lang.util.IgniteNameUtils; +import org.apache.ignite.sql.ColumnType; import org.apache.ignite.table.RecordView; import org.apache.ignite.table.Tuple; import org.hamcrest.Matchers; @@ -69,6 +72,8 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { private static final String TABLE_BYTE_TYPE_MATCH = "test_byte_type_match"; + private static final String TABLE_NAME_FOR_TYPE_CAST = "test_type_cast"; + private Map<String, TestTableDefinition> schemaAwareTestTables; @BeforeAll @@ -124,6 +129,18 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { new Column("STR", NativeTypes.stringOf(3), true), new Column("BLOB", NativeTypes.blobOf(3), true) } + ), + new TestTableDefinition( + TABLE_NAME_FOR_TYPE_CAST, + DEFAULT_KEY, + new Column[]{ + new Column("C_BYTE", NativeTypes.INT8, true), + new Column("C_SHORT", NativeTypes.INT16, true), + new Column("C_INT", NativeTypes.INT32, true), + new Column("C_LONG", NativeTypes.INT64, true), + new Column("C_FLOAT", NativeTypes.FLOAT, true), + new Column("C_DOUBLE", NativeTypes.DOUBLE, true) + } ) ); @@ -328,7 +345,7 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { RecordView<Tuple> tbl = testCase.view(); - Tuple keyTuple0 = Tuple.create().set("id", 0).set("id1", 0); + Tuple keyTuple0 = Tuple.create().set("id", Double.MAX_VALUE).set("id1", 0); Tuple keyTuple1 = Tuple.create().set("id1", 0); Tuple tuple0 = Tuple.create().set("id", 1L).set("str", "qweqweqwe").set("val", 11L); Tuple tuple1 = Tuple.create().set("id", 1L).set("blob", new byte[]{0, 1, 2, 3}).set("val", 22L); @@ -791,6 +808,414 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { } } + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsByte(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_BYTE"; + ColumnType targetType = ColumnType.INT8; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put short value. + { + Tuple val = Tuple.copy(key).set(valName, (short) Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (short) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT16, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (short) Byte.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (short) (Byte.MIN_VALUE - 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT16, testCase.thin); + } + + // Put int value. + { + Tuple val = Tuple.copy(key).set(valName, (int) Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Byte.MAX_VALUE + 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (int) Byte.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Byte.MIN_VALUE - 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + // Put long value. + { + Tuple val = Tuple.copy(key).set(valName, (long) Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (long) Byte.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).byteValue(valName), is(Byte.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Byte.MIN_VALUE - 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsShort(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_SHORT"; + ColumnType targetType = ColumnType.INT16; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put byte value. + { + Tuple val = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).shortValue(valName), is((short) Byte.MAX_VALUE)); + } + + // Put int value. + { + Tuple val = Tuple.copy(key).set(valName, (int) Short.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).shortValue(valName), is(Short.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Short.MAX_VALUE + 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (int) Short.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).shortValue(valName), is(Short.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Short.MIN_VALUE - 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT32, testCase.thin); + } + + // Put long value. + { + Tuple val = Tuple.copy(key).set(valName, (long) Short.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).shortValue(valName), is(Short.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Short.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (long) Short.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).shortValue(valName), is(Short.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Short.MIN_VALUE - 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsInt(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_INT"; + ColumnType targetType = ColumnType.INT32; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put byte value. + { + Tuple val = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).intValue(valName), is((int) Byte.MAX_VALUE)); + } + + // Put short value. + { + Tuple val = Tuple.copy(key).set(valName, Short.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).intValue(valName), is((int) Short.MAX_VALUE)); + } + + // Put long value. + { + Tuple val = Tuple.copy(key).set(valName, (long) Integer.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).intValue(valName), is(Integer.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, ((long) Integer.MAX_VALUE) + 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (long) Integer.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).intValue(valName), is(Integer.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, ((long) Integer.MIN_VALUE) - 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsLong(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_LONG"; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put byte value. + { + Tuple val = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).longValue(valName), is((long) Byte.MAX_VALUE)); + } + + // Put short value. + { + Tuple val = Tuple.copy(key).set(valName, Short.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).longValue(valName), is((long) Short.MAX_VALUE)); + } + + // Put int value. + { + Tuple val = Tuple.copy(key).set(valName, Integer.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).longValue(valName), is((long) Integer.MAX_VALUE)); + } + + ColumnType targetType = ColumnType.INT64; + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, NativeTypes.FLOAT, testCase.thin); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsFloat(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_FLOAT"; + ColumnType targetType = ColumnType.FLOAT; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put double value. + { + Tuple val = Tuple.copy(key).set(valName, (double) Float.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).floatValue(valName), is(Float.MAX_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + { + Tuple val = Tuple.copy(key).set(valName, (double) Float.MIN_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).floatValue(valName), is(Float.MIN_VALUE)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Double.MIN_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, NativeTypes.DOUBLE, testCase.thin); + } + + // NaN value. + { + Tuple val = Tuple.copy(key).set(valName, Double.NaN); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).floatValue(valName), is(Float.NaN)); + } + + // Positive infinity value. + { + Tuple val = Tuple.copy(key).set(valName, Double.POSITIVE_INFINITY); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).floatValue(valName), is(Float.POSITIVE_INFINITY)); + } + + // Negative infinity value. + { + Tuple val = Tuple.copy(key).set(valName, Double.NEGATIVE_INFINITY); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).floatValue(valName), is(Float.NEGATIVE_INFINITY)); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, byteValue), valName, targetType, NativeTypes.INT8, testCase.thin); + + Tuple shortValue = Tuple.copy(key).set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, shortValue), valName, targetType, NativeTypes.INT16, testCase.thin); + + Tuple intValue = Tuple.copy(key).set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, intValue), valName, targetType, NativeTypes.INT32, testCase.thin); + + Tuple longValue = Tuple.copy(key).set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, longValue), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + + @ParameterizedTest + @MethodSource("typeCastTestCases") + public void testWriteAsDouble(BinTestCase testCase) { + RecordView<Tuple> recordView = testCase.view(); + String keyName = DEFAULT_KEY[0].name(); + String valName = "C_DOUBLE"; + ColumnType targetType = ColumnType.DOUBLE; + + Tuple key = Tuple.create().set(keyName, 1L); + + // Put float value. + { + Tuple val = Tuple.copy(key).set(valName, Float.MAX_VALUE); + + recordView.upsert(null, val); + assertThat(recordView.get(null, key).doubleValue(valName), is((double) Float.MAX_VALUE)); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, byteValue), valName, targetType, NativeTypes.INT8, testCase.thin); + + Tuple shortValue = Tuple.copy(key).set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, shortValue), valName, targetType, NativeTypes.INT16, testCase.thin); + + Tuple intValue = Tuple.copy(key).set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, intValue), valName, targetType, NativeTypes.INT32, testCase.thin); + + Tuple longValue = Tuple.copy(key).set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, longValue), valName, targetType, NativeTypes.INT64, testCase.thin); + } + + // Wrong (decimal) type + { + BigDecimal decimal = new BigDecimal(1); + NativeType valueType = NativeTypes.fromObject(decimal); + Tuple decimalValue = Tuple.copy(key).set(valName, decimal); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, valueType, testCase.thin); + } + } + /** * Check tuples equality. * @@ -863,6 +1288,10 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { return generateRecordViewTestArguments(TABLE_BYTE_TYPE_MATCH, Tuple.class); } + private List<Arguments> typeCastTestCases() { + return generateRecordViewTestArguments(TABLE_NAME_FOR_TYPE_CAST, Tuple.class); + } + @Override TestCaseFactory getFactory(String name) { return new TestCaseFactory(name) { @@ -891,7 +1320,7 @@ public class ItRecordBinaryViewApiTest extends ItRecordViewApiBaseTest { } void checkValueTypeDoesNotMatchError(Executable run) { - String expectedMessage = "Value type does not match [column='ID', expected=INT64, actual=INT32]"; + String expectedMessage = "Value type does not match [column='ID', expected=INT64, actual=DOUBLE]"; if (thin) { IgniteException ex = (IgniteException) IgniteTestUtils.assertThrows(IgniteException.class, run, expectedMessage); diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTableViewApiUnifiedBaseTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTableViewApiUnifiedBaseTest.java index 2ff57ce1db9..003ef09fbb1 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTableViewApiUnifiedBaseTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTableViewApiUnifiedBaseTest.java @@ -19,6 +19,7 @@ package org.apache.ignite.internal.table; import static java.util.stream.Collectors.toList; import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -37,13 +38,17 @@ import org.apache.ignite.internal.schema.Column; import org.apache.ignite.internal.schema.SchemaDescriptor; import org.apache.ignite.internal.sql.engine.util.Commons; import org.apache.ignite.internal.sql.engine.util.TypeUtils; +import org.apache.ignite.internal.type.NativeType; import org.apache.ignite.internal.type.NativeTypes; +import org.apache.ignite.lang.MarshallerException; import org.apache.ignite.lang.util.IgniteNameUtils; +import org.apache.ignite.sql.ColumnType; import org.apache.ignite.table.Tuple; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.function.Executable; /** * Base class for integration testing of key-value/record view public API. @@ -128,6 +133,16 @@ abstract class ItTableViewApiUnifiedBaseTest extends ClusterPerClassIntegrationT } } + static void expectTypeMismatch(Executable executable, String columnName, ColumnType expected, NativeType actual, boolean thin) { + // TODO https://issues.apache.org/jira/browse/IGNITE-21793 Thin client message must use native type display name. + String actualTypeName = thin ? actual.spec().name() : actual.displayName(); + + //noinspection ThrowableNotThrown + assertThrows(MarshallerException.class, executable, + IgniteStringFormatter.format("Value type does not match [column='{}', expected={}, actual={}]", + columnName.toUpperCase(), expected.name(), actualTypeName)); + } + private static List<String> getClientAddresses(List<Ignite> nodes) { return nodes.stream() .map(ignite -> unwrapIgniteImpl(ignite).clientAddress().port())
