You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ko...@apache.org on 2025/11/21 11:46:17 UTC

(ignite-3) branch main updated: IGNITE-26705 Sql. Replace sql.engine.exec.row.RowSchema with StructNativeType (#6968)

This is an automated email from the ASF dual-hosted git repository.

korlov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 38489d84f29 IGNITE-26705 Sql. Replace sql.engine.exec.row.RowSchema with StructNativeType (#6968)
38489d84f29 is described below

commit 38489d84f294501f60fc30eadea96e375c528d6d
Author: korlov42 <ko...@apache.org>
AuthorDate: Fri Nov 21 13:46:11 2025 +0200

    IGNITE-26705 Sql. Replace sql.engine.exec.row.RowSchema with StructNativeType (#6968)
---
 .../java/org/apache/ignite/sql/ColumnType.java     |   5 +-
 .../ignite/table/AbstractImmutableTupleTest.java   |   2 +-
 .../ignite/internal/compute/PojoConverter.java     |   1 +
 .../internal/compute/SharedComputeUtils.java       |   1 +
 .../org/apache/ignite/client/ClientTupleTest.java  |   2 +-
 .../apache/ignite/internal/type/NativeType.java    |  52 ++---
 .../apache/ignite/internal/type/NativeTypes.java   |  80 +++++++-
 .../ignite/internal/type/StructNativeType.java     | 126 ++++++++++++
 .../internal/table/ItPublicApiColocationTest.java  |   6 +-
 .../CatalogToSchemaDescriptorConverterTest.java    |   2 +-
 .../sql/engine/ItDynamicParameterTest.java         |   2 +-
 .../sql/engine/exec/LogicalRelImplementor.java     |  44 ++---
 .../internal/sql/engine/exec/RowHandler.java       |   6 +-
 .../internal/sql/engine/exec/SqlRowHandler.java    |  62 +++---
 .../sql/engine/exec/UpdatableTableImpl.java        |  22 +--
 .../sql/engine/exec/exp/ExpressionFactory.java     |   4 +-
 .../sql/engine/exec/exp/ExpressionFactoryImpl.java |   4 +-
 .../engine/exec/exp/JoinProjectionImplementor.java |  23 +--
 .../sql/engine/exec/exp/ProjectionImplementor.java |  37 ++--
 .../engine/exec/exp/RowProviderImplementor.java    |  26 +--
 .../sql/engine/exec/exp/ValuesImplementor.java     |   6 +-
 .../exec/exp/func/SystemRangeTableFunction.java    |   6 +-
 .../internal/sql/engine/exec/rel/HashJoinNode.java |  13 +-
 .../sql/engine/exec/rel/MergeJoinNode.java         |  13 +-
 .../internal/sql/engine/exec/rel/ModifyNode.java   |  19 +-
 .../sql/engine/exec/rel/NestedLoopJoinNode.java    |  13 +-
 .../internal/sql/engine/exec/row/BaseTypeSpec.java |  78 --------
 .../internal/sql/engine/exec/row/NullTypeSpec.java |  47 -----
 .../internal/sql/engine/exec/row/RowSchema.java    | 136 -------------
 .../sql/engine/exec/row/RowSchemaTypes.java        | 106 ----------
 .../internal/sql/engine/exec/row/RowType.java      |  72 -------
 .../internal/sql/engine/exec/row/TypeSpec.java     |  57 ------
 .../sql/engine/prepare/KeyValueGetPlan.java        |   9 +-
 .../sql/engine/prepare/SelectCountPlan.java        |  11 +-
 .../sql/engine/type/IgniteTypeFactory.java         |  28 ++-
 .../ignite/internal/sql/engine/util/TypeUtils.java | 132 +++++++------
 .../exec/IdentityDistributionFunctionSelfTest.java |   8 +-
 .../sql/engine/exec/PartitionsResolutionTest.java  |  12 +-
 .../exec/ProjectedTableRowConverterSelfTest.java   |   8 +-
 .../sql/engine/exec/TableRowConverterSelfTest.java |  24 +--
 .../engine/exec/exp/ExpressionFactoryImplTest.java |   3 +-
 .../sql/engine/exec/rel/AsyncRootNodeTest.java     |   6 +-
 .../sql/engine/exec/rel/BaseAggregateTest.java     |  16 +-
 .../rel/CorrelatedNestedLoopJoinExecutionTest.java |  12 +-
 .../exec/rel/DataSourceScanNodeExecutionTest.java  |  35 ++--
 .../sql/engine/exec/rel/ExchangeExecutionTest.java |   8 +-
 .../exec/rel/HashAggregateExecutionTest.java       |  10 +-
 .../rel/HashAggregateSingleGroupExecutionTest.java |   5 +-
 .../exec/rel/IndexScanNodeExecutionTest.java       |  12 +-
 .../engine/exec/rel/IntersectExecutionTest.java    |  22 +--
 .../sql/engine/exec/rel/MinusExecutionTest.java    |  20 +-
 .../engine/exec/rel/ModifyNodeExecutionTest.java   |  84 ++++----
 .../engine/exec/rel/ScannableTableSelfTest.java    |  11 +-
 .../exec/rel/SortAggregateExecutionTest.java       |  10 +-
 .../exec/rel/TableScanNodeExecutionTest.java       |  10 +-
 .../sql/engine/exec/row/RowSchemaTest.java         | 130 ------------
 .../sql/engine/exec/row/RowSchemaTypesTest.java    | 217 ---------------------
 .../sql/engine/exec/row/SqlRowHandlerTest.java     | 100 ++++------
 .../sql/engine/framework/ArrayRowHandler.java      |  15 +-
 .../internal/sql/engine/util/TypeUtilsTest.java    | 124 +++++-------
 .../internal/sql/engine/util/SqlTestUtils.java     |   1 +
 .../internal/systemview/SystemViewManagerTest.java |   2 +-
 .../ignite/internal/table/ItColocationTest.java    |   8 +-
 63 files changed, 739 insertions(+), 1427 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/sql/ColumnType.java b/modules/api/src/main/java/org/apache/ignite/sql/ColumnType.java
index 22ecb67785b..b22f5b3b2ef 100644
--- a/modules/api/src/main/java/org/apache/ignite/sql/ColumnType.java
+++ b/modules/api/src/main/java/org/apache/ignite/sql/ColumnType.java
@@ -91,7 +91,10 @@ public enum ColumnType {
     PERIOD(17, Period.class, true, false, false),
 
     /** Time interval. SQL type: none. */
-    DURATION(18, Duration.class, true, false, false);
+    DURATION(18, Duration.class, true, false, false),
+
+    /** Structured type having named fields. SQL type: none. */
+    STRUCT(19, Object.class, false, false, false);
 
     private final Class<?> javaClass;
     private final boolean precisionAllowed;
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 c321eec566d..b4933892c7f 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
@@ -101,7 +101,7 @@ public abstract class AbstractImmutableTupleTest {
 
     protected abstract Tuple createTupleOfSingleColumn(ColumnType type, String columnName, Object value);
 
-    @EnumSource(value = ColumnType.class, mode = Mode.EXCLUDE, names = {"PERIOD", "DURATION", "NULL"})
+    @EnumSource(value = ColumnType.class, mode = Mode.EXCLUDE, names = {"PERIOD", "DURATION", "NULL", "STRUCT"})
     @ParameterizedTest
     public void testTupleColumnsEquality(ColumnType type) {
         ThreadLocalRandom rnd = ThreadLocalRandom.current();
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/compute/PojoConverter.java b/modules/client-common/src/main/java/org/apache/ignite/internal/compute/PojoConverter.java
index 9333a1a1039..a3484a94ceb 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/internal/compute/PojoConverter.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/compute/PojoConverter.java
@@ -33,6 +33,7 @@ import org.apache.ignite.table.mapper.PojoMapper;
 /** Converts POJO to Tuple and back. */
 public class PojoConverter {
     private static final Set<Class<?>> NATIVE_TYPES = Arrays.stream(ColumnType.values())
+            .filter(type -> type != ColumnType.STRUCT)
             .map(ColumnType::javaClass)
             .collect(Collectors.toUnmodifiableSet());
 
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/compute/SharedComputeUtils.java b/modules/client-common/src/main/java/org/apache/ignite/internal/compute/SharedComputeUtils.java
index a750cee0af3..8fde6c25bc0 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/internal/compute/SharedComputeUtils.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/compute/SharedComputeUtils.java
@@ -53,6 +53,7 @@ import org.jetbrains.annotations.Nullable;
  */
 public class SharedComputeUtils {
     private static final Set<Class<?>> NATIVE_TYPES = Arrays.stream(ColumnType.values())
+            .filter(type -> type != ColumnType.STRUCT)
             .map(ColumnType::javaClass)
             .collect(Collectors.toUnmodifiableSet());
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
index 633bdba46e1..e0f7696b589 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
@@ -234,7 +234,7 @@ public class ClientTupleTest extends AbstractMutableTupleTest {
                 .collect(Collectors.toSet());
 
         for (ColumnType columnType : ColumnType.values()) {
-            if (columnType == ColumnType.NULL) {
+            if (columnType == ColumnType.NULL || columnType == ColumnType.STRUCT) {
                 continue;
             }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/type/NativeType.java b/modules/core/src/main/java/org/apache/ignite/internal/type/NativeType.java
index 6abbbae3744..b956e1905fd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/type/NativeType.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/type/NativeType.java
@@ -17,7 +17,7 @@
 
 package org.apache.ignite.internal.type;
 
-import java.util.Arrays;
+import java.util.EnumSet;
 import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.sql.ColumnType;
 import org.jetbrains.annotations.TestOnly;
@@ -41,18 +41,11 @@ public class NativeType implements Comparable<NativeType> {
      * @param size     Type size in bytes.
      */
     protected NativeType(ColumnType typeSpec, int size) {
-        this.fixedSize = !((typeSpec.precisionAllowed() && typeSpec.scaleAllowed()) || typeSpec.lengthAllowed());
-
-        if (!this.fixedSize) {
-            throw new IllegalArgumentException("Size must be provided only for fixed-length types: " + typeSpec);
-        }
-
-        if (size <= 0) {
-            throw new IllegalArgumentException("Size must be positive [typeSpec=" + typeSpec + ", size=" + size + ']');
-        }
-
-        this.typeSpec = typeSpec;
-        this.size = size;
+        this(
+                typeSpec,
+                isFixedSize(typeSpec),
+                size
+        );
     }
 
     /**
@@ -61,15 +54,29 @@ public class NativeType implements Comparable<NativeType> {
      * @param typeSpec Type spec.
      */
     protected NativeType(ColumnType typeSpec) {
-        this.fixedSize = !((typeSpec.precisionAllowed() && typeSpec.scaleAllowed()) || typeSpec.lengthAllowed());
+        this(
+                typeSpec,
+                isFixedSize(typeSpec),
+                0
+        );
+    }
 
-        if (this.fixedSize) {
-            throw new IllegalArgumentException("Fixed-length types must be created by the "
-                    + "length-aware constructor: " + typeSpec);
+    private static boolean isFixedSize(ColumnType typeSpec) {
+        return !((typeSpec.precisionAllowed() && typeSpec.scaleAllowed()) || typeSpec.lengthAllowed());
+    }
+
+    protected NativeType(ColumnType typeSpec, boolean fixedSize, int size) {
+        if (fixedSize && size <= 0) {
+            throw new IllegalArgumentException("Size must be positive [typeSpec=" + typeSpec + ", size=" + size + ']');
+        }
+
+        if (!fixedSize && size > 0) {
+            throw new IllegalArgumentException("Size must be provided only for fixed-length types: " + typeSpec);
         }
 
+        this.fixedSize = fixedSize;
         this.typeSpec = typeSpec;
-        this.size = 0;
+        this.size = size;
     }
 
     /**
@@ -170,11 +177,12 @@ public class NativeType implements Comparable<NativeType> {
                 "fixed", fixedLength());
     }
 
-
-    // TODO https://issues.apache.org/jira/browse/IGNITE-17373 Remove filter after this issue is resolved
+    /** Returns an array of fully supported types. */
     @TestOnly
+    // TODO https://issues.apache.org/jira/browse/IGNITE-17373 Remove filter after this issue is resolved
     public static ColumnType[] nativeTypes() {
-        return Arrays.stream(ColumnType.values()).filter(v -> v != ColumnType.NULL && v != ColumnType.DURATION
-                && v != ColumnType.PERIOD).toArray(ColumnType[]::new);
+        return EnumSet.complementOf(EnumSet.of(
+                ColumnType.NULL, ColumnType.DURATION, ColumnType.PERIOD, ColumnType.STRUCT
+        )).toArray(ColumnType[]::new);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/type/NativeTypes.java b/modules/core/src/main/java/org/apache/ignite/internal/type/NativeTypes.java
index de044a6904c..6b1acfe025f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/type/NativeTypes.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/type/NativeTypes.java
@@ -22,6 +22,10 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.internal.type.StructNativeType.Field;
+import org.apache.ignite.internal.util.StringUtils;
 import org.apache.ignite.sql.ColumnType;
 import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.Nullable;
@@ -37,6 +41,9 @@ public class NativeTypes {
      */
     public static final int MAX_TIME_PRECISION = 9;
 
+    /** NULL type. */
+    public static final NativeType NULL = new NativeType(ColumnType.NULL, 1);
+
     /**
      * BOOLEAN type.
      */
@@ -128,7 +135,7 @@ public class NativeTypes {
      * Creates a DECIMAL type with maximal precision and scale.
      *
      * @param precision Precision.
-     * @param scale     Scale.
+     * @param scale Scale.
      * @return Native type.
      */
     public static NativeType decimalOf(int precision, int scale) {
@@ -165,6 +172,11 @@ public class NativeTypes {
         return TemporalNativeType.timestamp(precision);
     }
 
+    /** Returns builder to create structured type. */
+    public static RowTypeBuilder rowBuilder() {
+        return new RowTypeBuilder();
+    }
+
     /**
      * Return the native type for specified object.
      *
@@ -258,4 +270,70 @@ public class NativeTypes {
 
         return MAX_TIME_PRECISION - trailingZeroes;
     }
+
+    /** A builder for constructing {@link StructNativeType} instances. */
+    public static class RowTypeBuilder {
+        private RowTypeBuilder() {
+        }
+
+        private final List<Field> fields = new ArrayList<>();
+
+        /**
+         * Adds a field to this struct type builder.
+         *
+         * <p>Fields are added in the order this method is called, and this order is preserved in the resulting {@link StructNativeType}.
+         *
+         * @param name The name of the field; Must not be null or blank.
+         * @param type The native type of the field; Must not be null.
+         * @param nullable Whether the field can contain null values.
+         * @return This builder instance for method chaining.
+         * @throws IllegalArgumentException If any argument violates provided constraints.
+         */
+        public RowTypeBuilder addField(String name, NativeType type, boolean nullable) {
+            if (StringUtils.nullOrBlank(name)) {
+                throw new IllegalArgumentException("Name must not be null or blank: "
+                        + (name == null ? "<null>" : "\"" + name + "\"") + ".");
+            }
+
+            if (type == null) {
+                throw new IllegalArgumentException("Type must not be null.");
+            }
+
+            fields.add(new Field(name, type, nullable));
+
+            return this;
+        }
+
+        /**
+         * Adds a field to this struct type builder.
+         *
+         * <p>Fields are added in the order this method is called, and this order is preserved in the resulting {@link StructNativeType}.
+         *
+         * @param field The field to add; Must not be null.
+         * @return This builder instance for method chaining.
+         * @throws IllegalArgumentException If any argument violates provided constraints.
+         */
+        public RowTypeBuilder addField(Field field) {
+            if (field == null) {
+                throw new IllegalArgumentException("Field must not be null.");
+            }
+
+            fields.add(field);
+
+            return this;
+        }
+
+        /**
+         * Builds and returns a new {@link StructNativeType} with the fields that have been added.
+         *
+         * <p>The returned struct contains an immutable copy of the fields in the order they were added. This method can be called multiple
+         * times to create multiple struct instances, though subsequent modifications to the builder will not affect previously built
+         * instances.
+         *
+         * @return A new {@code StructNativeType} instance containing the configured fields.
+         */
+        public StructNativeType build() {
+            return new StructNativeType(List.copyOf(fields));
+        }
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/type/StructNativeType.java b/modules/core/src/main/java/org/apache/ignite/internal/type/StructNativeType.java
new file mode 100644
index 00000000000..27b157f77fc
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/type/StructNativeType.java
@@ -0,0 +1,126 @@
+/*
+ * 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.type;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Represents a structured native type that contains multiple named fields.
+ *
+ * <p>A struct type is a composite data type that groups together zero or more fields, each with its own name, type, and nullability
+ * constraint.
+ *
+ * @see NativeType
+ * @see Field
+ */
+public class StructNativeType extends NativeType {
+    private final List<Field> fields;
+
+    StructNativeType(List<Field> fields) {
+        super(ColumnType.STRUCT, false, 0);
+
+        this.fields = fields;
+    }
+
+    /** Returns the list of fields in this struct. */
+    public List<Field> fields() {
+        return fields;
+    }
+
+    /** Returns the number of fields in this struct. */
+    public int fieldsCount() {
+        return fields.size();
+    }
+
+    /**
+     * Represents a single field within a struct type.
+     *
+     * <p>Each field has a name, a native type, and a nullability flag. Fields are immutable once created and can be compared for equality
+     * based on all three properties.
+     */
+    public static class Field {
+        private final String name;
+        private final NativeType type;
+        private final boolean nullable;
+
+        Field(String name, NativeType type, boolean nullable) {
+            this.name = name;
+            this.type = type;
+            this.nullable = nullable;
+        }
+
+        /** Returns the name of this field. */
+        public String name() {
+            return name;
+        }
+
+        /** Returns the type of this field. */
+        public NativeType type() {
+            return type;
+        }
+
+        /** Returns whether this field can contain null values. */
+        public boolean nullable() {
+            return nullable;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Field field = (Field) o;
+            return nullable == field.nullable && Objects.equals(name, field.name) && Objects.equals(type, field.type);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(name, type, nullable);
+        }
+
+        @Override
+        public String toString() {
+            return S.toString(this);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+        StructNativeType that = (StructNativeType) o;
+        return Objects.equals(fields, that.fields);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), fields);
+    }
+
+    @Override
+    public String toString() {
+        return S.toString(this);
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java
index 3c7fedc3ffa..21e19fd7337 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItPublicApiColocationTest.java
@@ -69,7 +69,7 @@ public class ItPublicApiColocationTest extends ClusterPerClassIntegrationTest {
      * Check colocation by one column PK and explicit colocation key for all types.
      */
     @ParameterizedTest(name = "type=" + ARGUMENTS_PLACEHOLDER)
-    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION"}, mode = Mode.EXCLUDE)
+    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Mode.EXCLUDE)
     public void colocationOneColumn(ColumnType type) {
         String sqlType = SqlTestUtils.toSqlType(type);
         String createTableScript = String.join(";",
@@ -121,8 +121,8 @@ public class ItPublicApiColocationTest extends ClusterPerClassIntegrationTest {
      */
     @CartesianTest
     public void colocationTwoColumns(
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
     ) {
         String sqlType0 = SqlTestUtils.toSqlType(t0);
         String sqlType1 = SqlTestUtils.toSqlType(t1);
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/catalog/CatalogToSchemaDescriptorConverterTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/catalog/CatalogToSchemaDescriptorConverterTest.java
index d21fe73152a..4ce85e19223 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/catalog/CatalogToSchemaDescriptorConverterTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/catalog/CatalogToSchemaDescriptorConverterTest.java
@@ -58,7 +58,7 @@ public class CatalogToSchemaDescriptorConverterTest extends AbstractSchemaConver
     private static final int TEST_SCALE = 5;
 
     @ParameterizedTest
-    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION"}, mode = Mode.EXCLUDE)
+    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Mode.EXCLUDE)
     public void convertColumnType(ColumnType typeSpec) {
         CatalogTableColumnDescriptor columnDescriptor = TestColumnDescriptors.forSpec(typeSpec);
 
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
index e702d00eb44..c99a5966f10 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
@@ -256,7 +256,7 @@ public class ItDynamicParameterTest extends BaseSqlIntegrationTest {
     @ParameterizedTest
     @EnumSource(value = ColumnType.class,
             // TODO: https://issues.apache.org/jira/browse/IGNITE-17373
-            names = {"DURATION", "PERIOD"},
+            names = {"DURATION", "PERIOD", "STRUCT"},
             mode = Mode.EXCLUDE
     )
     void testMetadataTypesForDynamicParameters(ColumnType type) {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
index 26dee4328d9..1ab2f74f6d2 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/LogicalRelImplementor.java
@@ -20,7 +20,7 @@ package org.apache.ignite.internal.sql.engine.exec;
 import static org.apache.calcite.rel.RelDistribution.Type.HASH_DISTRIBUTED;
 import static org.apache.ignite.internal.sql.engine.rule.LogicalScanConverterRule.createMapping;
 import static org.apache.ignite.internal.sql.engine.util.TypeUtils.combinedRowType;
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 import static org.apache.ignite.internal.util.ArrayUtils.asList;
 import static org.apache.ignite.internal.util.CollectionUtils.first;
 import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
@@ -97,7 +97,6 @@ import org.apache.ignite.internal.sql.engine.exec.rel.SortNode;
 import org.apache.ignite.internal.sql.engine.exec.rel.TableScanNode;
 import org.apache.ignite.internal.sql.engine.exec.rel.TableSpoolNode;
 import org.apache.ignite.internal.sql.engine.exec.rel.UnionAllNode;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
 import org.apache.ignite.internal.sql.engine.rel.IgniteCorrelatedNestedLoopJoin;
 import org.apache.ignite.internal.sql.engine.rel.IgniteExchange;
@@ -356,7 +355,6 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
         RelDataType outType = rel.getRowType();
         RelDataType leftType = rel.getLeft().getRowType();
         RelDataType rightType = rel.getRight().getRowType();
-        RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightType));
 
         assert rel.getJoinType() == JoinRelType.INNER || rel.getJoinType() == JoinRelType.LEFT
                 : CNLJ_NOT_SUPPORTED_JOIN_ASSERTION_MSG;
@@ -368,7 +366,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
         SqlJoinPredicate<RowT> joinPredicate = expressionFactory.joinPredicate(rel.getCondition(), outType, leftType.getFieldCount());
         BiPredicate<RowT, RowT> cond = (left, right) -> joinPredicate.test(ctx, left, right);
 
-        RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
+        RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(convertStructuredType(rightType));
 
         Node<RowT> node = new CorrelatedNestedLoopJoinNode<>(ctx, cond, rel.getVariablesSet(),
                 rel.getJoinType(), rightRowFactory, joinProjection);
@@ -515,8 +513,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             return new ScanNode<>(ctx, Collections.emptyList());
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
         PartitionProvider<RowT> partitionProvider = ctx.getPartitionProvider(rel.sourceId(), group, tbl);
 
         return new IndexScanNode<>(
@@ -570,8 +567,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
         }
 
         // TODO: IGNITE-22822 fix required columns.
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         PartitionProvider<RowT> partitionProvider = ctx.getPartitionProvider(rel.sourceId(), group, tbl);
 
@@ -614,8 +610,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             prj = row -> sqlProjection.project(ctx, row);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
         return new DataSourceScanNode<>(
                 ctx,
                 rowFactory,
@@ -756,8 +751,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
     public Node<RowT> visit(IgniteSetOp rel) {
         RelDataType rowType = rel.getRowType();
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         List<Node<RowT>> inputs = Commons.transform(rel.getInputs(), this::visit);
         int columnNum;
@@ -811,11 +805,9 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
         IgniteTable table = rel.getTable().unwrapOrThrow(IgniteTable.class);
         UpdatableTable updatableTable = resolvedDependencies.updatableTable(table.id());
         RelDataType rowType = rel.getInput().getRowType();
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
 
         ModifyNode<RowT> node = new ModifyNode<>(
-                ctx, updatableTable, rel.sourceId(), rel.getOperation(), rel.getUpdateColumnList(), rowFactory
+                ctx, updatableTable, rel.sourceId(), rel.getOperation(), rel.getUpdateColumnList(), convertStructuredType(rowType)
         );
 
         Node<RowT> input = visit(rel.getInput());
@@ -830,9 +822,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
     public Node<RowT> visit(IgniteReceiver rel) {
         RelDataType rowType = rel.getRowType();
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         RelCollation collation = rel.collation();
         Comparator<RowT> comp = null;
@@ -868,8 +858,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         HashAggregateNode<RowT> node = new HashAggregateNode<>(ctx, type, rel.getGroupSets(), accumulators, rowFactory);
 
@@ -897,8 +886,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         HashAggregateNode<RowT> node = new HashAggregateNode<>(ctx, type, rel.getGroupSets(), accumulators, rowFactory);
 
@@ -925,8 +913,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         HashAggregateNode<RowT> node = new HashAggregateNode<>(ctx, type, rel.getGroupSets(), accumulators, rowFactory);
 
@@ -954,8 +941,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         RelCollation collation = rel.collation();
         Comparator<RowT> comp = null;
@@ -1002,8 +988,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         RelCollation collation = rel.collation();
         Comparator<RowT> comp = null;
@@ -1049,8 +1034,7 @@ public class LogicalRelImplementor<RowT> implements IgniteRelVisitor<Node<RowT>>
             ).get(ctx);
         }
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<RowT> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         RelCollation collation = rel.collation();
         Comparator<RowT> comp = null;
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowHandler.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowHandler.java
index e0dee713cc6..2cf92d15e0e 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowHandler.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/RowHandler.java
@@ -20,7 +20,7 @@ package org.apache.ignite.internal.sql.engine.exec;
 import java.nio.ByteBuffer;
 import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryTuple;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -63,7 +63,7 @@ public interface RowHandler<RowT> {
     String toString(RowT row);
 
     /** Creates a factory that produces rows with fields defined by the given schema. */
-    RowFactory<RowT> factory(RowSchema rowSchema);
+    RowFactory<RowT> factory(StructNativeType rowSchema);
 
     /**
      * Provide methods for inner row assembly.
@@ -100,7 +100,7 @@ public interface RowHandler<RowT> {
          *
          * @return RowSchema.
          */
-        RowSchema rowSchema();
+        StructNativeType rowSchema();
 
         /**
          * The result row will satisfy the current factory's schema.
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/SqlRowHandler.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/SqlRowHandler.java
index c37540e83d5..3e5ccb61e2e 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/SqlRowHandler.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/SqlRowHandler.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec;
 
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.sql.engine.util.Commons.readValue;
+import static org.apache.ignite.sql.ColumnType.NULL;
 
 import java.math.BigDecimal;
 import java.time.Duration;
@@ -34,12 +35,11 @@ import org.apache.ignite.internal.lang.IgniteStringBuilder;
 import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchemaTypes;
-import org.apache.ignite.internal.sql.engine.exec.row.TypeSpec;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.type.DecimalNativeType;
 import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -55,12 +55,12 @@ import org.jetbrains.annotations.Nullable;
  *
  * <p>Factory method {@link RowFactory#create(InternalTuple)} allows to
  * create rows without any additional conversions. But the fields in
- * binary tuple must match the factory {@link RowSchema row schema}.
+ * binary tuple must match the factory {@link StructNativeType row schema}.
  */
 public class SqlRowHandler implements RowHandler<RowWrapper> {
     public static final RowHandler<RowWrapper> INSTANCE = new SqlRowHandler();
 
-    private static final ObjectsArrayRowWrapper EMPTY_ROW = new ObjectsArrayRowWrapper(RowSchema.builder().build(), new Object[0]);
+    private static final ObjectsArrayRowWrapper EMPTY_ROW = new ObjectsArrayRowWrapper(NativeTypes.rowBuilder().build(), new Object[0]);
 
     private SqlRowHandler() {
     }
@@ -104,7 +104,7 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
     }
 
     @Override
-    public RowFactory<RowWrapper> factory(RowSchema rowSchema) {
+    public RowFactory<RowWrapper> factory(StructNativeType rowSchema) {
         int schemaLen = rowSchema.fields().size();
 
         return new RowFactory<>() {
@@ -143,7 +143,7 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
 
             /** {@inheritDoc} */
             @Override
-            public RowSchema rowSchema() {
+            public StructNativeType rowSchema() {
                 return rowSchema;
             }
 
@@ -178,12 +178,12 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
      * Wrapper over an array of objects.
      */
     private static class ObjectsArrayRowWrapper extends RowWrapper {
-        private final RowSchema rowSchema;
+        private final StructNativeType rowType;
         private final Object[] row;
 
-        ObjectsArrayRowWrapper(RowSchema rowSchema, Object[] row) {
-            assert row.length == rowSchema.fields().size();
-            this.rowSchema = rowSchema;
+        ObjectsArrayRowWrapper(StructNativeType rowType, Object[] row) {
+            assert row.length == rowType.fields().size();
+            this.rowType = rowType;
             this.row = row;
         }
 
@@ -208,9 +208,9 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
             int estimatedSize = 0;
             boolean exactEstimate = true;
             for (int i = 0; i < row.length; i++) {
-                NativeType nativeType = RowSchemaTypes.toNativeType(rowSchema.fields().get(i));
+                NativeType nativeType = rowType.fields().get(i).type();
 
-                if (nativeType == null) {
+                if (nativeType.spec() == NULL) {
                     assert row[i] == null;
 
                     continue;
@@ -247,26 +247,24 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
             for (int i = 0; i < row.length; i++) {
                 Object value = row[i];
 
-                appendValue(tupleBuilder, rowSchema.fields().get(i), value);
+                appendValue(tupleBuilder, rowType.fields().get(i).type(), value);
             }
 
             return new BinaryTuple(row.length, tupleBuilder.build());
         }
 
-        private static void appendValue(BinaryTupleBuilder builder, TypeSpec schemaType, @Nullable Object value) {
+        private static void appendValue(BinaryTupleBuilder builder, NativeType type, @Nullable Object value) {
             if (value == null) {
                 builder.appendNull();
 
                 return;
             }
 
-            NativeType nativeType = RowSchemaTypes.toNativeType(schemaType);
+            value = TypeUtils.fromInternal(value, type.spec());
 
-            value = TypeUtils.fromInternal(value, nativeType.spec());
+            assert value != null : type;
 
-            assert value != null : nativeType;
-
-            switch (nativeType.spec()) {
+            switch (type.spec()) {
                 case BOOLEAN:
                     builder.appendBoolean((boolean) value);
                     break;
@@ -296,7 +294,7 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
                     break;
 
                 case DECIMAL:
-                    builder.appendDecimalNotNull((BigDecimal) value, ((DecimalNativeType) nativeType).scale());
+                    builder.appendDecimalNotNull((BigDecimal) value, ((DecimalNativeType) type).scale());
                     break;
 
                 case UUID:
@@ -336,7 +334,7 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
                     break;
 
                 default:
-                    throw new UnsupportedOperationException("Unknown type " + nativeType);
+                    throw new UnsupportedOperationException("Unknown type " + type);
             }
         }
     }
@@ -345,11 +343,11 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
      * Wrapper over an {@link BinaryTuple}.
      */
     private static class BinaryTupleRowWrapper extends RowWrapper {
-        private final RowSchema rowSchema;
+        private final StructNativeType rowType;
         private final InternalTuple tuple;
 
-        BinaryTupleRowWrapper(RowSchema rowSchema, InternalTuple tuple) {
-            this.rowSchema = rowSchema;
+        BinaryTupleRowWrapper(StructNativeType rowType, InternalTuple tuple) {
+            this.rowType = rowType;
             this.tuple = tuple;
         }
 
@@ -361,9 +359,9 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
         @Override
         @Nullable
         Object get(int field) {
-            NativeType nativeType = RowSchemaTypes.toNativeType(rowSchema.fields().get(field));
+            NativeType nativeType = rowType.fields().get(field).type();
 
-            if (nativeType == null) {
+            if (nativeType.spec() == NULL) {
                 return null;
             }
 
@@ -395,15 +393,15 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
 
         private final int schemaLen;
 
-        private final RowSchema rowSchema;
+        private final StructNativeType rowType;
 
         Object[] data;
 
         int fieldIdx;
 
-        RowBuilderImpl(RowSchema rowSchema) {
-            this.rowSchema = rowSchema;
-            this.schemaLen = rowSchema.fields().size();
+        RowBuilderImpl(StructNativeType rowType) {
+            this.rowType = rowType;
+            this.schemaLen = rowType.fields().size();
             fieldIdx = 0;
         }
 
@@ -425,7 +423,7 @@ public class SqlRowHandler implements RowHandler<RowWrapper> {
         public RowWrapper build() {
             checkState();
 
-            return rowSchema.fields().isEmpty() ? EMPTY_ROW : new ObjectsArrayRowWrapper(rowSchema, data);
+            return rowType.fields().isEmpty() ? EMPTY_ROW : new ObjectsArrayRowWrapper(rowType, data);
         }
 
         /** {@inheritDoc} */
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/UpdatableTableImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/UpdatableTableImpl.java
index b6e6f63b4b6..dee3c66b9dd 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/UpdatableTableImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/UpdatableTableImpl.java
@@ -22,7 +22,7 @@ import static org.apache.ignite.internal.partition.replicator.network.replicatio
 import static org.apache.ignite.internal.partition.replicator.network.replication.RequestType.RW_UPSERT_ALL;
 import static org.apache.ignite.internal.replicator.message.ReplicaMessageUtils.toReplicationGroupIdMessage;
 import static org.apache.ignite.internal.sql.engine.util.RowTypeUtils.rowType;
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 import static org.apache.ignite.internal.table.distributed.storage.InternalTableImpl.collectRejectedRowsResponses;
 import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
 import static org.apache.ignite.lang.ErrorGroups.Sql.CONSTRAINT_VIOLATION_ERR;
@@ -35,7 +35,6 @@ import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Supplier;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.util.Static;
 import org.apache.ignite.internal.components.NodeProperties;
@@ -54,7 +53,6 @@ import org.apache.ignite.internal.replicator.message.ReplicationGroupIdMessage;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryRowEx;
 import org.apache.ignite.internal.sql.engine.exec.mapping.ColocationGroup;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.prepare.IgniteSqlValidatorErrorMessages;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
@@ -63,6 +61,7 @@ import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.table.InternalTable;
 import org.apache.ignite.internal.table.distributed.storage.RowBatch;
 import org.apache.ignite.internal.tx.InternalTransaction;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.sql.SqlException;
 import org.jetbrains.annotations.Nullable;
@@ -97,7 +96,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
 
     private final TableRowConverter rowConverter;
 
-    private RowSchema rowSchema;
+    private StructNativeType rowSchema;
 
     /** Constructor. */
     UpdatableTableImpl(
@@ -137,7 +136,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
         validateNotNullConstraint(ectx.rowHandler(), rows);
 
         RelDataType rowType = rowType(descriptor(), ectx.getTypeFactory());
-        Supplier<RowSchema> schemaSupplier = makeSchemaSupplier(ectx);
+        Supplier<StructNativeType> schemaSupplier = makeSchemaSupplier(ectx);
 
         rows = validateCharactersOverflowAndTrimIfPossible(rowType, ectx.rowHandler(), rows, schemaSupplier);
 
@@ -228,7 +227,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
         validateNotNullConstraint(ectx.rowHandler(), row);
 
         RelDataType rowType = rowType(descriptor(), ectx.getTypeFactory());
-        Supplier<RowSchema> schemaSupplier = makeSchemaSupplier(ectx);
+        Supplier<StructNativeType> schemaSupplier = makeSchemaSupplier(ectx);
 
         RowT validatedRow = TypeUtils.validateStringTypesOverflowAndTrimIfPossible(rowType, ectx.rowHandler(), row, schemaSupplier);
 
@@ -259,7 +258,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
         validateNotNullConstraint(ectx.rowHandler(), rows);
 
         RelDataType rowType = rowType(descriptor(), ectx.getTypeFactory());
-        Supplier<RowSchema> schemaSupplier = makeSchemaSupplier(ectx);
+        Supplier<StructNativeType> schemaSupplier = makeSchemaSupplier(ectx);
 
         rows = validateCharactersOverflowAndTrimIfPossible(rowType, ectx.rowHandler(), rows, schemaSupplier);
 
@@ -387,8 +386,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
 
                     RowHandler<RowT> handler = ectx.rowHandler();
                     IgniteTypeFactory typeFactory = ectx.getTypeFactory();
-                    RowSchema rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType(desc, typeFactory)));
-                    RowHandler.RowFactory<RowT> rowFactory = handler.factory(rowSchema);
+                    RowHandler.RowFactory<RowT> rowFactory = handler.factory(convertStructuredType(rowType(desc, typeFactory)));
 
                     ArrayList<String> conflictRows = new ArrayList<>(response.size());
 
@@ -419,7 +417,7 @@ public final class UpdatableTableImpl implements UpdatableTable {
             RelDataType rowType,
             RowHandler<RowT> rowHandler,
             List<RowT> rows,
-            Supplier<RowSchema> schemaSupplier
+            Supplier<StructNativeType> schemaSupplier
     ) {
         List<RowT> out = new ArrayList<>(rows.size());
 
@@ -451,14 +449,14 @@ public final class UpdatableTableImpl implements UpdatableTable {
         }
     }
 
-    private <RowT> Supplier<RowSchema> makeSchemaSupplier(ExecutionContext<RowT> ectx) {
+    private <RowT> Supplier<StructNativeType> makeSchemaSupplier(ExecutionContext<RowT> ectx) {
         return () -> {
             if (rowSchema != null) {
                 return rowSchema;
             }
 
             RelDataType rowType = rowType(descriptor(), ectx.getTypeFactory());
-            rowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
+            rowSchema = convertStructuredType(rowType);
             return rowSchema;
         };
     }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
index 9dd8789d586..302a73b73d5 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactory.java
@@ -83,10 +83,10 @@ public interface ExpressionFactory<RowT> {
      * Creates a Project function. Resulting function returns a row with different fields, fields order, fields types, etc.
      *
      * @param projects Projection expressions.
-     * @param rowType  Input row type.
+     * @param inputRowType  Input row type.
      * @return Project function.
      */
-    SqlProjection<RowT> project(List<RexNode> projects, RelDataType rowType);
+    SqlProjection<RowT> project(List<RexNode> projects, RelDataType inputRowType);
 
     /**
      * Creates a Project function. Resulting function returns a row with different fields, fields order, fields types, etc.
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
index aee61662f05..373ff6e6086 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImpl.java
@@ -138,8 +138,8 @@ public class ExpressionFactoryImpl<RowT> implements ExpressionFactory<RowT> {
 
     /** {@inheritDoc} */
     @Override
-    public SqlProjection<RowT> project(List<RexNode> projects, RelDataType rowType) {
-        return projectionImplementor.implement(projects, rowType);
+    public SqlProjection<RowT> project(List<RexNode> projects, RelDataType inputRowType) {
+        return projectionImplementor.implement(projects, inputRowType);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/JoinProjectionImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/JoinProjectionImplementor.java
index 920f65865a9..d4e9c12e719 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/JoinProjectionImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/JoinProjectionImplementor.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
 
 import static org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl.digest;
 import static org.apache.ignite.internal.sql.engine.util.Commons.cast;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.structuredTypeFromRelTypeList;
 
 import java.lang.reflect.Modifier;
 import java.util.List;
@@ -40,11 +41,10 @@ import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
 import org.apache.ignite.internal.sql.engine.exec.exp.RexToLixTranslator.InputGetter;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.IgniteMethod;
-import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.sql.engine.util.cache.Cache;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.sql.SqlException;
 
@@ -82,10 +82,9 @@ class JoinProjectionImplementor {
         Cache<String, SqlJoinProjection<RowT>> cache = cast(this.cache);
 
         return cache.get(digest, key -> {
-            RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(RexUtil.types(projections));
             SqlJoinProjectionExt<RowT> projectionExt = implementInternal(projections, type, firstRowSize);
 
-            return new SqlJoinProjectionImpl<>(projectionExt, rowSchema);
+            return new SqlJoinProjectionImpl<>(projectionExt, structuredTypeFromRelTypeList(RexUtil.types(projections)));
         });
     }
 
@@ -152,21 +151,15 @@ class JoinProjectionImplementor {
 
     private static class SqlJoinProjectionImpl<RowT> implements SqlJoinProjection<RowT> {
         private final SqlJoinProjectionExt<RowT> projection;
-        private final RowSchema rowSchema;
-
-        /**
-         * Constructor.
-         *
-         * @param projection Scalar.
-         * @param rowSchema Row factory.
-         */
-        private SqlJoinProjectionImpl(SqlJoinProjectionExt<RowT> projection, RowSchema rowSchema) {
+        private final StructNativeType rowType;
+
+        private SqlJoinProjectionImpl(SqlJoinProjectionExt<RowT> projection, StructNativeType rowType) {
             this.projection = projection;
-            this.rowSchema = rowSchema;
+            this.rowType = rowType;
         }
 
         private RowBuilder<RowT> builder(ExecutionContext<RowT> context) {
-            return context.rowHandler().factory(rowSchema).rowBuilder();
+            return context.rowHandler().factory(rowType).rowBuilder();
         }
 
         @Override
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ProjectionImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ProjectionImplementor.java
index 87e18b5d420..c6c07d3ded0 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ProjectionImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ProjectionImplementor.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
 
 import static org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl.digest;
 import static org.apache.ignite.internal.sql.engine.util.Commons.cast;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.structuredTypeFromRelTypeList;
 
 import java.lang.reflect.Modifier;
 import java.util.List;
@@ -40,11 +41,10 @@ import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
 import org.apache.ignite.internal.sql.engine.exec.exp.RexToLixTranslator.InputGetter;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.IgniteMethod;
-import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.sql.engine.util.cache.Cache;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.sql.SqlException;
 
@@ -71,25 +71,24 @@ class ProjectionImplementor {
      * Implements given list of projections as {@link SqlProjection}.
      *
      * @param projections The list of projections, i.e. expressions used to compute a new row.
-     * @param type The type of the input row.
+     * @param inputRowType The type of the input row.
      * @param <RowT> The type of the execution row.
      * @return An implementation of projection.
      * @see SqlProjection
      */
-    public <RowT> SqlProjection<RowT> implement(List<RexNode> projections, RelDataType type) {
-        String digest = digest(SqlProjection.class, projections, type);
+    public <RowT> SqlProjection<RowT> implement(List<RexNode> projections, RelDataType inputRowType) {
+        String digest = digest(SqlProjection.class, projections, inputRowType);
         Cache<String, SqlProjection<RowT>> cache = cast(this.cache);
 
         return cache.get(digest, key -> {
-            RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(RexUtil.types(projections));
-            SqlProjectionExt<RowT> projectionExt = implementInternal(projections, type);
+            SqlProjectionExt<RowT> projectionExt = implementInternal(projections, inputRowType);
 
-            return new SqlProjectionImpl<>(projectionExt, rowSchema);
+            return new SqlProjectionImpl<>(projectionExt, structuredTypeFromRelTypeList(RexUtil.types(projections)));
         });
     }
 
-    private <RowT> SqlProjectionExt<RowT> implementInternal(List<RexNode> projections, RelDataType type) {
-        RexProgramBuilder programBuilder = new RexProgramBuilder(type, rexBuilder);
+    private <RowT> SqlProjectionExt<RowT> implementInternal(List<RexNode> projections, RelDataType inputRowType) {
+        RexProgramBuilder programBuilder = new RexProgramBuilder(inputRowType, rexBuilder);
 
         for (RexNode node : projections) {
             assert node != null : "unexpected nullable node";
@@ -111,7 +110,7 @@ class ProjectionImplementor {
 
         Expression rowHandler = builder.append("hnd", Expressions.call(ctx, IgniteMethod.CONTEXT_ROW_HANDLER.method()));
 
-        InputGetter inputGetter = new FieldGetter(rowHandler, row, type);
+        InputGetter inputGetter = new FieldGetter(rowHandler, row, inputRowType);
 
         Function1<String, InputGetter> correlates = new CorrelatesBuilder(builder, ctx, rowHandler).build(projections);
 
@@ -150,21 +149,15 @@ class ProjectionImplementor {
 
     private static class SqlProjectionImpl<RowT> implements SqlProjection<RowT> {
         private final SqlProjectionExt<RowT> projection;
-        private final RowSchema rowSchema;
-
-        /**
-         * Constructor.
-         *
-         * @param projection Scalar.
-         * @param rowSchema Row factory.
-         */
-        private SqlProjectionImpl(SqlProjectionExt<RowT> projection, RowSchema rowSchema) {
+        private final StructNativeType rowType;
+
+        private SqlProjectionImpl(SqlProjectionExt<RowT> projection, StructNativeType rowType) {
             this.projection = projection;
-            this.rowSchema = rowSchema;
+            this.rowType = rowType;
         }
 
         private RowBuilder<RowT> builder(ExecutionContext<RowT> context) {
-            return context.rowHandler().factory(rowSchema).rowBuilder();
+            return context.rowHandler().factory(rowType).rowBuilder();
         }
 
         @Override
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RowProviderImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RowProviderImplementor.java
index 78c0a2362c3..f500e85c6aa 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RowProviderImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RowProviderImplementor.java
@@ -43,13 +43,13 @@ import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
 import org.apache.ignite.internal.sql.engine.exec.exp.RexToLixTranslator.InputGetter;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.IgniteMethod;
 import org.apache.ignite.internal.sql.engine.util.Primitives;
 import org.apache.ignite.internal.sql.engine.util.RexUtils;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.sql.engine.util.cache.Cache;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.sql.SqlException;
 
@@ -98,9 +98,9 @@ class RowProviderImplementor {
                 Cache<String, SqlRowProvider<RowT>> cache = cast(this.cache);
 
                 return cache.get(digest, key -> {
-                    RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(typeList);
+                    StructNativeType rowType = TypeUtils.structuredTypeFromRelTypeList(typeList);
 
-                    return new SqlRowProviderImpl<>(implementInternal(values), rowSchema);
+                    return new SqlRowProviderImpl<>(implementInternal(values), rowType);
                 });
             }
 
@@ -110,8 +110,8 @@ class RowProviderImplementor {
             literalValues.add((RexLiteral) values.get(i));
         }
 
-        RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(typeList);
-        return new ConstantRow<>(literalValues, types, rowSchema);
+        StructNativeType rowType = TypeUtils.structuredTypeFromRelTypeList(typeList);
+        return new ConstantRow<>(literalValues, types, rowType);
     }
 
     private <RowT> SqlRowProviderExt<RowT> implementInternal(List<RexNode> values) {
@@ -174,8 +174,8 @@ class RowProviderImplementor {
     private static class SqlRowProviderImpl<RowT> extends AbstractRowProvider<RowT> {
         private final SqlRowProviderExt<RowT> rowProvider;
 
-        private SqlRowProviderImpl(SqlRowProviderExt<RowT> rowProvider, RowSchema rowSchema) {
-            super(rowSchema);
+        private SqlRowProviderImpl(SqlRowProviderExt<RowT> rowProvider, StructNativeType rowType) {
+            super(rowType);
 
             this.rowProvider = rowProvider;
         }
@@ -190,8 +190,8 @@ class RowProviderImplementor {
         private final List<RexLiteral> values;
         private final List<Class<?>> types;
 
-        private ConstantRow(List<RexLiteral> values, List<Class<?>> types, RowSchema rowSchema) {
-            super(rowSchema);
+        private ConstantRow(List<RexLiteral> values, List<Class<?>> types, StructNativeType rowType) {
+            super(rowType);
 
             this.values = values;
             this.types = types;
@@ -211,14 +211,14 @@ class RowProviderImplementor {
     }
 
     private abstract static class AbstractRowProvider<RowT> implements SqlRowProvider<RowT> {
-        private final RowSchema rowSchema;
+        private final StructNativeType rowType;
 
-        private AbstractRowProvider(RowSchema rowSchema) {
-            this.rowSchema = rowSchema;
+        private AbstractRowProvider(StructNativeType rowType) {
+            this.rowType = rowType;
         }
 
         private RowBuilder<RowT> builder(ExecutionContext<RowT> context) {
-            return context.rowHandler().factory(rowSchema).rowBuilder();
+            return context.rowHandler().factory(rowType).rowBuilder();
         }
 
         abstract void buildRow(ExecutionContext<RowT> context, RowBuilder<RowT> rowBuilder);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ValuesImplementor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ValuesImplementor.java
index 8c1bb0c881d..178d7dd1454 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ValuesImplementor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/ValuesImplementor.java
@@ -26,10 +26,10 @@ import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.Primitives;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.type.StructNativeType;
 
 /** Implementor which implements {@link SqlScalar} returning list of rows. */
 class ValuesImplementor {
@@ -51,12 +51,12 @@ class ValuesImplementor {
     <RowT> SqlScalar<RowT, List<RowT>> implement(List<List<RexLiteral>> values, RelDataType rowType) {
         List<RelDataType> typeList = RelOptUtil.getFieldTypeList(rowType);
 
-        RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(typeList);
+        StructNativeType structNativeType = TypeUtils.structuredTypeFromRelTypeList(typeList);
         List<Class<?>> types = Commons.transform(typeList, type -> Primitives.wrap((Class<?>) typeFactory.getJavaClass(type)));
 
         return context -> {
             List<RowT> results = new ArrayList<>(values.size());
-            RowBuilder<RowT> rowBuilder = context.rowHandler().factory(rowSchema).rowBuilder();
+            RowBuilder<RowT> rowBuilder = context.rowHandler().factory(structNativeType).rowBuilder();
 
             for (List<RexLiteral> row : values) {
                 for (int i = 0; i < types.size(); i++) {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/func/SystemRangeTableFunction.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/func/SystemRangeTableFunction.java
index 908ab5cab9f..f2d84fcf873 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/func/SystemRangeTableFunction.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/func/SystemRangeTableFunction.java
@@ -22,16 +22,16 @@ import java.util.Objects;
 import java.util.function.Supplier;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.sql.fun.IgniteSqlOperatorTable;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /** Implementation of {@link IgniteSqlOperatorTable#SYSTEM_RANGE system range function}. */
 public final class SystemRangeTableFunction<RowT> implements TableFunction<RowT> {
 
-    private final RowSchema rowSchema = RowSchema.builder()
-            .addField(NativeTypes.INT64)
+    private final StructNativeType rowSchema = NativeTypes.rowBuilder()
+            .addField("X", NativeTypes.INT64, false)
             .build();
 
     private final Supplier<Long> startExpr;
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
index 2cf74b79a4b..9013a1752cc 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
@@ -18,7 +18,7 @@
 package org.apache.ignite.internal.sql.engine.exec.rel;
 
 import static org.apache.ignite.internal.sql.engine.util.Commons.cast;
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
 import java.util.ArrayList;
@@ -29,7 +29,6 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.function.BiPredicate;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.core.JoinInfo;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
@@ -37,7 +36,7 @@ import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlJoinProjection;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /** HashJoin implementor. */
@@ -104,7 +103,7 @@ public abstract class HashJoinNode<RowT> extends AbstractRightMaterializedJoinNo
             case LEFT: {
                 assert projection != null;
 
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
                 RowHandler.RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
 
                 return new LeftHashJoin<>(ctx, joinInfo, projection, rightRowFactory, nonEquiCondition);
@@ -112,7 +111,7 @@ public abstract class HashJoinNode<RowT> extends AbstractRightMaterializedJoinNo
             case RIGHT: {
                 assert projection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
                 RowHandler.RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
 
                 return new RightHashJoin<>(ctx, joinInfo, projection, leftRowFactory, nonEquiCondition);
@@ -120,8 +119,8 @@ public abstract class HashJoinNode<RowT> extends AbstractRightMaterializedJoinNo
             case FULL: {
                 assert projection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
 
                 RowHandler.RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
                 RowHandler.RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinNode.java
index c7cbb2a0367..d381ef47d13 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinNode.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/MergeJoinNode.java
@@ -17,7 +17,7 @@
 
 package org.apache.ignite.internal.sql.engine.exec.rel;
 
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
 
 import java.util.ArrayDeque;
@@ -25,7 +25,6 @@ import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Deque;
 import java.util.List;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.ignite.internal.lang.IgniteStringBuilder;
@@ -33,7 +32,7 @@ import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlJoinProjection;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -227,7 +226,7 @@ public abstract class MergeJoinNode<RowT> extends AbstractNode<RowT> {
             case LEFT: {
                 assert outputProjection != null;
 
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
                 RowHandler.RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
 
                 return new LeftJoin<>(ctx, comp, outputProjection, rightRowFactory);
@@ -236,7 +235,7 @@ public abstract class MergeJoinNode<RowT> extends AbstractNode<RowT> {
             case RIGHT: {
                 assert outputProjection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
                 RowHandler.RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
 
                 return new RightJoin<>(ctx, comp, outputProjection, leftRowFactory);
@@ -245,8 +244,8 @@ public abstract class MergeJoinNode<RowT> extends AbstractNode<RowT> {
             case FULL: {
                 assert outputProjection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
 
                 RowHandler.RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
                 RowHandler.RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNode.java
index 4506cfc9d9a..98f92d35258 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNode.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNode.java
@@ -34,12 +34,13 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.UpdatableTable;
 import org.apache.ignite.internal.sql.engine.exec.mapping.ColocationGroup;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.util.Pair;
 import org.jetbrains.annotations.Nullable;
 
@@ -76,8 +77,8 @@ import org.jetbrains.annotations.Nullable;
  */
 public class ModifyNode<RowT> extends AbstractNode<RowT> implements SingleNode<RowT>, Downstream<RowT> {
 
-    private static final RowSchema MODIFY_RESULT = RowSchema.builder()
-            .addField(NativeTypes.INT64)
+    private static final StructNativeType MODIFY_RESULT = NativeTypes.rowBuilder()
+            .addField("UPDATE_COUNT", NativeTypes.INT64, false)
             .build();
 
     private final TableModify.Operation modifyOp;
@@ -114,7 +115,7 @@ public class ModifyNode<RowT> extends AbstractNode<RowT> implements SingleNode<R
      * @param sourceId Source id.
      * @param op A type of the update operation.
      * @param updateColumns Enumeration of columns to update if applicable.
-     * @param inputRowFactory Row factory for input row.
+     * @param inputRowType Input row type.
      */
     public ModifyNode(
             ExecutionContext<RowT> ctx,
@@ -122,7 +123,7 @@ public class ModifyNode<RowT> extends AbstractNode<RowT> implements SingleNode<R
             long sourceId,
             TableModify.Operation op,
             @Nullable List<String> updateColumns,
-            RowFactory<RowT> inputRowFactory
+            StructNativeType inputRowType
     ) {
         super(ctx);
 
@@ -131,9 +132,7 @@ public class ModifyNode<RowT> extends AbstractNode<RowT> implements SingleNode<R
         this.modifyOp = op;
         this.updateColumns = updateColumns;
 
-        RowSchema rowSchema = inputRowFactory.rowSchema();
-        int fullRowSize = rowSchema.fields().size();
-        this.mapping = mapping(table.descriptor(), updateColumns, fullRowSize);
+        this.mapping = mapping(table.descriptor(), updateColumns, inputRowType.fieldsCount());
 
         // insert mapping actually can be required only for INSERT and MERGE operations
         if (op == Operation.INSERT || op == Operation.MERGE) {
@@ -145,8 +144,8 @@ public class ModifyNode<RowT> extends AbstractNode<RowT> implements SingleNode<R
             this.insertRowMapping = null;
         }
 
-        RowSchema mappedRowSchema =  mapping != null ? RowSchema.map(rowSchema, mapping) : rowSchema;
-        RowSchema mappedInsertRowSchema =  insertRowMapping != null ? RowSchema.map(rowSchema, insertRowMapping) : rowSchema;
+        StructNativeType mappedRowSchema =  mapping != null ? TypeUtils.map(inputRowType, mapping) : inputRowType;
+        StructNativeType mappedInsertRowSchema =  insertRowMapping != null ? TypeUtils.map(inputRowType, insertRowMapping) : inputRowType;
 
         this.mappedRowFactory = ctx.rowHandler().factory(mappedRowSchema);
         this.mappedInsertRowFactory = ctx.rowHandler().factory(mappedInsertRowSchema);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/NestedLoopJoinNode.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/NestedLoopJoinNode.java
index 8792861c3be..3d86ee417f1 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/NestedLoopJoinNode.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/NestedLoopJoinNode.java
@@ -17,20 +17,19 @@
 
 package org.apache.ignite.internal.sql.engine.exec.rel;
 
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.List;
 import java.util.PrimitiveIterator;
 import java.util.function.BiPredicate;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlJoinProjection;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -105,7 +104,7 @@ public abstract class NestedLoopJoinNode<RowT> extends AbstractRightMaterialized
             case LEFT: {
                 assert joinProjection != null;
 
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
                 RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
 
                 return new LeftJoin<>(ctx, cond, joinProjection, rightRowFactory);
@@ -114,7 +113,7 @@ public abstract class NestedLoopJoinNode<RowT> extends AbstractRightMaterialized
             case RIGHT: {
                 assert joinProjection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
                 RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
 
                 return new RightJoin<>(ctx, cond, joinProjection, leftRowFactory);
@@ -123,8 +122,8 @@ public abstract class NestedLoopJoinNode<RowT> extends AbstractRightMaterialized
             case FULL: {
                 assert joinProjection != null;
 
-                RowSchema leftRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(leftRowType));
-                RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightRowType));
+                StructNativeType leftRowSchema = convertStructuredType(leftRowType);
+                StructNativeType rightRowSchema = convertStructuredType(rightRowType);
 
                 RowFactory<RowT> leftRowFactory = ctx.rowHandler().factory(leftRowSchema);
                 RowFactory<RowT> rightRowFactory = ctx.rowHandler().factory(rightRowSchema);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/BaseTypeSpec.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/BaseTypeSpec.java
deleted file mode 100644
index 55c5453d67f..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/BaseTypeSpec.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import java.util.Objects;
-import org.apache.ignite.internal.tostring.S;
-import org.apache.ignite.internal.type.NativeType;
-
-/**
- * Base type represents a {@link NativeType}.
- */
-public class BaseTypeSpec extends TypeSpec {
-
-    private final NativeType nativeType;
-
-    /** Creates a basic type with default nullability (non-nullable). */
-    BaseTypeSpec(NativeType nativeType) {
-        super(false);
-        this.nativeType = nativeType;
-    }
-
-    /** Creates a basic type with the given nullability. */
-    public BaseTypeSpec(NativeType nativeType, boolean nullable) {
-        super(nullable);
-        // TODO Uncomment his check after https://issues.apache.org/jira/browse/IGNITE-20163 is fixed
-        // this.nativeType = Objects.requireNonNull(nativeType, "native type has not been specified.");
-        this.nativeType = nativeType;
-    }
-
-    /** Returns a native type that this base type represents. */
-    public NativeType nativeType() {
-        return nativeType;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        if (!super.equals(o)) {
-            return false;
-        }
-        BaseTypeSpec that = (BaseTypeSpec) o;
-        return Objects.equals(nativeType, that.nativeType);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public int hashCode() {
-        return Objects.hash(super.hashCode(), nativeType);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public String toString() {
-        return S.toString(BaseTypeSpec.class, this, "nullable", isNullable());
-    }
-
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/NullTypeSpec.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/NullTypeSpec.java
deleted file mode 100644
index eb6fb8227d7..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/NullTypeSpec.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import org.apache.ignite.internal.tostring.S;
-
-/** A type for untyped null. This type is used to represent untyped null produced the optimizer. */
-public final class NullTypeSpec extends TypeSpec {
-
-    /** Constructor. */
-    NullTypeSpec() {
-        super(true);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public int hashCode() {
-        return super.hashCode();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean equals(Object obj) {
-        return super.equals(obj);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public String toString() {
-        return S.toString(NullTypeSpec.class, this);
-    }
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchema.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchema.java
deleted file mode 100644
index 1d3cbb693eb..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchema.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import org.apache.ignite.internal.tostring.S;
-import org.apache.ignite.internal.type.NativeType;
-
-/**
- * Row schema used by execution engine.
- */
-public final class RowSchema {
-    private final List<TypeSpec> fields;
-
-    private RowSchema(List<TypeSpec> types) {
-        this.fields = types;
-    }
-
-    /** A list of schema fields. */
-    public List<TypeSpec> fields() {
-        return fields;
-    }
-
-
-    /** {@inheritDoc}. */
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        RowSchema rowSchema = (RowSchema) o;
-        return Objects.equals(fields, rowSchema.fields);
-    }
-
-    /** {@inheritDoc}. */
-    @Override
-    public int hashCode() {
-        return Objects.hash(fields);
-    }
-
-    /** {@inheritDoc}. */
-    @Override
-    public String toString() {
-        return S.toString(RowSchema.class, this, "fields", this.fields);
-    }
-
-    /** Creates a builder that creates instances of row schemas. */
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    /** Row schema builder. */
-    public static class Builder {
-
-        private final List<TypeSpec> types = new ArrayList<>();
-
-        private Builder() {
-
-        }
-
-        /** Adds a field of the given type. */
-        public Builder addField(TypeSpec typeSpec) {
-            types.add(typeSpec);
-            return this;
-        }
-
-        /** Adds a field of the given non-nullable native type. */
-        public Builder addField(NativeType nativeType) {
-            return addField(nativeType, false);
-        }
-
-        /** Adds a field of the given native type with the specified nullability. */
-        public Builder addField(NativeType nativeType, boolean nullable) {
-            types.add(new BaseTypeSpec(nativeType, nullable));
-            return this;
-        }
-
-        /** Creates an instance of row schema. */
-        public RowSchema build() {
-            return new RowSchema(types);
-        }
-    }
-
-    /**
-     * Creates a new schema based on original schema and provided mapping.
-     */
-    public static RowSchema map(RowSchema schema, int[] mapping) {
-        assert mapping != null && mapping.length > 0;
-
-        List<TypeSpec> fields = schema.fields();
-        Builder builder = builder();
-
-        for (int i : mapping) {
-            TypeSpec typeSpec = fields.get(i);
-            builder.addField(typeSpec);
-        }
-
-        return builder.build();
-    }
-
-    /**
-     * Creates a new schema as a concatenation of the given schemas.
-     */
-    public static RowSchema concat(RowSchema left, RowSchema right) {
-        Builder builder = builder();
-
-        for (TypeSpec typeSpec : left.fields()) {
-            builder.addField(typeSpec);
-        }
-        for (TypeSpec typeSpec : right.fields()) {
-            builder.addField(typeSpec);
-        }
-
-        return builder.build();
-    }
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypes.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypes.java
deleted file mode 100644
index 97835612d85..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypes.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
-import org.apache.ignite.internal.type.NativeType;
-import org.apache.ignite.internal.type.NativeTypes;
-import org.apache.ignite.internal.util.Pair;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Defines methods to provide instances of {@link TypeSpec}s.
- */
-public final class RowSchemaTypes {
-
-    /** NULL type. */
-    public static final NullTypeSpec NULL = new NullTypeSpec();
-
-    /** Map of {@code <not nullable, nullable>} {@link BaseTypeSpec}s.*/
-    static final Map<NativeType, Pair<BaseTypeSpec, BaseTypeSpec>> INTERNED_TYPES;
-
-    static {
-        List<NativeType> nativeTypeList = Arrays.asList(
-                NativeTypes.BOOLEAN,
-                NativeTypes.INT8,
-                NativeTypes.INT16,
-                NativeTypes.INT32,
-                NativeTypes.INT64,
-                NativeTypes.FLOAT,
-                NativeTypes.DOUBLE,
-                NativeTypes.STRING,
-                NativeTypes.BYTES,
-                NativeTypes.DATE,
-                NativeTypes.UUID
-        );
-
-        INTERNED_TYPES = nativeTypeList.stream().map(t -> {
-            BaseTypeSpec notNull = new BaseTypeSpec(t, false);
-            BaseTypeSpec nullable = new BaseTypeSpec(t, true);
-
-            return Map.entry(t, new Pair<>(notNull, nullable));
-        }).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
-    }
-
-    private RowSchemaTypes() {
-
-    }
-
-    /** Returns a non-nullable {@link BaseTypeSpec} that corresponds to the given native type without nullability. */
-    public static BaseTypeSpec nativeType(NativeType nativeType) {
-        return nativeTypeWithNullability(nativeType, false);
-    }
-
-    /** Returns a {@link BaseTypeSpec} that corresponds to the given native type with the specified nullability. */
-    public static BaseTypeSpec nativeTypeWithNullability(NativeType nativeType, boolean nullable) {
-        Pair<BaseTypeSpec, BaseTypeSpec> typePair = INTERNED_TYPES.get(nativeType);
-
-        if (typePair != null) {
-            return nullable ? typePair.getSecond() : typePair.getFirst();
-        } else {
-            return new BaseTypeSpec(nativeType, nullable);
-        }
-    }
-
-    /**
-     * Convert specified schema {@link TypeSpec type} to {@link NativeType native type}.
-     *
-     * @param type Row schema type.
-     * @return Native type or {@code null} if type is a {@link NullTypeSpec}.
-     * @throws IllegalArgumentException If provided type cannot be converted to a native type.
-     */
-    public static @Nullable NativeType toNativeType(TypeSpec type) {
-        if (type instanceof NullTypeSpec) {
-            return null;
-        }
-
-        if (!(type instanceof BaseTypeSpec)) {
-            throw new IllegalArgumentException("Unexpected type; " + type);
-        }
-
-        NativeType nativeType = ((BaseTypeSpec) type).nativeType();
-
-        assert nativeType != null : type;
-
-        return nativeType;
-    }
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowType.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowType.java
deleted file mode 100644
index f0615b4c0f7..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/RowType.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import java.util.List;
-import java.util.Objects;
-import org.apache.ignite.internal.tostring.S;
-
-/**
- * Row type is a type that consists of a list of other {@link TypeSpec}s.
- */
-public class RowType extends TypeSpec {
-
-    private final List<TypeSpec> fields;
-
-    /** Creates a row type with the given nullability. */
-    public RowType(List<TypeSpec> fields, boolean nullable) {
-        super(nullable);
-        if (fields.isEmpty()) {
-            throw new IllegalArgumentException("Field list can not be empty");
-        }
-        this.fields = fields;
-    }
-
-    /** A list of types this row consists of. */
-    public List<TypeSpec> fields() {
-        return fields;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        if (!super.equals(o)) {
-            return false;
-        }
-        RowType that = (RowType) o;
-        return Objects.equals(fields, that.fields);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public int hashCode() {
-        return Objects.hash(super.hashCode(), fields);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public String toString() {
-        return S.toString(RowType.class, this, "nullable", isNullable());
-    }
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/TypeSpec.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/TypeSpec.java
deleted file mode 100644
index 07a8e86d143..00000000000
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/row/TypeSpec.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import java.util.Objects;
-
-/**
- * Base class for types of fields of {@link RowSchema}.
- */
-public abstract class TypeSpec {
-
-    private final boolean nullable;
-
-    /** Constructor. */
-    public TypeSpec(boolean nullable) {
-        this.nullable = nullable;
-    }
-
-    /** Returns {@code true} if values of this type can be {@code null}. */
-    public boolean isNullable() {
-        return nullable;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        TypeSpec typeSpec = (TypeSpec) o;
-        return nullable == typeSpec.nullable;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public int hashCode() {
-        return Objects.hash(nullable);
-    }
-}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/KeyValueGetPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/KeyValueGetPlan.java
index 683a63757fb..97678c141a7 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/KeyValueGetPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/KeyValueGetPlan.java
@@ -18,13 +18,13 @@
 package org.apache.ignite.internal.sql.engine.prepare;
 
 import static org.apache.ignite.internal.sql.engine.util.Commons.cast;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.util.ImmutableIntList;
@@ -45,7 +45,6 @@ import org.apache.ignite.internal.sql.engine.exec.ScannableTable;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlPredicate;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlProjection;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlRowProvider;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.prepare.partitionawareness.PartitionAwarenessMetadata;
 import org.apache.ignite.internal.sql.engine.prepare.pruning.PartitionPruningMetadata;
 import org.apache.ignite.internal.sql.engine.rel.IgniteKeyValueGet;
@@ -57,6 +56,7 @@ import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.IteratorToDataCursorAdapter;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.tx.InternalTransaction;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.sql.ResultSetMetadata;
 import org.jetbrains.annotations.Nullable;
 
@@ -175,8 +175,9 @@ public class KeyValueGetPlan implements ExplainablePlan, ExecutablePlan {
         SqlProjection<RowT> projection = projectionExpr == null ? null : ctx.expressionFactory().project(projectionExpr, rowType);
 
         RowHandler<RowT> rowHandler = ctx.rowHandler();
-        RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rowType));
-        RowFactory<RowT> rowFactory = rowHandler.factory(rowSchema);
+        StructNativeType nativeType = convertStructuredType(rowType);
+
+        RowFactory<RowT> rowFactory = rowHandler.factory(nativeType);
 
         List<RexNode> keyExpressions = lookupNode.keyExpressions();
         SqlRowProvider<RowT> keySupplier = ctx.expressionFactory().rowSource(keyExpressions);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/SelectCountPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/SelectCountPlan.java
index 6bff7395ade..e851b600357 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/SelectCountPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/SelectCountPlan.java
@@ -40,7 +40,6 @@ import org.apache.ignite.internal.sql.engine.exec.ExecutableTableRegistry;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlProjection;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.prepare.partitionawareness.PartitionAwarenessMetadata;
 import org.apache.ignite.internal.sql.engine.prepare.pruning.PartitionPruningMetadata;
 import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
@@ -53,6 +52,7 @@ import org.apache.ignite.internal.sql.engine.util.IteratorToDataCursorAdapter;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.sql.ResultSetMetadata;
 import org.jetbrains.annotations.Nullable;
 
@@ -171,13 +171,12 @@ public class SelectCountPlan implements ExplainablePlan, ExecutablePlan {
 
         RowHandler<RowT> rowHandler = ctx.rowHandler();
         SchemaAwareConverter<Object, Object> internalTypeConverter = TypeUtils.resultTypeConverter(resultType);
+        StructNativeType rowType = NativeTypes.rowBuilder()
+                .addField("COUNT", NativeTypes.INT64, false)
+                .build();
 
         return rowCount -> {
-            RowSchema rowSchema = RowSchema.builder()
-                    .addField(NativeTypes.INT64)
-                    .build();
-
-            RowT rowCountRow = ctx.rowHandler().factory(rowSchema)
+            RowT rowCountRow = ctx.rowHandler().factory(rowType)
                     .rowBuilder()
                     .addField(rowCount)
                     .build();
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
index 570fc8e947b..cda666dc12d 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
@@ -43,7 +43,9 @@ import org.apache.calcite.avatica.util.ByteString;
 import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rel.type.RelRecordType;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.parser.SqlParserPos;
@@ -55,6 +57,7 @@ import org.apache.ignite.internal.sql.engine.util.IgniteCustomAssignmentsRules;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -235,16 +238,21 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
     }
 
     /**
-     * Gets ColumnType type for RelDataType.
+     * Converts given {@link RelDataType relation type} to {@link NativeType native type}.
      *
      * @param relType Rel type.
-     * @return ColumnType type or null.
+     * @return Native type. Never null.
+     * @throws IllegalArgumentException If conversion is not supported for given relation type.
      */
-    public static NativeType relDataTypeToNative(RelDataType relType) {
+    public static NativeType relDataTypeToNative(RelDataType relType) throws IllegalArgumentException {
         assert relType instanceof BasicSqlType
-                || relType instanceof IntervalSqlType : "Not supported:" + relType;
+                || relType instanceof IntervalSqlType
+                || relType instanceof RelRecordType
+                : "Not supported: " + relType;
 
         switch (relType.getSqlTypeName()) {
+            case NULL:
+                return NativeTypes.NULL;
             case BOOLEAN:
                 return NativeTypes.BOOLEAN;
             case TINYINT:
@@ -277,7 +285,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
             case INTERVAL_YEAR_MONTH:
             case INTERVAL_MONTH:
                 // TODO: https://issues.apache.org/jira/browse/IGNITE-17373
-                throw new IllegalArgumentException("Type is not supported yet: " + relType);
+                return NativeTypes.PERIOD;
             case INTERVAL_DAY:
             case INTERVAL_DAY_HOUR:
             case INTERVAL_DAY_MINUTE:
@@ -289,7 +297,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
             case INTERVAL_MINUTE_SECOND:
             case INTERVAL_SECOND:
                 // TODO: https://issues.apache.org/jira/browse/IGNITE-17373
-                throw new IllegalArgumentException("Type is not supported yet:" + relType);
+                return NativeTypes.DURATION;
             case VARCHAR:
             case CHAR:
                 return relType.getPrecision() == PRECISION_NOT_SPECIFIED
@@ -302,6 +310,14 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
                         : NativeTypes.blobOf(relType.getPrecision());
             case UUID:
                 return NativeTypes.UUID;
+            case ROW:
+                RowTypeBuilder builder = NativeTypes.rowBuilder();
+
+                for (RelDataTypeField field : relType.getFieldList()) {
+                    builder.addField(field.getName(), relDataTypeToNative(field.getType()), field.getType().isNullable());
+                }
+
+                return builder.build(); 
             case ANY:
             default:
                 throw new IllegalArgumentException("Type is not supported: " + relType);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
index 527afe7231e..eddc3196661 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
@@ -32,7 +32,6 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.Period;
 import java.time.ZoneOffset;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.HashSet;
@@ -49,8 +48,6 @@ import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeField;
-import org.apache.calcite.sql.type.BasicSqlType;
-import org.apache.calcite.sql.type.IntervalSqlType;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeName.Limit;
 import org.apache.calcite.sql.type.SqlTypeUtil;
@@ -58,16 +55,13 @@ import org.apache.ignite.internal.sql.engine.SchemaAwareConverter;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
-import org.apache.ignite.internal.sql.engine.exec.row.BaseTypeSpec;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchemaTypes;
-import org.apache.ignite.internal.sql.engine.exec.row.RowType;
-import org.apache.ignite.internal.sql.engine.exec.row.TypeSpec;
 import org.apache.ignite.internal.sql.engine.prepare.ParameterType;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.type.DecimalNativeType;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.type.TemporalNativeType;
 import org.apache.ignite.internal.type.VarlenNativeType;
 import org.apache.ignite.sql.ColumnMetadata;
@@ -165,7 +159,9 @@ public class TypeUtils {
 
     private static class SupportedParamClassesHolder {
         // TODO: https://issues.apache.org/jira/browse/IGNITE-17373
-        static final Set<ColumnType> UNSUPPORTED_COLUMN_TYPES_AS_PARAMETERS = Set.of(ColumnType.PERIOD, ColumnType.DURATION);
+        static final Set<ColumnType> UNSUPPORTED_COLUMN_TYPES_AS_PARAMETERS = Set.of(
+                ColumnType.PERIOD, ColumnType.DURATION, ColumnType.STRUCT
+        );
         static final Set<Class<?>> SUPPORTED_PARAM_CLASSES;
 
         static {
@@ -557,6 +553,8 @@ public class TypeUtils {
     /** Converts {@link ColumnType} to corresponding {@link NativeType}. */
     public static NativeType columnType2NativeType(ColumnType columnType, int precision, int scale, int length) {
         switch (columnType) {
+            case NULL:
+                return NativeTypes.NULL;
             case BOOLEAN:
                 return NativeTypes.BOOLEAN;
             case INT8:
@@ -590,7 +588,6 @@ public class TypeUtils {
             // fallthrough
             case PERIOD:
             case DURATION:
-            case NULL:
             default:
                 throw new IllegalArgumentException("No NativeType for type: " + columnType);
         }
@@ -710,58 +707,81 @@ public class TypeUtils {
         return true;
     }
 
-    /** Creates an instance of {@link RowSchema} from a list of the given {@link RelDataType}s. */
-    public static RowSchema rowSchemaFromRelTypes(List<RelDataType> types) {
-        RowSchema.Builder fieldTypes = RowSchema.builder();
+    /**
+     * Converts a relational record type to a {@link StructNativeType}.
+     *
+     * <p>This method takes a {@link RelDataType} and converts it to its native struct representation using the {@link IgniteTypeFactory}.
+     * The method validates that the resulting native type is indeed a struct type.
+     *
+     * @param recordType The relational data type to convert; must represent a record/struct type.
+     * @return The corresponding {@code StructNativeType}.
+     * @throws IllegalArgumentException If the provided type cannot be converted to a struct type or does not represent a record
+     *         type.
+     * @see IgniteTypeFactory#relDataTypeToNative(RelDataType)
+     */
+    public static StructNativeType convertStructuredType(RelDataType recordType) {
+        NativeType result = IgniteTypeFactory.relDataTypeToNative(recordType);
 
-        for (RelDataType relType : types) {
-            TypeSpec typeSpec = convertToTypeSpec(relType);
-            fieldTypes.addField(typeSpec);
+        if (result instanceof StructNativeType) {
+            return (StructNativeType) result;
         }
 
-        return fieldTypes.build();
+        throw new IllegalArgumentException("Provided type is not record type: " + recordType);
     }
 
-    private static TypeSpec convertToTypeSpec(RelDataType type) {
-        boolean simpleType = type instanceof BasicSqlType;
-        boolean nullable = type.isNullable();
-
-        if (SqlTypeName.ANY == type.getSqlTypeName()) {
-            // TODO Some JSON functions that return ANY as well : https://issues.apache.org/jira/browse/IGNITE-20163
-            return new BaseTypeSpec(null, nullable);
-        } else if (SqlTypeUtil.isNull(type)) {
-            return RowSchemaTypes.NULL;
-        } else if (simpleType) {
-            NativeType nativeType = IgniteTypeFactory.relDataTypeToNative(type);
-            return RowSchemaTypes.nativeTypeWithNullability(nativeType, nullable);
-        } else if (type instanceof IntervalSqlType) {
-            IntervalSqlType intervalType = (IntervalSqlType) type;
-            boolean yearMonth = intervalType.getIntervalQualifier().isYearMonth();
-
-            if (yearMonth) {
-                // YEAR MONTH interval is stored as number of days in ints.
-                return RowSchemaTypes.nativeTypeWithNullability(NativeTypes.PERIOD, nullable);
-            } else {
-                // DAY interval is stored as time as long.
-                return RowSchemaTypes.nativeTypeWithNullability(NativeTypes.DURATION, nullable);
-            }
-        } else if (SqlTypeUtil.isRow(type)) {
-            List<TypeSpec> fields = new ArrayList<>();
+    /**
+     * Creates a {@link StructNativeType} from a list of relational data types.
+     *
+     * <p>This method constructs a struct where each field is automatically named using the pattern "F0", "F1", "F2", etc., corresponding to
+     * the position of the type in the input list. Each field's type is converted to its native equivalent, and nullability is preserved
+     * from the original relational type.
+     *
+     * <p>Example: Given types [INT, STRING, BOOLEAN], this creates a struct with fields:
+     * <ul>
+     *   <li>F0: INT (nullable as per input type)</li>
+     *   <li>F1: STRING (nullable as per input type)</li>
+     *   <li>F2: BOOLEAN (nullable as per input type)</li>
+     * </ul>
+     *
+     * @param types The list of relational data types to convert; must not be null.
+     * @return A new {@code StructNativeType} with auto-generated field names.
+     * @see IgniteTypeFactory#relDataTypeToNative(RelDataType)
+     */
+    public static StructNativeType structuredTypeFromRelTypeList(List<RelDataType> types) {
+        RowTypeBuilder builder = NativeTypes.rowBuilder();
 
-            for (RelDataTypeField field : type.getFieldList()) {
-                TypeSpec fieldTypeSpec = convertToTypeSpec(field.getType());
-                fields.add(fieldTypeSpec);
-            }
+        int idx = 0;
+        for (RelDataType type : types) {
+            builder.addField("F" + idx++, IgniteTypeFactory.relDataTypeToNative(type), type.isNullable());
+        }
 
-            return new RowType(fields, type.isNullable());
+        return builder.build();
+    }
 
-        } else if (SqlTypeUtil.isMap(type) || SqlTypeUtil.isMultiset(type) || SqlTypeUtil.isArray(type)) {
-            // TODO https://issues.apache.org/jira/browse/IGNITE-20162
-            //  Add collection types support
-            throw new IllegalArgumentException("Collection types is not supported: " + type);
-        } else {
-            throw new IllegalArgumentException("Unexpected type: " + type);
+    /**
+     * Creates a new {@link StructNativeType} by selecting and reordering fields from an existing schema.
+     *
+     * <p>This method produces a struct containing only the fields specified by the mapping array, in the order they appear in the mapping.
+     * Fields are referenced by their index in the original schema. The same field can be included multiple times if its index appears
+     * multiple times in the mapping.
+     *
+     * <p>Example: Given a schema with fields [A, B, C, D] and mapping [2, 0, 2], the result will contain fields [C, A, C].
+     *
+     * @param schema The source struct schema to map from; must not be null.
+     * @param mapping The array of field indices specifying which fields to include and their order; must not be null or empty, and
+     *         all indices must be valid for the schema.
+     * @return A new {@code StructNativeType} containing the mapped fields.
+     */
+    public static StructNativeType map(StructNativeType schema, int[] mapping) {
+        assert mapping != null && mapping.length > 0;
+
+        RowTypeBuilder builder = NativeTypes.rowBuilder();
+
+        for (int i : mapping) {
+            builder.addField(schema.fields().get(i));
         }
+
+        return builder.build();
     }
 
     /** Check limitation for character and binary types and throws exception if row does not fit into type defined.
@@ -789,7 +809,7 @@ public class TypeUtils {
             RelDataType rowType,
             RowHandler<RowT> rowHandler,
             RowT row,
-            Supplier<RowSchema> schema
+            Supplier<StructNativeType> schema
     ) {
         boolean containValidatedType =
                 rowType.getFieldList().stream().anyMatch(t -> STRING_TYPES.contains(t.getType().getSqlTypeName()));
@@ -871,7 +891,9 @@ public class TypeUtils {
         }
     }
 
-    private static <RowT> RowBuilder<RowT> buildPartialRow(RowHandler<RowT> rowHandler, Supplier<RowSchema> schema, int endPos, RowT row) {
+    private static <RowT> RowBuilder<RowT> buildPartialRow(
+            RowHandler<RowT> rowHandler, Supplier<StructNativeType> schema, int endPos, RowT row
+    ) {
         RowFactory<RowT> factory = rowHandler.factory(schema.get());
         RowBuilder<RowT> bldr = factory.rowBuilder();
 
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/IdentityDistributionFunctionSelfTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/IdentityDistributionFunctionSelfTest.java
index 18643885bce..de6d431a28d 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/IdentityDistributionFunctionSelfTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/IdentityDistributionFunctionSelfTest.java
@@ -28,13 +28,13 @@ import java.util.Collection;
 import java.util.List;
 import org.apache.calcite.rel.RelDistribution.Type;
 import org.apache.ignite.internal.sql.engine.exec.mapping.ColocationGroup;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.trait.Destination;
 import org.apache.ignite.internal.sql.engine.trait.DistributionFunction.IdentityDistribution;
 import org.apache.ignite.internal.sql.engine.trait.Identity;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -50,9 +50,9 @@ public class IdentityDistributionFunctionSelfTest {
 
     private final RowHandler<Object[]> rowHandler = ArrayRowHandler.INSTANCE;
 
-    private final RowSchema rowSchema = RowSchema.builder()
-            .addField(NativeTypes.STRING)
-            .addField(NativeTypes.STRING)
+    private final StructNativeType rowSchema = NativeTypes.rowBuilder()
+            .addField("C1", NativeTypes.STRING, true)
+            .addField("C2", NativeTypes.STRING, true)
             .build();
 
     private final ColocationGroup colocationGroup = new ColocationGroup(LongList.of(1L), List.of(NODE_1, NODE_2, NODE_3),
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/PartitionsResolutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/PartitionsResolutionTest.java
index 0240546e205..eb650ced3be 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/PartitionsResolutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/PartitionsResolutionTest.java
@@ -26,7 +26,6 @@ import org.apache.calcite.rel.type.RelDataTypeFactory.Builder;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.planner.AbstractPlannerTest.TestTableDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.PartitionCalculator;
@@ -35,6 +34,7 @@ import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.junit.jupiter.api.Test;
 
 /** Assignments resolution test. */
@@ -93,11 +93,11 @@ public class PartitionsResolutionTest {
         return extractor.partition(row);
     }
 
-    private final RowSchema rowSchema = RowSchema.builder()
-            .addField(NativeTypes.STRING)
-            .addField(NativeTypes.INT32)
-            .addField(NativeTypes.INT32)
-            .addField(NativeTypes.INT32)
+    private final StructNativeType rowSchema = NativeTypes.rowBuilder()
+            .addField("C1", NativeTypes.STRING, true)
+            .addField("C2", NativeTypes.INT32, true)
+            .addField("C3", NativeTypes.INT32, true)
+            .addField("C4", NativeTypes.INT32, true)
             .build();
 
     private static TableDescriptor createTableDescriptor(List<Integer> distrKeys) {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterSelfTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterSelfTest.java
index 36e18de50ec..4a04de15aba 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterSelfTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterSelfTest.java
@@ -33,10 +33,10 @@ import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -69,9 +69,9 @@ public class ProjectedTableRowConverterSelfTest extends BaseIgniteAbstractTest {
                 null
         );
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(NativeTypes.STRING)
-                .addField(NativeTypes.INT32)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.STRING, false)
+                .addField("C2", NativeTypes.INT32, false)
                 .build();
 
         RowHandler<RowWrapper> rowHandler = SqlRowHandler.INSTANCE;
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterSelfTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterSelfTest.java
index 019867afb6d..2ca3abfe59e 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterSelfTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterSelfTest.java
@@ -37,11 +37,11 @@ import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.util.ColocationUtils;
 import org.apache.ignite.internal.util.HashCalculator;
 import org.jetbrains.annotations.Nullable;
@@ -77,9 +77,9 @@ public class TableRowConverterSelfTest extends BaseIgniteAbstractTest {
                 null
         );
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(NativeTypes.STRING)
-                .addField(NativeTypes.INT32)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.STRING, false)
+                .addField("C2", NativeTypes.INT32, false)
                 .build();
 
         RowHandler<RowWrapper> rowHandler = SqlRowHandler.INSTANCE;
@@ -116,11 +116,11 @@ public class TableRowConverterSelfTest extends BaseIgniteAbstractTest {
                 null
         );
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(NativeTypes.STRING)
-                .addField(NativeTypes.BOOLEAN)
-                .addField(NativeTypes.INT32)
-                .addField(NativeTypes.INT32)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.STRING, false)
+                .addField("C2", NativeTypes.BOOLEAN, false)
+                .addField("C3", NativeTypes.INT32, false)
+                .addField("C4", NativeTypes.INT32, false)
                 .build();
 
         RowHandler<RowWrapper> rowHandler = SqlRowHandler.INSTANCE;
@@ -166,9 +166,9 @@ public class TableRowConverterSelfTest extends BaseIgniteAbstractTest {
                 List.of(keyColumnNames[colocationColumn])
         );
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(schema.keyColumns().get(0).type())
-                .addField(schema.keyColumns().get(1).type())
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", schema.keyColumns().get(0).type(), false)
+                .addField("C2", schema.keyColumns().get(1).type(), false)
                 .build();
 
         RowHandler<RowWrapper> rowHandler = SqlRowHandler.INSTANCE;
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
index 534f70e31de..4477f49fcac 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/ExpressionFactoryImplTest.java
@@ -882,7 +882,8 @@ public class ExpressionFactoryImplTest extends BaseIgniteAbstractTest {
         EnumSet<ColumnType> ignoredTypes = EnumSet.of(
                 // TODO https://issues.apache.org/jira/browse/IGNITE-17373
                 ColumnType.DURATION,
-                ColumnType.PERIOD
+                ColumnType.PERIOD,
+                ColumnType.STRUCT
         );
 
         List<Arguments> arguments = new ArrayList<>();
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNodeTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNodeTest.java
index a018e32a73d..6c5173fbe12 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNodeTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AsyncRootNodeTest.java
@@ -46,8 +46,8 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.ScannableDataSource;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.util.AsyncCursor.BatchedResult;
 import org.junit.jupiter.api.Test;
 
@@ -56,8 +56,8 @@ import org.junit.jupiter.api.Test;
  */
 @SuppressWarnings("NumericCastThatLosesPrecision")
 class AsyncRootNodeTest extends AbstractExecutionTest<RowWrapper> {
-    private static final RowSchema SINGLE_INT_ROW_SCHEMA = RowSchema.builder()
-            .addField(NativeTypes.INT32)
+    private static final StructNativeType SINGLE_INT_ROW_SCHEMA = NativeTypes.rowBuilder()
+            .addField("C1", NativeTypes.INT32, true)
             .build();
 
     private static final BinaryTupleSchema SINGLE_INT_SCHEMA = BinaryTupleSchema.create(
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/BaseAggregateTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/BaseAggregateTest.java
index 34dc7e642b8..ab1c4873189 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/BaseAggregateTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/BaseAggregateTest.java
@@ -36,11 +36,9 @@ import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.stream.IntStream;
 import java.util.stream.StreamSupport;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
@@ -51,12 +49,13 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AccumulatorWrapper;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.Accumulators;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AggregateType;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -765,25 +764,26 @@ public abstract class BaseAggregateTest extends AbstractExecutionTest<Object[]>
         return ctx.expressionFactory().accumulatorsFactory(type, asList(call), inRowType).get(ctx);
     }
 
-    protected static RowSchema createOutputSchema(
+    protected static StructNativeType createOutputSchema(
             ExecutionContext<Object[]> ctx,
             AggregateCall call,
             RelDataType inRowType,
             ImmutableBitSet grpSet
     ) {
-        RelDataTypeFactory.Builder outputType = new RelDataTypeFactory.Builder(ctx.getTypeFactory());
+        RowTypeBuilder builder = NativeTypes.rowBuilder();
 
         // Add keys
         for (int i = 0; i < grpSet.length(); i++) {
             if (grpSet.get(i)) {
-                outputType.add("F" + i, inRowType.getFieldList().get(i).getType());
+                RelDataType type = inRowType.getFieldList().get(i).getType();
+                builder.addField("F" + i, IgniteTypeFactory.relDataTypeToNative(type), type.isNullable());
             }
         }
 
         // Add aggregation result
-        outputType.add("R0", call.getType());
+        builder.addField("R0", IgniteTypeFactory.relDataTypeToNative(call.getType()), false);
 
-        return TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(outputType.build()));
+        return builder.build();
     }
 
     enum TestAggregateType {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/CorrelatedNestedLoopJoinExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/CorrelatedNestedLoopJoinExecutionTest.java
index 36f2d3a00fc..0477ca78ca7 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/CorrelatedNestedLoopJoinExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/CorrelatedNestedLoopJoinExecutionTest.java
@@ -19,7 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec.rel;
 
 import static org.apache.calcite.rel.core.JoinRelType.INNER;
 import static org.apache.calcite.rel.core.JoinRelType.LEFT;
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -32,14 +32,12 @@ import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.stream.IntStream;
 import java.util.stream.StreamSupport;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.exp.func.IterableTableFunction;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
@@ -77,14 +75,12 @@ public class CorrelatedNestedLoopJoinExecutionTest extends AbstractExecutionTest
         IgniteTypeFactory tf = ctx.getTypeFactory();
         RelDataType rightType = TypeUtils.createRowType(tf, TypeUtils.native2relationalTypes(tf, NativeTypes.INT32, NativeTypes.STRING));
 
-        RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightType));
-
         CorrelatedNestedLoopJoinNode<Object[]> join = new CorrelatedNestedLoopJoinNode<>(
                 ctx,
                 (r1, r2) -> Objects.equals(hnd.get(2, r1), hnd.get(0, r2)),
                 Set.of(new CorrelationId(0)),
                 joinType,
-                hnd.factory(rightRowSchema),
+                hnd.factory(convertStructuredType(rightType)),
                 identityProjection()
         );
 
@@ -233,8 +229,6 @@ public class CorrelatedNestedLoopJoinExecutionTest extends AbstractExecutionTest
         IgniteTypeFactory tf = ctx.getTypeFactory();
         RelDataType rightType = TypeUtils.createRowType(tf, TypeUtils.native2relationalTypes(tf, NativeTypes.INT32, NativeTypes.STRING));
 
-        RowSchema rightRowSchema = rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(rightType));
-
         RowHandler<Object[]> hnd = ctx.rowHandler();
 
         return new CorrelatedNestedLoopJoinNode<>(
@@ -242,7 +236,7 @@ public class CorrelatedNestedLoopJoinExecutionTest extends AbstractExecutionTest
                 (r1, r2) -> Objects.equals(hnd.get(2, r1), hnd.get(0, r2)),
                 Set.of(new CorrelationId(0)),
                 joinType,
-                hnd.factory(rightRowSchema),
+                hnd.factory(convertStructuredType(rightType)),
                 identityProjection()
         );
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/DataSourceScanNodeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/DataSourceScanNodeExecutionTest.java
index 362b69a7747..1028a991096 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/DataSourceScanNodeExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/DataSourceScanNodeExecutionTest.java
@@ -48,23 +48,23 @@ import org.apache.ignite.internal.sql.engine.exec.ScannableDataSource;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler;
 import org.apache.ignite.internal.sql.engine.exec.SqlRowHandler.RowWrapper;
 import org.apache.ignite.internal.sql.engine.exec.TestDownstream;
-import org.apache.ignite.internal.sql.engine.exec.row.BaseTypeSpec;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.TypeSpec;
 import org.apache.ignite.internal.sql.engine.framework.DataProvider;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
+import org.apache.ignite.internal.type.StructNativeType.Field;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 
 /** Tests to verify {@link DataSourceScanNode}. */
 @SuppressWarnings("resource")
 public class DataSourceScanNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
-    private static final RowSchema ROW_SCHEMA = RowSchema.builder()
-            .addField(NativeTypes.INT32)
-            .addField(NativeTypes.INT64)
-            .addField(NativeTypes.INT8)
-            .addField(NativeTypes.stringOf(64))
-            .addField(NativeTypes.UUID)
+    private static final StructNativeType ROW_SCHEMA = NativeTypes.rowBuilder()
+            .addField("C1", NativeTypes.INT32, true)
+            .addField("C2", NativeTypes.INT64, true)
+            .addField("C3", NativeTypes.INT8, true)
+            .addField("C4", NativeTypes.stringOf(64), true)
+            .addField("C5", NativeTypes.UUID, true)
             .build();
 
     private static final BinaryTupleSchema TUPLE_SCHEMA = fromRowSchema(ROW_SCHEMA);
@@ -242,7 +242,7 @@ public class DataSourceScanNodeExecutionTest extends AbstractExecutionTest<RowWr
     private void checkDataSourceScan(int bufferSize, int sourceSize) {
         ExecutionContext<RowWrapper> ctx = executionContext(bufferSize);
 
-        RowSchema schema = RowSchema.builder().addField(NativeTypes.INT32).build();
+        StructNativeType schema = NativeTypes.rowBuilder().addField("C1", NativeTypes.INT32, true).build();
         RowFactory<RowWrapper> rowFactory = ctx.rowHandler().factory(schema);
         BinaryTupleSchema tupleSchema = fromRowSchema(schema);
         TupleFactory tupleFactory = tupleFactoryFromSchema(tupleSchema);
@@ -340,23 +340,22 @@ public class DataSourceScanNodeExecutionTest extends AbstractExecutionTest<RowWr
         return SqlRowHandler.INSTANCE;
     }
 
-    private static BinaryTupleSchema fromRowSchema(RowSchema schema) {
+    private static BinaryTupleSchema fromRowSchema(StructNativeType schema) {
         Element[] elements = new Element[schema.fields().size()];
 
         int idx = 0;
-        for (TypeSpec spec : schema.fields()) {
-            assert spec instanceof BaseTypeSpec : spec;
-
-            elements[idx++] = new Element(((BaseTypeSpec) spec).nativeType(), spec.isNullable());
+        for (Field field : schema.fields()) {
+            elements[idx++] = new Element(field.type(), field.nullable());
         }
 
         return BinaryTupleSchema.create(elements);
     }
 
-    private static RowSchema project(RowSchema schema, int[] projection) {
-        RowSchema.Builder builder = RowSchema.builder();
+    private static StructNativeType project(StructNativeType schema, int[] projection) {
+        RowTypeBuilder builder = NativeTypes.rowBuilder();
         for (int i : projection) {
-            builder.addField(schema.fields().get(i));
+            Field field = schema.fields().get(i);
+            builder.addField(field.name(), field.type(), field.nullable());
         }
 
         return builder.build();
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ExchangeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ExchangeExecutionTest.java
index 0bace4eaf6a..6ed8c850ee9 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ExchangeExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ExchangeExecutionTest.java
@@ -61,7 +61,6 @@ import org.apache.ignite.internal.sql.engine.exec.QueryTaskExecutor;
 import org.apache.ignite.internal.sql.engine.exec.QueryTaskExecutorImpl;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.mapping.FragmentDescription;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.framework.ClusterServiceFactory;
 import org.apache.ignite.internal.sql.engine.framework.DataProvider;
@@ -76,6 +75,7 @@ import org.apache.ignite.internal.testframework.IgniteTestUtils.PredicateMatcher
 import org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher;
 import org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.internal.util.AsyncCursor.BatchedResult;
 import org.apache.ignite.internal.util.IgniteSpinBusyLock;
 import org.apache.ignite.network.NetworkAddress;
@@ -112,9 +112,9 @@ public class ExchangeExecutionTest extends AbstractExecutionTest<Object[]> {
      * Schema of the rows used in the tests. All data providers created within this test class must
      * conform to this row schema.
      */
-    private static final RowSchema ROW_SCHEMA = RowSchema.builder()
-            .addField(NativeTypes.INT32)
-            .addField(NativeTypes.INT32)
+    private static final StructNativeType ROW_SCHEMA = NativeTypes.rowBuilder()
+            .addField("C1", NativeTypes.INT32, true)
+            .addField("C2", NativeTypes.INT32, true)
             .build();
 
     private final Map<String, MailboxRegistry> mailboxes = new HashMap<>();
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateExecutionTest.java
index 3444b8f24e9..d79b14d08be 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateExecutionTest.java
@@ -25,7 +25,6 @@ import static org.apache.ignite.internal.util.CollectionUtils.first;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.IntStream;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.AggregateCall;
@@ -36,12 +35,12 @@ import org.apache.calcite.util.mapping.Mapping;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlComparator;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates.MapReduceAgg;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.PlanUtils;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.type.StructNativeType;
 
 /**
  * HashAggregateExecutionTest.
@@ -61,7 +60,7 @@ public class HashAggregateExecutionTest extends BaseAggregateTest {
         assert grpSets.size() == 1 : "Test checks only simple GROUP BY";
 
         ImmutableBitSet grpSet = grpSets.get(0);
-        RowSchema outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
+        StructNativeType outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
         RowFactory<Object[]> outputRowFactory = ctx.rowHandler().factory(outputRowSchema);
 
         HashAggregateNode<Object[]> agg = new HashAggregateNode<>(
@@ -122,8 +121,7 @@ public class HashAggregateExecutionTest extends BaseAggregateTest {
         // Map node
 
         RelDataType reduceRowType = PlanUtils.createHashAggRowType(grpSets, ctx.getTypeFactory(), inRowType, List.of(call));
-        RowSchema reduceRowSchema = TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(reduceRowType));
-        RowFactory<Object[]> mapRowFactory = ctx.rowHandler().factory(reduceRowSchema);
+        RowFactory<Object[]> mapRowFactory = ctx.rowHandler().factory(TypeUtils.convertStructuredType(reduceRowType));
 
         HashAggregateNode<Object[]> aggMap = new HashAggregateNode<>(
                 ctx,
@@ -147,7 +145,7 @@ public class HashAggregateExecutionTest extends BaseAggregateTest {
                 true
         );
 
-        RowSchema outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
+        StructNativeType outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
         RowFactory<Object[]> outputRowFactory = ctx.rowHandler().factory(outputRowSchema);
 
         HashAggregateNode<Object[]> aggRdc = new HashAggregateNode<>(
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateSingleGroupExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateSingleGroupExecutionTest.java
index 094567aad66..8bb26622346 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateSingleGroupExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/HashAggregateSingleGroupExecutionTest.java
@@ -26,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Arrays;
 import java.util.List;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.type.RelDataType;
@@ -40,7 +39,6 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AccumulatorWrapper;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AggregateType;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates.MapReduceAgg;
@@ -378,8 +376,7 @@ public class HashAggregateSingleGroupExecutionTest extends AbstractExecutionTest
         List<AccumulatorWrapper<Object[]>> accumulators =
                 ctx.expressionFactory().accumulatorsFactory(aggregateType, List.of(call), inputType).get(ctx);
 
-        RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(outputType));
-        RowFactory<Object[]> rowFactory = rowHandler().factory(rowSchema);
+        RowFactory<Object[]> rowFactory = rowHandler().factory(TypeUtils.convertStructuredType(outputType));
 
         return new HashAggregateNode<>(ctx, aggregateType, grpSets, accumulators, rowFactory);
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
index 028594bb23c..8e83a21362f 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IndexScanNodeExecutionTest.java
@@ -49,7 +49,6 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.ScannableTable;
 import org.apache.ignite.internal.sql.engine.exec.exp.RangeCondition;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.framework.DataProvider;
 import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
@@ -64,6 +63,8 @@ import org.apache.ignite.internal.sql.engine.trait.TraitUtils;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -162,7 +163,7 @@ public class IndexScanNodeExecutionTest extends AbstractExecutionTest<Object[]>
 
         SingleRangeIterable<Object[]> conditions = new SingleRangeIterable<>(new Object[]{}, null, false, false);
 
-        RowSchema schema = RowSchema.builder().addField(NativeTypes.INT32).build();
+        StructNativeType schema = NativeTypes.rowBuilder().addField("C1", NativeTypes.INT32, false).build();
         RowFactory<Object[]> rowFactory = ctx.rowHandler().factory(schema);
 
         TableDescriptor tableDescriptor = createTableDescriptor(columns);
@@ -271,13 +272,14 @@ public class IndexScanNodeExecutionTest extends AbstractExecutionTest<Object[]>
     private static IndexScanNode<Object[]> createIndexNode(ExecutionContext<Object[]> ctx, IgniteIndex indexDescriptor,
             TableDescriptor tableDescriptor, TestScannableTable<?> scannableTable, @Nullable Comparator<Object[]> comparator) {
 
-        RowSchema.Builder rowSchemaBuilder = RowSchema.builder();
+        RowTypeBuilder rowSchemaBuilder = NativeTypes.rowBuilder();
 
+        int idx = 0;
         for (RelFieldCollation ignored : indexDescriptor.collation().getFieldCollations()) {
-            rowSchemaBuilder = rowSchemaBuilder.addField(NativeTypes.INT32);
+            rowSchemaBuilder = rowSchemaBuilder.addField("C" + idx++, NativeTypes.INT32, true);
         }
 
-        RowSchema rowSchema = rowSchemaBuilder.build();
+        StructNativeType rowSchema = rowSchemaBuilder.build();
 
         RowFactory<Object[]> rowFactory = ctx.rowHandler().factory(rowSchema);
         SingleRangeIterable<Object[]> conditions = new SingleRangeIterable<>(new Object[]{}, null, false, false);
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IntersectExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IntersectExecutionTest.java
index 170528d3cee..3998934d2d2 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IntersectExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/IntersectExecutionTest.java
@@ -22,8 +22,8 @@ import java.util.List;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AggregateType;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 
 /**
  * Test execution of INTERSECT operator.
@@ -34,25 +34,25 @@ public class IntersectExecutionTest extends AbstractSetOpExecutionTest {
     protected AbstractSetOpNode<Object[]> setOpNodeFactory(ExecutionContext<Object[]> ctx,
             AggregateType type, int columnCount, boolean all, int inputsCnt) {
 
-        RowSchema rowSchema;
+        StructNativeType rowSchema;
 
         switch (type) {
             case MAP:
-                rowSchema = RowSchema.builder()
+                rowSchema = NativeTypes.rowBuilder()
                         // input columns
-                        .addField(NativeTypes.STRING)
+                        .addField("C1", NativeTypes.STRING, false)
                         // counters
-                        .addField(NativeTypes.INT32)
-                        .addField(NativeTypes.INT32)
-                        .addField(NativeTypes.INT32)
-                        .addField(NativeTypes.INT32)
+                        .addField("C2", NativeTypes.INT32, false)
+                        .addField("C3", NativeTypes.INT32, false)
+                        .addField("C4", NativeTypes.INT32, false)
+                        .addField("C5", NativeTypes.INT32, false)
                         .build();
                 break;
             case REDUCE:
             case SINGLE:
-                rowSchema = RowSchema.builder()
-                        .addField(NativeTypes.STRING)
-                        .addField(NativeTypes.INT32)
+                rowSchema = NativeTypes.rowBuilder()
+                        .addField("C1", NativeTypes.STRING, false)
+                        .addField("C2", NativeTypes.INT32, false)
                         .build();
                 break;
             default:
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MinusExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MinusExecutionTest.java
index 388d1e073fe..d290f74878c 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MinusExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/MinusExecutionTest.java
@@ -21,8 +21,8 @@ import java.util.Arrays;
 import java.util.List;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.exp.agg.AggregateType;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 
 /**
  * Test execution of MINUS (EXCEPT) operator.
@@ -33,24 +33,24 @@ public class MinusExecutionTest extends AbstractSetOpExecutionTest {
     protected AbstractSetOpNode<Object[]> setOpNodeFactory(ExecutionContext<Object[]> ctx,
             AggregateType type, int columnCount, boolean all, int inputsCnt) {
 
-        RowSchema rowSchema;
+        StructNativeType rowSchema;
 
         switch (type) {
             case MAP:
-                rowSchema = RowSchema.builder()
+                rowSchema = NativeTypes.rowBuilder()
                         // input columns
-                        .addField(NativeTypes.STRING)
-                        .addField(NativeTypes.INT32)
+                        .addField("C1", NativeTypes.STRING, false)
+                        .addField("C1", NativeTypes.INT32, false)
                         // counters
-                        .addField(NativeTypes.INT32)
-                        .addField(NativeTypes.INT32)
+                        .addField("C1", NativeTypes.INT32, false)
+                        .addField("C1", NativeTypes.INT32, false)
                         .build();
                 break;
             case REDUCE:
             case SINGLE:
-                rowSchema = RowSchema.builder()
-                        .addField(NativeTypes.STRING)
-                        .addField(NativeTypes.INT32)
+                rowSchema = NativeTypes.rowBuilder()
+                        .addField("C1", NativeTypes.STRING, false)
+                        .addField("C1", NativeTypes.INT32, false)
                         .build();
                 break;
             default:
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNodeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNodeExecutionTest.java
index 4f0791b2392..c227f6ddb66 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNodeExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ModifyNodeExecutionTest.java
@@ -52,9 +52,6 @@ import org.apache.ignite.internal.sql.engine.exec.TestDownstream;
 import org.apache.ignite.internal.sql.engine.exec.UpdatableTable;
 import org.apache.ignite.internal.sql.engine.exec.mapping.ColocationGroup;
 import org.apache.ignite.internal.sql.engine.exec.mapping.FragmentDescription;
-import org.apache.ignite.internal.sql.engine.exec.row.BaseTypeSpec;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema.Builder;
 import org.apache.ignite.internal.sql.engine.framework.DataProvider;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptorImpl;
@@ -63,6 +60,9 @@ import org.apache.ignite.internal.sql.engine.schema.TableDescriptor;
 import org.apache.ignite.internal.sql.engine.schema.TableDescriptorImpl;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
+import org.apache.ignite.internal.type.StructNativeType.Field;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -83,23 +83,22 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
 
     private static final long SOURCE_ID = 42;
 
-    private static final RowSchema INT_LONG_SCHEMA = RowSchema.builder()
-            .addField(NativeTypes.INT32)
-            .addField(NativeTypes.INT64)
+    private static final StructNativeType INT_LONG_SCHEMA = NativeTypes.rowBuilder()
+            .addField("C0", NativeTypes.INT32, true)
+            .addField("C1", NativeTypes.INT64, true)
             .build();
 
     @SuppressWarnings("OverridableMethodCallDuringObjectConstruction")
     private final RowHandler<RowWrapper> handler = rowHandler();
-    private final RowHandler.RowFactory<RowWrapper> rowFactory = handler.factory(INT_LONG_SCHEMA);
 
     @Mock
     private UpdatableTable updatableTable;
 
     @BeforeEach
     void setUpMock() {
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(NativeTypes.INT32)
-                .addField(NativeTypes.INT64)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.INT32, false)
+                .addField("C2", NativeTypes.INT64, false)
                 .build();
 
         TableDescriptor tableDescriptor = createTableDescriptor(rowSchema);
@@ -114,7 +113,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.INSERT, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.INSERT, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -147,7 +146,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.UPDATE, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.UPDATE, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -180,7 +179,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.DELETE, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.DELETE, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -213,7 +212,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.INSERT, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.INSERT, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -241,7 +240,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.UPDATE, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.UPDATE, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -269,7 +268,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         Node<RowWrapper> sourceNode = createSource(sourceSize, context);
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.DELETE, null, rowFactory
+                context, updatableTable, SOURCE_ID, Operation.DELETE, null, INT_LONG_SCHEMA
         );
 
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
@@ -326,27 +325,27 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         ExecutionContext<RowWrapper> context = executionContext();
         RowHandler<RowWrapper> rowHandler = context.rowHandler();
 
-        RowSchema.Builder dstRowSchemaBuilder = RowSchema.builder();
+        RowTypeBuilder dstRowSchemaBuilder = NativeTypes.rowBuilder();
 
         for (int i = 0; i < colCount; i++) {
-            dstRowSchemaBuilder.addField(NativeTypes.INT32, true);
+            dstRowSchemaBuilder.addField("C" + i, NativeTypes.INT32, true);
         }
 
-        RowSchema dstRowSchema = dstRowSchemaBuilder.build();
+        StructNativeType dstRowSchema = dstRowSchemaBuilder.build();
 
-        RowSchema.Builder srcRowSchemaBuilder = RowSchema.builder();
+        RowTypeBuilder srcRowSchemaBuilder = NativeTypes.rowBuilder();
 
         for (int i = 0; i < colCount; i++) {
-            srcRowSchemaBuilder.addField(NativeTypes.INT32, true);
+            srcRowSchemaBuilder.addField("C" + i, NativeTypes.INT32, true);
         }
 
-        RowSchema srcRowSchema = srcRowSchemaBuilder.build();
+        StructNativeType srcRowSchema = srcRowSchemaBuilder.build();
 
-        RowSchema updateSchema = RowSchema.builder()
-                .addField(NativeTypes.INT32, true)
+        StructNativeType updateSchema = NativeTypes.rowBuilder()
+                .addField("C0", NativeTypes.INT32, true)
                 .build();
 
-        RowSchema mergeRowSchema = RowSchema.concat(RowSchema.concat(srcRowSchema, dstRowSchema), updateSchema);
+        StructNativeType mergeRowSchema = concat(srcRowSchema, dstRowSchema, updateSchema);
 
         Mockito.reset(updatableTable);
 
@@ -393,13 +392,6 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         RowWrapper mergeRow1 = concatRow(mergeRowFactory, srcRow1, dstRow1, noUpdate);
         RowWrapper mergeRow2 = concatRow(mergeRowFactory, srcRow2, dstRow2, update);
 
-        Builder inputRowBuilder = RowSchema.builder();
-        srcFactory.rowSchema().fields().forEach(inputRowBuilder::addField);
-        dstFactory.rowSchema().fields().forEach(inputRowBuilder::addField);
-        inputRowBuilder.addField(NativeTypes.INT32, true); // updated field value
-
-        RowFactory<RowWrapper> inputRowFactory = rowHandler.factory(inputRowBuilder.build());
-
         Node<RowWrapper> sourceNode = new ScanNode<>(
                 context, DataProvider.fromCollection(List.of(mergeRow1, mergeRow2))
         );
@@ -407,7 +399,7 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         TestDownstream<RowWrapper> downstream = new TestDownstream<>();
 
         ModifyNode<RowWrapper> modifyNode = new ModifyNode<>(
-                context, updatableTable, SOURCE_ID, Operation.MERGE, List.of("C1"), inputRowFactory
+                context, updatableTable, SOURCE_ID, Operation.MERGE, List.of("C1"), mergeRowSchema
         );
         modifyNode.register(List.of(sourceNode));
         modifyNode.onRegister(downstream);
@@ -454,19 +446,19 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         return builder.build();
     }
 
-    private static TableDescriptor createTableDescriptor(RowSchema rowSchema) {
+    private static TableDescriptor createTableDescriptor(StructNativeType rowSchema) {
         List<ColumnDescriptor> columns = new ArrayList<>();
 
         for (int i = 0; i < rowSchema.fields().size(); i++) {
-            BaseTypeSpec typeSpec = (BaseTypeSpec) rowSchema.fields().get(i);
+            Field field = rowSchema.fields().get(i);
             ColumnDescriptorImpl col = new ColumnDescriptorImpl(
-                    "C" + i,
+                    field.name(),
                     false,
                     false,
                     false,
-                    typeSpec.isNullable(),
+                    field.nullable(),
                     i,
-                    typeSpec.nativeType(),
+                    field.type(),
                     DefaultValueStrategy.DEFAULT_NULL,
                     null
             );
@@ -495,9 +487,9 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         return rowCount / MODIFY_BATCH_SIZE + (rowCount % MODIFY_BATCH_SIZE == 0 ? 0 : 1);
     }
 
-    private Node<RowWrapper> createSource(int rowCount, ExecutionContext<RowWrapper> context) {
+    private static Node<RowWrapper> createSource(int rowCount, ExecutionContext<RowWrapper> context) {
         return new ScanNode<>(
-                context, DataProvider.fromRow(rowFactory.create(1, 1L), rowCount)
+                context, DataProvider.fromRow(context.rowHandler().factory(INT_LONG_SCHEMA).create(1, 1L), rowCount)
         );
     }
 
@@ -511,4 +503,16 @@ public class ModifyNodeExecutionTest extends AbstractExecutionTest<RowWrapper> {
         ColocationGroup colocationGroup = new ColocationGroup(LongList.of(), List.of(), Int2ObjectMaps.emptyMap());
         return new FragmentDescription(0, true, Long2ObjectMaps.singleton(SOURCE_ID, colocationGroup), null, null, null);
     }
+
+    private static StructNativeType concat(StructNativeType... types) {
+        RowTypeBuilder builder = NativeTypes.rowBuilder();
+
+        for (StructNativeType type : types) {
+            for (Field field : type.fields()) {
+                builder.addField(field.name(), field.type(), field.nullable());
+            }
+        }
+
+        return builder.build();
+    }
 }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ScannableTableSelfTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ScannableTableSelfTest.java
index 0b60d17f050..03ae832cef8 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ScannableTableSelfTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/ScannableTableSelfTest.java
@@ -70,17 +70,17 @@ import org.apache.ignite.internal.sql.engine.exec.ScannableTableImpl;
 import org.apache.ignite.internal.sql.engine.exec.TableRowConverter;
 import org.apache.ignite.internal.sql.engine.exec.TxAttributes;
 import org.apache.ignite.internal.sql.engine.exec.exp.RangeCondition;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.framework.NoOpTransaction;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.sql.engine.util.Commons;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.table.IndexScanCriteria;
 import org.apache.ignite.internal.table.InternalTable;
 import org.apache.ignite.internal.table.OperationContext;
 import org.apache.ignite.internal.table.TxContext;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
-import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.hamcrest.Matchers;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Named;
@@ -613,7 +613,7 @@ public class ScannableTableSelfTest extends BaseIgniteAbstractTest {
 
         final RelDataType rowType;
 
-        final RowSchema rowSchema;
+        final StructNativeType rowSchema;
 
         final BitSet indexColumns = new BitSet();
 
@@ -623,17 +623,14 @@ public class ScannableTableSelfTest extends BaseIgniteAbstractTest {
 
         TestInput(int columnCount) {
             Builder builder = new Builder(TYPE_FACTORY);
-            RowSchema.Builder rowSchema = RowSchema.builder();
-
             for (int i = 1; i <= columnCount; i++) {
                 builder.add("C" + i, SqlTypeName.INTEGER);
-                rowSchema.addField(NativeTypes.INT32);
             }
 
             indexColumns.set(0);
 
             rowType = builder.build();
-            this.rowSchema = rowSchema.build();
+            this.rowSchema = TypeUtils.convertStructuredType(rowType);
         }
 
         void addRow(BinaryRow row) {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateExecutionTest.java
index 823de37eb8b..bd177444c40 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/SortAggregateExecutionTest.java
@@ -27,7 +27,6 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.AggregateCall;
@@ -38,11 +37,11 @@ import org.apache.calcite.util.mapping.Mapping;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.exp.SqlComparator;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates;
 import org.apache.ignite.internal.sql.engine.rel.agg.MapReduceAggregates.MapReduceAgg;
 import org.apache.ignite.internal.sql.engine.util.Commons;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.type.StructNativeType;
 
 /**
  * SortAggregateExecutionTest.
@@ -74,7 +73,7 @@ public class SortAggregateExecutionTest extends BaseAggregateTest {
             cmp = (r1, r2) -> comparator.compare(ctx, r1, r2);
         }
 
-        RowSchema outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
+        StructNativeType outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
         RowFactory<Object[]> outputRowFactory = ctx.rowHandler().factory(outputRowSchema);
 
         SortAggregateNode<Object[]> agg = new SortAggregateNode<>(
@@ -125,8 +124,7 @@ public class SortAggregateExecutionTest extends BaseAggregateTest {
         }
 
         // Map node
-        RowSchema reduceRowSchema = TypeUtils.rowSchemaFromRelTypes(RelOptUtil.getFieldTypeList(inRowType));
-        RowFactory<Object[]> mapRowFactory = ctx.rowHandler().factory(reduceRowSchema);
+        RowFactory<Object[]> mapRowFactory = ctx.rowHandler().factory(TypeUtils.convertStructuredType(inRowType));
 
         SortAggregateNode<Object[]> aggMap = new SortAggregateNode<>(
                 ctx,
@@ -175,7 +173,7 @@ public class SortAggregateExecutionTest extends BaseAggregateTest {
                 true
         );
 
-        RowSchema outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
+        StructNativeType outputRowSchema = createOutputSchema(ctx, call, inRowType, grpSet);
         RowFactory<Object[]> outputRowFactory = ctx.rowHandler().factory(outputRowSchema);
 
         SortAggregateNode<Object[]> aggRdc = new SortAggregateNode<>(
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java
index 6f1bdda4e2b..e2f06383bd9 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/TableScanNodeExecutionTest.java
@@ -19,7 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec.rel;
 
 import static org.apache.ignite.internal.replicator.ReplicatorConstants.DEFAULT_IDLE_SAFE_TIME_PROPAGATION_PERIOD_MILLISECONDS;
 import static org.apache.ignite.internal.sql.engine.util.Commons.IN_BUFFER_SIZE;
-import static org.apache.ignite.internal.sql.engine.util.TypeUtils.rowSchemaFromRelTypes;
+import static org.apache.ignite.internal.sql.engine.util.TypeUtils.convertStructuredType;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.deriveUuidFrom;
 import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static org.apache.ignite.internal.util.IgniteUtils.closeAll;
@@ -75,7 +75,6 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowFactory;
 import org.apache.ignite.internal.sql.engine.exec.ScannableTable;
 import org.apache.ignite.internal.sql.engine.exec.ScannableTableImpl;
 import org.apache.ignite.internal.sql.engine.exec.TableRowConverter;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.framework.DataProvider;
 import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
@@ -98,6 +97,7 @@ import org.apache.ignite.internal.tx.impl.TxManagerImpl;
 import org.apache.ignite.internal.tx.storage.state.TxStateStorage;
 import org.apache.ignite.internal.tx.test.TestLocalRwTxCounter;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.network.NetworkAddress;
 import org.apache.ignite.table.QualifiedName;
 import org.apache.ignite.table.QualifiedNameHelper;
@@ -131,8 +131,6 @@ public class TableScanNodeExecutionTest extends AbstractExecutionTest<Object[]>
         RelDataType rowType = TypeUtils.createRowType(tf, TypeUtils.native2relationalTypes(tf,
                 NativeTypes.INT32, NativeTypes.STRING, NativeTypes.INT32));
 
-        RowSchema rowSchema = rowSchemaFromRelTypes(List.of(rowType));
-
         int inBufSize = IN_BUFFER_SIZE;
 
         List<PartitionWithConsistencyToken> partsWithConsistencyTokens = IntStream.range(0, TestInternalTableImpl.PART_CNT)
@@ -147,7 +145,7 @@ public class TableScanNodeExecutionTest extends AbstractExecutionTest<Object[]>
             sizes[i] = inBufSize * (i + 1) + ThreadLocalRandom.current().nextInt(100);
         }
 
-        RowFactory<Object[]> rowFactory = ctx.rowHandler().factory(rowSchema);
+        RowFactory<Object[]> rowFactory = ctx.rowHandler().factory(convertStructuredType(rowType));
 
         int i = 0;
 
@@ -276,7 +274,7 @@ public class TableScanNodeExecutionTest extends AbstractExecutionTest<Object[]>
                 .mapToObj(i -> new PartitionWithConsistencyToken(1, 42L))
                 .collect(Collectors.toList());
 
-        RowSchema schema = RowSchema.builder().addField(NativeTypes.INT32).build();
+        StructNativeType schema = NativeTypes.rowBuilder().addField("C1", NativeTypes.INT32, false).build();
         RowFactory<Object[]> rowFactory = ctx.rowHandler().factory(schema);
 
         ScannableTable scannableTable = TestBuilders.tableScan(DataProvider.fromRow(new Object[]{42}, partDataSize));
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTest.java
deleted file mode 100644
index 31355bb1d33..00000000000
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTest.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-
-import java.util.List;
-import org.apache.ignite.internal.type.NativeTypes;
-import org.junit.jupiter.api.Test;
-
-/** Tests for {@link RowSchema}. */
-public class RowSchemaTest {
-
-    /** Schema equality. */
-    @Test
-    public void testSchemaEquality() {
-        RowSchema s1 = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .build();
-        assertEquals(s1, s1);
-
-        List<BaseTypeSpec> fields = List.of(new BaseTypeSpec(NativeTypes.INT32));
-        assertEquals(fields, s1.fields());
-
-        RowSchema s2 = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .build();
-
-        assertEquals(s1, s2);
-    }
-
-    /** Schema inequality. */
-    @Test
-    public void testSchemaInequality() {
-        {
-            RowSchema s1 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .build();
-
-            RowSchema s2 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.STRING))
-                    .build();
-
-            assertNotEquals(s1, s2);
-        }
-
-        {
-            RowSchema s1 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .build();
-
-            RowSchema s2 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .build();
-
-            assertNotEquals(s1, s2);
-        }
-
-        {
-            RowSchema s1 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .addField(new BaseTypeSpec(NativeTypes.STRING))
-                    .build();
-
-            RowSchema s2 = RowSchema.builder()
-                    .addField(new BaseTypeSpec(NativeTypes.STRING))
-                    .addField(new BaseTypeSpec(NativeTypes.INT32))
-                    .build();
-
-            assertNotEquals(s1, s2);
-        }
-    }
-
-    /** Schema concatenation. */
-    @Test
-    public void testSchemaConcat() {
-        RowSchema s1 = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.STRING))
-                .build();
-
-        RowSchema s2 = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .build();
-
-        RowSchema expected = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.STRING))
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .build();
-
-        RowSchema concatenated = RowSchema.concat(s1, s2);
-
-        assertEquals(expected, concatenated);
-    }
-
-    /** Schema projection. */
-    @Test
-    public void testSchemaProjection() {
-        RowSchema s1 = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .addField(new BaseTypeSpec(NativeTypes.INT64))
-                .addField(new BaseTypeSpec(NativeTypes.STRING))
-                .build();
-
-        RowSchema expected = RowSchema.builder()
-                .addField(new BaseTypeSpec(NativeTypes.STRING))
-                .addField(new BaseTypeSpec(NativeTypes.INT32))
-                .build();
-
-        RowSchema projected = RowSchema.map(s1, new int[]{2, 0});
-
-        assertEquals(expected, projected);
-    }
-}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypesTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypesTest.java
deleted file mode 100644
index d0624d7586b..00000000000
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/RowSchemaTypesTest.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * 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.sql.engine.exec.row;
-
-import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import com.google.common.collect.Streams;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.ignite.internal.type.NativeType;
-import org.apache.ignite.internal.type.NativeTypes;
-import org.apache.ignite.internal.util.Pair;
-import org.apache.ignite.sql.ColumnType;
-import org.junit.jupiter.api.DynamicTest;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestFactory;
-
-/** Tests for {@link RowSchemaTypes}. */
-public class RowSchemaTypesTest {
-
-    /** NULL type. */
-    @Test
-    public void testNullType() {
-        assertEquals(new NullTypeSpec(), RowSchemaTypes.NULL);
-        assertTrue(RowSchemaTypes.NULL.isNullable(), "NULL should be nullable");
-    }
-
-    /** {@link RowType} equality. */
-    @Test
-    public void testRowType() {
-        RowType rowType1 = new RowType(List.of(RowSchemaTypes.nativeType(NativeTypes.INT32)), false);
-        RowType rowType2 = new RowType(List.of(RowSchemaTypes.nativeType(NativeTypes.INT32)), false);
-
-        RowType rowType3 = new RowType(List.of(
-                RowSchemaTypes.nativeType(NativeTypes.INT32),
-                RowSchemaTypes.nativeType(NativeTypes.INT32)),
-                false);
-
-        assertEquals(rowType1, rowType2);
-        assertNotEquals(rowType1, rowType3);
-    }
-
-    /** Creating {@link RowType} with no fields should not be possible. */
-    @Test
-    public void testEmptyRowTypeIsIllegal() {
-        assertThrows(IllegalArgumentException.class, () -> new RowType(List.of(), false));
-    }
-
-    /** Conversion from {@link NativeType} to {@link BaseTypeSpec}. */
-    @TestFactory
-    public Stream<DynamicTest> testFromNativeType() {
-        List<NativeTypeToBaseTestCase> testCaseList = new ArrayList<>();
-
-        for (ConversionTest testCase : ConversionTest.values()) {
-            NativeType nativeType = testCase.nativeType;
-            Supplier<BaseTypeSpec> fromNotNull = () -> RowSchemaTypes.nativeType(nativeType);
-
-            if (testCase.interned) {
-                Pair<BaseTypeSpec, BaseTypeSpec> typePair = RowSchemaTypes.INTERNED_TYPES.get(nativeType);
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, true, typePair.getFirst(), false, fromNotNull));
-
-            } else {
-                BaseTypeSpec notNull = new BaseTypeSpec(nativeType, false);
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, false, notNull, false, fromNotNull));
-            }
-        }
-
-        return testCaseList.stream().map(NativeTypeToBaseTestCase::toTest);
-    }
-
-    /** Conversion from {@link NativeType} to {@link BaseTypeSpec}. */
-    @TestFactory
-    public Stream<DynamicTest> testFromNativeTypeWithNullability() {
-        List<NativeTypeToBaseTestCase> testCaseList = new ArrayList<>();
-
-        for (ConversionTest testCase : ConversionTest.values()) {
-            NativeType nativeType = testCase.nativeType;
-            Supplier<BaseTypeSpec> fromNotNull = () -> RowSchemaTypes.nativeTypeWithNullability(nativeType, false);
-            Supplier<BaseTypeSpec> fromNullable = () -> RowSchemaTypes.nativeTypeWithNullability(nativeType, true);
-
-            if (testCase.interned) {
-                Pair<BaseTypeSpec, BaseTypeSpec> typePair = RowSchemaTypes.INTERNED_TYPES.get(nativeType);
-
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, true, typePair.getFirst(), false, fromNotNull));
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, true, typePair.getSecond(), true, fromNullable));
-            } else {
-                BaseTypeSpec notNull = new BaseTypeSpec(nativeType, false);
-                BaseTypeSpec nullable = new BaseTypeSpec(nativeType, true);
-
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, false, notNull, false, fromNotNull));
-                testCaseList.add(new NativeTypeToBaseTestCase(nativeType, false, nullable, true, fromNullable));
-            }
-        }
-
-        // Check for missed types add fail if such types present.
-
-        Set<Entry<ColumnType, Boolean>> typeSpecs = new CopyOnWriteArraySet<>();
-
-        for (ColumnType spec : NativeType.nativeTypes()) {
-            typeSpecs.add(Map.entry(spec, true));
-            typeSpecs.add(Map.entry(spec, false));
-        }
-
-        DynamicTest dynamicTest = DynamicTest.dynamicTest("Ensure all native types were checked", () -> {
-            for (NativeTypeToBaseTestCase test : testCaseList) {
-                typeSpecs.remove(Map.entry(test.nativeType.spec(), test.nullable));
-            }
-            List<String> typePairs = typeSpecs.stream()
-                    .map(p -> format("<{}, nullable={}>", p.getKey().name(), p.getValue()))
-                    .collect(Collectors.toList());
-
-            assertTrue(typeSpecs.isEmpty(), typePairs.toString());
-        });
-
-        return Streams.concat(testCaseList.stream().map(NativeTypeToBaseTestCase::toTest), Stream.of(dynamicTest));
-    }
-
-    enum ConversionTest {
-        // type, interned or not.
-        BOOLEAN(NativeTypes.BOOLEAN, true),
-
-        INT8(NativeTypes.INT8, true),
-        INT16(NativeTypes.INT16, true),
-        INT32(NativeTypes.INT32, true),
-        INT64(NativeTypes.INT64, true),
-        FLOAT(NativeTypes.FLOAT, true),
-        DOUBLE(NativeTypes.DOUBLE, true),
-        DECIMAL_10_0(NativeTypes.decimalOf(10, 0), false),
-        DECIMAL_10_4(NativeTypes.decimalOf(10, 4), false),
-
-        STRING(NativeTypes.INT16, true),
-        STRING_8(NativeTypes.stringOf(8), false),
-        BYTES(NativeTypes.BYTES, true),
-        BYTES_8(NativeTypes.blobOf(8), false),
-
-        UUID(NativeTypes.UUID, true),
-
-        DATE(NativeTypes.DATE, true),
-        TIME_2(NativeTypes.time(2), false),
-        DATETIME_2(NativeTypes.datetime(2), false),
-        TIMESTAMP_2(NativeTypes.timestamp(2), false),
-        ;
-
-        final NativeType nativeType;
-        final boolean interned;
-
-        ConversionTest(NativeType nativeType, boolean interned) {
-            this.nativeType = nativeType;
-            this.interned = interned;
-        }
-    }
-
-    private static final class NativeTypeToBaseTestCase {
-
-        final NativeType nativeType;
-
-        final boolean internedType;
-
-        final BaseTypeSpec expected;
-
-        final boolean nullable;
-
-        final Supplier<BaseTypeSpec> result;
-
-        NativeTypeToBaseTestCase(NativeType nativeType, boolean sameType,
-                BaseTypeSpec expected, boolean nullable, Supplier<BaseTypeSpec> result) {
-            this.internedType = sameType;
-            this.nativeType = nativeType;
-            this.nullable = nullable;
-            this.expected = expected;
-            this.result = result;
-        }
-
-        DynamicTest toTest() {
-            String name = format("{} {} {}", (nullable ? "NULLABLE " : ""), nativeType, (internedType ? "SAME" : "EQUAL"));
-
-            return DynamicTest.dynamicTest(name, () -> {
-                BaseTypeSpec actual = result.get();
-
-                if (internedType) {
-                    assertSame(expected, actual, "descriptors should point to the same object");
-                } else {
-                    assertEquals(expected, actual, "descriptors does not match");
-                }
-
-                assertEquals(nullable, actual.isNullable(), "Nullability does not match: " + actual);
-            });
-        }
-    }
-}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/SqlRowHandlerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/SqlRowHandlerTest.java
index 9cf3e24be1b..66289f751b3 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/SqlRowHandlerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/row/SqlRowHandlerTest.java
@@ -41,12 +41,13 @@ import org.apache.ignite.internal.sql.engine.exec.RowHandler.RowBuilder;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler.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.sql.engine.exec.row.RowSchema.Builder;
 import org.apache.ignite.internal.sql.engine.util.SqlTestUtils;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.testframework.IgniteAbstractTest;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.NativeTypes.RowTypeBuilder;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.sql.ColumnType;
 import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.BeforeEach;
@@ -76,7 +77,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
     public void testBytebufferSerialization() {
         List<ColumnType> columnTypes = shuffledColumnTypes();
         Object[] sourceData = values(columnTypes);
-        RowSchema schema = rowSchema(columnTypes, sourceData);
+        StructNativeType schema = rowSchema(columnTypes, sourceData);
 
         int elementsCount = schema.fields().size();
 
@@ -89,8 +90,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
         for (int i = 0; i < elementsCount; i++) {
             String msg = schema.fields().get(i).toString();
-            TypeSpec typeSpec = schema.fields().get(i);
-            Object expected = convertToInternal(typeSpec, sourceData[i]);
+            Object expected = convertToInternal(schema.fields().get(i).type(), sourceData[i]);
 
             assertThat(msg, handler.get(i, src), equalTo(expected));
             assertThat(msg, handler.get(i, dest), equalTo(expected));
@@ -111,7 +111,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
         int[] mapping = {3, 5};
 
         Object[] sourceData = values(columnTypes);
-        RowSchema schema = rowSchema(columnTypes, sourceData);
+        StructNativeType schema = rowSchema(columnTypes, sourceData);
 
         RowFactory<RowWrapper> factory = handler.factory(schema);
 
@@ -119,7 +119,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
         RowWrapper srcBinRow = factory.create(handler.toBinaryTuple(srcRow));
 
-        RowSchema mappedSchema = rowSchema(columnTypes.subList(0, mapping.length), Arrays.copyOf(sourceData, mapping.length));
+        StructNativeType mappedSchema = rowSchema(columnTypes.subList(0, mapping.length), Arrays.copyOf(sourceData, mapping.length));
         RowFactory<RowWrapper> mappedFactory = handler.factory(mappedSchema);
 
         RowWrapper mappedRow = mappedFactory.map(srcRow, mapping);
@@ -143,18 +143,18 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
     public void testUpdateRowSchemaOnMapping() {
         RowHandler<RowWrapper> handler = SqlRowHandler.INSTANCE;
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(NativeTypes.INT32)
-                .addField(NativeTypes.STRING)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.INT32, false)
+                .addField("C2", NativeTypes.STRING, false)
                 .build();
 
         RowWrapper row1 = handler.factory(rowSchema).rowBuilder()
                 .addField(1).addField("2")
                 .build();
 
-        RowSchema reverseRowSchema = RowSchema.builder()
-                .addField(NativeTypes.STRING)
-                .addField(NativeTypes.INT32)
+        StructNativeType reverseRowSchema = NativeTypes.rowBuilder()
+                .addField("C1", NativeTypes.STRING, false)
+                .addField("C2", NativeTypes.INT32, false)
                 .build();
 
         RowFactory<RowWrapper> factory = handler.factory(reverseRowSchema);
@@ -180,7 +180,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
     public void testRowBuilder(ColumnType type) {
         Object value1 = SqlTestUtils.generateValueByTypeWithMaxScalePrecisionForSql(type);
 
-        RowSchema rowSchema = rowSchema(List.of(type), new Object[]{value1});
+        StructNativeType rowSchema = rowSchema(List.of(type), new Object[]{value1});
         RowFactory<RowWrapper> rowFactory = handler.factory(rowSchema);
         RowBuilder<RowWrapper> builder = rowFactory.rowBuilder();
 
@@ -199,7 +199,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
     @Test
     public void testRowBuilderRejectInvalidField() {
-        RowSchema rowSchema = rowSchema(List.of(ColumnType.INT32), new Object[]{1});
+        StructNativeType rowSchema = rowSchema(List.of(ColumnType.INT32), new Object[]{1});
         RowFactory<RowWrapper> rowFactory = handler.factory(rowSchema);
 
         RowBuilder<RowWrapper> builder = rowFactory.rowBuilder();
@@ -211,7 +211,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
     @Test
     public void testRowBuilderBuildReset() {
-        RowSchema rowSchema = rowSchema(List.of(ColumnType.INT32), new Object[]{1});
+        StructNativeType rowSchema = rowSchema(List.of(ColumnType.INT32), new Object[]{1});
         RowFactory<RowWrapper> rowFactory = handler.factory(rowSchema);
 
         RowBuilder<RowWrapper> builder = rowFactory.rowBuilder();
@@ -237,7 +237,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
     @Test
     public void testRowBuilderBuildingIncompleteRowIsNotAllowed() {
-        RowSchema rowSchema = rowSchema(List.of(ColumnType.INT32, ColumnType.INT32), new Object[]{1, 2});
+        StructNativeType rowSchema = rowSchema(List.of(ColumnType.INT32, ColumnType.INT32), new Object[]{1, 2});
         RowFactory<RowWrapper> rowFactory = handler.factory(rowSchema);
 
         RowBuilder<RowWrapper> rowBuilder = rowFactory.rowBuilder();
@@ -249,19 +249,19 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
     @Test
     public void testRowBuilderEmptyRow() {
-        RowFactory<RowWrapper> rowFactory = handler.factory(RowSchema.builder().build());
+        RowFactory<RowWrapper> rowFactory = handler.factory(NativeTypes.rowBuilder().build());
         RowBuilder<RowWrapper> rowBuilder = rowFactory.rowBuilder();
         assertNotNull(rowBuilder.build());
     }
 
     @ParameterizedTest
     // TODO: https://issues.apache.org/jira/browse/IGNITE-17373 Interval type support.
-    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION"}, mode = EnumSource.Mode.EXCLUDE)
+    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = EnumSource.Mode.EXCLUDE)
     public void testIsNull(ColumnType columnType) {
         NativeType nativeType = TypeUtils.columnType2NativeType(columnType, 3, 3, 0);
 
-        RowSchema rowSchema = RowSchema.builder()
-                .addField(nativeType, true)
+        StructNativeType rowSchema = NativeTypes.rowBuilder()
+                .addField("C1", nativeType, true)
                 .build();
 
         RowFactory<RowWrapper> rowFactory = handler.factory(rowSchema);
@@ -279,23 +279,20 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
         }
     }
 
-    private RowSchema rowSchema(List<ColumnType> columnTypes, Object[] values) {
-        Builder schemaBuilder = RowSchema.builder();
+    private StructNativeType rowSchema(List<ColumnType> columnTypes, Object[] values) {
+        RowTypeBuilder schemaBuilder = NativeTypes.rowBuilder();
 
         for (int i = 0; i < values.length; i++) {
             ColumnType type = columnTypes.get(i);
 
-            if (type == ColumnType.NULL) {
-                schemaBuilder.addField(new NullTypeSpec());
-
-                continue;
-            }
-
             NativeType nativeType = values[i] == null
                     ? TypeUtils.columnType2NativeType(type, 9, 3, 20)
                     : NativeTypes.fromObject(values[i]);
 
-            schemaBuilder.addField(nativeType, values[i] == null || rnd.nextBoolean());
+            assertNotNull(nativeType, "Unable to create nativeType for columnType=" 
+                    + type + " and value={" + values[i] + "}");
+
+            schemaBuilder.addField("F$" + i, nativeType, values[i] == null || rnd.nextBoolean());
         }
 
         return schemaBuilder.build();
@@ -313,12 +310,11 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
         return values;
     }
 
-    private static Object[] wrap(Object[] values, RowSchema rowSchema) {
+    private static Object[] wrap(Object[] values, StructNativeType rowSchema) {
         Object[] newValues = new Object[values.length];
 
         for (int i = 0; i < values.length; i++) {
-            TypeSpec typeSpec = rowSchema.fields().get(i);
-            newValues[i] = convertToInternal(typeSpec, values[i]);
+            newValues[i] = convertToInternal(rowSchema.fields().get(i).type(), values[i]);
         }
 
         return newValues;
@@ -326,7 +322,7 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
 
     private static Set<ColumnType> columnTypes() {
         // TODO Include ignored types to test after https://issues.apache.org/jira/browse/IGNITE-17373
-        return EnumSet.complementOf(EnumSet.of(ColumnType.PERIOD, ColumnType.DURATION));
+        return EnumSet.complementOf(EnumSet.of(ColumnType.PERIOD, ColumnType.DURATION, ColumnType.STRUCT));
     }
 
     private List<ColumnType> shuffledColumnTypes() {
@@ -337,41 +333,11 @@ public class SqlRowHandlerTest extends IgniteAbstractTest {
         return columnTypes;
     }
 
-    private final class ConcatTestParameters {
-        final RowSchema leftSchema;
-        final RowSchema rightSchema;
-        final Object[] leftData;
-        final Object[] rightData;
-        final RowWrapper left;
-        final RowWrapper right;
-
-        ConcatTestParameters(boolean leftTupleRequired, boolean rightTupleRequired) {
-            List<ColumnType> columnTypes1 = shuffledColumnTypes();
-            List<ColumnType> columnTypes2 = shuffledColumnTypes();
-
-            leftData = values(columnTypes1);
-            rightData = values(columnTypes2);
-            leftSchema = rowSchema(columnTypes1, leftData);
-            rightSchema = rowSchema(columnTypes2, rightData);
-
-            RowFactory<RowWrapper> factory1 = handler.factory(leftSchema);
-            RowFactory<RowWrapper> factory2 = handler.factory(rightSchema);
-
-            RowWrapper left = factory1.create(wrap(leftData, leftSchema));
-            RowWrapper right = factory2.create(wrap(rightData, rightSchema));
-
-            this.left = leftTupleRequired ? factory1.create(handler.toBinaryTuple(left)) : left;
-            this.right = rightTupleRequired ? factory2.create(handler.toBinaryTuple(right)) : right;
-        }
-    }
-
-    private static @Nullable Object convertToInternal(TypeSpec typeSpec, Object value) {
-        if (typeSpec instanceof NullTypeSpec) {
+    private static @Nullable Object convertToInternal(NativeType type, Object value) {
+        if (type.spec() == ColumnType.NULL) {
             return null;
-        } else {
-            BaseTypeSpec baseTypeSpec = (BaseTypeSpec) typeSpec;
-            NativeType nativeType = baseTypeSpec.nativeType();
-            return TypeUtils.toInternal(value, nativeType.spec());
         }
+
+        return TypeUtils.toInternal(value, type.spec());
     }
 }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/ArrayRowHandler.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/ArrayRowHandler.java
index 0fea828a45f..a919c963acd 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/ArrayRowHandler.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/ArrayRowHandler.java
@@ -31,12 +31,11 @@ import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.schema.InvalidTypeException;
 import org.apache.ignite.internal.sql.engine.exec.RowHandler;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchemaTypes;
 import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.type.DecimalNativeType;
 import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -84,8 +83,8 @@ public class ArrayRowHandler implements RowHandler<Object[]> {
 
     /** {@inheritDoc} */
     @Override
-    public RowFactory<Object[]> factory(RowSchema rowSchema) {
-        int schemaLen = rowSchema.fields().size();
+    public RowFactory<Object[]> factory(StructNativeType rowType) {
+        int schemaLen = rowType.fields().size();
 
         return new RowFactory<>() {
             /** {@inheritDoc} */
@@ -119,7 +118,7 @@ public class ArrayRowHandler implements RowHandler<Object[]> {
                 Object[] row = new Object[tuple.elementCount()];
 
                 for (int i = 0; i < row.length; i++) {
-                    NativeType nativeType = RowSchemaTypes.toNativeType(rowSchema.fields().get(i));
+                    NativeType nativeType = rowType.fields().get(i).type();
 
                     if (nativeType == null) {
                         row[i] = null;
@@ -135,13 +134,13 @@ public class ArrayRowHandler implements RowHandler<Object[]> {
 
             /** {@inheritDoc} */
             @Override
-            public RowSchema rowSchema() {
-                return rowSchema;
+            public StructNativeType rowSchema() {
+                return rowType;
             }
 
             @Override
             public Object[] map(Object[] row, int[] mapping) {
-                assert mapping.length == rowSchema.fields().size();
+                assert mapping.length == rowType.fields().size();
                 Object[] newRow = new Object[mapping.length];
 
                 for (int i = 0; i < mapping.length; i++) {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
index 6cead4f4e85..dd40eb1e555 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/TypeUtilsTest.java
@@ -21,6 +21,17 @@ import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
 import static org.apache.ignite.internal.sql.engine.util.TypeUtils.lowerBoundFor;
 import static org.apache.ignite.internal.sql.engine.util.TypeUtils.upperBoundFor;
+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;
+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.NULL;
+import static org.apache.ignite.internal.type.NativeTypes.STRING;
+import static org.apache.ignite.internal.type.NativeTypes.UUID;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -40,25 +51,20 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.calcite.avatica.util.ByteString;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeFactory.Builder;
-import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.ignite.internal.sql.engine.exec.row.BaseTypeSpec;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
-import org.apache.ignite.internal.sql.engine.exec.row.RowSchemaTypes;
-import org.apache.ignite.internal.sql.engine.exec.row.RowType;
-import org.apache.ignite.internal.sql.engine.exec.row.TypeSpec;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.StructNativeType;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.sql.ColumnType;
 import org.junit.jupiter.api.DynamicTest;
@@ -78,17 +84,6 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
 
     private static final IgniteTypeFactory TYPE_FACTORY = Commons.typeFactory();
 
-    private static final BaseTypeSpec BOOLEAN = RowSchemaTypes.nativeType(NativeTypes.BOOLEAN);
-    private static final BaseTypeSpec INT8 = RowSchemaTypes.nativeType(NativeTypes.INT8);
-    private static final BaseTypeSpec INT16 = RowSchemaTypes.nativeType(NativeTypes.INT16);
-    private static final BaseTypeSpec INT32 = RowSchemaTypes.nativeType(NativeTypes.INT32);
-    private static final BaseTypeSpec INT64 = RowSchemaTypes.nativeType(NativeTypes.INT64);
-    private static final BaseTypeSpec FLOAT = RowSchemaTypes.nativeType(NativeTypes.FLOAT);
-    private static final BaseTypeSpec DOUBLE = RowSchemaTypes.nativeType(NativeTypes.DOUBLE);
-    private static final BaseTypeSpec STRING = RowSchemaTypes.nativeType(NativeTypes.stringOf(65536));
-    private static final BaseTypeSpec BYTES = RowSchemaTypes.nativeType(NativeTypes.blobOf(65536));
-    private static final BaseTypeSpec UUID = RowSchemaTypes.nativeType(NativeTypes.UUID);
-
     @Test
     public void testValidateCharactersOverflowAndTrimIfPossible() {
         IgniteTypeFactory typeFactory = Commons.typeFactory();
@@ -244,8 +239,7 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
     }
 
     private static Object[] buildTrimmedRow(RelDataType rowType, Object[] input) {
-        List<RelDataType> columnTypes = rowType.getFieldList().stream().map(RelDataTypeField::getType).collect(Collectors.toList());
-        RowSchema rowSchema = TypeUtils.rowSchemaFromRelTypes(columnTypes);
+        StructNativeType rowSchema = TypeUtils.convertStructuredType(rowType);
 
         Object[] newRow = TypeUtils.validateStringTypesOverflowAndTrimIfPossible(rowType,
                 ArrayRowHandler.INSTANCE,
@@ -387,11 +381,12 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
         });
     }
 
-    /** Conversion to base types + their NULLABLE variants from rel types. */
+    /** Conversion to base types. */
     @TestFactory
     public Stream<DynamicTest> testSimpleTypesConversion() {
         List<RelToExecTestCase> testCaseList = new ArrayList<>();
 
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.NULL, NULL));
         testCaseList.add(new RelToExecTestCase(SqlTypeName.BOOLEAN, BOOLEAN));
 
         testCaseList.add(new RelToExecTestCase(SqlTypeName.TINYINT, INT8));
@@ -402,68 +397,48 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
         testCaseList.add(new RelToExecTestCase(SqlTypeName.REAL, FLOAT));
         testCaseList.add(new RelToExecTestCase(SqlTypeName.DOUBLE, DOUBLE));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, RowSchemaTypes.nativeType(NativeTypes.decimalOf(32767, 0))));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, 10, RowSchemaTypes.nativeType(NativeTypes.decimalOf(10, 0))));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, 10, 4, RowSchemaTypes.nativeType(NativeTypes.decimalOf(10, 4))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, NativeTypes.decimalOf(32767, 0)));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, 10, NativeTypes.decimalOf(10, 0)));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.DECIMAL, 10, 4, NativeTypes.decimalOf(10, 4)));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.CHAR, RowSchemaTypes.nativeType(NativeTypes.stringOf(1))));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.CHAR, 8, RowSchemaTypes.nativeType(NativeTypes.stringOf(8))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.CHAR, NativeTypes.stringOf(1)));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.CHAR, 8, NativeTypes.stringOf(8)));
         testCaseList.add(new RelToExecTestCase(SqlTypeName.VARCHAR, STRING));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.VARCHAR, 8, RowSchemaTypes.nativeType(NativeTypes.stringOf(8))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.VARCHAR, 8, NativeTypes.stringOf(8)));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.BINARY, RowSchemaTypes.nativeType(NativeTypes.blobOf(1))));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.BINARY, 8, RowSchemaTypes.nativeType(NativeTypes.blobOf(8))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.BINARY, NativeTypes.blobOf(1)));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.BINARY, 8, NativeTypes.blobOf(8)));
         testCaseList.add(new RelToExecTestCase(SqlTypeName.VARBINARY, BYTES));
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.VARBINARY, 8, RowSchemaTypes.nativeType(NativeTypes.blobOf(8))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.VARBINARY, 8, NativeTypes.blobOf(8)));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.DATE, RowSchemaTypes.nativeType(NativeTypes.DATE)));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.DATE, NativeTypes.DATE));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIME, 4, RowSchemaTypes.nativeType(NativeTypes.time(4))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIME, 4, NativeTypes.time(4)));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, 4, RowSchemaTypes.nativeType(NativeTypes.time(4))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE, 4, NativeTypes.time(4)));
 
-        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIMESTAMP, 4, RowSchemaTypes.nativeType(NativeTypes.datetime(4))));
+        testCaseList.add(new RelToExecTestCase(SqlTypeName.TIMESTAMP, 4, NativeTypes.datetime(4)));
 
         testCaseList.add(new RelToExecTestCase(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 4,
-                RowSchemaTypes.nativeType(NativeTypes.timestamp(4))));
+                NativeTypes.timestamp(4)));
 
         for (SqlTypeName interval : SqlTypeName.YEAR_INTERVAL_TYPES) {
             SqlIntervalQualifier yearMonth = new SqlIntervalQualifier(interval.getStartUnit(), interval.getEndUnit(), SqlParserPos.ZERO);
             testCaseList.add(new RelToExecTestCase(TYPE_FACTORY.createSqlIntervalType(yearMonth),
-                    RowSchemaTypes.nativeType(NativeTypes.PERIOD)));
+                    NativeTypes.PERIOD));
         }
 
         for (SqlTypeName interval : SqlTypeName.DAY_INTERVAL_TYPES) {
             SqlIntervalQualifier dayTime = new SqlIntervalQualifier(interval.getStartUnit(), interval.getEndUnit(), SqlParserPos.ZERO);
             testCaseList.add(new RelToExecTestCase(TYPE_FACTORY.createSqlIntervalType(dayTime),
-                    RowSchemaTypes.nativeType(NativeTypes.DURATION)));
+                    NativeTypes.DURATION));
         }
 
         testCaseList.add(new RelToExecTestCase(SqlTypeName.UUID, UUID));
 
-        // Add test cases for nullable variants
-        for (RelToExecTestCase testCase : new ArrayList<>(testCaseList)) {
-            RelDataType nullableRelType = TYPE_FACTORY.createTypeWithNullability(testCase.input, true);
-            BaseTypeSpec typeSpec = (BaseTypeSpec) testCase.expected;
-
-            testCaseList.add(new RelToExecTestCase(nullableRelType, new BaseTypeSpec(typeSpec.nativeType(), true)));
-        }
-
         return testCaseList.stream().map(RelToExecTestCase::toTest);
     }
 
-    /** NULL type conversion. */
-    @TestFactory
-    public Stream<DynamicTest> testNullTypeConversion() {
-        RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
-
-        return Stream.of(
-                        new RelToExecTestCase(nullType, RowSchemaTypes.NULL),
-                        new RelToExecTestCase(TYPE_FACTORY.createTypeWithNullability(nullType, true), RowSchemaTypes.NULL)
-                )
-                .map(RelToExecTestCase::toTest);
-    }
-
     /** Conversion to row types from struct rel types. */
     @TestFactory
     public Stream<DynamicTest> testRowTypesConversion() {
@@ -476,7 +451,10 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
                 .add("f2", TYPE_FACTORY.createTypeWithNullability(TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER), true))
                 .build();
 
-        RowType expected1 = new RowType(List.of(BOOLEAN, new BaseTypeSpec(INT32.nativeType(), true)), false);
+        StructNativeType expected1 = NativeTypes.rowBuilder()
+                .addField("f1", BOOLEAN, false)
+                .addField("f2", INT32, true)
+                .build();
 
         RelToExecTestCase simpleRow = new RelToExecTestCase(relType1, expected1);
         testCaseList.add(simpleRow);
@@ -494,11 +472,18 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
                 )
                 .build();
 
-        RowType expected2 = new RowType(List.of(
-                BOOLEAN,
-                new BaseTypeSpec(INT32.nativeType(), true),
-                new RowType(List.of(new BaseTypeSpec(INT64.nativeType(), true), STRING), false)),
-                false);
+        StructNativeType expected2 = NativeTypes.rowBuilder()
+                .addField("f1", BOOLEAN, false)
+                .addField("f2", INT32, true)
+                .addField(
+                        "f3",
+                        NativeTypes.rowBuilder()
+                                .addField("f3_f1", INT64, true)
+                                .addField("f3_f2", STRING, false)
+                                .build(),
+                        false
+                )
+                .build();
 
         testCaseList.add(new RelToExecTestCase(relType2, expected2));
 
@@ -581,33 +566,32 @@ public class TypeUtilsTest extends BaseIgniteAbstractTest {
 
         final RelDataType input;
 
-        final TypeSpec expected;
+        final NativeType expected;
 
-        RelToExecTestCase(SqlTypeName input, TypeSpec expected) {
+        RelToExecTestCase(SqlTypeName input, NativeType expected) {
             this.input = TYPE_FACTORY.createSqlType(input);
             this.expected = expected;
         }
 
-        RelToExecTestCase(SqlTypeName input, int precision, TypeSpec expected) {
+        RelToExecTestCase(SqlTypeName input, int precision, NativeType expected) {
             this.input = TYPE_FACTORY.createSqlType(input, precision);
             this.expected = expected;
         }
 
-        RelToExecTestCase(SqlTypeName input, int precision, int scale, TypeSpec expected) {
+        RelToExecTestCase(SqlTypeName input, int precision, int scale, NativeType expected) {
             this.input = TYPE_FACTORY.createSqlType(input, precision, scale);
             this.expected = expected;
         }
 
-        RelToExecTestCase(RelDataType input, TypeSpec expected) {
+        RelToExecTestCase(RelDataType input, NativeType expected) {
             this.input = input;
             this.expected = expected;
         }
 
         DynamicTest toTest() {
             return DynamicTest.dynamicTest((input.isNullable() ? "NULLABLE " : "") + input, () -> {
-                RowSchema schema = TypeUtils.rowSchemaFromRelTypes(List.of(input));
+                NativeType actualType = IgniteTypeFactory.relDataTypeToNative(input);
 
-                TypeSpec actualType = schema.fields().get(0);
                 assertEquals(expected, actualType, input.getFullTypeString());
             });
         }
diff --git a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
index a560000f3f3..2082ff7f36a 100644
--- a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
+++ b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
@@ -127,6 +127,7 @@ public class SqlTestUtils {
         COLUMN_TYPE_TO_SQL_TYPE_NAME_MAP.put(ColumnType.UUID, SqlTypeName.UUID);
         COLUMN_TYPE_TO_SQL_TYPE_NAME_MAP.put(ColumnType.PERIOD, null);
         COLUMN_TYPE_TO_SQL_TYPE_NAME_MAP.put(ColumnType.DURATION, null);
+        COLUMN_TYPE_TO_SQL_TYPE_NAME_MAP.put(ColumnType.STRUCT, null);
 
         for (ColumnType value : ColumnType.values()) {
             assert COLUMN_TYPE_TO_SQL_TYPE_NAME_MAP.containsKey(value) : "absent type is " + value;
diff --git a/modules/system-view/src/test/java/org/apache/ignite/internal/systemview/SystemViewManagerTest.java b/modules/system-view/src/test/java/org/apache/ignite/internal/systemview/SystemViewManagerTest.java
index 7b55b50654a..b4f122b78d3 100644
--- a/modules/system-view/src/test/java/org/apache/ignite/internal/systemview/SystemViewManagerTest.java
+++ b/modules/system-view/src/test/java/org/apache/ignite/internal/systemview/SystemViewManagerTest.java
@@ -151,7 +151,7 @@ public class SystemViewManagerTest extends BaseIgniteAbstractTest {
     }
 
     @ParameterizedTest
-    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION"}, mode = Mode.EXCLUDE)
+    @EnumSource(value = ColumnType.class, names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Mode.EXCLUDE)
     public void registerAllColumnTypes(ColumnType typeSpec) {
         NativeType type = SchemaTestUtils.specToType(typeSpec);
 
diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java
index 5c7e6d2d3a8..3f20527a039 100644
--- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java
+++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItColocationTest.java
@@ -402,8 +402,8 @@ public class ItColocationTest extends BaseIgniteAbstractTest {
      */
     @CartesianTest
     public void colocationTwoColumnsInsert(
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
     ) {
         init(t0, t1);
 
@@ -427,8 +427,8 @@ public class ItColocationTest extends BaseIgniteAbstractTest {
      */
     @CartesianTest
     public void colocationTwoColumnsInsertAll(
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
-            @Enum(names = {"NULL", "PERIOD", "DURATION"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t0,
+            @Enum(names = {"NULL", "PERIOD", "DURATION", "STRUCT"}, mode = Enum.Mode.EXCLUDE) ColumnType t1
     ) {
         int keysCount = t0 == ColumnType.BOOLEAN && t0 == t1 ? 2 : KEYS;