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

amashenkov 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 2cb73723e41 IGNITE-24976 Sql. Optimize sql row serialization (#5536)
2cb73723e41 is described below

commit 2cb73723e4149b252fe0b4dded53ef511c71a46f
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Mon Apr 7 17:32:00 2025 +0300

    IGNITE-24976 Sql. Optimize sql row serialization (#5536)
---
 .../internal/binarytuple/BinaryTupleParser.java    |   2 +-
 .../internal/binarytuple/BinaryTupleReader.java    |  15 +++
 .../internal/benchmark/UpgradeRowBenchmark.java    | 113 +++++++++++++++++++++
 .../ignite/internal/schema/BinaryRowConverter.java |  69 +++++++++++++
 .../apache/ignite/internal/schema/BinaryTuple.java |   9 +-
 .../{BinaryTuple.java => InternalTupleEx.java}     |  28 ++---
 .../schema/registry/UpgradingRowAdapter.java       |  20 +++-
 .../org/apache/ignite/internal/schema/row/Row.java |   4 +-
 .../apache/ignite/internal/schema/row/RowImpl.java |  10 +-
 .../exec/ProjectedTableRowConverterImpl.java       |  27 ++---
 .../engine/exec/TableRowConverterFactoryImpl.java  |  15 +--
 .../internal/sql/engine/exec/VirtualColumn.java    |  10 +-
 ...ectedTuple.java => ExtendedProjectedTuple.java} |  70 ++++++++-----
 .../util/FieldDeserializingProjectedTuple.java     |   6 +-
 ...wareProjectedTuple.java => ProjectedTuple.java} |  62 +++++------
 .../exec/ProjectedTableRowConverterSelfTest.java   |   5 +-
 .../sql/engine/util/ProjectedTupleTest.java        |  50 ++++++++-
 17 files changed, 384 insertions(+), 131 deletions(-)

diff --git 
a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleParser.java
 
b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleParser.java
index cd28909643e..7f1985d1fd0 100644
--- 
a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleParser.java
+++ 
b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleParser.java
@@ -67,7 +67,7 @@ public class BinaryTupleParser {
     private final int valueBase;
 
     /** Binary tuple. */
-    private final ByteBuffer buffer;
+    protected final ByteBuffer buffer;
 
     /**
      * Constructor.
diff --git 
a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleReader.java
 
b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleReader.java
index a40f0b25c9c..db533a4a02b 100644
--- 
a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleReader.java
+++ 
b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleReader.java
@@ -395,4 +395,19 @@ public class BinaryTupleReader extends BinaryTupleParser 
implements BinaryTupleP
     public void seek(int index) {
         fetch(index, this);
     }
+
+    /**
+     * Copies raw value of the specified element to the builder.
+     *
+     * @param builder Builder to copy value to.
+     * @param index Index of the element.
+     */
+    protected void copyRawValue(BinaryTupleBuilder builder, int index) {
+        seek(index);
+        if (begin == end) {
+            builder.appendNull();
+        } else {
+            builder.appendElementBytes(buffer, begin, end - begin);
+        }
+    }
 }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/benchmark/UpgradeRowBenchmark.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/benchmark/UpgradeRowBenchmark.java
new file mode 100644
index 00000000000..6b86b0dafae
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/benchmark/UpgradeRowBenchmark.java
@@ -0,0 +1,113 @@
+/*
+ * 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.benchmark;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.sql.ResultSet;
+import org.apache.ignite.sql.SqlRow;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+/**
+ * Benchmark that runs sql queries via embedded client on clusters of 
different size.
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(1)
+@Warmup(iterations = 10, time = 2)
+@Measurement(iterations = 20, time = 2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class UpgradeRowBenchmark extends AbstractMultiNodeBenchmark {
+    private static final int TABLE_SIZE = 300_000;
+
+    private IgniteSql sql;
+
+    @Param({"1"})
+    private int clusterSize;
+
+    /** Fills the table with data. */
+    @Setup
+    public void setUp() throws IOException {
+        populateTable(TABLE_NAME, TABLE_SIZE, 1_000);
+
+        sql = publicIgnite.sql();
+
+        try (ResultSet<SqlRow> rs = sql.execute(null, format("ALTER TABLE {} 
ADD COLUMN new_col INT DEFAULT 42", TABLE_NAME));) {
+            while (rs.hasNext()) {
+                rs.next();
+            }
+        }
+    }
+
+    /** Benchmark that measures performance of `SELECT *` query over entire 
table. */
+    @Benchmark
+    public void selectAll(Blackhole bh) {
+        try (var rs = sql.execute(null, "SELECT * FROM usertable")) {
+            while (rs.hasNext()) {
+                bh.consume(rs.next());
+            }
+        }
+    }
+
+    /** Benchmark that measures performance of `SELECT` query with projection 
over entire table. */
+    @Benchmark
+    public void selectAllProjected(Blackhole bh) {
+        try (var rs = sql.execute(null, "SELECT field1, field2, field3 FROM 
usertable")) {
+            while (rs.hasNext()) {
+                bh.consume(rs.next());
+            }
+        }
+    }
+
+    /**
+     * Benchmark's entry point.
+     */
+    public static void main(String[] args) throws RunnerException {
+        Options opt = new OptionsBuilder()
+                .include(".*" + UpgradeRowBenchmark.class.getSimpleName() + 
".*")
+                .build();
+
+        new Runner(opt).run();
+    }
+
+    @Override
+    protected int nodes() {
+        return clusterSize;
+    }
+}
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryRowConverter.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryRowConverter.java
index 8ea03495b08..3821c957cf2 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryRowConverter.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryRowConverter.java
@@ -28,6 +28,7 @@ import 
org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.binarytuple.BinaryTupleFormatException;
 import org.apache.ignite.internal.binarytuple.BinaryTupleParser;
 import org.apache.ignite.internal.binarytuple.BinaryTupleParser.Sink;
+import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.TestOnly;
@@ -149,6 +150,74 @@ public class BinaryRowConverter implements 
ColumnsExtractor {
         throw new InvalidTypeException("Unexpected type value: " + 
element.typeSpec());
     }
 
+    /**
+     * Helper method that copy column value from given tuple to the binary 
tuple builder.
+     *
+     * @param delegate Source tuple to copy value from.
+     * @param builder Binary tuple builder to copy value to.
+     * @param element Binary schema element of the source tuple.
+     * @param col Column index in the delegate tuple.
+     */
+    public static void copyColumnValue(InternalTuple delegate, 
BinaryTupleBuilder builder, Element element, int col) {
+        if (delegate.hasNullValue(col)) {
+            builder.appendNull();
+
+            return;
+        }
+
+        switch (element.typeSpec()) {
+            case BOOLEAN:
+                builder.appendBoolean(delegate.booleanValue(col));
+                return;
+            case INT8:
+                builder.appendByte(delegate.byteValue(col));
+                return;
+            case INT16:
+                builder.appendShort(delegate.shortValue(col));
+                return;
+            case INT32:
+                builder.appendInt(delegate.intValue(col));
+                return;
+            case INT64:
+                builder.appendLong(delegate.longValue(col));
+                return;
+            case FLOAT:
+                builder.appendFloat(delegate.floatValue(col));
+                return;
+            case DOUBLE:
+                builder.appendDouble(delegate.doubleValue(col));
+                return;
+            case DECIMAL:
+                builder.appendDecimalNotNull(delegate.decimalValue(col, 
element.decimalScale()), element.decimalScale());
+                return;
+            case UUID:
+                builder.appendUuidNotNull(delegate.uuidValue(col));
+                return;
+            case BYTES:
+                builder.appendBytesNotNull(delegate.bytesValue(col));
+                return;
+            case STRING:
+                builder.appendStringNotNull(delegate.stringValue(col));
+                return;
+            case DATE:
+                builder.appendDateNotNull(delegate.dateValue(col));
+                return;
+            case TIME:
+                builder.appendTimeNotNull(delegate.timeValue(col));
+                return;
+            case DATETIME:
+                builder.appendDateTimeNotNull(delegate.dateTimeValue(col));
+                return;
+            case TIMESTAMP:
+                builder.appendTimestampNotNull(delegate.timestampValue(col));
+                return;
+            default:
+                break;
+        }
+
+        throw new InvalidTypeException("Unexpected type value: " + 
element.typeSpec());
+    }
+
     /**
      * Returns destination tuple schema.
      */
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
index 2d8da1d4484..688138cca30 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
@@ -18,14 +18,14 @@
 package org.apache.ignite.internal.schema;
 
 import java.nio.ByteBuffer;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
-import org.apache.ignite.internal.lang.InternalTuple;
 
 /**
  * Utility for access to binary tuple elements as typed values and with schema 
knowledge that allows to read
  * elements as objects.
  */
-public class BinaryTuple extends BinaryTupleReader implements InternalTuple {
+public class BinaryTuple extends BinaryTupleReader implements InternalTupleEx {
     /**
      * Constructor.
      *
@@ -45,4 +45,9 @@ public class BinaryTuple extends BinaryTupleReader implements 
InternalTuple {
     public BinaryTuple(int elementCount, ByteBuffer buffer) {
         super(elementCount, buffer);
     }
+
+    @Override
+    public void copyValue(BinaryTupleBuilder builder, int columnIndex) {
+        copyRawValue(builder, columnIndex);
+    }
 }
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/InternalTupleEx.java
similarity index 55%
copy from 
modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
copy to 
modules/schema/src/main/java/org/apache/ignite/internal/schema/InternalTupleEx.java
index 2d8da1d4484..335524c5d46 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/BinaryTuple.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/InternalTupleEx.java
@@ -17,32 +17,18 @@
 
 package org.apache.ignite.internal.schema;
 
-import java.nio.ByteBuffer;
-import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.lang.InternalTuple;
 
 /**
- * Utility for access to binary tuple elements as typed values and with schema 
knowledge that allows to read
- * elements as objects.
+ * Interface that provides a method to copy value (maybe raw) of the given 
column directly into a tuple builder.
  */
-public class BinaryTuple extends BinaryTupleReader implements InternalTuple {
+public interface InternalTupleEx extends InternalTuple {
     /**
-     * Constructor.
+     * Copy value of the given column.
      *
-     * @param elementCount Number of tuple elements.
-     * @param bytes Binary tuple.
+     * @param builder Binary tuple builder to copy value to.
+     * @param columnIndex Column index.
      */
-    public BinaryTuple(int elementCount, byte[] bytes) {
-        super(elementCount, bytes);
-    }
-
-    /**
-     * Constructor.
-     *
-     * @param elementCount Number of tuple elements.
-     * @param buffer Buffer with a binary tuple.
-     */
-    public BinaryTuple(int elementCount, ByteBuffer buffer) {
-        super(elementCount, buffer);
-    }
+    void copyValue(BinaryTupleBuilder builder, int columnIndex);
 }
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
index 58026797b57..1e8afe9cd8d 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
@@ -31,7 +31,6 @@ import 
org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.schema.BinaryRowConverter;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
-import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
 import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.InvalidTypeException;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
@@ -458,14 +457,25 @@ public class UpgradingRowAdapter implements Row {
         var builder = new BinaryTupleBuilder(size);
 
         for (int col = 0; col < size; col++) {
-            Element element = newBinaryTupleSchema.element(col);
-
-            BinaryRowConverter.appendValue(builder, element, value(col));
+            copyValue(builder, col);
         }
 
         return new BinaryTuple(size, builder.build()).byteBuffer();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void copyValue(BinaryTupleBuilder builder, int colIdx) {
+        int mappedId = mapColumn(colIdx);
+
+        if (mappedId < 0) {
+            Column column = mapper.mappedColumn(colIdx);
+            BinaryRowConverter.appendValue(builder, 
newBinaryTupleSchema.element(colIdx), column.defaultValue());
+        } else {
+            row.copyValue(builder, mappedId);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public int tupleSliceLength() {
@@ -478,7 +488,7 @@ public class UpgradingRowAdapter implements Row {
         throw new UnsupportedOperationException("Underlying binary can't be 
accessed directly.");
     }
 
-    private void ensureTypeConversionAllowed(ColumnType from, ColumnType to) 
throws InvalidTypeException {
+    private static void ensureTypeConversionAllowed(ColumnType from, 
ColumnType to) throws InvalidTypeException {
         if (!isSupportedColumnTypeChange(from, to)) {
             throw new SchemaException(format("Type conversion is not allowed: 
{} -> {}", from, to));
         }
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
index 34894a1fd63..67e09fcdd15 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
@@ -19,17 +19,17 @@ package org.apache.ignite.internal.schema.row;
 
 import java.math.BigDecimal;
 import org.apache.ignite.internal.binarytuple.BinaryTupleContainer;
-import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryRowEx;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
+import org.apache.ignite.internal.schema.InternalTupleEx;
 import org.apache.ignite.internal.schema.SchemaAware;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 
 /**
  * Schema-aware row interface.
  */
-public interface Row extends SchemaAware, BinaryRowEx, InternalTuple, 
BinaryTupleContainer {
+public interface Row extends SchemaAware, BinaryRowEx, InternalTupleEx, 
BinaryTupleContainer {
     /**
      * Creates a row from a given {@code BinaryRow}.
      *
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowImpl.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowImpl.java
index 592065f6fcf..181c256d3cc 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowImpl.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowImpl.java
@@ -19,9 +19,9 @@ package org.apache.ignite.internal.schema.row;
 
 import java.math.BigDecimal;
 import java.nio.ByteBuffer;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.schema.BinaryRow;
-import org.apache.ignite.internal.schema.BinaryRowEx;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.Column;
@@ -38,7 +38,7 @@ import org.jetbrains.annotations.Nullable;
  *
  * <p>When a non-boxed primitive is read from a null column value, it is 
converted to the primitive type default value.
  */
-public class RowImpl extends BinaryTupleReader implements Row, BinaryRowEx {
+public class RowImpl extends BinaryTupleReader implements Row {
     /** Schema descriptor. */
     private final SchemaDescriptor schema;
 
@@ -135,6 +135,12 @@ public class RowImpl extends BinaryTupleReader implements 
Row, BinaryRowEx {
         return new BinaryTuple(binaryTupleSchema.elementCount(), 
row.tupleSlice());
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void copyValue(BinaryTupleBuilder builder, int columnIndex) {
+        copyRawValue(builder, columnIndex);
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean equals(Object o) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterImpl.java
index 99a19bdacb6..d0b99f578a2 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/ProjectedTableRowConverterImpl.java
@@ -17,19 +17,18 @@
 
 package org.apache.ignite.internal.sql.engine.exec;
 
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.util.BitSet;
-import java.util.List;
 import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryTuple;
-import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.Column;
+import org.apache.ignite.internal.schema.InternalTupleEx;
 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.util.ExtendedFieldDeserializingProjectedTuple;
-import 
org.apache.ignite.internal.sql.engine.util.FieldDeserializingProjectedTuple;
-import org.apache.ignite.internal.sql.engine.util.FormatAwareProjectedTuple;
+import org.apache.ignite.internal.sql.engine.util.ExtendedProjectedTuple;
+import org.apache.ignite.internal.sql.engine.util.ProjectedTuple;
 
 /**
  * Converts rows to execution engine representation.
@@ -40,21 +39,17 @@ public class ProjectedTableRowConverterImpl extends 
TableRowConverterImpl {
      */
     private final int[] requiredColumnsMapping;
 
-    private final BinaryTupleSchema fullTupleSchema;
-
-    private final List<VirtualColumn> virtualColumns;
+    private final Int2ObjectMap<VirtualColumn> virtualColumns;
 
     /** Constructor. */
     ProjectedTableRowConverterImpl(
             SchemaRegistry schemaRegistry,
-            BinaryTupleSchema fullTupleSchema,
             SchemaDescriptor schemaDescriptor,
             BitSet requiredColumns,
-            List<VirtualColumn> extraColumns
+            Int2ObjectMap<VirtualColumn> extraColumns
     ) {
         super(schemaRegistry, schemaDescriptor);
 
-        this.fullTupleSchema = fullTupleSchema;
         this.virtualColumns = extraColumns;
 
         int size = requiredColumns.cardinality();
@@ -68,7 +63,7 @@ public class ProjectedTableRowConverterImpl extends 
TableRowConverterImpl {
             }
         }
 
-        for (VirtualColumn col : extraColumns) {
+        for (VirtualColumn col : extraColumns.values()) {
             requiredColumnsMapping[requiredIndex++] = col.columnIndex();
         }
     }
@@ -78,16 +73,14 @@ public class ProjectedTableRowConverterImpl extends 
TableRowConverterImpl {
         InternalTuple tuple;
         boolean rowSchemaMatches = tableRow.schemaVersion() == 
schemaDescriptor.version();
 
-        InternalTuple tableTuple = rowSchemaMatches
+        InternalTupleEx tableTuple = rowSchemaMatches
                 ? new BinaryTuple(schemaDescriptor.length(), 
tableRow.tupleSlice())
                 : schemaRegistry.resolve(tableRow, schemaDescriptor);
 
         if (!virtualColumns.isEmpty()) {
-            tuple = new 
ExtendedFieldDeserializingProjectedTuple(fullTupleSchema, tableTuple, 
requiredColumnsMapping, virtualColumns);
-        } else if (rowSchemaMatches) {
-            tuple = new FormatAwareProjectedTuple(tableTuple, 
requiredColumnsMapping);
+            tuple = new ExtendedProjectedTuple(tableTuple, 
requiredColumnsMapping, virtualColumns);
         } else {
-            tuple = new FieldDeserializingProjectedTuple(fullTupleSchema, 
tableTuple, requiredColumnsMapping);
+            tuple = new ProjectedTuple(tableTuple, requiredColumnsMapping);
         }
 
         return factory.create(tuple);
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterFactoryImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterFactoryImpl.java
index 1cac952417d..f187a47a049 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterFactoryImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/TableRowConverterFactoryImpl.java
@@ -17,10 +17,10 @@
 
 package org.apache.ignite.internal.sql.engine.exec;
 
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import java.util.BitSet;
-import java.util.List;
 import java.util.function.IntFunction;
-import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
 import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
@@ -35,7 +35,6 @@ import org.jetbrains.annotations.Nullable;
 public class TableRowConverterFactoryImpl implements TableRowConverterFactory {
     private final SchemaRegistry schemaRegistry;
     private final SchemaDescriptor schemaDescriptor;
-    private final BinaryTupleSchema fullTupleSchema;
     private final TableRowConverter fullRowConverter;
     private final BitSet tableColumnSet;
     private IntFunction<VirtualColumn> virtualColumnFactory;
@@ -56,7 +55,6 @@ public class TableRowConverterFactoryImpl implements 
TableRowConverterFactory {
     ) {
         this.schemaRegistry = schemaRegistry;
         this.schemaDescriptor = schemaDescriptor;
-        this.fullTupleSchema = 
BinaryTupleSchema.createRowSchema(schemaDescriptor);
 
         fullRowConverter = new TableRowConverterImpl(
                 schemaRegistry,
@@ -99,10 +97,15 @@ public class TableRowConverterFactoryImpl implements 
TableRowConverterFactory {
 
         return new ProjectedTableRowConverterImpl(
                 schemaRegistry,
-                fullTupleSchema,
                 schemaDescriptor,
                 requiredColumns,
-                requireVirtualColumn ? 
List.of(virtualColumnFactory.apply(partId)) : List.of()
+                requireVirtualColumn ? createVirtualColumns(partId) : 
Int2ObjectMaps.emptyMap()
         );
     }
+
+    private Int2ObjectMap<VirtualColumn> createVirtualColumns(int partId) {
+        VirtualColumn column = virtualColumnFactory.apply(partId);
+
+        return Int2ObjectMaps.singleton(column.columnIndex(), column);
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/VirtualColumn.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/VirtualColumn.java
index fb643469b1b..be1d2add033 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/VirtualColumn.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/VirtualColumn.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.sql.engine.exec;
 
+import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
 import org.apache.ignite.internal.tostring.IgniteToStringExclude;
 import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.internal.type.NativeType;
@@ -26,15 +27,16 @@ import org.apache.ignite.internal.type.NativeType;
  */
 public class VirtualColumn {
     private final int columnIndex;
-    private final NativeType type;
+    private final Element type;
     private final boolean nullable;
     @IgniteToStringExclude
     private final Object value;
 
-    VirtualColumn(int columnIndex, NativeType type, boolean nullable, Object 
value) {
+    /** Creates a new virtual column. */
+    public VirtualColumn(int columnIndex, NativeType type, boolean nullable, 
Object value) {
         this.columnIndex = columnIndex;
         this.value = value;
-        this.type = type;
+        this.type = new Element(type, nullable);
         this.nullable = nullable;
     }
 
@@ -42,7 +44,7 @@ public class VirtualColumn {
         return columnIndex;
     }
 
-    public NativeType type() {
+    public Element schemaType() {
         return type;
     }
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedFieldDeserializingProjectedTuple.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedProjectedTuple.java
similarity index 79%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedFieldDeserializingProjectedTuple.java
rename to 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedProjectedTuple.java
index 6529e5102f1..20d37b70478 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedFieldDeserializingProjectedTuple.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ExtendedProjectedTuple.java
@@ -18,79 +18,95 @@
 package org.apache.ignite.internal.sql.engine.util;
 
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
-import java.util.List;
 import java.util.UUID;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
-import org.apache.ignite.internal.lang.InternalTuple;
+import org.apache.ignite.internal.binarytuple.BinaryTupleParser.Sink;
 import org.apache.ignite.internal.schema.BinaryRowConverter;
 import org.apache.ignite.internal.schema.BinaryTuple;
-import org.apache.ignite.internal.schema.BinaryTupleSchema;
-import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
+import org.apache.ignite.internal.schema.InternalTupleEx;
 import org.apache.ignite.internal.sql.engine.exec.VirtualColumn;
 
 /**
- * A projected tuple that enriches {@link FieldDeserializingProjectedTuple} 
with extra columns.
+ * A projected tuple that enriches {@link ProjectedTuple} with extra columns.
  *
  * <p>Not thread safe!
  *
- * @see FieldDeserializingProjectedTuple
+ * @see ProjectedTuple
  */
-public class ExtendedFieldDeserializingProjectedTuple extends 
FieldDeserializingProjectedTuple {
+public class ExtendedProjectedTuple extends ProjectedTuple {
 
-    private final Int2ObjectMap<VirtualColumn> extraColumns;
+    private Int2ObjectMap<VirtualColumn> extraColumns;
 
     /**
      * Constructor.
      *
-     * @param schema A schema of the original tuple (represented by delegate). 
Used to read content of the delegate to build a
-     *         proper byte buffer which content satisfying the schema with 
regard to given projection.
      * @param delegate An original tuple to create projection from.
      * @param projection A projection. That is, desired order of fields in 
original tuple. In that projection, index of the array is
      *         an index of field in resulting projection, and an element of 
the array at that index is an index of column in original
      *         tuple.
      * @param extraColumns Extra columns.
      */
-    public ExtendedFieldDeserializingProjectedTuple(BinaryTupleSchema schema, 
InternalTuple delegate, int[] projection,
-            List<VirtualColumn> extraColumns) {
-        super(schema, delegate, projection);
+    public ExtendedProjectedTuple(InternalTupleEx delegate, int[] projection,
+            Int2ObjectMap<VirtualColumn> extraColumns) {
+        super(delegate, projection);
 
-        this.extraColumns = new Int2ObjectOpenHashMap<>(extraColumns.size());
-
-        extraColumns.forEach(c -> this.extraColumns.put(c.columnIndex(), c));
+        this.extraColumns = extraColumns;
     }
 
     @Override
     protected void normalize() {
-        var builder = new BinaryTupleBuilder(projection.length, 32, false);
+        int estimatedValueSize = 32;
+
+        if (delegate instanceof BinaryTuple) {
+            // Estimate total data size.
+            var stats = new Sink() {
+                int estimatedValueSize = 0;
+
+                @Override
+                public void nextElement(int index, int begin, int end) {
+                    estimatedValueSize += end - begin;
+                }
+            };
+
+
+            for (int columnIndex : projection) {
+                if (extraColumns.containsKey(columnIndex)) {
+                    stats.estimatedValueSize += 8;
+                    continue;
+                }
+                ((BinaryTuple) delegate).fetch(columnIndex, stats);
+            }
+            estimatedValueSize = stats.estimatedValueSize;
+        }
+
+        var builder = new BinaryTupleBuilder(projection.length, 
estimatedValueSize, false);
         var newProjection = new int[projection.length];
 
+        assert delegate instanceof InternalTupleEx;
+        InternalTupleEx delegate0 = (InternalTupleEx) delegate;
+
         for (int i = 0; i < projection.length; i++) {
             int col = projection[i];
-
             newProjection[i] = i;
 
             if (extraColumns.containsKey(col)) {
-                VirtualColumn column = extraColumns.get(col);
-
-                BinaryRowConverter.appendValue(builder, new 
Element(column.type(), true), column.value());
-
+                VirtualColumn virtualColumn = extraColumns.get(col);
+                BinaryRowConverter.appendValue(builder, 
virtualColumn.schemaType(), virtualColumn.value());
                 continue;
             }
 
-            Element element = schema.element(col);
-
-            BinaryRowConverter.appendValue(builder, element, 
schema.value(delegate, col));
+            delegate0.copyValue(builder, col);
         }
 
         delegate = new BinaryTuple(projection.length, builder.build());
         projection = newProjection;
-        extraColumns.clear();
+        extraColumns = Int2ObjectMaps.emptyMap();
     }
 
     private boolean isExtraColumn(int col) {
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FieldDeserializingProjectedTuple.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FieldDeserializingProjectedTuple.java
index 1bcd63b6e03..115858f6c24 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FieldDeserializingProjectedTuple.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FieldDeserializingProjectedTuple.java
@@ -62,11 +62,11 @@ public class FieldDeserializingProjectedTuple extends 
AbstractProjectedTuple {
         for (int i = 0; i < projection.length; i++) {
             int col = projection[i];
 
-            newProjection[i] = i;
-
             Element element = schema.element(col);
 
-            BinaryRowConverter.appendValue(builder, element, 
schema.value(delegate, col));
+            BinaryRowConverter.copyColumnValue(delegate, builder, element, 
col);
+
+            newProjection[i] = i;
         }
 
         delegate = new BinaryTuple(projection.length, builder.build());
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FormatAwareProjectedTuple.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ProjectedTuple.java
similarity index 55%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FormatAwareProjectedTuple.java
rename to 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ProjectedTuple.java
index 5e6b24ea4ef..67e38bd09c7 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/FormatAwareProjectedTuple.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/ProjectedTuple.java
@@ -17,26 +17,19 @@
 
 package org.apache.ignite.internal.sql.engine.util;
 
-import java.nio.ByteBuffer;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
-import org.apache.ignite.internal.binarytuple.BinaryTupleParser;
 import org.apache.ignite.internal.binarytuple.BinaryTupleParser.Sink;
-import org.apache.ignite.internal.lang.InternalTuple;
 import org.apache.ignite.internal.schema.BinaryTuple;
+import org.apache.ignite.internal.schema.InternalTupleEx;
 
 /**
- * A projected tuple that aware of the format of delegate.
- *
- * <p>That is, the format of delegate is known to be Binary Tuple, thus it's 
possible to avoid unnecessary
- * (de-)serialization during tuple normalization.
- *
- * <p>It's up to the caller to get sure that provided tuple respect the format.
+ * A projected tuple wrapper that is best effort to avoiding unnecessary 
(de-)serialization during tuple normalization.
  *
  * <p>Not thread safe!
  *
  * @see AbstractProjectedTuple
  */
-public class FormatAwareProjectedTuple extends AbstractProjectedTuple {
+public class ProjectedTuple extends AbstractProjectedTuple {
     /**
      * Constructor.
      *
@@ -45,46 +38,45 @@ public class FormatAwareProjectedTuple extends 
AbstractProjectedTuple {
      *         an index of field in resulting projection, and an element of 
the array at that index is an index of column in original
      *         tuple.
      */
-    public FormatAwareProjectedTuple(InternalTuple delegate, int[] projection) 
{
+    public ProjectedTuple(InternalTupleEx delegate, int[] projection) {
         super(delegate, projection);
     }
 
     @Override
     protected void normalize() {
-        int[] newProjection = new int[projection.length];
-        ByteBuffer tupleBuffer = delegate.byteBuffer();
+        BinaryTupleBuilder builder;
 
-        BinaryTupleParser parser = new 
BinaryTupleParser(delegate.elementCount(), tupleBuffer);
+        if (delegate instanceof BinaryTuple) {
+            // Estimate total data size.
+            var stats = new Sink() {
+                int estimatedValueSize = 0;
+
+                @Override
+                public void nextElement(int index, int begin, int end) {
+                    estimatedValueSize += end - begin;
+                }
+            };
 
-        // Estimate total data size.
-        var stats = new Sink() {
-            int estimatedValueSize = 0;
+            BinaryTuple binaryTuple = (BinaryTuple) delegate;
 
-            @Override
-            public void nextElement(int index, int begin, int end) {
-                estimatedValueSize += end - begin;
+            for (int columnIndex : projection) {
+                binaryTuple.fetch(columnIndex, stats);
             }
-        };
 
-        for (int columnIndex : projection) {
-            parser.fetch(columnIndex, stats);
+            builder = new BinaryTupleBuilder(projection.length, 
stats.estimatedValueSize, true);
+        } else {
+            builder = new BinaryTupleBuilder(projection.length, 32, false);
         }
 
-        // Now compose the tuple.
-        BinaryTupleBuilder builder = new BinaryTupleBuilder(projection.length, 
stats.estimatedValueSize);
+        assert delegate instanceof InternalTupleEx;
+        InternalTupleEx delegate0 = (InternalTupleEx) delegate;
 
+        // Now compose the tuple.
+        int[] newProjection = new int[projection.length];
         for (int i = 0; i < projection.length; i++) {
-            int columnIndex = projection[i];
-
-            parser.fetch(columnIndex, (index, begin, end) -> {
-                if (begin == end) {
-                    builder.appendNull();
-                } else {
-                    builder.appendElementBytes(tupleBuffer, begin, end - 
begin);
-                }
-            });
+            delegate0.copyValue(builder, projection[i]);
 
-            newProjection[i] = columnIndex;
+            newProjection[i] = i;
         }
 
         delegate = new BinaryTuple(projection.length, builder.build());
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 067450001ec..09e32e42dc0 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
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.exec;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.Map;
@@ -28,7 +29,6 @@ import org.apache.calcite.util.BitSets;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryRowImpl;
-import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
@@ -89,10 +89,9 @@ public class ProjectedTableRowConverterSelfTest extends 
BaseIgniteAbstractTest {
 
         ProjectedTableRowConverterImpl converter = new 
ProjectedTableRowConverterImpl(
                 schemaRegistry,
-                BinaryTupleSchema.createRowSchema(schema),
                 schema,
                 BitSets.of(1, 3),
-                List.of()
+                Int2ObjectMaps.emptyMap()
         );
 
         RowWrapper row = converter.toRow(executionContext, binaryRow, 
rowFactory);
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/ProjectedTupleTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/ProjectedTupleTest.java
index 5a97e4ca261..e4cac75daba 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/ProjectedTupleTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/ProjectedTupleTest.java
@@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Random;
@@ -37,6 +38,7 @@ import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
 import org.apache.ignite.internal.schema.BinaryTupleSchema.Element;
 import org.apache.ignite.internal.schema.SchemaTestUtils;
+import org.apache.ignite.internal.sql.engine.exec.VirtualColumn;
 import org.apache.ignite.internal.type.NativeTypeSpec;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -95,7 +97,7 @@ class ProjectedTupleTest {
         InternalTuple projection1 = new FieldDeserializingProjectedTuple(
                 ALL_TYPES_SCHEMA, TUPLE, new int[projectionSize]
         );
-        InternalTuple projection2 = new FormatAwareProjectedTuple(
+        InternalTuple projection2 = new ProjectedTuple(
                 TUPLE, new int[projectionSize]
         );
 
@@ -113,14 +115,14 @@ class ProjectedTupleTest {
         int[] projection = {f1, f2, f3};
 
         InternalTuple projectedTuple = useOptimizeProjection
-                ? new FormatAwareProjectedTuple(TUPLE, projection)
+                ? new ProjectedTuple(TUPLE, projection)
                 : new FieldDeserializingProjectedTuple(ALL_TYPES_SCHEMA, 
TUPLE, projection);
 
         Element e1 = ALL_TYPES_SCHEMA.element(f1);
         Element e2 = ALL_TYPES_SCHEMA.element(f2);
         Element e3 = ALL_TYPES_SCHEMA.element(f3);
 
-        BinaryTupleSchema projectedSchema = BinaryTupleSchema.create(new 
Element[] {
+        BinaryTupleSchema projectedSchema = BinaryTupleSchema.create(new 
Element[]{
                 e1, e2, e3
         });
 
@@ -133,5 +135,47 @@ class ProjectedTupleTest {
         assertThat(projectedSchema.value(restored, 0), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f1)));
         assertThat(projectedSchema.value(restored, 1), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f2)));
         assertThat(projectedSchema.value(restored, 2), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f3)));
+
+        // Ensure projected tuple is the same after normalization.
+        assertThat(projectedSchema.value(projectedTuple, 0), 
equalTo(projectedSchema.value(restored, 0)));
+        assertThat(projectedSchema.value(projectedTuple, 1), 
equalTo(projectedSchema.value(restored, 1)));
+        assertThat(projectedSchema.value(projectedTuple, 2), 
equalTo(projectedSchema.value(restored, 2)));
+    }
+
+    @Test
+    void testProjectionWithExtraColumn() {
+        int f1 = RND.nextInt(ALL_TYPES_SCHEMA.elementCount());
+        int f2 = RND.nextInt(ALL_TYPES_SCHEMA.elementCount());
+        int f3 = RND.nextInt(ALL_TYPES_SCHEMA.elementCount());
+        int f4 = RND.nextInt(ALL_TYPES_SCHEMA.elementCount());
+
+        VirtualColumn virtualColumn = new VirtualColumn(
+                ALL_TYPES_SCHEMA.elementCount(),
+                specToType(ALL_TYPES_SCHEMA.element(f2).typeSpec()),
+                true, ALL_TYPES_SCHEMA.value(TUPLE, f2));
+
+        int[] projection = {f1, virtualColumn.columnIndex(), f3, f4};
+
+        InternalTuple projectedTuple = new ExtendedProjectedTuple(TUPLE, 
projection,
+                Int2ObjectMaps.singleton(ALL_TYPES_SCHEMA.elementCount(), 
virtualColumn));
+
+        Element e1 = ALL_TYPES_SCHEMA.element(f1);
+        Element e2 = ALL_TYPES_SCHEMA.element(f2);
+        Element e3 = ALL_TYPES_SCHEMA.element(f3);
+        Element e4 = ALL_TYPES_SCHEMA.element(f4);
+
+        BinaryTupleSchema projectedSchema = BinaryTupleSchema.create(new 
Element[]{e1, e2, e3, e4});
+
+        assertThat(projectedSchema.value(projectedTuple, 0), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f1)));
+        assertThat(projectedSchema.value(projectedTuple, 1), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f2)));
+        assertThat(projectedSchema.value(projectedTuple, 2), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f3)));
+        assertThat(projectedSchema.value(projectedTuple, 3), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f4)));
+
+        InternalTuple restored = new BinaryTuple(projection.length, 
projectedTuple.byteBuffer());
+
+        assertThat(projectedSchema.value(restored, 0), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f1)));
+        assertThat(projectedSchema.value(restored, 1), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f2)));
+        assertThat(projectedSchema.value(restored, 2), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f3)));
+        assertThat(projectedSchema.value(restored, 3), 
equalTo(ALL_TYPES_SCHEMA.value(TUPLE, f4)));
     }
 }


Reply via email to