lbug 0.16.1

An in-process property graph database management system built for query speed and scalability
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
#pragma once

#include <algorithm>
#include <string_view>
#include <type_traits>

#include "common/cast.h"
#include "common/serializer/buffer_reader.h"
#include "common/serializer/serializer.h"
#include "common/type_utils.h"
#include "common/types/string_t.h"
#include "common/types/types.h"
#include "hash_index_header.h"
#include "hash_index_slot.h"
#include "index.h"
#include "storage/buffer_manager/memory_manager.h"
#include "storage/disk_array_collection.h"
#include "storage/index/hash_index_utils.h"
#include "storage/index/in_mem_hash_index.h"
#include "storage/local_storage/local_hash_index.h"

namespace lbug {
namespace common {
class VirtualFileSystem;
}
namespace transaction {
class Transaction;
enum class TransactionType : uint8_t;
} // namespace transaction
namespace storage {

class FileHandle;
class BufferManager;
class OverflowFileHandle;
template<typename T>
class DiskArray;
class PageManager;

class OnDiskHashIndex {
public:
    virtual ~OnDiskHashIndex() = default;
    virtual bool checkpoint(PageAllocator& pageAllocator) = 0;
    virtual bool checkpointInMemory() = 0;
    virtual bool rollbackInMemory() = 0;
    virtual void rollbackCheckpoint() = 0;
    virtual void bulkReserve(uint64_t numValuesToAppend) = 0;
    virtual void reclaimStorage(PageAllocator& pageAllocator) = 0;
    virtual bool tryLock() = 0;
    virtual std::unique_lock<std::shared_mutex> adoptLock() = 0;
};

// HashIndex is the entrance to handle all updates and lookups into the index after building from
// scratch through InMemHashIndex.
// The index consists of two parts, one is the persistent storage (from the persistent index file),
// and the other is the local storage. All lookups/deletions/insertions go through local storage,
// and then the persistent storage if necessary.
//
// Key interfaces:
// - lookup(): Given a key, find its result. Return true if the key is found, else, return false.
//   Lookups go through the local storage first, check if the key is marked as deleted or not, then
//   check whether it can be found inside local insertions or not. If the key is neither marked as
//   deleted nor found in local insertions, we proceed to lookups in the persistent store.
// - delete(): Delete the given key.
//   Deletions are directly marked in the local storage.
// - insert(): Insert the given key and value. Return true if the given key doesn't exist in the
// index before insertion, otherwise, return false.
//   First check if the key to be inserted already exists in local insertions or the persistent
//   store. If the key doesn't exist yet, append it to local insertions, and also remove it from
//   local deletions if it was marked as deleted.
//
// T is the key type used to access values
// S is the stored type, which is usually the same as T, with the exception of strings
template<typename T>
class HashIndex final : public OnDiskHashIndex {
public:
    HashIndex(MemoryManager& memoryManager, OverflowFileHandle* overflowFileHandle,
        DiskArrayCollection& diskArrays, uint64_t indexPos, ShadowFile* shadowFile,
        const HashIndexHeader& indexHeaderForReadTrx, HashIndexHeader& indexHeaderForWriteTrx);

    ~HashIndex() override;

public:
    using OnDiskSlotType = Slot<T>;
    static constexpr auto PERSISTENT_SLOT_CAPACITY = getSlotCapacity<T>();

    static_assert(DiskArray<OnDiskSlotType>::getAlignedElementSize() <=
                  common::HashIndexConstants::SLOT_CAPACITY_BYTES);
    static_assert(DiskArray<OnDiskSlotType>::getAlignedElementSize() >
                  common::HashIndexConstants::SLOT_CAPACITY_BYTES / 2);

    using Key =
        typename std::conditional<std::same_as<T, common::string_t>, std::string_view, T>::type;
    // For read transactions, local storage is skipped, lookups are performed on the persistent
    // storage. For write transactions, lookups are performed in the local storage first, then in
    // the persistent storage if necessary. In details, there are three cases for the local storage
    // lookup:
    // - the key is found in the local storage, directly return true;
    // - the key has been marked as deleted in the local storage, return false;
    // - the key is neither deleted nor found in the local storage, lookup in the persistent
    // storage.
    bool lookupInternal(const transaction::Transaction* transaction, Key key,
        common::offset_t& result, visible_func isVisible) {
        auto localLookupState = localStorage->lookup(key, result, isVisible);
        if (localLookupState == HashIndexLocalLookupState::KEY_DELETED) {
            return false;
        }
        if (localLookupState == HashIndexLocalLookupState::KEY_FOUND) {
            return true;
        }
        DASSERT(localLookupState == HashIndexLocalLookupState::KEY_NOT_EXIST);
        return lookupInPersistentIndex(transaction, key, result, isVisible);
    }

    // For deletions, we don't check if the deleted keys exist or not. Thus, we don't need to check
    // in the persistent storage and directly delete keys in the local storage.
    void deleteInternal(Key key) const { localStorage->deleteKey(key); }
    // Discards from local storage, but will not insert a deletion (used for rollbacks)
    bool discardLocal(Key key) const { return localStorage->discard(key); }

    // For insertions, we first check in the local storage. There are three cases:
    // - the key is found in the local storage, return false;
    // - the key is marked as deleted in the local storage, insert the key to the local storage;
    // - the key doesn't exist in the local storage, check if the key exists in the persistent
    // index, if
    //   so, return false, else insert the key to the local storage.
    using InsertType = InMemHashIndex<T>::OwnedType;
    bool insertInternal(const transaction::Transaction* transaction, InsertType&& key,
        common::offset_t value, visible_func isVisible) {
        common::offset_t tmpResult = 0;
        auto localLookupState = localStorage->lookup(key, tmpResult, isVisible);
        if (localLookupState == HashIndexLocalLookupState::KEY_FOUND) {
            return false;
        }
        if (localLookupState != HashIndexLocalLookupState::KEY_DELETED) {
            if (lookupInPersistentIndex(transaction, key, tmpResult, isVisible)) {
                return false;
            }
        }
        return localStorage->insert(std::move(key), value, isVisible);
    }

    using BufferKeyType =
        typename std::conditional<std::same_as<T, common::string_t>, std::string, T>::type;
    // Appends the buffer to the index. Returns the number of values successfully inserted
    // Note that this function does not acquire locks internally, as the caller is expected to hold
    // the lock already.
    size_t appendNoLock(const transaction::Transaction* transaction,
        IndexBuffer<BufferKeyType>& buffer, uint64_t bufferOffset, visible_func isVisible) {
        // Check if values already exist in persistent storage
        if (indexHeaderForWriteTrx.numEntries > 0) {
            localStorage->reserveSpaceForAppendNoLock(buffer.size() - bufferOffset);
            size_t numValuesInserted = 0;
            common::offset_t result = 0;
            for (size_t i = bufferOffset; i < buffer.size(); i++) {
                auto& [key, value] = buffer[i];
                if (lookupInPersistentIndex(transaction, key, result, isVisible)) {
                    return i - bufferOffset;
                } else {
                    numValuesInserted +=
                        localStorage->appendNoLock(std::move(key), value, isVisible);
                }
            }
            return numValuesInserted;
        } else {
            return localStorage->appendNoLock(buffer, bufferOffset, isVisible);
        }
    }

    bool tryLock() override { return localStorage->tryLock(); }
    std::unique_lock<std::shared_mutex> adoptLock() override { return localStorage->adoptLock(); }

    bool checkpoint(PageAllocator& pageAllocator) override;
    bool checkpointInMemory() override;
    bool rollbackInMemory() override;
    void rollbackCheckpoint() override;
    void reclaimStorage(PageAllocator& pageAllocator) override;

private:
    bool lookupInPersistentIndex(const transaction::Transaction* transaction, Key key,
        common::offset_t& result, visible_func isVisible) {
        auto& header = transaction->getType() == transaction::TransactionType::CHECKPOINT ?
                           this->indexHeaderForWriteTrx :
                           this->indexHeaderForReadTrx;
        // There may not be any primary key slots if we try to lookup on an empty index
        if (header.numEntries == 0) {
            return false;
        }
        auto hashValue = HashIndexUtils::hash(key);
        auto fingerprint = HashIndexUtils::getFingerprintForHash(hashValue);
        auto iter = getSlotIterator(HashIndexUtils::getPrimarySlotIdForHash(header, hashValue),
            transaction);
        do {
            auto entryPos =
                findMatchedEntryInSlot(transaction, iter.slot, key, fingerprint, isVisible);
            if (entryPos != SlotHeader::INVALID_ENTRY_POS) {
                result = iter.slot.entries[entryPos].value;
                return true;
            }
        } while (nextChainedSlot(transaction, iter));
        return false;
    }
    void deleteFromPersistentIndex(const transaction::Transaction* transaction, Key key,
        visible_func isVisible);

    entry_pos_t findMatchedEntryInSlot(const transaction::Transaction* transaction,
        const OnDiskSlotType& slot, Key key, uint8_t fingerprint,
        const visible_func& isVisible) const {
        for (auto entryPos = 0u; entryPos < PERSISTENT_SLOT_CAPACITY; entryPos++) {
            if (slot.header.isEntryValid(entryPos) &&
                slot.header.fingerprints[entryPos] == fingerprint &&
                equals(transaction, key, slot.entries[entryPos].key) &&
                isVisible(slot.entries[entryPos].value)) {
                return entryPos;
            }
        }
        return SlotHeader::INVALID_ENTRY_POS;
    }

    inline void updateSlot(const transaction::Transaction* transaction, const SlotInfo& slotInfo,
        const OnDiskSlotType& slot) {
        slotInfo.slotType == SlotType::PRIMARY ?
            pSlots->update(transaction, slotInfo.slotId, slot) :
            oSlots->update(transaction, slotInfo.slotId, slot);
    }

    inline OnDiskSlotType getSlot(const transaction::Transaction* transaction,
        const SlotInfo& slotInfo) const {
        return slotInfo.slotType == SlotType::PRIMARY ? pSlots->get(slotInfo.slotId, transaction) :
                                                        oSlots->get(slotInfo.slotId, transaction);
    }

    void splitSlots(PageAllocator& pageAllocator, const transaction::Transaction* transaction,
        HashIndexHeader& header, slot_id_t numSlotsToSplit);

    // Resizes the local storage to support the given number of new entries
    void bulkReserve(uint64_t newEntries) override;
    // Resizes the on-disk index to support the given number of new entries
    void reserve(PageAllocator& pageAllocator, const transaction::Transaction* transaction,
        uint64_t newEntries);

    struct HashIndexEntryView {
        slot_id_t diskSlotId;
        uint8_t fingerprint;
        const SlotEntry<typename InMemHashIndex<T>::OwnedType>* entry;
    };

    void sortEntries(const transaction::Transaction* transaction,
        const InMemHashIndex<T>& insertLocalStorage,
        typename InMemHashIndex<T>::SlotIterator& slotToMerge,
        std::vector<HashIndexEntryView>& entries);
    void mergeBulkInserts(PageAllocator& pageAllocator, const transaction::Transaction* transaction,
        const InMemHashIndex<T>& insertLocalStorage);
    // Returns the number of elements merged which matched the given slot id
    size_t mergeSlot(PageAllocator& pageAllocator, const transaction::Transaction* transaction,
        const std::vector<HashIndexEntryView>& slotToMerge,
        typename DiskArray<OnDiskSlotType>::WriteIterator& diskSlotIterator,
        typename DiskArray<OnDiskSlotType>::WriteIterator& diskOverflowSlotIterator,
        slot_id_t diskSlotId);

    inline bool equals(const transaction::Transaction* /*transaction*/, Key keyToLookup,
        const T& keyInEntry) const {
        return keyToLookup == keyInEntry;
    }

    inline common::hash_t hashStored(const transaction::Transaction* /*transaction*/,
        const T& key) const {
        return HashIndexUtils::hash(key);
    }

    inline common::hash_t hashStored(const transaction::Transaction* /*transaction*/,
        std::string_view key) const {
        return HashIndexUtils::hash(key);
    }

    struct SlotIterator {
        SlotInfo slotInfo;
        OnDiskSlotType slot;
    };

    SlotIterator getSlotIterator(slot_id_t slotId, const transaction::Transaction* transaction) {
        return SlotIterator{SlotInfo{slotId, SlotType::PRIMARY},
            getSlot(transaction, SlotInfo{slotId, SlotType::PRIMARY})};
    }

    bool nextChainedSlot(const transaction::Transaction* transaction, SlotIterator& iter) const {
        DASSERT(iter.slotInfo.slotType == SlotType::PRIMARY ||
                iter.slotInfo.slotId != iter.slot.header.nextOvfSlotId);
        if (iter.slot.header.nextOvfSlotId != SlotHeader::INVALID_OVERFLOW_SLOT_ID) {
            iter.slotInfo.slotId = iter.slot.header.nextOvfSlotId;
            iter.slotInfo.slotType = SlotType::OVF;
            iter.slot = getSlot(transaction, iter.slotInfo);
            return true;
        }
        return false;
    }

    std::vector<std::pair<SlotInfo, OnDiskSlotType>> getChainedSlots(
        const transaction::Transaction* transaction, slot_id_t pSlotId);

private:
    ShadowFile* shadowFile;
    uint64_t headerPageIdx;
    std::unique_ptr<DiskArray<OnDiskSlotType>> pSlots;
    std::unique_ptr<DiskArray<OnDiskSlotType>> oSlots;
    OverflowFileHandle* overflowFileHandle;
    std::unique_ptr<HashIndexLocalStorage<T>> localStorage;
    const HashIndexHeader& indexHeaderForReadTrx;
    HashIndexHeader& indexHeaderForWriteTrx;
    MemoryManager& memoryManager;
};

template<>
common::hash_t HashIndex<common::string_t>::hashStored(const transaction::Transaction* transaction,
    const common::string_t& key) const;

template<>
bool HashIndex<common::string_t>::equals(const transaction::Transaction* transaction,
    std::string_view keyToLookup, const common::string_t& keyInEntry) const;

struct PrimaryKeyIndexStorageInfo final : IndexStorageInfo {
    common::page_idx_t firstHeaderPage;
    common::page_idx_t overflowHeaderPage;

    PrimaryKeyIndexStorageInfo()
        : firstHeaderPage{common::INVALID_PAGE_IDX}, overflowHeaderPage{common::INVALID_PAGE_IDX} {}
    PrimaryKeyIndexStorageInfo(common::page_idx_t firstHeaderPage,
        common::page_idx_t overflowHeaderPage)
        : firstHeaderPage{firstHeaderPage}, overflowHeaderPage{overflowHeaderPage} {}

    DELETE_COPY_DEFAULT_MOVE(PrimaryKeyIndexStorageInfo);

    std::shared_ptr<common::BufferWriter> serialize() const override {
        auto bufferWriter = std::make_shared<common::BufferWriter>();
        auto serializer = common::Serializer(bufferWriter);
        serializer.write<common::page_idx_t>(firstHeaderPage);
        serializer.write<common::page_idx_t>(overflowHeaderPage);
        return bufferWriter;
    }

    static std::unique_ptr<IndexStorageInfo> deserialize(
        std::unique_ptr<common::BufferReader> reader);
};

class PrimaryKeyIndex final : public Index {
public:
    static constexpr const char* DEFAULT_NAME = "_PK";

    struct InsertState final : Index::InsertState {
        visible_func isVisible; // Function to check visibility of the inserted key

        explicit InsertState(visible_func isVisible_) : isVisible{std::move(isVisible_)} {}
    };

    // Construct an existing index
    PrimaryKeyIndex(IndexInfo indexInfo, std::unique_ptr<IndexStorageInfo> storageInfo,
        bool inMemMode, MemoryManager& memoryManager, PageAllocator& pageAllocator,
        ShadowFile* shadowFile);
    ~PrimaryKeyIndex() override;

    static std::unique_ptr<PrimaryKeyIndex> createNewIndex(IndexInfo indexInfo, bool inMemMode,
        MemoryManager& memoryManager, PageAllocator& pageAllocator, ShadowFile* shadowFile);

    template<typename T>
    inline HashIndex<HashIndexType<T>>* getTypedHashIndex(T key) {
        return common::dynamic_cast_checked<HashIndex<HashIndexType<T>>*>(
            hashIndices[HashIndexUtils::getHashIndexPosition(key)].get());
    }
    template<common::IndexHashable T>
    inline HashIndex<T>* getTypedHashIndexByPos(uint64_t indexPos) {
        return common::dynamic_cast_checked<HashIndex<HashIndexType<T>>*>(
            hashIndices[indexPos].get());
    }

    bool tryLockTypedIndex(uint64_t indexPos) { return hashIndices[indexPos]->tryLock(); }
    std::unique_lock<std::shared_mutex> adoptLockOfTypedIndex(uint64_t indexPos) {
        return hashIndices[indexPos]->adoptLock();
    }

    bool lookup(const transaction::Transaction* trx, common::string_t key, common::offset_t& result,
        visible_func isVisible) {
        return lookup(trx, key.getAsStringView(), result, isVisible);
    }
    template<common::IndexHashable T>
    inline bool lookup(const transaction::Transaction* trx, T key, common::offset_t& result,
        visible_func isVisible) {
        DASSERT(indexInfo.keyDataTypes[0] == common::TypeUtils::getPhysicalTypeIDForType<T>());
        return getTypedHashIndex(key)->lookupInternal(trx, key, result, isVisible);
    }

    bool lookup(const transaction::Transaction* trx, common::ValueVector* keyVector,
        uint64_t vectorPos, common::offset_t& result, visible_func isVisible);

    std::unique_ptr<Index::InsertState> initInsertState(main::ClientContext*,
        visible_func isVisible) override {
        return std::make_unique<InsertState>(isVisible);
    }
    void insert(transaction::Transaction*, const common::ValueVector&,
        const std::vector<common::ValueVector*>&, Index::InsertState&) override {
        // DO NOTHING.
        // For hash index, we don't need to do anything here because the insertions are handled when
        // the transaction commits.
    }
    bool insert(const transaction::Transaction* transaction, common::string_t key,
        common::offset_t value, visible_func isVisible) {
        return insert(transaction, key.getAsString(), value, isVisible);
    }
    template<common::IndexHashable T>
    inline bool insert(const transaction::Transaction* transaction, T key, common::offset_t value,
        visible_func isVisible) {
        DASSERT(indexInfo.keyDataTypes[0] == common::TypeUtils::getPhysicalTypeIDForType<T>());
        return getTypedHashIndex(key)->insertInternal(transaction, std::move(key), value,
            isVisible);
    }
    bool insert(const transaction::Transaction* transaction, const common::ValueVector* keyVector,
        uint64_t vectorPos, common::offset_t value, visible_func isVisible);
    bool needCommitInsert() const override { return true; }
    void commitInsert(transaction::Transaction* transaction,
        const common::ValueVector& nodeIDVector,
        const std::vector<common::ValueVector*>& indexVectors,
        Index::InsertState& insertState) override;

    // Appends the buffer to the index. Returns the number of values successfully inserted.
    // If a key fails to insert, it immediately returns without inserting any more values,
    // and the returned value is also the index of the key which failed to insert.
    template<common::IndexHashable T>
    size_t appendWithIndexPosNoLock(const transaction::Transaction* transaction,
        IndexBuffer<T>& buffer, uint64_t bufferOffset, uint64_t indexPos, visible_func isVisible) {
        DASSERT(indexInfo.keyDataTypes[0] == common::TypeUtils::getPhysicalTypeIDForType<T>());
        DASSERT(std::all_of(buffer.begin(), buffer.end(), [&](auto& elem) {
            return HashIndexUtils::getHashIndexPosition(elem.first) == indexPos;
        }));
        return getTypedHashIndexByPos<HashIndexType<T>>(indexPos)->appendNoLock(transaction, buffer,
            bufferOffset, isVisible);
    }

    void bulkReserve(uint64_t numValuesToAppend) {
        uint32_t eachSize = numValuesToAppend / NUM_HASH_INDEXES + 1;
        for (auto i = 0u; i < NUM_HASH_INDEXES; i++) {
            hashIndices[i]->bulkReserve(eachSize);
        }
    }

    void delete_(common::string_t key) { return delete_(key.getAsStringView()); }
    std::unique_ptr<DeleteState> initDeleteState(const transaction::Transaction* /*transaction*/,
        MemoryManager* /*mm*/, visible_func /*isVisible*/) override {
        return std::make_unique<DeleteState>();
    }
    void delete_(transaction::Transaction* /*transaction*/,
        const common::ValueVector& /*nodeIDVector*/, DeleteState& /*deleteState*/) override {
        // DO NOTHING.
    }
    template<common::IndexHashable T>
    inline void delete_(T key) {
        DASSERT(indexInfo.keyDataTypes[0] == common::TypeUtils::getPhysicalTypeIDForType<T>());
        return getTypedHashIndex(key)->deleteInternal(key);
    }

    bool discardLocal(common::string_t key) { return discardLocal(key.getAsStringView()); }
    template<common::IndexHashable T>
    inline bool discardLocal(T key) {
        DASSERT(indexInfo.keyDataTypes[0] == common::TypeUtils::getPhysicalTypeIDForType<T>());
        return getTypedHashIndex(key)->discardLocal(key);
    }

    void delete_(common::ValueVector* keyVector);

    void checkpointInMemory() override;
    void checkpoint(main::ClientContext*, storage::PageAllocator& pageAllocator) override;
    OverflowFile* getOverflowFile() const { return overflowFile.get(); }

    void rollbackCheckpoint() override;

    common::PhysicalTypeID keyTypeID() const {
        DASSERT(indexInfo.keyDataTypes.size() == 1);
        return indexInfo.keyDataTypes[0];
    }
    void reclaimStorage(PageAllocator& pageAllocator) const;

    static LBUG_API std::unique_ptr<Index> load(main::ClientContext* context,
        StorageManager* storageManager, IndexInfo indexInfo, std::span<uint8_t> storageInfoBuffer);

    static IndexType getIndexType() {
        static const IndexType HASH_INDEX_TYPE{"HASH", IndexConstraintType::PRIMARY,
            IndexDefinitionType::BUILTIN, load};
        return HASH_INDEX_TYPE;
    }

private:
    void writeHeaders(PageAllocator& pageAllocator) const;

    void initOverflowAndSubIndices(bool inMemMode, MemoryManager& mm, PageAllocator& pageAllocator,
        PrimaryKeyIndexStorageInfo& storageInfo);

    common::page_idx_t getFirstHeaderPage() const;

    common::page_idx_t getDiskArrayFirstHeaderPage() const;

private:
    std::unique_ptr<OverflowFile> overflowFile;
    std::vector<std::unique_ptr<OnDiskHashIndex>> hashIndices;
    std::vector<HashIndexHeader> hashIndexHeadersForReadTrx;
    std::vector<HashIndexHeader> hashIndexHeadersForWriteTrx;
    ShadowFile& shadowFile;
    // Stores both primary and overflow slots
    std::unique_ptr<DiskArrayCollection> hashIndexDiskArrays;
};

} // namespace storage
} // namespace lbug