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

ptupitsyn 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 e7746cf275 IGNITE-21526 .NET: Clean up IEP-54 leftovers (#3349)
e7746cf275 is described below

commit e7746cf275a65a611b83ca6453aee2c3be08c000
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Tue Mar 5 14:06:23 2024 +0200

    IGNITE-21526 .NET: Clean up IEP-54 leftovers (#3349)
    
    Refactor the logic that relies on "key columns come first" design, support 
any key column order (use `keyIndex` from the server schema).
---
 .../SerializerHandlerBenchmarksBase.cs             |  16 +--
 .../SerializerHandlerReadBenchmarks.cs             |  12 +-
 .../SerializerHandlerWriteBenchmarks.cs            |  24 +---
 .../Proto/ColocationHashTests.cs                   |  14 +-
 .../Table/BinaryTupleIgniteTupleAdapterTests.cs    |  55 +++++++-
 .../Serialization/ObjectSerializerHandlerTests.cs  |  14 +-
 .../SerializerHandlerConsistencyTests.cs           | 152 +++++++++++++++++++++
 .../BinaryTuple/BinaryTupleIgniteTupleAdapter.cs   |  33 ++---
 .../dotnet/Apache.Ignite/Internal/Table/Column.cs  |  22 ++-
 .../Apache.Ignite/Internal/Table/DataStreamer.cs   |   6 +-
 .../{Column.cs => HashedColumnIndexProvider.cs}    |  35 +++--
 .../dotnet/Apache.Ignite/Internal/Table/Schema.cs  |  99 ++++++++++++--
 .../Serialization/IRecordSerializerHandler.cs      |  15 +-
 .../Table/Serialization/ObjectSerializerHandler.cs |  75 +++++-----
 .../Serialization/TuplePairSerializerHandler.cs    |  45 +++---
 .../Table/Serialization/TupleSerializerHandler.cs  |  41 +++---
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |  24 +---
 17 files changed, 480 insertions(+), 202 deletions(-)

diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
index bc94378f00..ad3f1b5188 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -44,16 +44,14 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             [nameof(Car.Seats)] = Object.Seats
         };
 
-        internal static readonly Schema Schema = new(
-            Version: 1,
-            TableId: 1,
-            KeyColumnCount: 1,
-            ColocationColumnCount: 1,
-            Columns: new[]
+        internal static readonly Schema Schema = Schema.CreateInstance(
+            version: 1,
+            tableId: 1,
+            columns: new[]
             {
-                new Column(nameof(Car.Id), ColumnType.Uuid, IsNullable: false, 
ColocationIndex: 0, IsKey: true, SchemaIndex: 0, Scale: 0, Precision: 0),
-                new Column(nameof(Car.BodyType), ColumnType.String, 
IsNullable: false, ColocationIndex: -1, IsKey: false, SchemaIndex: 1, Scale: 0, 
Precision: 0),
-                new Column(nameof(Car.Seats), ColumnType.Int32, IsNullable: 
false, ColocationIndex: -1, IsKey: false, SchemaIndex: 2, Scale: 0, Precision: 
0)
+                new Column(nameof(Car.Id), ColumnType.Uuid, IsNullable: false, 
ColocationIndex: 0, KeyIndex: 0, SchemaIndex: 0, Scale: 0, Precision: 0),
+                new Column(nameof(Car.BodyType), ColumnType.String, 
IsNullable: false, ColocationIndex: -1, KeyIndex: -1, SchemaIndex: 1, Scale: 0, 
Precision: 0),
+                new Column(nameof(Car.Seats), ColumnType.Int32, IsNullable: 
false, ColocationIndex: -1, KeyIndex: -1, SchemaIndex: 2, Scale: 0, Precision: 
0)
             });
 
         internal static readonly byte[] SerializedData = GetSerializedData();
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
index 4ad9571b2a..07c2f8d410 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
@@ -24,16 +24,16 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     using Internal.Table.Serialization;
 
     /// <summary>
-    /// Benchmarks for <see cref="IRecordSerializerHandler{T}.Read"/> 
implementations.
+    /// Benchmarks for <see cref="IRecordSerializerHandler{T}"/> read methods.
     ///
-    /// Results on i9-12900H, .NET SDK 6.0.405, Ubuntu 22.04:
+    /// Results on i9-12900H, .NET SDK 6.0.419, Ubuntu 22.04:
     ///
     /// |             Method |      Mean |    Error |   StdDev | Ratio | 
RatioSD |  Gen 0 | Allocated |
     /// |------------------- 
|----------:|---------:|---------:|------:|--------:|-------:|----------:|
-    /// |   ReadObjectManual |  53.23 ns | 0.320 ns | 0.268 ns |  1.00 |    
0.00 | 0.0003 |      80 B |
-    /// |         ReadObject |  88.01 ns | 0.484 ns | 0.453 ns |  1.65 |    
0.01 | 0.0002 |      80 B |
-    /// |          ReadTuple |  23.66 ns | 0.274 ns | 0.257 ns |  0.44 |    
0.00 | 0.0004 |     120 B |
-    /// | ReadTupleAndFields | 136.34 ns | 2.014 ns | 1.884 ns |  2.56 |    
0.04 | 0.0007 |     208 B |.
+    /// |   ReadObjectManual |  52.88 ns | 0.348 ns | 0.325 ns |  1.00 |    
0.00 | 0.0003 |      80 B |
+    /// |         ReadObject |  89.68 ns | 0.738 ns | 0.654 ns |  1.70 |    
0.02 | 0.0002 |      80 B |
+    /// |          ReadTuple |  20.02 ns | 0.147 ns | 0.131 ns |  0.38 |    
0.00 | 0.0004 |     112 B |
+    /// | ReadTupleAndFields | 127.92 ns | 2.234 ns | 2.194 ns |  2.42 |    
0.05 | 0.0007 |     200 B |.
     /// </summary>
     [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", 
Justification = "Benchmarks.")]
     [MemoryDiagnoser]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
index b91818af3c..f573ed8158 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
@@ -24,25 +24,15 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     using Internal.Table.Serialization;
 
     /// <summary>
-    /// Benchmarks for <see cref="IRecordSerializerHandler{T}.Write(ref 
Apache.Ignite.Internal.Proto.MsgPack.MsgPackWriter,Apache.Ignite.Internal.Table.Schema,T,bool,bool)"/>
 implementations.
+    /// Benchmarks for <see cref="IRecordSerializerHandler{T}"/> write methods.
     ///
-    /// Comparison of MessagePack library and our own implementation, 
i9-12900H, .NET SDK 6.0.405, Ubuntu 22.04:
+    /// Results on i9-12900H, .NET SDK 6.0.419, Ubuntu 22.04:
     ///
-    /// MessagePack 2.1.90 (old):
-    ///
-    /// |            Method |     Mean |   Error |  StdDev | Ratio |  Gen 0 | 
Allocated |
-    /// |------------------ 
|---------:|--------:|--------:|------:|-------:|----------:|
-    /// | WriteObjectManual | 189.6 ns | 0.55 ns | 0.51 ns |  1.00 | 0.0002 |  
    80 B |
-    /// |       WriteObject | 221.7 ns | 0.78 ns | 0.73 ns |  1.17 | 0.0002 |  
    80 B |
-    /// |        WriteTuple | 310.3 ns | 1.59 ns | 1.41 ns |  1.64 | 0.0005 |  
   184 B |
-    ///
-    /// Custom MsgPack (new):
-    ///
-    /// |            Method |     Mean |   Error |  StdDev | Ratio |  Gen 0 | 
Allocated |
-    /// |------------------ 
|---------:|--------:|--------:|------:|-------:|----------:|
-    /// | WriteObjectManual | 135.7 ns | 1.13 ns | 1.06 ns |  1.00 | 0.0002 |  
    80 B |
-    /// |       WriteObject | 161.2 ns | 0.59 ns | 0.52 ns |  1.19 | 0.0002 |  
    80 B |
-    /// |        WriteTuple | 250.5 ns | 0.91 ns | 0.85 ns |  1.85 | 0.0005 |  
   184 B |.
+    /// |            Method |     Mean |   Error |  StdDev | Ratio | RatioSD | 
 Gen 0 | Allocated |
+    /// |------------------ 
|---------:|--------:|--------:|------:|--------:|-------:|----------:|
+    /// | WriteObjectManual | 116.4 ns | 1.33 ns | 1.11 ns |  1.00 |    0.00 | 
0.0002 |      80 B |
+    /// |       WriteObject | 137.5 ns | 1.26 ns | 1.18 ns |  1.18 |    0.01 | 
0.0002 |      80 B |
+    /// |        WriteTuple | 213.3 ns | 2.76 ns | 2.59 ns |  1.83 |    0.02 | 
0.0007 |     184 B |.
     /// </summary>
     [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", 
Justification = "Benchmarks.")]
     [MemoryDiagnoser]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
index 9d9b3e103b..6f19c79f25 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
@@ -239,7 +239,7 @@ public class ColocationHashTests : IgniteTestsBase
             var schema = GetSchema(arr, timePrecision, timestampPrecision);
             var noValueSet = new byte[arr.Count].AsSpan();
 
-            TupleSerializerHandler.Instance.Write(ref builder, igniteTuple, 
schema, arr.Count, noValueSet);
+            TupleSerializerHandler.Instance.Write(ref builder, igniteTuple, 
schema, keyOnly: false, noValueSet);
             return builder.GetHash();
         }
         finally
@@ -265,7 +265,7 @@ public class ColocationHashTests : IgniteTestsBase
     {
         var columns = arr.Select((obj, ci) => GetColumn(obj, ci, 
timePrecision, timestampPrecision)).ToArray();
 
-        return new Schema(Version: 0, 0, arr.Count, arr.Count, columns);
+        return Schema.CreateInstance(version: 0, tableId: 0, columns);
     }
 
     private static Column GetColumn(object value, int schemaIndex, int 
timePrecision, int timestampPrecision)
@@ -301,7 +301,15 @@ public class ColocationHashTests : IgniteTestsBase
 
         var scale = value is decimal d ? 
BitConverter.GetBytes(decimal.GetBits(d)[3])[2] : 0;
 
-        return new Column("m_Item" + (schemaIndex + 1), colType, false, true, 
schemaIndex, schemaIndex, Scale: scale, precision);
+        return new Column(
+            Name: "m_Item" + (schemaIndex + 1),
+            Type: colType,
+            IsNullable: false,
+            KeyIndex: schemaIndex,
+            ColocationIndex: schemaIndex,
+            SchemaIndex: schemaIndex,
+            Scale: scale,
+            Precision: precision);
     }
 
     private async Task AssertClientAndServerHashesAreEqual(int timePrecision = 
9, int timestampPrecision = 6, params object[] keys)
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
index 3a1727b461..cac949c183 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
@@ -65,6 +65,55 @@ public class BinaryTupleIgniteTupleAdapterTests : 
IgniteTupleTests
         Assert.IsNull(tuple.GetFieldValue<object>("_schema"));
     }
 
+    [Test]
+    public void TestReverseKeyColumnOrder([Values(true, false)] bool keyOnly)
+    {
+        var cols = new[]
+        {
+            new Column("val1", ColumnType.String, false, KeyIndex: -1, 
ColocationIndex: -1, SchemaIndex: 0, 0, 0),
+            new Column("key1", ColumnType.Int32, false, KeyIndex: 1, 
ColocationIndex: 0, SchemaIndex: 1, 0, 0),
+            new Column("val2", ColumnType.Uuid, false, KeyIndex: -1, 
ColocationIndex: -1, SchemaIndex: 2, 0, 0),
+            new Column("key2", ColumnType.Int64, false, KeyIndex: 0, 
ColocationIndex: 1, SchemaIndex: 3, 0, 0)
+        };
+
+        var schema = Schema.CreateInstance(0, 0, cols);
+
+        using var builder = new 
BinaryTupleBuilder(schema.GetColumnsFor(keyOnly).Length);
+
+        if (keyOnly)
+        {
+            builder.AppendLong(456L);
+            builder.AppendInt(123);
+        }
+        else
+        {
+            builder.AppendString("v1");
+            builder.AppendInt(123);
+            builder.AppendGuid(Guid.Empty);
+            builder.AppendLong(456L);
+        }
+
+        var buf = builder.Build().ToArray();
+        var keyTuple = new BinaryTupleIgniteTupleAdapter(buf, schema, keyOnly: 
keyOnly);
+
+        Assert.AreEqual(keyOnly ? 2 : 4, keyTuple.FieldCount);
+        Assert.AreEqual(123, keyTuple["key1"]);
+        Assert.AreEqual(456L, keyTuple["key2"]);
+
+        if (keyOnly)
+        {
+            Assert.AreEqual(123, keyTuple[1]);
+            Assert.AreEqual(456L, keyTuple[0]);
+        }
+        else
+        {
+            Assert.AreEqual("v1", keyTuple[0]);
+            Assert.AreEqual(123, keyTuple[1]);
+            Assert.AreEqual(Guid.Empty, keyTuple[2]);
+            Assert.AreEqual(456L, keyTuple[3]);
+        }
+    }
+
     protected override string GetShortClassName() => 
nameof(BinaryTupleIgniteTupleAdapter);
 
     protected override IIgniteTuple CreateTuple(IIgniteTuple source)
@@ -77,16 +126,16 @@ public class BinaryTupleIgniteTupleAdapterTests : 
IgniteTupleTests
             var name = source.GetName(i);
             var val = source[i]!;
             var type = GetColumnType(val);
-            var col = new Column(name, type, true, false, 0, i, 0, 0);
+            var col = new Column(Name: name, Type: type, IsNullable: true, 
KeyIndex: i, ColocationIndex: i, SchemaIndex: i, Scale: 0, Precision: 0);
 
             cols.Add(col);
             builder.AppendObject(val, type);
         }
 
         var buf = builder.Build().ToArray();
-        var schema = new Schema(0, 0, 0, 0, cols);
+        var schema = Schema.CreateInstance(0, 0, cols.ToArray());
 
-        return new BinaryTupleIgniteTupleAdapter(buf, schema, cols.Count);
+        return new BinaryTupleIgniteTupleAdapter(buf, schema, keyOnly: false);
 
         static ColumnType GetColumnType(object? obj)
         {
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
index 58a08e61aa..17f52c8da6 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
@@ -32,15 +32,13 @@ namespace Apache.Ignite.Tests.Table.Serialization
     // ReSharper disable NotAccessedPositionalProperty.Local
     public class ObjectSerializerHandlerTests
     {
-        private static readonly Schema Schema = new(
-            Version: 1,
-            TableId: 1,
-            KeyColumnCount: 1,
-            ColocationColumnCount: 1,
-            Columns: new[]
+        private static readonly Schema Schema = Schema.CreateInstance(
+            version: 1,
+            tableId: 1,
+            columns: new[]
             {
-                new Column("Key", ColumnType.Int64, IsNullable: false, 
ColocationIndex: 0, IsKey: true, SchemaIndex: 0, Scale: 0, Precision: 0),
-                new Column("Val", ColumnType.String, IsNullable: false, 
ColocationIndex: -1, IsKey: false, SchemaIndex: 1, Scale: 0, Precision: 0)
+                new Column("Key", ColumnType.Int64, IsNullable: false, 
ColocationIndex: 0, KeyIndex: 0, SchemaIndex: 0, Scale: 0, Precision: 0),
+                new Column("Val", ColumnType.String, IsNullable: false, 
ColocationIndex: -1, KeyIndex: -1, SchemaIndex: 1, Scale: 0, Precision: 0)
             });
 
         [Test]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/SerializerHandlerConsistencyTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/SerializerHandlerConsistencyTests.cs
new file mode 100644
index 0000000000..084416438d
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/SerializerHandlerConsistencyTests.cs
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+// ReSharper disable UnusedAutoPropertyAccessor.Local
+namespace Apache.Ignite.Tests.Table.Serialization;
+
+using System;
+using Ignite.Sql;
+using Ignite.Table;
+using Internal.Buffers;
+using Internal.Table;
+using Internal.Table.Serialization;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests that different serializers produce consistent results.
+/// </summary>
+public class SerializerHandlerConsistencyTests
+{
+    private const int ExpectedColocationHash = 1326971215;
+
+    private static readonly Schema Schema = Schema.CreateInstance(
+        version: 0,
+        tableId: 0,
+        columns: new[]
+        {
+            new Column("val1", ColumnType.String, false, KeyIndex: -1, 
ColocationIndex: -1, SchemaIndex: 0, 0, 0),
+            new Column("key1", ColumnType.Int32, false, KeyIndex: 1, 
ColocationIndex: 0, SchemaIndex: 1, 0, 0),
+            new Column("val2", ColumnType.Uuid, false, KeyIndex: -1, 
ColocationIndex: -1, SchemaIndex: 2, 0, 0),
+            new Column("key2", ColumnType.String, false, KeyIndex: 0, 
ColocationIndex: 1, SchemaIndex: 3, 0, 0)
+        });
+
+    [Test]
+    public void TestSerializationAndHashing([Values(true, false)] bool keyOnly)
+    {
+        var tupleHandler = TupleSerializerHandler.Instance;
+        var tupleKvHandler = TuplePairSerializerHandler.Instance;
+        var objectHandler = new ObjectSerializerHandler<Poco>();
+        var objectKvHandler = new ObjectSerializerHandler<KvPair<PocoKey, 
PocoVal>>();
+
+        var poco = new Poco
+        {
+            Val1 = "v1",
+            Key1 = 123,
+            Val2 = Guid.NewGuid(),
+            Key2 = "k2"
+        };
+
+        var pocoKv = new KvPair<PocoKey, PocoVal>
+        {
+            Key = new PocoKey
+            {
+                Key1 = poco.Key1,
+                Key2 = poco.Key2
+            },
+            Val = new PocoVal
+            {
+                Val1 = poco.Val1,
+                Val2 = poco.Val2
+            }
+        };
+
+        var tuple = new IgniteTuple
+        {
+            ["key1"] = poco.Key1,
+            ["key2"] = poco.Key2
+        };
+
+        if (!keyOnly)
+        {
+            tuple["val1"] = poco.Val1;
+            tuple["val2"] = poco.Val2;
+        }
+
+        var tupleKv = new KvPair<IIgniteTuple, IIgniteTuple>(
+            new IgniteTuple
+            {
+                ["key1"] = poco.Key1,
+                ["key2"] = poco.Key2
+            },
+            new IgniteTuple());
+
+        if (!keyOnly)
+        {
+            tupleKv.Val["val1"] = poco.Val1;
+            tupleKv.Val["val2"] = poco.Val2;
+        }
+
+        var (tupleBuf, tupleHash) = Serialize(tupleHandler, tuple, keyOnly);
+        var (tupleKvBuf, tupleKvHash) = Serialize(tupleKvHandler, tupleKv, 
keyOnly);
+        var (pocoBuf, pocoHash) = Serialize(objectHandler, poco, keyOnly);
+        var (pocoKvBuf, pocoKvHash) = Serialize(objectKvHandler, pocoKv, 
keyOnly);
+
+        Assert.AreEqual(ExpectedColocationHash, tupleHash);
+        Assert.AreEqual(ExpectedColocationHash, tupleKvHash);
+        Assert.AreEqual(ExpectedColocationHash, pocoHash);
+        Assert.AreEqual(ExpectedColocationHash, pocoKvHash);
+
+        CollectionAssert.AreEqual(tupleBuf, tupleKvBuf);
+        CollectionAssert.AreEqual(tupleBuf, pocoBuf);
+        CollectionAssert.AreEqual(tupleBuf, pocoKvBuf);
+    }
+
+    private static (byte[] Buf, int Hash) 
Serialize<T>(IRecordSerializerHandler<T> handler, T obj, bool keyOnly = false)
+    {
+        using var buf = new PooledArrayBuffer();
+
+        var writer = buf.MessageWriter;
+        var hash = handler.Write(ref writer, Schema, obj, keyOnly, 
computeHash: true);
+
+        return (buf.GetWrittenMemory().ToArray(), hash);
+    }
+
+    private class Poco
+    {
+        public string? Val1 { get; set; }
+
+        public int Key1 { get; set; }
+
+        public Guid Val2 { get; set; }
+
+        public string Key2 { get; set; } = string.Empty;
+    }
+
+    private class PocoKey
+    {
+        public int Key1 { get; set; }
+
+        public string Key2 { get; set; } = string.Empty;
+    }
+
+    private class PocoVal
+    {
+        public string? Val1 { get; set; }
+
+        public Guid Val2 { get; set; }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
index 2e4230be28..289d8dd4a5 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
@@ -30,14 +30,12 @@ using Table.Serialization;
 /// </summary>
 internal sealed class BinaryTupleIgniteTupleAdapter : IIgniteTuple, 
IEquatable<BinaryTupleIgniteTupleAdapter>, IEquatable<IIgniteTuple>
 {
-    private readonly int _schemaFieldCount; // Key-only tuples have less 
fields than schema.
+    private readonly bool _keyOnly;
 
     private Memory<byte> _data;
 
     private Schema? _schema;
 
-    private Dictionary<string, int>? _indexes;
-
     private IgniteTuple? _tuple;
 
     /// <summary>
@@ -45,25 +43,25 @@ internal sealed class BinaryTupleIgniteTupleAdapter : 
IIgniteTuple, IEquatable<B
     /// </summary>
     /// <param name="data">Binary tuple data.</param>
     /// <param name="schema">Schema.</param>
-    /// <param name="fieldCount">Field count.</param>
-    public BinaryTupleIgniteTupleAdapter(Memory<byte> data, Schema schema, int 
fieldCount)
+    /// <param name="keyOnly">Whether only the key part should be 
exposed.</param>
+    public BinaryTupleIgniteTupleAdapter(Memory<byte> data, Schema schema, 
bool keyOnly)
     {
-        Debug.Assert(fieldCount <= schema.Columns.Count, "fieldCount <= 
schema.Columns.Count");
-
         _data = data;
         _schema = schema;
-        _schemaFieldCount = fieldCount;
+        _keyOnly = keyOnly;
     }
 
     /// <inheritdoc/>
-    public int FieldCount => _tuple?.FieldCount ?? _schemaFieldCount;
+    public int FieldCount => _tuple?.FieldCount ?? Columns.Count;
+
+    private IReadOnlyCollection<Column> Columns => 
_schema!.GetColumnsFor(_keyOnly);
 
     /// <inheritdoc/>
     public object? this[int ordinal]
     {
         get => _tuple != null
             ? _tuple[ordinal]
-            : TupleSerializerHandler.ReadObject(_data.Span, _schema!, 
_schemaFieldCount, ordinal);
+            : TupleSerializerHandler.ReadObject(_data.Span, _schema!, 
_keyOnly, ordinal);
 
         set => InitTuple()[ordinal] = value;
     }
@@ -93,17 +91,13 @@ internal sealed class BinaryTupleIgniteTupleAdapter : 
IIgniteTuple, IEquatable<B
             return _tuple.GetOrdinal(name);
         }
 
-        if (_indexes == null)
+        var column = _schema!.GetColumn(name);
+        if (column == null)
         {
-            _indexes = new Dictionary<string, int>(_schema!.Columns.Count);
-
-            for (var i = 0; i < _schema.Columns.Count; i++)
-            {
-                
_indexes[IgniteTupleCommon.ParseColumnName(_schema.Columns[i].Name)] = i;
-            }
+            return -1;
         }
 
-        return _indexes.TryGetValue(IgniteTupleCommon.ParseColumnName(name), 
out var index) ? index : -1;
+        return _keyOnly ? column.KeyIndex : column.SchemaIndex;
     }
 
     /// <inheritdoc/>
@@ -131,11 +125,10 @@ internal sealed class BinaryTupleIgniteTupleAdapter : 
IIgniteTuple, IEquatable<B
         Debug.Assert(_schema != null, "_schema != null");
 
         // Copy data to a mutable IgniteTuple.
-        _tuple = TupleSerializerHandler.ReadTuple(_data.Span, _schema, 
_schemaFieldCount);
+        _tuple = TupleSerializerHandler.ReadTuple(_data.Span, _schema, 
_keyOnly);
 
         // Release schema and data.
         _schema = default;
-        _indexes = default;
         _data = default;
 
         return _tuple;
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
index 0f3729291e..1288eca621 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
@@ -26,8 +26,26 @@ internal record Column(
     string Name,
     ColumnType Type,
     bool IsNullable,
-    bool IsKey,
+    int KeyIndex,
     int ColocationIndex,
     int SchemaIndex,
     int Scale,
-    int Precision);
+    int Precision)
+{
+    /// <summary>
+    /// Gets a value indicating whether this column is a part of the key.
+    /// </summary>
+    public bool IsKey => KeyIndex >= 0;
+
+    /// <summary>
+    /// Gets a value indicating whether this column is a part of the 
colocation key.
+    /// </summary>
+    public bool IsColocation => ColocationIndex >= 0;
+
+    /// <summary>
+    /// Gets the column index within a binary tuple.
+    /// </summary>
+    /// <param name="keyOnly">Whether a key-only binary tuple is used.</param>
+    /// <returns>Index within a binary tuple.</returns>
+    public int GetBinaryTupleIndex(bool keyOnly) => keyOnly ? KeyIndex : 
SchemaIndex;
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
index e6d388405d..63268259fb 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
@@ -162,7 +162,7 @@ internal static class DataStreamer
         (Batch<T> Batch, string Partition) Add(T item)
         {
             var schema0 = schema;
-            var tupleBuilder = new BinaryTupleBuilder(schema0.Columns.Count, 
hashedColumnsPredicate: schema0);
+            var tupleBuilder = new BinaryTupleBuilder(schema0.Columns.Length, 
hashedColumnsPredicate: schema0.HashedColumnIndexProvider);
 
             try
             {
@@ -176,14 +176,14 @@ internal static class DataStreamer
 
         (Batch<T> Batch, string Partition) Add0(T item, ref BinaryTupleBuilder 
tupleBuilder, Schema schema0)
         {
-            var columnCount = schema0.Columns.Count;
+            var columnCount = schema0.Columns.Length;
 
             // Use MemoryMarshal to work around [CS8352]: "Cannot use variable 
'noValueSet' in this context
             // because it may expose referenced variables outside of their 
declaration scope".
             Span<byte> noValueSet = stackalloc byte[columnCount / 8 + 1];
             Span<byte> noValueSetRef = MemoryMarshal.CreateSpan(ref 
MemoryMarshal.GetReference(noValueSet), columnCount);
 
-            writer.Handler.Write(ref tupleBuilder, item, schema0, columnCount, 
noValueSetRef);
+            writer.Handler.Write(ref tupleBuilder, item, schema0, keyOnly: 
false, noValueSetRef);
 
             // ReSharper disable once AccessToModifiedClosure (reviewed)
             var partitionAssignment0 = partitionAssignment;
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/HashedColumnIndexProvider.cs
similarity index 51%
copy from modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
copy to 
modules/platforms/dotnet/Apache.Ignite/Internal/Table/HashedColumnIndexProvider.cs
index 0f3729291e..a914dc920a 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/HashedColumnIndexProvider.cs
@@ -17,17 +17,30 @@
 
 namespace Apache.Ignite.Internal.Table;
 
-using Ignite.Sql;
+using System.Collections.Generic;
+using Proto.BinaryTuple;
 
 /// <summary>
-/// Schema column.
+/// Schema-based hashed column index provider.
 /// </summary>
-internal record Column(
-    string Name,
-    ColumnType Type,
-    bool IsNullable,
-    bool IsKey,
-    int ColocationIndex,
-    int SchemaIndex,
-    int Scale,
-    int Precision);
+internal sealed class HashedColumnIndexProvider : IHashedColumnIndexProvider
+{
+    private readonly IReadOnlyList<Column> _columns;
+
+    /// <summary>
+    /// Initializes a new instance of the <see 
cref="HashedColumnIndexProvider"/> class.
+    /// </summary>
+    /// <param name="columns">Columns.</param>
+    /// <param name="hashedColumnCount">Hashed column count.</param>
+    public HashedColumnIndexProvider(IReadOnlyList<Column> columns, int 
hashedColumnCount)
+    {
+        _columns = columns;
+        HashedColumnCount = hashedColumnCount;
+    }
+
+    /// <inheritdoc/>
+    public int HashedColumnCount { get; init; }
+
+    /// <inheritdoc/>
+    public int HashedColumnOrder(int index) => _columns[index].ColocationIndex;
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
index 8fa9c84238..d4e260d998 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
@@ -18,6 +18,9 @@
 namespace Apache.Ignite.Internal.Table
 {
     using System.Collections.Generic;
+    using System.Diagnostics;
+    using System.Diagnostics.CodeAnalysis;
+    using System.Linq;
     using Proto.BinaryTuple;
 
     /// <summary>
@@ -25,27 +28,101 @@ namespace Apache.Ignite.Internal.Table
     /// </summary>
     /// <param name="Version">Version.</param>
     /// <param name="TableId">Table id.</param>
-    /// <param name="KeyColumnCount">Key column count.</param>
     /// <param name="ColocationColumnCount">Colocation column count.</param>
     /// <param name="Columns">Columns in schema order.</param>
+    /// <param name="KeyColumns">Key part columns.</param>
+    /// <param name="ValColumns">Val part columns.</param>
+    /// <param name="ColumnsByName">Column name map.</param>
+    /// <param name="HashedColumnIndexProvider">Hashed column index 
provider.</param>
+    /// <param name="KeyOnlyHashedColumnIndexProvider">Hashed column index 
provider for key-only mode.</param>
+    [SuppressMessage("Performance", "CA1819:Properties should not return 
arrays", Justification = "Reviewed.")]
     internal sealed record Schema(
         int Version,
         int TableId,
-        int KeyColumnCount,
         int ColocationColumnCount,
-        IReadOnlyList<Column> Columns) : IHashedColumnIndexProvider
+        Column[] Columns,
+        Column[] KeyColumns,
+        Column[] ValColumns,
+        IReadOnlyDictionary<string, Column> ColumnsByName,
+        IHashedColumnIndexProvider HashedColumnIndexProvider,
+        IHashedColumnIndexProvider KeyOnlyHashedColumnIndexProvider)
     {
         /// <summary>
-        /// Gets the value column count.
+        /// Gets column by name.
         /// </summary>
-        public int ValueColumnCount => Columns.Count - KeyColumnCount;
+        /// <param name="name">Column name.</param>
+        /// <returns>Column or null.</returns>
+        public Column? GetColumn(string name) => 
ColumnsByName!.GetValueOrDefault(IgniteTupleCommon.ParseColumnName(name), null);
 
-        /// <inheritdoc/>
-        public int HashedColumnCount => ColocationColumnCount;
+        /// <summary>
+        /// Create schema instance.
+        /// </summary>
+        /// <param name="version">Version.</param>
+        /// <param name="tableId">Table ID.</param>
+        /// <param name="columns">Columns.</param>
+        /// <returns>Schema.</returns>
+        public static Schema CreateInstance(int version, int tableId, Column[] 
columns)
+        {
+            var keyColumnCount = columns.Count(static x => x.IsKey);
+            var keyColumns = new Column[keyColumnCount];
+            var valColumns = new Column[columns.Length - keyColumnCount];
+            int colocationColumnCount = 0;
+            int valIdx = 0;
+
+            foreach (var column in columns)
+            {
+                if (column.IsKey)
+                {
+                    Debug.Assert(keyColumns[column.KeyIndex] == null, 
"Duplicate key index: " + column);
+                    keyColumns[column.KeyIndex] = column;
+                }
+                else
+                {
+                    valColumns[valIdx++] = column;
+                }
+
+                if (column.IsColocation)
+                {
+                    colocationColumnCount++;
+                }
+            }
+
+            // ReSharper disable once 
ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+            Debug.Assert(keyColumns.All(x => x != null), "Some key columns are 
missing");
+            Debug.Assert(columns.Length == 0 || colocationColumnCount > 0, "No 
hashed columns");
+
+            var columnMap = new Dictionary<string, Column>(columns.Length);
+            foreach (var column in columns)
+            {
+                columnMap[IgniteTupleCommon.ParseColumnName(column.Name)] = 
column;
+            }
 
-        /// <inheritdoc/>
-        public int HashedColumnOrder(int index) => index < KeyColumnCount
-            ? Columns[index].ColocationIndex
-            : -1;
+            return new Schema(
+                version,
+                tableId,
+                colocationColumnCount,
+                columns,
+                keyColumns,
+                valColumns,
+                columnMap,
+                new HashedColumnIndexProvider(columns, colocationColumnCount),
+                new HashedColumnIndexProvider(keyColumns, 
colocationColumnCount));
+        }
+
+        /// <summary>
+        /// Gets columns.
+        /// </summary>
+        /// <param name="keyOnly">Key only flag.</param>
+        /// <returns>Columns according to the key flag.</returns>
+        public Column[] GetColumnsFor(bool keyOnly) =>
+            keyOnly ? KeyColumns : Columns;
+
+        /// <summary>
+        /// Gets the hashed column index provider for the specified key-only 
flag.
+        /// </summary>
+        /// <param name="keyOnly">Key only flag.</param>
+        /// <returns>Hashed column index provider.</returns>
+        public IHashedColumnIndexProvider GetHashedColumnIndexProviderFor(bool 
keyOnly) =>
+            keyOnly ? KeyOnlyHashedColumnIndexProvider : 
HashedColumnIndexProvider;
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
index 4b8219bb59..3446f3bada 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
@@ -47,15 +47,18 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// <returns>Key hash when <paramref name="computeHash"/> is 
<c>true</c>; 0 otherwise.</returns>
         int Write(ref MsgPackWriter writer, Schema schema, T record, bool 
keyOnly = false, bool computeHash = false)
         {
-            var columns = schema.Columns;
-            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+            var count = keyOnly ? schema.KeyColumns.Length : 
schema.Columns.Length;
             var noValueSet = writer.WriteBitSet(count);
 
-            var tupleBuilder = new BinaryTupleBuilder(count, 
hashedColumnsPredicate: computeHash ? schema : null);
+            var hashedColumnsPredicate = computeHash
+                ? schema.GetHashedColumnIndexProviderFor(keyOnly)
+                : null;
+
+            var tupleBuilder = new BinaryTupleBuilder(count, 
hashedColumnsPredicate: hashedColumnsPredicate);
 
             try
             {
-                Write(ref tupleBuilder, record, schema, count, noValueSet);
+                Write(ref tupleBuilder, record, schema, keyOnly, noValueSet);
 
                 var binaryTupleMemory = tupleBuilder.Build();
                 writer.Write(binaryTupleMemory.Span);
@@ -74,13 +77,13 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// <param name="tupleBuilder">Tuple builder.</param>
         /// <param name="record">Record.</param>
         /// <param name="schema">Schema.</param>
-        /// <param name="columnCount">Column count.</param>
+        /// <param name="keyOnly">Key only part.</param>
         /// <param name="noValueSet">No-value set.</param>
         void Write(
             ref BinaryTupleBuilder tupleBuilder,
             T record,
             Schema schema,
-            int columnCount,
+            bool keyOnly,
             Span<byte> noValueSet);
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
index d42651954b..35bcf52947 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -37,7 +37,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
     /// <typeparam name="T">Object type.</typeparam>
     internal sealed class ObjectSerializerHandler<T> : 
IRecordSerializerHandler<T>
     {
-        private readonly ConcurrentDictionary<(int, int), WriteDelegate<T>> 
_writers = new();
+        private readonly ConcurrentDictionary<(int, bool), WriteDelegate<T>> 
_writers = new();
 
         private readonly ConcurrentDictionary<(int, bool), ReadDelegate<T>> 
_readers = new();
 
@@ -56,7 +56,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 ? w
                 : _readers.GetOrAdd(cacheKey, EmitReader(schema, keyOnly));
 
-            var columnCount = keyOnly ? schema.KeyColumnCount : 
schema.Columns.Count;
+            var columnCount = keyOnly ? schema.KeyColumns.Length : 
schema.Columns.Length;
 
             var binaryTupleReader = new BinaryTupleReader(reader.ReadBinary(), 
columnCount);
 
@@ -64,18 +64,18 @@ namespace Apache.Ignite.Internal.Table.Serialization
         }
 
         /// <inheritdoc/>
-        public void Write(ref BinaryTupleBuilder tupleBuilder, T record, 
Schema schema, int columnCount, Span<byte> noValueSet)
+        public void Write(ref BinaryTupleBuilder tupleBuilder, T record, 
Schema schema, bool keyOnly, Span<byte> noValueSet)
         {
-            var cacheKey = (schema.Version, columnCount);
+            var cacheKey = (schema.Version, keyOnly);
 
             var writeDelegate = _writers.TryGetValue(cacheKey, out var w)
                 ? w
-                : _writers.GetOrAdd(cacheKey, EmitWriter(schema, columnCount));
+                : _writers.GetOrAdd(cacheKey, EmitWriter(schema, keyOnly));
 
             writeDelegate(ref tupleBuilder, noValueSet, record);
         }
 
-        private static WriteDelegate<T> EmitWriter(Schema schema, int count)
+        private static WriteDelegate<T> EmitWriter(Schema schema, bool keyOnly)
         {
             var type = typeof(T);
 
@@ -88,12 +88,12 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             if (type.IsGenericType && type.GetGenericTypeDefinition() == 
typeof(KvPair<,>))
             {
-                return EmitKvWriter(schema, count, method);
+                return EmitKvWriter(schema, keyOnly, method);
             }
 
             var il = method.GetILGenerator();
 
-            var columns = schema.Columns;
+            var columns = schema.GetColumnsFor(keyOnly);
             var columnMap = type.GetFieldsByColumnName();
 
             if (BinaryTupleMethods.GetWriteMethodOrNull(type) is { } 
directWriteMethod)
@@ -116,7 +116,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
                 il.Emit(OpCodes.Call, directWriteMethod);
 
-                for (var index = 1; index < count; index++)
+                for (var index = 1; index < columns.Length; index++)
                 {
                     il.Emit(OpCodes.Ldarg_0); // writer
                     il.Emit(OpCodes.Ldarg_1); // noValueSet
@@ -130,9 +130,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             int mappedCount = 0;
 
-            for (var index = 0; index < count; index++)
+            foreach (var col in columns)
             {
-                var col = columns[index];
                 var fieldInfo = columnMap.TryGetValue(col.Name, out var 
columnInfo) ? columnInfo.Field : null;
 
                 if (fieldInfo == null)
@@ -164,14 +163,14 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 }
             }
 
-            ValidateMappedCount(mappedCount, type, schema, count);
+            ValidateMappedCount(mappedCount, type, schema, keyOnly);
 
             il.Emit(OpCodes.Ret);
 
             return 
(WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
         }
 
-        private static WriteDelegate<T> EmitKvWriter(Schema schema, int count, 
DynamicMethod method)
+        private static WriteDelegate<T> EmitKvWriter(Schema schema, bool 
keyOnly, DynamicMethod method)
         {
             var (keyType, valType, keyField, valField, keyColumnMap, 
valColumnMap) = GetKeyValTypes();
 
@@ -180,21 +179,19 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             var il = method.GetILGenerator();
 
-            var columns = schema.Columns;
+            var columns = schema.GetColumnsFor(keyOnly);
 
             int mappedCount = 0;
 
-            for (var index = 0; index < count; index++)
+            foreach (var col in columns)
             {
-                var col = columns[index];
-
                 FieldInfo? fieldInfo;
 
-                if (keyWriteMethod != null && index == 0)
+                if (keyWriteMethod != null && col.IsKey)
                 {
                     fieldInfo = keyField;
                 }
-                else if (valWriteMethod != null && index == 
schema.KeyColumnCount)
+                else if (valWriteMethod != null && !col.IsKey)
                 {
                     fieldInfo = valField;
                 }
@@ -219,7 +216,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 {
                     ValidateFieldType(fieldInfo, col);
 
-                    var field = index < schema.KeyColumnCount ? keyField : 
valField;
+                    var field = col.IsKey ? keyField : valField;
 
                     if (field != fieldInfo)
                     {
@@ -262,7 +259,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 keyWriteMethod != null ? null : keyType,
                 valWriteMethod != null ? null : valType,
                 schema,
-                count);
+                keyOnly);
 
             il.Emit(OpCodes.Ret);
 
@@ -306,16 +303,15 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             var local = il.DeclareAndInitLocal(type);
 
-            var columns = schema.Columns;
-            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+            var columns = schema.GetColumnsFor(keyOnly);
             var columnMap = type.GetFieldsByColumnName();
 
-            for (var i = 0; i < count; i++)
+            foreach (var col in columns)
             {
-                var col = columns[i];
                 var fieldInfo = columnMap.TryGetValue(col.Name, out var 
columnInfo) ? columnInfo.Field : null;
+                var idx = col.GetBinaryTupleIndex(keyOnly);
 
-                EmitFieldRead(fieldInfo, il, col, i, local);
+                EmitFieldRead(fieldInfo, il, col, idx, local);
             }
 
             il.Emit(OpCodes.Ldloc_0); // res
@@ -338,22 +334,20 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var keyLocal = keyMethod == null ? il.DeclareAndInitLocal(keyType) 
: null;
             var valLocal = valMethod == null ? il.DeclareAndInitLocal(valType) 
: null;
 
-            var columns = schema.Columns;
-            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+            var columns = schema.GetColumnsFor(keyOnly);
 
-            for (var i = 0; i < count; i++)
+            foreach (var col in columns)
             {
-                var col = columns[i];
                 FieldInfo? fieldInfo;
                 LocalBuilder? local;
 
-                if (i == 0 && keyMethod != null)
+                if (col.IsKey && keyMethod != null)
                 {
                     fieldInfo = keyField;
                     local = kvLocal;
                     ValidateSingleFieldMappingType(keyType, col);
                 }
-                else if (i == schema.KeyColumnCount && valMethod != null)
+                else if (!col.IsKey && valMethod != null)
                 {
                     fieldInfo = valField;
                     local = kvLocal;
@@ -369,7 +363,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
                             : null;
                 }
 
-                EmitFieldRead(fieldInfo, il, col, i, local);
+                var idx = col.GetBinaryTupleIndex(keyOnly);
+                EmitFieldRead(fieldInfo, il, col, idx, local);
             }
 
             // Copy Key to KvPair.
@@ -475,7 +470,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             }
         }
 
-        private static void ValidateMappedCount(int mappedCount, Type type, 
Schema schema, int columnCount)
+        private static void ValidateMappedCount(int mappedCount, Type type, 
Schema schema, bool keyOnly)
         {
             if (mappedCount == 0)
             {
@@ -483,7 +478,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 throw new ArgumentException($"Can't map '{type}' to columns 
'{columnStr}'. Matching fields not found.");
             }
 
-            if (columnCount < schema.Columns.Count)
+            if (keyOnly)
             {
                 // Key-only mode - skip "all fields are mapped" validation.
                 // It will be performed anyway when using the whole schema.
@@ -501,9 +496,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
                     extraColumns.Add(field.Name);
                 }
 
-                for (var index = 0; index < columnCount; index++)
+                foreach (var column in schema.GetColumnsFor(keyOnly))
                 {
-                    var column = schema.Columns[index];
                     extraColumns.Remove(column.Name);
                 }
 
@@ -511,7 +505,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             }
         }
 
-        private static void ValidateKvMappedCount(int mappedCount, Type? 
keyType, Type? valType, Schema schema, int columnCount)
+        private static void ValidateKvMappedCount(int mappedCount, Type? 
keyType, Type? valType, Schema schema, bool keyOnly)
         {
             if (mappedCount == 0)
             {
@@ -520,7 +514,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             }
 
             var keyFields = keyType?.GetColumns() ?? 
Array.Empty<ReflectionUtils.ColumnInfo>();
-            var valFields = valType != null && columnCount > 
schema.KeyColumnCount
+            var valFields = valType != null && !keyOnly
                 ? valType.GetColumns()
                 : Array.Empty<ReflectionUtils.ColumnInfo>();
 
@@ -544,9 +538,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
                     extraColumns.Add(field.Name);
                 }
 
-                for (var index = 0; index < columnCount; index++)
+                foreach (var column in schema.GetColumnsFor(keyOnly))
                 {
-                    var column = schema.Columns[index];
                     extraColumns.Remove(column.Name);
                 }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TuplePairSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TuplePairSerializerHandler.cs
index 7aa0aa24f5..18474d6846 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TuplePairSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TuplePairSerializerHandler.cs
@@ -47,21 +47,32 @@ internal sealed class TuplePairSerializerHandler : 
IRecordSerializerHandler<KvPa
     /// <inheritdoc/>
     public KvPair<IIgniteTuple, IIgniteTuple> Read(ref MsgPackReader reader, 
Schema schema, bool keyOnly = false)
     {
-        var columns = schema.Columns;
-        var count = keyOnly ? schema.KeyColumnCount : columns.Count;
-        var keyTuple = new IgniteTuple(count);
-        var valTuple = keyOnly ? null! : new 
IgniteTuple(schema.ValueColumnCount);
-        var tupleReader = new BinaryTupleReader(reader.ReadBinary(), count);
-
-        for (var index = 0; index < count; index++)
+        if (keyOnly)
         {
-            var column = columns[index];
+            var keyTuple = new IgniteTuple(schema.KeyColumns.Length);
+            var tupleReader = new BinaryTupleReader(reader.ReadBinary(), 
schema.KeyColumns.Length);
 
-            var tuple = index < schema.KeyColumnCount ? keyTuple : valTuple;
-            tuple[column.Name] = tupleReader.GetObject(index, column.Type, 
column.Scale);
+            foreach (var column in schema.KeyColumns)
+            {
+                keyTuple[column.Name] = tupleReader.GetObject(column.KeyIndex, 
column.Type, column.Scale);
+            }
+
+            return new(keyTuple);
         }
+        else
+        {
+            var keyTuple = new IgniteTuple(schema.KeyColumns.Length);
+            var valTuple = new IgniteTuple(schema.ValColumns.Length);
+            var tupleReader = new BinaryTupleReader(reader.ReadBinary(), 
schema.Columns.Length);
 
-        return new(keyTuple, valTuple);
+            foreach (var column in schema.Columns)
+            {
+                var tuple = column.IsKey ? keyTuple : valTuple;
+                tuple[column.Name] = tupleReader.GetObject(column.SchemaIndex, 
column.Type, column.Scale);
+            }
+
+            return new(keyTuple, valTuple);
+        }
     }
 
     /// <inheritdoc/>
@@ -69,7 +80,7 @@ internal sealed class TuplePairSerializerHandler : 
IRecordSerializerHandler<KvPa
         ref BinaryTupleBuilder tupleBuilder,
         KvPair<IIgniteTuple, IIgniteTuple> record,
         Schema schema,
-        int columnCount,
+        bool keyOnly,
         Span<byte> noValueSet)
     {
         var key = record.Key;
@@ -77,17 +88,17 @@ internal sealed class TuplePairSerializerHandler : 
IRecordSerializerHandler<KvPa
 
         IgniteArgumentCheck.NotNull(key);
 
-        if (columnCount > schema.KeyColumnCount)
+        if (!keyOnly)
         {
             IgniteArgumentCheck.NotNull(val);
         }
 
         int written = 0;
+        var columns = schema.GetColumnsFor(keyOnly);
 
-        for (var index = 0; index < columnCount; index++)
+        foreach (var col in columns)
         {
-            var col = schema.Columns[index];
-            var rec = index < schema.KeyColumnCount ? key : val;
+            var rec = col.IsKey ? key : val;
             var colIdx = rec.GetOrdinal(col.Name);
 
             if (colIdx >= 0)
@@ -101,7 +112,7 @@ internal sealed class TuplePairSerializerHandler : 
IRecordSerializerHandler<KvPa
             }
         }
 
-        ValidateMappedCount(record, schema, columnCount, written);
+        ValidateMappedCount(record, schema, columns.Length, written);
     }
 
     private static void ValidateMappedCount(KvPair<IIgniteTuple, IIgniteTuple> 
record, Schema schema, int columnCount, int written)
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
index edeccb4c82..1e8dfdadeb 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
@@ -19,7 +19,6 @@ namespace Apache.Ignite.Internal.Table.Serialization
 {
     using System;
     using System.Collections.Generic;
-    using System.Diagnostics;
     using System.Linq;
     using Common;
     using Ignite.Table;
@@ -49,20 +48,17 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// </summary>
         /// <param name="buf">Buffer.</param>
         /// <param name="schema">Schema.</param>
-        /// <param name="count">Column count to read.</param>
+        /// <param name="keyOnly">Whether to read only the key columns.</param>
         /// <returns>Tuple.</returns>
-        public static IgniteTuple ReadTuple(ReadOnlySpan<byte> buf, Schema 
schema, int count)
+        public static IgniteTuple ReadTuple(ReadOnlySpan<byte> buf, Schema 
schema, bool keyOnly)
         {
-            Debug.Assert(count <= schema.Columns.Count, "count <= 
schema.Columns.Count");
+            var columns = schema.GetColumnsFor(keyOnly);
+            var tuple = new IgniteTuple(columns.Length);
+            var tupleReader = new BinaryTupleReader(buf, columns.Length);
 
-            var tuple = new IgniteTuple(count);
-            var tupleReader = new BinaryTupleReader(buf, count);
-            var columns = schema.Columns;
-
-            for (var index = 0; index < count; index++)
+            foreach (var column in columns)
             {
-                var column = columns[index];
-                tuple[column.Name] = tupleReader.GetObject(index, column.Type, 
column.Scale);
+                tuple[column.Name] = 
tupleReader.GetObject(column.GetBinaryTupleIndex(keyOnly), column.Type, 
column.Scale);
             }
 
             return tuple;
@@ -73,13 +69,14 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// </summary>
         /// <param name="buf">Binary tuple buffer.</param>
         /// <param name="schema">Schema.</param>
-        /// <param name="count">Column count.</param>
+        /// <param name="keyOnly">Whether <paramref name="buf"/> is a key-only 
binary tuple.</param>
         /// <param name="index">Column index.</param>
         /// <returns>Column value.</returns>
-        public static object? ReadObject(ReadOnlySpan<byte> buf, Schema 
schema, int count, int index)
+        public static object? ReadObject(ReadOnlySpan<byte> buf, Schema 
schema, bool keyOnly, int index)
         {
-            var tupleReader = new BinaryTupleReader(buf, count);
-            var column = schema.Columns[index];
+            var columns = schema.GetColumnsFor(keyOnly);
+            var tupleReader = new BinaryTupleReader(buf, columns.Length);
+            var column = columns[index];
 
             return tupleReader.GetObject(index, column.Type, column.Scale);
         }
@@ -89,16 +86,16 @@ namespace Apache.Ignite.Internal.Table.Serialization
             new BinaryTupleIgniteTupleAdapter(
                 data: reader.ReadBinary().ToArray(),
                 schema: schema,
-                fieldCount: keyOnly ? schema.KeyColumnCount : 
schema.Columns.Count);
+                keyOnly);
 
         /// <inheritdoc/>
-        public void Write(ref BinaryTupleBuilder tupleBuilder, IIgniteTuple 
record, Schema schema, int columnCount, Span<byte> noValueSet)
+        public void Write(ref BinaryTupleBuilder tupleBuilder, IIgniteTuple 
record, Schema schema, bool keyOnly, Span<byte> noValueSet)
         {
             int written = 0;
+            var columns = keyOnly ? schema.KeyColumns : schema.Columns;
 
-            for (var index = 0; index < columnCount; index++)
+            foreach (var col in columns)
             {
-                var col = schema.Columns[index];
                 var colIdx = record.GetOrdinal(col.Name);
 
                 if (colIdx >= 0)
@@ -112,7 +109,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 }
             }
 
-            ValidateMappedCount(record, schema, columnCount, written);
+            ValidateMappedCount(record, schema, columns.Length, written);
         }
 
         private static void ValidateMappedCount(IIgniteTuple record, Schema 
schema, int columnCount, int written)
@@ -130,12 +127,10 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 {
                     var name = record.GetName(i);
 
-                    if (extraColumns.Contains(name))
+                    if (!extraColumns.Add(name))
                     {
                         throw new ArgumentException("Duplicate column in 
Tuple: " + name, nameof(record));
                     }
-
-                    extraColumns.Add(name);
                 }
 
                 for (var i = 0; i < columnCount; i++)
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 4f39ad1ea3..ffb772e985 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -332,8 +332,6 @@ namespace Apache.Ignite.Internal.Table
         {
             var schemaVersion = r.ReadInt32();
             var columnCount = r.ReadInt32();
-            var keyColumnCount = 0;
-            var colocationColumnCount = 0;
 
             var columns = new Column[columnCount];
 
@@ -347,7 +345,6 @@ namespace Apache.Ignite.Internal.Table
                 var name = r.ReadString();
                 var type = r.ReadInt32();
                 var keyIndex = r.ReadInt32();
-                var isKey = keyIndex >= 0;
                 var isNullable = r.ReadBoolean();
                 var colocationIndex = r.ReadInt32();
                 var scale = r.ReadInt32();
@@ -355,27 +352,10 @@ namespace Apache.Ignite.Internal.Table
 
                 r.Skip(propertyCount - expectedCount);
 
-                var column = new Column(name, (ColumnType)type, isNullable, 
isKey, colocationIndex, i, scale, precision);
-
-                columns[i] = column;
-
-                if (isKey)
-                {
-                    keyColumnCount++;
-                }
-
-                if (colocationIndex >= 0)
-                {
-                    colocationColumnCount++;
-                }
+                columns[i] = new Column(name, (ColumnType)type, isNullable, 
keyIndex, colocationIndex, i, scale, precision);
             }
 
-            var schema = new Schema(
-                Version: schemaVersion,
-                TableId: Id,
-                KeyColumnCount: keyColumnCount,
-                ColocationColumnCount: colocationColumnCount,
-                Columns: columns);
+            var schema = Schema.CreateInstance(schemaVersion, Id, columns);
 
             _schemas[schemaVersion] = Task.FromResult(schema);
 

Reply via email to