This is an automated email from the ASF dual-hosted git repository. ppa pushed a commit to branch ignite-27552 in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit ee174399b851920e2bbf781b4d382b01cb11c781 Author: Pavel Pereslegin <[email protected]> AuthorDate: Tue Jan 13 20:22:54 2026 +0300 IGNITE-27552 Support cast of numerics values when writing tuples --- .../client/proto/ClientBinaryTupleUtils.java | 171 ++++++++++- .../ignite/internal/util/TupleTypeCastUtils.java | 47 ++- .../org/apache/ignite/internal/schema/Column.java | 5 + .../ignite/internal/schema/row/RowAssembler.java | 12 +- .../table/ItKeyValueBinaryViewApiTest.java | 320 +++++++++++++++++++- .../internal/table/ItRecordBinaryViewApiTest.java | 326 ++++++++++++++++++++- .../table/ItTableViewApiUnifiedBaseTest.java | 11 + 7 files changed, 874 insertions(+), 18 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..254c93ae745 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 @@ -204,27 +204,27 @@ public class ClientBinaryTupleUtils { return; case INT8: - builder.appendByte((byte) v); + appendByteValue(builder, v); return; case INT16: - builder.appendShort((short) v); + appendShortValue(builder, v); return; case INT32: - builder.appendInt((int) v); + appendIntValue(builder, v); return; case INT64: - builder.appendLong((long) v); + appendLongValue(builder, v); return; case FLOAT: - builder.appendFloat((float) v); + appendFloatValue(builder, v); return; case DOUBLE: - builder.appendDouble((double) v); + appendDoubleValue(builder, v); return; case DECIMAL: @@ -288,6 +288,165 @@ public class ClientBinaryTupleUtils { } } + private static void appendByteValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Byte.class) { + builder.appendByte((byte) val); + return; + } + + if (val.getClass() == Short.class) { + short shortVal = (short) val; + byte byteVal = (byte) shortVal; + + if (shortVal == byteVal) { + builder.appendByte(byteVal); + return; + } + } + + if (val.getClass() == Integer.class) { + int intVal = (int) val; + byte byteVal = (byte) intVal; + + if (intVal == byteVal) { + builder.appendByte(byteVal); + return; + } + } + + if (val.getClass() == Long.class) { + long longVal = (long) val; + byte byteVal = (byte) longVal; + + if (longVal == byteVal) { + builder.appendByte(byteVal); + return; + } + } + + throw new ClassCastException("Cannot cast to byte: " + val.getClass()); + } + + private static void appendShortValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Short.class) { + builder.appendShort((short) val); + return; + } + + if (val.getClass() == Byte.class) { + builder.appendShort((byte) val); + return; + } + + if (val.getClass() == Integer.class) { + int intVal = (int) val; + short shortVal = (short) intVal; + + if (intVal == shortVal) { + builder.appendShort(shortVal); + return; + } + } + + if (val.getClass() == Long.class) { + long longVal = (long) val; + short shortVal = (short) longVal; + + if (longVal == shortVal) { + builder.appendShort(shortVal); + return; + } + } + + throw new ClassCastException(); + } + + private static void appendIntValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Integer.class) { + builder.appendInt((int) val); + return; + } + + if (val.getClass() == Short.class) { + builder.appendInt((short) val); + return; + } + + if (val.getClass() == Byte.class) { + builder.appendInt((byte) val); + return; + } + + if (val.getClass() == Long.class) { + long longVal = (long) val; + int intVal = (int) longVal; + + if (longVal == intVal) { + builder.appendInt(intVal); + return; + } + } + + throw new ClassCastException("Cannot cast to int: " + val.getClass()); + } + + private static void appendLongValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Integer.class) { + builder.appendLong((int) val); + return; + } + + if (val.getClass() == Short.class) { + builder.appendLong((short) val); + return; + } + + if (val.getClass() == Byte.class) { + builder.appendLong((byte) val); + return; + } + + if (val.getClass() == Long.class) { + builder.appendLong((long) val); + return; + } + + throw new ClassCastException("Cannot cast to long: " + val.getClass()); + } + + private static void appendFloatValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Float.class) { + builder.appendFloat((float) val); + return; + } + + if (val.getClass() == Double.class) { + double doubleVal = (double) val; + float floatVal = (float) doubleVal; + + if (doubleVal == floatVal) { + builder.appendFloat(floatVal); + return; + } + } + + throw new ClassCastException("Cannot cast to float: " + val.getClass()); + } + + private static void appendDoubleValue(BinaryTupleBuilder builder, Object val) { + if (val.getClass() == Double.class) { + builder.appendDouble((double) val); + return; + } + + if (val.getClass() == Float.class) { + builder.appendDouble((float) val); + return; + } + + throw new ClassCastException("Cannot cast to double: " + val.getClass()); + } + private static void appendTypeAndScale(BinaryTupleBuilder builder, ColumnType type, int scale) { builder.appendInt(type.id()); builder.appendInt(scale); 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 c965ca7633a..273eeeb917e 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 @@ -195,6 +195,45 @@ public class TupleTypeCastUtils { } } + /** + * 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 (!(floatingPointType(from) && floatingPointType(to)) + && !(integerType(from) && integerType(to))) { + return false; + } + + Number number = (Number) val; + + switch (to) { + case INT8: + return number.byteValue() == number.longValue(); + case INT16: + return number.shortValue() == number.longValue(); + case INT32: + return number.intValue() == number.longValue(); + case FLOAT: + return number.floatValue() == number.doubleValue(); + case INT64: + case DOUBLE: + return true; + + default: + throw new UnsupportedOperationException(from.name() + " -> " + to.name()); + } + } + /** Casts an integer value from the tuple to {@code byte} performing range checks. */ private static byte castToByte(InternalTuple binaryTuple, int binaryTupleIndex, ColumnType valueType) { switch (valueType) { @@ -332,7 +371,11 @@ public class TupleTypeCastUtils { return new ClassCastException(IgniteStringFormatter.format(TYPE_CAST_ERROR_COLUMN_NAME, columnName, actualType, requestedType)); } - private static boolean integerType(ColumnType actualType) { - return INT_TYPES.contains(actualType); + private static boolean integerType(ColumnType type) { + return INT_TYPES.contains(type); + } + + private static boolean floatingPointType(ColumnType type) { + return type == ColumnType.FLOAT || type == ColumnType.DOUBLE; } } 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..1f2dbea6053 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 @@ -124,22 +124,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..1b2e9ed6320 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; @@ -43,6 +44,7 @@ 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 +67,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 +100,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 +457,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 +598,302 @@ 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) 127)); + + Tuple outOfRange = Tuple.create().set(valName, (short) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, ColumnType.INT16); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.create().set(valName, Byte.MAX_VALUE + 1); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, ColumnType.INT32); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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) 127)); + } + + // 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, ColumnType.INT32); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.create().set(valName, (long) (Short.MAX_VALUE + 1)); + expectTypeMismatch(() -> tbl.put(null, key, outOfRange), valName, targetType, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.create().set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.create().set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.DOUBLE); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.create().set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, byteValue), valName, targetType, ColumnType.INT8); + + Tuple shortValue = Tuple.create().set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, shortValue), valName, targetType, ColumnType.INT16); + + Tuple intValue = Tuple.create().set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, intValue), valName, targetType, ColumnType.INT32); + + Tuple longValue = Tuple.create().set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, longValue), valName, targetType, ColumnType.INT64); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.INT8); + + Tuple shortValue = Tuple.create().set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, shortValue), valName, targetType, ColumnType.INT16); + + Tuple intValue = Tuple.create().set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, intValue), valName, targetType, ColumnType.INT32); + + Tuple longValue = Tuple.create().set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> tbl.put(null, key, longValue), valName, targetType, ColumnType.INT64); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.create().set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> tbl.put(null, key, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + 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 +911,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 +998,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..e2637358397 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; @@ -39,6 +40,7 @@ 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 +71,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 +128,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 +344,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 +807,308 @@ 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) 127)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (short) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, ColumnType.INT16); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.copy(key).set(valName, Byte.MAX_VALUE + 1); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, ColumnType.INT32); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Byte.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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) 127)); + } + + // 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, ColumnType.INT32); + } + + // 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) 127)); + + Tuple outOfRange = Tuple.copy(key).set(valName, (long) (Short.MAX_VALUE + 1)); + expectTypeMismatch(() -> recordView.upsert(null, outOfRange), valName, targetType, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.INT64); + } + + // Wrong (floating point) types + { + Tuple floatValue = Tuple.copy(key).set(valName, Float.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, floatValue), valName, targetType, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.FLOAT); + + Tuple doubleValue = Tuple.copy(key).set(valName, Double.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, doubleValue), valName, targetType, ColumnType.DOUBLE); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.DOUBLE); + } + + // Wrong (integer) types + { + Tuple byteValue = Tuple.copy(key).set(valName, Byte.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, byteValue), valName, targetType, ColumnType.INT8); + + Tuple shortValue = Tuple.copy(key).set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, shortValue), valName, targetType, ColumnType.INT16); + + Tuple intValue = Tuple.copy(key).set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, intValue), valName, targetType, ColumnType.INT32); + + Tuple longValue = Tuple.copy(key).set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, longValue), valName, targetType, ColumnType.INT64); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + + @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, ColumnType.INT8); + + Tuple shortValue = Tuple.copy(key).set(valName, Short.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, shortValue), valName, targetType, ColumnType.INT16); + + Tuple intValue = Tuple.copy(key).set(valName, Integer.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, intValue), valName, targetType, ColumnType.INT32); + + Tuple longValue = Tuple.copy(key).set(valName, Long.MAX_VALUE); + expectTypeMismatch(() -> recordView.upsert(null, longValue), valName, targetType, ColumnType.INT64); + } + + // Wrong (decimal) type + { + Tuple decimalValue = Tuple.copy(key).set(valName, new BigDecimal(1)); + expectTypeMismatch(() -> recordView.upsert(null, decimalValue), valName, targetType, ColumnType.DECIMAL); + } + } + /** * Check tuples equality. * @@ -863,6 +1181,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 +1213,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..b8bf7b62498 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; @@ -38,12 +39,15 @@ 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.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 +132,13 @@ abstract class ItTableViewApiUnifiedBaseTest extends ClusterPerClassIntegrationT } } + static void expectTypeMismatch(Executable executable, String columnName, ColumnType expected, ColumnType actual) { + //noinspection ThrowableNotThrown + assertThrows(MarshallerException.class, executable, + IgniteStringFormatter.format("Value type does not match [column='{}', expected={}, actual={}", + columnName.toUpperCase(), expected.name(), actual.name())); + } + private static List<String> getClientAddresses(List<Ignite> nodes) { return nodes.stream() .map(ignite -> unwrapIgniteImpl(ignite).clientAddress().port())
