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

ibessonov 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 f0a43c18b7 IGNITE-17339 Hash index storage for page-memory based 
engines. (#1055)
f0a43c18b7 is described below

commit f0a43c18b7816c8131e937da92843c7697e7e525
Author: ibessonov <[email protected]>
AuthorDate: Wed Sep 7 10:31:04 2022 +0300

    IGNITE-17339 Hash index storage for page-memory based engines. (#1055)
---
 .../pagememory/freelist/AbstractFreeList.java      |  54 -----
 .../internal/pagememory/freelist/FreeList.java     |  15 --
 .../internal/pagememory/io/AbstractDataPageIo.java |  90 +--------
 .../pagememory/persistence/PartitionMeta.java      |  90 ++++++++-
 .../pagememory/persistence/io/PartitionMetaIo.java |  74 +++++--
 .../persistence/PartitionMetaManagerTest.java      |   6 +-
 .../pagememory/persistence/PartitionMetaTest.java  |   2 +-
 .../persistence/checkpoint/CheckpointerTest.java   |   2 +-
 .../internal/storage/index/HashIndexStorage.java   |  21 +-
 .../chm/TestConcurrentHashMapMvTableStorage.java   |   7 +-
 .../index/AbstractHashIndexStorageTest.java        |  30 ++-
 .../pagememory/AbstractPageMemoryTableStorage.java |  29 ++-
 .../PersistentPageMemoryTableStorage.java          | 218 +++++++++++++++------
 .../pagememory/VolatilePageMemoryDataRegion.java   |  38 +++-
 .../pagememory/VolatilePageMemoryTableStorage.java |  45 ++++-
 .../index/freelist/IndexColumnsFreeList.java       |  19 +-
 .../index/freelist/io/IndexColumnsDataIo.java      |   5 +
 .../hash/InsertHashIndexRowInvokeClosure.java      |   2 +-
 .../index/hash/PageMemoryHashIndexStorage.java     | 135 +++++++++++++
 .../hash/RemoveHashIndexRowInvokeClosure.java      |   2 +-
 .../storage/pagememory/index/meta/IndexMeta.java   |  21 +-
 .../pagememory/index/meta/io/IndexMetaIo.java      |  10 +-
 .../mv/AbstractPageMemoryMvPartitionStorage.java   | 136 ++++++++++++-
 .../mv/PersistentPageMemoryMvPartitionStorage.java |  52 +++--
 .../mv/VolatilePageMemoryMvPartitionStorage.java   |  29 +--
 .../storage/pagememory/util/TreeCursorAdapter.java |  77 ++++++++
 .../PersistentPageMemoryHashIndexStorageTest.java  | 105 ++++++++++
 .../VolatilePageMemoryHashIndexStorageTest.java    |  99 ++++++++++
 .../AbstractPageMemoryMvPartitionStorageTest.java  |   3 +-
 .../storage/rocksdb/RocksDbTableStorage.java       |   1 +
 30 files changed, 1074 insertions(+), 343 deletions(-)

diff --git 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
index 9c104cd9a0..fb342ec503 100644
--- 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
+++ 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
@@ -83,8 +83,6 @@ public abstract class AbstractFreeList<T extends Storable> 
extends PagesList imp
     /** Page eviction tracker. */
     protected final PageEvictionTracker evictionTracker;
 
-    private final PageHandler<T, Boolean> updateRow = new UpdateRowHandler();
-
     /** Write a single row on a single page. */
     private final WriteRowHandler writeRowHnd = new WriteRowHandler();
 
@@ -93,31 +91,6 @@ public abstract class AbstractFreeList<T extends Storable> 
extends PagesList imp
 
     private final PageHandler<ReuseBag, Long> rmvRow;
 
-    private final class UpdateRowHandler implements PageHandler<T, Boolean> {
-        /** {@inheritDoc} */
-        @Override
-        public Boolean run(
-                int cacheId,
-                long pageId,
-                long page,
-                long pageAddr,
-                PageIo iox,
-                T row,
-                int itemId,
-                IoStatisticsHolder statHolder
-        ) throws IgniteInternalCheckedException {
-            AbstractDataPageIo<T> io = (AbstractDataPageIo<T>) iox;
-
-            int rowSize = row.size();
-
-            boolean updated = io.updateRow(pageAddr, itemId, pageSize(), null, 
row, rowSize);
-
-            evictionTracker.touchPage(pageId);
-
-            return updated;
-        }
-    }
-
     private class WriteRowHandler implements PageHandler<T, Integer> {
         /** {@inheritDoc} */
         @Override
@@ -722,33 +695,6 @@ public abstract class AbstractFreeList<T extends Storable> 
extends PagesList imp
         }
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public boolean updateDataRow(
-            long link,
-            T row,
-            IoStatisticsHolder statHolder
-    ) throws IgniteInternalCheckedException {
-        assert link != 0;
-
-        try {
-            long pageId = pageId(link);
-            int itemId = itemId(link);
-
-            Boolean updated = write(pageId, updateRow, row, itemId, null, 
statHolder);
-
-            assert updated != null; // Can't fail here.
-
-            return updated;
-        } catch (AssertionError e) {
-            throw corruptedFreeListException(e);
-        } catch (IgniteInternalCheckedException | Error e) {
-            throw e;
-        } catch (Throwable t) {
-            throw new CorruptedFreeListException("Failed to update data row", 
t, grpId);
-        }
-    }
-
     /** {@inheritDoc} */
     @Override
     public <S, R> R updateDataRow(
diff --git 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
index 9c582bd503..ecfbeceaf9 100644
--- 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
+++ 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
@@ -46,21 +46,6 @@ public interface FreeList<T extends Storable> {
      */
     void insertDataRows(Collection<T> rows, IoStatisticsHolder statHolder) 
throws IgniteInternalCheckedException;
 
-    /**
-     * Makes an in-place update of a row identified by the link.
-     * This has a couple of restrictions:
-     * 1. The size of the payload must not change, otherwise the page will be 
broken (and next insertion will fail due
-     * to assertion failure).
-     * 2. The row cannot be fragmented. If it is, this will return {@code 
false} without doing anything.
-     *
-     * @param link Row link.
-     * @param row New row data.
-     * @param statHolder Statistics holder to track IO operations.
-     * @return {@code True} if was able to update row.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    boolean updateDataRow(long link, T row, IoStatisticsHolder statHolder) 
throws IgniteInternalCheckedException;
-
     /**
      * Updates a row by link.
      *
diff --git 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
index ac7b967b7f..01bb2e1a80 100644
--- 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
+++ 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
@@ -31,7 +31,6 @@ import org.apache.ignite.internal.pagememory.util.PageIdUtils;
 import org.apache.ignite.internal.pagememory.util.PageUtils;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
 import org.apache.ignite.lang.IgniteStringBuilder;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Data pages IO.
@@ -907,45 +906,6 @@ public abstract class AbstractDataPageIo<T extends 
Storable> extends PageIo {
         return -1;
     }
 
-    /**
-     * Updates a row.
-     *
-     * @param pageAddr Page address.
-     * @param itemId Item ID.
-     * @param pageSize Page size.
-     * @param payload Row data.
-     * @param row Row.
-     * @param rowSize Row size.
-     * @return {@code True} if entry is not fragmented.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public boolean updateRow(
-            final long pageAddr,
-            int itemId,
-            int pageSize,
-            @Nullable byte[] payload,
-            @Nullable T row,
-            final int rowSize
-    ) throws IgniteInternalCheckedException {
-        assert checkIndex(itemId) : itemId;
-        assert row != null ^ payload != null;
-        assertPageType(pageAddr);
-
-        final int dataOff = getDataOffset(pageAddr, itemId, pageSize);
-
-        if (isFragmented(pageAddr, dataOff)) {
-            return false;
-        }
-
-        if (row != null) {
-            writeRowData(pageAddr, dataOff, rowSize, row, false);
-        } else {
-            writeRowData(pageAddr, dataOff, payload);
-        }
-
-        return true;
-    }
-
     /**
      * Removes a row.
      *
@@ -1083,6 +1043,7 @@ public abstract class AbstractDataPageIo<T extends 
Storable> extends PageIo {
             final int pageSize
     ) throws IgniteInternalCheckedException {
         assert rowSize <= getFreeSpace(pageAddr) : "can't call addRow if not 
enough space for the whole row";
+        assert rowSize <= 0xFFFF : "Row size is too big: " + rowSize;
         assertPageType(pageAddr);
 
         int fullEntrySize = getPageEntrySize(rowSize, SHOW_PAYLOAD_LEN | 
SHOW_ITEM);
@@ -1099,35 +1060,6 @@ public abstract class AbstractDataPageIo<T extends 
Storable> extends PageIo {
         setLinkByPageId(row, pageId, itemId);
     }
 
-    /**
-     * Adds row to this data page and sets respective link to the given row 
object.
-     *
-     * @param pageAddr Page address.
-     * @param payload Payload.
-     * @param pageSize Page size.
-     * @return Item ID.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public int addRow(
-            long pageAddr,
-            byte[] payload,
-            int pageSize
-    ) throws IgniteInternalCheckedException {
-        assert payload.length <= getFreeSpace(pageAddr) : "can't call addRow 
if not enough space for the whole row";
-        assertPageType(pageAddr);
-
-        int fullEntrySize = getPageEntrySize(payload.length, SHOW_PAYLOAD_LEN 
| SHOW_ITEM);
-
-        int directCnt = getDirectCount(pageAddr);
-        int indirectCnt = getIndirectCount(pageAddr);
-
-        int dataOff = getDataOffsetForWrite(pageAddr, fullEntrySize, 
directCnt, indirectCnt, pageSize);
-
-        writeRowData(pageAddr, dataOff, payload);
-
-        return addItem(pageAddr, fullEntrySize, directCnt, indirectCnt, 
dataOff, pageSize);
-    }
-
     /**
      * Compacts a page if needed.
      *
@@ -1506,26 +1438,6 @@ public abstract class AbstractDataPageIo<T extends 
Storable> extends PageIo {
             boolean newRow
     ) throws IgniteInternalCheckedException;
 
-    /**
-     * Writes a row.
-     *
-     * @param pageAddr Page address.
-     * @param dataOff Data offset.
-     * @param payload Payload
-     */
-    protected void writeRowData(
-            long pageAddr,
-            int dataOff,
-            byte[] payload
-    ) {
-        assertPageType(pageAddr);
-
-        PageUtils.putShort(pageAddr, dataOff, (short) payload.length);
-        dataOff += 2;
-
-        PageUtils.putBytes(pageAddr, dataOff, payload);
-    }
-
     /**
      * Defines closure interface for applying computations to data page items.
      *
diff --git 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
index c1234377fb..bb722b7edc 100644
--- 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
+++ 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
@@ -26,6 +26,7 @@ import java.util.UUID;
 import org.apache.ignite.internal.pagememory.persistence.io.PartitionMetaIo;
 import org.apache.ignite.internal.tostring.S;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
 
 /**
  * Partition meta information.
@@ -47,9 +48,13 @@ public class PartitionMeta {
 
     private volatile long lastAppliedIndex;
 
+    private volatile long rowVersionFreeListRootPageId;
+
+    private volatile long indexColumnsFreeListRootPageId;
+
     private volatile long versionChainTreeRootPageId;
 
-    private volatile long rowVersionFreeListRootPageId;
+    private volatile long indexTreeMetaPageId;
 
     private volatile int pageCount;
 
@@ -58,6 +63,7 @@ public class PartitionMeta {
     /**
      * Default constructor.
      */
+    @TestOnly
     public PartitionMeta() {
         metaSnapshot = new PartitionMetaSnapshot(null, this);
     }
@@ -67,20 +73,24 @@ public class PartitionMeta {
      *
      * @param checkpointId Checkpoint ID.
      * @param lastAppliedIndex Last applied index value.
-     * @param versionChainTreeRootPageId Version chain tree root page ID.
      * @param rowVersionFreeListRootPageId Row version free list root page ID.
+     * @param versionChainTreeRootPageId Version chain tree root page ID.
      * @param pageCount Count of pages in the partition.
      */
     public PartitionMeta(
             @Nullable UUID checkpointId,
             long lastAppliedIndex,
-            long versionChainTreeRootPageId,
             long rowVersionFreeListRootPageId,
+            long indexColumnsFreeListRootPageId,
+            long versionChainTreeRootPageId,
+            long indexTreeMetaPageId,
             int pageCount
     ) {
         this.lastAppliedIndex = lastAppliedIndex;
-        this.versionChainTreeRootPageId = versionChainTreeRootPageId;
         this.rowVersionFreeListRootPageId = rowVersionFreeListRootPageId;
+        this.indexColumnsFreeListRootPageId = indexColumnsFreeListRootPageId;
+        this.versionChainTreeRootPageId = versionChainTreeRootPageId;
+        this.indexTreeMetaPageId = indexTreeMetaPageId;
         this.pageCount = pageCount;
 
         metaSnapshot = new PartitionMetaSnapshot(checkpointId, this);
@@ -97,8 +107,10 @@ public class PartitionMeta {
         this(
                 checkpointId,
                 metaIo.getLastAppliedIndex(pageAddr),
-                metaIo.getVersionChainTreeRootPageId(pageAddr),
                 metaIo.getRowVersionFreeListRootPageId(pageAddr),
+                metaIo.getIndexColumnsFreeListRootPageId(pageAddr),
+                metaIo.getVersionChainTreeRootPageId(pageAddr),
+                metaIo.getIndexTreeMetaPageId(pageAddr),
                 metaIo.getPageCount(pageAddr)
         );
     }
@@ -160,6 +172,44 @@ public class PartitionMeta {
         this.rowVersionFreeListRootPageId = rowVersionFreeListRootPageId;
     }
 
+    /**
+     * Returns index columns free list root page id.
+     */
+    public long indexColumnsFreeListRootPageId() {
+        return indexColumnsFreeListRootPageId;
+    }
+
+    /**
+     * Sets an index columns free list root page id.
+     *
+     * @param checkpointId Checkpoint id.
+     * @param indexColumnsFreeListRootPageId Index columns free list root page 
id.
+     */
+    public void indexColumnsFreeListRootPageId(@Nullable UUID checkpointId, 
long indexColumnsFreeListRootPageId) {
+        updateSnapshot(checkpointId);
+
+        this.indexColumnsFreeListRootPageId = indexColumnsFreeListRootPageId;
+    }
+
+    /**
+     * Returns index meta tree meta page id.
+     */
+    public long indexTreeMetaPageId() {
+        return indexTreeMetaPageId;
+    }
+
+    /**
+     * Sets an index meta tree meta page id.
+     *
+     * @param checkpointId Checkpoint id.
+     * @param indexTreeMetaPageId Index meta tree meta page id.
+     */
+    public void indexTreeMetaPageId(@Nullable UUID checkpointId, long 
indexTreeMetaPageId) {
+        updateSnapshot(checkpointId);
+
+        this.indexTreeMetaPageId = indexTreeMetaPageId;
+    }
+
     /**
      * Returns count of pages in the partition.
      */
@@ -219,6 +269,10 @@ public class PartitionMeta {
 
         private final long rowVersionFreeListRootPageId;
 
+        private final long indexColumnsFreeListRootPageId;
+
+        private final long indexTreeMetaPageId;
+
         private final int pageCount;
 
         /**
@@ -229,10 +283,12 @@ public class PartitionMeta {
          */
         private PartitionMetaSnapshot(@Nullable UUID checkpointId, 
PartitionMeta partitionMeta) {
             this.checkpointId = checkpointId;
-            this.lastAppliedIndex = partitionMeta.lastAppliedIndex;
-            this.versionChainTreeRootPageId = 
partitionMeta.versionChainTreeRootPageId;
-            this.rowVersionFreeListRootPageId = 
partitionMeta.rowVersionFreeListRootPageId;
-            this.pageCount = partitionMeta.pageCount;
+            lastAppliedIndex = partitionMeta.lastAppliedIndex;
+            versionChainTreeRootPageId = 
partitionMeta.versionChainTreeRootPageId;
+            rowVersionFreeListRootPageId = 
partitionMeta.rowVersionFreeListRootPageId;
+            indexColumnsFreeListRootPageId = 
partitionMeta.indexColumnsFreeListRootPageId;
+            indexTreeMetaPageId = partitionMeta.indexTreeMetaPageId;
+            pageCount = partitionMeta.pageCount;
         }
 
         /**
@@ -256,6 +312,20 @@ public class PartitionMeta {
             return rowVersionFreeListRootPageId;
         }
 
+        /**
+         * Returns index columns free list root page ID.
+         */
+        public long indexColumnsFreeListRootPageId() {
+            return indexColumnsFreeListRootPageId;
+        }
+
+        /**
+         * Returns index meta tree meta page ID.
+         */
+        public long indexTreeMetaPageId() {
+            return indexTreeMetaPageId;
+        }
+
         /**
          * Returns count of pages in the partition.
          */
@@ -272,8 +342,10 @@ public class PartitionMeta {
         void writeTo(PartitionMetaIo metaIo, long pageAddr) {
             metaIo.setLastAppliedIndex(pageAddr, lastAppliedIndex);
             metaIo.setVersionChainTreeRootPageId(pageAddr, 
versionChainTreeRootPageId);
+            metaIo.setIndexColumnsFreeListRootPageId(pageAddr, 
indexColumnsFreeListRootPageId);
             metaIo.setRowVersionFreeListRootPageId(pageAddr, 
rowVersionFreeListRootPageId);
             metaIo.setPageCount(pageAddr, pageCount);
+            metaIo.setIndexTreeMetaPageId(pageAddr, indexTreeMetaPageId);
         }
 
         /** {@inheritDoc} */
diff --git 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
index d2cf8e3af1..e0e8e8bd77 100644
--- 
a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
+++ 
b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
@@ -33,11 +33,15 @@ import org.apache.ignite.lang.IgniteStringBuilder;
 public class PartitionMetaIo extends PageIo {
     private static final int LAST_APPLIED_INDEX_OFF = COMMON_HEADER_END;
 
-    private static final int VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF = 
LAST_APPLIED_INDEX_OFF + Long.BYTES;
+    private static final int ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF = 
LAST_APPLIED_INDEX_OFF + Long.BYTES;
 
-    private static final int ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF = 
VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF + Long.BYTES;
+    private static final int INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF = 
ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
 
-    private static final int PAGE_COUNT_OFF = 
ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
+    private static final int VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF = 
INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
+
+    public static final int INDEX_TREE_META_PAGE_ID_OFF = 
VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF + Long.BYTES;
+
+    private static final int PAGE_COUNT_OFF = INDEX_TREE_META_PAGE_ID_OFF + 
Long.BYTES;
 
     /** Page IO type. */
     public static final short T_TABLE_PARTITION_META_IO = 7;
@@ -60,8 +64,10 @@ public class PartitionMetaIo extends PageIo {
         super.initNewPage(pageAddr, pageId, pageSize);
 
         setLastAppliedIndex(pageAddr, 0);
-        setVersionChainTreeRootPageId(pageAddr, 0);
         setRowVersionFreeListRootPageId(pageAddr, 0);
+        setIndexColumnsFreeListRootPageId(pageAddr, 0);
+        setVersionChainTreeRootPageId(pageAddr, 0);
+        setIndexTreeMetaPageId(pageAddr, 0);
         setPageCount(pageAddr, 0);
     }
 
@@ -86,6 +92,48 @@ public class PartitionMetaIo extends PageIo {
         return getLong(pageAddr, LAST_APPLIED_INDEX_OFF);
     }
 
+    /**
+     * Sets row version free list root page ID.
+     *
+     * @param pageAddr Page address.
+     * @param pageId Row version free list root page ID.
+     */
+    public void setRowVersionFreeListRootPageId(long pageAddr, long pageId) {
+        assertPageType(pageAddr);
+
+        putLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
+    }
+
+    /**
+     * Returns row version free list root page ID.
+     *
+     * @param pageAddr Page address.
+     */
+    public long getRowVersionFreeListRootPageId(long pageAddr) {
+        return getLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF);
+    }
+
+    /**
+     * Sets an index columns free list root page id.
+     *
+     * @param pageAddr Page address.
+     * @param pageId Root page id.
+     */
+    public void setIndexColumnsFreeListRootPageId(long pageAddr, long pageId) {
+        assertPageType(pageAddr);
+
+        putLong(pageAddr, INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
+    }
+
+    /**
+     * Returns an index columns free list root page id.
+     *
+     * @param pageAddr Page address.
+     */
+    public long getIndexColumnsFreeListRootPageId(long pageAddr) {
+        return getLong(pageAddr, INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF);
+    }
+
     /**
      * Sets version chain tree root page ID.
      *
@@ -108,24 +156,24 @@ public class PartitionMetaIo extends PageIo {
     }
 
     /**
-     * Sets row version free list root page ID.
+     * Sets an index meta tree meta page id.
      *
      * @param pageAddr Page address.
-     * @param pageId Row version free list root page ID.
+     * @param pageId Meta page id.
      */
-    public void setRowVersionFreeListRootPageId(long pageAddr, long pageId) {
+    public void setIndexTreeMetaPageId(long pageAddr, long pageId) {
         assertPageType(pageAddr);
 
-        putLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
+        putLong(pageAddr, INDEX_TREE_META_PAGE_ID_OFF, pageId);
     }
 
     /**
-     * Returns row version free list root page ID.
+     * Returns an index meta tree meta page id.
      *
      * @param pageAddr Page address.
      */
-    public long getRowVersionFreeListRootPageId(long pageAddr) {
-        return getLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF);
+    public long getIndexTreeMetaPageId(long pageAddr) {
+        return getLong(pageAddr, INDEX_TREE_META_PAGE_ID_OFF);
     }
 
     /**
@@ -154,8 +202,10 @@ public class PartitionMetaIo extends PageIo {
     protected void printPage(long addr, int pageSize, IgniteStringBuilder sb) {
         sb.app("TablePartitionMeta [").nl()
                 .app("lastAppliedIndex=").app(getLastAppliedIndex(addr)).nl()
-                .app(", 
versionChainTreeRootPageId=").appendHex(getVersionChainTreeRootPageId(addr)).nl()
                 .app(", 
rowVersionFreeListRootPageId=").appendHex(getRowVersionFreeListRootPageId(addr)).nl()
+                .app(", 
indexColumnsFreeListRootPageId(=").appendHex(getIndexColumnsFreeListRootPageId(addr)).nl()
+                .app(", 
versionChainTreeRootPageId=").appendHex(getVersionChainTreeRootPageId(addr)).nl()
+                .app(", 
indexTreeMetaPageId=").appendHex(getIndexTreeMetaPageId(addr)).nl()
                 .app(", pageCount=").app(getPageCount(addr)).nl()
                 .app(']');
     }
diff --git 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
index 79a6db2612..5da0648fff 100644
--- 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
+++ 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
@@ -133,7 +133,7 @@ public class PartitionMetaManagerTest {
             try (FilePageStore filePageStore = 
createFilePageStore(testFilePath)) {
                 manager.writeMetaToBuffer(
                         partId,
-                        new PartitionMeta(UUID.randomUUID(), 100, 300, 900, 
4).metaSnapshot(null),
+                        new PartitionMeta(UUID.randomUUID(), 100, 900, 500, 
300, 200, 4).metaSnapshot(null),
                         buffer.rewind()
                 );
 
@@ -149,8 +149,10 @@ public class PartitionMetaManagerTest {
                 PartitionMeta meta = manager.readOrCreateMeta(null, partId, 
filePageStore);
 
                 assertEquals(100, meta.lastAppliedIndex());
-                assertEquals(300, meta.versionChainTreeRootPageId());
                 assertEquals(900, meta.rowVersionFreeListRootPageId());
+                assertEquals(500, meta.indexColumnsFreeListRootPageId());
+                assertEquals(300, meta.versionChainTreeRootPageId());
+                assertEquals(200, meta.indexTreeMetaPageId());
                 assertEquals(4, meta.pageCount());
             }
 
diff --git 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
index cf203d6e36..7d12c08e57 100644
--- 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
+++ 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
@@ -99,7 +99,7 @@ public class PartitionMetaTest {
     void testSnapshot() {
         UUID checkpointId = null;
 
-        PartitionMeta meta = new PartitionMeta(checkpointId, 0, 0, 0, 0);
+        PartitionMeta meta = new PartitionMeta(checkpointId, 0, 0, 0, 0, 0, 0);
 
         checkSnapshot(meta.metaSnapshot(checkpointId), 0, 0, 0, 0);
         checkSnapshot(meta.metaSnapshot(checkpointId = UUID.randomUUID()), 0, 
0, 0, 0);
diff --git 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
index 466ec92408..f3874ad9b0 100644
--- 
a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
+++ 
b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
@@ -356,7 +356,7 @@ public class CheckpointerTest {
 
         partitionMetaManager.addMeta(
                 new GroupPartitionId(0, 0),
-                new PartitionMeta(null, 0, 0, 0, 3)
+                new PartitionMeta(null, 0, 0, 0, 0, 0, 3)
         );
 
         FilePageStore filePageStore = mock(FilePageStore.class);
diff --git 
a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/HashIndexStorage.java
 
b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/HashIndexStorage.java
index 64f6a209e3..be48a8e027 100644
--- 
a/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/HashIndexStorage.java
+++ 
b/modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/HashIndexStorage.java
@@ -17,8 +17,11 @@
 
 package org.apache.ignite.internal.storage.index;
 
+import java.util.UUID;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.storage.RowId;
+import org.apache.ignite.internal.storage.StorageException;
+import org.apache.ignite.internal.storage.engine.MvTableStorage;
 import org.apache.ignite.internal.util.Cursor;
 
 /**
@@ -37,16 +40,20 @@ public interface HashIndexStorage {
 
     /**
      * Returns a cursor over {@code RowId}s associated with the given index 
key.
+     *
+     * @throws StorageException If failed to read data.
      */
-    Cursor<RowId> get(BinaryTuple key);
+    Cursor<RowId> get(BinaryTuple key) throws StorageException;
 
     /**
      * Adds the given index row to the index.
      *
      * <p>Usage note: this method <b>must</b> always be called inside the 
corresponding partition's
      * {@link 
org.apache.ignite.internal.storage.MvPartitionStorage#runConsistently} closure.
+     *
+     * @throws StorageException If failed to put data.
      */
-    void put(IndexRow row);
+    void put(IndexRow row) throws StorageException;
 
     /**
      * Removes the given row from the index.
@@ -55,11 +62,17 @@ public interface HashIndexStorage {
      *
      * <p>Usage note: this method <b>must</b> always be called inside the 
corresponding partition's
      * {@link 
org.apache.ignite.internal.storage.MvPartitionStorage#runConsistently} closure.
+     *
+     * @throws StorageException If failed to remove data.
      */
-    void remove(IndexRow row);
+    void remove(IndexRow row) throws StorageException;
 
     /**
      * Removes all data from this index.
+     *
+     * @throws StorageException If failed to destory index.
+     * @deprecated IGNITE-17626 Synchronous API should be removed. {@link 
MvTableStorage#destroyIndex(UUID)} must be the only public option.
      */
-    void destroy();
+    @Deprecated
+    void destroy() throws StorageException;
 }
diff --git 
a/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/chm/TestConcurrentHashMapMvTableStorage.java
 
b/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/chm/TestConcurrentHashMapMvTableStorage.java
index fa877e67a3..241026779b 100644
--- 
a/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/chm/TestConcurrentHashMapMvTableStorage.java
+++ 
b/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/chm/TestConcurrentHashMapMvTableStorage.java
@@ -137,7 +137,12 @@ public class TestConcurrentHashMapMvTableStorage 
implements MvTableStorage {
     @Override
     public CompletableFuture<Void> destroyIndex(UUID indexId) {
         sortedIndicesById.remove(indexId);
-        hashIndicesById.remove(indexId);
+
+        HashIndices hashIndex = hashIndicesById.remove(indexId);
+
+        if (hashIndex != null) {
+            
hashIndex.storageByPartitionId.values().forEach(HashIndexStorage::destroy);
+        }
 
         return CompletableFuture.completedFuture(null);
     }
diff --git 
a/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/index/AbstractHashIndexStorageTest.java
 
b/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/index/AbstractHashIndexStorageTest.java
index 6e33696ee5..11f95d5106 100644
--- 
a/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/index/AbstractHashIndexStorageTest.java
+++ 
b/modules/storage-api/src/test/java/org/apache/ignite/internal/storage/index/AbstractHashIndexStorageTest.java
@@ -55,6 +55,8 @@ public abstract class AbstractHashIndexStorageTest {
 
     private static final String STR_COLUMN_NAME = "strVal";
 
+    private MvTableStorage tableStorage;
+
     private MvPartitionStorage partitionStorage;
 
     private HashIndexStorage indexStorage;
@@ -62,6 +64,8 @@ public abstract class AbstractHashIndexStorageTest {
     private BinaryTupleRowSerializer serializer;
 
     protected void initialize(MvTableStorage tableStorage) {
+        this.tableStorage = tableStorage;
+
         createTestTable(tableStorage.configuration());
 
         this.partitionStorage = 
tableStorage.getOrCreateMvPartition(TEST_PARTITION);
@@ -114,7 +118,7 @@ public abstract class AbstractHashIndexStorageTest {
      * Tests the {@link HashIndexStorage#get} method.
      */
     @Test
-    void testGet() {
+    public void testGet() {
         // First two rows have the same index key, but different row IDs
         IndexRow row1 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
         IndexRow row2 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
@@ -137,7 +141,7 @@ public abstract class AbstractHashIndexStorageTest {
      * Tests that {@link HashIndexStorage#put} does not create row ID 
duplicates.
      */
     @Test
-    void testPutIdempotence() {
+    public void testPutIdempotence() {
         IndexRow row = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
 
         put(row);
@@ -150,7 +154,7 @@ public abstract class AbstractHashIndexStorageTest {
      * Tests the {@link HashIndexStorage#remove} method.
      */
     @Test
-    void testRemove() {
+    public void testRemove() {
         IndexRow row1 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
         IndexRow row2 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
         IndexRow row3 = serializer.serializeRow(new Object[]{ 2, "bar" }, new 
RowId(TEST_PARTITION));
@@ -186,7 +190,7 @@ public abstract class AbstractHashIndexStorageTest {
      * Tests that {@link HashIndexStorage#remove} works normally when removing 
a non-existent row.
      */
     @Test
-    void testRemoveIdempotence() {
+    public void testRemoveIdempotence() {
         IndexRow row = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
 
         assertDoesNotThrow(() -> remove(row));
@@ -201,7 +205,7 @@ public abstract class AbstractHashIndexStorageTest {
     }
 
     @Test
-    void testDestroy() {
+    public void testDestroy() throws Exception {
         IndexRow row1 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
         IndexRow row2 = serializer.serializeRow(new Object[]{ 1, "foo" }, new 
RowId(TEST_PARTITION));
         IndexRow row3 = serializer.serializeRow(new Object[]{ 2, "bar" }, new 
RowId(TEST_PARTITION));
@@ -210,13 +214,27 @@ public abstract class AbstractHashIndexStorageTest {
         put(row2);
         put(row3);
 
-        indexStorage.destroy();
+        CompletableFuture<Void> destroyFuture = 
tableStorage.destroyIndex(indexStorage.indexDescriptor().id());
 
+        waitForDurableCompletion(destroyFuture);
+
+        //TODO IGNITE-17626 Index must be invalid, we should assert that 
getIndex returns null and that in won't surface upon restart.
+        // "destroy" is not "clear", you know. Maybe "getAndCreateIndex" will 
do it for the test, idk
         assertThat(getAll(row1), is(empty()));
         assertThat(getAll(row2), is(empty()));
         assertThat(getAll(row3), is(empty()));
     }
 
+    private void waitForDurableCompletion(CompletableFuture<?> future) {
+        while (true) {
+            if (future.isDone()) {
+                return;
+            }
+
+            partitionStorage.flush().join();
+        }
+    }
+
     private Collection<RowId> getAll(IndexRow row) {
         try (Cursor<RowId> cursor = indexStorage.get(row.indexColumns())) {
             return cursor.stream().collect(toList());
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/AbstractPageMemoryTableStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/AbstractPageMemoryTableStorage.java
index 80a002e6ce..6474da7ae5 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/AbstractPageMemoryTableStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/AbstractPageMemoryTableStorage.java
@@ -18,14 +18,13 @@
 package org.apache.ignite.internal.storage.pagememory;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.ignite.configuration.schemas.table.TableConfiguration;
 import org.apache.ignite.configuration.schemas.table.TableView;
+import org.apache.ignite.internal.pagememory.DataRegion;
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.storage.MvPartitionStorage;
 import org.apache.ignite.internal.storage.StorageException;
@@ -44,9 +43,6 @@ import org.jetbrains.annotations.Nullable;
 public abstract class AbstractPageMemoryTableStorage implements MvTableStorage 
{
     protected final TableConfiguration tableCfg;
 
-    /** List of objects to be closed on the {@link #stop}. */
-    protected final List<AutoCloseable> autoCloseables = new 
CopyOnWriteArrayList<>();
-
     protected volatile boolean started;
 
     protected volatile 
AtomicReferenceArray<AbstractPageMemoryMvPartitionStorage> mvPartitions;
@@ -66,6 +62,11 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
         return tableCfg;
     }
 
+    /**
+     * Returns a data region instance for the table.
+     */
+    public abstract DataRegion<?> dataRegion();
+
     /** {@inheritDoc} */
     @Override
     public void start() throws StorageException {
@@ -92,7 +93,7 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
 
     /** {@inheritDoc} */
     @Override
-    public MvPartitionStorage getOrCreateMvPartition(int partitionId) throws 
StorageException {
+    public AbstractPageMemoryMvPartitionStorage getOrCreateMvPartition(int 
partitionId) throws StorageException {
         AbstractPageMemoryMvPartitionStorage partition = 
getMvPartition(partitionId);
 
         if (partition != null) {
@@ -101,6 +102,8 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
 
         partition = createMvPartitionStorage(partitionId);
 
+        partition.start();
+
         mvPartitions.set(partitionId, partition);
 
         return partition;
@@ -149,7 +152,7 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
 
     @Override
     public HashIndexStorage getOrCreateHashIndex(int partitionId, UUID 
indexId) {
-        throw new UnsupportedOperationException("Not implemented yet");
+        return 
getOrCreateMvPartition(partitionId).getOrCreateHashIndex(indexId);
     }
 
     /** {@inheritDoc} */
@@ -159,7 +162,7 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
     }
 
     /**
-     * Closes all {@link #mvPartitions} and {@link #autoCloseables}.
+     * Closes all {@link #mvPartitions}.
      *
      * @param destroy Destroy partitions.
      * @throws StorageException If failed.
@@ -167,24 +170,20 @@ public abstract class AbstractPageMemoryTableStorage 
implements MvTableStorage {
     protected void close(boolean destroy) throws StorageException {
         started = false;
 
-        List<AutoCloseable> autoCloseables = new 
ArrayList<>(this.autoCloseables);
+        List<AutoCloseable> closeables = new ArrayList<>();
 
         for (int i = 0; i < mvPartitions.length(); i++) {
             AbstractPageMemoryMvPartitionStorage partition = 
mvPartitions.getAndUpdate(i, p -> null);
 
             if (partition != null) {
-                autoCloseables.add(destroy ? partition::destroy : partition);
+                closeables.add(destroy ? partition::destroy : partition);
             }
         }
 
-        Collections.reverse(autoCloseables);
-
         try {
-            IgniteUtils.closeAll(autoCloseables);
+            IgniteUtils.closeAll(closeables);
         } catch (Exception e) {
             throw new StorageException("Failed to stop PageMemory table 
storage.", e);
         }
-
-        this.autoCloseables.clear();
     }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
index be4647428f..58a949f5ce 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
@@ -35,11 +35,13 @@ import 
org.apache.ignite.internal.pagememory.persistence.store.FilePageStore;
 import org.apache.ignite.internal.pagememory.reuse.ReuseList;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import 
org.apache.ignite.internal.storage.pagememory.mv.PersistentPageMemoryMvPartitionStorage;
 import org.apache.ignite.internal.storage.pagememory.mv.RowVersionFreeList;
-import org.apache.ignite.internal.storage.pagememory.mv.VersionChain;
 import org.apache.ignite.internal.storage.pagememory.mv.VersionChainTree;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link AbstractPageMemoryTableStorage} for persistent 
case.
@@ -76,6 +78,11 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
         return engine;
     }
 
+    @Override
+    public PersistentPageMemoryDataRegion dataRegion() {
+        return dataRegion;
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean isVolatile() {
@@ -122,16 +129,12 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
         checkpointTimeoutLock.checkpointReadLock();
 
         try {
-            PersistentPageMemory persistentPageMemory = 
dataRegion.pageMemory();
+            PersistentPageMemory pageMemory = dataRegion.pageMemory();
 
             int grpId = tableView.tableId();
 
-            CheckpointProgress lastCheckpointProgress = 
checkpointManager.lastCheckpointProgress();
-
-            UUID checkpointId = lastCheckpointProgress == null ? null : 
lastCheckpointProgress.id();
-
             PartitionMeta meta = 
dataRegion.partitionMetaManager().readOrCreateMeta(
-                    checkpointId,
+                    lastCheckpointId(),
                     new GroupPartitionId(grpId, partitionId),
                     filePageStore
             );
@@ -143,53 +146,26 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
             filePageStore.setPageAllocationListener(pageIdx -> {
                 assert checkpointTimeoutLock.checkpointLockIsHeldByThread();
 
-                CheckpointProgress last = 
checkpointManager.lastCheckpointProgress();
-
-                meta.incrementPageCount(last == null ? null : last.id());
+                meta.incrementPageCount(lastCheckpointId());
             });
 
-            boolean initNewVersionChainTree = false;
+            RowVersionFreeList rowVersionFreeList = 
createRowVersionFreeList(tableView, partitionId, pageMemory, meta);
 
-            if (meta.versionChainTreeRootPageId() == 0) {
-                meta.versionChainTreeRootPageId(checkpointId, 
persistentPageMemory.allocatePage(grpId, partitionId, FLAG_AUX));
+            IndexColumnsFreeList indexColumnsFreeList
+                    = createIndexColumnsFreeList(tableView, partitionId, 
rowVersionFreeList, pageMemory, meta);
 
-                initNewVersionChainTree = true;
-            }
-
-            boolean initRowVersionFreeList = false;
-
-            if (meta.rowVersionFreeListRootPageId() == 0) {
-                meta.rowVersionFreeListRootPageId(checkpointId, 
persistentPageMemory.allocatePage(grpId, partitionId, FLAG_AUX));
+            VersionChainTree versionChainTree = 
createVersionChainTree(tableView, partitionId, rowVersionFreeList, pageMemory, 
meta);
 
-                initRowVersionFreeList = true;
-            }
-
-            RowVersionFreeList rowVersionFreeList = createRowVersionFreeList(
-                    tableView,
-                    partitionId,
-                    meta.rowVersionFreeListRootPageId(),
-                    initRowVersionFreeList
-            );
-
-            autoCloseables.add(rowVersionFreeList::close);
-
-            VersionChainTree versionChainTree = createVersionChainTree(
-                    tableView,
-                    partitionId,
-                    rowVersionFreeList,
-                    meta.versionChainTreeRootPageId(),
-                    initNewVersionChainTree
-            );
+            IndexMetaTree indexMetaTree = createIndexMetaTree(tableView, 
partitionId, rowVersionFreeList, pageMemory, meta);
 
             return new PersistentPageMemoryMvPartitionStorage(
                     this,
                     partitionId,
-                    tableView,
-                    dataRegion,
-                    checkpointManager,
                     meta,
                     rowVersionFreeList,
-                    versionChainTree
+                    indexColumnsFreeList,
+                    versionChainTree,
+                    indexMetaTree
             );
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException(
@@ -224,29 +200,48 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
         }
     }
 
+    /**
+     * Returns id of the last started checkpoint, or {@code null} if no 
checkpoints were started yet.
+     */
+    public @Nullable UUID lastCheckpointId() {
+        CheckpointProgress lastCeckpointProgress = 
dataRegion.checkpointManager().lastCheckpointProgress();
+
+        return lastCeckpointProgress == null ? null : 
lastCeckpointProgress.id();
+    }
+
     /**
      * Returns new {@link RowVersionFreeList} instance for partition.
      *
      * @param tableView Table configuration.
      * @param partId Partition ID.
-     * @param rootPageId Root page ID.
-     * @param initNew {@code True} if new metadata should be initialized.
+     * @param pageMemory Persistent page memory instance.
+     * @param meta Partition metadata.
      * @throws StorageException If failed.
      */
     private RowVersionFreeList createRowVersionFreeList(
             TableView tableView,
             int partId,
-            long rootPageId,
-            boolean initNew
+            PersistentPageMemory pageMemory,
+            PartitionMeta meta
     ) throws StorageException {
         try {
+            boolean initNew = false;
+
+            if (meta.rowVersionFreeListRootPageId() == 0) {
+                long rootPageId = pageMemory.allocatePage(tableView.tableId(), 
partId, FLAG_AUX);
+
+                meta.rowVersionFreeListRootPageId(lastCheckpointId(), 
rootPageId);
+
+                initNew = true;
+            }
+
             return new RowVersionFreeList(
                     tableView.tableId(),
                     partId,
                     dataRegion.pageMemory(),
                     null,
                     PageLockListenerNoOp.INSTANCE,
-                    rootPageId,
+                    meta.rowVersionFreeListRootPageId(),
                     initNew,
                     dataRegion.pageListCacheLimit(),
                     PageEvictionTrackerNoOp.INSTANCE,
@@ -260,36 +255,92 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
         }
     }
 
+    /**
+     * Returns new {@link IndexColumnsFreeList} instance for partition.
+     *
+     * @param tableView Table configuration.
+     * @param partitionId Partition ID.
+     * @param reuseList Reuse list.
+     * @param pageMemory Persistent page memory instance.
+     * @param meta Partition metadata.
+     * @throws StorageException If failed.
+     */
+    private IndexColumnsFreeList createIndexColumnsFreeList(
+            TableView tableView,
+            int partitionId,
+            ReuseList reuseList,
+            PersistentPageMemory pageMemory,
+            PartitionMeta meta
+    ) {
+        try {
+            boolean initNew = false;
+
+            if (meta.indexColumnsFreeListRootPageId() == 0L) {
+                long rootPageId = pageMemory.allocatePage(tableView.tableId(), 
partitionId, FLAG_AUX);
+
+                meta.indexColumnsFreeListRootPageId(lastCheckpointId(), 
rootPageId);
+
+                initNew = true;
+            }
+
+            return new IndexColumnsFreeList(
+                    tableView.tableId(),
+                    partitionId,
+                    pageMemory,
+                    reuseList,
+                    PageLockListenerNoOp.INSTANCE,
+                    meta.indexColumnsFreeListRootPageId(),
+                    initNew,
+                    new AtomicLong(),
+                    PageEvictionTrackerNoOp.INSTANCE,
+                    IoStatisticsHolderNoOp.INSTANCE
+            );
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException(
+                    String.format("Error creating IndexColumnsFreeList 
[tableName=%s, partitionId=%s]", tableView.name(), partitionId),
+                    e
+            );
+        }
+    }
+
     /**
      * Returns new {@link VersionChainTree} instance for partition.
      *
      * @param tableView Table configuration.
      * @param partId Partition ID.
-     * @param freeList {@link VersionChain} free list.
-     * @param rootPageId Root page ID.
-     * @param initNewTree {@code True} if new tree should be created.
+     * @param reuseList Reuse list.
+     * @param pageMemory Persistent page memory instance.
+     * @param meta Partition metadata.
      * @throws StorageException If failed.
      */
     private VersionChainTree createVersionChainTree(
             TableView tableView,
             int partId,
-            ReuseList freeList,
-            long rootPageId,
-            boolean initNewTree
+            ReuseList reuseList,
+            PersistentPageMemory pageMemory,
+            PartitionMeta meta
     ) throws StorageException {
-        int grpId = tableView.tableId();
-
         try {
+            boolean initNew = false;
+
+            if (meta.versionChainTreeRootPageId() == 0) {
+                long rootPageId = pageMemory.allocatePage(tableView.tableId(), 
partId, FLAG_AUX);
+
+                meta.versionChainTreeRootPageId(lastCheckpointId(), 
rootPageId);
+
+                initNew = true;
+            }
+
             return new VersionChainTree(
-                    grpId,
+                    tableView.tableId(),
                     tableView.name(),
                     partId,
                     dataRegion.pageMemory(),
                     PageLockListenerNoOp.INSTANCE,
                     new AtomicLong(),
-                    rootPageId,
-                    freeList,
-                    initNewTree
+                    meta.versionChainTreeRootPageId(),
+                    reuseList,
+                    initNew
             );
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException(
@@ -298,4 +349,51 @@ public class PersistentPageMemoryTableStorage extends 
AbstractPageMemoryTableSto
             );
         }
     }
+
+    /**
+     * Returns new {@link IndexMetaTree} instance for partition.
+     *
+     * @param tableView Table configuration.
+     * @param partitionId Partition ID.
+     * @param reuseList Reuse list.
+     * @param pageMemory Persistent page memory instance.
+     * @param meta Partition metadata.
+     * @throws StorageException If failed.
+     */
+    private IndexMetaTree createIndexMetaTree(
+            TableView tableView,
+            int partitionId,
+            ReuseList reuseList,
+            PersistentPageMemory pageMemory,
+            PartitionMeta meta
+    ) {
+        try {
+            boolean initNew = false;
+
+            if (meta.indexTreeMetaPageId() == 0) {
+                long rootPageId = pageMemory.allocatePage(tableView.tableId(), 
partitionId, FLAG_AUX);
+
+                meta.indexTreeMetaPageId(lastCheckpointId(), rootPageId);
+
+                initNew = true;
+            }
+
+            return new IndexMetaTree(
+                    tableView.tableId(),
+                    tableView.name(),
+                    partitionId,
+                    dataRegion.pageMemory(),
+                    PageLockListenerNoOp.INSTANCE,
+                    new AtomicLong(),
+                    meta.indexTreeMetaPageId(),
+                    reuseList,
+                    initNew
+            );
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException(
+                    String.format("Error creating IndexMetaTree [tableName=%s, 
partitionId=%s]", tableView.name(), partitionId),
+                    e
+            );
+        }
+    }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
index 26f7fd866b..19eaa17c03 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
@@ -30,9 +30,9 @@ import 
org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.reuse.ReuseList;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.mv.RowVersionFreeList;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Implementation of {@link DataRegion} for in-memory case.
@@ -52,6 +52,8 @@ public class VolatilePageMemoryDataRegion implements 
DataRegion<VolatilePageMemo
 
     private volatile RowVersionFreeList rowVersionFreeList;
 
+    private volatile IndexColumnsFreeList indexColumnsFreeList;
+
     /**
      * Constructor.
      *
@@ -79,7 +81,9 @@ public class VolatilePageMemoryDataRegion implements 
DataRegion<VolatilePageMemo
         pageMemory.start();
 
         try {
-            rowVersionFreeList = createRowVersionFreeList(pageMemory, null);
+            rowVersionFreeList = createRowVersionFreeList(pageMemory);
+
+            indexColumnsFreeList = createIndexColumnsFreeList(pageMemory, 
rowVersionFreeList);
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException("Error creating a RowVersionFreeList", 
e);
         }
@@ -88,12 +92,30 @@ public class VolatilePageMemoryDataRegion implements 
DataRegion<VolatilePageMemo
     }
 
     private static RowVersionFreeList createRowVersionFreeList(
-            PageMemory pageMemory,
-            @Nullable ReuseList reuseList
+            PageMemory pageMemory
     ) throws IgniteInternalCheckedException {
         long metaPageId = pageMemory.allocatePage(FREE_LIST_GROUP_ID, 
FREE_LIST_PARTITION_ID, FLAG_AUX);
 
         return new RowVersionFreeList(
+                FREE_LIST_GROUP_ID,
+                FREE_LIST_PARTITION_ID,
+                pageMemory,
+                null,
+                PageLockListenerNoOp.INSTANCE,
+                metaPageId,
+                true,
+                // Because in memory.
+                null,
+                PageEvictionTrackerNoOp.INSTANCE,
+                IoStatisticsHolderNoOp.INSTANCE
+        );
+    }
+
+    private IndexColumnsFreeList createIndexColumnsFreeList(VolatilePageMemory 
pageMemory, ReuseList reuseList)
+            throws IgniteInternalCheckedException {
+        long metaPageId = pageMemory.allocatePage(FREE_LIST_GROUP_ID, 
FREE_LIST_PARTITION_ID, FLAG_AUX);
+
+        return new IndexColumnsFreeList(
                 FREE_LIST_GROUP_ID,
                 FREE_LIST_PARTITION_ID,
                 pageMemory,
@@ -126,6 +148,10 @@ public class VolatilePageMemoryDataRegion implements 
DataRegion<VolatilePageMemo
         return pageMemory;
     }
 
+    public ReuseList reuseList() {
+        return rowVersionFreeList();
+    }
+
     /**
      * Returns version chain free list.
      *
@@ -137,6 +163,10 @@ public class VolatilePageMemoryDataRegion implements 
DataRegion<VolatilePageMemo
         return rowVersionFreeList;
     }
 
+    public IndexColumnsFreeList indexColumnsFreeList() {
+        return indexColumnsFreeList;
+    }
+
     /**
      * Checks that the data region has started.
      *
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
index f1f33a4632..3d5bb16c96 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
@@ -24,6 +24,7 @@ import 
org.apache.ignite.configuration.schemas.table.TableConfiguration;
 import org.apache.ignite.configuration.schemas.table.TableView;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.storage.pagememory.mv.VersionChainTree;
 import 
org.apache.ignite.internal.storage.pagememory.mv.VolatilePageMemoryMvPartitionStorage;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
@@ -32,7 +33,7 @@ import org.apache.ignite.lang.IgniteInternalCheckedException;
  * Implementation of {@link AbstractPageMemoryTableStorage} for in-memory case.
  */
 public class VolatilePageMemoryTableStorage extends 
AbstractPageMemoryTableStorage {
-    private VolatilePageMemoryDataRegion dataRegion;
+    private final VolatilePageMemoryDataRegion dataRegion;
 
     /**
      * Constructor.
@@ -46,20 +47,48 @@ public class VolatilePageMemoryTableStorage extends 
AbstractPageMemoryTableStora
         this.dataRegion = dataRegion;
     }
 
+    @Override
+    public VolatilePageMemoryDataRegion dataRegion() {
+        return dataRegion;
+    }
+
     /** {@inheritDoc} */
     @Override
     public VolatilePageMemoryMvPartitionStorage createMvPartitionStorage(int 
partitionId) throws StorageException {
         VersionChainTree versionChainTree = 
createVersionChainTree(partitionId, tableCfg.value());
 
+        IndexMetaTree indexMetaTree = createIndexMetaTree(partitionId, 
tableCfg.value());
+
         return new VolatilePageMemoryMvPartitionStorage(
+                this,
                 partitionId,
-                tableCfg.value(),
-                dataRegion.pageMemory(),
-                dataRegion.rowVersionFreeList(),
-                versionChainTree
+                versionChainTree,
+                indexMetaTree
         );
     }
 
+    private IndexMetaTree createIndexMetaTree(int partitionId, TableView 
tableCfgView) {
+        int grpId = tableCfgView.tableId();
+
+        long metaPageId = dataRegion.pageMemory().allocatePage(grpId, 
partitionId, FLAG_AUX);
+
+        try {
+            return new IndexMetaTree(
+                    grpId,
+                    tableCfgView.name(),
+                    partitionId,
+                    dataRegion.pageMemory(),
+                    PageLockListenerNoOp.INSTANCE,
+                    new AtomicLong(),
+                    metaPageId,
+                    dataRegion.reuseList(),
+                    true
+            );
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException(e);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean isVolatile() {
@@ -83,6 +112,8 @@ public class VolatilePageMemoryTableStorage extends 
AbstractPageMemoryTableStora
         int grpId = tableView.tableId();
 
         try {
+            long metaPageId = dataRegion.pageMemory().allocatePage(grpId, 
partId, FLAG_AUX);
+
             return new VersionChainTree(
                     grpId,
                     tableView.name(),
@@ -90,8 +121,8 @@ public class VolatilePageMemoryTableStorage extends 
AbstractPageMemoryTableStora
                     dataRegion.pageMemory(),
                     PageLockListenerNoOp.INSTANCE,
                     new AtomicLong(),
-                    dataRegion.pageMemory().allocatePage(grpId, partId, 
FLAG_AUX),
-                    dataRegion.rowVersionFreeList(),
+                    metaPageId,
+                    dataRegion.reuseList(),
                     true
             );
         } catch (IgniteInternalCheckedException e) {
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
index b66373bcfc..713fa0330d 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
@@ -19,6 +19,7 @@ package 
org.apache.ignite.internal.storage.pagememory.index.freelist;
 
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
 import org.apache.ignite.internal.pagememory.freelist.AbstractFreeList;
@@ -32,6 +33,10 @@ import org.jetbrains.annotations.Nullable;
  * Free list implementation to store {@link IndexColumns} values.
  */
 public class IndexColumnsFreeList extends AbstractFreeList<IndexColumns>  {
+    private static final IgniteLogger LOG = 
Loggers.forClass(IndexColumnsFreeList.class);
+
+    private final IoStatisticsHolder statHolder;
+
     /**
      * Constructor.
      *
@@ -40,7 +45,6 @@ public class IndexColumnsFreeList extends 
AbstractFreeList<IndexColumns>  {
      * @param pageMem Page memory.
      * @param reuseList Reuse list or {@code null} if this free list will be a 
reuse list for itself.
      * @param lockLsnr Page lock listener.
-     * @param log Logger.
      * @param metaPageId Metadata page ID.
      * @param initNew {@code True} if new metadata should be initialized.
      * @param pageListCacheLimit Page list cache limit.
@@ -53,7 +57,6 @@ public class IndexColumnsFreeList extends 
AbstractFreeList<IndexColumns>  {
             PageMemory pageMem,
             @Nullable ReuseList reuseList,
             PageLockListener lockLsnr,
-            IgniteLogger log,
             long metaPageId,
             boolean initNew,
             @Nullable AtomicLong pageListCacheLimit,
@@ -67,11 +70,21 @@ public class IndexColumnsFreeList extends 
AbstractFreeList<IndexColumns>  {
                 pageMem,
                 reuseList,
                 lockLsnr,
-                log,
+                LOG,
                 metaPageId,
                 initNew,
                 pageListCacheLimit,
                 evictionTracker
         );
+        this.statHolder = statHolder;
+    }
+
+    /**
+     * Shortcut method for {@link #saveMetadata(IoStatisticsHolder)} with 
statistics holder.
+     *
+     * @throws IgniteInternalCheckedException If failed.
+     */
+    public void saveMetadata() throws IgniteInternalCheckedException {
+        saveMetadata(statHolder);
     }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
index 002c86b949..3e107fc4c5 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
@@ -19,6 +19,7 @@ package 
org.apache.ignite.internal.storage.pagememory.index.freelist.io;
 
 import static 
org.apache.ignite.internal.pagememory.util.PageUtils.putByteBuffer;
 import static org.apache.ignite.internal.pagememory.util.PageUtils.putInt;
+import static org.apache.ignite.internal.pagememory.util.PageUtils.putShort;
 
 import java.nio.ByteBuffer;
 import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
@@ -48,6 +49,10 @@ public class IndexColumnsDataIo extends 
AbstractDataPageIo<IndexColumns> {
     protected void writeRowData(long pageAddr, int dataOff, int payloadSize, 
IndexColumns row, boolean newRow) {
         assertPageType(pageAddr);
 
+        putShort(pageAddr, dataOff, (short) payloadSize);
+
+        dataOff += Short.BYTES;
+
         putInt(pageAddr, dataOff + IndexColumns.SIZE_OFFSET, row.valueSize());
 
         putByteBuffer(pageAddr, dataOff + IndexColumns.VALUE_OFFSET, 
row.valueBuffer());
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
index c6099ead0a..f803e89e22 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
@@ -32,7 +32,7 @@ import org.jetbrains.annotations.Nullable;
  * Insert closure that inserts corresponding {@link IndexColumns} into a 
{@link IndexColumnsFreeList} before writing to the
  * {@link HashIndexTree}.
  */
-public class InsertHashIndexRowInvokeClosure implements 
InvokeClosure<HashIndexRow> {
+class InsertHashIndexRowInvokeClosure implements InvokeClosure<HashIndexRow> {
     /** Hash index row instance for insertion. */
     private final HashIndexRow hashIndexRow;
 
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
new file mode 100644
index 0000000000..3a2ef1a6cb
--- /dev/null
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.storage.pagememory.index.hash;
+
+import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
+import org.apache.ignite.internal.schema.BinaryTuple;
+import org.apache.ignite.internal.storage.RowId;
+import org.apache.ignite.internal.storage.StorageException;
+import org.apache.ignite.internal.storage.index.HashIndexDescriptor;
+import org.apache.ignite.internal.storage.index.HashIndexStorage;
+import org.apache.ignite.internal.storage.index.IndexRow;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
+import org.apache.ignite.internal.storage.pagememory.util.TreeCursorAdapter;
+import org.apache.ignite.internal.util.Cursor;
+import org.apache.ignite.internal.util.IgniteCursor;
+import org.apache.ignite.lang.IgniteInternalCheckedException;
+
+/**
+ * Hash index storage implementation.
+ */
+public class PageMemoryHashIndexStorage implements HashIndexStorage {
+    /** Index descriptor. */
+    private final HashIndexDescriptor descriptor;
+
+    /** Free list to store index columns. */
+    private final IndexColumnsFreeList freeList;
+
+    /** Hash index tree instance. */
+    private final HashIndexTree hashIndexTree;
+
+    /** Partition id. */
+    private final int partitionId;
+
+    /** Lowest possible RowId according to signed long ordering. */
+    private final RowId lowestRowId;
+
+    /** Highest possible RowId according to signed long ordering. */
+    private final RowId highestRowId;
+
+    /**
+     * Constructor.
+     *
+     * @param descriptor Hash index descriptor.
+     * @param freeList Free list to store indx columns.
+     * @param hashIndexTree Hash index tree instance.
+     */
+    public PageMemoryHashIndexStorage(HashIndexDescriptor descriptor, 
IndexColumnsFreeList freeList, HashIndexTree hashIndexTree) {
+        this.descriptor = descriptor;
+        this.freeList = freeList;
+        this.hashIndexTree = hashIndexTree;
+
+        partitionId = hashIndexTree.partitionId();
+
+        lowestRowId = new RowId(partitionId, Long.MIN_VALUE, Long.MIN_VALUE);
+
+        highestRowId = new RowId(partitionId, Long.MAX_VALUE, Long.MAX_VALUE);
+    }
+
+    @Override
+    public HashIndexDescriptor indexDescriptor() {
+        return descriptor;
+    }
+
+    @Override
+    public Cursor<RowId> get(BinaryTuple key) throws StorageException {
+        IndexColumns indexColumns = new IndexColumns(partitionId, 
key.byteBuffer());
+
+        HashIndexRow lowerBound = new HashIndexRow(indexColumns, lowestRowId);
+        HashIndexRow upperBound = new HashIndexRow(indexColumns, highestRowId);
+
+        IgniteCursor<HashIndexRow> cursor;
+
+        try {
+            cursor = hashIndexTree.find(lowerBound, upperBound, null);
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to create scan cursor", e);
+        }
+
+        return Cursor.fromIterator(new TreeCursorAdapter<>(cursor, 
HashIndexRow::rowId));
+    }
+
+    @Override
+    public void put(IndexRow row) throws StorageException {
+        IndexColumns indexColumns = new IndexColumns(partitionId, 
row.indexColumns().byteBuffer());
+
+        try {
+            HashIndexRow hashIndexRow = new HashIndexRow(indexColumns, 
row.rowId());
+
+            var insert = new InsertHashIndexRowInvokeClosure(hashIndexRow, 
freeList, IoStatisticsHolderNoOp.INSTANCE);
+
+            hashIndexTree.invoke(hashIndexRow, null, insert);
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to put value into index", e);
+        }
+    }
+
+    @Override
+    public void remove(IndexRow row) throws StorageException {
+        IndexColumns indexColumns = new IndexColumns(partitionId, 
row.indexColumns().byteBuffer());
+
+        try {
+            HashIndexRow hashIndexRow = new HashIndexRow(indexColumns, 
row.rowId());
+
+            var remove = new RemoveHashIndexRowInvokeClosure(hashIndexRow, 
freeList, IoStatisticsHolderNoOp.INSTANCE);
+
+            hashIndexTree.invoke(hashIndexRow, null, remove);
+
+            // Performs actual deletion from freeList if necessary.
+            remove.afterCompletion();
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to remove value from index", e);
+        }
+    }
+
+    @Override
+    public void destroy() throws StorageException {
+        //TODO IGNITE-17626 Implement.
+    }
+}
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
index bdd342bc75..e4a9717315 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
@@ -33,7 +33,7 @@ import org.jetbrains.annotations.Nullable;
  * Insert closure that removes corresponding {@link IndexColumns} from a 
{@link IndexColumnsFreeList} after removing it from the
  * {@link HashIndexTree}.
  */
-public class RemoveHashIndexRowInvokeClosure implements 
InvokeClosure<HashIndexRow> {
+class RemoveHashIndexRowInvokeClosure implements InvokeClosure<HashIndexRow> {
     /** Hash index row instance for removal. */
     private final HashIndexRow hashIndexRow;
 
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/IndexMeta.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/IndexMeta.java
index cdcc22d0e0..d3019d3665 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/IndexMeta.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/IndexMeta.java
@@ -18,27 +18,28 @@
 package org.apache.ignite.internal.storage.pagememory.index.meta;
 
 import java.util.UUID;
-import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.IgniteToStringExclude;
 import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.internal.util.IgniteUtils;
 
 /**
  * Index tree meta information.
  */
 public class IndexMeta {
-    @IgniteToStringInclude
     private final UUID id;
 
-    private final long rootPageId;
+    @IgniteToStringExclude
+    private final long metaPageId;
 
     /**
      * Constructor.
      *
      * @param id Index ID.
-     * @param rootPageId Index root page ID.
+     * @param metaPageId Index tree meta page ID.
      */
-    public IndexMeta(UUID id, long rootPageId) {
+    public IndexMeta(UUID id, long metaPageId) {
         this.id = id;
-        this.rootPageId = rootPageId;
+        this.metaPageId = metaPageId;
     }
 
     /**
@@ -49,15 +50,15 @@ public class IndexMeta {
     }
 
     /**
-     * Returns the index root page ID.
+     * Returns page ID of the index tree meta page.
      */
-    public long rootPageId() {
-        return rootPageId;
+    public long metaPageId() {
+        return metaPageId;
     }
 
     /** {@inheritDoc} */
     @Override
     public String toString() {
-        return S.toString(IndexMeta.class, this);
+        return S.toString(IndexMeta.class, this, "metaPageId", 
IgniteUtils.hexLong(metaPageId));
     }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/io/IndexMetaIo.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/io/IndexMetaIo.java
index 61e8e95dd4..d7a582fa7c 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/io/IndexMetaIo.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/io/IndexMetaIo.java
@@ -41,8 +41,8 @@ public interface IndexMetaIo {
     /** Offset of the {@link UUID#getLeastSignificantBits() least significant 
bits} of the index ID (8 bytes). */
     int INDEX_ID_LSB_OFFSET = INDEX_ID_MSB_OFFSET + Long.BYTES;
 
-    /** Index root page ID offset - long (8 bytes). */
-    int INDEX_ROOT_PAGE_ID_OFFSET = INDEX_ID_LSB_OFFSET + Long.BYTES;
+    /** Index tree meta page id offset - long (8 bytes). */
+    int INDEX_TREE_META_PAGE_ID_OFFSET = INDEX_ID_LSB_OFFSET + Long.BYTES;
 
     /** Payload size in bytes. */
     int SIZE_IN_BYTES = 2 * Long.BYTES /* Index ID - {@link UUID} (16 bytes) 
*/ + Long.BYTES /* Index root page ID - long (8 bytes) */;
@@ -86,9 +86,9 @@ public interface IndexMetaIo {
         long indexIdMsb = getLong(pageAddr, elementOffset + 
INDEX_ID_MSB_OFFSET);
         long indexIdLsb = getLong(pageAddr, elementOffset + 
INDEX_ID_LSB_OFFSET);
 
-        long indexRootPageId = getLong(pageAddr, elementOffset + 
INDEX_ROOT_PAGE_ID_OFFSET);
+        long indexTreeMetaPageId = getLong(pageAddr, elementOffset + 
INDEX_TREE_META_PAGE_ID_OFFSET);
 
-        return new IndexMeta(new UUID(indexIdMsb, indexIdLsb), 
indexRootPageId);
+        return new IndexMeta(new UUID(indexIdMsb, indexIdLsb), 
indexTreeMetaPageId);
     }
 
     /**
@@ -112,6 +112,6 @@ public interface IndexMetaIo {
         putLong(pageAddr, off + INDEX_ID_MSB_OFFSET, 
row.id().getMostSignificantBits());
         putLong(pageAddr, off + INDEX_ID_LSB_OFFSET, 
row.id().getLeastSignificantBits());
 
-        putLong(pageAddr, off + INDEX_ROOT_PAGE_ID_OFFSET, row.rootPageId());
+        putLong(pageAddr, off + INDEX_TREE_META_PAGE_ID_OFFSET, 
row.metaPageId());
     }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
index 5c89a17678..9deb69241f 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
@@ -17,24 +17,43 @@
 
 package org.apache.ignite.internal.storage.pagememory.mv;
 
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.getByInternalId;
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
 
 import java.nio.ByteBuffer;
 import java.util.NoSuchElementException;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiConsumer;
 import java.util.function.Predicate;
+import org.apache.ignite.configuration.NamedListView;
+import org.apache.ignite.configuration.schemas.table.HashIndexView;
+import org.apache.ignite.configuration.schemas.table.SortedIndexView;
+import org.apache.ignite.configuration.schemas.table.TableIndexView;
 import org.apache.ignite.configuration.schemas.table.TableView;
 import org.apache.ignite.hlc.HybridTimestamp;
+import org.apache.ignite.internal.pagememory.PageIdAllocator;
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.datapage.DataPageReader;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
+import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.ByteBufferRow;
 import org.apache.ignite.internal.storage.MvPartitionStorage;
 import org.apache.ignite.internal.storage.RowId;
 import org.apache.ignite.internal.storage.StorageException;
 import org.apache.ignite.internal.storage.TxIdMismatchException;
+import org.apache.ignite.internal.storage.index.HashIndexDescriptor;
+import org.apache.ignite.internal.storage.index.HashIndexStorage;
+import 
org.apache.ignite.internal.storage.pagememory.AbstractPageMemoryTableStorage;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
+import org.apache.ignite.internal.storage.pagememory.index.hash.HashIndexTree;
+import 
org.apache.ignite.internal.storage.pagememory.index.hash.PageMemoryHashIndexStorage;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMeta;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.util.Cursor;
 import org.apache.ignite.internal.util.IgniteCursor;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
@@ -52,39 +71,134 @@ public abstract class AbstractPageMemoryMvPartitionStorage 
implements MvPartitio
 
     private static final Predicate<HybridTimestamp> ALWAYS_LOAD_VALUE = 
timestamp -> true;
 
-    private final int partitionId;
-    private final int groupId;
+    protected final int partitionId;
+
+    protected final int groupId;
+
+    protected final AbstractPageMemoryTableStorage tableStorage;
+
+    protected final VersionChainTree versionChainTree;
 
-    private final VersionChainTree versionChainTree;
     protected final RowVersionFreeList rowVersionFreeList;
-    private final DataPageReader rowVersionDataPageReader;
+
+    protected final IndexColumnsFreeList indexFreeList;
+
+    protected final IndexMetaTree indexMetaTree;
+
+    protected final DataPageReader rowVersionDataPageReader;
+
+    protected final ConcurrentMap<UUID, HashIndexStorage> indexes = new 
ConcurrentHashMap<>();
 
     /**
      * Constructor.
      *
      * @param partitionId Partition id.
-     * @param tableView Table configuration.
-     * @param pageMemory Page memory.
+     * @param tableStorage Table storage instance.
      * @param rowVersionFreeList Free list for {@link RowVersion}.
+     * @param indexFreeList Free list fot {@link IndexColumns}.
      * @param versionChainTree Table tree for {@link VersionChain}.
+     * @param indexMetaTree Tree that contains SQL indexes' metadata.
      */
     protected AbstractPageMemoryMvPartitionStorage(
             int partitionId,
-            TableView tableView,
-            PageMemory pageMemory,
+            AbstractPageMemoryTableStorage tableStorage,
             RowVersionFreeList rowVersionFreeList,
-            VersionChainTree versionChainTree
+            IndexColumnsFreeList indexFreeList,
+            VersionChainTree versionChainTree,
+            IndexMetaTree indexMetaTree
     ) {
         this.partitionId = partitionId;
+        this.tableStorage = tableStorage;
 
         this.rowVersionFreeList = rowVersionFreeList;
+        this.indexFreeList = indexFreeList;
+
         this.versionChainTree = versionChainTree;
+        this.indexMetaTree = indexMetaTree;
+
+        PageMemory pageMemory = tableStorage.dataRegion().pageMemory();
 
-        groupId = tableView.tableId();
+        groupId = tableStorage.configuration().value().tableId();
 
         rowVersionDataPageReader = new DataPageReader(pageMemory, groupId, 
IoStatisticsHolderNoOp.INSTANCE);
     }
 
+    /**
+     * Starts a partition by initializing its internal structures.
+     */
+    public void start() {
+        try {
+            IgniteCursor<IndexMeta> cursor = indexMetaTree.find(null, null);
+
+            NamedListView<? extends TableIndexView> indicesCfgView = 
tableStorage.configuration().value().indices();
+
+            while (cursor.next()) {
+                IndexMeta indexMeta = cursor.get();
+
+                TableIndexView indexCfgView = getByInternalId(indicesCfgView, 
indexMeta.id());
+
+                if (indexCfgView instanceof HashIndexView) {
+                    createOrRestoreHashIndex(indexMeta);
+                } else if (indexCfgView instanceof SortedIndexView) {
+                    throw new UnsupportedOperationException("Not implemented 
yet");
+                } else {
+                    assert indexCfgView == null;
+
+                    //TODO IGNITE-17626 Drop the index synchronously.
+                }
+            }
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to process SQL indexes during 
the partition start", e);
+        }
+    }
+
+    /**
+     * Returns a hash index instance, creating index it if necessary.
+     *
+     * @param indexId Index UUID.
+     */
+    public HashIndexStorage getOrCreateHashIndex(UUID indexId) {
+        return indexes.computeIfAbsent(indexId, uuid -> 
createOrRestoreHashIndex(new IndexMeta(indexId, 0L)));
+    }
+
+    private PageMemoryHashIndexStorage createOrRestoreHashIndex(IndexMeta 
indexMeta) {
+        TableView tableView = tableStorage.configuration().value();
+
+        var indexDescriptor = new HashIndexDescriptor(indexMeta.id(), 
tableView);
+
+        try {
+            PageMemory pageMemory = tableStorage.dataRegion().pageMemory();
+
+            boolean initNew = indexMeta.metaPageId() == 0L;
+
+            long metaPageId = initNew
+                    ? pageMemory.allocatePage(groupId, partitionId, 
PageIdAllocator.FLAG_AUX)
+                    : indexMeta.metaPageId();
+
+            HashIndexTree hashIndexTree = new HashIndexTree(
+                    groupId,
+                    tableView.name(),
+                    partitionId,
+                    pageMemory,
+                    PageLockListenerNoOp.INSTANCE,
+                    new AtomicLong(),
+                    metaPageId,
+                    rowVersionFreeList,
+                    initNew
+            );
+
+            if (initNew) {
+                boolean replaced = indexMetaTree.putx(new 
IndexMeta(indexMeta.id(), metaPageId));
+
+                assert !replaced;
+            }
+
+            return new PageMemoryHashIndexStorage(indexDescriptor, 
indexFreeList, hashIndexTree);
+        } catch (IgniteInternalCheckedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public @Nullable BinaryRow read(RowId rowId, UUID txId) throws 
TxIdMismatchException, StorageException {
@@ -392,6 +506,8 @@ public abstract class AbstractPageMemoryMvPartitionStorage 
implements MvPartitio
     @Override
     public void close() {
         versionChainTree.close();
+
+        indexMetaTree.close();
     }
 
     /**
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
index ab2cb51d48..5535c7e089 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
@@ -20,10 +20,7 @@ package org.apache.ignite.internal.storage.pagememory.mv;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
-import org.apache.ignite.configuration.schemas.table.TableView;
-import org.apache.ignite.internal.pagememory.DataRegion;
 import org.apache.ignite.internal.pagememory.persistence.PartitionMeta;
-import org.apache.ignite.internal.pagememory.persistence.PersistentPageMemory;
 import 
org.apache.ignite.internal.pagememory.persistence.checkpoint.CheckpointListener;
 import 
org.apache.ignite.internal.pagememory.persistence.checkpoint.CheckpointManager;
 import 
org.apache.ignite.internal.pagememory.persistence.checkpoint.CheckpointProgress;
@@ -32,8 +29,12 @@ import 
org.apache.ignite.internal.pagememory.persistence.checkpoint.CheckpointTi
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.storage.MvPartitionStorage;
 import org.apache.ignite.internal.storage.StorageException;
+import org.apache.ignite.internal.storage.index.HashIndexStorage;
 import 
org.apache.ignite.internal.storage.pagememory.PersistentPageMemoryTableStorage;
 import 
org.apache.ignite.internal.storage.pagememory.configuration.schema.PersistentPageMemoryStorageEngineView;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
+import 
org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.lang.IgniteInternalCheckedException;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.jetbrains.annotations.Nullable;
@@ -42,9 +43,6 @@ import org.jetbrains.annotations.Nullable;
  * Implementation of {@link MvPartitionStorage} based on a {@link BplusTree} 
for persistent case.
  */
 public class PersistentPageMemoryMvPartitionStorage extends 
AbstractPageMemoryMvPartitionStorage {
-    /** Table storage instance. */
-    private final PersistentPageMemoryTableStorage tableStorage;
-
     /** Checkpoint manager instance. */
     private final CheckpointManager checkpointManager;
 
@@ -65,28 +63,24 @@ public class PersistentPageMemoryMvPartitionStorage extends 
AbstractPageMemoryMv
      *
      * @param tableStorage Table storage.
      * @param partitionId Partition id.
-     * @param tableView Table configuration.
-     * @param dataRegion Data region.
-     * @param checkpointManager Checkpoint manager.
      * @param meta Partition meta.
      * @param rowVersionFreeList Free list for {@link RowVersion}.
+     * @param indexFreeList Free list fot {@link IndexColumns}.
      * @param versionChainTree Table tree for {@link VersionChain}.
+     * @param indexMetaTree Tree that contains SQL indexes' metadata.
      */
     public PersistentPageMemoryMvPartitionStorage(
             PersistentPageMemoryTableStorage tableStorage,
             int partitionId,
-            TableView tableView,
-            DataRegion<PersistentPageMemory> dataRegion,
-            CheckpointManager checkpointManager,
             PartitionMeta meta,
             RowVersionFreeList rowVersionFreeList,
-            VersionChainTree versionChainTree
+            IndexColumnsFreeList indexFreeList,
+            VersionChainTree versionChainTree,
+            IndexMetaTree indexMetaTree
     ) {
-        super(partitionId, tableView, dataRegion.pageMemory(), 
rowVersionFreeList, versionChainTree);
-
-        this.tableStorage = tableStorage;
+        super(partitionId, tableStorage, rowVersionFreeList, indexFreeList, 
versionChainTree, indexMetaTree);
 
-        this.checkpointManager = checkpointManager;
+        checkpointManager = tableStorage.engine().checkpointManager();
         checkpointTimeoutLock = checkpointManager.checkpointTimeoutLock();
 
         checkpointManager.addCheckpointListener(checkpointListener = new 
CheckpointListener() {
@@ -109,7 +103,7 @@ public class PersistentPageMemoryMvPartitionStorage extends 
AbstractPageMemoryMv
             public void afterCheckpointEnd(CheckpointProgress progress) {
                 persistedIndex = 
meta.metaSnapshot(progress.id()).lastAppliedIndex();
             }
-        }, dataRegion);
+        }, tableStorage.dataRegion());
 
         this.meta = meta;
     }
@@ -136,7 +130,9 @@ public class PersistentPageMemoryMvPartitionStorage extends 
AbstractPageMemoryMv
         if (lastCheckpoint != null && 
meta.metaSnapshot(lastCheckpoint.id()).lastAppliedIndex() == 
meta.lastAppliedIndex()) {
             scheduledCheckpoint = lastCheckpoint;
         } else {
-            PersistentPageMemoryStorageEngineView engineCfg = 
tableStorage.engine().configuration().value();
+            var persistentTableStorage = (PersistentPageMemoryTableStorage) 
tableStorage;
+
+            PersistentPageMemoryStorageEngineView engineCfg = 
persistentTableStorage.engine().configuration().value();
 
             int checkpointDelayMillis = 
engineCfg.checkpoint().checkpointDelayMillis();
             scheduledCheckpoint = 
checkpointManager.scheduleCheckpoint(checkpointDelayMillis, "Triggered by 
replicator");
@@ -169,10 +165,18 @@ public class PersistentPageMemoryMvPartitionStorage 
extends AbstractPageMemoryMv
         return persistedIndex;
     }
 
+    @Override
+    public HashIndexStorage getOrCreateHashIndex(UUID indexId) {
+        return runConsistently(() -> super.getOrCreateHashIndex(indexId));
+    }
+
     /** {@inheritDoc} */
     @Override
     public void close() {
         checkpointManager.removeCheckpointListener(checkpointListener);
+
+        rowVersionFreeList.close();
+        indexFreeList.close();
     }
 
     /**
@@ -184,6 +188,8 @@ public class PersistentPageMemoryMvPartitionStorage extends 
AbstractPageMemoryMv
     private void syncMetadataOnCheckpoint(@Nullable Executor executor) throws 
IgniteInternalCheckedException {
         if (executor == null) {
             rowVersionFreeList.saveMetadata();
+
+            indexFreeList.saveMetadata();
         } else {
             executor.execute(() -> {
                 try {
@@ -192,6 +198,14 @@ public class PersistentPageMemoryMvPartitionStorage 
extends AbstractPageMemoryMv
                     throw new IgniteInternalException("Failed to save 
RowVersionFreeList metadata", e);
                 }
             });
+
+            executor.execute(() -> {
+                try {
+                    indexFreeList.saveMetadata();
+                } catch (IgniteInternalCheckedException e) {
+                    throw new IgniteInternalException("Failed to save 
IndexColumnsFreeList metadata", e);
+                }
+            });
         }
     }
 }
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
index dbcbd7c4a8..dc90fc9f37 100644
--- 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
@@ -18,11 +18,11 @@
 package org.apache.ignite.internal.storage.pagememory.mv;
 
 import java.util.concurrent.CompletableFuture;
-import org.apache.ignite.configuration.schemas.table.TableView;
-import org.apache.ignite.internal.pagememory.inmemory.VolatilePageMemory;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.storage.MvPartitionStorage;
 import org.apache.ignite.internal.storage.StorageException;
+import 
org.apache.ignite.internal.storage.pagememory.VolatilePageMemoryTableStorage;
+import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 
 /**
  * Implementation of {@link MvPartitionStorage} based on a {@link BplusTree} 
for in-memory case.
@@ -34,20 +34,25 @@ public class VolatilePageMemoryMvPartitionStorage extends 
AbstractPageMemoryMvPa
     /**
      * Constructor.
      *
-     * @param partId Partition id.
-     * @param tableView Table configuration.
-     * @param pageMemory Page memory.
-     * @param rowVersionFreeList Free list for {@link RowVersion}.
+     * @param tableStorage Table storage instance.
+     * @param partitionId Partition id.
      * @param versionChainTree Table tree for {@link VersionChain}.
+     * @param indexMetaTree Tree that contains SQL indexes' metadata.
      */
     public VolatilePageMemoryMvPartitionStorage(
-            int partId,
-            TableView tableView,
-            VolatilePageMemory pageMemory,
-            RowVersionFreeList rowVersionFreeList,
-            VersionChainTree versionChainTree
+            VolatilePageMemoryTableStorage tableStorage,
+            int partitionId,
+            VersionChainTree versionChainTree,
+            IndexMetaTree indexMetaTree
     ) {
-        super(partId, tableView, pageMemory, rowVersionFreeList, 
versionChainTree);
+        super(
+                partitionId,
+                tableStorage,
+                tableStorage.dataRegion().rowVersionFreeList(),
+                tableStorage.dataRegion().indexColumnsFreeList(),
+                versionChainTree,
+                indexMetaTree
+        );
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/util/TreeCursorAdapter.java
 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/util/TreeCursorAdapter.java
new file mode 100644
index 0000000000..2d7c7abd5a
--- /dev/null
+++ 
b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/util/TreeCursorAdapter.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.storage.pagememory.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import org.apache.ignite.internal.storage.StorageException;
+import org.apache.ignite.internal.util.IgniteCursor;
+import org.apache.ignite.lang.IgniteInternalCheckedException;
+
+/**
+ * Wraps {@link IgniteCursor} into a {@link Iterator}.
+ *
+ * @param <TREE_ROWT> Type of elements in a tree cursor.
+ * @param <CURSOR_ROWT> Type of elements in a resulting iterator.
+ */
+public class TreeCursorAdapter<TREE_ROWT, CURSOR_ROWT> implements 
Iterator<CURSOR_ROWT> {
+    /** Cursor instance from the tree. */
+    private final IgniteCursor<TREE_ROWT> cursor;
+
+    /** Value mapper to transform the data. */
+    private final Function<TREE_ROWT, CURSOR_ROWT> mapper;
+
+    /** Cached {@link IgniteCursor#next()} value. */
+    private Boolean hasNext;
+
+    public TreeCursorAdapter(IgniteCursor<TREE_ROWT> cursor, 
Function<TREE_ROWT, CURSOR_ROWT> mapper) {
+        this.cursor = cursor;
+        this.mapper = mapper;
+    }
+
+    @Override
+    public boolean hasNext() throws StorageException {
+        try {
+            if (hasNext == null) {
+                hasNext = cursor.next();
+            }
+
+            return hasNext;
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to read next element from the 
tree", e);
+        }
+    }
+
+    @Override
+    public CURSOR_ROWT next() throws NoSuchElementException, StorageException {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        try {
+            TREE_ROWT treeRow = cursor.get();
+
+            hasNext = null;
+
+            return mapper.apply(treeRow);
+        } catch (IgniteInternalCheckedException e) {
+            throw new StorageException("Failed to read next element from the 
tree", e);
+        }
+    }
+}
diff --git 
a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/PersistentPageMemoryHashIndexStorageTest.java
 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/PersistentPageMemoryHashIndexStorageTest.java
new file mode 100644
index 0000000000..86774dba9c
--- /dev/null
+++ 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/PersistentPageMemoryHashIndexStorageTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.storage.pagememory.index;
+
+import java.nio.file.Path;
+import 
org.apache.ignite.configuration.schemas.table.ConstantValueDefaultConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.EntryCountBudgetConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.FunctionCallDefaultConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.NullValueDefaultConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.TableConfiguration;
+import 
org.apache.ignite.configuration.schemas.table.UnlimitedBudgetConfigurationSchema;
+import 
org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import 
org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import 
org.apache.ignite.internal.pagememory.configuration.schema.UnsafeMemoryAllocatorConfigurationSchema;
+import org.apache.ignite.internal.pagememory.io.PageIoRegistry;
+import org.apache.ignite.internal.storage.index.AbstractHashIndexStorageTest;
+import 
org.apache.ignite.internal.storage.pagememory.PersistentPageMemoryStorageEngine;
+import 
org.apache.ignite.internal.storage.pagememory.PersistentPageMemoryTableStorage;
+import 
org.apache.ignite.internal.storage.pagememory.configuration.schema.PersistentPageMemoryDataStorageConfigurationSchema;
+import 
org.apache.ignite.internal.storage.pagememory.configuration.schema.PersistentPageMemoryStorageEngineConfiguration;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Hash index test implementation for persistent page memory storage.
+ */
+@ExtendWith({ConfigurationExtension.class, WorkDirectoryExtension.class})
+class PersistentPageMemoryHashIndexStorageTest extends 
AbstractHashIndexStorageTest {
+    @WorkDirectory
+    private Path workDir;
+
+    @InjectConfiguration(polymorphicExtensions = 
UnsafeMemoryAllocatorConfigurationSchema.class)
+    private PersistentPageMemoryStorageEngineConfiguration engineConfig;
+
+    @InjectConfiguration(
+            name = "table",
+            value = "mock.dataStorage.name = " + 
PersistentPageMemoryStorageEngine.ENGINE_NAME,
+            polymorphicExtensions = {
+                    HashIndexConfigurationSchema.class,
+                    PersistentPageMemoryDataStorageConfigurationSchema.class,
+                    ConstantValueDefaultConfigurationSchema.class,
+                    FunctionCallDefaultConfigurationSchema.class,
+                    NullValueDefaultConfigurationSchema.class,
+                    UnlimitedBudgetConfigurationSchema.class,
+                    EntryCountBudgetConfigurationSchema.class
+            }
+    )
+    private TableConfiguration tableCfg;
+
+    private PersistentPageMemoryStorageEngine engine;
+
+    private PersistentPageMemoryTableStorage table;
+
+    @BeforeEach
+    void setUp() {
+        PageIoRegistry ioRegistry = new PageIoRegistry();
+
+        ioRegistry.loadFromServiceLoader();
+
+        engine = new PersistentPageMemoryStorageEngine("test", engineConfig, 
ioRegistry, workDir, null);
+
+        engine.start();
+
+        table = engine.createMvTable(tableCfg);
+
+        table.start();
+
+        initialize(table);
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(
+                table == null ? null : table::stop,
+                engine == null ? null : engine::stop
+        );
+    }
+
+    //TODO IGNITE-17626 Enable the test.
+    @Disabled
+    @Override
+    public void testDestroy() {
+    }
+}
diff --git 
a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/VolatilePageMemoryHashIndexStorageTest.java
 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/VolatilePageMemoryHashIndexStorageTest.java
new file mode 100644
index 0000000000..a81df0c5d6
--- /dev/null
+++ 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/index/VolatilePageMemoryHashIndexStorageTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.storage.pagememory.index;
+
+import 
org.apache.ignite.configuration.schemas.table.ConstantValueDefaultConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.EntryCountBudgetConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.FunctionCallDefaultConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.HashIndexConfigurationSchema;
+import 
org.apache.ignite.configuration.schemas.table.NullValueDefaultConfigurationSchema;
+import org.apache.ignite.configuration.schemas.table.TableConfiguration;
+import 
org.apache.ignite.configuration.schemas.table.UnlimitedBudgetConfigurationSchema;
+import 
org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import 
org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import 
org.apache.ignite.internal.pagememory.configuration.schema.UnsafeMemoryAllocatorConfigurationSchema;
+import org.apache.ignite.internal.pagememory.io.PageIoRegistry;
+import org.apache.ignite.internal.storage.index.AbstractHashIndexStorageTest;
+import 
org.apache.ignite.internal.storage.pagememory.VolatilePageMemoryStorageEngine;
+import 
org.apache.ignite.internal.storage.pagememory.VolatilePageMemoryTableStorage;
+import 
org.apache.ignite.internal.storage.pagememory.configuration.schema.VolatilePageMemoryDataStorageConfigurationSchema;
+import 
org.apache.ignite.internal.storage.pagememory.configuration.schema.VolatilePageMemoryStorageEngineConfiguration;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Hash index test implementation for volatile page memory storage.
+ */
+@ExtendWith(ConfigurationExtension.class)
+class VolatilePageMemoryHashIndexStorageTest extends 
AbstractHashIndexStorageTest {
+    @InjectConfiguration(polymorphicExtensions = 
UnsafeMemoryAllocatorConfigurationSchema.class)
+    private VolatilePageMemoryStorageEngineConfiguration engineConfig;
+
+    @InjectConfiguration(
+            name = "table",
+            value = "mock.dataStorage.name = " + 
VolatilePageMemoryStorageEngine.ENGINE_NAME,
+            polymorphicExtensions = {
+                    HashIndexConfigurationSchema.class,
+                    VolatilePageMemoryDataStorageConfigurationSchema.class,
+                    ConstantValueDefaultConfigurationSchema.class,
+                    FunctionCallDefaultConfigurationSchema.class,
+                    NullValueDefaultConfigurationSchema.class,
+                    UnlimitedBudgetConfigurationSchema.class,
+                    EntryCountBudgetConfigurationSchema.class
+            }
+    )
+    private TableConfiguration tableCfg;
+
+    private VolatilePageMemoryStorageEngine engine;
+
+    private VolatilePageMemoryTableStorage table;
+
+    @BeforeEach
+    void setUp() {
+        PageIoRegistry ioRegistry = new PageIoRegistry();
+
+        ioRegistry.loadFromServiceLoader();
+
+        engine = new VolatilePageMemoryStorageEngine(engineConfig, ioRegistry);
+
+        engine.start();
+
+        table = engine.createMvTable(tableCfg);
+
+        table.start();
+
+        initialize(table);
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(
+                table == null ? null : table::stop,
+                engine == null ? null : engine::stop
+        );
+    }
+
+    //TODO IGNITE-17626 Enable the test.
+    @Disabled
+    @Override
+    public void testDestroy() {
+    }
+}
diff --git 
a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorageTest.java
 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorageTest.java
index 1fb6f8ad7c..fb594309e1 100644
--- 
a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorageTest.java
+++ 
b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorageTest.java
@@ -35,8 +35,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 /**
  * Base test for MV partition storages based on PageMemory.
  */
-@ExtendWith(ConfigurationExtension.class)
-@ExtendWith(WorkDirectoryExtension.class)
+@ExtendWith({ConfigurationExtension.class, WorkDirectoryExtension.class})
 abstract class AbstractPageMemoryMvPartitionStorageTest extends 
AbstractMvPartitionStorageTest {
     protected final PageIoRegistry ioRegistry = new PageIoRegistry();
 
diff --git 
a/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/RocksDbTableStorage.java
 
b/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/RocksDbTableStorage.java
index ea0dab45e7..5b3d98d323 100644
--- 
a/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/RocksDbTableStorage.java
+++ 
b/modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/RocksDbTableStorage.java
@@ -425,6 +425,7 @@ public class RocksDbTableStorage implements MvTableStorage {
             return CompletableFuture.completedFuture(null);
         }
 
+        //TODO IGNITE-17626 Destroy indexes as well...
         mvPartition.destroy();
 
         // Wait for the data to actually be removed from the disk and close 
the storage.

Reply via email to