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 24a0555bae989907d83ba553632937f56ba2e4ca
Author: Pavel Pereslegin <[email protected]>
AuthorDate: Tue Jan 20 15:17:24 2026 +0300

    IGNITE-26491 Add support for reading from tuples with allowed type casting 
(#7400)
---
 .../java/org/apache/ignite/table/TupleImpl.java    | 185 ++++++--
 .../org/apache/ignite/table/TupleImplTest.java     |  10 +
 .../ignite/table/AbstractImmutableTupleTest.java   | 511 ++++++++++++++++++++-
 .../table/MutableTupleBinaryTupleAdapter.java      | 133 +++---
 .../requests/table/ClientHandlerTupleTests.java    |  40 +-
 .../internal/client/sql/ClientSqlRowTest.java      | 171 +++++--
 .../ignite/internal/util/TupleTypeCastUtils.java   | 457 ++++++++++++++++++
 .../ignite/internal/schema/SchemaTestUtils.java    |  32 --
 modules/sql-engine/build.gradle                    |   1 +
 .../internal/sql/api/AsyncResultSetImpl.java       |  90 +++-
 .../apache/ignite/internal/sql/api/SqlRowTest.java | 217 +++++++--
 .../internal/table/AbstractRowTupleAdapter.java    |  88 ++--
 12 files changed, 1649 insertions(+), 286 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java 
b/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
index d197f69f27a..86fece7de09 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
@@ -196,73 +196,97 @@ class TupleImpl implements Tuple, Serializable {
     /** {@inheritDoc} */
     @Override
     public byte byteValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToByte(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public byte byteValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToByte(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public short shortValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToShort(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public short shortValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToShort(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public int intValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToInt(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public int intValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToInt(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public long longValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToLong(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public long longValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToLong(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public float floatValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToFloat(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public float floatValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToFloat(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public double doubleValue(String columnName) {
-        return valueNotNull(columnName);
+        Object number = valueNotNull(columnName);
+
+        return castToDouble(number);
     }
 
     /** {@inheritDoc} */
     @Override
     public double doubleValue(int columnIndex) {
-        return valueNotNull(columnIndex);
+        Object number = valueNotNull(columnIndex);
+
+        return castToDouble(number);
     }
 
     /** {@inheritDoc} */
@@ -419,25 +443,6 @@ class TupleImpl implements Tuple, Serializable {
         return (idx == null) ? def : (T) colValues.get(idx);
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public String toString() {
-        // Keep the same as IgniteToStringBuilder.toString().
-        StringBuilder b = new StringBuilder();
-
-        b.append(getClass().getSimpleName()).append(" [");
-        for (int i = 0; i < columnCount(); i++) {
-            if (i > 0) {
-                b.append(", ");
-            }
-            Object value = value(i);
-            b.append(columnName(i)).append('=').append(value);
-        }
-        b.append(']');
-
-        return b.toString();
-    }
-
     private <T> T valueNotNull(int columnIndex) {
         T value = value(columnIndex);
 
@@ -459,4 +464,122 @@ class TupleImpl implements Tuple, Serializable {
 
         return value;
     }
+
+    /** Casts a {@link Number} to {@code byte}. */
+    private static byte castToByte(Object number) {
+        if (number instanceof Byte) {
+            return (byte) number;
+        }
+
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Short) {
+            long longVal = ((Number) number).longValue();
+            byte byteVal = ((Number) number).byteValue();
+
+            if (longVal == byteVal) {
+                return byteVal;
+            }
+
+            throw new ArithmeticException("Byte value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ byte.class);
+    }
+
+    /** Casts a {@link Number} to {@code short}. */
+    private static short castToShort(Object number) {
+        if (number instanceof Short) {
+            return (short) number;
+        }
+
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Byte) {
+            long longVal = ((Number) number).longValue();
+            short shortVal = ((Number) number).shortValue();
+
+            if (longVal == shortVal) {
+                return shortVal;
+            }
+
+            throw new ArithmeticException("Short value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ short.class);
+    }
+
+    /** Casts a {@link Number} to {@code int}. */
+    private static int castToInt(Object number) {
+        if (number instanceof Integer) {
+            return (int) number;
+        }
+
+        if (number instanceof Long || number instanceof Short || number 
instanceof Byte) {
+            long longVal = ((Number) number).longValue();
+            int intVal = ((Number) number).intValue();
+
+            if (longVal == intVal) {
+                return intVal;
+            }
+
+            throw new ArithmeticException("Int value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ int.class);
+    }
+
+    /** Casts a {@link Number} to {@code long}. */
+    private static long castToLong(Object number) {
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Short || number instanceof Byte) {
+            return ((Number) number).longValue();
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ long.class);
+    }
+
+    /** Casts a {@link Number} to {@code float}. */
+    private static float castToFloat(Object number) {
+        if (number instanceof Float) {
+            return (float) number;
+        }
+
+        if (number instanceof Double) {
+            double doubleVal = ((Number) number).doubleValue();
+            float floatVal = ((Number) number).floatValue();
+
+            //noinspection FloatingPointEquality
+            if (doubleVal == floatVal || Double.isNaN(doubleVal)) {
+                return floatVal;
+            }
+
+            throw new ArithmeticException("Float value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ float.class);
+    }
+
+    /** Casts a {@link Number} to {@code double}. */
+    private static double castToDouble(Object number) {
+        if (number instanceof Double || number instanceof Float) {
+            return ((Number) number).doubleValue();
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ double.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        // Keep the same as IgniteToStringBuilder.toString().
+        StringBuilder b = new StringBuilder();
+
+        b.append(getClass().getSimpleName()).append(" [");
+        for (int i = 0; i < columnCount(); i++) {
+            if (i > 0) {
+                b.append(", ");
+            }
+            Object value = value(i);
+            b.append(columnName(i)).append('=').append(value);
+        }
+        b.append(']');
+
+        return b.toString();
+    }
 }
diff --git 
a/modules/api/src/test/java/org/apache/ignite/table/TupleImplTest.java 
b/modules/api/src/test/java/org/apache/ignite/table/TupleImplTest.java
index eda4b5076f5..dafaa832fe7 100644
--- a/modules/api/src/test/java/org/apache/ignite/table/TupleImplTest.java
+++ b/modules/api/src/test/java/org/apache/ignite/table/TupleImplTest.java
@@ -22,7 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import java.util.Map;
 import java.util.function.Function;
 import org.apache.ignite.sql.ColumnType;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
 
 /**
  * Tests server tuple builder implementation.
@@ -91,4 +93,12 @@ public class TupleImplTest extends AbstractMutableTupleTest {
         // must be found by non normalized name, regular method does 
normalization
         assertEquals("non-normalized", tuple.valueOrDefault("\"Name\"", 
"default"));
     }
+
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-27577";)
+    @ParameterizedTest
+    @Override
+    @SuppressWarnings("JUnitMalformedDeclaration")
+    public void allTypesUnsupportedConversion(ColumnType from, ColumnType to) {
+        super.allTypesUnsupportedConversion(from, to);
+    }
 }
diff --git 
a/modules/api/src/testFixtures/java/org/apache/ignite/table/AbstractImmutableTupleTest.java
 
b/modules/api/src/testFixtures/java/org/apache/ignite/table/AbstractImmutableTupleTest.java
index a3fb7e0e4eb..4904d0cba34 100644
--- 
a/modules/api/src/testFixtures/java/org/apache/ignite/table/AbstractImmutableTupleTest.java
+++ 
b/modules/api/src/testFixtures/java/org/apache/ignite/table/AbstractImmutableTupleTest.java
@@ -18,6 +18,10 @@
 package org.apache.ignite.table;
 
 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -38,7 +42,10 @@ import java.time.Year;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.temporal.Temporal;
+import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
@@ -334,6 +341,386 @@ public abstract class AbstractImmutableTupleTest {
         assertEquals(String.format(NULL_TO_PRIMITIVE_NAMED_ERROR_MESSAGE, 
"VAL"), err.getMessage());
     }
 
+    @Test
+    void testReadAsByte() {
+        Tuple tuple = createTupleOfSingleColumn(ColumnType.INT8, "INT8", 
Byte.MAX_VALUE);
+
+        assertThat(tuple.byteValue("INT8"), is(Byte.MAX_VALUE));
+        assertThat(tuple.shortValue("INT8"), is((short) Byte.MAX_VALUE));
+        assertThat(tuple.intValue("INT8"), is((int) Byte.MAX_VALUE));
+        assertThat(tuple.longValue("INT8"), is((long) Byte.MAX_VALUE));
+
+        assertThat(tuple.byteValue(0), is(Byte.MAX_VALUE));
+        assertThat(tuple.shortValue(0), is((short) Byte.MAX_VALUE));
+        assertThat(tuple.intValue(0), is((int) Byte.MAX_VALUE));
+        assertThat(tuple.longValue(0), is((long) Byte.MAX_VALUE));
+    }
+
+    @Test
+    void testReadAsShort() {
+        // The field value is within the byte range
+        {
+            String columnName = "INT16";
+            short value = Byte.MAX_VALUE;
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT16, 
columnName, value);
+
+            assertThat(tuple.byteValue(columnName), is((byte) value));
+            assertThat(tuple.shortValue(columnName), is(value));
+            assertThat(tuple.intValue(columnName), is((int) value));
+            assertThat(tuple.longValue(columnName), is((long) value));
+
+            assertThat(tuple.byteValue(0), is((byte) value));
+            assertThat(tuple.shortValue(0), is(value));
+            assertThat(tuple.intValue(0), is((int) value));
+            assertThat(tuple.longValue(0), is((long) value));
+        }
+
+        // The field value is out of the byte range.
+        {
+            String columnName = "INT16";
+            short value = Byte.MAX_VALUE + 1;
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT16, 
columnName, value);
+
+            ArithmeticException ex0 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(columnName));
+            assertThat(ex0.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(columnName), is(value));
+            assertThat(tuple.intValue(columnName), is((int) value));
+            assertThat(tuple.longValue(columnName), is((long) value));
+
+            ArithmeticException ex1 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(0));
+            assertThat(ex1.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(0), is(value));
+            assertThat(tuple.intValue(0), is((int) value));
+            assertThat(tuple.longValue(0), is((long) value));
+        }
+    }
+
+    @Test
+    void testReadAsInt() {
+        {
+            int value = Byte.MAX_VALUE;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT32, 
columnName, value);
+
+            assertThat(tuple.byteValue(columnName), is((byte) value));
+            assertThat(tuple.shortValue(columnName), is((short) value));
+            assertThat(tuple.intValue(columnName), is(value));
+            assertThat(tuple.longValue(columnName), is((long) value));
+
+            assertThat(tuple.byteValue(0), is((byte) value));
+            assertThat(tuple.shortValue(0), is((short) value));
+            assertThat(tuple.intValue(0), is(value));
+            assertThat(tuple.longValue(0), is((long) value));
+        }
+
+        {
+            int value = Byte.MAX_VALUE + 1;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT32, 
columnName, value);
+
+            ArithmeticException ex0 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(columnName));
+            assertThat(ex0.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(columnName), is((short) value));
+            assertThat(tuple.intValue(columnName), is(value));
+            assertThat(tuple.longValue(columnName), is((long) value));
+
+            ArithmeticException ex1 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(0));
+            assertThat(ex1.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(0), is((short) value));
+            assertThat(tuple.intValue(0), is(value));
+            assertThat(tuple.longValue(0), is((long) value));
+        }
+
+        {
+            int value = Short.MAX_VALUE + 1;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT32, 
columnName, value);
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            assertThat(tuple.intValue(columnName), is(value));
+            assertThat(tuple.longValue(columnName), is((long) value));
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(0));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(0));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            assertThat(tuple.intValue(0), is(value));
+            assertThat(tuple.longValue(0), is((long) value));
+        }
+    }
+
+    @Test
+    void testReadAsLong() {
+        {
+            long value = Byte.MAX_VALUE;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT64, 
columnName, value);
+
+            assertThat(tuple.byteValue(columnName), is((byte) value));
+            assertThat(tuple.shortValue(columnName), is((short) value));
+            assertThat(tuple.intValue(columnName), is((int) value));
+            assertThat(tuple.longValue(columnName), is(value));
+
+            assertThat(tuple.byteValue(0), is((byte) value));
+            assertThat(tuple.shortValue(0), is((short) value));
+            assertThat(tuple.intValue(0), is((int) value));
+            assertThat(tuple.longValue(0), is(value));
+        }
+
+        {
+            long value = Byte.MAX_VALUE + 1;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT64, 
columnName, value);
+
+            ArithmeticException ex0 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(columnName));
+            assertThat(ex0.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(columnName), is((short) value));
+            assertThat(tuple.intValue(columnName), is((int) value));
+            assertThat(tuple.longValue(columnName), is(value));
+
+            ArithmeticException ex1 = assertThrows(ArithmeticException.class, 
() -> tuple.byteValue(0));
+            assertThat(ex1.getMessage(), equalTo("Byte value overflow: " + 
value));
+
+            assertThat(tuple.shortValue(0), is((short) value));
+            assertThat(tuple.intValue(0), is((int) value));
+            assertThat(tuple.longValue(0), is(value));
+        }
+
+        {
+            long value = Short.MAX_VALUE + 1;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT64, 
columnName, value);
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            assertThat(tuple.intValue(columnName), is((int) value));
+            assertThat(tuple.longValue(columnName), is(value));
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(0));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(0));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            assertThat(tuple.intValue(0), is((int) value));
+            assertThat(tuple.longValue(0), is(value));
+        }
+
+        {
+            long value = Integer.MAX_VALUE + 1L;
+            String columnName = "VALUE";
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.INT64, 
columnName, value);
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.intValue(columnName));
+                assertThat(ex.getMessage(), equalTo("Int value overflow: " + 
value));
+            }
+
+            assertThat(tuple.longValue(columnName), is(value));
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.byteValue(0));
+                assertThat(ex.getMessage(), equalTo("Byte value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.shortValue(0));
+                assertThat(ex.getMessage(), equalTo("Short value overflow: " + 
value));
+            }
+
+            {
+                ArithmeticException ex = 
assertThrows(ArithmeticException.class, () -> tuple.intValue(0));
+                assertThat(ex.getMessage(), equalTo("Int value overflow: " + 
value));
+            }
+
+            assertThat(tuple.longValue(0), is(value));
+        }
+    }
+
+    @Test
+    void testReadAsFloat() {
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.FLOAT, "FLOAT", 
Float.MAX_VALUE);
+
+            assertThat(tuple.floatValue("FLOAT"), is(Float.MAX_VALUE));
+            assertThat(tuple.doubleValue("FLOAT"), is((double) 
Float.MAX_VALUE));
+
+            assertThat(tuple.floatValue(0), is(Float.MAX_VALUE));
+            assertThat(tuple.doubleValue(0), is((double) Float.MAX_VALUE));
+        }
+
+        // NaN
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.FLOAT, "FLOAT", 
Float.NaN);
+
+            assertThat(Float.isNaN(tuple.floatValue("FLOAT")), is(true));
+            assertThat(Double.isNaN(tuple.doubleValue("FLOAT")), is(true));
+
+            assertThat(Float.isNaN(tuple.floatValue(0)), is(true));
+            assertThat(Double.isNaN(tuple.doubleValue(0)), is(true));
+        }
+
+        // Positive infinity
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.FLOAT, "FLOAT", 
Float.POSITIVE_INFINITY);
+
+            assertThat(tuple.floatValue("FLOAT"), is(Float.POSITIVE_INFINITY));
+            assertThat(tuple.doubleValue("FLOAT"), 
is(Double.POSITIVE_INFINITY));
+
+            assertThat(tuple.floatValue(0), is(Float.POSITIVE_INFINITY));
+            assertThat(tuple.doubleValue(0), is(Double.POSITIVE_INFINITY));
+        }
+
+        // Negative infinity
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.FLOAT, "FLOAT", 
Float.NEGATIVE_INFINITY);
+
+            assertThat(tuple.floatValue("FLOAT"), is(Float.NEGATIVE_INFINITY));
+            assertThat(tuple.doubleValue("FLOAT"), 
is(Double.NEGATIVE_INFINITY));
+
+            assertThat(tuple.floatValue(0), is(Float.NEGATIVE_INFINITY));
+            assertThat(tuple.doubleValue(0), is(Double.NEGATIVE_INFINITY));
+        }
+    }
+
+    @Test
+    void testReadAsDouble() {
+        String columnName = "DOUBLE";
+
+        // The field value can be represented as float.
+        {
+            double value = Float.MAX_VALUE;
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.DOUBLE, 
columnName, value);
+
+            assertThat(tuple.floatValue(columnName), is((float) value));
+            assertThat(tuple.floatValue(0), is((float) value));
+
+            assertThat(tuple.doubleValue(columnName), is(value));
+            assertThat(tuple.doubleValue(0), is(value));
+        }
+
+        // The field value cannot be represented as float.
+        {
+            double value = Double.MAX_VALUE;
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.DOUBLE, 
columnName, value);
+
+            ArithmeticException ex0 = assertThrows(ArithmeticException.class, 
() -> tuple.floatValue(columnName));
+            assertThat(ex0.getMessage(), equalTo("Float value overflow: " + 
value));
+
+            ArithmeticException ex1 = assertThrows(ArithmeticException.class, 
() -> tuple.floatValue(0));
+            assertThat(ex1.getMessage(), equalTo("Float value overflow: " + 
value));
+
+            assertThat(tuple.doubleValue(columnName), is(value));
+            assertThat(tuple.doubleValue(0), is(value));
+        }
+
+        // NaN
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.DOUBLE, 
columnName, Double.NaN);
+
+            assertThat(Float.isNaN(tuple.floatValue(columnName)), is(true));
+            assertThat(Double.isNaN(tuple.doubleValue(columnName)), is(true));
+
+            assertThat(Float.isNaN(tuple.floatValue(0)), is(true));
+            assertThat(Double.isNaN(tuple.doubleValue(0)), is(true));
+        }
+
+        // Positive infinity
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.DOUBLE, 
columnName, Double.POSITIVE_INFINITY);
+
+            assertThat(tuple.floatValue(columnName), 
is(Float.POSITIVE_INFINITY));
+            assertThat(tuple.doubleValue(columnName), 
is(Double.POSITIVE_INFINITY));
+
+            assertThat(tuple.floatValue(0), is(Float.POSITIVE_INFINITY));
+            assertThat(tuple.doubleValue(0), is(Double.POSITIVE_INFINITY));
+        }
+
+        // Negative infinity
+        {
+            Tuple tuple = createTupleOfSingleColumn(ColumnType.DOUBLE, 
columnName, Double.NEGATIVE_INFINITY);
+
+            assertThat(tuple.floatValue(columnName), 
is(Float.NEGATIVE_INFINITY));
+            assertThat(tuple.doubleValue(columnName), 
is(Double.NEGATIVE_INFINITY));
+
+            assertThat(tuple.floatValue(0), is(Float.NEGATIVE_INFINITY));
+            assertThat(tuple.doubleValue(0), is(Double.NEGATIVE_INFINITY));
+        }
+    }
+
+    @ParameterizedTest(name = "{0} -> {1}")
+    @MethodSource("allTypesUnsupportedConversionArgs")
+    public void allTypesUnsupportedConversion(ColumnType from, ColumnType to) {
+        Object value = generateMaxValue(from);
+        String columnName = "VALUE";
+        Tuple tuple = createTupleOfSingleColumn(from, columnName, value);
+
+        {
+            ClassCastException ex = assertThrows(ClassCastException.class, () 
-> readValue(tuple, to, columnName, null));
+            String template = "Column with name '%s' has type %s but %s was 
requested";
+            assertThat(ex.getMessage(), containsString(String.format(template, 
columnName, from.name(), to.name())));
+        }
+
+        {
+            ClassCastException ex = assertThrows(ClassCastException.class, () 
-> readValue(tuple, to, null, 0));
+            String template = "Column with index %d has type %s but %s was 
requested";
+            assertThat(ex.getMessage(), containsString(String.format(template, 
0, from.name(), to.name())));
+        }
+    }
+
+    @ParameterizedTest(name = "{0} -> {1}")
+    @MethodSource("allTypesDowncastOverflowArgs")
+    public void allTypesDowncastOverflow(ColumnType from, ColumnType to) {
+        Object value = generateMaxValue(from);
+        String columnName = "VALUE";
+        Tuple tuple = createTupleOfSingleColumn(from, columnName, value);
+
+        assertThrows(ArithmeticException.class, () -> readValue(tuple, to, 
columnName, null));
+        assertThrows(ArithmeticException.class, () -> readValue(tuple, to, 
null, 0));
+    }
+
     /**
      * Adds sample values for columns of default schema: id (long), simpleName 
(string), "QuotedName" (string), noValue (null).
      *
@@ -417,7 +804,7 @@ public abstract class AbstractImmutableTupleTest {
         return new BigInteger("10").pow(NANOS_IN_SECOND - 
precision).intValue();
     }
 
-    private static @Nullable Object generateValue(Random rnd, ColumnType type) 
{
+    protected @Nullable Object generateValue(Random rnd, ColumnType type) {
         switch (type) {
             case NULL:
                 return null;
@@ -475,6 +862,25 @@ public abstract class AbstractImmutableTupleTest {
         }
     }
 
+    private Object generateMaxValue(ColumnType type) {
+        switch (type) {
+            case INT8:
+                return Byte.MAX_VALUE;
+            case INT16:
+                return Short.MAX_VALUE;
+            case INT32:
+                return Integer.MAX_VALUE;
+            case INT64:
+                return Long.MAX_VALUE;
+            case FLOAT:
+                return Float.MAX_VALUE;
+            case DOUBLE:
+                return Double.MAX_VALUE;
+            default:
+                return 
Objects.requireNonNull(generateValue(ThreadLocalRandom.current(), type));
+        }
+    }
+
     private static Instant generateInstant(Random rnd) {
         long minTs = LocalDateTime.of(LocalDate.of(1, 1, 1), LocalTime.MIN)
                 
.minusSeconds(ZoneOffset.MIN.getTotalSeconds()).toInstant(ZoneOffset.UTC).toEpochMilli();
@@ -507,4 +913,107 @@ public abstract class AbstractImmutableTupleTest {
                 Arguments.of(ColumnType.DOUBLE, (BiConsumer<Tuple, String>) 
Tuple::doubleValue)
         );
     }
+
+    private static Object readValue(Tuple tuple, ColumnType type, @Nullable 
String colName, @Nullable Integer index) {
+        assert colName == null ^ index == null;
+
+        switch (type) {
+            case TIME:
+                return colName == null ? tuple.timeValue(index) : 
tuple.timeValue(colName);
+            case TIMESTAMP:
+                return colName == null ? tuple.timestampValue(index) : 
tuple.timestampValue(colName);
+            case DATE:
+                return colName == null ? tuple.dateValue(index) : 
tuple.dateValue(colName);
+            case DATETIME:
+                return colName == null ? tuple.datetimeValue(index) : 
tuple.datetimeValue(colName);
+            case INT8:
+                return colName == null ? tuple.byteValue(index) : 
tuple.byteValue(colName);
+            case INT16:
+                return colName == null ? tuple.shortValue(index) : 
tuple.shortValue(colName);
+            case INT32:
+                return colName == null ? tuple.intValue(index) : 
tuple.intValue(colName);
+            case INT64:
+                return colName == null ? tuple.longValue(index) : 
tuple.longValue(colName);
+            case FLOAT:
+                return colName == null ? tuple.floatValue(index) : 
tuple.floatValue(colName);
+            case DOUBLE:
+                return colName == null ? tuple.doubleValue(index) : 
tuple.doubleValue(colName);
+            case UUID:
+                return colName == null ? tuple.uuidValue(index) : 
tuple.uuidValue(colName);
+            case STRING:
+                return colName == null ? tuple.stringValue(index) : 
tuple.stringValue(colName);
+            case BOOLEAN:
+                return colName == null ? tuple.booleanValue(index) : 
tuple.booleanValue(colName);
+            case DECIMAL:
+                return colName == null ? tuple.decimalValue(index) : 
tuple.decimalValue(colName);
+            case BYTE_ARRAY:
+                return colName == null ? tuple.bytesValue(index) : 
tuple.bytesValue(colName);
+            default:
+                throw new UnsupportedOperationException("Unexpected type: " + 
type);
+        }
+    }
+
+    private static List<Arguments> allTypesUnsupportedConversionArgs() {
+        EnumSet<ColumnType> allTypes = EnumSet.complementOf(
+                EnumSet.of(ColumnType.NULL, ColumnType.STRUCT, 
ColumnType.PERIOD, ColumnType.DURATION));
+        List<Arguments> arguments = new ArrayList<>();
+
+        for (ColumnType from : allTypes) {
+            for (ColumnType to : allTypes) {
+                if (from == to || isSupportedUpcast(from, to) || 
isSupportedDowncast(from, to)) {
+                    continue;
+                }
+
+                arguments.add(Arguments.of(from, to));
+            }
+        }
+
+        return arguments;
+    }
+
+    private static List<Arguments> allTypesDowncastOverflowArgs() {
+        EnumSet<ColumnType> allTypes = EnumSet.complementOf(
+                EnumSet.of(ColumnType.NULL, ColumnType.STRUCT, 
ColumnType.PERIOD, ColumnType.DURATION));
+        List<Arguments> arguments = new ArrayList<>();
+
+        for (ColumnType from : allTypes) {
+            for (ColumnType to : allTypes) {
+                if (isSupportedDowncast(from, to)) {
+                    arguments.add(Arguments.of(from, to));
+                }
+            }
+        }
+
+        return arguments;
+    }
+
+    private static boolean isSupportedUpcast(ColumnType source, ColumnType 
target) {
+        switch (source) {
+            case INT8:
+                return target == ColumnType.INT16 || target == 
ColumnType.INT32 || target == ColumnType.INT64;
+            case INT16:
+                return target == ColumnType.INT32 || target == 
ColumnType.INT64;
+            case INT32:
+                return target == ColumnType.INT64;
+            case FLOAT:
+                return target == ColumnType.DOUBLE;
+            default:
+                return false;
+        }
+    }
+
+    private static boolean isSupportedDowncast(ColumnType source, ColumnType 
target) {
+        switch (source) {
+            case INT64:
+                return target == ColumnType.INT8 || target == ColumnType.INT16 
|| target == ColumnType.INT32;
+            case INT32:
+                return target == ColumnType.INT8 || target == ColumnType.INT16;
+            case INT16:
+                return target == ColumnType.INT8;
+            case DOUBLE:
+                return target == ColumnType.FLOAT;
+            default:
+                return false;
+        }
+    }
 }
diff --git 
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/table/MutableTupleBinaryTupleAdapter.java
 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/table/MutableTupleBinaryTupleAdapter.java
index a9804e793a3..790c973926e 100644
--- 
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/table/MutableTupleBinaryTupleAdapter.java
+++ 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/table/MutableTupleBinaryTupleAdapter.java
@@ -17,6 +17,13 @@
 
 package org.apache.ignite.internal.client.table;
 
+import static org.apache.ignite.internal.util.TupleTypeCastUtils.readByteValue;
+import static 
org.apache.ignite.internal.util.TupleTypeCastUtils.readDoubleValue;
+import static 
org.apache.ignite.internal.util.TupleTypeCastUtils.readFloatValue;
+import static org.apache.ignite.internal.util.TupleTypeCastUtils.readIntValue;
+import static org.apache.ignite.internal.util.TupleTypeCastUtils.readLongValue;
+import static 
org.apache.ignite.internal.util.TupleTypeCastUtils.readShortValue;
+
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -97,7 +104,7 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.columnIndex(columnName);
         }
 
-        int binaryTupleIndex = binaryTupleIndex(columnName, null);
+        int binaryTupleIndex = binaryTupleIndex(columnName);
 
         return binaryTupleIndex < 0 ? -1 : publicIndex(binaryTupleIndex);
     }
@@ -109,7 +116,7 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.valueOrDefault(columnName, defaultValue);
         }
 
-        int binaryTupleIndex = binaryTupleIndex(columnName, null);
+        int binaryTupleIndex = binaryTupleIndex(columnName);
 
         return binaryTupleIndex < 0
                 || publicIndex(binaryTupleIndex) < 0
@@ -125,7 +132,7 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.value(columnName);
         }
 
-        int binaryTupleIndex = binaryTupleIndex(columnName, null);
+        int binaryTupleIndex = binaryTupleIndex(columnName);
 
         if (binaryTupleIndex < 0 || publicIndex(binaryTupleIndex) < 0) {
             throw new IllegalArgumentException("Column doesn't exist [name=" + 
columnName + ']');
@@ -182,11 +189,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.byteValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.INT8);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+        int binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.byteValue(binaryTupleIndex);
+        return readByteValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -196,11 +202,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.byteValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.INT8);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.byteValue(binaryTupleIndex);
+        return readByteValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -210,11 +216,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.shortValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.INT16);
+        var binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
-
-        return binaryTuple.shortValue(binaryTupleIndex);
+        return readShortValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -224,11 +229,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.shortValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.INT16);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.shortValue(binaryTupleIndex);
+        return readShortValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -238,11 +243,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.intValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.INT32);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+        var binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.intValue(binaryTupleIndex);
+        return readIntValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -252,11 +256,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.intValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.INT32);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.intValue(binaryTupleIndex);
+        return readIntValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -266,11 +270,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.longValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.INT64);
+        var binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
-
-        return binaryTuple.longValue(binaryTupleIndex);
+        return readLongValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -280,11 +283,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.longValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.INT64);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.longValue(binaryTupleIndex);
+        return readLongValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -294,11 +297,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.floatValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.FLOAT);
+        var binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
-
-        return binaryTuple.floatValue(binaryTupleIndex);
+        return readFloatValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -308,11 +310,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.floatValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.FLOAT);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.floatValue(binaryTupleIndex);
+        return readFloatValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -322,11 +324,10 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.doubleValue(columnName);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnName, 
ColumnType.DOUBLE);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+        var binaryTupleIndex = resolveBinaryTupleIndexByColumnName(columnName);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.doubleValue(binaryTupleIndex);
+        return readDoubleValue(binaryTuple, binaryTupleIndex, actualType, 
columnName);
     }
 
     /** {@inheritDoc} */
@@ -336,11 +337,11 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             return tuple.doubleValue(columnIndex);
         }
 
-        int binaryTupleIndex = validateSchemaColumnType(columnIndex, 
ColumnType.DOUBLE);
-
-        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+        Objects.checkIndex(columnIndex, columnCount);
+        int binaryTupleIndex = binaryTupleIndex(columnIndex);
+        ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
-        return binaryTuple.doubleValue(binaryTupleIndex);
+        return readDoubleValue(binaryTuple, binaryTupleIndex, actualType, 
columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -539,15 +540,15 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
         return publicIndex;
     }
 
-    private int binaryTupleIndex(String columnName, @Nullable ColumnType type) 
{
-        var binaryTupleIndex = binaryTupleIndex(columnName);
+    private int validateSchemaColumnType(String columnName, ColumnType type) {
+        var index = binaryTupleIndex(columnName);
 
-        if (binaryTupleIndex < 0) {
-            return binaryTupleIndex;
+        if (index < 0) {
+            throw new IllegalArgumentException("Column doesn't exist [name=" + 
columnName + ']');
         }
 
         if (type != null) {
-            ColumnType actualType = schemaColumnType(binaryTupleIndex);
+            ColumnType actualType = schemaColumnType(index);
 
             if (type != actualType) {
                 throw new ClassCastException("Column with name '" + columnName 
+ "' has type " + actualType
@@ -555,16 +556,6 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
             }
         }
 
-        return binaryTupleIndex;
-    }
-
-    private int validateSchemaColumnType(String columnName, ColumnType type) {
-        var index = binaryTupleIndex(columnName, type);
-
-        if (index < 0) {
-            throw new IllegalArgumentException("Column doesn't exist [name=" + 
columnName + ']');
-        }
-
         return index;
     }
 
@@ -652,6 +643,16 @@ public abstract class MutableTupleBinaryTupleAdapter 
implements Tuple, BinaryTup
         }
     }
 
+    private int resolveBinaryTupleIndexByColumnName(String columnName) {
+        int binaryTupleIndex = binaryTupleIndex(columnName);
+
+        if (binaryTupleIndex < 0) {
+            throw new IllegalArgumentException("Column doesn't exist [name=" + 
columnName + ']');
+        }
+
+        return binaryTupleIndex;
+    }
+
     @Override
     public String toString() {
         return S.tupleToString(this);
diff --git 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/table/ClientHandlerTupleTests.java
 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/table/ClientHandlerTupleTests.java
index abf338e5dbe..0293df2c50a 100644
--- 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/table/ClientHandlerTupleTests.java
+++ 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/table/ClientHandlerTupleTests.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.client.handler.requests.table;
 
+import static org.apache.ignite.internal.type.NativeTypes.BOOLEAN;
 import static org.apache.ignite.internal.type.NativeTypes.BYTES;
 import static org.apache.ignite.internal.type.NativeTypes.DOUBLE;
 import static org.apache.ignite.internal.type.NativeTypes.FLOAT;
@@ -37,9 +38,12 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.Month;
+import java.util.IdentityHashMap;
 import java.util.Random;
 import java.util.UUID;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
 import java.util.stream.Stream;
 import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.lang.IgniteStringFormatter;
@@ -77,6 +81,7 @@ public class ClientHandlerTupleTests {
     private final SchemaDescriptor fullSchema = new SchemaDescriptor(42,
             new Column[]{new Column("keyUuidCol".toUpperCase(), 
NativeTypes.UUID, false)},
             new Column[]{
+                    new Column("valBooleanCol".toUpperCase(), BOOLEAN, true),
                     new Column("valByteCol".toUpperCase(), INT8, true),
                     new Column("valShortCol".toUpperCase(), INT16, true),
                     new Column("valIntCol".toUpperCase(), INT32, true),
@@ -136,8 +141,9 @@ public class ClientHandlerTupleTests {
     public void testValueReturnsValueByIndex() {
         Tuple tuple = createTuple();
 
-        assertEquals(1, (byte) tuple.value(0));
-        assertEquals(4L, tuple.longValue(3));
+        assertEquals(true, tuple.value(0));
+        assertEquals(1, (byte) tuple.value(1));
+        assertEquals(4L, tuple.longValue(4));
 
         assertThrows(IndexOutOfBoundsException.class, () -> tuple.value(123));
     }
@@ -153,7 +159,7 @@ public class ClientHandlerTupleTests {
 
     @Test
     public void testColumnCount() {
-        assertEquals(14, createTuple().columnCount());
+        assertEquals(15, createTuple().columnCount());
         assertEquals(1, createKeyTuple().columnCount());
     }
 
@@ -161,8 +167,9 @@ public class ClientHandlerTupleTests {
     public void testColumnIndex() {
         Tuple tuple = createTuple();
 
-        assertEquals(0, tuple.columnIndex("valByteCol"));
-        assertEquals(3, tuple.columnIndex("valLongCol"));
+        assertEquals(0, tuple.columnIndex("valBooleanCol"));
+        assertEquals(1, tuple.columnIndex("valByteCol"));
+        assertEquals(4, tuple.columnIndex("valLongCol"));
         assertEquals(-1, tuple.columnIndex("bad_name"));
     }
 
@@ -218,6 +225,7 @@ public class ClientHandlerTupleTests {
         Random rnd = new Random();
 
         return Tuple.create()
+                .set("valBooleanCol", true)
                 .set("valByteCol", (byte) 1)
                 .set("valShortCol", (short) 2)
                 .set("valIntCol", 3)
@@ -235,7 +243,27 @@ public class ClientHandlerTupleTests {
     }
 
     private static Stream<Arguments> primitiveAccessors() {
-        return SchemaTestUtils.PRIMITIVE_ACCESSORS.entrySet().stream()
+        IdentityHashMap<NativeType, BiConsumer<Tuple, Object>> map = new 
IdentityHashMap<>();
+
+        map.put(BOOLEAN, (tuple, index) -> invoke(index, tuple::booleanValue, 
tuple::booleanValue));
+        map.put(INT8, (tuple, index) -> invoke(index, tuple::byteValue, 
tuple::byteValue));
+        map.put(INT16, (tuple, index) -> invoke(index, tuple::shortValue, 
tuple::shortValue));
+        map.put(INT32, (tuple, index) -> invoke(index, tuple::intValue, 
tuple::intValue));
+        map.put(INT64, (tuple, index) -> invoke(index, tuple::longValue, 
tuple::longValue));
+        map.put(FLOAT, (tuple, index) -> invoke(index, tuple::floatValue, 
tuple::floatValue));
+        map.put(DOUBLE, (tuple, index) -> invoke(index, tuple::doubleValue, 
tuple::doubleValue));
+
+        return map.entrySet().stream()
                 .map(e -> Arguments.of(e.getKey(), e.getValue()));
     }
+
+    private static void invoke(Object index, IntConsumer intConsumer, 
Consumer<String> strConsumer) {
+        if (index instanceof Integer) {
+            intConsumer.accept((int) index);
+        } else {
+            assert index instanceof String : index.getClass();
+
+            strConsumer.accept((String) index);
+        }
+    }
 }
diff --git 
a/modules/client/src/test/java/org/apache/ignite/internal/client/sql/ClientSqlRowTest.java
 
b/modules/client/src/test/java/org/apache/ignite/internal/client/sql/ClientSqlRowTest.java
index 3d8f6a10e83..bef512ca671 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/internal/client/sql/ClientSqlRowTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/internal/client/sql/ClientSqlRowTest.java
@@ -17,69 +17,106 @@
 
 package org.apache.ignite.internal.client.sql;
 
+import static org.apache.ignite.internal.type.NativeTypes.BOOLEAN;
+import static org.apache.ignite.internal.type.NativeTypes.BYTES;
+import static org.apache.ignite.internal.type.NativeTypes.DATE;
+import static org.apache.ignite.internal.type.NativeTypes.DOUBLE;
+import static org.apache.ignite.internal.type.NativeTypes.FLOAT;
+import static org.apache.ignite.internal.type.NativeTypes.INT16;
 import static org.apache.ignite.internal.type.NativeTypes.INT32;
+import static org.apache.ignite.internal.type.NativeTypes.INT64;
+import static org.apache.ignite.internal.type.NativeTypes.INT8;
+import static org.apache.ignite.internal.type.NativeTypes.STRING;
+import static org.apache.ignite.internal.type.NativeTypes.datetime;
+import static org.apache.ignite.internal.type.NativeTypes.time;
+import static org.apache.ignite.internal.type.NativeTypes.timestamp;
 
+import java.util.ArrayList;
 import java.util.List;
-import java.util.function.BiConsumer;
-import java.util.stream.Stream;
+import java.util.function.Function;
+import org.apache.ignite.client.handler.requests.table.ClientTableCommon;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
-import org.apache.ignite.internal.lang.IgniteStringFormatter;
 import org.apache.ignite.internal.schema.BinaryRow;
+import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaTestUtils;
 import org.apache.ignite.internal.sql.ColumnMetadataImpl;
 import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
-import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
-import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.type.NativeType;
-import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.lang.util.IgniteNameUtils;
+import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.table.AbstractImmutableTupleTest;
 import org.apache.ignite.table.Tuple;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
 
 /**
- * Ensures that reading a {@code null} value as primitive from the
- * {@link ClientSqlRow} instance produces a {@link NullPointerException}.
+ * Tests {@link ClientSqlRow} tuple implementation.
  */
-public class ClientSqlRowTest extends BaseIgniteAbstractTest {
-    @ParameterizedTest(name = "{0}")
-    @MethodSource("primitiveAccessors")
-    @SuppressWarnings("ThrowableNotThrown")
-    void nullPointerWhenReadingNullAsPrimitive(
-            NativeType type,
-            BiConsumer<Tuple, Object> fieldAccessor
-    ) {
-        String valueColumn = "VAL";
-        Tuple row = createRow(valueColumn, type);
-
-        IgniteTestUtils.assertThrows(
-                NullPointerException.class,
-                () -> fieldAccessor.accept(row, 1),
-                
IgniteStringFormatter.format(IgniteUtils.NULL_TO_PRIMITIVE_ERROR_MESSAGE, 1)
-        );
+public class ClientSqlRowTest extends AbstractImmutableTupleTest {
+    /** Schema descriptor for default test tuple. */
+    private final SchemaDescriptor schema = new SchemaDescriptor(
+            42,
+            List.of(
+                    new Column("ID", INT64, false),
+                    new Column("SIMPLENAME", STRING, true),
+                    new Column("QuotedName", STRING, true),
+                    new Column("NOVALUE", STRING, true)
+            ),
+            List.of("ID"),
+            null
+    );
 
-        IgniteTestUtils.assertThrows(
-                NullPointerException.class,
-                () -> fieldAccessor.accept(row, valueColumn),
-                
IgniteStringFormatter.format(IgniteUtils.NULL_TO_PRIMITIVE_NAMED_ERROR_MESSAGE, 
valueColumn)
-        );
+    /** Schema descriptor for tuple with columns of all the supported types. */
+    private final SchemaDescriptor fullSchema = new SchemaDescriptor(42,
+            List.of(
+                    new Column("valBoolCol".toUpperCase(), BOOLEAN, true),
+                    new Column("valByteCol".toUpperCase(), INT8, true),
+                    new Column("valShortCol".toUpperCase(), INT16, true),
+                    new Column("valIntCol".toUpperCase(), INT32, true),
+                    new Column("valLongCol".toUpperCase(), INT64, true),
+                    new Column("valFloatCol".toUpperCase(), FLOAT, true),
+                    new Column("valDoubleCol".toUpperCase(), DOUBLE, true),
+                    new Column("valDateCol".toUpperCase(), DATE, true),
+                    new Column("keyUuidCol".toUpperCase(), NativeTypes.UUID, 
false),
+                    new Column("valUuidCol".toUpperCase(), NativeTypes.UUID, 
false),
+                    new Column("valTimeCol".toUpperCase(), 
time(TIME_PRECISION), true),
+                    new Column("valDateTimeCol".toUpperCase(), 
datetime(TIMESTAMP_PRECISION), true),
+                    new Column("valTimeStampCol".toUpperCase(), 
timestamp(TIMESTAMP_PRECISION), true),
+                    new Column("valBytesCol".toUpperCase(), BYTES, false),
+                    new Column("valStringCol".toUpperCase(), STRING, false),
+                    new Column("valDecimalCol".toUpperCase(), 
NativeTypes.decimalOf(25, 5), false)
+            ),
+            List.of("keyUuidCol".toUpperCase()),
+            null
+    );
 
-        IgniteTestUtils.assertThrows(
-                UnsupportedOperationException.class,
-                () -> row.set("NEW", null),
-                null
-        );
+    @Test
+    @Override
+    public void testSerialization() {
+        Assumptions.abort(ClientSqlRow.class.getSimpleName() + " is not 
serializable.");
+    }
+
+    @Override
+    protected Tuple createTuple(Function<Tuple, Tuple> transformer) {
+        Tuple tuple = Tuple.create().set("ID", 1L);
+
+        tuple = transformer.apply(tuple);
+
+        return createClientSqlRow(schema, tuple);
     }
 
-    private static Tuple createRow(String columnName, NativeType type) {
+    @Override
+    protected Tuple createNullValueTuple(ColumnType valueType) {
         SchemaDescriptor schema = new SchemaDescriptor(
                 1,
                 new Column[]{new Column("ID", INT32, false)},
-                new Column[]{new Column(columnName, type, true)}
+                new Column[]{new Column("VAL", 
SchemaTestUtils.specToType(valueType), true)}
         );
 
         BinaryRow binaryRow = SchemaTestUtils.binaryRow(schema, 1, null);
@@ -87,14 +124,62 @@ public class ClientSqlRowTest extends 
BaseIgniteAbstractTest {
 
         ResultSetMetadata resultSetMetadata = new 
ResultSetMetadataImpl(List.of(
                 new ColumnMetadataImpl("ID", ColumnType.INT32, 0, 0, false, 
null),
-                new ColumnMetadataImpl(columnName, type.spec(), 0, 0, true, 
null)
+                new ColumnMetadataImpl("VAL", valueType, 0, 0, true, null)
         ));
 
         return new ClientSqlRow(binaryTuple, resultSetMetadata);
     }
 
-    private static Stream<Arguments> primitiveAccessors() {
-        return SchemaTestUtils.PRIMITIVE_ACCESSORS.entrySet().stream()
-                .map(e -> Arguments.of(e.getKey(), e.getValue()));
+    @Override
+    protected Tuple getTuple() {
+        Tuple tuple = Tuple.create();
+
+        tuple = addColumnsForDefaultSchema(tuple);
+
+        return createClientSqlRow(schema, tuple);
+    }
+
+    @Override
+    protected Tuple getTupleWithColumnOfAllTypes() {
+        Tuple tuple = Tuple.create().set("keyUuidCol", UUID_VALUE);
+
+        tuple = addColumnOfAllTypes(tuple);
+
+        return createClientSqlRow(fullSchema, tuple);
+    }
+
+    @Override
+    protected Tuple createTupleOfSingleColumn(ColumnType type, String 
columnName, Object value) {
+        NativeType nativeType = NativeTypes.fromObject(value);
+        SchemaDescriptor schema = new SchemaDescriptor(42,
+                new Column[]{new Column(columnName.toUpperCase(), nativeType, 
false)},
+                new Column[]{}
+        );
+
+        Tuple tuple = Tuple.create().set(columnName, value);
+
+        return createClientSqlRow(schema, tuple);
+    }
+
+    private static Tuple createClientSqlRow(SchemaDescriptor schema, Tuple 
valuesTuple) {
+        List<ColumnMetadata> columnsMeta = new 
ArrayList<>(valuesTuple.columnCount());
+        BinaryTupleBuilder binaryTupleBuilder = new 
BinaryTupleBuilder(valuesTuple.columnCount());
+        BinaryTupleSchema binaryTupleSchema = 
BinaryTupleSchema.createRowSchema(schema);
+
+        for (int i = 0; i < valuesTuple.columnCount(); i++) {
+            Column field = schema.columns().get(i);
+            Object value = 
valuesTuple.value(IgniteNameUtils.quoteIfNeeded(field.name()));
+
+            binaryTupleSchema.appendValue(binaryTupleBuilder, i, value);
+
+            int precision = ClientTableCommon.getPrecision(field.type());
+            int scale = ClientTableCommon.getDecimalScale(field.type());
+
+            columnsMeta.add(new ColumnMetadataImpl(field.name(), 
field.type().spec(), precision, scale, field.nullable(), null));
+        }
+
+        BinaryTupleReader binaryTuple = new 
BinaryTupleReader(valuesTuple.columnCount(), binaryTupleBuilder.build());
+
+        return new ClientSqlRow(binaryTuple, new 
ResultSetMetadataImpl(columnsMeta));
     }
 }
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
new file mode 100644
index 00000000000..845a69990ac
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/TupleTypeCastUtils.java
@@ -0,0 +1,457 @@
+/*
+ * 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 org.apache.ignite.internal.lang.IgniteStringFormatter;
+import org.apache.ignite.internal.lang.InternalTuple;
+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}.
+ *
+ * <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 {
+    private static final String TYPE_CAST_ERROR_COLUMN_NAME = "Column with 
name '{}' has type {} but {} was requested";
+
+    private static final String TYPE_CAST_ERROR_COLUMN_INDEX = "Column with 
index {} has type {} but {} was requested";
+
+    private static final int INT_COLUMN_TYPES_BITMASK = 
buildIntegerTypesBitMask();
+
+    /** 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)) {
+            throwClassCastException(ColumnType.INT8, actualType, columnIndex);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+
+        return toByte(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and converts it to a byte if possible. */
+    public static byte readByteValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT8, actualType, columnName);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+
+        return toByte(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and converts it to a short if possible. */
+    public static short readShortValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, int columnIndex) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT16, actualType, columnIndex);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+
+        return toShort(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and converts it to a short if possible. */
+    public static short readShortValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT16, actualType, columnName);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+
+        return toShort(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and converts it to an int if possible. */
+    public static int readIntValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, int columnIndex) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT32, actualType, columnIndex);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+
+        return toInt(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and converts it to an int if possible. */
+    public static int readIntValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT32, actualType, columnName);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+
+        return toInt(binaryTuple, binaryTupleIndex, actualType);
+    }
+
+    /** Reads a value from the tuple and returns it as a long. Only integer 
column types are allowed. */
+    public static long readLongValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, int columnIndex) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT64, actualType, columnIndex);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnIndex);
+
+        return binaryTuple.longValue(binaryTupleIndex);
+    }
+
+    /** Reads a value from the tuple and returns it as a long. Only integer 
column types are allowed. */
+    public static long readLongValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (!integerType(actualType)) {
+            throwClassCastException(ColumnType.INT64, actualType, columnName);
+        }
+
+        IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, columnName);
+
+        return binaryTuple.longValue(binaryTupleIndex);
+    }
+
+    /** Reads a value from the tuple and converts it to a float if possible. */
+    public static float readFloatValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, int columnIndex) {
+        if (actualType == ColumnType.FLOAT || actualType == ColumnType.DOUBLE) 
{
+            IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, 
columnIndex);
+
+            return toFloat(binaryTuple, binaryTupleIndex, actualType);
+        }
+
+        throw newClassCastException(ColumnType.FLOAT, actualType, columnIndex);
+    }
+
+    /** Reads a value from the tuple and converts it to a float if possible. */
+    public static float readFloatValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (actualType == ColumnType.FLOAT || actualType == ColumnType.DOUBLE) 
{
+            IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, 
columnName);
+
+            return toFloat(binaryTuple, binaryTupleIndex, actualType);
+        }
+
+        throw newClassCastException(ColumnType.FLOAT, actualType, columnName);
+    }
+
+    /** Reads a value from the tuple and returns it as a double. */
+    public static double readDoubleValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, int columnIndex) {
+        if (actualType == ColumnType.DOUBLE || actualType == ColumnType.FLOAT) 
{
+            IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, 
columnIndex);
+
+            return binaryTuple.doubleValue(binaryTupleIndex);
+        }
+
+        throw newClassCastException(ColumnType.DOUBLE, actualType, 
columnIndex);
+    }
+
+    /** Reads a value from the tuple and returns it as a double. */
+    public static double readDoubleValue(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType, String columnName) {
+        if (actualType == ColumnType.DOUBLE || actualType == ColumnType.FLOAT) 
{
+            IgniteUtils.ensureNotNull(binaryTuple, binaryTupleIndex, 
columnName);
+
+            return binaryTuple.doubleValue(binaryTupleIndex);
+        }
+
+        throw newClassCastException(ColumnType.DOUBLE, actualType, columnName);
+    }
+
+    /**
+     * Validates that the requested column type matches the actual type and 
throws {@link ClassCastException}
+     * otherwise.
+     */
+    public static void validateColumnType(ColumnType requestedType, ColumnType 
actualType, int columnIndex) {
+        if (requestedType != actualType) {
+            throwClassCastException(requestedType, actualType, columnIndex);
+        }
+    }
+
+    /**
+     * Validates that the requested column type matches the actual type and 
throws {@link ClassCastException}
+     * otherwise.
+     */
+    public static void validateColumnType(ColumnType requestedType, ColumnType 
actualType, String columnName) {
+        if (requestedType != actualType) {
+            throwClassCastException(requestedType, actualType, columnName);
+        }
+    }
+
+    /** Casts an object to {@code byte} if possible. */
+    public static byte castToByte(Object number) {
+        if (number instanceof Byte) {
+            return (byte) number;
+        }
+
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Short) {
+            long longVal = ((Number) number).longValue();
+            byte byteVal = ((Number) number).byteValue();
+
+            if (longVal == byteVal) {
+                return byteVal;
+            }
+
+            throw new ArithmeticException("Byte value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ byte.class);
+    }
+
+    /** Casts an object to {@code short} if possible. */
+    public static short castToShort(Object number) {
+        if (number instanceof Short) {
+            return (short) number;
+        }
+
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Byte) {
+            long longVal = ((Number) number).longValue();
+            short shortVal = ((Number) number).shortValue();
+
+            if (longVal == shortVal) {
+                return shortVal;
+            }
+
+            throw new ArithmeticException("Short value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ short.class);
+    }
+
+    /** Casts an object to {@code int} if possible. */
+    public static int castToInt(Object number) {
+        if (number instanceof Integer) {
+            return (int) number;
+        }
+
+        if (number instanceof Long || number instanceof Short || number 
instanceof Byte) {
+            long longVal = ((Number) number).longValue();
+            int intVal = ((Number) number).intValue();
+
+            if (longVal == intVal) {
+                return intVal;
+            }
+
+            throw new ArithmeticException("Int value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ int.class);
+    }
+
+    /** Casts an object to {@code long} if possible. */
+    public static long castToLong(Object number) {
+        if (number instanceof Long || number instanceof Integer || number 
instanceof Short || number instanceof Byte) {
+            return ((Number) number).longValue();
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ long.class);
+    }
+
+    /** Casts an object to {@code float} if possible. */
+    public static float castToFloat(Object number) {
+        if (number instanceof Float) {
+            return (float) number;
+        }
+
+        if (number instanceof Double) {
+            double doubleVal = ((Number) number).doubleValue();
+            float floatVal = ((Number) number).floatValue();
+
+            //noinspection FloatingPointEquality
+            if (doubleVal == floatVal || Double.isNaN(doubleVal)) {
+                return floatVal;
+            }
+
+            throw new ArithmeticException("Float value overflow: " + number);
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ float.class);
+    }
+
+    /** Casts an object to {@code double} if possible. */
+    public static double castToDouble(Object number) {
+        if (number instanceof Double || number instanceof Float) {
+            return ((Number) number).doubleValue();
+        }
+
+        throw new ClassCastException(number.getClass() + " cannot be cast to " 
+ double.class);
+    }
+
+    /** Casts an integer value from the tuple to {@code byte} performing range 
checks. */
+    @SuppressWarnings("NumericCastThatLosesPrecision")
+    private static byte toByte(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType valueType) {
+        switch (valueType) {
+            case INT8:
+                return binaryTuple.byteValue(binaryTupleIndex);
+
+            case INT16: {
+                short value = binaryTuple.shortValue(binaryTupleIndex);
+                byte byteValue = (byte) value;
+
+                if (byteValue == value) {
+                    return byteValue;
+                }
+
+                throw new ArithmeticException("Byte value overflow: " + value);
+            }
+            case INT32: {
+                int value = binaryTuple.intValue(binaryTupleIndex);
+                byte byteValue = (byte) value;
+
+                if (byteValue == value) {
+                    return byteValue;
+                }
+
+                throw new ArithmeticException("Byte value overflow: " + value);
+            }
+            case INT64: {
+                long value = binaryTuple.longValue(binaryTupleIndex);
+                byte byteValue = (byte) value;
+
+                if (byteValue == value) {
+                    return byteValue;
+                }
+
+                throw new ArithmeticException("Byte value overflow: " + value);
+            }
+
+            default:
+        }
+
+        throw new IllegalArgumentException("invalid type: " + valueType);
+    }
+
+    /** Casts an integer value from the tuple to {@code short} performing 
range checks. */
+    @SuppressWarnings("NumericCastThatLosesPrecision")
+    private static short toShort(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType valueType) {
+        switch (valueType) {
+            case INT16:
+            case INT8:
+                return binaryTuple.shortValue(binaryTupleIndex);
+
+            case INT32: {
+                int value = binaryTuple.intValue(binaryTupleIndex);
+                short shortValue = (short) value;
+
+                if (shortValue == value) {
+                    return shortValue;
+                }
+
+                throw new ArithmeticException("Short value overflow: " + 
value);
+            }
+            case INT64: {
+                long value = binaryTuple.longValue(binaryTupleIndex);
+                short shortValue = (short) value;
+
+                if (shortValue == value) {
+                    return shortValue;
+                }
+
+                throw new ArithmeticException("Short value overflow: " + 
value);
+            }
+
+            default:
+        }
+
+        throw new IllegalArgumentException("invalid type: " + valueType);
+    }
+
+    /** Casts an integer value from the tuple to {@code int} performing range 
checks. */
+    @SuppressWarnings("NumericCastThatLosesPrecision")
+    private static int toInt(InternalTuple binaryTuple, int binaryTupleIndex, 
ColumnType valueType) {
+        switch (valueType) {
+            case INT32:
+            case INT16:
+            case INT8:
+                return binaryTuple.intValue(binaryTupleIndex);
+
+            case INT64: {
+                long value = binaryTuple.longValue(binaryTupleIndex);
+                int intValue = (int) value;
+
+                if (intValue == value) {
+                    return intValue;
+                }
+
+                throw new ArithmeticException("Int value overflow: " + value);
+            }
+
+            default:
+                assert false : valueType;
+        }
+
+        throw new IllegalArgumentException("invalid type: " + valueType);
+    }
+
+    /** Casts a floating-point value from the tuple to {@code float} 
performing precision checks. */
+    @SuppressWarnings({"NumericCastThatLosesPrecision", 
"FloatingPointEquality"})
+    private static float toFloat(InternalTuple binaryTuple, int 
binaryTupleIndex, ColumnType actualType) {
+        if (actualType == ColumnType.FLOAT) {
+            return binaryTuple.floatValue(binaryTupleIndex);
+        }
+
+        double doubleValue = binaryTuple.doubleValue(binaryTupleIndex);
+        float floatValue = (float) doubleValue;
+
+        if (doubleValue == floatValue || Double.isNaN(doubleValue)) {
+            return floatValue;
+        }
+
+        throw new ArithmeticException("Float value overflow: " + doubleValue);
+    }
+
+    private static void throwClassCastException(ColumnType requestedType, 
ColumnType actualType, int index) {
+        throw newClassCastException(requestedType, actualType, index);
+    }
+
+    private static void throwClassCastException(ColumnType requestedType, 
ColumnType actualType, String columnName) {
+        throw newClassCastException(requestedType, actualType, columnName);
+    }
+
+    private static RuntimeException newClassCastException(ColumnType 
requestedType, ColumnType actualType, int index) {
+        return new 
ClassCastException(IgniteStringFormatter.format(TYPE_CAST_ERROR_COLUMN_INDEX, 
index, actualType, requestedType));
+    }
+
+    private static RuntimeException newClassCastException(ColumnType 
requestedType, ColumnType actualType, String columnName) {
+        return new 
ClassCastException(IgniteStringFormatter.format(TYPE_CAST_ERROR_COLUMN_NAME, 
columnName, actualType, requestedType));
+    }
+
+    private static boolean integerType(ColumnType type) {
+        return (INT_COLUMN_TYPES_BITMASK & (1 << type.ordinal())) != 0;
+    }
+
+    private static int buildIntegerTypesBitMask() {
+        assert ColumnType.values().length < Integer.SIZE : "Too many column 
types to fit in an integer bitmask";
+
+        ColumnType[] intTypes = {
+                ColumnType.INT8,
+                ColumnType.INT16,
+                ColumnType.INT32,
+                ColumnType.INT64
+        };
+
+        int elements = 0;
+
+        for (ColumnType e : intTypes) {
+            elements |= (1 << e.ordinal());
+        }
+
+        return elements;
+    }
+}
diff --git 
a/modules/schema/src/testFixtures/java/org/apache/ignite/internal/schema/SchemaTestUtils.java
 
b/modules/schema/src/testFixtures/java/org/apache/ignite/internal/schema/SchemaTestUtils.java
index 9691e3d2615..acb04c4cace 100644
--- 
a/modules/schema/src/testFixtures/java/org/apache/ignite/internal/schema/SchemaTestUtils.java
+++ 
b/modules/schema/src/testFixtures/java/org/apache/ignite/internal/schema/SchemaTestUtils.java
@@ -30,15 +30,10 @@ import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.IdentityHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.function.IntConsumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.ignite.internal.schema.row.RowAssembler;
@@ -48,7 +43,6 @@ import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
 import org.apache.ignite.internal.type.TemporalNativeType;
 import org.apache.ignite.sql.ColumnType;
-import org.apache.ignite.table.Tuple;
 
 /**
  * Test utility class.
@@ -78,8 +72,6 @@ public final class SchemaTestUtils {
             NativeTypes.BYTES,
             NativeTypes.STRING);
 
-    public static final Map<NativeType, BiConsumer<Tuple, Object>> 
PRIMITIVE_ACCESSORS = makePrimitiveAccessorsMap();
-
     /**
      * Generates random value of given type.
      *
@@ -221,28 +213,4 @@ public final class SchemaTestUtils {
         return Instant.ofEpochMilli(minTs + (long) (rnd.nextDouble() * (maxTs 
- minTs))).truncatedTo(ChronoUnit.SECONDS)
                 .plusNanos(normalizeNanos(rnd.nextInt(1_000_000_000), 
type.precision()));
     }
-
-    private static Map<NativeType, BiConsumer<Tuple, Object>> 
makePrimitiveAccessorsMap() {
-        IdentityHashMap<NativeType, BiConsumer<Tuple, Object>> map = new 
IdentityHashMap<>();
-
-        map.put(NativeTypes.BOOLEAN, (tuple, index) -> invoke(index, 
tuple::booleanValue, tuple::booleanValue));
-        map.put(NativeTypes.INT8, (tuple, index) -> invoke(index, 
tuple::byteValue, tuple::byteValue));
-        map.put(NativeTypes.INT16, (tuple, index) -> invoke(index, 
tuple::shortValue, tuple::shortValue));
-        map.put(NativeTypes.INT32, (tuple, index) -> invoke(index, 
tuple::intValue, tuple::intValue));
-        map.put(NativeTypes.INT64, (tuple, index) -> invoke(index, 
tuple::longValue, tuple::longValue));
-        map.put(NativeTypes.FLOAT, (tuple, index) -> invoke(index, 
tuple::floatValue, tuple::floatValue));
-        map.put(NativeTypes.DOUBLE, (tuple, index) -> invoke(index, 
tuple::doubleValue, tuple::doubleValue));
-
-        return Collections.unmodifiableMap(map);
-    }
-
-    private static void invoke(Object index, IntConsumer intConsumer, 
Consumer<String> strConsumer) {
-        if (index instanceof Integer) {
-            intConsumer.accept((int) index);
-        } else {
-            assert index instanceof String : index.getClass();
-
-            strConsumer.accept((String) index);
-        }
-    }
 }
diff --git a/modules/sql-engine/build.gradle b/modules/sql-engine/build.gradle
index 3aa7c06e7fd..6f637a479e6 100644
--- a/modules/sql-engine/build.gradle
+++ b/modules/sql-engine/build.gradle
@@ -101,6 +101,7 @@ dependencies {
     testImplementation project(':ignite-placement-driver')
     testImplementation libs.jmh.core
     testImplementation libs.awaitility
+    testImplementation testFixtures(project(':ignite-api'))
     testImplementation testFixtures(project(':ignite-core'))
     testImplementation testFixtures(project(':ignite-configuration'))
     testImplementation testFixtures(project(':ignite-schema'))
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
index 9b792bbf7bb..3d1fe18b9ec 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/AsyncResultSetImpl.java
@@ -37,6 +37,7 @@ import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.internal.util.AsyncCursor.BatchedResult;
 import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.internal.util.TransformingIterator;
+import org.apache.ignite.internal.util.TupleTypeCastUtils;
 import org.apache.ignite.sql.NoRowSetExpectedException;
 import org.apache.ignite.sql.ResultSetMetadata;
 import org.apache.ignite.sql.SqlRow;
@@ -216,9 +217,13 @@ public class AsyncResultSetImpl<T> implements 
AsyncResultSet<T> {
         /** {@inheritDoc} */
         @Override
         public <T> T valueOrDefault(String columnName, T defaultValue) {
-            T ret = (T) row.get(columnIndexChecked(columnName));
+            int columnIndex = columnIndex(columnName);
 
-            return ret != null ? ret : defaultValue;
+            if (columnIndex == -1) {
+                return defaultValue;
+            }
+
+            return (T) row.get(columnIndex);
         }
 
         /** {@inheritDoc} */
@@ -254,73 +259,97 @@ public class AsyncResultSetImpl<T> implements 
AsyncResultSet<T> {
         /** {@inheritDoc} */
         @Override
         public byte byteValue(String columnName) {
-            return (byte) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToByte(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public byte byteValue(int columnIndex) {
-            return (byte) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToByte(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public short shortValue(String columnName) {
-            return (short) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToShort(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public short shortValue(int columnIndex) {
-            return (short) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToShort(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public int intValue(String columnName) {
-            return (int) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToInt(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public int intValue(int columnIndex) {
-            return (int) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToInt(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public long longValue(String columnName) {
-            return (long) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToLong(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public long longValue(int columnIndex) {
-            return (long) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToLong(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public float floatValue(String columnName) {
-            return (float) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToFloat(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public float floatValue(int columnIndex) {
-            return (float) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToFloat(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public double doubleValue(String columnName) {
-            return (double) getValueNotNull(columnName);
+            Object number = getValueNotNull(columnName);
+
+            return TupleTypeCastUtils.castToDouble(number);
         }
 
         /** {@inheritDoc} */
         @Override
         public double doubleValue(int columnIndex) {
-            return (double) getValueNotNull(columnIndex);
+            Object number = getValueNotNull(columnIndex);
+
+            return TupleTypeCastUtils.castToDouble(number);
         }
 
         /** {@inheritDoc} */
@@ -432,28 +461,49 @@ public class AsyncResultSetImpl<T> implements 
AsyncResultSet<T> {
 
         /** {@inheritDoc} */
         @Override
-        public String toString() {
-            return S.tupleToString(this);
+        public int hashCode() {
+            return Tuple.hashCode(this);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            //noinspection SimplifiableIfStatement
+            if (obj instanceof Tuple) {
+                return Tuple.equals(this, (Tuple) obj);
+            }
+
+            return false;
         }
 
-        private Object getValueNotNull(int columnIndex) {
+        private <T> T getValueNotNull(int columnIndex) {
             Object value = row.get(columnIndex);
 
             if (value == null) {
                 throw new 
NullPointerException(format(IgniteUtils.NULL_TO_PRIMITIVE_ERROR_MESSAGE, 
columnIndex));
             }
 
-            return value;
+            return (T) value;
         }
 
-        private Object getValueNotNull(String columnName) {
+        private <T> T getValueNotNull(String columnName) {
             Object value = row.get(columnIndexChecked(columnName));
 
             if (value == null) {
                 throw new 
NullPointerException(format(IgniteUtils.NULL_TO_PRIMITIVE_NAMED_ERROR_MESSAGE, 
columnName));
             }
 
-            return value;
+            return (T) value;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            return S.tupleToString(this);
         }
     }
 }
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SqlRowTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SqlRowTest.java
index 42916a58846..3fc28ca75a7 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SqlRowTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/api/SqlRowTest.java
@@ -17,13 +17,32 @@
 
 package org.apache.ignite.internal.sql.api;
 
-import static 
org.apache.ignite.internal.sql.engine.util.TypeUtils.IDENTITY_ROW_CONVERTER;
+import static org.apache.ignite.internal.type.NativeTypes.BOOLEAN;
+import static org.apache.ignite.internal.type.NativeTypes.BYTES;
+import static org.apache.ignite.internal.type.NativeTypes.DATE;
+import static org.apache.ignite.internal.type.NativeTypes.DOUBLE;
+import static org.apache.ignite.internal.type.NativeTypes.FLOAT;
+import static org.apache.ignite.internal.type.NativeTypes.INT16;
+import static org.apache.ignite.internal.type.NativeTypes.INT32;
+import static org.apache.ignite.internal.type.NativeTypes.INT64;
+import static org.apache.ignite.internal.type.NativeTypes.INT8;
+import static org.apache.ignite.internal.type.NativeTypes.STRING;
+import static org.apache.ignite.internal.type.NativeTypes.datetime;
+import static org.apache.ignite.internal.type.NativeTypes.time;
+import static org.apache.ignite.internal.type.NativeTypes.timestamp;
 
+import java.time.LocalTime;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.function.BiConsumer;
-import java.util.stream.Stream;
-import org.apache.ignite.internal.lang.IgniteStringFormatter;
+import java.util.Random;
+import java.util.function.Function;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
+import org.apache.ignite.internal.schema.BinaryTupleSchema;
+import org.apache.ignite.internal.schema.Column;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaTestUtils;
+import org.apache.ignite.internal.schema.marshaller.TupleMarshaller;
 import org.apache.ignite.internal.sql.ColumnMetadataImpl;
 import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
 import org.apache.ignite.internal.sql.api.AsyncResultSetImpl.SqlRowImpl;
@@ -31,70 +50,166 @@ import 
org.apache.ignite.internal.sql.engine.InternalSqlRowImpl;
 import org.apache.ignite.internal.sql.engine.api.expressions.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
-import org.apache.ignite.internal.testframework.IgniteTestUtils;
-import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.table.KeyValueTestUtils;
+import org.apache.ignite.internal.table.TableRow;
 import org.apache.ignite.internal.type.NativeTypes;
-import org.apache.ignite.internal.type.StructNativeType;
-import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.type.NativeTypes.StructTypeBuilder;
+import org.apache.ignite.lang.util.IgniteNameUtils;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.sql.SqlRow;
+import org.apache.ignite.table.AbstractImmutableTupleTest;
 import org.apache.ignite.table.Tuple;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * Ensures that reading a {@code null} value as primitive from the
- * {@link SqlRow} instance produces a {@link NullPointerException}.
+ * Tests {@link SqlRow} tuple implementation.
  */
-public class SqlRowTest extends BaseIgniteAbstractTest {
-    @ParameterizedTest(name = "{0}")
-    @MethodSource("primitiveAccessors")
-    @SuppressWarnings("ThrowableNotThrown")
-    void nullPointerWhenReadingNullAsPrimitive(
-            NativeType type,
-            BiConsumer<Tuple, Object> fieldAccessor
-    ) {
-        String columnName = "VAL";
-        Tuple row = createRow(columnName, type);
-
-        IgniteTestUtils.assertThrows(
-                NullPointerException.class,
-                () -> fieldAccessor.accept(row, 0),
-                
IgniteStringFormatter.format(IgniteUtils.NULL_TO_PRIMITIVE_ERROR_MESSAGE, 0)
-        );
+public class SqlRowTest extends AbstractImmutableTupleTest {
+    /** Schema descriptor for default test tuple. */
+    private final SchemaDescriptor schema = new SchemaDescriptor(
+            42,
+            List.of(
+                    new Column("ID", INT64, false),
+                    new Column("SIMPLENAME", STRING, true),
+                    new Column("QuotedName", STRING, true),
+                    new Column("NOVALUE", STRING, true)
+            ),
+            List.of("ID"),
+            null
+    );
+
+    /** Schema descriptor for tuple with columns of all the supported types. */
+    private final SchemaDescriptor fullSchema = new SchemaDescriptor(42,
+            List.of(
+                    new Column("valBoolCol".toUpperCase(), BOOLEAN, true),
+                    new Column("valByteCol".toUpperCase(), INT8, true),
+                    new Column("valShortCol".toUpperCase(), INT16, true),
+                    new Column("valIntCol".toUpperCase(), INT32, true),
+                    new Column("valLongCol".toUpperCase(), INT64, true),
+                    new Column("valFloatCol".toUpperCase(), FLOAT, true),
+                    new Column("valDoubleCol".toUpperCase(), DOUBLE, true),
+                    new Column("valDateCol".toUpperCase(), DATE, true),
+                    new Column("keyUuidCol".toUpperCase(), NativeTypes.UUID, 
false),
+                    new Column("valUuidCol".toUpperCase(), NativeTypes.UUID, 
false),
+                    new Column("valTimeCol".toUpperCase(), 
time(TIME_PRECISION), true),
+                    new Column("valDateTimeCol".toUpperCase(), 
datetime(TIMESTAMP_PRECISION), true),
+                    new Column("valTimeStampCol".toUpperCase(), 
timestamp(TIMESTAMP_PRECISION), true),
+                    new Column("valBytesCol".toUpperCase(), BYTES, false),
+                    new Column("valStringCol".toUpperCase(), STRING, false),
+                    new Column("valDecimalCol".toUpperCase(), 
NativeTypes.decimalOf(25, 5), false)
+            ),
+            List.of("keyUuidCol".toUpperCase()),
+            null
+    );
+
+    @Override
+    protected Tuple createTuple(Function<Tuple, Tuple> transformer) {
+        Tuple tuple = Tuple.create().set("ID", 1L);
+
+        tuple = transformer.apply(tuple);
 
-        IgniteTestUtils.assertThrows(
-                NullPointerException.class,
-                () -> fieldAccessor.accept(row, columnName),
-                
IgniteStringFormatter.format(IgniteUtils.NULL_TO_PRIMITIVE_NAMED_ERROR_MESSAGE, 
columnName)
+        TupleMarshaller marshaller = 
KeyValueTestUtils.createMarshaller(schema);
+
+        return TableRow.tuple(marshaller.marshal(tuple));
+    }
+
+    @Override
+    protected Tuple createNullValueTuple(ColumnType valueType) {
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("ID", INT32, false)},
+                new Column[]{new Column("VAL", 
SchemaTestUtils.specToType(valueType), true)}
         );
 
-        IgniteTestUtils.assertThrows(
-                UnsupportedOperationException.class,
-                () -> row.set("NEW", null),
-                null
+        return createSqlRow(schema, Tuple.create().set("ID", 1).set("VAL", 
null));
+    }
+
+    @Test
+    @Override
+    public void testSerialization() {
+        Assumptions.abort(SqlRow.class.getSimpleName() + " is not 
serializable.");
+    }
+
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-27577";)
+    @ParameterizedTest
+    @Override
+    @SuppressWarnings("JUnitMalformedDeclaration")
+    public void allTypesUnsupportedConversion(ColumnType from, ColumnType to) {
+        super.allTypesUnsupportedConversion(from, to);
+    }
+
+    @Override
+    protected Tuple getTuple() {
+        Tuple tuple = Tuple.create();
+
+        tuple = addColumnsForDefaultSchema(tuple);
+
+        return createSqlRow(schema, tuple);
+    }
+
+    @Override
+    protected Tuple getTupleWithColumnOfAllTypes() {
+        Tuple tuple = Tuple.create().set("keyUuidCol", UUID_VALUE);
+
+        tuple = addColumnOfAllTypes(tuple);
+
+        return createSqlRow(fullSchema, tuple);
+    }
+
+    @Override
+    protected Tuple createTupleOfSingleColumn(ColumnType type, String 
columnName, Object value) {
+        SchemaDescriptor schema = new SchemaDescriptor(42,
+                new Column[]{new Column(columnName.toUpperCase(), 
NativeTypes.fromObject(value), false)},
+                new Column[]{}
         );
+
+        Tuple tuple = Tuple.create().set(columnName, value);
+
+        return createSqlRow(schema, tuple);
     }
 
-    private static Tuple createRow(String columnName, NativeType type) {
+    @Override
+    protected @Nullable Object generateValue(Random rnd, ColumnType type) {
+        if (type == ColumnType.TIME) {
+            // SQL currently support only milliseconds.
+            return LocalTime.of(rnd.nextInt(24), rnd.nextInt(60), 
rnd.nextInt(60),
+                    rnd.nextInt(1_000) * 1_000_000);
+        }
+
+        return super.generateValue(rnd, type);
+    }
+
+    private static Tuple createSqlRow(SchemaDescriptor descriptor, Tuple 
valuesTuple) {
         SqlRowHandler handler = SqlRowHandler.INSTANCE;
-        StructNativeType schema = NativeTypes.structBuilder()
-                .addField(columnName, type, true)
-                .build();
+        StructTypeBuilder structTypeBuilder = NativeTypes.structBuilder();
+        List<ColumnMetadata> columnsMeta = new 
ArrayList<>(valuesTuple.columnCount());
+        BinaryTupleBuilder binaryTupleBuilder = new 
BinaryTupleBuilder(valuesTuple.columnCount());
+        BinaryTupleSchema binaryTupleSchema = 
BinaryTupleSchema.createRowSchema(descriptor);
 
-        RowFactory<RowWrapper> factory = handler.create(schema);
-        RowWrapper binaryTupleRow = 
factory.create(handler.toBinaryTuple(factory.create(new Object[]{null})));
+        for (int i = 0; i < valuesTuple.columnCount(); i++) {
+            Column column = descriptor.columns().get(i);
+            Object value = 
valuesTuple.value(IgniteNameUtils.quoteIfNeeded(column.name()));
 
-        InternalSqlRowImpl<RowWrapper> internalSqlRow =
-                new InternalSqlRowImpl<>(binaryTupleRow, handler, 
IDENTITY_ROW_CONVERTER);
+            binaryTupleSchema.appendValue(binaryTupleBuilder, i, value);
+            structTypeBuilder.addField(column.name(), column.type(), 
column.nullable());
+            columnsMeta.add(new ColumnMetadataImpl(column.name(), 
column.type().spec(), -1, -1, column.nullable(), null));
+        }
 
-        return new SqlRowImpl(internalSqlRow, new 
ResultSetMetadataImpl(List.of(
-                new ColumnMetadataImpl(columnName, type.spec(), 0, 0, true, 
null))));
-    }
+        BinaryTupleReader binaryTuple = new 
BinaryTupleReader(valuesTuple.columnCount(), binaryTupleBuilder.build());
+
+        RowFactory<RowWrapper> factory = 
handler.create(structTypeBuilder.build());
+        RowWrapper binaryTupleRow = factory.create(binaryTuple);
+
+        InternalSqlRowImpl<RowWrapper> internalSqlRow =
+                new InternalSqlRowImpl<>(binaryTupleRow, 
SqlRowHandler.INSTANCE, (index, val) ->
+                        val == null ? null : TypeUtils.fromInternal(val, 
descriptor.column(index).type().spec()));
 
-    private static Stream<Arguments> primitiveAccessors() {
-        return SchemaTestUtils.PRIMITIVE_ACCESSORS.entrySet().stream()
-                .map(e -> Arguments.of(e.getKey(), e.getValue()));
+        return new SqlRowImpl(internalSqlRow, new 
ResultSetMetadataImpl(columnsMeta));
     }
 }
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
index 08a9f898904..4c5279ddd9e 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.table;
 
+import static 
org.apache.ignite.internal.util.TupleTypeCastUtils.validateColumnType;
+
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -31,7 +33,9 @@ import org.apache.ignite.internal.schema.SchemaAware;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.TupleTypeCastUtils;
 import org.apache.ignite.lang.util.IgniteNameUtils;
+import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.table.Tuple;
 import org.jetbrains.annotations.Nullable;
 
@@ -115,6 +119,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public boolean booleanValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.BOOLEAN, col.type().spec(), columnName);
+
         int binaryTupleIndex = correctIndex(col);
 
         IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
@@ -127,6 +133,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public boolean booleanValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.BOOLEAN, col.type().spec(), columnIndex);
+
         int binaryTupleIndex = correctIndex(col);
 
         IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
@@ -141,9 +149,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.byteValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readByteValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -153,9 +159,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.byteValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readByteValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -165,9 +169,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.shortValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readShortValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -177,9 +179,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.shortValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readShortValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -189,9 +189,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.intValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readIntValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -201,9 +199,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.intValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readIntValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -213,9 +209,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.longValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readLongValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -225,9 +219,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.longValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readLongValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -237,9 +229,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.floatValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readFloatValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -249,9 +239,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.floatValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readFloatValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -261,9 +249,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnName);
-
-        return row.doubleValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readDoubleValue(row, binaryTupleIndex, 
col.type().spec(), columnName);
     }
 
     /** {@inheritDoc} */
@@ -273,9 +259,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         int binaryTupleIndex = correctIndex(col);
 
-        IgniteUtils.ensureNotNull(row, binaryTupleIndex, columnIndex);
-
-        return row.doubleValue(binaryTupleIndex);
+        return TupleTypeCastUtils.readDoubleValue(row, binaryTupleIndex, 
col.type().spec(), columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -283,6 +267,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public BigDecimal decimalValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.DECIMAL, col.type().spec(), columnName);
+
         return row.decimalValue(correctIndex(col));
     }
 
@@ -291,6 +277,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public BigDecimal decimalValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.DECIMAL, col.type().spec(), columnIndex);
+
         return row.decimalValue(correctIndex(col));
     }
 
@@ -299,6 +287,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public String stringValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.STRING, col.type().spec(), columnName);
+
         return row.stringValue(correctIndex(col));
     }
 
@@ -307,6 +297,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public String stringValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.STRING, col.type().spec(), columnIndex);
+
         return row.stringValue(correctIndex(col));
     }
 
@@ -315,6 +307,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public byte[] bytesValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.BYTE_ARRAY, col.type().spec(), 
columnName);
+
         return row.bytesValue(correctIndex(col));
     }
 
@@ -323,6 +317,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public byte[] bytesValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.BYTE_ARRAY, col.type().spec(), 
columnIndex);
+
         return row.bytesValue(correctIndex(col));
     }
 
@@ -331,6 +327,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public UUID uuidValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.UUID, col.type().spec(), columnName);
+
         return row.uuidValue(correctIndex(col));
     }
 
@@ -339,6 +337,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public UUID uuidValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.UUID, col.type().spec(), columnIndex);
+
         return row.uuidValue(correctIndex(col));
     }
 
@@ -347,6 +347,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalDate dateValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.DATE, col.type().spec(), columnName);
+
         return row.dateValue(correctIndex(col));
     }
 
@@ -355,6 +357,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalDate dateValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.DATE, col.type().spec(), columnIndex);
+
         return row.dateValue(correctIndex(col));
     }
 
@@ -363,6 +367,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalTime timeValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.TIME, col.type().spec(), columnName);
+
         return row.timeValue(correctIndex(col));
     }
 
@@ -371,6 +377,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalTime timeValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.TIME, col.type().spec(), columnIndex);
+
         return row.timeValue(correctIndex(col));
     }
 
@@ -379,6 +387,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalDateTime datetimeValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.DATETIME, col.type().spec(), columnName);
+
         return row.dateTimeValue(correctIndex(col));
     }
 
@@ -387,6 +397,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public LocalDateTime datetimeValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.DATETIME, col.type().spec(), 
columnIndex);
+
         return row.dateTimeValue(correctIndex(col));
     }
 
@@ -395,6 +407,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public Instant timestampValue(String columnName) {
         Column col = rowColumnByName(columnName);
 
+        validateColumnType(ColumnType.TIMESTAMP, col.type().spec(), 
columnName);
+
         return row.timestampValue(correctIndex(col));
     }
 
@@ -403,6 +417,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     public Instant timestampValue(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
+        validateColumnType(ColumnType.TIMESTAMP, col.type().spec(), 
columnIndex);
+
         return row.timestampValue(correctIndex(col));
     }
 

Reply via email to