171 lines
10 KiB
C++
171 lines
10 KiB
C++
|
|
// 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<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;
|
||
|
|
};
|
||
|
|
|
||
|
|
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<TimePoint::duration>(std::chrono::seconds(seconds)));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::uint64_t> Ids(auto&& query) {
|
||
|
|
std::vector<std::uint64_t> ids;
|
||
|
|
for (auto row : query) ids.push_back(row.Id());
|
||
|
|
return ids;
|
||
|
|
}
|
||
|
|
|
||
|
|
template<class Query>
|
||
|
|
std::vector<std::uint64_t> IdsOf(Query query) {
|
||
|
|
std::vector<std::uint64_t> 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<Transaction> 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<Transaction> r) { return r[&Transaction::iban] == keyA; })), std::vector<std::uint64_t>{ 0, 3, 5 }, "lambda with Key");
|
||
|
|
CheckEqual(IdsOf(table.Where([&](DB::Row<Transaction> r) { return r[&Transaction::iban] == ibanB; })), std::vector<std::uint64_t>{ 1 }, "lambda with string_view, prefix-equal neighbour rejected");
|
||
|
|
CheckEqual(IdsOf(table.Where([](DB::Row<Transaction> r) { return r[&Transaction::description] == "rent"; })), std::vector<std::uint64_t>{ 0, 1 }, "description equality is exact (case and length)");
|
||
|
|
CheckEqual(table.Where([](DB::Row<Transaction> 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<std::uint64_t>{ 0, 3, 5 }, "expression ==");
|
||
|
|
CheckEqual(IdsOf(table.Where(iban != ibanA)), std::vector<std::uint64_t>{ 1, 2, 4 }, "expression !=");
|
||
|
|
CheckEqual(IdsOf(table.Where(iban == ibanA && amount > 0)), std::vector<std::uint64_t>{ 3 }, "expression &&");
|
||
|
|
CheckEqual(IdsOf(table.Where(description == "coffee" || description == "salary")), std::vector<std::uint64_t>{ 2, 3 }, "expression ||");
|
||
|
|
CheckEqual(IdsOf(table.Where(!(amount < 0))), std::vector<std::uint64_t>{ 3 }, "expression !");
|
||
|
|
CheckEqual(IdsOf(table.Where(description < "d")), std::vector<std::uint64_t>{ 2, 5 }, "string ordering compare ('RENT' and 'coffee' sort before 'd')");
|
||
|
|
|
||
|
|
// --- ordered column -----------------------------------------------------
|
||
|
|
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(20)), 1, "LowerBound on equal keys");
|
||
|
|
CheckEqual<std::uint64_t>(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound on equal keys");
|
||
|
|
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(5)), 0, "LowerBound below all");
|
||
|
|
CheckEqual<std::uint64_t>(table.LowerBound<&Transaction::timePoint>(At(55)), 6, "LowerBound above all");
|
||
|
|
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(40))), std::vector<std::uint64_t>{ 1, 2, 3, 4 }, "Range inclusive both ends");
|
||
|
|
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(21), At(29))), std::vector<std::uint64_t>{}, "Range with no rows");
|
||
|
|
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(40), At(20))), std::vector<std::uint64_t>{}, "Range with inverted bounds is empty");
|
||
|
|
CheckEqual(IdsOf(table.From<&Transaction::timePoint>(At(30))), std::vector<std::uint64_t>{ 3, 4, 5 }, "From");
|
||
|
|
CheckEqual(IdsOf(table.After<&Transaction::timePoint>(At(30))), std::vector<std::uint64_t>{ 4, 5 }, "After");
|
||
|
|
CheckEqual(IdsOf(table.Until<&Transaction::timePoint>(At(20))), std::vector<std::uint64_t>{ 0, 1, 2 }, "Until");
|
||
|
|
CheckEqual(IdsOf(table.Before<&Transaction::timePoint>(At(20))), std::vector<std::uint64_t>{ 0 }, "Before");
|
||
|
|
|
||
|
|
// --- range + property ---------------------------------------------------
|
||
|
|
const std::vector<std::uint64_t> chained = IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where(iban == ibanA));
|
||
|
|
CheckEqual(chained, std::vector<std::uint64_t>{ 3, 5 }, "Range().Where(expression)");
|
||
|
|
CheckEqual(IdsOf(table.Range<&Transaction::timePoint>(At(20), At(50)).Where([&](DB::Row<Transaction> 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<std::uint64_t>{ 3, 4 }, "exclusive bounds");
|
||
|
|
CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector<std::uint64_t>{ 1, 2 }, "equality bound");
|
||
|
|
CheckEqual(IdsOf(table.Where(timePoint == At(20) || iban == ibanA)), std::vector<std::uint64_t>{ 0, 1, 2, 3, 5 }, "|| disables bound extraction but stays correct");
|
||
|
|
|
||
|
|
// --- ranges composition -------------------------------------------------
|
||
|
|
std::uint64_t taken = 0;
|
||
|
|
for (DB::Row<Transaction> row : table | std::views::filter([](DB::Row<Transaction> r) { return r[&Transaction::amount] < 0; }) | std::views::take(2)) taken += row.Id() + 1;
|
||
|
|
CheckEqual<std::uint64_t>(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<std::int64_t>(total, -100000 - 200 - 350 + 300000 - 100000 - 5, "Select + fold_left");
|
||
|
|
CheckEqual<std::size_t>(table.Slots().size(), 6, "Slots size");
|
||
|
|
Check(std::ranges::lower_bound(table.Slots(), At(30), {}, [](DB::Row<Transaction> 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<std::uint64_t>{ 1 }, "Range skips tombstones but still finds neighbours");
|
||
|
|
CheckEqual<std::uint64_t>(table.UpperBound<&Transaction::timePoint>(At(20)), 3, "UpperBound unchanged by a tombstone (keys survive)");
|
||
|
|
CheckEqual(IdsOf(table.Where(timePoint == At(20))), std::vector<std::uint64_t>{ 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<std::uint64_t>(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<std::uint64_t>(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;
|
||
|
|
}
|