diff --git a/ydb/core/tablet_flat/flat_page_btree_index.h b/ydb/core/tablet_flat/flat_page_btree_index.h index 749a6bab86cb..d3a9121d7209 100644 --- a/ydb/core/tablet_flat/flat_page_btree_index.h +++ b/ydb/core/tablet_flat/flat_page_btree_index.h @@ -24,8 +24,8 @@ namespace NKikimr::NTable::NPage { TBtreeIndexNode page binary layout - TLabel - page label - THeader - page header - - TKey[N] - keys data <-- var-size - TPgSize[N] - keys offsets + - TKey[N] - keys data <-- var-size - TChild[N+1] - children */ @@ -118,7 +118,8 @@ namespace NKikimr::NTable::NPage { TString ToString() const noexcept { - return TStringBuilder() << "PageId: " << PageId << " Count: " << Count << " Size: " << Size; + // copy values to prevent 'reference binding to misaligned address' error + return TStringBuilder() << "PageId: " << TPageId(PageId) << " Count: " << TRowId(Count) << " Size: " << ui64(Size); } } Y_PACKED; @@ -137,12 +138,12 @@ namespace NKikimr::NTable::NPage { Keys.Count = header->KeysCount; size_t offset = sizeof(THeader); - Keys.Base = Raw.data(); - offset += header->KeysSize; - Keys.Offsets = TDeref::At(header, offset); offset += Keys.Count * sizeof(TRecordsEntry); + Keys.Base = Raw.data(); + offset += header->KeysSize; + Children = TDeref::At(header, offset); offset += (1 + Keys.Count) * sizeof(TChild); @@ -177,4 +178,7 @@ namespace NKikimr::NTable::NPage { const TChild* Children; }; + struct TBtreeIndexMeta : public TBtreeIndexNode::TChild { + size_t LevelsCount; + }; } diff --git a/ydb/core/tablet_flat/flat_page_btree_index_writer.h b/ydb/core/tablet_flat/flat_page_btree_index_writer.h index da1358815279..c5885d604e70 100644 --- a/ydb/core/tablet_flat/flat_page_btree_index_writer.h +++ b/ydb/core/tablet_flat/flat_page_btree_index_writer.h @@ -1,6 +1,7 @@ #pragma once #include "flat_page_btree_index.h" +#include "flat_part_iface.h" namespace NKikimr::NTable::NPage { @@ -17,52 +18,74 @@ namespace NKikimr::NTable::NPage { { } - // TODO: pass serialized key + size from TBtreeIndexBuilder - void AddKey(TString serializedKey) { - TSerializedCellVec key(serializedKey); - KeysSize += CalcSize(key.GetCells()); - // TODO: serialize to page bytes directly - Keys.push_back(serializedKey); + void AddKey(TCellsRef cells) { + AddKey(SerializeKey(cells)); + } + + void AddKey(TString&& key) { + KeysSize += key.size(); + Keys.emplace_back(std::move(key)); } void AddChild(TChild child) { Children.push_back(child); } + void EnsureEmpty() { + Y_ABORT_UNLESS(!Keys); + Y_ABORT_UNLESS(!KeysSize); + Y_ABORT_UNLESS(!Children); + Y_ABORT_UNLESS(!Ptr); + Y_ABORT_UNLESS(!End); + } + + TString SerializeKey(TCellsRef cells) { + Y_ABORT_UNLESS(cells.size() <= GroupInfo.KeyTypes.size()); + + TString buf; + buf.ReserveAndResize(CalcKeySize(cells)); + Ptr = buf.Detach(); + End = buf.end(); + + PlaceKey(cells); + + Y_ABORT_UNLESS(Ptr == End); + NSan::CheckMemIsInitialized(buf.data(), buf.size()); + Ptr = 0; + End = 0; + + return buf; + } + TSharedData Finish() { Y_ABORT_UNLESS(Keys.size()); Y_ABORT_UNLESS(Children.size() == Keys.size() + 1); - size_t childSize = sizeof(TChild); - size_t pageSize = - sizeof(TLabel) + sizeof(THeader) + - KeysSize + - sizeof(TRecordsEntry) * Keys.size() + - childSize * Children.size(); - + size_t pageSize = CalcPageSize(); TSharedData buf = TSharedData::Uninitialized(pageSize); Ptr = buf.mutable_begin(); End = buf.end(); - WriteUnaligned(Advance(Ptr, sizeof(TLabel)), TLabel::Encode(EPage::BTreeIndex, 0, pageSize)); + WriteUnaligned(Advance(sizeof(TLabel)), TLabel::Encode(EPage::BTreeIndex, 0, pageSize)); auto &header = Place(); header.KeysCount = Keys.size(); Y_ABORT_UNLESS(KeysSize < Max(), "KeysSize is out of bounds"); header.KeysSize = KeysSize; - char* keyOffsetPtr = Ptr + KeysSize; + size_t keyOffset = Ptr - buf.mutable_begin() + sizeof(TRecordsEntry) * Keys.size(); for (const auto &key : Keys) { - auto &meta = Place(keyOffsetPtr); - size_t offset = Ptr - buf.mutable_begin(); - Y_ABORT_UNLESS(offset < Max(), "Key offset is out of bounds"); - meta.Offset = offset; + auto &meta = Place(); + Y_ABORT_UNLESS(keyOffset < Max(), "Key offset is out of bounds"); + meta.Offset = keyOffset; + keyOffset += key.size(); + } + Y_ABORT_UNLESS(Ptr == buf.mutable_begin() + sizeof(TLabel) + sizeof(THeader) + sizeof(TRecordsEntry) * Keys.size()); - TSerializedCellVec cells(key); - PlaceKey(cells.GetCells()); + for (auto &key : Keys) { + PlaceBytes(std::move(key)); } - Y_ABORT_UNLESS(Ptr == buf.mutable_begin() + sizeof(TLabel) + sizeof(THeader) + KeysSize); - Ptr = keyOffsetPtr; + Y_ABORT_UNLESS(Ptr == buf.mutable_begin() + sizeof(TLabel) + sizeof(THeader) + sizeof(TRecordsEntry) * Keys.size() + KeysSize); Keys.clear(); KeysSize = 0; @@ -76,18 +99,32 @@ namespace NKikimr::NTable::NPage { return buf; }; + size_t CalcPageSize() const { + return CalcPageSize(KeysSize, Keys.size()); + } + + static size_t CalcPageSize(size_t keysSize, size_t keysCount) { + return + sizeof(TLabel) + sizeof(THeader) + + sizeof(TRecordsEntry) * keysCount + + keysSize + + sizeof(TChild) * (keysCount + 1); + } + + size_t GetKeysCount() const { + return Keys.size(); + } + private: - TPgSize CalcSize(TCellsRef key) const noexcept + TPgSize CalcKeySize(TCellsRef cells) const noexcept { - Y_ABORT_UNLESS(key.size() <= GroupInfo.KeyTypes.size()); - TPgSize size = TKey::NullBitmapLength(GroupInfo.ColsKeyIdx.size()); - for (TPos pos : xrange(key.size())) { - if (const auto &val = key[pos]) { + for (TPos pos : xrange(cells.size())) { + if (const auto &val = cells[pos]) { size += GroupInfo.ColsKeyIdx[pos].FixedSize; // fixed data or data ref if (!GroupInfo.ColsKeyIdx[pos].IsFixed) { - size += key[pos].Size(); + size += cells[pos].Size(); } } } @@ -97,13 +134,10 @@ namespace NKikimr::NTable::NPage { void PlaceKey(TCellsRef cells) { - const TPos count = GroupInfo.ColsKeyIdx.size(); - Y_ABORT_UNLESS(cells.size() <= count); - auto *key = TDeref::At(Ptr); // mark all cells as non-null - Zero(key->NullBitmapLength(count)); + Zero(key->NullBitmapLength(GroupInfo.ColsKeyIdx.size())); auto* keyCellsPtr = Ptr; @@ -144,39 +178,31 @@ namespace NKikimr::NTable::NPage { } } + void PlaceBytes(TString&& data) noexcept + { + std::copy(data.data(), data.data() + data.size(), Advance(data.size())); + } + template void PlaceVector(TVector &vector) noexcept { - auto *dst = reinterpret_cast(Advance(Ptr, sizeof(T)*vector.size())); + auto *dst = reinterpret_cast(Advance(sizeof(T)*vector.size())); std::copy(vector.begin(), vector.end(), dst); vector.clear(); } template - T& Place() + T& Place() noexcept { return *reinterpret_cast(Advance(TPgSizeOf::Value)); } - template - T& Place(char*& ptr) - { - return *reinterpret_cast(Advance(ptr, TPgSizeOf::Value)); - } - void Zero(size_t size) noexcept { - auto *from = Advance(Ptr, size); + auto *from = Advance(size); std::fill(from, Ptr, 0); } - char* Advance(char*& ptr, size_t size) noexcept - { - auto newPtr = ptr + size; - Y_ABORT_UNLESS(newPtr <= End); - return std::exchange(ptr, newPtr); - } - char* Advance(size_t size) noexcept { auto newPtr = Ptr + size; @@ -200,4 +226,178 @@ namespace NKikimr::NTable::NPage { const char* End = 0; }; + class TBtreeIndexBuilder { + public: + using TChild = TBtreeIndexNode::TChild; + + private: + struct TLevel { + void PushKey(TString&& key) { + KeysSize += key.size(); + Keys.emplace_back(std::move(key)); + } + + TString PopKey() { + Y_ABORT_UNLESS(Keys); + TString key = std::move(Keys.front()); + KeysSize -= key.size(); + Keys.pop_front(); + return std::move(key); + } + + void PushChild(TChild child) { + Children.push_back(child); + } + + TChild PopChild() { + Y_ABORT_UNLESS(Children); + TChild result = Children.front(); + Children.pop_front(); + return result; + } + + size_t GetKeysSize() { + return KeysSize; + } + + size_t GetKeysCount() { + return Keys.size(); + } + + size_t GetChildrenCount() { + return Children.size(); + } + + private: + size_t KeysSize = 0; + TDeque Keys; + TDeque Children; + }; + + public: + TBtreeIndexBuilder(TIntrusiveConstPtr scheme, TGroupId groupId, + ui32 nodeTargetSize, ui32 nodeKeysMin, ui32 nodeKeysMax) + : Writer(std::move(scheme), groupId) + , Levels(1) + , NodeTargetSize(nodeTargetSize) + , NodeKeysMin(nodeKeysMin) + , NodeKeysMax(nodeKeysMax) + { + Y_ABORT_UNLESS(NodeTargetSize > 0); + Y_ABORT_UNLESS(NodeKeysMin > 0); + Y_ABORT_UNLESS(NodeKeysMax >= NodeKeysMin); + } + + void AddKey(TCellsRef cells) { + Levels[0].PushKey(Writer.SerializeKey(cells)); + } + + void AddChild(TChild child) { + // aggregate in order to perform search by row id from any leaf node + child.Count = (ChildrenCount += child.Count); + child.ErasedCount = (ChildrenErasedCount += child.ErasedCount); + child.Size = (ChildrenSize += child.Size); + + Levels[0].PushChild(child); + } + + std::optional Flush(IPageWriter &pager, bool last) { + for (size_t levelIndex = 0; levelIndex < Levels.size(); levelIndex++) { + if (last && !Levels[levelIndex].GetKeysCount()) { + Y_ABORT_UNLESS(Levels[levelIndex].GetChildrenCount() == 1, "Should be root"); + return TBtreeIndexMeta{ Levels[levelIndex].PopChild(), levelIndex }; + } + + if (!TryFlush(levelIndex, pager, last)) { + Y_ABORT_UNLESS(!last); + break; + } + } + + Y_ABORT_UNLESS(!last, "Should have returned root"); + return { }; + } + + private: + bool TryFlush(size_t levelIndex, IPageWriter &pager, bool last) { + Y_ABORT_UNLESS(Levels[levelIndex].GetKeysCount(), "Shouldn't have empty levels"); + + if (!last && Levels[levelIndex].GetKeysCount() <= 2 * NodeKeysMax) { + // Note: node should meet both NodeKeysMin and NodeSize restrictions for split + + if (Levels[levelIndex].GetKeysCount() <= 2 * NodeKeysMin) { + // not enough keys for split + return false; + } + + // Note: this size check is approximate and we might not produce 2 full-sized pages + if (CalcPageSize(Levels[levelIndex]) <= 2 * NodeTargetSize) { + // not enough bytes for split + return false; + } + } + + Writer.EnsureEmpty(); + + // Note: for now we build last nodes from all remaining level's keys + // we may to try splitting them more evenly later + + while (last || Writer.GetKeysCount() < NodeKeysMin || Writer.CalcPageSize() < NodeTargetSize) { + if (!last && Levels[levelIndex].GetKeysCount() < 3) { + // we shouldn't produce empty nodes (but can violate NodeKeysMin restriction) + break; + } + if (!last && Writer.GetKeysCount() >= NodeKeysMax) { + // have enough keys + break; + } + if (last && !Levels[levelIndex].GetKeysCount()) { + // nothing left + break; + } + + Writer.AddChild(Levels[levelIndex].PopChild()); + Writer.AddKey(Levels[levelIndex].PopKey()); + } + auto lastChild = Levels[levelIndex].PopChild(); + Writer.AddChild(lastChild); + + auto pageId = pager.Write(Writer.Finish(), EPage::BTreeIndex, 0); + + if (levelIndex + 1 == Levels.size()) { + Levels.emplace_back(); + } + Levels[levelIndex + 1].PushChild(TChild{pageId, lastChild.Count, lastChild.ErasedCount, lastChild.Size}); + if (!last) { + Levels[levelIndex + 1].PushKey(Levels[levelIndex].PopKey()); + } + + if (last) { + Y_ABORT_UNLESS(!Levels[levelIndex].GetKeysCount()); + Y_ABORT_UNLESS(!Levels[levelIndex].GetKeysSize()); + Y_ABORT_UNLESS(!Levels[levelIndex].GetChildrenCount()); + } else { + Y_ABORT_UNLESS(Levels[levelIndex].GetKeysCount(), "Shouldn't leave empty levels"); + } + + return true; + } + + size_t CalcPageSize(TLevel& level) { + return Writer.CalcPageSize(level.GetKeysSize(), level.GetKeysCount()); + } + + private: + TBtreeIndexNodeWriter Writer; + TVector Levels; // from bottom to top + + const ui32 NodeTargetSize; + const ui32 NodeKeysMin; + const ui32 NodeKeysMax; + + TRowId ChildrenCount = 0; + TRowId ChildrenErasedCount = 0; + ui64 ChildrenSize = 0; + }; + } diff --git a/ydb/core/tablet_flat/test/libs/table/test_writer.h b/ydb/core/tablet_flat/test/libs/table/test_writer.h index f856ee1e8335..dfa969a5afb1 100644 --- a/ydb/core/tablet_flat/test/libs/table/test_writer.h +++ b/ydb/core/tablet_flat/test/libs/table/test_writer.h @@ -164,6 +164,11 @@ namespace NTest { { new TWriteStats(written), std::move(scheme), std::move(Parts) }; } + TStore& Back() noexcept + { + return Store ? *Store : *(Store = new TStore(Groups, NextGlobOffset)); + } + private: TPageId WriteOuter(TSharedData blob) noexcept override { @@ -186,11 +191,6 @@ namespace NTest { return Back().WriteLarge(TSharedData::Copy(blob)); } - TStore& Back() noexcept - { - return Store ? *Store : *(Store = new TStore(Groups, NextGlobOffset)); - } - void Finish(TString overlay) noexcept override { Y_ABORT_UNLESS(Store, "Finish called without any writes"); diff --git a/ydb/core/tablet_flat/ut/ut_btree_index.cpp b/ydb/core/tablet_flat/ut/ut_btree_index.cpp index fabcb71758f6..27724b477aba 100644 --- a/ydb/core/tablet_flat/ut/ut_btree_index.cpp +++ b/ydb/core/tablet_flat/ut/ut_btree_index.cpp @@ -1,5 +1,6 @@ #include "flat_page_btree_index.h" #include "flat_page_btree_index_writer.h" +#include "test/libs/table/test_writer.h" #include #include @@ -52,19 +53,38 @@ namespace { return TSerializedCellVec::Serialize(cells); } - void Dump(const NPage::TBtreeIndexNode& node, const TPartScheme& scheme) noexcept + TChild MakeChild(ui32 index) { + return TChild{index + 10000, index + 100, index + 30, index + 1000}; + } + + void Dump(TChild page, const TPartScheme& scheme, const TStore& store, ui32 level = 0) noexcept { + TString intend(level * 2, ' '); + auto dumpChild = [&] (TChild child) { + if (child.PageId < 1000) { + Dump(child, scheme, store, level + 1); + } else { + Cerr << intend << "| " << child.ToString() << Endl; + } + }; + + auto node = TBtreeIndexNode(*store.GetPage(0, page.PageId)); + auto label = node.Label(); Cerr - << " + BTreeIndex{" << (ui16)label.Type << " rev " - << label.Format << ", " << label.Size << "b}" + << intend + << "+ BTreeIndex{id=" << page.PageId << ", " + << "cnt=" << page.Count << ", " + << "size=" << page.Size << ", " + << (ui16)label.Type << " rev " << label.Format << ", " + << label.Size << "b}" << Endl; - Cerr << node.GetChild(0).ToString() << Endl; + dumpChild(node.GetChild(0)); for (TRecIdx i : xrange(node.GetKeysCount())) { - Cerr << "> "; + Cerr << intend << "|-> "; auto cells = node.GetKeyCells(i, scheme.Groups[0].ColsKeyIdx); for (TPos pos : xrange(cells.Count())) { @@ -77,12 +97,19 @@ namespace { } Cerr << Endl; - Cerr << node.GetChild(i + 1).ToString() << Endl; + dumpChild(node.GetChild(i + 1)); } Cerr << Endl; } + void Dump(TSharedData node, const TPartScheme& scheme) { + TWriterBundle pager(1, TLogoBlobID()); + auto pageId = ((IPageWriter*)&pager)->Write(node, EPage::BTreeIndex, 0); + TChild page{pageId, 0, 0, 0}; + Dump(page, scheme, pager.Back()); + } + void CheckKeys(const NPage::TBtreeIndexNode& node, const TVector& keys, const TPartScheme& scheme) { UNIT_ASSERT_VALUES_EQUAL(node.GetKeysCount(), keys.size()); for (TRecIdx i : xrange(node.GetKeysCount())) { @@ -100,6 +127,12 @@ namespace { } } + void CheckKeys(TPageId pageId, const TVector& keys, const TPartScheme& scheme, const TStore& store) { + auto page = store.GetPage(0, pageId); + auto node = TBtreeIndexNode(*page); + CheckKeys(node, keys, scheme); + } + void CheckChildren(const NPage::TBtreeIndexNode& node, const TVector& children) { UNIT_ASSERT_VALUES_EQUAL(node.GetKeysCount() + 1, children.size()); for (TRecIdx i : xrange(node.GetKeysCount() + 1)) { @@ -155,11 +188,12 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { TVector children; for (ui32 i : xrange(keys.size() + 1)) { - children.push_back(TChild{i * 10, i * 100, i * 30, i * 1000}); + children.push_back(MakeChild(i)); } for (auto k : keys) { - writer.AddKey(k); + TSerializedCellVec deserialized(k); + writer.AddKey(deserialized.GetCells()); } for (auto c : children) { writer.AddChild(c); @@ -169,7 +203,7 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { auto node = TBtreeIndexNode(serialized); - Dump(node, *writer.Scheme); + Dump(serialized, *writer.Scheme); CheckKeys(node, keys, *writer.Scheme); CheckChildren(node, children); } @@ -185,11 +219,12 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { TVector children; for (ui32 i : xrange(keys.size() + 1)) { - children.push_back(TChild{i * 10, i * 100, i * 30, i * 1000}); + children.push_back(MakeChild(i)); } for (auto k : keys) { - writer.AddKey(k); + TSerializedCellVec deserialized(k); + writer.AddKey(deserialized.GetCells()); } for (auto c : children) { writer.AddChild(c); @@ -199,7 +234,7 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { auto node = TBtreeIndexNode(serialized); - Dump(node, *writer.Scheme); + Dump(serialized, *writer.Scheme); CheckKeys(node, keys, *writer.Scheme); CheckChildren(node, children); }; @@ -227,11 +262,12 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { TVector children; for (ui32 i : xrange(keys.size() + 1)) { - children.push_back(TChild{i * 10, i * 100, i * 30, i * 1000}); + children.push_back(MakeChild(i)); } for (auto k : keys) { - writer.AddKey(k); + TSerializedCellVec deserialized(k); + writer.AddKey(deserialized.GetCells()); } for (auto c : children) { writer.AddChild(c); @@ -241,7 +277,8 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { keys.erase(keys.begin()); children.erase(children.begin()); for (auto k : keys) { - writer.AddKey(k); + TSerializedCellVec deserialized(k); + writer.AddKey(deserialized.GetCells()); } for (auto c : children) { writer.AddChild(c); @@ -251,7 +288,7 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { auto node = TBtreeIndexNode(serialized); - Dump(node, *writer.Scheme); + Dump(serialized, *writer.Scheme); CheckKeys(node, keys, *writer.Scheme); CheckChildren(node, children); } @@ -281,11 +318,12 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { TVector children; for (ui32 i : xrange(fullKeys.size() + 1)) { - children.push_back(TChild{i * 10, i * 100, i * 30, i * 1000}); + children.push_back(MakeChild(i)); } for (auto k : cutKeys) { - writer.AddKey(k); + TSerializedCellVec deserialized(k); + writer.AddKey(deserialized.GetCells()); } for (auto c : children) { writer.AddChild(c); @@ -295,11 +333,170 @@ Y_UNIT_TEST_SUITE(TBtreeIndexNode) { auto node = TBtreeIndexNode(serialized); - Dump(node, *writer.Scheme); + Dump(serialized, *writer.Scheme); CheckKeys(node, fullKeys, *writer.Scheme); CheckChildren(node, children); } } +Y_UNIT_TEST_SUITE(TBtreeIndexBuilder) { + using namespace NTest; + using TChild = TBtreeIndexNode::TChild; + + Y_UNIT_TEST(OneNode) { + TLayoutCook lay = MakeLayout(); + TIntrusivePtr scheme = new TPartScheme(lay.RowScheme()->Cols); + + TBtreeIndexBuilder builder(scheme, { }, Max(), Max(), Max()); + + TVector keys; + for (ui32 i : xrange(10)) { + keys.push_back(MakeKey(i, std::string{char('a' + i)}, i % 2, i * 10)); + } + TVector children; + for (ui32 i : xrange(keys.size() + 1)) { + children.push_back(MakeChild(i)); + } + + for (auto k : keys) { + TSerializedCellVec deserialized(k); + builder.AddKey(deserialized.GetCells()); + } + for (auto c : children) { + builder.AddChild(c); + } + + TWriterBundle pager(1, TLogoBlobID()); + auto result = builder.Flush(pager, true); + UNIT_ASSERT(result); + + Dump(*result, *scheme, pager.Back()); + + UNIT_ASSERT_VALUES_EQUAL(result->LevelsCount, 1); + CheckKeys(result->PageId, keys, *scheme, pager.Back()); + } + + Y_UNIT_TEST(FewNodes) { + TLayoutCook lay = MakeLayout(); + TIntrusivePtr scheme = new TPartScheme(lay.RowScheme()->Cols); + + TBtreeIndexBuilder builder(scheme, { }, Max(), 1, 2); + + TVector keys; + for (ui32 i : xrange(20)) { + keys.push_back(MakeKey(i, std::string{char('a' + i)}, i % 2, i * 10)); + } + TVector children; + for (ui32 i : xrange(keys.size() + 1)) { + children.push_back(MakeChild(i)); + } + + TWriterBundle pager(1, TLogoBlobID()); + + builder.AddChild(children[0]); + for (ui32 i : xrange(keys.size())) { + TSerializedCellVec deserialized(keys[i]); + builder.AddKey(deserialized.GetCells()); + builder.AddChild(children[i + 1]); + UNIT_ASSERT(!builder.Flush(pager, false)); + } + + auto result = builder.Flush(pager, true); + UNIT_ASSERT(result); + + Dump(*result, *scheme, pager.Back()); + + UNIT_ASSERT_VALUES_EQUAL(result->LevelsCount, 3); + + auto checkKeys = [&](TPageId pageId, const TVector& keys) { + CheckKeys(pageId, keys, *scheme, pager.Back()); + }; + + // Level 0: + checkKeys(0, { + keys[0], keys[1] + }); + // -> keys[2] + checkKeys(1, { + keys[3], keys[4] + }); + // -> keys[5] + checkKeys(2, { + keys[6], keys[7] + }); + // -> keys[8] + checkKeys(3, { + keys[9], keys[10] + }); + // -> keys[11] + checkKeys(4, { + keys[12], keys[13] + }); + // -> keys[14] + checkKeys(6, { + keys[15], keys[16] + }); + // -> keys[17] + checkKeys(7, { + keys[18], keys[19] + }); + + // Level 1: + checkKeys(5, { + keys[2], keys[5] + }); + checkKeys(8, { + keys[11], keys[14], keys[17] + }); + + // Level 2 (root): + checkKeys(9, { + keys[8] + }); + + TChild expected{9, 0, 0, 0}; + for (auto c : children) { + expected.Count += c.Count; + expected.ErasedCount += c.ErasedCount; + expected.Size += c.Size; + } + UNIT_ASSERT_EQUAL(*result, expected); + } + + Y_UNIT_TEST(SplitBySize) { + TLayoutCook lay = MakeLayout(); + TIntrusivePtr scheme = new TPartScheme(lay.RowScheme()->Cols); + + TBtreeIndexBuilder builder(scheme, { }, 600, 1, Max()); + + TVector keys; + for (ui32 i : xrange(100)) { + keys.push_back(MakeKey(i, TString(i + 1, 'x'))); + } + TVector children; + for (ui32 i : xrange(keys.size() + 1)) { + children.push_back(MakeChild(i)); + } + + TWriterBundle pager(1, TLogoBlobID()); + + builder.AddChild(children[0]); + for (ui32 i : xrange(keys.size())) { + TSerializedCellVec deserialized(keys[i]); + builder.AddKey(deserialized.GetCells()); + builder.AddChild(children[i + 1]); + UNIT_ASSERT(!builder.Flush(pager, false)); + } + + auto result = builder.Flush(pager, true); + UNIT_ASSERT(result); + + Dump(*result, *scheme, pager.Back()); + + UNIT_ASSERT_VALUES_EQUAL(result->LevelsCount, 3); + } + +} + }