// 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 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; // 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(); constexpr std::array 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(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(column.name.View(), expected[i].name, std::format("column {} name", i)); Check(column.kind == expected[i].kind, std::format("column {} kind", i)); CheckEqual(column.offset, expected[i].offset, std::format("column {} offset", i)); CheckEqual(column.size, expected[i].size, std::format("column {} size", i)); CheckEqual(column.flags, expected[i].flags, std::format("column {} flags", i)); CheckEqual(column.paramNum, expected[i].paramNum, std::format("column {} paramNum", i)); CheckEqual(column.paramDen, expected[i].paramDen, std::format("column {} paramDen", i)); CheckEqual(column.variant, expected[i].variant, std::format("column {} variant", i)); } CheckEqual(Layout::orderedColumn, orderedColumn, "ordered column index"); CheckEqual(Layout::fingerprint, SpecFingerprint(), "fingerprint matches the specification"); CheckEqual(DB::MakeSchemaInfo().Fingerprint(Layout::orderedColumn), Layout::fingerprint, "SchemaInfo fingerprint matches Layout"); // Descriptor blob round trip. const DB::SchemaInfo info = DB::MakeSchemaInfo(); const std::vector blob = info.Serialize(); DB::SchemaInfo parsed; Check(DB::SchemaInfo::Parse(blob, info.rowAlign, parsed), "schema blob parses"); CheckEqual(parsed.columns.size(), 8, "parsed column count"); CheckEqual(parsed.Name(4), "iban", "parsed column name"); CheckEqual(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 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(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(bic.length, 8, "inline handle length"); std::array inlineBytes{}; std::memcpy(inlineBytes.data(), row + 24 + 4, 12); CheckEqual(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(iban.length, 18, "heap handle length"); std::array prefix{}; std::memcpy(prefix.data(), &iban.prefix, 4); CheckEqual(std::string(prefix.data(), 4), "NL91", "heap handle prefix"); const std::span heapBytes = table.File().HeapView().Read(iban.tailOrOffset, iban.length); CheckEqual(std::string(reinterpret_cast(heapBytes.data()), heapBytes.size()), "NL91ABNA0417164300", "heap holds the full value, prefix included"); CheckEqual(iban.tailOrOffset, 0, "first heap value sits at heap offset 0"); CheckEqual(table.Stats().heapUsed, 18, "heap used equals the one out-of-line value"); std::int64_t micros; std::memcpy(µs, row + 80, 8); CheckEqual(micros, 1, "timePoint stored as its microsecond rep"); } fs::remove_all(dir); if (failures == 0) std::println("ok"); return failures == 0 ? 0 : 1; }