Crafter.DB/tests/ShouldKeepLayoutGolden/main.cpp

171 lines
7.5 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Pins the on-disk layout of the reference Transaction row: 88 bytes, the
// documented column offsets, the fingerprint recomputed from an independent
// spelling of the canonical hash, and the exact bytes of the 16-byte string
// handle for an empty, an inline and a heap value. A change here is a file
// format change.
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;
}
}
enum class Currency : std::uint16_t { Euro };
struct Transaction {
std::string name;
std::string bic;
std::string iban;
std::string description;
Currency currency;
std::int64_t amount;
TimePoint timePoint;
static constexpr std::uint32_t version = 1;
static constexpr auto orderedBy = &Transaction::timePoint;
};
using Layout = DB::Layout<Transaction>;
// The documented row: flags at 0, ascending alignment classes after it.
struct ExpectedColumn {
std::string_view name;
DB::Kind kind;
std::uint16_t offset;
std::uint16_t size;
std::uint8_t flags;
std::uint32_t paramNum;
std::uint32_t paramDen;
std::uint32_t variant;
};
constexpr std::uint32_t systemClock = DB::TypeCode<std::chrono::system_clock>();
constexpr std::array<ExpectedColumn, 8> expected = { {
{ "flags", DB::Kind::Flags, 0, 1, 0, 0, 0, 0 },
{ "currency", DB::Kind::UInt16, 2, 2, DB::columnFlagEnum, 0, 0, 0 },
{ "name", DB::Kind::String, 8, 16, 0, 0, 0, 0 },
{ "bic", DB::Kind::String, 24, 16, 0, 0, 0, 0 },
{ "iban", DB::Kind::String, 40, 16, 0, 0, 0, 0 },
{ "description", DB::Kind::String, 56, 16, 0, 0, 0, 0 },
{ "amount", DB::Kind::Int64, 72, 8, 0, 0, 0, 0 },
{ "timePoint", DB::Kind::TimePoint, 80, 8, 0, 1, 1000000, systemClock },
} };
constexpr std::uint16_t orderedColumn = 7; // timePoint
// Independent spelling of the fingerprint, written from the format
// description rather than from Layout's code.
std::uint64_t SpecFingerprint() {
std::uint64_t hash = DB::Fnv1a(DB::fnvOffset, "crafter-db-v1");
hash = DB::Fnv1a(hash, std::uint64_t(88));
hash = DB::Fnv1a(hash, std::uint64_t(8));
hash = DB::Fnv1a(hash, std::uint64_t(orderedColumn));
for (const ExpectedColumn& c : expected) {
hash = DB::Fnv1a(hash, c.name);
hash = DB::Fnv1a(hash, static_cast<std::uint64_t>(c.kind));
hash = DB::Fnv1a(hash, c.offset);
hash = DB::Fnv1a(hash, c.size);
hash = DB::Fnv1a(hash, c.flags);
hash = DB::Fnv1a(hash, c.paramNum);
hash = DB::Fnv1a(hash, c.paramDen);
hash = DB::Fnv1a(hash, c.variant);
}
return hash;
}
}
int main() {
static_assert(Layout::rowSize == 88);
static_assert(Layout::rowAlign == 8);
static_assert(Layout::columnCount == 8);
for (std::size_t i = 0; i < expected.size(); i++) {
const DB::ColumnInfo& column = Layout::columns[i];
CheckEqual<std::string_view>(column.name.View(), expected[i].name, std::format("column {} name", i));
Check(column.kind == expected[i].kind, std::format("column {} kind", i));
CheckEqual<std::uint16_t>(column.offset, expected[i].offset, std::format("column {} offset", i));
CheckEqual<std::uint16_t>(column.size, expected[i].size, std::format("column {} size", i));
CheckEqual<std::uint8_t>(column.flags, expected[i].flags, std::format("column {} flags", i));
CheckEqual<std::uint32_t>(column.paramNum, expected[i].paramNum, std::format("column {} paramNum", i));
CheckEqual<std::uint32_t>(column.paramDen, expected[i].paramDen, std::format("column {} paramDen", i));
CheckEqual<std::uint32_t>(column.variant, expected[i].variant, std::format("column {} variant", i));
}
CheckEqual<std::uint16_t>(Layout::orderedColumn, orderedColumn, "ordered column index");
CheckEqual<std::uint64_t>(Layout::fingerprint, SpecFingerprint(), "fingerprint matches the specification");
CheckEqual<std::uint64_t>(DB::MakeSchemaInfo<Transaction>().Fingerprint(Layout::orderedColumn), Layout::fingerprint, "SchemaInfo fingerprint matches Layout");
// Descriptor blob round trip.
const DB::SchemaInfo info = DB::MakeSchemaInfo<Transaction>();
const std::vector<std::byte> blob = info.Serialize();
DB::SchemaInfo parsed;
Check(DB::SchemaInfo::Parse(blob, info.rowAlign, parsed), "schema blob parses");
CheckEqual<std::size_t>(parsed.columns.size(), 8, "parsed column count");
CheckEqual<std::string_view>(parsed.Name(4), "iban", "parsed column name");
CheckEqual<std::uint64_t>(parsed.Fingerprint(Layout::orderedColumn), Layout::fingerprint, "parsed fingerprint");
// Raw handle bytes.
const fs::path dir = fs::temp_directory_path() / "crafter-db-layout";
fs::remove_all(dir);
{
DB::Table<Transaction> table(dir / "t.cdb");
table.Append({ "", "ABNANL2A", "NL91ABNA0417164300", "", Currency::Euro, 1, TimePoint(std::chrono::microseconds(1)) });
const std::byte* row = table[0].Bytes();
std::uint8_t flags;
std::memcpy(&flags, row, 1);
CheckEqual<std::uint8_t>(flags, 0, "live row has flags 0");
DB::Var empty;
std::memcpy(&empty, row + 8, 16);
Check(empty.length == 0 && empty.prefix == 0 && empty.tailOrOffset == 0, "empty string is an all-zero handle");
DB::Var bic;
std::memcpy(&bic, row + 24, 16);
CheckEqual<std::uint32_t>(bic.length, 8, "inline handle length");
std::array<char, 12> inlineBytes{};
std::memcpy(inlineBytes.data(), row + 24 + 4, 12);
CheckEqual<std::string>(std::string(inlineBytes.data(), 8), "ABNANL2A", "inline handle bytes 4..12 hold the value");
Check(inlineBytes[8] == 0 && inlineBytes[11] == 0, "inline handle is zero padded");
DB::Var iban;
std::memcpy(&iban, row + 40, 16);
CheckEqual<std::uint32_t>(iban.length, 18, "heap handle length");
std::array<char, 4> prefix{};
std::memcpy(prefix.data(), &iban.prefix, 4);
CheckEqual<std::string>(std::string(prefix.data(), 4), "NL91", "heap handle prefix");
const std::span<const std::byte> heapBytes = table.File().HeapView().Read(iban.tailOrOffset, iban.length);
CheckEqual<std::string>(std::string(reinterpret_cast<const char*>(heapBytes.data()), heapBytes.size()), "NL91ABNA0417164300", "heap holds the full value, prefix included");
CheckEqual<std::uint64_t>(iban.tailOrOffset, 0, "first heap value sits at heap offset 0");
CheckEqual<std::uint64_t>(table.Stats().heapUsed, 18, "heap used equals the one out-of-line value");
std::int64_t micros;
std::memcpy(&micros, row + 80, 8);
CheckEqual<std::int64_t>(micros, 1, "timePoint stored as its microsecond rep");
}
fs::remove_all(dir);
if (failures == 0) std::println("ok");
return failures == 0 ? 0 : 1;
}