Copilot commented on code in PR #7406:
URL: https://github.com/apache/ignite-3/pull/7406#discussion_r2689227438
##########
modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java:
##########
@@ -582,6 +598,302 @@ public void schemaMismatch(TestCase testCase) {
);
}
+ @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, 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.MAX_VALUE));
+
+ 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.MAX_VALUE));
+
+ 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)
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, ColumnType.INT32);
+ }
+
+ // 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, 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);
+ }
+ }
Review Comment:
The test coverage for floating-point type casting doesn't include special
cases like NaN, positive infinity, and negative infinity. These are important
edge cases that should be tested to ensure the type casting logic handles them
correctly when converting between Float and Double types.
##########
modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util;
+
+import java.util.EnumSet;
+import java.util.Set;
+import org.apache.ignite.internal.lang.InternalTuple;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Helper methods for reading values from {@link InternalTuple} with allowed
numeric type conversions.
+ *
+ * <p>The following conversions are supported:
+ * <ul>
+ * <li>Any integer type to any other integer type with range checks (e.g.
{@link ColumnType#INT64} to {@link ColumnType#INT8}
+ * may throw {@link ArithmeticException} if the value doesn't fit into
{@link ColumnType#INT8}).</li>
+ * <li>{@link ColumnType#FLOAT} to {@link ColumnType#DOUBLE} are always
allowed.</li>
+ * <li>{@link ColumnType#DOUBLE} to {@link ColumnType#FLOAT} with
precision checks (may throw {@link ArithmeticException}
+ * if the value cannot be represented as FLOAT without precision
loss).</li>
Review Comment:
The javadoc mentions throwing ArithmeticException for narrowing conversions,
but the actual implementation returns false for invalid casts and doesn't throw
any exception. The documentation should be updated to accurately reflect that
the method returns a boolean value indicating whether the cast is allowed.
```suggestion
* is considered invalid (and {@link #isCastAllowed(ColumnType,
ColumnType, Object)} returns {@code false}) if the value
* doesn't fit into {@link ColumnType#INT8}).</li>
* <li>{@link ColumnType#FLOAT} to {@link ColumnType#DOUBLE} are always
allowed.</li>
* <li>{@link ColumnType#DOUBLE} to {@link ColumnType#FLOAT} with
precision checks (the cast is considered invalid, and
* {@link #isCastAllowed(ColumnType, ColumnType, Object)} returns {@code
false}, if the value cannot be represented as
* {@link ColumnType#FLOAT} without precision loss).</li>
```
##########
modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java:
##########
@@ -288,6 +288,165 @@ public static void appendValue(BinaryTupleBuilder
builder, ColumnType type, Stri
}
}
+ 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();
Review Comment:
The error message in appendShortValue is inconsistent with other methods.
While appendByteValue and appendIntValue include the class name in the error
message, this method doesn't. For consistency, consider changing this to:
"Cannot cast to short: " + val.getClass()
```suggestion
throw new ClassCastException("Cannot cast to short: " +
val.getClass());
```
##########
modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java:
##########
@@ -791,6 +807,308 @@ public void bytesTypeMatch(BinTestCase testCase) {
}
}
+ @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, 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.MAX_VALUE));
+
+ 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.MAX_VALUE));
+
+ 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);
+ }
+ }
Review Comment:
The test cases only verify MAX_VALUE boundaries but don't test MIN_VALUE
boundaries for narrowing conversions. For example, when casting Long to Byte,
negative values like Byte.MIN_VALUE should also be tested to ensure the range
checking works correctly for both positive and negative bounds.
##########
modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java:
##########
@@ -288,6 +288,165 @@ public static void appendValue(BinaryTupleBuilder
builder, ColumnType type, Stri
}
}
+ 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) {
Review Comment:
The comparison `doubleVal == floatVal` doesn't handle special floating-point
values correctly. For example, Double.NaN cast to float will produce Float.NaN,
and NaN == NaN is always false, which would incorrectly reject valid NaN
values. Consider using Double.compare() or Float.isNaN() to handle special
cases like NaN and infinity correctly.
```suggestion
if (Double.compare(doubleVal, floatVal) == 0) {
```
##########
modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItKeyValueBinaryViewApiTest.java:
##########
@@ -582,6 +598,302 @@ public void schemaMismatch(TestCase testCase) {
);
}
+ @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, 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.MAX_VALUE));
+
+ 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.MAX_VALUE));
+
+ 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);
+ }
+ }
Review Comment:
The test cases only verify MAX_VALUE boundaries but don't test MIN_VALUE
boundaries for narrowing conversions. For example, when casting Long to Byte,
negative values like Byte.MIN_VALUE should also be tested to ensure the range
checking works correctly for both positive and negative bounds.
##########
modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util;
+
+import java.util.EnumSet;
+import java.util.Set;
+import org.apache.ignite.internal.lang.InternalTuple;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Helper methods for reading values from {@link InternalTuple} with allowed
numeric type conversions.
+ *
+ * <p>The following conversions are supported:
+ * <ul>
+ * <li>Any integer type to any other integer type with range checks (e.g.
{@link ColumnType#INT64} to {@link ColumnType#INT8}
+ * may throw {@link ArithmeticException} if the value doesn't fit into
{@link ColumnType#INT8}).</li>
+ * <li>{@link ColumnType#FLOAT} to {@link ColumnType#DOUBLE} are always
allowed.</li>
+ * <li>{@link ColumnType#DOUBLE} to {@link ColumnType#FLOAT} with
precision checks (may throw {@link ArithmeticException}
+ * if the value cannot be represented as FLOAT without precision
loss).</li>
+ * </ul>
+ */
+public class TupleTypeCastUtils {
+ /** Integer column types. */
+ private static final Set<ColumnType> INT_TYPES =
+ EnumSet.of(ColumnType.INT8, ColumnType.INT16, ColumnType.INT32,
ColumnType.INT64);
+
+ /**
+ * 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();
Review Comment:
The comparison `number.floatValue() == number.doubleValue()` doesn't handle
special floating-point values correctly. For example, Double.NaN will fail the
comparison since NaN == NaN is always false. This would incorrectly reject
valid NaN values when casting from Double to Float. Consider handling special
cases like NaN and infinity explicitly.
```suggestion
case FLOAT: {
double d = number.doubleValue();
float f = number.floatValue();
if (Double.isNaN(d)) {
// NaN is representable as Float.NaN without precision
loss.
return true;
}
// For non-NaN values, keep the existing precision check
logic.
return f == d;
}
```
##########
modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util;
+
+import java.util.EnumSet;
+import java.util.Set;
+import org.apache.ignite.internal.lang.InternalTuple;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Helper methods for reading values from {@link InternalTuple} with allowed
numeric type conversions.
Review Comment:
The javadoc states "Helper methods for reading values" but this utility is
actually used for writing/validating values during tuple insertion. The comment
should be updated to reflect the actual usage, such as "Helper methods for
validating numeric type conversions when writing values to tuples".
```suggestion
* Helper methods for validating numeric type conversions when writing
values to {@link InternalTuple}.
```
##########
modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItRecordBinaryViewApiTest.java:
##########
@@ -791,6 +807,308 @@ public void bytesTypeMatch(BinTestCase testCase) {
}
}
+ @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, 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.MAX_VALUE));
+
+ 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.MAX_VALUE));
+
+ 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) 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, ColumnType.INT32);
+ }
+
+ // 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, 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);
+ }
+ }
Review Comment:
The test coverage for floating-point type casting doesn't include special
cases like NaN, positive infinity, and negative infinity. These are important
edge cases that should be tested to ensure the type casting logic handles them
correctly when converting between Float and Double types.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]