Initial commit: Crafter.DB
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>
This commit is contained in:
commit
bfd0cc261b
34 changed files with 7506 additions and 0 deletions
94
tests/ShouldReadWhileAppending/main.cpp
Normal file
94
tests/ShouldReadWhileAppending/main.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Readers scanning (with tombstones present, so the per-row flag test is on)
|
||||
// while one writer appends and erases. Every row a scan yields must be a
|
||||
// published, live-at-some-point row with increasing ids, and nothing may
|
||||
// read past the published slots. Guards the sentinel-based iteration and the
|
||||
// tombstone flag against the Append race.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 16-byte rows: a power-of-two size, so a full extent ends exactly on a
|
||||
// row boundary and the slot after the last published one can be the
|
||||
// first byte of the unmapped reservation.
|
||||
struct Tick {
|
||||
std::uint64_t sequence;
|
||||
std::uint64_t value;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
static constexpr auto orderedBy = &Tick::sequence;
|
||||
};
|
||||
static_assert(DB::Layout<Tick>::rowSize == 24 || DB::Layout<Tick>::rowSize == 32);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path dir = fs::temp_directory_path() / "crafter-db-concurrent";
|
||||
fs::remove_all(dir);
|
||||
DB::Table<Tick> table(dir / "ticks.cdb");
|
||||
table.Append({ 0, 0 });
|
||||
table.Append({ 1, 1 });
|
||||
table.Erase(0); // a tombstone from the start: scans run with the flag test
|
||||
|
||||
std::atomic<bool> stop{ false };
|
||||
std::atomic<std::uint64_t> scans{ 0 };
|
||||
std::atomic<std::uint64_t> badOrder{ 0 };
|
||||
std::atomic<std::uint64_t> badId{ 0 };
|
||||
std::atomic<std::uint64_t> sawErased{ 0 };
|
||||
|
||||
const auto reader = [&] {
|
||||
while (!stop.load(std::memory_order_relaxed)) {
|
||||
std::uint64_t last = 0;
|
||||
bool first = true;
|
||||
const std::uint64_t slots = table.SlotCount();
|
||||
for (DB::Row<Tick> row : table) {
|
||||
const std::uint64_t id = row.Id();
|
||||
if (!first && id <= last) badOrder.fetch_add(1, std::memory_order_relaxed);
|
||||
if (id >= table.SlotCount()) badId.fetch_add(1, std::memory_order_relaxed);
|
||||
if (row[&Tick::sequence] != id) badId.fetch_add(1, std::memory_order_relaxed);
|
||||
first = false;
|
||||
last = id;
|
||||
}
|
||||
if (slots > 2 && table.Rows(0, 1).Any()) sawErased.fetch_add(1, std::memory_order_relaxed);
|
||||
scans.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::jthread> readers;
|
||||
for (std::int32_t i = 0; i < 4; i++) readers.emplace_back(reader);
|
||||
|
||||
std::vector<Tick> batch;
|
||||
for (std::uint64_t sequence = 2; sequence < 400'000; sequence++) {
|
||||
batch.push_back({ sequence, sequence });
|
||||
if (batch.size() == 64) {
|
||||
table.Append(batch);
|
||||
batch.clear();
|
||||
if (sequence % 1024 == 1) table.Erase(sequence - 500);
|
||||
}
|
||||
}
|
||||
stop.store(true);
|
||||
readers.clear();
|
||||
|
||||
Check(scans.load() > 0, "readers ran");
|
||||
Check(badOrder.load() == 0, std::format("ids strictly increase within a scan ({} violations)", badOrder.load()));
|
||||
Check(badId.load() == 0, std::format("every yielded row is a published row ({} violations)", badId.load()));
|
||||
Check(sawErased.load() == 0, std::format("the erased first row never shows up ({} times)", sawErased.load()));
|
||||
Check(table.Verify().Ok(), "table verifies after the run");
|
||||
|
||||
fs::remove_all(dir);
|
||||
if (failures == 0) std::println("ok");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue