// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Crafter.DB against a cleaned-up copy of Nostro's hand-rolled format // (iostream, uint16-prefixed strings inline, linear scans) on the five // operations Nostro performs. Prints a table; never fails. Runs on 100k // rows so a full suite stays fast; raise `rowCount` for a serious run. import Crafter.DB; import std; using namespace Crafter; namespace fs = std::filesystem; using TimePoint = std::chrono::system_clock::time_point; using Clock = std::chrono::steady_clock; namespace { 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; }; inline constexpr DB::Column<&Transaction::iban> iban{}; inline constexpr DB::Column<&Transaction::timePoint> timePoint{}; constexpr std::uint64_t rowCount = 100'000; std::vector MakeRows() { std::mt19937_64 rng(42); const std::array names = { "Albert Heijn", "Jumbo Supermarkten", "NS Reizigers", "Vattenfall", "Bol.com", "Gemeente Utrecht" }; const std::array bics = { "ABNANL2A", "RABONL2U", "INGBNL2A", "TRIONL2U" }; std::vector ibans; for (std::uint32_t i = 0; i < 1000; i++) ibans.push_back(std::format("NL{:02}ABNA{:010}", i % 97, 417164300u + i)); std::vector rows; rows.reserve(rowCount); for (std::uint64_t i = 0; i < rowCount; i++) { rows.push_back({ std::string(names[rng() % names.size()]), std::string(bics[rng() % bics.size()]), ibans[rng() % ibans.size()], std::format("payment {} for order {}", i, rng() % 100000), Currency::Euro, static_cast(rng() % 200000) - 100000, TimePoint(std::chrono::microseconds(1'700'000'000'000'000 + i * 60'000'000)) }); } return rows; } // --- the Nostro format, cleaned up ----------------------------------------- namespace Nostro { void WriteString(std::ofstream& file, std::string_view str) { const std::uint16_t size = static_cast(str.size()); file.write(reinterpret_cast(&size), sizeof(size)); file.write(str.data(), size); } void ReadString(std::ifstream& file, std::string& str) { std::uint16_t size; file.read(reinterpret_cast(&size), sizeof(size)); str.resize(size); file.read(str.data(), size); } void Save(const fs::path& path, std::span rows) { std::ofstream file(path, std::ios::binary | std::ios::app); for (const Transaction& tr : rows) { WriteString(file, tr.name); WriteString(file, tr.bic); WriteString(file, tr.iban); WriteString(file, tr.description); file.write(reinterpret_cast(&tr.amount), sizeof(tr.amount)); file.write(reinterpret_cast(&tr.currency), sizeof(Currency)); const std::int64_t time = std::chrono::duration_cast(tr.timePoint.time_since_epoch()).count(); file.write(reinterpret_cast(&time), sizeof(time)); } } bool Load(std::ifstream& file, Transaction& tr) { if (file.peek() == std::ifstream::traits_type::eof()) return false; ReadString(file, tr.name); ReadString(file, tr.bic); ReadString(file, tr.iban); ReadString(file, tr.description); file.read(reinterpret_cast(&tr.amount), sizeof(tr.amount)); file.read(reinterpret_cast(&tr.currency), sizeof(tr.currency)); std::int64_t time; file.read(reinterpret_cast(&time), sizeof(time)); tr.timePoint = TimePoint(std::chrono::microseconds(time)); return true; } TimePoint LastTime(const fs::path& path) { std::ifstream file(path, std::ios::binary); file.seekg(-static_cast(sizeof(std::int64_t)), std::ios::end); std::int64_t time; file.read(reinterpret_cast(&time), sizeof(time)); return TimePoint(std::chrono::microseconds(time)); } std::vector LoadAll(const fs::path& path) { std::ifstream file(path, std::ios::binary); std::vector out; Transaction tr; while (Load(file, tr)) out.push_back(tr); return out; } std::vector LoadRange(const fs::path& path, TimePoint from, TimePoint to) { std::ifstream file(path, std::ios::binary); std::vector out; Transaction tr; while (Load(file, tr)) { if (tr.timePoint >= from && tr.timePoint <= to) out.push_back(tr); if (tr.timePoint > to) break; } return out; } std::vector LoadByIban(const fs::path& path, std::string_view wanted) { std::ifstream file(path, std::ios::binary); std::vector out; Transaction tr; while (Load(file, tr)) { if (tr.iban == wanted) out.push_back(tr); } return out; } } template double Millis(F&& f) { const Clock::time_point start = Clock::now(); f(); return std::chrono::duration(Clock::now() - start).count(); } struct Line { std::string name; double nostro; double crafter; std::string note; }; } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-db-benchmark"; fs::remove_all(dir); fs::create_directories(dir); const fs::path nostroPath = dir / "nostro.bin"; const fs::path crafterPath = dir / "crafter.cdb"; const std::vector rows = MakeRows(); const std::string_view wantedIban = rows[rowCount / 2].iban; const TimePoint from = rows[rowCount * 45 / 100].timePoint; const TimePoint to = rows[rowCount * 55 / 100].timePoint; std::vector lines; std::size_t sink = 0; // append { const double nostro = Millis([&] { Nostro::Save(nostroPath, rows); }); const double crafter = Millis([&] { DB::Table table(crafterPath); table.Append(rows); table.Flush(); }); lines.push_back({ std::format("append {} rows", rowCount), nostro, crafter, "incl. open, Crafter.DB incl. fdatasync" }); } // last timestamp (1000 opens each) { const double nostro = Millis([&] { for (std::int32_t i = 0; i < 1000; i++) sink += Nostro::LastTime(nostroPath).time_since_epoch().count() & 1; }); const double crafter = Millis([&] { for (std::int32_t i = 0; i < 1000; i++) { const DB::Table table(crafterPath, { .readOnly = true }); sink += table.Back()[&Transaction::timePoint].time_since_epoch().count() & 1; } }); lines.push_back({ "last timestamp (1000x open+read)", nostro, crafter, "open dominates both" }); } DB::Table table(crafterPath, { .readOnly = true }); // load all { const double nostro = Millis([&] { sink += Nostro::LoadAll(nostroPath).size(); }); const double crafter = Millis([&] { sink += table.Materialize().size(); }); double zeroCopy = Millis([&] { std::int64_t total = 0; for (DB::Row row : table) total += row[&Transaction::amount] + static_cast(row[&Transaction::iban].Length()); sink += static_cast(total & 1); }); lines.push_back({ "load all (materialised)", nostro, crafter, std::format("zero-copy scan of two columns: {:.2f} ms", zeroCopy) }); } // time range (10% of rows) { std::size_t nostroCount = 0; std::size_t crafterCount = 0; const double nostro = Millis([&] { nostroCount = Nostro::LoadRange(nostroPath, from, to).size(); }); const double crafter = Millis([&] { crafterCount = table.Range<&Transaction::timePoint>(from, to).Materialize().size(); }); lines.push_back({ "time range (10% of rows, materialised)", nostro, crafter, std::format("{} / {} rows", nostroCount, crafterCount) }); } // iban == { std::size_t nostroCount = 0; std::size_t keyCount = 0; std::size_t exprCount = 0; std::size_t viewCount = 0; const double nostro = Millis([&] { nostroCount = Nostro::LoadByIban(nostroPath, wantedIban).size(); }); const double withKey = Millis([&] { const DB::Key key(wantedIban); keyCount = table.Where([&](DB::Row r) { return r[&Transaction::iban] == key; }).Count(); }); const double withExpression = Millis([&] { exprCount = table.Where(iban == wantedIban).Count(); }); const double withView = Millis([&] { viewCount = table.Where([&](DB::Row r) { return r[&Transaction::iban] == wantedIban; }).Count(); }); lines.push_back({ "iban == x (lambda + Key)", nostro, withKey, std::format("{} / {} rows, {:.1f} ns/row", nostroCount, keyCount, withKey * 1e6 / rowCount) }); lines.push_back({ "iban == x (expression)", nostro, withExpression, std::format("{} rows, {:.1f} ns/row", exprCount, withExpression * 1e6 / rowCount) }); lines.push_back({ "iban == x (lambda + string_view)", nostro, withView, std::format("{} rows, {:.1f} ns/row", viewCount, withView * 1e6 / rowCount) }); } // range + iban { std::size_t nostroCount = 0; std::size_t crafterCount = 0; const double nostro = Millis([&] { for (const Transaction& tr : Nostro::LoadRange(nostroPath, from, to)) nostroCount += tr.iban == wantedIban ? 1 : 0; }); const double crafter = Millis([&] { crafterCount = table.Where(timePoint >= from && timePoint <= to && iban == wantedIban).Count(); }); lines.push_back({ "time range + iban == x", nostro, crafter, std::format("{} / {} rows", nostroCount, crafterCount) }); } std::println("{:<42} {:>12} {:>12} {:>8} {}", "operation", "nostro ms", "crafter ms", "speedup", "note"); for (const Line& line : lines) { std::println("{:<42} {:>12.3f} {:>12.3f} {:>7.1f}x {}", line.name, line.nostro, line.crafter, line.nostro / std::max(line.crafter, 0.0001), line.note); } std::println("files: nostro {} bytes, crafter {} bytes", fs::file_size(nostroPath), fs::file_size(crafterPath)); std::println("(sink {})", sink & 1); fs::remove_all(dir); return 0; }