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 7e1e0cf74c IGNITE-19914 .NET: Fix colocation column order (#2308)
7e1e0cf74c is described below

commit 7e1e0cf74c741a8d9c7c9e678535a4ca04fb8dc9
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Wed Jul 12 13:46:16 2023 +0300

    IGNITE-19914 .NET: Fix colocation column order (#2308)
    
    Colocation key column order can be different from the schema column order, 
for example:
    ```sql
    create table test(id integer, id0 bigint, id1 varchar, primary key(id, 
id0)) colocate by (id1, id0)
    ```
    
    Instead of computing the hash as we go in `BinaryTupleBuilder`, store 
individual hashes in the right order into a preallocated area in the buffer and 
combine them later.
---
 .../SerializerHandlerBenchmarksBase.cs             |  17 ++-
 .../Proto/ColocationHashTests.cs                   |  61 ++++++++--
 .../Serialization/ObjectSerializerHandlerTests.cs  |  15 ++-
 .../Proto/BinaryTuple/BinaryTupleBuilder.cs        | 125 +++++++++++++--------
 .../BinaryTuple/IHashedColumnIndexProvider.cs      |  11 +-
 .../Apache.Ignite/Internal/Proto/HashUtils.cs      |   2 +-
 .../Apache.Ignite/Internal/Table/DataStreamer.cs   |   2 +-
 .../dotnet/Apache.Ignite/Internal/Table/Schema.cs  |   9 +-
 .../Serialization/IRecordSerializerHandler.cs      |   2 +-
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |  15 ++-
 10 files changed, 184 insertions(+), 75 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 02bc0f63f1..bc94378f00 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -44,12 +44,17 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             [nameof(Car.Seats)] = Object.Seats
         };
 
-        internal static readonly Schema Schema = new(1, 1, 1, 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)
-        });
+        internal static readonly Schema Schema = new(
+            Version: 1,
+            TableId: 1,
+            KeyColumnCount: 1,
+            ColocationColumnCount: 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)
+            });
 
         internal static readonly byte[] SerializedData = GetSerializedData();
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
index c63da30322..d0187bd90f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
@@ -29,6 +29,7 @@ using Ignite.Compute;
 using Ignite.Sql;
 using Ignite.Table;
 using Internal.Buffers;
+using Internal.Proto;
 using Internal.Proto.BinaryTuple;
 using Internal.Proto.MsgPack;
 using Internal.Table;
@@ -41,7 +42,11 @@ using NUnit.Framework;
 /// </summary>
 public class ColocationHashTests : IgniteTestsBase
 {
-    private const string ColocationHashJob = 
"org.apache.ignite.internal.runner.app.PlatformTestNodeRunner$ColocationHashJob";
+    private const string PlatformTestNodeRunner = 
"org.apache.ignite.internal.runner.app.PlatformTestNodeRunner";
+
+    private const string ColocationHashJob = PlatformTestNodeRunner + 
"$ColocationHashJob";
+
+    private const string TableRowColocationHashJob = PlatformTestNodeRunner + 
"$TableRowColocationHashJob";
 
     private static readonly object[] TestCases =
     {
@@ -161,16 +166,56 @@ public class ColocationHashTests : IgniteTestsBase
         }
     }
 
+    [Test]
+    public async Task TestCustomColocationColumnOrder([Values(true, false)] 
bool reverseColocationOrder)
+    {
+        var tableName = 
$"{nameof(TestCustomColocationColumnOrder)}_{reverseColocationOrder}";
+        var sql = $"create table if not exists {tableName} " +
+                  $"(id integer, id0 bigint, id1 varchar, v INTEGER, primary 
key(id, id0, id1)) " +
+                  $"colocate by {(reverseColocationOrder ? "(id1, id0)" : 
"(id0, id1)")}";
+
+        await Client.Sql.ExecuteAsync(null, sql);
+
+        // Perform get to populate schema.
+        var table = await Client.Tables.GetTableAsync(tableName);
+        var view = table!.RecordBinaryView;
+        await view.GetAsync(null, new IgniteTuple{["id"] = 1, ["id0"] = 2L, 
["id1"] = "3", ["v"] = 4});
+
+        var ser = view.GetFieldValue<RecordSerializer<IIgniteTuple>>("_ser");
+        var schemas = table.GetFieldValue<IDictionary<int, 
Task<Schema>>>("_schemas");
+        var schema = schemas[1].GetAwaiter().GetResult();
+        var clusterNodes = await Client.GetClusterNodesAsync();
+
+        for (int i = 0; i < 100; i++)
+        {
+            var key = new IgniteTuple { ["id"] = 1 + i, ["id0"] = 2L + i, 
["id1"] = "3" + i, ["v"] = 4 + i };
+
+            using var writer = ProtoCommon.GetMessageWriter();
+            var clientColocationHash = ser.Write(writer, null, schema, key);
+
+            var serverColocationHash = await Client.Compute.ExecuteAsync<int>(
+                clusterNodes,
+                Array.Empty<DeploymentUnit>(),
+                TableRowColocationHashJob,
+                tableName,
+                i);
+
+            Assert.AreEqual(serverColocationHash, clientColocationHash, 
key.ToString());
+        }
+    }
+
     private static (byte[] Bytes, int Hash) 
WriteAsBinaryTuple(IReadOnlyCollection<object> arr, int timePrecision, int 
timestampPrecision)
     {
-        using var builder = new BinaryTupleBuilder(arr.Count * 3, 
hashedColumnsPredicate: new TestIndexProvider(x => x % 3 == 2));
+        using var builder = new BinaryTupleBuilder(
+            numElements: arr.Count * 3,
+            hashedColumnsPredicate: new TestIndexProvider(x => x % 3 == 2 ? x 
/ 3 : -1, arr.Count));
 
         foreach (var obj in arr)
         {
             builder.AppendObjectWithType(obj, timePrecision, 
timestampPrecision);
         }
 
-        return (builder.Build().ToArray(), builder.Hash);
+        return (builder.Build().ToArray(), Hash: builder.GetHash());
     }
 
     private static int WriteAsIgniteTuple(IReadOnlyCollection<object> arr, int 
timePrecision, int timestampPrecision)
@@ -183,7 +228,7 @@ public class ColocationHashTests : IgniteTestsBase
             igniteTuple["m_Item" + i++] = obj;
         }
 
-        var builder = new BinaryTupleBuilder(arr.Count, 
hashedColumnsPredicate: new TestIndexProvider(_ => true));
+        var builder = new BinaryTupleBuilder(arr.Count, 
hashedColumnsPredicate: new TestIndexProvider(idx => idx, arr.Count));
 
         try
         {
@@ -191,7 +236,7 @@ public class ColocationHashTests : IgniteTestsBase
             var noValueSet = new byte[arr.Count].AsSpan();
 
             TupleSerializerHandler.Instance.Write(ref builder, igniteTuple, 
schema, arr.Count, noValueSet);
-            return builder.Hash;
+            return builder.GetHash();
         }
         finally
         {
@@ -216,7 +261,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, columns);
+        return new Schema(Version: 0, 0, arr.Count, arr.Count, columns);
     }
 
     private static Column GetColumn(object value, int schemaIndex, int 
timePrecision, int timestampPrecision)
@@ -291,8 +336,8 @@ public class ColocationHashTests : IgniteTestsBase
             timestampPrecision);
     }
 
-    private record TestIndexProvider(Func<int, bool> Delegate) : 
IHashedColumnIndexProvider
+    private record TestIndexProvider(Func<int, int> ColumnOrderDelegate, int 
HashedColumnCount) : IHashedColumnIndexProvider
     {
-        public bool IsHashedColumnIndex(int index) => Delegate(index);
+        public int HashedColumnOrder(int index) => ColumnOrderDelegate(index);
     }
 }
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 8d3eb6d22c..58a08e61aa 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
@@ -32,11 +32,16 @@ namespace Apache.Ignite.Tests.Table.Serialization
     // ReSharper disable NotAccessedPositionalProperty.Local
     public class ObjectSerializerHandlerTests
     {
-        private static readonly Schema Schema = new(1, 1, 1, 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)
-        });
+        private static readonly Schema Schema = new(
+            Version: 1,
+            TableId: 1,
+            KeyColumnCount: 1,
+            ColocationColumnCount: 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)
+            });
 
         [Test]
         public void TestWrite()
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
index 0b88821997..7540b6c060 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
@@ -22,6 +22,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
     using System.Collections;
     using System.Diagnostics;
     using System.Numerics;
+    using System.Runtime.InteropServices;
     using Buffers;
     using Ignite.Sql;
     using NodaTime;
@@ -53,9 +54,6 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /** Current element. */
         private int _elementIndex;
 
-        /** Current element. */
-        private int _hash;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="BinaryTupleBuilder"/> 
struct.
         /// </summary>
@@ -72,11 +70,13 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
 
             _numElements = numElements;
             _hashedColumnsPredicate = hashedColumnsPredicate;
-            _hash = 0;
             _buffer = new();
             _elementIndex = 0;
 
-            _entryBase = BinaryTupleCommon.HeaderSize;
+            // Reserve buffer for individual hash codes.
+            _entryBase = _hashedColumnsPredicate != null
+                ? BinaryTupleCommon.HeaderSize + 
_hashedColumnsPredicate.HashedColumnCount * 4
+                : BinaryTupleCommon.HeaderSize;
 
             _entrySize = totalValueSize < 0
                 ? 4
@@ -96,16 +96,34 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <summary>
         /// Gets the hash from column values according to specified <see 
cref="IHashedColumnIndexProvider"/>.
         /// </summary>
-        public int Hash => _hash;
+        /// <returns>Column hash according to specified <see 
cref="IHashedColumnIndexProvider"/>.</returns>
+        public int GetHash()
+        {
+            if (_hashedColumnsPredicate == null)
+            {
+                return 0;
+            }
+
+            var hash = 0;
+            var hashes = GetHashSpan();
+
+            for (var i = 0; i < _hashedColumnsPredicate.HashedColumnCount; i++)
+            {
+                var colHash = hashes[i];
+                hash = HashUtils.Combine(hash, colHash);
+            }
+
+            return hash;
+        }
 
         /// <summary>
         /// Appends a null value.
         /// </summary>
         public void AppendNull()
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32((sbyte)0));
+                PutHash(hashOrder, HashUtils.Hash32((sbyte)0));
             }
 
             OnWrite();
@@ -117,9 +135,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendByte(sbyte value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             PutByte(value);
@@ -148,9 +166,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendShort(short value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             if (value >= sbyte.MinValue && value <= sbyte.MaxValue)
@@ -187,9 +205,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendInt(int value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             if (value >= sbyte.MinValue && value <= sbyte.MaxValue)
@@ -230,9 +248,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendLong(long value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             if (value >= sbyte.MinValue && value <= sbyte.MaxValue)
@@ -277,9 +295,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendFloat(float value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             PutFloat(value);
@@ -308,9 +326,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendDouble(double value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             // ReSharper disable once CompareOfFloatsByEqualityOperator
@@ -375,9 +393,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendBytes(Span<byte> value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             PutBytes(value);
@@ -415,13 +433,13 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             var span = GetSpan(16);
             UuidSerializer.Write(value, span);
 
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
                 var lo = BinaryPrimitives.ReadInt64LittleEndian(span[..8]);
                 var hi = BinaryPrimitives.ReadInt64LittleEndian(span[8..]);
                 var hash = HashUtils.Hash32(hi, HashUtils.Hash32(lo));
 
-                _hash = HashUtils.Combine(_hash, hash);
+                PutHash(hashOrder, hash);
             }
 
             OnWrite();
@@ -464,9 +482,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
 
                 var resBytes = arr.AsSpan()[..size];
 
-                if (ShouldHash())
+                if (GetHashOrder() is { } hashOrder)
                 {
-                    _hash = HashUtils.Combine(_hash, 
HashUtils.Hash32(resBytes));
+                    PutHash(hashOrder, HashUtils.Hash32(resBytes));
                 }
 
                 PutBytes(resBytes);
@@ -530,9 +548,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             var destination = GetSpan(size);
             var success = value.TryWriteBytes(destination, out int written, 
isBigEndian: true);
 
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, 
HashUtils.Hash32(destination[..written]));
+                PutHash(hashOrder, HashUtils.Hash32(destination[..written]));
             }
 
             Debug.Assert(success, "success");
@@ -563,9 +581,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendDate(LocalDate value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value));
+                PutHash(hashOrder, HashUtils.Hash32(value));
             }
 
             PutDate(value);
@@ -595,9 +613,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="precision">Precision.</param>
         public void AppendTime(LocalTime value, int precision)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value, 
precision));
+                PutHash(hashOrder, HashUtils.Hash32(value, precision));
             }
 
             PutTime(value, precision);
@@ -628,9 +646,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="precision">Precision.</param>
         public void AppendDateTime(LocalDateTime value, int precision)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
-                _hash = HashUtils.Combine(_hash, HashUtils.Hash32(value, 
precision));
+                PutHash(hashOrder, HashUtils.Hash32(value, precision));
             }
 
             PutDate(value.Date);
@@ -664,10 +682,10 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         {
             var (seconds, nanos) = PutTimestamp(value, precision);
 
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder)
             {
                 var hash = HashUtils.Hash32(nanos, HashUtils.Hash32(seconds));
-                _hash = HashUtils.Combine(_hash, hash);
+                PutHash(hashOrder, hash);
             }
 
             OnWrite();
@@ -696,7 +714,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendDuration(Duration value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is not null)
             {
                 // Colocation keys can't include Duration.
                 throw new NotSupportedException("Duration hashing is not 
supported.");
@@ -728,7 +746,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="value">Value.</param>
         public void AppendPeriod(Period value)
         {
-            if (ShouldHash())
+            if (GetHashOrder() is not null)
             {
                 // Colocation keys can't include Period.
                 throw new NotSupportedException("Period hashing is not 
supported.");
@@ -953,7 +971,8 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <returns>Resulting memory.</returns>
         public Memory<byte> Build()
         {
-            int offset = 0;
+            int baseOffset = _entryBase - BinaryTupleCommon.HeaderSize;
+            int offset = baseOffset;
 
             int valueSize = _buffer.Position - _valueBase;
             byte flags = BinaryTupleCommon.ValueSizeToFlags(valueSize);
@@ -991,7 +1010,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                     }
                 }
 
-                offset = (_entrySize - desiredEntrySize) * _numElements;
+                offset = baseOffset + (_entrySize - desiredEntrySize) * 
_numElements;
             }
 
             _buffer.WriteByte(flags, offset);
@@ -1049,9 +1068,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         {
             if (value.Length == 0)
             {
-                if (ShouldHash())
+                if (GetHashOrder() is { } hashOrder)
                 {
-                    _hash = HashUtils.Combine(_hash, 
HashUtils.Hash32(Span<byte>.Empty));
+                    PutHash(hashOrder, HashUtils.Hash32(Span<byte>.Empty));
                 }
 
                 _buffer.GetSpan(1)[0] = BinaryTupleCommon.VarlenEmptyByte;
@@ -1063,10 +1082,11 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             var span = _buffer.GetSpan(maxByteCount);
 
             var actualBytes = ProtoCommon.StringEncoding.GetBytes(value, span);
+            span = span[..actualBytes];
 
-            if (ShouldHash())
+            if (GetHashOrder() is { } hashOrder2)
             {
-                _hash = HashUtils.Combine(_hash, 
HashUtils.Hash32(span[..actualBytes]));
+                PutHash(hashOrder2, HashUtils.Hash32(span));
             }
 
             _buffer.Advance(actualBytes);
@@ -1236,6 +1256,21 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             return span;
         }
 
-        private bool ShouldHash() => 
_hashedColumnsPredicate?.IsHashedColumnIndex(_elementIndex) == true;
+        private int? GetHashOrder() => 
_hashedColumnsPredicate?.HashedColumnOrder(_elementIndex) switch
+        {
+            null or < 0 => null,
+            { } order => order
+        };
+
+        private void PutHash(int index, int hash)
+        {
+            Debug.Assert(_hashedColumnsPredicate != null, 
"_hashedColumnsPredicate != null");
+            Debug.Assert(index >= 0, "index >= 0");
+            Debug.Assert(index < _hashedColumnsPredicate.HashedColumnCount, 
"index < _hashedColumnsPredicate.HashedColumnCount");
+
+            GetHashSpan()[index] = hash;
+        }
+
+        private Span<int> GetHashSpan() => MemoryMarshal.Cast<byte, 
int>(_buffer.GetWrittenMemory().Span);
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/IHashedColumnIndexProvider.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/IHashedColumnIndexProvider.cs
index 2aef8d986b..6f76e6fc61 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/IHashedColumnIndexProvider.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/IHashedColumnIndexProvider.cs
@@ -23,9 +23,14 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple;
 internal interface IHashedColumnIndexProvider
 {
     /// <summary>
-    /// Gets a value indicating whether the value of a column at specified 
index should be hashed.
+    /// Gets the number of hashed columns.
+    /// </summary>
+    int HashedColumnCount { get; }
+
+    /// <summary>
+    /// Gets a value indicating the hash order for the column at specified 
index.
     /// </summary>
     /// <param name="index">Column index.</param>
-    /// <returns>True when hashed column; false otherwise.</returns>
-    bool IsHashedColumnIndex(int index);
+    /// <returns>The order of the column within the hash, when applicable; -1 
otherwise.</returns>
+    int HashedColumnOrder(int index);
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/HashUtils.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/HashUtils.cs
index ebb4d1c2e9..cebc6b9fd6 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/HashUtils.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/HashUtils.cs
@@ -103,7 +103,7 @@ internal static class HashUtils
     /// </summary>
     /// <param name="data">Input data.</param>
     /// <returns>Resulting hash.</returns>
-    public static int Hash32(Span<byte> data) => Hash32Internal(data, 0);
+    public static int Hash32(Span<byte> data) => data.IsEmpty ? 0 : 
Hash32Internal(data, 0);
 
     /// <summary>
     /// Generates 32-bit hash.
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
index 13b43ddb36..63a702f992 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/DataStreamer.cs
@@ -157,7 +157,7 @@ internal static class DataStreamer
 
             var partition = partitionAssignment == null
                 ? string.Empty // Default connection.
-                : partitionAssignment[Math.Abs(tupleBuilder.Hash % 
partitionAssignment.Length)];
+                : partitionAssignment[Math.Abs(tupleBuilder.GetHash() % 
partitionAssignment.Length)];
 
             var batch = GetOrCreateBatch(partition);
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
index c4bf0fac06..8fa9c84238 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
@@ -26,11 +26,13 @@ namespace Apache.Ignite.Internal.Table
     /// <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>
     internal sealed record Schema(
         int Version,
         int TableId,
         int KeyColumnCount,
+        int ColocationColumnCount,
         IReadOnlyList<Column> Columns) : IHashedColumnIndexProvider
     {
         /// <summary>
@@ -39,6 +41,11 @@ namespace Apache.Ignite.Internal.Table
         public int ValueColumnCount => Columns.Count - KeyColumnCount;
 
         /// <inheritdoc/>
-        public bool IsHashedColumnIndex(int index) => index < KeyColumnCount 
&& Columns[index].IsColocation;
+        public int HashedColumnCount => ColocationColumnCount;
+
+        /// <inheritdoc/>
+        public int HashedColumnOrder(int index) => index < KeyColumnCount
+            ? Columns[index].ColocationIndex
+            : -1;
     }
 }
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 785f39899b..4b8219bb59 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
@@ -60,7 +60,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 var binaryTupleMemory = tupleBuilder.Build();
                 writer.Write(binaryTupleMemory.Span);
 
-                return tupleBuilder.Hash;
+                return tupleBuilder.GetHash();
             }
             finally
             {
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 1d43e02588..859872dcb0 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -323,6 +323,7 @@ namespace Apache.Ignite.Internal.Table
             var schemaVersion = r.ReadInt32();
             var columnCount = r.ReadArrayHeader();
             var keyColumnCount = 0;
+            var colocationColumnCount = 0;
 
             var columns = new Column[columnCount];
 
@@ -351,13 +352,19 @@ namespace Apache.Ignite.Internal.Table
                 {
                     keyColumnCount++;
                 }
+
+                if (colocationIndex >= 0)
+                {
+                    colocationColumnCount++;
+                }
             }
 
             var schema = new Schema(
-                schemaVersion,
-                Id,
-                keyColumnCount,
-                columns);
+                Version: schemaVersion,
+                TableId: Id,
+                KeyColumnCount: keyColumnCount,
+                ColocationColumnCount: colocationColumnCount,
+                Columns: columns);
 
             _schemas[schemaVersion] = Task.FromResult(schema);
 

Reply via email to