You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2023/07/06 04:12:58 UTC

[ignite-3] branch main updated: IGNITE-19756 Java client: fix colocation column order (#2289)

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 61d3b03382 IGNITE-19756 Java client: fix colocation column order (#2289)
61d3b03382 is described below

commit 61d3b03382f34a5efd568fa6a2cb0f94c2722463
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Thu Jul 6 07:12:52 2023 +0300

    IGNITE-19756 Java client: fix colocation column order (#2289)
    
    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)
    ```
    
    * Pass `colocationIndex` instead of `isColocation` to the client
    * Fix Java client to respect colocation order when computing partition awareness hash
---
 .../handler/requests/table/ClientTableCommon.java  |  7 +---
 .../internal/client/sql/ClientAsyncResultSet.java  |  4 +-
 .../ignite/internal/client/table/ClientColumn.java | 43 ++++++++++----------
 .../ignite/internal/client/table/ClientSchema.java | 25 ++++++------
 .../ignite/internal/client/table/ClientTable.java  | 22 +++++++++--
 .../org/apache/ignite/client/ClientTupleTest.java  | 46 +++++++++++-----------
 .../ignite/client/PartitionAwarenessTest.java      | 12 +++---
 .../apache/ignite/internal/util/ArrayUtils.java    |  2 +-
 .../SerializerHandlerBenchmarksBase.cs             |  6 +--
 .../dotnet/Apache.Ignite.Tests/FakeServer.cs       | 10 ++---
 .../Proto/ColocationHashTests.cs                   |  2 +-
 .../Serialization/ObjectSerializerHandlerTests.cs  |  4 +-
 .../dotnet/Apache.Ignite/Internal/Table/Column.cs  | 10 ++++-
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   | 11 ++++--
 .../runner/app/PlatformTestNodeRunner.java         | 27 +++++++++++++
 .../internal/table/ItThinClientColocationTest.java | 42 ++++++++++++++++++--
 .../ignite/internal/schema/SchemaDescriptor.java   | 33 ++++++++++++++--
 17 files changed, 210 insertions(+), 96 deletions(-)

diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
index d417a5a805..b21764f65d 100644
--- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
@@ -22,7 +22,6 @@ import static org.apache.ignite.lang.ErrorGroups.Client.TABLE_ID_NOT_FOUND_ERR;
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Set;
 import org.apache.ignite.client.handler.ClientResourceRegistry;
 import org.apache.ignite.internal.binarytuple.BinaryTupleContainer;
 import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
@@ -60,7 +59,7 @@ public class ClientTableCommon {
      * @param schemaVer Schema version.
      * @param schema    Schema.
      */
-    public static void writeSchema(ClientMessagePacker packer, int schemaVer, SchemaDescriptor schema) {
+    static void writeSchema(ClientMessagePacker packer, int schemaVer, SchemaDescriptor schema) {
         packer.packInt(schemaVer);
 
         if (schema == null) {
@@ -72,8 +71,6 @@ public class ClientTableCommon {
         var colCnt = schema.columnNames().size();
         packer.packArrayHeader(colCnt);
 
-        var colocationCols = Set.of(schema.colocationColumns());
-
         for (var colIdx = 0; colIdx < colCnt; colIdx++) {
             var col = schema.column(colIdx);
 
@@ -82,7 +79,7 @@ public class ClientTableCommon {
             packer.packInt(getColumnType(col.type().spec()).ordinal());
             packer.packBoolean(schema.isKeyColumn(colIdx));
             packer.packBoolean(col.nullable());
-            packer.packBoolean(colocationCols.contains(col));
+            packer.packInt(schema.colocationIndex(col));
             packer.packInt(getDecimalScale(col.type()));
             packer.packInt(getPrecision(col.type()));
         }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
index de22ff0d8f..f002cd5847 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientAsyncResultSet.java
@@ -318,7 +318,7 @@ class ClientAsyncResultSet<T> implements AsyncResultSet<T> {
                     metaColumn.type(),
                     metaColumn.nullable(),
                     true,
-                    false,
+                    -1,
                     i,
                     metaColumn.scale(),
                     metaColumn.precision());
@@ -326,7 +326,7 @@ class ClientAsyncResultSet<T> implements AsyncResultSet<T> {
             schemaColumns[i] = schemaColumn;
         }
 
-        var schema = new ClientSchema(0, schemaColumns);
+        var schema = new ClientSchema(0, schemaColumns, null);
         return schema.getMarshaller(mapper, TuplePart.KEY_AND_VAL);
     }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientColumn.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientColumn.java
index 699d66a4ad..8484bf69c8 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientColumn.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientColumn.java
@@ -35,8 +35,8 @@ public class ClientColumn {
     /** Key column flag. */
     private final boolean isKey;
 
-    /** Key column flag. */
-    private final boolean isColocation;
+    /** Colocation index. */
+    private final int colocationIndex;
 
     /** Index of the column in the schema. */
     private final int schemaIndex;
@@ -50,33 +50,34 @@ public class ClientColumn {
     /**
      * Constructor.
      *
-     * @param name         Column name.
-     * @param type         Column type.
-     * @param nullable     Nullable flag.
-     * @param isKey        Key column flag.
-     * @param isColocation Colocation column flag.
-     * @param schemaIndex  Index of the column in the schema.
+     * @param name Column name.
+     * @param type Column type.
+     * @param nullable Nullable flag.
+     * @param isKey Key column flag.
+     * @param colocationIndex Colocation index.
+     * @param schemaIndex Index of the column in the schema.
      */
-    public ClientColumn(String name, ColumnType type, boolean nullable, boolean isKey, boolean isColocation, int schemaIndex) {
-        this(name, type, nullable, isKey, isColocation, schemaIndex, 0, 0);
+    public ClientColumn(String name, ColumnType type, boolean nullable, boolean isKey, int colocationIndex, int schemaIndex) {
+        this(name, type, nullable, isKey, colocationIndex, schemaIndex, 0, 0);
     }
 
     /**
      * Constructor.
      *
-     * @param name        Column name.
-     * @param type        Column type code.
-     * @param nullable    Nullable flag.
-     * @param isKey       Key column flag.
+     * @param name Column name.
+     * @param type Column type code.
+     * @param nullable Nullable flag.
+     * @param isKey Key column flag.
+     * @param colocationIndex Colocation index.
      * @param schemaIndex Index of the column in the schema.
-     * @param scale       Scale of the column, if applicable.
+     * @param scale Scale of the column, if applicable.
      */
     public ClientColumn(
             String name,
             ColumnType type,
             boolean nullable,
             boolean isKey,
-            boolean isColocation,
+            int colocationIndex,
             int schemaIndex,
             int scale,
             int precision) {
@@ -87,7 +88,7 @@ public class ClientColumn {
         this.type = type;
         this.nullable = nullable;
         this.isKey = isKey;
-        this.isColocation = isColocation;
+        this.colocationIndex = colocationIndex;
         this.schemaIndex = schemaIndex;
         this.scale = scale;
         this.precision = precision;
@@ -125,12 +126,12 @@ public class ClientColumn {
     }
 
     /**
-     * Gets a value indicating whether this column is a part of colocation key.
+     * Gets the colocation index, or -1 when not part of the colocation key.
      *
-     * @return Value indicating whether this column is a part of colocation key.
+     * @return Index within colocation key, or -1 when not part of the colocation key.
      */
-    public boolean colocation() {
-        return isColocation;
+    public int colocationIndex() {
+        return colocationIndex;
     }
 
     /**
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
index 580cdd6ea2..79c373d460 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
@@ -17,9 +17,7 @@
 
 package org.apache.ignite.internal.client.table;
 
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import org.apache.ignite.internal.client.proto.TuplePart;
 import org.apache.ignite.internal.marshaller.BinaryMode;
@@ -48,7 +46,7 @@ public class ClientSchema {
     private final ClientColumn[] columns;
 
     /** Colocation columns. */
-    private final List<ClientColumn> colocationColumns;
+    private final ClientColumn[] colocationColumns;
 
     /** Columns map by name. */
     private final Map<String, ClientColumn> map = new HashMap<>();
@@ -56,17 +54,16 @@ public class ClientSchema {
     /**
      * Constructor.
      *
-     * @param ver     Schema version.
+     * @param ver Schema version.
      * @param columns Columns.
+     * @param colocationColumns Colocation columns. When null, all key columns are used.
      */
-    public ClientSchema(int ver, ClientColumn[] columns) {
+    public ClientSchema(int ver, ClientColumn[] columns, ClientColumn @Nullable [] colocationColumns) {
         assert ver >= 0;
         assert columns != null;
 
         this.ver = ver;
         this.columns = columns;
-        this.colocationColumns = new ArrayList<>();
-
         var keyCnt = 0;
 
         for (var col : columns) {
@@ -75,13 +72,17 @@ public class ClientSchema {
             }
 
             map.put(col.name(), col);
-
-            if (col.colocation()) {
-                colocationColumns.add(col);
-            }
         }
 
         keyColumnCount = keyCnt;
+
+        if (colocationColumns == null) {
+            this.colocationColumns = new ClientColumn[keyCnt];
+
+            System.arraycopy(columns, 0, this.colocationColumns, 0, keyCnt);
+        } else {
+            this.colocationColumns = colocationColumns;
+        }
     }
 
     /**
@@ -107,7 +108,7 @@ public class ClientSchema {
      *
      * @return Colocation columns.
      */
-    public List<ClientColumn> colocationColumns() {
+    ClientColumn[] colocationColumns() {
         return colocationColumns;
     }
 
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
index f8b30195c7..e71e6cf786 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
@@ -200,8 +200,8 @@ public class ClientTable implements Table {
     private ClientSchema readSchema(ClientMessageUnpacker in) {
         var schemaVer = in.unpackInt();
         var colCnt = in.unpackArrayHeader();
-
         var columns = new ClientColumn[colCnt];
+        int colocationColumnCnt = 0;
 
         for (int i = 0; i < colCnt; i++) {
             var propCnt = in.unpackArrayHeader();
@@ -212,18 +212,32 @@ public class ClientTable implements Table {
             var type = ColumnTypeConverter.fromOrdinalOrThrow(in.unpackInt());
             var isKey = in.unpackBoolean();
             var isNullable = in.unpackBoolean();
-            var isColocation = in.unpackBoolean();
+            var colocationIndex = in.unpackInt();
             var scale = in.unpackInt();
             var precision = in.unpackInt();
 
             // Skip unknown extra properties, if any.
             in.skipValues(propCnt - 7);
 
-            var column = new ClientColumn(name, type, isNullable, isKey, isColocation, i, scale, precision);
+            var column = new ClientColumn(name, type, isNullable, isKey, colocationIndex, i, scale, precision);
             columns[i] = column;
+
+            if (colocationIndex >= 0) {
+                colocationColumnCnt++;
+            }
+        }
+
+        var colocationColumns = colocationColumnCnt > 0 ? new ClientColumn[colocationColumnCnt] : null;
+        if (colocationColumns != null) {
+            for (ClientColumn col : columns) {
+                int idx = col.colocationIndex();
+                if (idx >= 0) {
+                    colocationColumns[idx] = col;
+                }
+            }
         }
 
-        var schema = new ClientSchema(schemaVer, columns);
+        var schema = new ClientSchema(schemaVer, columns, colocationColumns);
 
         schemas.put(schemaVer, CompletableFuture.completedFuture(schema));
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
index ef39887a01..724bcfc39d 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientTupleTest.java
@@ -58,31 +58,31 @@ import org.junit.jupiter.api.Test;
  */
 public class ClientTupleTest {
     private static final ClientSchema SCHEMA = new ClientSchema(1, new ClientColumn[]{
-            new ClientColumn("ID", ColumnType.INT64, false, true, true, 0),
-            new ClientColumn("NAME", ColumnType.STRING, false, false, false, 1)
-    });
+            new ClientColumn("ID", ColumnType.INT64, false, true, 0, 0),
+            new ClientColumn("NAME", ColumnType.STRING, false, false, -1, 1)
+    }, null);
 
     private static final ClientSchema FULL_SCHEMA = new ClientSchema(100, new ClientColumn[]{
-            new ClientColumn("I8", ColumnType.INT8, false, false, false, 0),
-            new ClientColumn("I16", ColumnType.INT16, false, false, false, 1),
-            new ClientColumn("I32", ColumnType.INT32, false, false, false, 2),
-            new ClientColumn("I64", ColumnType.INT64, false, false, false, 3),
-            new ClientColumn("FLOAT", ColumnType.FLOAT, false, false, false, 4),
-            new ClientColumn("DOUBLE", ColumnType.DOUBLE, false, false, false, 5),
-            new ClientColumn("UUID", ColumnType.UUID, false, false, false, 6),
-            new ClientColumn("STR", ColumnType.STRING, false, false, false, 7),
-            new ClientColumn("BITS", ColumnType.BITMASK, false, false, false, 8),
-            new ClientColumn("DATE", ColumnType.DATE, false, false, false, 9),
-            new ClientColumn("TIME", ColumnType.TIME, false, false, false, 10),
-            new ClientColumn("DATETIME", ColumnType.DATETIME, false, false, false, 11),
-            new ClientColumn("TIMESTAMP", ColumnType.TIMESTAMP, false, false, false, 12),
-            new ClientColumn("BOOL", ColumnType.BOOLEAN, false, false, false, 13),
-            new ClientColumn("DECIMAL", ColumnType.DECIMAL, false, false, false, 14),
-            new ClientColumn("BYTES", ColumnType.BYTE_ARRAY, false, false, false, 15),
-            new ClientColumn("PERIOD", ColumnType.PERIOD, false, false, false, 16),
-            new ClientColumn("DURATION", ColumnType.DURATION, false, false, false, 17),
-            new ClientColumn("NUMBER", ColumnType.NUMBER, false, false, false, 18)
-    });
+            new ClientColumn("I8", ColumnType.INT8, false, false, -1, 0),
+            new ClientColumn("I16", ColumnType.INT16, false, false, -1, 1),
+            new ClientColumn("I32", ColumnType.INT32, false, false, -1, 2),
+            new ClientColumn("I64", ColumnType.INT64, false, false, -1, 3),
+            new ClientColumn("FLOAT", ColumnType.FLOAT, false, false, -1, 4),
+            new ClientColumn("DOUBLE", ColumnType.DOUBLE, false, false, -1, 5),
+            new ClientColumn("UUID", ColumnType.UUID, false, false, -1, 6),
+            new ClientColumn("STR", ColumnType.STRING, false, false, -1, 7),
+            new ClientColumn("BITS", ColumnType.BITMASK, false, false, -1, 8),
+            new ClientColumn("DATE", ColumnType.DATE, false, false, -1, 9),
+            new ClientColumn("TIME", ColumnType.TIME, false, false, -1, 10),
+            new ClientColumn("DATETIME", ColumnType.DATETIME, false, false, -1, 11),
+            new ClientColumn("TIMESTAMP", ColumnType.TIMESTAMP, false, false, -1, 12),
+            new ClientColumn("BOOL", ColumnType.BOOLEAN, false, false, -1, 13),
+            new ClientColumn("DECIMAL", ColumnType.DECIMAL, false, false, -1, 14),
+            new ClientColumn("BYTES", ColumnType.BYTE_ARRAY, false, false, -1, 15),
+            new ClientColumn("PERIOD", ColumnType.PERIOD, false, false, -1, 16),
+            new ClientColumn("DURATION", ColumnType.DURATION, false, false, -1, 17),
+            new ClientColumn("NUMBER", ColumnType.NUMBER, false, false, -1, 18)
+    }, null);
 
     private static final UUID GUID = UUID.randomUUID();
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/PartitionAwarenessTest.java b/modules/client/src/test/java/org/apache/ignite/client/PartitionAwarenessTest.java
index 871eccf2cb..490f0b12c5 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/PartitionAwarenessTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/PartitionAwarenessTest.java
@@ -195,14 +195,14 @@ public class PartitionAwarenessTest extends AbstractClientTest {
         RecordView<Tuple> recordView = table(FakeIgniteTables.TABLE_COLOCATION_KEY).recordView();
 
         // COLO-2 is nullable and not set.
-        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0")));
-        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 2).set("COLO-1", "0")));
-        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 3).set("COLO-1", "0")));
-        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 3).set("COLO-1", "1")));
+        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0")));
+        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 2).set("COLO-1", "0")));
+        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 3).set("COLO-1", "0")));
+        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 3).set("COLO-1", "2")));
 
         // COLO-2 is set.
-        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0").set("COLO-2", 1)));
-        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0").set("COLO-2", 2)));
+        assertOpOnNode("server-1", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0").set("COLO-2", 1)));
+        assertOpOnNode("server-2", "get", x -> recordView.get(null, Tuple.create().set("ID", 0).set("COLO-1", "0").set("COLO-2", 2)));
     }
 
     @Test
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/ArrayUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/ArrayUtils.java
index 525d6af39b..6f3245ffd4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/ArrayUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/ArrayUtils.java
@@ -186,7 +186,7 @@ public final class ArrayUtils {
      * @param <T> Array element type.
      * @return {@code true} if {@code null} or an empty array is provided, {@code false} otherwise.
      */
-    public static <T> boolean nullOrEmpty(T[] arr) {
+    public static <T> boolean nullOrEmpty(T @Nullable[] arr) {
         return arr == null || arr.length == 0;
     }
 
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 be02cf68ce..02bc0f63f1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -46,9 +46,9 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
 
         internal static readonly Schema Schema = new(1, 1, 1, new[]
         {
-            new Column(nameof(Car.Id), ColumnType.Uuid, IsNullable: false, IsColocation: true, IsKey: true, SchemaIndex: 0, Scale: 0, Precision: 0),
-            new Column(nameof(Car.BodyType), ColumnType.String, IsNullable: false, IsColocation: false, IsKey: false, SchemaIndex: 1, Scale: 0, Precision: 0),
-            new Column(nameof(Car.Seats), ColumnType.Int32, IsNullable: false, IsColocation: false, IsKey: false, SchemaIndex: 2, Scale: 0, Precision: 0)
+            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/FakeServer.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
index 5b2fd625c3..909a6a6abb 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/FakeServer.cs
@@ -498,7 +498,7 @@ namespace Apache.Ignite.Tests
                 writer.Write((int)ColumnType.Int32);
                 writer.Write(true); // Key.
                 writer.Write(false); // Nullable.
-                writer.Write(true); // Colocation.
+                writer.Write(0); // Colocation index.
                 writer.Write(0); // Scale.
                 writer.Write(0); // Precision.
             }
@@ -511,7 +511,7 @@ namespace Apache.Ignite.Tests
                 writer.Write((int)ColumnType.String);
                 writer.Write(true); // Key.
                 writer.Write(false); // Nullable.
-                writer.Write(true); // Colocation.
+                writer.Write(0); // Colocation index.
                 writer.Write(0); // Scale.
                 writer.Write(0); // Precision.
 
@@ -520,7 +520,7 @@ namespace Apache.Ignite.Tests
                 writer.Write((int)ColumnType.Uuid);
                 writer.Write(true); // Key.
                 writer.Write(false); // Nullable.
-                writer.Write(true); // Colocation.
+                writer.Write(1); // Colocation index.
                 writer.Write(0); // Scale.
                 writer.Write(0); // Precision.
             }
@@ -533,7 +533,7 @@ namespace Apache.Ignite.Tests
                 writer.Write((int)ColumnType.String);
                 writer.Write(true); // Key.
                 writer.Write(false); // Nullable.
-                writer.Write(true); // Colocation.
+                writer.Write(0); // Colocation index.
                 writer.Write(0); // Scale.
                 writer.Write(0); // Precision.
 
@@ -542,7 +542,7 @@ namespace Apache.Ignite.Tests
                 writer.Write((int)ColumnType.Uuid);
                 writer.Write(true); // Key.
                 writer.Write(false); // Nullable.
-                writer.Write(false); // Colocation.
+                writer.Write(-1); // Colocation index.
                 writer.Write(0); // Scale.
                 writer.Write(0); // Precision.
             }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
index 68434e9352..c63da30322 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs
@@ -252,7 +252,7 @@ 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, true, schemaIndex, Scale: scale, precision);
+        return new Column("m_Item" + (schemaIndex + 1), colType, false, true, schemaIndex, schemaIndex, Scale: scale, precision);
     }
 
     private async Task AssertClientAndServerHashesAreEqual(int timePrecision = 9, int timestampPrecision = 6, params object[] keys)
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 4ee9a143fb..8d3eb6d22c 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
@@ -34,8 +34,8 @@ namespace Apache.Ignite.Tests.Table.Serialization
     {
         private static readonly Schema Schema = new(1, 1, 1, new[]
         {
-            new Column("Key", ColumnType.Int64, IsNullable: false, IsColocation: true, IsKey: true, SchemaIndex: 0, Scale: 0, Precision: 0),
-            new Column("Val", ColumnType.String, IsNullable: false, IsColocation: false, IsKey: false, SchemaIndex: 1, Scale: 0, Precision: 0)
+            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]
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
index a4686e699f..fe1a5f4bbc 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Column.cs
@@ -26,8 +26,14 @@ internal record Column(
     string Name,
     ColumnType Type,
     bool IsNullable,
-    bool IsColocation,
     bool IsKey,
+    int ColocationIndex,
     int SchemaIndex,
     int Scale,
-    int Precision);
+    int Precision)
+{
+    /// <summary>
+    /// Gets a value indicating whether this column participates in colocation.
+    /// </summary>
+    public bool IsColocation => ColocationIndex >= 0;
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 7dfacbdbc0..1d43e02588 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -337,13 +337,13 @@ namespace Apache.Ignite.Internal.Table
                 var type = r.ReadInt32();
                 var isKey = r.ReadBoolean();
                 var isNullable = r.ReadBoolean();
-                var isColocation = r.ReadBoolean(); // IsColocation.
+                var colocationIndex = r.ReadInt32();
                 var scale = r.ReadInt32();
                 var precision = r.ReadInt32();
 
                 r.Skip(propertyCount - expectedCount);
 
-                var column = new Column(name, (ColumnType)type, isNullable, isColocation, isKey, i, scale, precision);
+                var column = new Column(name, (ColumnType)type, isNullable, isKey, colocationIndex, i, scale, precision);
 
                 columns[i] = column;
 
@@ -353,7 +353,12 @@ namespace Apache.Ignite.Internal.Table
                 }
             }
 
-            var schema = new Schema(schemaVersion, Id, keyColumnCount, columns);
+            var schema = new Schema(
+                schemaVersion,
+                Id,
+                keyColumnCount,
+                columns);
+
             _schemas[schemaVersion] = Task.FromResult(schema);
 
             if (_logger?.IsEnabled(LogLevel.Debug) == true)
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
index c7eb34f143..4307c87641 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
@@ -53,11 +53,14 @@ import org.apache.ignite.internal.schema.testutils.builder.SchemaBuilders;
 import org.apache.ignite.internal.schema.testutils.definition.ColumnType;
 import org.apache.ignite.internal.schema.testutils.definition.ColumnType.TemporalColumnType;
 import org.apache.ignite.internal.schema.testutils.definition.TableDefinition;
+import org.apache.ignite.internal.table.RecordBinaryViewImpl;
 import org.apache.ignite.internal.table.distributed.TableManager;
 import org.apache.ignite.internal.table.impl.DummySchemaManagerImpl;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.testframework.TestIgnitionManager;
 import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.sql.Session;
+import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
 import org.jetbrains.annotations.NotNull;
 
@@ -547,6 +550,30 @@ public class PlatformTestNodeRunner {
         }
     }
 
+    /**
+     * Compute job that computes row colocation hash according to the current table schema.
+     */
+    @SuppressWarnings({"unused"}) // Used by platform tests.
+    private static class TableRowColocationHashJob implements ComputeJob<Integer> {
+        @Override
+        public Integer execute(JobExecutionContext context, Object... args) {
+            String tableName = (String) args[0];
+            int i = (int) args[1];
+            Tuple key = Tuple.create().set("id", 1 + i).set("id0", 2L + i).set("id1", "3" + i);
+
+            @SuppressWarnings("resource")
+            Table table = context.ignite().tables().table(tableName);
+            RecordBinaryViewImpl view = (RecordBinaryViewImpl) table.recordView();
+            TupleMarshallerImpl marsh = IgniteTestUtils.getFieldValue(view, "marsh");
+
+            try {
+                return marsh.marshal(key).colocationHash();
+            } catch (TupleMarshallerException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     /**
      * Compute job that enables or disables client authentication.
      */
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItThinClientColocationTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItThinClientColocationTest.java
index 5fad6aec13..b829700efc 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItThinClientColocationTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItThinClientColocationTest.java
@@ -22,7 +22,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Stream;
+import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.client.handler.requests.table.ClientTableCommon;
 import org.apache.ignite.internal.client.table.ClientColumn;
 import org.apache.ignite.internal.client.table.ClientSchema;
@@ -36,16 +39,20 @@ import org.apache.ignite.internal.schema.marshaller.TupleMarshaller;
 import org.apache.ignite.internal.schema.marshaller.TupleMarshallerException;
 import org.apache.ignite.internal.schema.marshaller.TupleMarshallerImpl;
 import org.apache.ignite.internal.schema.row.Row;
+import org.apache.ignite.internal.sql.engine.ClusterPerClassIntegrationTest;
 import org.apache.ignite.internal.table.impl.DummySchemaManagerImpl;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
+import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * Tests that client and server have matching colocation logic.
  */
-public class ItThinClientColocationTest {
+public class ItThinClientColocationTest extends ClusterPerClassIntegrationTest {
     @ParameterizedTest
     @MethodSource("nativeTypes")
     public void testClientAndServerColocationHashesAreSame(NativeType type)
@@ -68,18 +75,47 @@ public class ItThinClientColocationTest {
         }
     }
 
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testCustomColocationColumnOrder(boolean reverseColocationOrder) throws Exception {
+        String tableName = "testCustomColocationColumnOrder_" + reverseColocationOrder;
+
+        sql("create table " + tableName + "(id integer, id0 bigint, id1 varchar, v INTEGER, "
+                + "primary key(id, id0, id1)) colocate by " + (reverseColocationOrder ? "(id1, id0)" : "(id0, id1)"));
+
+        Table serverTable = CLUSTER_NODES.get(0).tables().table(tableName);
+        RecordBinaryViewImpl serverView = (RecordBinaryViewImpl) serverTable.recordView();
+        TupleMarshallerImpl marsh = IgniteTestUtils.getFieldValue(serverView, "marsh");
+
+        try (IgniteClient client = IgniteClient.builder().addresses("localhost").build()) {
+            // Perform get to populate schema.
+            Table clientTable = client.tables().table(tableName);
+            clientTable.recordView().get(null, Tuple.create().set("id", 1).set("id0", 2L).set("id1", "3"));
+
+            Map<Integer, CompletableFuture<ClientSchema>> schemas = IgniteTestUtils.getFieldValue(clientTable, "schemas");
+
+            for (int i = 0; i < 100; i++) {
+                Tuple key = Tuple.create().set("id", 1 + i).set("id0", 2L + i).set("id1", Integer.toString(3 + i));
+                int serverHash = marsh.marshal(key).colocationHash();
+                int clientHash = ClientTupleSerializer.getColocationHash(schemas.values().iterator().next().get(), key);
+
+                assertEquals(serverHash, clientHash);
+            }
+        }
+    }
+
     private static ClientSchema clientSchema(NativeType type, String columnName) {
         var clientColumn = new ClientColumn(
                 columnName,
                 ClientTableCommon.getColumnType(type.spec()),
                 false,
                 true,
-                true,
+                -1,
                 0,
                 ClientTableCommon.getDecimalScale(type),
                 ClientTableCommon.getPrecision(type));
 
-        return new ClientSchema(0, new ClientColumn[]{clientColumn});
+        return new ClientSchema(0, new ClientColumn[]{clientColumn}, null);
     }
 
     private static TupleMarshallerImpl tupleMarshaller(NativeType type, String columnName) {
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
index 95518c5e62..b9aaa5f47a 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.schema;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -48,6 +49,9 @@ public class SchemaDescriptor {
     /** Colocation columns. */
     private final Column[] colocationCols;
 
+    /** Colocation columns. */
+    private final @Nullable Map<Column, Integer> colocationColIndexes;
+
     /** Mapping 'Column name' -&gt; Column. */
     private final Map<String, Column> colMap;
 
@@ -76,7 +80,7 @@ public class SchemaDescriptor {
      * @param colocationCols Colocation column names.
      * @param valCols Value columns.
      */
-    public SchemaDescriptor(int ver, Column[] keyCols, @Nullable String[] colocationCols, Column[] valCols) {
+    public SchemaDescriptor(int ver, Column[] keyCols, String @Nullable[] colocationCols, Column[] valCols) {
         assert keyCols.length > 0 : "No key columns are configured.";
 
         this.ver = ver;
@@ -102,8 +106,19 @@ public class SchemaDescriptor {
 
         // Preserving key chunk column order is not actually required.
         // It is sufficient to has same column order for all nodes.
-        this.colocationCols = (ArrayUtils.nullOrEmpty(colocationCols)) ? this.keyCols.columns() :
-                Arrays.stream(colocationCols).map(colMap::get).toArray(Column[]::new);
+        if (ArrayUtils.nullOrEmpty(colocationCols)) {
+            this.colocationCols = this.keyCols.columns();
+            this.colocationColIndexes = null;
+        } else {
+            this.colocationCols = new Column[colocationCols.length];
+            this.colocationColIndexes = new HashMap<>(colocationCols.length);
+
+            for (int i = 0; i < colocationCols.length; i++) {
+                Column col = colMap.get(colocationCols[i]);
+                this.colocationCols[i] = col;
+                this.colocationColIndexes.put(col, i);
+            }
+        }
     }
 
     /**
@@ -185,6 +200,18 @@ public class SchemaDescriptor {
         return colocationCols;
     }
 
+    /**
+     * Get colocation index of the specified column.
+     *
+     * @param col Column.
+     * @return Index in the colocationColumns array, or -1 when not applicable.
+     */
+    public int colocationIndex(Column col) {
+        return colocationColIndexes == null
+                ? -1
+                : colocationColIndexes.getOrDefault(col, -1);
+    }
+
     /**
      * Get value columns.
      *