// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Queries: lambda filters with Key and string_view, column expressions, // binary search on the ordered column (Range/From/After/Until/Before, // Lower/UpperBound at the boundaries, equal keys, tombstoned slots), chaining // Range().Where(), bound extraction from a combined expression, ranges // composition, and the OrderError checks on Update/Set. 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; }; inline constexpr DB::Column<&Transaction::iban> iban{}; inline constexpr DB::Column<&Transaction::description> description{}; inline constexpr DB::Column<&Transaction::amount> amount{}; inline constexpr DB::Column<&Transaction::timePoint> timePoint{}; TimePoint At(std::int64_t seconds) { return TimePoint(std::chrono::duration_cast(std::chrono::seconds(seconds))); } std::vector Ids(auto&& query) { std::vector ids; for (auto row : query) ids.push_back(row.Id()); return ids; } template std::vector IdsOf(Query query) { std::vector ids; for (auto row : query) ids.push_back(row.Id()); return ids; } } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-db-query"; fs::remove_all(dir); DB::Table table(dir / "q.cdb"); const std::string_view ibanA = "NL91ABNA0417164300"; const std::string_view ibanB = "NL91ABNA0417164301"; // same length, same 4-byte prefix const std::string_view ibanC = "DE89370400440532013000"; // id key table.Append({ "a", "ABNANL2A", std::string(ibanA), "rent", Currency::Euro, -100000, At(10) }); // 0 table.Append({ "b", "ABNANL2A", std::string(ibanB), "rent", Currency::Euro, -200, At(20) }); // 1 table.Append({ "c", "RABONL2U", std::string(ibanC), "coffee", Currency::Euro, -350, At(20) }); // 2 equal key table.Append({ "d", "RABONL2U", std::string(ibanA), "salary", Currency::Euro, 300000, At(30) }); // 3 table.Append({ "e", "INGBNL2A", std::string(ibanC), "rent ", Currency::Euro, -100000, At(40) }); // 4 trailing space: same length as "rent" + 1 table.Append({ "f", "INGBNL2A", std::string(ibanA), "RENT", Currency::Euro, -5, At(50) }); // 5 // --- lambda filters --------------------------------------------------- const DB::Key keyA(ibanA); CheckEqual(IdsOf(table.Where([&](DB::Row r) { return r[&Transaction::iban] == keyA; })), std::vector{ 0, 3, 5 }, "lambda with Key"); CheckEqual(IdsOf(table.Where([&](DB::Row r) { return r[&Transaction::iban] == ibanB; })), std::vector{ 1 }, "lambda with string_view, prefix-equal neighbour rejected"); CheckEqual(IdsOf(table.Where([](DB::Row r) { return r[&Transaction::description] == "rent"; })), std::vector{ 0, 1 }, "description equality is exact (case and length)"); CheckEqual(table.Where([](DB::Row r) { return r[&Transaction::amount] < 0; }).Count(), std::uint64_t(5), "Count"); Check(table.Where(amount > 0).First().has_value() && table.Where(amount > 0).First()->Id() == 3, "First"); Check(!table.Where(amount > 1'000'000).Any(), "Any on no match"); // --- column expressions ------------------------------------------------- CheckEqual(IdsOf(table.Where(iban == ibanA)), std::vector{ 0, 3, 5 }, "expression =="); CheckEqual(IdsOf(table.Where(iban != ibanA)), std::vector{ 1, 2, 4 }, "expression !="); CheckEqual(IdsOf(table.Where(iban == ibanA && amount > 0)), std::vector{ 3 }, "expression &&"); CheckEqual(IdsOf(table.Where(description == "coffee" || description == "salary")), std::vector{ 2, 3 }, "expression ||"); CheckEqual(IdsOf(table.Where(!(amount < 0))), std::vector{ 3 }, "expression !"); CheckEqual(IdsOf(table.Where(description < "d")), std::vector{ 2, 5 }, "string ordering compare ('RENT' and 'coffee' sort before 'd')"); // --- ordered column ----------------------------------------------------- CheckEqual(table.LowerBound<&Transaction::timePoint>(At(20)), 1, "LowerBound on equal keys"); CheckEqual(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound on equal keys"); CheckEqual(table.LowerBound<&Transaction::timePoint>(At(5)), 0, "LowerBound below all"); CheckEqual(table.LowerBound<&Transaction::timePoint>(At(55)), 6, "LowerBound above all"); CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(40))), std::vector{ 1, 2, 3, 4 }, "Range inclusive both ends"); CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(21), At(29))), std::vector{}, "Range with no rows"); CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(40), At(20))), std::vector{}, "Range with inverted bounds is empty"); CheckEqual(IdsOf(table.From<&Transaction::timePoint>(At(30))), std::vector{ 3, 4, 5 }, "From"); CheckEqual(IdsOf(table.After<&Transaction::timePoint>(At(30))), std::vector{ 4, 5 }, "After"); CheckEqual(IdsOf(table.Until<&Transaction::timePoint>(At(20))), std::vector{ 0, 1, 2 }, "Until"); CheckEqual(IdsOf(table.Before<&Transaction::timePoint>(At(20))), std::vector{ 0 }, "Before"); // --- range + property --------------------------------------------------- const std::vector chained = IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where(iban == ibanA)); CheckEqual(chained, std::vector{ 3, 5 }, "Range().Where(expression)"); CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where([&](DB::Row r) { return r[&Transaction::iban] == keyA; })), chained, "Range().Where(lambda)"); CheckEqual(IdsOf(table.Where(timePoint >= At(20) && timePoint <= At(50) && iban == ibanA)), chained, "combined expression extracts bounds"); CheckEqual(IdsOf(table.Where(timePoint > At(20) && timePoint < At(50))), std::vector{ 3, 4 }, "exclusive bounds"); CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector{ 1, 2 }, "equality bound"); CheckEqual(IdsOf(table.Where(timePoint == At(20) || iban == ibanA)), std::vector{ 0, 1, 2, 3, 5 }, "|| disables bound extraction but stays correct"); // --- ranges composition ------------------------------------------------- std::uint64_t taken = 0; for (DB::Row row : table | std::views::filter([](DB::Row r) { return r[&Transaction::amount] < 0; }) | std::views::take(2)) taken += row.Id() + 1; CheckEqual(taken, 1 + 2, "views::filter | views::take over the table"); const std::int64_t total = std::ranges::fold_left(table.Select<&Transaction::amount>(), std::int64_t(0), std::plus{}); CheckEqual(total, -100000 - 200 - 350 + 300000 - 100000 - 5, "Select + fold_left"); CheckEqual(table.Slots().size(), 6, "Slots size"); Check(std::ranges::lower_bound(table.Slots(), At(30), {}, [](DB::Row r) { return r[&Transaction::timePoint]; }) - table.Slots().begin() == 3, "std::ranges::lower_bound over Slots()"); // --- tombstones in the search ------------------------------------------- table.Erase(2); CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(20))), std::vector{ 1 }, "Range skips tombstones but still finds neighbours"); CheckEqual(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound unchanged by a tombstone (keys survive)"); CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector{ 1 }, "bound extraction with a tombstone"); // --- order checks on Update/Set ----------------------------------------- bool threw = false; try { table.Update(3, { "d", "RABONL2U", std::string(ibanA), "salary", Currency::Euro, 300000, At(15) }); // would sort before row 1/2 } catch (const DB::OrderError&) { threw = true; } Check(threw, "Update that breaks the order throws"); threw = false; try { table.Set<&Transaction::timePoint>(3, At(45)); // would sort after row 4 } catch (const DB::OrderError&) { threw = true; } Check(threw, "Set that breaks the order throws"); table.Set<&Transaction::timePoint>(3, At(35)); Check(table[3][&Transaction::timePoint] == At(35), "Set within the neighbours succeeds"); table.Set<&Transaction::description>(3, "wage"); Check(table[3][&Transaction::description] == "wage", "Set a string column"); CheckEqual(table.Stats().heapGarbage, 22, "short strings leave no heap garbage (only the erased row's 22-byte iban is garbage)"); table.Set<&Transaction::description>(3, "a description longer than twelve"); Check(table[3][&Transaction::description] == "a description longer than twelve", "Set a long string column"); table.Set<&Transaction::description>(3, "short"); CheckEqual(table.Stats().heapGarbage, 22 + 32, "replacing a heap value orphans its bytes"); fs::remove_all(dir); if (failures == 0) std::println("ok"); return failures == 0 ? 0 : 1; }