// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Tombstones and compaction: erased rows disappear from iteration while // RowIds stay put, Back() skips trailing tombstones, Compact renumbers // densely and reclaims heap garbage, Clear empties, a stale temp file is // removed on open, and views taken before Compact are not used after it. import Crafter.DB; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } template void CheckEqual(const T& actual, const T& expected, std::string_view what) { if (!(actual == expected)) { std::println("FAIL: {} — expected '{}', got '{}'", what, expected, actual); ++failures; } } struct Note { std::string text; std::int32_t rank; static constexpr std::uint32_t version = 1; static constexpr auto orderedBy = &Note::rank; friend bool operator==(const Note&, const Note&) = default; }; struct Memo { std::optional text; std::int32_t id; static constexpr std::uint32_t version = 1; }; std::vector Ranks(const DB::Table& table) { std::vector ranks; for (DB::Row row : table) ranks.push_back(row[&Note::rank]); return ranks; } } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-db-compact"; fs::remove_all(dir); const fs::path path = dir / "notes.cdb"; { DB::Table table(path); for (std::int32_t i = 0; i < 10; i++) table.Append({ std::format("note number {} with a long body", i), i }); const std::uint64_t heapBefore = table.Stats().heapUsed; Check(table.Erase(2), "Erase returns true"); Check(!table.Erase(2), "second Erase returns false"); table.Erase(5); table.Erase(9); CheckEqual(table.Size(), 7, "Size after erase"); CheckEqual(table.SlotCount(), 10, "SlotCount unchanged"); CheckEqual(Ranks(table), std::vector{ 0, 1, 3, 4, 6, 7, 8 }, "iteration skips tombstones"); Check(table[7][&Note::rank] == 7, "RowIds stable after erase"); Check(table.Back()[&Note::rank] == 8, "Back skips a trailing tombstone"); Check(table.Front()[&Note::rank] == 0, "Front"); Check(!table.Contains(2) && table.Contains(3) && !table.Contains(10), "Contains"); Check(table[2].Erased(), "Erased flag visible through Slots"); bool threw = false; try { table.At(2); } catch (const std::out_of_range&) { threw = true; } Check(threw, "At on an erased row throws"); Check(table.Stats().heapGarbage == 3 * std::string("note number 0 with a long body").size(), "erase accounts heap garbage"); CheckEqual(table.EraseIf([](DB::Row r) { return r[&Note::rank] % 4 == 3; }), 2, "EraseIf count (3 and 7)"); CheckEqual(Ranks(table), std::vector{ 0, 1, 4, 6, 8 }, "EraseIf result"); table.Update(4, { "four", 4 }); Check(table[4][&Note::text] == "four", "Update in place"); const std::uint64_t sizeBefore = fs::file_size(path); table.Flush(); table.Compact(); CheckEqual(table.Size(), 5, "Size after Compact"); CheckEqual(table.SlotCount(), 5, "SlotCount after Compact"); CheckEqual(Ranks(table), std::vector{ 0, 1, 4, 6, 8 }, "order preserved by Compact"); Check(table[2][&Note::text] == "four", "rows renumbered densely"); Check(table[4][&Note::text] == "note number 8 with a long body", "heap values repacked correctly"); CheckEqual(table.Stats().heapGarbage, 0, "no garbage after Compact"); Check(table.Stats().heapUsed < heapBefore, "heap shrank"); Check(fs::file_size(path) <= sizeBefore, "file did not grow"); table.Append({ "after compact", 9 }); CheckEqual(table.Size(), 6, "append after Compact"); CheckEqual(table.Back().Id(), 5, "new row gets the next dense id"); Check(table.Back()[&Note::text] == "after compact", "append after Compact readable"); } { // A leftover temp file (crash during Compact) is cleaned on the next open. const fs::path stale = fs::path(path.string() + ".tmp-deadbeef0000"); { std::ofstream(stale) << "junk"; } DB::Table table(path); Check(!fs::exists(stale), "stale temp file removed on open"); CheckEqual(table.Size(), 6, "data intact after reopen"); table.Clear(); CheckEqual(table.Size(), 0, "Clear empties the table"); CheckEqual(table.SlotCount(), 0, "Clear resets slots"); table.Append({ "fresh", 1 }); CheckEqual(table.Back().Id(), 0, "ids restart after Clear"); } { const DB::Table table(path, { .readOnly = true }); CheckEqual(table.Size(), 1, "Clear + append persisted"); Check(table[0][&Note::text] == "fresh", "row persisted"); Check(fs::file_size(path) <= 3 * 65536, std::format("Clear did not pre-size the new file to the old data ({} bytes)", fs::file_size(path))); const auto plain = table.Rows(0, 1); std::uint64_t seen = 0; for (DB::Row row : plain) seen += row[&Note::rank]; CheckEqual(seen, 1, "const Query over a LiveRange iterates"); CheckEqual(plain.Count(), 1, "Count on a const Query"); CheckEqual(table.Rows(0, 1).Count(), 1, "Count on a prvalue Query"); static_assert(std::ranges::range, "a Query over a plain range is const-iterable"); static_assert(!std::ranges::range) { return true; }))>, "a filter-backed Query is not const-iterable (filter_view has no const begin)"); Check(table.Where([](DB::Row r) { return r[&Note::rank] == 1; }).Any(), "Any on a prvalue filter Query"); } { // A disengaged optional must not keep its old heap bytes alive. const fs::path memos = dir / "memos.cdb"; DB::Table table(memos); table.Append({ std::string("a note long enough to live in the heap"), 1 }); const std::uint64_t used = table.Stats().heapUsed; Check(used > 0, "optional string went to the heap"); table.Set<&Memo::text>(0, std::optional()); Check(!table[0][&Memo::text].has_value(), "optional cleared"); CheckEqual(table.Stats().heapGarbage, used, "clearing the optional orphans its heap bytes"); table.Compact(); CheckEqual(table.Stats().heapUsed, 0, "Compact dropped the dead payload"); DB::Var payload; std::memcpy(&payload, table[0].Bytes() + DB::Layout::Offset(1), sizeof(payload)); Check(payload.length == 0 && payload.tailOrOffset == 0, "payload handle is zero after clearing"); } fs::remove_all(dir); if (failures == 0) std::println("ok"); return failures == 0 ? 0 : 1; }