You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ko...@apache.org on 2025/05/16 14:43:33 UTC

(ignite-3) branch main updated: IGNITE-25352 Sql. Support NULL ORDERING for indexes (#5834)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new c4a8b3e4ce1 IGNITE-25352 Sql. Support NULL ORDERING for indexes (#5834)
c4a8b3e4ce1 is described below

commit c4a8b3e4ce1a94630b4276ddde1439ac6d6ffe81
Author: korlov42 <ko...@apache.org>
AuthorDate: Fri May 16 17:43:27 2025 +0300

    IGNITE-25352 Sql. Support NULL ORDERING for indexes (#5834)
---
 .../internal/sql/engine/ItSecondaryIndexTest.java  | 63 ++++++++++++++++++++--
 .../src/main/codegen/includes/parserImpls.ftl      | 12 +++++
 .../prepare/ddl/DdlSqlToCommandConverter.java      | 17 +++++-
 .../sql/engine/sql/AbstractParserTest.java         |  4 +-
 .../internal/sql/engine/sql/SqlDdlParserTest.java  | 48 +++++++++++++++--
 .../index/StorageSortedIndexDescriptor.java        | 23 +++++++-
 .../ignite/internal/storage/util/StorageUtils.java |  4 +-
 .../storage/engine/AbstractStorageEngineTest.java  |  4 +-
 .../index/AbstractSortedIndexStorageTest.java      |  3 +-
 .../storage/rocksdb/ColumnFamilyUtils.java         | 14 +++--
 .../storage/rocksdb/ColumnFamilyUtilsTest.java     | 18 ++++---
 .../instance/SharedRocksDbInstanceTest.java        | 22 ++++----
 .../internal/table/distributed/IndexBaseTest.java  |  4 +-
 .../table/distributed/SortedIndexLockerTest.java   |  2 +-
 .../table/distributed/StorageCleanupTest.java      |  4 +-
 .../distributed/StorageUpdateHandlerTest.java      |  4 +-
 .../PartitionReplicaListenerIndexLockingTest.java  |  2 +-
 ...itionReplicaListenerSortedIndexLockingTest.java |  2 +-
 .../replication/PartitionReplicaListenerTest.java  |  2 +-
 .../ZonePartitionReplicaListenerTest.java          |  2 +-
 20 files changed, 204 insertions(+), 50 deletions(-)

diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
index b321b7f342a..0c1d9c2692b 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItSecondaryIndexTest.java
@@ -23,8 +23,10 @@ import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIn
 import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsSubPlan;
 import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsTableScan;
 import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsUnion;
+import static org.apache.ignite.internal.sql.engine.util.QueryChecker.matches;
 import static org.apache.ignite.internal.sql.engine.util.QueryChecker.matchesOnce;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.not;
 
 import java.time.LocalDate;
@@ -66,6 +68,8 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
                 .app("CREATE TABLE assignments(developer_id INT, project_id INT, primary key(developer_id, project_id));").nl()
                 .app("CREATE TABLE t1 (id INT PRIMARY KEY, val INT);").nl()
                 .app("CREATE INDEX t1_idx on t1(val DESC);").nl()
+                .app("CREATE INDEX t1_val_asc_nulls_last_idx on t1(val ASC NULLS LAST);").nl()
+                .app("CREATE INDEX t1_val_asc_nulls_first_idx on t1(val ASC NULLS FIRST);").nl()
                 .toString();
 
         sqlScript(query);
@@ -791,7 +795,11 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
     public void testIndexedNullableFieldGreaterThanFilter() {
         assertQuery("SELECT * FROM T1 WHERE val > 4")
                 .disableRules("LogicalTableScanConverterRule")
-                .matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
+                .matches(anyOf(
+                        containsIndexScan("PUBLIC", "T1", "T1_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX")
+                ))
                 .returns(5, 5)
                 .returns(6, 6)
                 .check();
@@ -860,7 +868,11 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
     public void testIndexedNullableFieldLessThanFilter() {
         assertQuery("SELECT * FROM T1 WHERE val <= 5")
                 .disableRules("LogicalTableScanConverterRule")
-                .matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
+                .matches(anyOf(
+                        containsIndexScan("PUBLIC", "T1", "T1_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX")
+                ))
                 .returns(3, 3)
                 .returns(4, 4)
                 .returns(5, 5)
@@ -915,7 +927,11 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
     @Test
     public void testNullCondition1() {
         assertQuery("SELECT * FROM T1 WHERE val is null")
-                .matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
+                .matches(anyOf(
+                        containsIndexScan("PUBLIC", "T1", "T1_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX")
+                ))
                 .matches(not(containsUnion()))
                 .returns(1, null)
                 .returns(2, null)
@@ -927,7 +943,11 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
     public void testNullCondition2() {
         assertQuery("SELECT * FROM T1 WHERE (val <= 5) or (val is null)")
                 .disableRules("LogicalTableScanConverterRule")
-                .matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
+                .matches(anyOf(
+                        containsIndexScan("PUBLIC", "T1", "T1_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX")
+                ))
                 .matches(not(containsUnion()))
                 .returns(1, null)
                 .returns(2, null)
@@ -942,7 +962,11 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
     public void testNullCondition3() {
         assertQuery("SELECT * FROM T1 WHERE (val >= 5) or (val is null)")
                 .disableRules("LogicalTableScanConverterRule")
-                .matches(containsIndexScan("PUBLIC", "T1", "T1_IDX"))
+                .matches(anyOf(
+                        containsIndexScan("PUBLIC", "T1", "T1_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"),
+                        containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX")
+                ))
                 .matches(not(containsUnion()))
                 .returns(1, null)
                 .returns(2, null)
@@ -1172,6 +1196,35 @@ public class ItSecondaryIndexTest extends BaseSqlIntegrationTest {
                 .check();
     }
 
+    @Test
+    void nullsOrderingTest() {
+        assertQuery("SELECT val FROM t1 ORDER BY val ASC NULLS FIRST")
+                .matches(containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_FIRST_IDX"))
+                .matches(not(matches("Sort")))
+                .ordered()
+                .returns(null)
+                .returns(null)
+                .returns(null)
+                .returns(3)
+                .returns(4)
+                .returns(5)
+                .returns(6)
+                .check();
+
+        assertQuery("SELECT val FROM t1 ORDER BY val ASC NULLS LAST")
+                .matches(containsIndexScan("PUBLIC", "T1", "T1_VAL_ASC_NULLS_LAST_IDX"))
+                .matches(not(matches("Sort")))
+                .ordered()
+                .returns(3)
+                .returns(4)
+                .returns(5)
+                .returns(6)
+                .returns(null)
+                .returns(null)
+                .returns(null)
+                .check();
+    }
+
     @ParameterizedTest
     @CsvSource(value = {
             // type, literal
diff --git a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
index b89bb20ecb1..533f15f6261 100644
--- a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
+++ b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
@@ -197,6 +197,18 @@ SqlNode ColumnNameWithSortDirection() :
             col = SqlStdOperatorTable.DESC.createCall(getPos(), col);
         }
     )?
+
+    (
+        LOOKAHEAD(2)
+        <NULLS> <FIRST> {
+            col = SqlStdOperatorTable.NULLS_FIRST.createCall(getPos(), col);
+        }
+    |
+        <NULLS> <LAST> {
+            col = SqlStdOperatorTable.NULLS_LAST.createCall(getPos(), col);
+        }
+    )?
+
     {
         return col;
     }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
index eb8de7660c4..430c1ee1d6d 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
@@ -657,6 +657,17 @@ public class DdlSqlToCommandConverter {
     ) {
         for (SqlNode col : columnList.getList()) {
             boolean asc = true;
+            Boolean nullsFirst = null;
+
+            if (col.getKind() == SqlKind.NULLS_FIRST) {
+                col = ((SqlCall) col).getOperandList().get(0);
+
+                nullsFirst = true;
+            } else if (col.getKind() == SqlKind.NULLS_LAST) {
+                col = ((SqlCall) col).getOperandList().get(0);
+
+                nullsFirst = false;
+            }
 
             if (col.getKind() == SqlKind.DESCENDING) {
                 col = ((SqlCall) col).getOperandList().get(0);
@@ -664,10 +675,14 @@ public class DdlSqlToCommandConverter {
                 asc = false;
             }
 
+            if (nullsFirst == null) {
+                nullsFirst = !asc;
+            }
+
             String columnName = ((SqlIdentifier) col).getSimple();
             columns.add(columnName);
             if (supportCollation) {
-                collations.add(CatalogColumnCollation.get(asc, !asc));
+                collations.add(CatalogColumnCollation.get(asc, nullsFirst));
             }
         }
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/AbstractParserTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/AbstractParserTest.java
index 670fd626101..4f58879d726 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/AbstractParserTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/AbstractParserTest.java
@@ -53,12 +53,12 @@ public abstract class AbstractParserTest {
      * @param pred Addition check that would be applied to the object.
      * @return {@code true} in case the object if instance of the given class and matches the predicat.
      */
-    static <T> Matcher<T> ofTypeMatching(String desc, Class<T> cls, Predicate<T> pred) {
+    static <WiderT, SpecificT extends WiderT> Matcher<WiderT> ofTypeMatching(String desc, Class<SpecificT> cls, Predicate<SpecificT> pred) {
         return new CustomMatcher<>(desc) {
             /** {@inheritDoc} */
             @Override
             public boolean matches(Object item) {
-                return item != null && cls.isAssignableFrom(item.getClass()) && pred.test((T) item);
+                return item != null && cls.isAssignableFrom(item.getClass()) && pred.test((SpecificT) item);
             }
         };
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java
index 378020473e7..7ab5fc15f83 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java
@@ -46,10 +46,10 @@ import org.apache.calcite.sql.SqlNumericLiteral;
 import org.apache.calcite.sql.SqlUnknownLiteral;
 import org.apache.calcite.sql.ddl.SqlColumnDeclaration;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.util.CollectionUtils;
 import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.hamcrest.CustomMatcher;
 import org.hamcrest.Matcher;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
@@ -58,7 +58,6 @@ import org.junit.jupiter.params.provider.ValueSource;
 /**
  * Test suite to verify parsing of the DDL command.
  */
-@SuppressWarnings("ThrowableNotThrown")
 public class SqlDdlParserTest extends AbstractParserTest {
     /**
      * Very simple case where only table name and a few columns are presented.
@@ -479,6 +478,50 @@ public class SqlDdlParserTest extends AbstractParserTest {
         );
     }
 
+    @Test
+    public void createTableWithSortedPkAndParticularNullOrdering() {
+        String query = "create table my_table(id1 int, id2 int, val varchar, " 
+                + "primary key using sorted (id1 ASC NULLS FIRST, id2 DESC NULLS LAST))";
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable) node;
+
+        assertThat(createTable.name().names, is(List.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+
+        SqlNode lastItem = CollectionUtils.last(createTable.columnList());
+
+        assertThat(lastItem, instanceOf((IgniteSqlPrimaryKeyConstraint.class)));
+
+        IgniteSqlPrimaryKeyConstraint pkConstraint = (IgniteSqlPrimaryKeyConstraint) lastItem;
+
+        assertThat(
+                pkConstraint.getColumnList().get(0),
+                ofTypeMatching("\"ID1\" NULLS FIRST", SqlBasicCall.class, bc -> bc.isA(Set.of(SqlKind.NULLS_FIRST))
+                        && bc.getOperandList().get(0) instanceof SqlIdentifier
+                        && ((SqlIdentifier) bc.getOperandList().get(0)).isSimple()
+                        && ((SqlIdentifier) bc.getOperandList().get(0)).getSimple().equals("ID1"))
+        );
+
+        assertThat(
+                pkConstraint.getColumnList().get(1),
+                ofTypeMatching("\"ID2\" DESC NULLS LAST", SqlBasicCall.class, bc -> bc.isA(Set.of(SqlKind.NULLS_LAST))
+                        && bc.getOperandList().get(0) instanceof SqlBasicCall
+                        && (bc.getOperandList().get(0)).isA(Set.of(SqlKind.DESCENDING))
+                        && ((SqlBasicCall) bc.getOperandList().get(0)).getOperandList().get(0) instanceof SqlIdentifier
+                        && ((SqlIdentifier) ((SqlBasicCall) bc.getOperandList().get(0)).getOperandList().get(0)).isSimple() 
+                        && ((SqlIdentifier) ((SqlBasicCall) bc.getOperandList().get(0)).getOperandList().get(0)).getSimple().equals("ID2"))
+        );
+
+        expectUnparsed(node, "CREATE TABLE \"MY_TABLE\" "
+                + "(\"ID1\" INTEGER, "
+                + "\"ID2\" INTEGER, "
+                + "\"VAL\" VARCHAR, "
+                + "PRIMARY KEY USING SORTED (\"ID1\" NULLS FIRST, \"ID2\" DESC NULLS LAST))"
+        );
+    }
+
     /**
      * Parsing of CREATE TABLE with primary key index type HASH.
      */
@@ -730,7 +773,6 @@ public class SqlDdlParserTest extends AbstractParserTest {
         expectUnparsed(node, "CREATE INDEX \"MY_INDEX\" ON \"MY_SCHEMA\".\"MY_TABLE\" (\"COL\")");
     }
 
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-21672")
     @Test
     public void createIndexExplicitNullDirection() {
         var query = "create index my_index on my_table (col1 nulls first, col2 nulls last, col3 desc nulls first)";
diff --git a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/StorageSortedIndexDescriptor.java b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/StorageSortedIndexDescriptor.java
index cca8568adfc..7ccfb723b2e 100644
--- a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/StorageSortedIndexDescriptor.java
+++ b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/StorageSortedIndexDescriptor.java
@@ -50,6 +50,8 @@ public class StorageSortedIndexDescriptor implements StorageIndexDescriptor {
 
         private final boolean asc;
 
+        private final boolean nullsFirst;
+
         /**
          * Creates a Column Descriptor.
          *
@@ -57,12 +59,20 @@ public class StorageSortedIndexDescriptor implements StorageIndexDescriptor {
          * @param type Type of the column.
          * @param nullable Flag indicating that the column may contain {@code null}s.
          * @param asc Sort order of the column.
+         * @param nullsFirst Flag denotes whether to put NULL values first or last in the sorted stream.
          */
-        public StorageSortedIndexColumnDescriptor(String name, NativeType type, boolean nullable, boolean asc) {
+        public StorageSortedIndexColumnDescriptor(
+                String name,
+                NativeType type,
+                boolean nullable,
+                boolean asc,
+                boolean nullsFirst
+        ) {
             this.name = name;
             this.type = type;
             this.nullable = nullable;
             this.asc = asc;
+            this.nullsFirst = nullsFirst;
         }
 
         @Override
@@ -89,6 +99,13 @@ public class StorageSortedIndexDescriptor implements StorageIndexDescriptor {
             return asc;
         }
 
+        /**
+         * Returns {@code true} if nulls in this column are put first, returns {@code false} otherwise.
+         */
+        public boolean nullsFirst() {
+            return nullsFirst;
+        }
+
         @Override
         public String toString() {
             return S.toString(this);
@@ -179,7 +196,9 @@ public class StorageSortedIndexDescriptor implements StorageIndexDescriptor {
 
                     CatalogColumnCollation collation = columnDescriptor.collation();
 
-                    return new StorageSortedIndexColumnDescriptor(columnName, getNativeType(column), column.nullable(), collation.asc());
+                    return new StorageSortedIndexColumnDescriptor(
+                            columnName, getNativeType(column), column.nullable(), collation.asc(), collation.nullsFirst()
+                    );
                 })
                 .collect(toList());
     }
diff --git a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/util/StorageUtils.java b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/util/StorageUtils.java
index e9fdc1fa301..88905cec352 100644
--- a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/util/StorageUtils.java
+++ b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/util/StorageUtils.java
@@ -286,7 +286,7 @@ public class StorageUtils {
         List<NativeType> columnTypes = new ArrayList<>(columns.size());
 
         for (StorageSortedIndexColumnDescriptor col : columns) {
-            columnCollation.add(CatalogColumnCollation.get(col.asc(), !col.asc()));
+            columnCollation.add(CatalogColumnCollation.get(col.asc(), col.nullsFirst()));
             columnTypes.add(col.type());
         }
 
@@ -301,7 +301,7 @@ public class StorageUtils {
         List<NativeType> columnTypes = new ArrayList<>(columns.size());
 
         for (StorageSortedIndexColumnDescriptor col : columns) {
-            columnCollation.add(CatalogColumnCollation.get(col.asc(), !col.asc()));
+            columnCollation.add(CatalogColumnCollation.get(col.asc(), col.nullsFirst()));
             columnTypes.add(col.type());
         }
 
diff --git a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/engine/AbstractStorageEngineTest.java b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/engine/AbstractStorageEngineTest.java
index ed7f6894972..95472e7d82b 100644
--- a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/engine/AbstractStorageEngineTest.java
+++ b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/engine/AbstractStorageEngineTest.java
@@ -164,7 +164,9 @@ public abstract class AbstractStorageEngineTest extends BaseMvStoragesTest {
 
         int numTables = 2;
 
-        var sortedIndexColumnDescriptor = new StorageSortedIndexColumnDescriptor("foo", NativeTypes.INT64, true, true);
+        var sortedIndexColumnDescriptor = new StorageSortedIndexColumnDescriptor(
+                "foo", NativeTypes.INT64, true, true, false
+        );
 
         var hashIndexColumnDescriptor = new StorageHashIndexColumnDescriptor("foo", NativeTypes.INT64, true);
 
diff --git a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/index/AbstractSortedIndexStorageTest.java b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/index/AbstractSortedIndexStorageTest.java
index 4104d1d0601..b803bc53cf6 100644
--- a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/index/AbstractSortedIndexStorageTest.java
+++ b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/index/AbstractSortedIndexStorageTest.java
@@ -22,6 +22,7 @@ import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toUnmodifiableList;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.ASC_NULLS_FIRST;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.ASC_NULLS_LAST;
+import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.DESC_NULLS_FIRST;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.DESC_NULLS_LAST;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogIndexStatus.AVAILABLE;
 import static org.apache.ignite.internal.storage.index.SortedIndexStorage.GREATER;
@@ -109,7 +110,7 @@ public abstract class AbstractSortedIndexStorageTest extends AbstractIndexStorag
                         .map(ColumnParams::name)
                         .map(columnName -> new CatalogIndexColumnDescriptor(
                                 columnName,
-                                random.nextBoolean() ? ASC_NULLS_FIRST : DESC_NULLS_LAST
+                                random.nextBoolean() ? ASC_NULLS_LAST : DESC_NULLS_FIRST
                         ))
                         .toArray(CatalogIndexColumnDescriptor[]::new)
         );
diff --git a/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtils.java b/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtils.java
index 5d8716bc951..140ebbe0ada 100644
--- a/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtils.java
+++ b/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtils.java
@@ -67,10 +67,13 @@ public class ColumnFamilyUtils {
     );
 
     /** Nullability flag mask for {@link #sortedIndexCfName(List)}. */
-    private static final int NULLABILITY_FLAG = 1;
+    private static final int NULLABILITY_FLAG = 0b1;
 
     /** Order flag mask for {@link #sortedIndexCfName(List)}. */
-    private static final int ASC_ORDER_FLAG = 2;
+    private static final int ASC_ORDER_FLAG = 0b10;
+
+    /** Flag denoting whether to put NULLs first or last for {@link #sortedIndexCfName(List)}. */
+    private static final int NULLS_FIRST_FLAG = 0b100;
 
     /** Utility enum to describe a type of the column family - meta or partition. */
     public enum ColumnFamilyType {
@@ -137,6 +140,10 @@ public class ColumnFamilyUtils {
                 flags |= ASC_ORDER_FLAG;
             }
 
+            if (column.nullsFirst()) {
+                flags |= NULLS_FIRST_FLAG;
+            }
+
             buf.put((byte) flags);
         }
 
@@ -230,8 +237,9 @@ public class ColumnFamilyUtils {
 
             boolean nullable = (nativeTypeFlags & NULLABILITY_FLAG) != 0;
             boolean asc = (nativeTypeFlags & ASC_ORDER_FLAG) != 0;
+            boolean nullsFirst = (nativeTypeFlags & NULLS_FIRST_FLAG) != 0;
 
-            columns.add(new StorageSortedIndexColumnDescriptor("<unknown>", nativeType, nullable, asc));
+            columns.add(new StorageSortedIndexColumnDescriptor("<unknown>", nativeType, nullable, asc, nullsFirst));
         }
 
         return new RocksDbBinaryTupleComparator(columns);
diff --git a/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtilsTest.java b/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtilsTest.java
index 5ac3e7ab39c..b34686e303a 100644
--- a/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtilsTest.java
+++ b/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/ColumnFamilyUtilsTest.java
@@ -17,8 +17,10 @@
 
 package org.apache.ignite.internal.storage.rocksdb;
 
+import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.ASC_NULLS_FIRST;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.ASC_NULLS_LAST;
 import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.DESC_NULLS_FIRST;
+import static org.apache.ignite.internal.catalog.descriptors.CatalogColumnCollation.DESC_NULLS_LAST;
 import static org.apache.ignite.internal.storage.rocksdb.ColumnFamilyUtils.comparatorFromCfName;
 import static org.apache.ignite.internal.storage.rocksdb.ColumnFamilyUtils.sortedIndexCfName;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.getFieldValue;
@@ -48,7 +50,7 @@ public class ColumnFamilyUtilsTest {
     @ParameterizedTest
     @VariableSource("ALL_TYPES")
     void testSortedIndexCfNameSingleType(NativeType nativeType) {
-        var descriptor = new StorageSortedIndexColumnDescriptor("<unused>", nativeType, false, false);
+        var descriptor = new StorageSortedIndexColumnDescriptor("<unused>", nativeType, false, false, false);
 
         assertArrayEquals(name(nativeType.spec().ordinal(), 0), sortedIndexCfName(List.of(descriptor)));
     }
@@ -56,21 +58,21 @@ public class ColumnFamilyUtilsTest {
     @Test
     void testSortedIndexCfNameFlags() {
         List<StorageSortedIndexColumnDescriptor> descriptors = List.of(
-                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT64, false, false),
-                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT32, true, false),
-                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT16, false, true),
-                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT8, true, true)
+                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT64, false, false, false),
+                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT32, true, false, true),
+                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT16, false, true, false),
+                new StorageSortedIndexColumnDescriptor("<unused>", NativeTypes.INT8, true, true, true)
         );
 
-        assertArrayEquals(name(3, 0, 2, 1, 1, 2, 0, 3), sortedIndexCfName(descriptors));
+        assertArrayEquals(name(3, 0, 2, 5, 1, 2, 0, 7), sortedIndexCfName(descriptors));
     }
 
     @Test
     void testComparatorFromCfName() {
-        RocksDbBinaryTupleComparator comparator = comparatorFromCfName(name(3, 0, 2, 1, 1, 2, 0, 3));
+        RocksDbBinaryTupleComparator comparator = comparatorFromCfName(name(3, 0, 2, 5, 1, 2, 0, 7));
 
         List<NativeType> expectedTypes = List.of(NativeTypes.INT64, NativeTypes.INT32, NativeTypes.INT16, NativeTypes.INT8);
-        List<CatalogColumnCollation> expectedCollations = List.of(DESC_NULLS_FIRST, DESC_NULLS_FIRST, ASC_NULLS_LAST, ASC_NULLS_LAST);
+        List<CatalogColumnCollation> expectedCollations = List.of(DESC_NULLS_LAST, DESC_NULLS_FIRST, ASC_NULLS_LAST, ASC_NULLS_FIRST);
 
         // I am sorry, this is for a single test only.
         List<NativeType> columnTypes = getFieldValue(comparator, "comparator", "columnTypes");
diff --git a/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/instance/SharedRocksDbInstanceTest.java b/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/instance/SharedRocksDbInstanceTest.java
index de59836c551..bb96a12c67a 100644
--- a/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/instance/SharedRocksDbInstanceTest.java
+++ b/modules/storage-rocksdb/src/test/java/org/apache/ignite/internal/storage/rocksdb/instance/SharedRocksDbInstanceTest.java
@@ -122,19 +122,19 @@ class SharedRocksDbInstanceTest extends IgniteAbstractTest {
     @Test
     void testSortedIndexCfCaching() {
         byte[] fooName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true, false)
         ));
 
         byte[] barName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("b", NativeTypes.UUID, true, true)
+                new StorageSortedIndexColumnDescriptor("b", NativeTypes.UUID, true, true, false)
         ));
 
         byte[] bazName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("c", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("c", NativeTypes.INT64, true, true, false)
         ));
 
         byte[] quuxName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("d", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("d", NativeTypes.INT64, true, true, false)
         ));
 
         ColumnFamily foo = rocksDb.getOrCreateSortedIndexCf(fooName, 1, 0);
@@ -173,19 +173,19 @@ class SharedRocksDbInstanceTest extends IgniteAbstractTest {
     @Test
     void testSortedIndexRecovery() throws Exception {
         byte[] fooName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true, false)
         ));
 
         byte[] barName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("b", NativeTypes.UUID, true, true)
+                new StorageSortedIndexColumnDescriptor("b", NativeTypes.UUID, true, true, false)
         ));
 
         byte[] bazName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("c", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("c", NativeTypes.INT64, true, true, false)
         ));
 
         byte[] quuxName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("d", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("d", NativeTypes.INT64, true, true, false)
         ));
 
         ColumnFamily foo = rocksDb.getOrCreateSortedIndexCf(fooName, 1, 0);
@@ -270,7 +270,7 @@ class SharedRocksDbInstanceTest extends IgniteAbstractTest {
     @RepeatedTest(10)
     void testConcurrentSortedIndexReadAndCreate() {
         byte[] fooName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true, false)
         ));
 
         rocksDb.getOrCreateSortedIndexCf(fooName, 0, 0);
@@ -311,7 +311,7 @@ class SharedRocksDbInstanceTest extends IgniteAbstractTest {
         int indexId = 0;
 
         byte[] fooName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true, false)
         ));
 
         ColumnFamily cf = rocksDb.getOrCreateSortedIndexCf(fooName, indexId, tableId);
@@ -326,7 +326,7 @@ class SharedRocksDbInstanceTest extends IgniteAbstractTest {
         int tableId = 0;
 
         byte[] fooName = sortedIndexCfName(List.of(
-                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true)
+                new StorageSortedIndexColumnDescriptor("a", NativeTypes.INT64, true, true, false)
         ));
 
         rocksDb.getOrCreateSortedIndexCf(fooName, 0, tableId);
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/IndexBaseTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/IndexBaseTest.java
index 63a58386df3..a39fd783996 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/IndexBaseTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/IndexBaseTest.java
@@ -127,8 +127,8 @@ public abstract class IndexBaseTest extends BaseMvStoragesTest {
                 new StorageSortedIndexDescriptor(
                         sortedIndexId,
                         List.of(
-                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true),
-                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true)
+                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true, false),
+                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true, false)
                         ),
                         true
                 )
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/SortedIndexLockerTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/SortedIndexLockerTest.java
index d702a6c2590..882c1d62f52 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/SortedIndexLockerTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/SortedIndexLockerTest.java
@@ -65,7 +65,7 @@ class SortedIndexLockerTest extends BaseIgniteAbstractTest {
                 PARTITION_ID,
                 new StorageSortedIndexDescriptor(
                         1,
-                        List.of(new StorageSortedIndexColumnDescriptor("c1", NativeTypes.INT32, false, true)),
+                        List.of(new StorageSortedIndexColumnDescriptor("c1", NativeTypes.INT32, false, true, false)),
                         false
                 )
         );
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageCleanupTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageCleanupTest.java
index 96010ec71eb..84081f647c3 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageCleanupTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageCleanupTest.java
@@ -124,8 +124,8 @@ public class StorageCleanupTest extends BaseMvStoragesTest {
                 new StorageSortedIndexDescriptor(
                         sortedIndexId,
                         List.of(
-                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true),
-                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true)
+                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true, false),
+                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true, false)
                         ),
                         false
                 )
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageUpdateHandlerTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageUpdateHandlerTest.java
index cd504d6cd7c..dfec8030b12 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageUpdateHandlerTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/StorageUpdateHandlerTest.java
@@ -115,8 +115,8 @@ public class StorageUpdateHandlerTest extends BaseMvStoragesTest {
                 new StorageSortedIndexDescriptor(
                         sortedIndexId,
                         List.of(
-                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true),
-                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true)
+                                new StorageSortedIndexColumnDescriptor("INTVAL", NativeTypes.INT32, false, true, false),
+                                new StorageSortedIndexColumnDescriptor("STRVAL", NativeTypes.STRING, false, true, false)
                         ),
                         false
                 )
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerIndexLockingTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerIndexLockingTest.java
index d40478feea8..0fa58efacbb 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerIndexLockingTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerIndexLockingTest.java
@@ -209,7 +209,7 @@ public class PartitionReplicaListenerIndexLockingTest extends IgniteAbstractTest
                 new TestSortedIndexStorage(PART_ID,
                         new StorageSortedIndexDescriptor(
                                 SORTED_INDEX_ID,
-                                List.of(new StorageSortedIndexColumnDescriptor("val", NativeTypes.INT32, false, true)),
+                                List.of(new StorageSortedIndexColumnDescriptor("val", NativeTypes.INT32, false, true, false)),
                                 false
                         )),
                 row2SortKeyConverter
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerSortedIndexLockingTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerSortedIndexLockingTest.java
index 2aa1d964876..b6622e7a7bc 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerSortedIndexLockingTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerSortedIndexLockingTest.java
@@ -175,7 +175,7 @@ public class PartitionReplicaListenerSortedIndexLockingTest extends IgniteAbstra
 
         StorageSortedIndexDescriptor pkIndexDescriptor = new StorageSortedIndexDescriptor(
                 PK_INDEX_ID,
-                List.of(new StorageSortedIndexColumnDescriptor("ID", NativeTypes.INT32, false, true)),
+                List.of(new StorageSortedIndexColumnDescriptor("ID", NativeTypes.INT32, false, true, false)),
                 false
         );
 
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerTest.java
index ee0d1ea0490..73d18af1f4c 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/PartitionReplicaListenerTest.java
@@ -566,7 +566,7 @@ public class PartitionReplicaListenerTest extends IgniteAbstractTest {
                 PART_ID,
                 new StorageSortedIndexDescriptor(
                         sortedIndexId,
-                        List.of(new StorageSortedIndexColumnDescriptor("intVal", NativeTypes.INT32, false, true)),
+                        List.of(new StorageSortedIndexColumnDescriptor("intVal", NativeTypes.INT32, false, true, false)),
                         false
                 )
         );
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/ZonePartitionReplicaListenerTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/ZonePartitionReplicaListenerTest.java
index cb4d6a8b568..80388d56bb5 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/ZonePartitionReplicaListenerTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/replication/ZonePartitionReplicaListenerTest.java
@@ -456,7 +456,7 @@ public class ZonePartitionReplicaListenerTest extends IgniteAbstractTest {
                 PART_ID,
                 new StorageSortedIndexDescriptor(
                         sortedIndexId,
-                        List.of(new StorageSortedIndexColumnDescriptor("intVal", NativeTypes.INT32, false, true)),
+                        List.of(new StorageSortedIndexColumnDescriptor("intVal", NativeTypes.INT32, false, true, false)),
                         false
                 )
         );