diff --git a/README.md b/README.md index 85150bc..8ffad51 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A C++ wrapper for SQLite that provides compile-time checked CRUD operations for - [Opening and closing the database](#opening-and-closing-the-database) - [Saving records](#saving-records) - [Fetching records](#fetching-records) +- [Working with query results](#working-with-query-results) - [Updating records](#updating-records) - [Deleting records](#deleting-records) - [Date and time fields](#date-and-time-fields) @@ -346,6 +347,38 @@ Available predicate helpers include: Predicate values are bound with SQLite prepared statements. Text payloads are treated as values, not SQL syntax. +## Working with query results + +`FetchAll` and `Fetch` return an [`fcpp::vector`](https://github.com/jkalias/functional_cpp) +rather than a `std::vector`, so functional operations can be chained directly on the result +without wrapping it yourself: + +```c++ +// Full names of every adult, straight off the query result. +const auto adult_names = db->FetchAll() + .filter([](const Person& p) { return p.age >= 18; }) + .map([](const Person& p) { return p.GetFullName(); }); + +// Predicate-style checks are available too. +const bool everyone_is_adult = db->FetchAll() + .all_of([](const Person& p) { return p.age >= 18; }); +``` + +`fcpp::vector` also supports `map`, `all_of`/`any_of`/`none_of`, `sort`, `distinct`, `zip`, and lazy +pipelines via `.lazy()`; see the [functional_cpp](https://github.com/jkalias/functional_cpp) +documentation. The batch `Save`, `SaveAutoIncrement`, and `Update` overloads accept an +`fcpp::vector` as well. + +**Migration note.** Because the result type changed from `std::vector` to `fcpp::vector`: + +- `auto` call sites, range-`for` loops, indexing (`result[i]`), and `size()` are unaffected. +- Code that binds a result to a `std::vector` explicitly no longer compiles, because there is no + implicit conversion back. Iterate the result directly, keep it as `fcpp::vector` (or `auto`), or + construct a `std::vector` from its iterators: `std::vector(result.begin(), result.end())`. +- Indexing is **always bounds-checked**, in release builds too: `result[i]` for an out-of-range `i` + calls `std::abort()` instead of being undefined behavior (unless the library is built with + `FCPP_NO_PRECONDITION_CHECKS`). + ## Updating records Fetch or construct a record, change its fields, and pass it to `Update`. The `id` identifies the row to update. @@ -363,7 +396,7 @@ Multiple records can be updated in one call: ```c++ // Update each record in the vector by its id. -std::vector people = db->FetchAll(); +auto people = db->FetchAll(); // fcpp::vector people[0].last_name = L"Rambo"; people[1].age = 20; @@ -494,6 +527,8 @@ You can also open the generated `build/sqlite-cpp-reflection.sln` in Visual Stud - CMake 3.14 or newer - A C++11-compatible compiler +- [functional_cpp](https://github.com/jkalias/functional_cpp) for `fcpp::vector` (query results) and + `fcpp::optional_t` (nullable fields), header-only, fetched by CMake - GoogleTest for tests, fetched by CMake - SQLite, vendored in `src/sqlite3.c` and `src/internal/sqlite3.h` diff --git a/include/database.h b/include/database.h index 7006260..7c7dd25 100644 --- a/include/database.h +++ b/include/database.h @@ -32,6 +32,7 @@ #include "queries.h" #include "query_predicates.h" #include "reflection.h" +#include "vector.h" // fcpp::vector, the functional container returned by the query API struct sqlite3; @@ -66,9 +67,11 @@ class REFLECTION_EXPORT Database { Database& operator=(Database&&) = delete; /// Retrieves all entries of a given record from the database. - /// This corresponds to a SELECT query in the SQL syntax + /// This corresponds to a SELECT query in the SQL syntax. + /// The result is an fcpp::vector, so functional operations (filter/map/...) can be chained + /// directly on it; range-for, indexing and size() work as with a std::vector. template - std::vector FetchAll() const { + fcpp::vector FetchAll() const { const auto type_id = typeid(T).name(); const auto& record = GetRecord(type_id); EmptyPredicate empty; @@ -76,9 +79,11 @@ class REFLECTION_EXPORT Database { } /// Retrieves all entries of a given record from the database, which match a given predicate. - /// This corresponds to a SELECT query in the SQL syntax + /// This corresponds to a SELECT query in the SQL syntax. + /// The result is an fcpp::vector, so functional operations (filter/map/...) can be chained + /// directly on it; range-for, indexing and size() work as with a std::vector. template - std::vector Fetch(const QueryPredicateBase* predicate) const { + fcpp::vector Fetch(const QueryPredicateBase* predicate) const { const auto type_id = typeid(T).name(); const auto& record = GetRecord(type_id); return FetchRecords(record, predicate); @@ -120,7 +125,7 @@ class REFLECTION_EXPORT Database { /// Saves multiple records iteratively in the database. /// This corresponds to an INSERT query in the SQL syntax template - void Save(const std::vector& models) { + void Save(const fcpp::vector& models) { const auto type_id = typeid(T).name(); const auto& record = GetRecord(type_id); for (const auto& model : models) { @@ -128,11 +133,17 @@ class REFLECTION_EXPORT Database { } } + /// std::vector compatibility overload: copies into an fcpp::vector and forwards. + template + void Save(const std::vector& models) { + Save(fcpp::vector(models)); + } + /// Saves multiple records iteratively in the database, letting the database assign their ids. /// The newly generated ids are written back into the passed-in models. /// This corresponds to an INSERT query in the SQL syntax template - void SaveAutoIncrement(std::vector& models) { + void SaveAutoIncrement(fcpp::vector& models) { const auto type_id = typeid(T).name(); const auto& record = GetRecord(type_id); for (auto& model : models) { @@ -140,6 +151,17 @@ class REFLECTION_EXPORT Database { } } + /// std::vector compatibility overload: saves each record through the single-record overload, so + /// every generated id is written back into the caller's std::vector as soon as its row is + /// inserted. This preserves the ids of already-committed rows if a later row in the batch fails + /// (each insert commits independently), matching the pre-fcpp::vector behavior. + template + void SaveAutoIncrement(std::vector& models) { + for (auto& model : models) { + SaveAutoIncrement(model); + } + } + /// Updates a given record in the database. /// This corresponds to an UPDATE query in the SQL syntax template @@ -152,7 +174,7 @@ class REFLECTION_EXPORT Database { /// Updates multiple records iteratively in the database. /// This corresponds to an UPDATE query in the SQL syntax template - void Update(const std::vector& models) { + void Update(const fcpp::vector& models) { const auto type_id = typeid(T).name(); const auto& record = GetRecord(type_id); for (const auto& model : models) { @@ -160,6 +182,12 @@ class REFLECTION_EXPORT Database { } } + /// std::vector compatibility overload: copies into an fcpp::vector and forwards. + template + void Update(const std::vector& models) { + Update(fcpp::vector(models)); + } + /// Deletes a given record from the database. /// This corresponds to an DELETE query in the SQL syntax template @@ -203,14 +231,14 @@ class REFLECTION_EXPORT Database { /// each matching row directly from the prepared statement into a newly constructed T - /// no intermediate string materialization of the result set template - std::vector FetchRecords(const Reflection& record, const QueryPredicateBase* predicate) const { + fcpp::vector FetchRecords(const Reflection& record, const QueryPredicateBase* predicate) const { std::lock_guard lock(db_mutex_); FetchRecordsQuery query(db_, record, predicate); - std::vector models; + fcpp::vector models; while (query.StepRow()) { T model; query.HydrateCurrentRow((void*)&model, record); - models.emplace_back(std::move(model)); + models.insert_back(std::move(model)); } return models; } diff --git a/tests/functional_vector_test.cc b/tests/functional_vector_test.cc new file mode 100644 index 0000000..b59b82f --- /dev/null +++ b/tests/functional_vector_test.cc @@ -0,0 +1,165 @@ +// MIT License +// +// Copyright (c) 2026 Ioannis Kaliakatsos +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include + +#include +#include + +#include "database.h" +#include "person.h" +#include "vector.h" + +using namespace sqlite_reflection; + +namespace { +class FunctionalVectorTest : public ::testing::Test { + void SetUp() override { + Database::Initialize(""); + } + + void TearDown() override { + Database::Finalize(); + } +}; + +// The whole point of the migration: a fetch result is an fcpp::vector, so functional operations +// can be chained directly on it, without the caller wrapping the std::vector themselves. +TEST_F(FunctionalVectorTest, FetchResultChainsFilterAndMap) { + const auto db = Database::Instance(); + db->Save(std::vector{ + {L"ada", L"lovelace", 36, true, 1}, + {L"grace", L"hopper", 17, true, 2}, + {L"alan", L"turing", 41, true, 3}, + }); + + // filter() (mutating, returns vector&) then map() (non-mutating) chained on the temporary + // returned by FetchAll(), exactly as advertised in the README. + const auto adult_names = db->FetchAll() + .filter([](const Person& p) { return p.age >= 18; }) + .map([](const Person& p) { return p.GetFullName(); }); + + ASSERT_EQ(2u, adult_names.size()); + EXPECT_EQ(L"ada lovelace", adult_names[0]); + EXPECT_EQ(L"alan turing", adult_names[1]); +} + +TEST_F(FunctionalVectorTest, FetchResultSupportsAllOfAnyOf) { + const auto db = Database::Instance(); + db->Save(std::vector{ + {L"ada", L"lovelace", 36, true, 1}, + {L"grace", L"hopper", 17, true, 2}, + }); + + const auto people = db->FetchAll(); + EXPECT_TRUE(people.all_of([](const Person& p) { return p.age > 0; })); + EXPECT_FALSE(people.all_of([](const Person& p) { return p.age >= 18; })); + EXPECT_TRUE(people.any_of([](const Person& p) { return p.age < 18; })); + EXPECT_FALSE(people.any_of([](const Person& p) { return p.age > 100; })); +} + +// range-for and indexing over a fetch result keep working as with a std::vector +TEST_F(FunctionalVectorTest, FetchResultSupportsRangeForAndIndexing) { + const auto db = Database::Instance(); + db->Save(std::vector{ + {L"ada", L"lovelace", 36, true, 1}, + {L"alan", L"turing", 41, true, 2}, + }); + + const auto people = db->FetchAll(); + + int64_t total_age = 0; + for (const auto& person : people) { + total_age += person.age; + } + EXPECT_EQ(77, total_age); + EXPECT_EQ(L"ada", people[0].first_name); + EXPECT_EQ(L"alan", people[1].first_name); +} + +// Batch Save/Update accept an fcpp::vector directly. +TEST_F(FunctionalVectorTest, BatchSaveAndUpdateAcceptFcppVector) { + const auto db = Database::Instance(); + + fcpp::vector people; + people.insert_back({L"ada", L"lovelace", 36, true, 1}).insert_back({L"alan", L"turing", 41, true, 2}); + + db->Save(people); + + auto fetched = db->FetchAll(); + ASSERT_EQ(2u, fetched.size()); + EXPECT_EQ(36, fetched[0].age); + + people[0].age = 37; + db->Update(people); + + fetched = db->FetchAll(); + ASSERT_EQ(2u, fetched.size()); + EXPECT_EQ(37, fetched[0].age); +} + +// SaveAutoIncrement writes the generated ids back into the passed-in fcpp::vector. +TEST_F(FunctionalVectorTest, SaveAutoIncrementWritesIdsBackIntoFcppVector) { + const auto db = Database::Instance(); + + fcpp::vector people; + people.insert_back({L"ada", L"lovelace", 36, true}).insert_back({L"alan", L"turing", 41, true}); + + db->SaveAutoIncrement(people); + + EXPECT_EQ(1, people[0].id); + EXPECT_EQ(2, people[1].id); + EXPECT_EQ(2u, db->FetchAll().size()); +} + +// The std::vector compatibility overloads keep working for callers that still pass a std::vector. +TEST_F(FunctionalVectorTest, StdVectorCompatibilityOverloadsStillWork) { + const auto db = Database::Instance(); + + std::vector people; + people.push_back({L"ada", L"lovelace", 36, true, 1}); + people.push_back({L"alan", L"turing", 41, true, 2}); + + db->Save(people); + EXPECT_EQ(2u, db->FetchAll().size()); + + people[0].age = 37; + db->Update(people); + EXPECT_EQ(37, db->Fetch(1).age); +} + +// The SaveAutoIncrement std::vector wrapper writes the generated ids back into the caller's +// std::vector, just like the fcpp::vector overload. +TEST_F(FunctionalVectorTest, SaveAutoIncrementStdVectorWrapperWritesIdsBack) { + const auto db = Database::Instance(); + + std::vector people; + people.push_back({L"ada", L"lovelace", 36, true}); + people.push_back({L"alan", L"turing", 41, true}); + + db->SaveAutoIncrement(people); + + EXPECT_EQ(1, people[0].id); + EXPECT_EQ(2, people[1].id); + EXPECT_EQ(2u, db->FetchAll().size()); +} +} // namespace