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>
111 lines
5.4 KiB
C++
111 lines
5.4 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Verify() reports a heap handle pointing outside the committed heap and an
|
|
// ordered column out of sequence, and is silent on a healthy file; the
|
|
// type-erased DynamicRow reads any column by name without the struct.
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
struct Reading {
|
|
std::string sensor;
|
|
std::optional<std::string> comment;
|
|
double value;
|
|
std::int64_t at;
|
|
|
|
static constexpr std::uint32_t version = 1;
|
|
static constexpr auto orderedBy = &Reading::at;
|
|
};
|
|
|
|
void Patch(const fs::path& path, std::uint64_t offset, std::span<const std::byte> bytes) {
|
|
std::fstream file(path, std::ios::in | std::ios::out | std::ios::binary);
|
|
file.seekp(static_cast<std::streamoff>(offset));
|
|
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const fs::path dir = fs::temp_directory_path() / "crafter-db-verify";
|
|
fs::remove_all(dir);
|
|
const fs::path path = dir / "r.cdb";
|
|
std::uint64_t rowsOffset = 0;
|
|
{
|
|
DB::Table<Reading> table(path);
|
|
table.Append({ "thermometer-kitchen", std::nullopt, 21.5, 100 });
|
|
table.Append({ "hygrometer", std::string("calibrated last week"), 40.0, 200 });
|
|
table.Append({ "barometer-outside-wall", std::nullopt, 1013.25, 300 });
|
|
table.Erase(1);
|
|
const DB::VerifyResult healthy = table.Verify();
|
|
Check(healthy.Ok(), healthy.Ok() ? "healthy table verifies" : std::format("healthy table verifies: {}", healthy.problems[0]));
|
|
|
|
const DB::DynamicRow dynamic = table.File().Dynamic(2);
|
|
const std::optional<std::size_t> sensor = dynamic.Find("sensor");
|
|
Check(sensor && dynamic.Text(*sensor) == "barometer-outside-wall", "DynamicRow text by name");
|
|
const std::optional<std::size_t> value = dynamic.Find("value");
|
|
Check(value && dynamic.Scalar(*value) && std::get<double>(*dynamic.Scalar(*value)) == 1013.25, "DynamicRow double by name");
|
|
const std::optional<std::size_t> comment = dynamic.Find("comment");
|
|
Check(comment && !dynamic.Present(*comment), "DynamicRow sees a disengaged optional");
|
|
Check(table.File().Dynamic(1).Deleted() && table.File().Dynamic(1).Present(*comment) && table.File().Dynamic(1).Text(*comment) == "calibrated last week", "DynamicRow on a tombstoned row with an engaged optional");
|
|
const std::string described = dynamic.Describe();
|
|
Check(described.find("sensor=\"barometer-outside-wall\"") != std::string::npos && described.find("comment=null") != std::string::npos && described.find("at=300") != std::string::npos, std::format("Describe: {}", described));
|
|
Check(!dynamic.Find("nope").has_value(), "unknown column name");
|
|
|
|
table.Flush();
|
|
const DB::Inspection info = DB::TableFile::Inspect(path);
|
|
rowsOffset = info.header.extents[0][0].fileOffset;
|
|
}
|
|
|
|
// Damage the heap offset of row 2's long sensor name (heap value).
|
|
{
|
|
const fs::path damaged = dir / "handle.cdb";
|
|
fs::copy_file(path, damaged);
|
|
const DB::Inspection info = DB::TableFile::Inspect(damaged);
|
|
std::uint16_t sensorOffset = 0;
|
|
for (std::size_t i = 0; i < info.schema.columns.size(); i++) {
|
|
if (info.schema.Name(i) == "sensor") sensorOffset = info.schema.columns[i].offset;
|
|
}
|
|
const std::uint64_t bogus = 1 << 30;
|
|
Patch(damaged, rowsOffset + 2 * info.header.rowSize + sensorOffset + 8, std::as_bytes(std::span(&bogus, 1)));
|
|
const DB::Table<Reading> table(damaged, { .readOnly = true });
|
|
const DB::VerifyResult result = table.Verify();
|
|
Check(!result.Ok() && result.problems[0].find("row 2") != std::string::npos && result.problems[0].find("sensor") != std::string::npos, result.Ok() ? "damaged handle reported" : std::format("damaged handle reported: {}", result.problems[0]));
|
|
bool threw = false;
|
|
try {
|
|
[[maybe_unused]] const std::string_view text = table[2][&Reading::sensor];
|
|
} catch (const DB::CorruptError&) {
|
|
threw = true;
|
|
}
|
|
Check(threw, "reading through the damaged handle throws CorruptError instead of faulting");
|
|
Check(table[0][&Reading::sensor] == "thermometer-kitchen", "other rows still read");
|
|
}
|
|
|
|
// Break the order of the ordered column.
|
|
{
|
|
const fs::path damaged = dir / "order.cdb";
|
|
fs::copy_file(path, damaged);
|
|
const DB::Inspection info = DB::TableFile::Inspect(damaged);
|
|
const std::int64_t early = 50;
|
|
Patch(damaged, rowsOffset + 2 * info.header.rowSize + info.schema.columns[info.header.orderedColumn].offset, std::as_bytes(std::span(&early, 1)));
|
|
const DB::Table<Reading> table(damaged, { .readOnly = true });
|
|
const DB::VerifyResult result = table.Verify();
|
|
Check(!result.Ok() && result.problems[0].find("out of order") != std::string::npos, result.Ok() ? "order violation reported" : std::format("order violation reported: {}", result.problems[0]));
|
|
}
|
|
|
|
fs::remove_all(dir);
|
|
if (failures == 0) std::println("ok");
|
|
return failures == 0 ? 0 : 1;
|
|
}
|