Header-only C++26 module library for typed, file-backed tables with schema reflection, migrations, queries, and crash-safe commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
102 lines
4.2 KiB
C++
102 lines
4.2 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Grows a table through many extents while holding Row views and
|
|
// string_views taken before the growth. Guards the fixed-address mapping
|
|
// promise: appends never move existing rows or heap bytes.
|
|
|
|
import Crafter.DB;
|
|
import std;
|
|
|
|
using namespace Crafter;
|
|
namespace fs = std::filesystem;
|
|
using TimePoint = std::chrono::system_clock::time_point;
|
|
|
|
namespace {
|
|
int failures = 0;
|
|
|
|
void Check(bool condition, std::string_view what) {
|
|
if (!condition) {
|
|
std::println("FAIL: {}", what);
|
|
++failures;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
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 Event {
|
|
std::string label;
|
|
std::string payload;
|
|
std::uint64_t sequence;
|
|
|
|
static constexpr std::uint32_t version = 1;
|
|
static constexpr auto orderedBy = &Event::sequence;
|
|
};
|
|
}
|
|
|
|
int main() {
|
|
const fs::path dir = fs::temp_directory_path() / "crafter-db-growth";
|
|
fs::remove_all(dir);
|
|
const fs::path path = dir / "events.cdb";
|
|
constexpr std::uint64_t count = 300'000;
|
|
|
|
{
|
|
DB::Table<Event> table(path);
|
|
table.Emplace(std::string_view("first"), std::string_view("payload of the very first row, long enough for the heap"), std::uint64_t(0));
|
|
const DB::Row<Event> first = table[0];
|
|
const std::string_view firstPayload = first[&Event::payload];
|
|
const std::byte* firstBytes = first.Bytes();
|
|
const char* firstHeapPointer = firstPayload.data();
|
|
|
|
std::vector<Event> batch;
|
|
batch.reserve(1000);
|
|
for (std::uint64_t i = 1; i < count; i++) {
|
|
batch.push_back({ std::format("row-{}", i), std::format("payload-{}-{}", i, std::string(i % 40, 'p')), i });
|
|
if (batch.size() == 1000) {
|
|
table.Append(batch);
|
|
batch.clear();
|
|
}
|
|
}
|
|
if (!batch.empty()) table.Append(batch);
|
|
|
|
CheckEqual<std::uint64_t>(table.Size(), count, "all rows appended");
|
|
Check(first.Bytes() == firstBytes, "row pointer unchanged after growth");
|
|
Check(firstPayload.data() == firstHeapPointer, "heap pointer unchanged after growth");
|
|
Check(firstPayload == "payload of the very first row, long enough for the heap", "early string_view still reads correctly");
|
|
Check(first[&Event::label] == "first", "early row view still decodes");
|
|
Check(table[count - 1][&Event::label] == std::format("row-{}", count - 1), "last row readable");
|
|
Check(table[123456][&Event::payload] == std::format("payload-{}-{}", 123456, std::string(123456 % 40, 'p')), "middle row readable");
|
|
|
|
const DB::TableStats stats = table.Stats();
|
|
Check(stats.fileLength > count * DB::Layout<Event>::rowSize, "file holds the rows");
|
|
table.Flush();
|
|
const DB::Inspection info = DB::TableFile::Inspect(path);
|
|
Check(info.commit.extentCount[0] >= 5, std::format("rows region grew through several extents ({})", info.commit.extentCount[0]));
|
|
Check(info.commit.extentCount[1] >= 5, std::format("heap region grew through several extents ({})", info.commit.extentCount[1]));
|
|
CheckEqual<std::uint64_t>(info.commit.rowCount, count, "committed row count");
|
|
Check(info.commit.fileLength == fs::file_size(path), "committed length equals file size after flush");
|
|
}
|
|
|
|
{
|
|
const DB::Table<Event> table(path, { .readOnly = true });
|
|
CheckEqual<std::uint64_t>(table.Size(), count, "rows survive reopen");
|
|
std::uint64_t expected = 0;
|
|
bool ordered = true;
|
|
for (DB::Row<Event> row : table) {
|
|
if (row[&Event::sequence] != expected) ordered = false;
|
|
expected++;
|
|
}
|
|
Check(ordered && expected == count, "sequence intact after reopen");
|
|
CheckEqual<std::uint64_t>(table.LowerBound<&Event::sequence>(150'000), 150'000, "binary search over a multi-extent table");
|
|
}
|
|
|
|
fs::remove_all(dir);
|
|
if (failures == 0) std::println("ok");
|
|
return failures == 0 ? 0 : 1;
|
|
}
|