// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Append rows of every string length class (empty, inline, heap, huge, // UTF-8), read them back through views and through Materialize, reopen the // file and read them again. Guards the encode/decode symmetry and the file // round trip that everything else builds on. 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, Dollar }; 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; friend bool operator==(const Transaction&, const Transaction&) = default; }; TimePoint At(std::int64_t seconds) { return TimePoint(std::chrono::duration_cast(std::chrono::seconds(seconds))); } std::vector Sample() { std::vector rows; rows.push_back({ "Albert Heijn", "ABNANL2A", "NL91ABNA0417164300", "groceries", Currency::Euro, -2345, At(100) }); rows.push_back({ "", "", "", "", Currency::Euro, 0, At(100) }); // all empty, equal key allowed rows.push_back({ "Twelve chars", "ABCDEFGHIJKL", "ABCDEFGHIJKLM", "exactly twelve", Currency::Dollar, 1, At(101) }); // 12 inline, 13 heap rows.push_back({ "Café Münster", "RABONL2U", "DE89370400440532013000", std::string(100 * 1024, 'x'), Currency::Euro, 99999999, At(102) }); rows.push_back({ "a", "b", "c", "d", Currency::Euro, -1, At(103) }); return rows; } } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-db-roundtrip"; fs::remove_all(dir); const fs::path path = dir / "sub" / "main.cdb"; const std::vector sample = Sample(); { DB::Table table(path); Check(table.Empty(), "new table is empty"); CheckEqual(table.Size(), 0, "new table size"); Check(fs::exists(path), "table file created"); Check((fs::status(path).permissions() & fs::perms::others_all) == fs::perms::none, "file is private"); Check((fs::status(dir / "sub").permissions() & fs::perms::group_all) == fs::perms::none, "directory is private"); for (const Transaction& row : sample) table.Append(row); CheckEqual(table.Size(), sample.size(), "size after append"); CheckEqual(table.SlotCount(), sample.size(), "slots after append"); const DB::Row last = table.Back(); Check(last[&Transaction::timePoint] == At(103), "Back() timePoint via member pointer"); Check(last.Get<&Transaction::amount>() == -1, "Back() amount via Get<>"); Check(last[&Transaction::iban] == "c", "Back() iban view compares to literal"); CheckEqual(last.Id(), 4, "Back() id"); const DB::Row second = table[2]; Check(second[&Transaction::bic] == std::string_view("ABCDEFGHIJKL"), "12-char string inline round trip"); Check(second[&Transaction::iban] == std::string("ABCDEFGHIJKLM"), "13-char string heap round trip"); Check(second[&Transaction::iban] != std::string_view("ABCDEFGHIJKLX"), "same-length prefix-equal non-match"); Check(!(second[&Transaction::iban] == std::string_view("ABCDEFGHIJKL")), "shorter prefix-equal non-match"); Check(second[&Transaction::currency] == Currency::Dollar, "enum round trip"); CheckEqual(std::string(second[&Transaction::description].View()), "exactly twelve", "StringRef::View"); const DB::Row empty = table[1]; Check(empty[&Transaction::name].Empty(), "empty string is empty"); Check(empty[&Transaction::name] == "", "empty string equals empty literal"); const DB::Row huge = table[3]; CheckEqual(huge[&Transaction::description].Length(), 100 * 1024, "huge string length"); Check(huge[&Transaction::description] == sample[3].description, "huge string equals"); Check(huge[&Transaction::name] == "Café Münster", "utf-8 inline/heap string"); const std::vector all = table.Materialize(); Check(all == sample, "Materialize equals input"); CheckEqual(std::string(DB::TypeName()).substr(0, 0), "", "noop"); const DB::RowId id = table.Emplace(std::string_view("Emplaced"), "BICBICBI", std::string("NL00EMPL0000000001"), std::string_view("via Emplace"), Currency::Euro, std::int64_t(42), At(104)); CheckEqual(id, 5, "Emplace returns the new slot"); Check(table[id][&Transaction::name] == "Emplaced", "Emplace string_view argument"); Check(table[id][&Transaction::iban] == "NL00EMPL0000000001", "Emplace std::string argument"); Check(table[id][&Transaction::amount] == 42, "Emplace scalar argument"); const Transaction materialized = static_cast(table[id]); CheckEqual(materialized.description, "via Emplace", "explicit conversion to T"); const std::string described = std::format("{}", table[4]); Check(described.find("name=\"a\"") != std::string::npos && described.find("amount=-1") != std::string::npos, std::format("row formatter output: {}", described)); std::uint64_t seen = 0; for (DB::Row row : table) seen += row.Id() == seen ? 1 : 0; CheckEqual(seen, 6, "range-for visits every live row in order"); bool threw = false; try { table.Append(Transaction{ "late", "", "", "", Currency::Euro, 0, At(50) }); } catch (const DB::OrderError&) { threw = true; } Check(threw, "out-of-order append throws OrderError"); CheckEqual(table.Size(), 6, "failed append added nothing"); } { const DB::Table reopened(path, { .mode = DB::OpenMode::OpenExisting }); CheckEqual(reopened.Size(), 6, "size after reopen"); const std::vector all = reopened.Materialize(); Check(all.size() == 6 && std::equal(sample.begin(), sample.end(), all.begin()), "rows survive reopen"); Check(reopened.Back()[&Transaction::name] == "Emplaced", "Emplaced row survives reopen"); const DB::Inspection info = DB::TableFile::Inspect(path); CheckEqual(info.header.schemaVersion, 1, "Inspect: schema version"); CheckEqual(info.schema.columns.size(), 8, "Inspect: column count incl. flags"); CheckEqual(info.commit.rowCount, 6, "Inspect: committed rows"); Check(info.header.schemaFingerprint == DB::Layout::fingerprint, "Inspect: fingerprint matches Layout"); Check(DB::MakeSchemaInfo().Fingerprint(DB::Layout::orderedColumn) == DB::Layout::fingerprint, "runtime and compile-time fingerprints agree"); CheckEqual(DB::PeekVersion(path), 1, "PeekVersion"); } { const DB::Table frozen(path, { .readOnly = true }); CheckEqual(frozen.Size(), 6, "read-only open sees rows"); bool threw = false; try { DB::Table(path, { .readOnly = true }); // second shared lock must coexist } catch (const DB::Error&) { threw = true; } Check(!threw, "two read-only opens coexist"); } std::uint64_t temps = 0; for (const fs::directory_entry& entry : fs::directory_iterator(dir / "sub")) { if (entry.path().filename().string().find(".tmp-") != std::string::npos) temps++; } CheckEqual(temps, 0, "no temp files left behind"); bool threwExisting = false; try { DB::Table missing(dir / "missing.cdb", { .mode = DB::OpenMode::OpenExisting }); } catch (const DB::IOError&) { threwExisting = true; } Check(threwExisting, "OpenExisting on a missing file throws IOError"); fs::remove_all(dir); if (failures == 0) std::println("ok"); return failures == 0 ? 0 : 1; }