From 6df48485656106c39de7906983d9187a062d6883 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sun, 9 Nov 2025 03:21:35 +0100 Subject: [PATCH 01/13] support move constructor and move assignment --- include/bw/sqlitemap/sqlitemap.hpp | 43 +++++++-- .../unit_tests/sqlitemap_core_tests.cpp | 96 +++++++++++++++++++ 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index ff03974..973749f 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -476,8 +476,8 @@ template struct codec using in_type = IN_T; using out_type = OUT_T; - const std::function encode; - const std::function decode; + std::function encode; + std::function decode; }; struct key_codec_tag @@ -606,8 +606,8 @@ template struct codec_pair using value_in_type = typename VC::in_type; using value_out_type = typename VC::out_type; - const KC key_codec; - const VC value_codec; + KC key_codec; + VC value_codec; }; template struct is_codec_pair : std::false_type @@ -1467,7 +1467,10 @@ template class sqlitemap { try { - close(); + if (db != nullptr) + { + close(); + } } catch (std::exception& ex) { @@ -1475,6 +1478,34 @@ template class sqlitemap } } + sqlitemap(sqlitemap&& other) noexcept + : _config(std::move(other._config)) + , _in_temp(other._in_temp) + , _logger(std::move(other._logger)) + , db(other.db) + { + other.db = nullptr; + log().debug("sqlitemap moved successfully"); + } + + sqlitemap& operator=(sqlitemap&& other) noexcept + { + if (this != &other) + { + _config = std::move(other._config); + _in_temp = other._in_temp; + _logger = std::move(other._logger); + db = other.db; + other.db = nullptr; + log().debug("sqlitemap move assigned successfully"); + } + return *this; + } + + // disable copy constructor and assignment operator + sqlitemap(const sqlitemap&) = delete; + sqlitemap& operator=(const sqlitemap&) = delete; + void open_database(const std::string& file) { try @@ -2013,7 +2044,7 @@ template class sqlitemap // Close the database connection sqlite3_close(db); - log().debug("Database closed"); + log().debug("Database '" + config().filename() + "' closed"); if (in_temp()) { diff --git a/test/catch2/unit_tests/sqlitemap_core_tests.cpp b/test/catch2/unit_tests/sqlitemap_core_tests.cpp index 0b23274..1103e88 100644 --- a/test/catch2/unit_tests/sqlitemap_core_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_core_tests.cpp @@ -52,6 +52,102 @@ TEST_CASE("sqlitemap assignment") REQUIRE((sqlitemap(file.string(), "cache", operation_mode::r).get("app-1") == "123")); } +TEST_CASE("sqlitemap move constructor and assignment") +{ + TempDir temp_dir(Config().enable_logging()); + auto file = (temp_dir.path() / "db.sqlite").string(); + + sqlitemap sm1(config().filename(file).log_level(log_level::trace)); + sm1.set("k1", "v1"); + sm1.commit(); + + { + // move constructor + sqlitemap sm2(std::move(sm1)); + REQUIRE(sm2.size() == 1); + REQUIRE((sm2["k1"] == "v1")); + sm2.set("k2", "v2"); + sm2.commit(); + + // move assignment + sqlitemap sm3; + sm3 = std::move(sm2); + REQUIRE(sm3.size() == 2); + REQUIRE((sm3["k1"] == "v1")); + REQUIRE((sm3["k2"] == "v2")); + sm3.set("k3", "v3"); + sm3.commit(); + } + + sqlitemap sm4(config().filename(file).log_level(log_level::trace)); + REQUIRE(sm4.size() == 3); + REQUIRE((sm4["k1"] == "v1")); + REQUIRE((sm4["k2"] == "v2")); + REQUIRE((sm4["k3"] == "v3")); + sm4.set("k4", "v4"); + sm4.commit(); + + // move assignment, reuse sm1, which was moved from before + sm1 = std::move(sm4); + REQUIRE(sm1.size() == 4); + REQUIRE((sm1["k1"] == "v1")); + REQUIRE((sm1["k2"] == "v2")); + REQUIRE((sm1["k3"] == "v3")); + REQUIRE((sm1["k4"] == "v4")); +} + +TEST_CASE("sqlitemap can be stored in a std::vector") +{ + TempDir temp_dir(Config().enable_logging()); + auto file1 = (temp_dir.path() / "db1.sqlite").string(); + auto file2 = (temp_dir.path() / "db2.sqlite").string(); + + std::vector> db_vector; + db_vector.emplace_back(config().filename(file1).log_level(log_level::debug)); + db_vector.push_back(sqlitemap(config().filename(file2).log_level(log_level::debug))); + + db_vector[0].set("k1", "v1"); + db_vector[0].commit(); + + auto& db2 = db_vector[1]; + db2.set("k2", "v2"); + db2.commit(); + + for (auto& db : db_vector) + { + db.set("loop_key", "loop_value"); + db.commit(); + } + + REQUIRE(db_vector[0].get("k1") == "v1"); + REQUIRE(db_vector[0].get("loop_key") == "loop_value"); + + REQUIRE((db2["k2"] == "v2")); + REQUIRE((db2["loop_key"] == "loop_value")); +} + +TEST_CASE("sqlitemap can be stored in a std::map") +{ + TempDir temp_dir(Config().enable_logging()); + auto file1 = (temp_dir.path() / "db1.sqlite").string(); + auto file2 = (temp_dir.path() / "db2.sqlite").string(); + + std::map> db_map; + + db_map["first"] = sqlitemap<>(config().filename(file1).log_level(log_level::debug)); + db_map["second"] = sqlitemap<>(config().filename(file2).log_level(log_level::debug)); + + db_map["first"].set("k1", "v1"); + db_map["first"].commit(); + + auto& db2 = db_map["second"]; + db2.set("k2", "v2"); + db2.commit(); + + REQUIRE(db_map["first"].get("k1") == "v1"); + REQUIRE((db2["k2"] == "v2")); +} + TEST_CASE("sqlitemap can be represented as string") { sqlitemap sm(config().filename(":memory:")); From e08fa88478373547bbf72365f9d0e2bacf9f9218 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Tue, 11 Nov 2025 03:48:45 +0100 Subject: [PATCH 02/13] test codecs defined by functors --- test/catch2/unit_tests/conversion_functor.hpp | 70 +++++++++++++++++++ test/catch2/unit_tests/custom.hpp | 2 + .../unit_tests/sqlitemap_codecs_tests.cpp | 52 ++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 test/catch2/unit_tests/conversion_functor.hpp diff --git a/test/catch2/unit_tests/conversion_functor.hpp b/test/catch2/unit_tests/conversion_functor.hpp new file mode 100644 index 0000000..e519754 --- /dev/null +++ b/test/catch2/unit_tests/conversion_functor.hpp @@ -0,0 +1,70 @@ +// sqlitemap +// SPDX-FileCopyrightText: 2024-present Benno Waldhauer +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +namespace bw::testhelper +{ + +template struct conversion_functor +{ + explicit conversion_functor(const std::string& name, + std::function&& convert) + : name(name) + , convert(std::move(convert)) + { + std::cout << name << " constructed\n"; + } + + conversion_functor(const conversion_functor& other) + : name(other.name) + , convert(other.convert) + { + std::cout << name << " copy-constructed\n"; + } + + conversion_functor(conversion_functor&& other) noexcept + : name(std::move(other.name)) + , convert(std::move(other.convert)) + { + std::cout << name << " move-constructed\n"; + } + + conversion_functor& operator=(const conversion_functor& other) + { + name = other.name; + convert = other.convert; + std::cout << name << " copy-assigned\n"; + return *this; + } + + conversion_functor& operator=(conversion_functor&& other) noexcept + { + name = std::move(other.name); + convert = std::move(other.convert); + std::cout << name << " move-assigned\n"; + return *this; + } + + ~conversion_functor() + { + std::cout << name << " destroyed\n"; + } + + OUT_T operator()(const IN_T& value) const + { + auto converted = convert(value); + std::cout << name << " converted " << value << " to " << converted << "\n"; + return converted; + } + + std::string name; + std::function convert; +}; + +} // namespace bw::testhelper \ No newline at end of file diff --git a/test/catch2/unit_tests/custom.hpp b/test/catch2/unit_tests/custom.hpp index fd2c640..6f692de 100644 --- a/test/catch2/unit_tests/custom.hpp +++ b/test/catch2/unit_tests/custom.hpp @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: 2024-present Benno Waldhauer // SPDX-License-Identifier: MIT +#pragma once + #include #include diff --git a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp index 193353e..7e4d2cf 100644 --- a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp @@ -6,6 +6,7 @@ #include +#include "conversion_functor.hpp" #include "custom.hpp" #include @@ -233,6 +234,57 @@ TEST_CASE("codecs can be defined by type identity function", "[codecs]") REQUIRE(sm.get(3) == Catch::Approx(7.333)); } +TEST_CASE("codecs can be definded by functor objects", "[codecs]") +{ + using namespace bw::testhelper; + + // clang-format off + struct key_codec_encode_functor : public conversion_functor + { + key_codec_encode_functor(): conversion_functor( + "KEY_ENCODE_FUNCTOR", [](int key){ return "key-" + std::to_string(key); }) + {} + }; + + struct key_codec_decode_functor : public conversion_functor + { + key_codec_decode_functor(): conversion_functor( + "KEY_DECODE_FUNCTOR", [](std::string key_str){ return std::atoi(key_str.substr(4).c_str()); }) + {} + }; + + struct value_codec_encode_functor : public conversion_functor + { + value_codec_encode_functor(): conversion_functor( + "VALUE_ENCODE_FUNCTOR", [](double value){ return "value-" + std::to_string(value); }) + {} + }; + + struct value_codec_decode_functor : public conversion_functor + { + value_codec_decode_functor(): conversion_functor( + "VALUE_DECODE_FUNCTOR", [](std::string value_str){ return std::atof(value_str.substr(6).c_str()); }) + {} + }; + // clang-format on + + auto kc = key_codec(std::move(key_codec_encode_functor{}), key_codec_decode_functor{}); + auto vc = value_codec(value_codec_encode_functor{}, value_codec_decode_functor{}); + + auto sm = sqlitemap(config(kc, vc)); + + REQUIRE_NOTHROW(sm.set(42, 0.1234)); + REQUIRE(sm.get(42) == Catch::Approx(0.1234)); + + REQUIRE_NOTHROW(sm.del(42)); + REQUIRE_THROWS_AS(sm.get(42), sqlitemap_error); + + REQUIRE_NOTHROW(sm.insert({{1, 9.111}, {2, 8.222}, {3, 7.333}})); + REQUIRE(sm.get(1) == Catch::Approx(9.111)); + REQUIRE(sm.get(2) == Catch::Approx(8.222)); + REQUIRE(sm.get(3) == Catch::Approx(7.333)); +} + template T create_test_value() { if constexpr (std::is_same_v) From 4e5cf9175382139848cb2515b531132bf619288f Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Tue, 11 Nov 2025 03:53:47 +0100 Subject: [PATCH 03/13] offer sqlitemap_t type helper --- examples/lines_to_sqlitemap.cpp | 2 +- include/bw/sqlitemap/sqlitemap.hpp | 179 ++++++++++++++- .../unit_tests/sqlitemap_codecs_tests.cpp | 207 ++++++++++++++++++ .../unit_tests/sqlitemap_core_tests.cpp | 3 +- 4 files changed, 387 insertions(+), 4 deletions(-) diff --git a/examples/lines_to_sqlitemap.cpp b/examples/lines_to_sqlitemap.cpp index 49b2368..1fef670 100644 --- a/examples/lines_to_sqlitemap.cpp +++ b/examples/lines_to_sqlitemap.cpp @@ -19,7 +19,7 @@ class processor { using key_type = long long; using value_type = std::string; - using DB = sm::sqlitemap().codecs())>; + using DB = sm::sqlitemap_t; public: processor(const std::string& file, const std::string& table, bool echo = true) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index 973749f..aef3bd2 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -1341,7 +1341,7 @@ template struct sqlitemap_node_type * results. As a result, iterators returned by STL-like operations are intended solely for data * access; advancing or reusing them in multiple passes is not supported. */ -template class sqlitemap +template > class sqlitemap { public: using key_type = typename CODEC_PAIR::key_in_type; @@ -2292,4 +2292,181 @@ template class sqlitemap logger _logger; }; +// Helper alias templates for sqlitemap with different number of template arguments + +struct sqlitemap_alias_no_arg +{ + using codec_pair = std::decay_t; + using type = sqlitemap; +}; + +// clang-format off + +template struct sqlitemap_alias_1_arg; + +// argument is a codec pair, key codec or value codec +template struct sqlitemap_alias_1_arg>::value>> +{ + using type = sqlitemap>; +}; + +// argument is a key codec +template struct sqlitemap_alias_1_arg>::value>> +{ + using value_codec_t = decltype(default_value_codec); + using type = sqlitemap, value_codec_t>>; +}; + +// argument is a value codec +template struct sqlitemap_alias_1_arg>::value>> +{ + using key_codec_t = decltype(default_key_codec); + using type = sqlitemap>>; +}; + +// argument is not a codec pair, key codec or value codec, but a value type +template struct sqlitemap_alias_1_arg>::value || + codecs::is_key_codec>::value || + codecs::is_value_codec>::value +)>> +{ + using key_codec_t = decltype(default_key_codec); + using value_codec_out_t = std::conditional_t(),CODEC_ARG, std::string>; + using value_codec_t = codecs::value_codec; + using type = sqlitemap>; +}; + + +// two arguments: key codec and value codec + +template +struct sqlitemap_alias_2_arg; + +template +struct sqlitemap_alias_2_arg>::value && + codecs::is_value_codec>::value +)>> +{ + using decayed_key_t = std::decay_t; + using decayed_value_t = std::decay_t; + + // require that the user really provided a key codec and a value codec + static_assert(codecs::is_key_codec::value, + "sqlitemap_alias_2_arg: KEY_CODEC must be a key codec"); + static_assert(codecs::is_value_codec::value, + "sqlitemap_alias_2_arg: VALUE_CODEC must be a value codec"); + + using type = sqlitemap>; +}; + +template +struct sqlitemap_alias_2_arg>() && + codecs::is_value_codec>::value +)>> +{ + using decayed_key_t = std::decay_t; + using decayed_value_t = std::decay_t; + using key_codec_t = codecs::key_codec; + using type = sqlitemap>; +}; + +template +struct sqlitemap_alias_2_arg>::value && + details::has_native_sqlite_support>() +)>> +{ + using decayed_key_t = std::decay_t; + using decayed_value_t = std::decay_t; + using value_codec_t = codecs::value_codec; + using type = sqlitemap>; +}; + +template +struct sqlitemap_alias_2_arg>() && + details::has_native_sqlite_support>() +)>> +{ + using decayed_key_t = std::decay_t; + using decayed_value_t = std::decay_t; + using key_codec_t = codecs::key_codec; + using value_codec_t = codecs::value_codec; + using type = sqlitemap>; +}; + +// clang-format on + +template struct sqlitemap_t_helper; + +template <> struct sqlitemap_t_helper<> +{ + using type = typename sqlitemap_alias_no_arg::type; +}; + +template struct sqlitemap_t_helper +{ + using type = typename sqlitemap_alias_1_arg::type; +}; + +template struct sqlitemap_t_helper +{ + using type = typename sqlitemap_alias_2_arg::type; +}; + +template using sqlitemap_t = typename sqlitemap_t_helper::type; + +// Helper alias templates for value_codec + +template struct value_codec_t_helper; + +template <> struct value_codec_t_helper<> +{ + using type = decltype(default_value_codec); +}; + +template struct value_codec_t_helper +{ + using type = codecs::value_codec; +}; + +template struct value_codec_t_helper +{ + using type = codecs::value_codec; +}; + +template using value_codec_t = typename value_codec_t_helper::type; + +// Helper alias templates for key_codec + +template struct key_codec_t_helper; + +template <> struct key_codec_t_helper<> +{ + using type = decltype(default_key_codec); +}; + +template struct key_codec_t_helper +{ + using type = codecs::key_codec; +}; + +template struct key_codec_t_helper +{ + using type = codecs::key_codec; +}; + +template using key_codec_t = typename key_codec_t_helper::type; + } // namespace bw::sqlitemap \ No newline at end of file diff --git a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp index 7e4d2cf..5189e04 100644 --- a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp @@ -380,3 +380,210 @@ TEST_CASE("use std::variant to store different types in one table") REQUIRE(std::get(sm.get("k1")) == "Hello World!"); REQUIRE(std::get(sm.get("k2")) == 42); } + +TEST_CASE("sqlitemap_t can be used as a member variable referencing a sqlitemap specialization", + "[codecs]") +{ + struct Post + { + std::string id; + std::string content; + }; + + class PostRepository + { + public: + PostRepository() + : db_(config(value_codec(post_to_string, string_to_post)) + .log_level(log_level::debug) + .table("posts")) + { + } + + sqlitemap_t& db() + { + return db_; + } + + std::optional find_post(const std::string& id) + { + return db_.try_get(id); + } + + Post save(const Post& p) + { + db_.set(p.id, p); + db_.commit(); + return db_.get(p.id); + } + + private: + static std::string post_to_string(const Post& p) + { + return p.id + "|" + p.content; + } + + static Post string_to_post(const std::string& s) + { + auto delimiter_pos = s.find('|'); + return Post{s.substr(0, delimiter_pos), s.substr(delimiter_pos + 1)}; + } + sqlitemap_t db_; + }; + + PostRepository post_repo; + + REQUIRE(post_repo.db().size() == 0); + REQUIRE_FALSE(post_repo.find_post("p1")); + + post_repo.save({"p1", "Hello World!"}); + + REQUIRE(post_repo.db().size() == 1); + REQUIRE(post_repo.find_post("p1")->content == "Hello World!"); +} + +TEST_CASE("sqlitemap_t codec alias helpers simplify referencing specialized sqlitemap types.", + "[codecs]") +{ + auto int_to_string = [](int i) { return std::to_string(i); }; + auto string_to_int = [](const std::string& s) { return std::atoi(s.c_str()); }; + + TempDir temp_dir(Config().enable_logging()); + std::string file = (temp_dir.path() / "db.sqlite").string(); + + using SM_DEFAULT = sqlitemap_t<>; + { + SM_DEFAULT sm_default( + config().filename(file).table("SM_DEFAULT").log_level(log_level::debug)); + sm_default["k1"] = "v1"; + REQUIRE(sm_default.get("k1") == "v1"); + } + + using SM_KC_0ARG = sqlitemap_t>; + { + SM_KC_0ARG sm_kc_0arg( + config().filename(file).table("SM_KC_0ARG").log_level(log_level::debug)); + sm_kc_0arg["k1"] = "v1"; + REQUIRE((sm_kc_0arg["k1"] == "v1")); + } + + using SM_KC_1ARG = sqlitemap_t>; + { + SM_KC_1ARG sm_kc_1arg(config() + .filename(file) + .table("SM_KC_1ARG") + .log_level(log_level::debug)); + sm_kc_1arg[1] = "v1"; + REQUIRE((sm_kc_1arg[1] == "v1")); + } + + using SM_KC_2ARG = sqlitemap_t>; + { + SM_KC_2ARG sm_kc_2arg(config(key_codec(int_to_string, string_to_int)) + .filename(file) + .table("SM_KC_2ARG") + .log_level(log_level::debug)); + sm_kc_2arg[1] = "v1"; + REQUIRE((sm_kc_2arg[1] == "v1")); + } + + using SM_VC_0ARG = sqlitemap_t>; + { + SM_VC_0ARG sm_vc_0arg( + config().filename(file).table("SM_VC_0ARG").log_level(log_level::debug)); + sm_vc_0arg["k1"] = "v1"; + REQUIRE((sm_vc_0arg["k1"] == "v1")); + } + + using SM_VC_1ARG = sqlitemap_t>; + { + SM_VC_1ARG sm_vc_1arg(config() + .filename(file) + .table("SM_VC_1ARG") + .log_level(log_level::debug)); + sm_vc_1arg["k1"] = 1; + REQUIRE(sm_vc_1arg.get("k1") == 1); + } + + using SM_VC_2ARG = sqlitemap_t>; + { + SM_VC_2ARG sm_vc_2arg(config(value_codec(int_to_string, string_to_int)) + .filename(file) + .table("SM_VC_2ARG") + .log_level(log_level::debug)); + sm_vc_2arg["k1"] = 1; + REQUIRE(sm_vc_2arg.get("k1") == 1); + } + + using SM_VC_BY_VALUE = sqlitemap_t; + { + using namespace bw::testhelper; + SM_VC_BY_VALUE sm_vc_by_value( + config(value_codec([&](const custom& c) { return int_to_string(c.counter); }, + [&](const std::string& s) { return custom{string_to_int(s)}; })) + .filename(file) + .table("SM_VC_BY_VALUE") + .log_level(log_level::debug)); + sm_vc_by_value["k1"] = custom{1}; + REQUIRE(sm_vc_by_value.get("k1").counter == 1); + } + + using SM_VC_BY_VALUE_INT = sqlitemap_t; + { + SM_VC_BY_VALUE_INT sm_vc_by_value_int(config() + .filename(file) + .table("SM_VC_BY_VALUE_INT") + .log_level(log_level::debug)); + sm_vc_by_value_int["k1"] = 1; + REQUIRE(sm_vc_by_value_int.get("k1") == 1); + } + + using SM_KC_VC = sqlitemap_t, value_codec_t>; + { + SM_KC_VC sm_kc_vc(config(key_codec(int_to_string, string_to_int), + value_codec(int_to_string, string_to_int)) + .filename(file) + .table("SM_KC_VC") + .log_level(log_level::debug)); + sm_kc_vc[1] = 1; + REQUIRE(sm_kc_vc.get(1) == 1); + } + + using SM_KC_NAT_VC = sqlitemap_t>; + { + SM_KC_NAT_VC sm_kc_nat_vc( + config(key_codec(), value_codec(int_to_string, string_to_int)) + .filename(file) + .table("SM_KC_NAT_VC") + .log_level(log_level::debug)); + sm_kc_nat_vc[1] = 1; + REQUIRE(sm_kc_nat_vc.get(1) == 1); + } + + using SM_KC_VC_NAT = sqlitemap_t, bool>; + { + SM_KC_VC_NAT sm_kc_vc_nat( + config(key_codec(int_to_string, string_to_int), value_codec()) + .filename(file) + .table("SM_KC_VC_NAT") + .log_level(log_level::debug)); + sm_kc_vc_nat[1] = true; + REQUIRE((sm_kc_vc_nat.get(1) == true)); + } + + using SM_KC_NAT_VC_NAT = sqlitemap_t; + { + SM_KC_NAT_VC_NAT sm_kc_nat_vc_nat(config() + .filename(file) + .table("SM_KC_NAT_VC_NAT") + .log_level(log_level::debug)); + sm_kc_nat_vc_nat[1] = true; + REQUIRE((sm_kc_nat_vc_nat.get(1) == true)); + } + + auto tables = bw::sqlitemap::get_tablenames(file); + REQUIRE(tables == std::vector{ + "SM_DEFAULT", "SM_KC_0ARG", "SM_KC_1ARG", "SM_KC_2ARG", "SM_VC_0ARG", + "SM_VC_1ARG", "SM_VC_2ARG", "SM_VC_BY_VALUE", "SM_VC_BY_VALUE_INT", + "SM_KC_VC", "SM_KC_NAT_VC", "SM_KC_VC_NAT", "SM_KC_NAT_VC_NAT"}); +} diff --git a/test/catch2/unit_tests/sqlitemap_core_tests.cpp b/test/catch2/unit_tests/sqlitemap_core_tests.cpp index 1103e88..ca92edf 100644 --- a/test/catch2/unit_tests/sqlitemap_core_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_core_tests.cpp @@ -25,8 +25,7 @@ TEST_CASE("sqlitemap assignment") auto sm_ptr = std::make_unique>(); // as unique_ptr with custom codecs - using codec_pair = decltype(config().codecs()); - auto smc_ptr = std::make_unique>(config()); + auto smc_ptr = std::make_unique>(config()); // as member of a class class App From 6b34f47b8d7cafc78fada55b54204fd1dbaee11f Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Tue, 11 Nov 2025 03:54:43 +0100 Subject: [PATCH 04/13] return configurations condecs as const ref --- include/bw/sqlitemap/sqlitemap.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index aef3bd2..5b0bba7 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -692,7 +692,7 @@ template class configuration "CODEC_PAIR must be a specialization of codec_pair"); } - CODEC_PAIR codecs() const + const CODEC_PAIR& codecs() const { return _codecs; } From c88bc4837b4a9aafcc35de3f07b2823b4b48035e Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Wed, 12 Nov 2025 21:17:18 +0100 Subject: [PATCH 05/13] forward codec objects --- include/bw/sqlitemap/sqlitemap.hpp | 39 +++++++++++-------- .../unit_tests/sqlitemap_codecs_tests.cpp | 5 +-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index 5b0bba7..43f38c4 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -490,7 +490,8 @@ struct key_codec : public codec, public key_codec_tag using codec::codec; key_codec(std::function encode, std::function decode) - : codec{encode, decode} + : codec{std::forward>(encode), + std::forward>(decode)} { } }; @@ -516,7 +517,8 @@ struct value_codec : public codec, public value_codec_tag using codec::codec; value_codec(std::function encode, std::function decode) - : codec{encode, decode} + : codec{std::forward>(encode), + std::forward>(decode)} { } }; @@ -555,11 +557,13 @@ template auto taged_codec_from(E encoder, D if constexpr (std::is_same_v) { - return key_codec, std::decay_t>{encoder, decoder}; + return key_codec, std::decay_t>{std::forward(encoder), + std::forward(decoder)}; } else if constexpr (std::is_same_v) { - return value_codec, std::decay_t>{encoder, decoder}; + return value_codec, std::decay_t>{std::forward(encoder), + std::forward(decoder)}; } else { @@ -623,7 +627,8 @@ template struct is_codec_pair> : s template auto key_codec(E encoder, D decoder) { - return codecs::taged_codec_from(encoder, decoder); + return codecs::taged_codec_from(std::forward(encoder), + std::forward(decoder)); } // use identity function of type T to define a key codec @@ -640,7 +645,8 @@ inline auto default_key_codec = key_codec(); template auto value_codec(E encoder, D decoder) { - return codecs::taged_codec_from(encoder, decoder); + return codecs::taged_codec_from(std::forward(encoder), + std::forward(decoder)); } // use identity function of type T to define a value codec @@ -804,29 +810,30 @@ template class configuration std::vector _pragma_statements; }; -template auto config(CODEC_PAIR codec) +template auto config(CODEC_ARG codec) { - if constexpr (codecs::is_codec_pair>::value) + if constexpr (codecs::is_codec_pair>::value) { - return configuration(codec); + return configuration(std::forward(codec)); } - else if constexpr (codecs::is_key_codec>::value) + else if constexpr (codecs::is_key_codec>::value) { - return configuration(codecs::codec_pair(codec, default_value_codec)); + return configuration( + codecs::codec_pair(std::forward(codec), default_value_codec)); } - else if constexpr (codecs::is_value_codec>::value) + else if constexpr (codecs::is_value_codec>::value) { - return configuration(codecs::codec_pair(default_key_codec, codec)); + return configuration(codecs::codec_pair(default_key_codec, std::forward(codec))); } else { - static_assert(codecs::unknown_codec_tag::value, "Unknown CODEC_PAIR type"); + static_assert(codecs::unknown_codec_tag::value, "Unknown CODEC_ARG type"); } } template auto config(KC key_codec, VC value_codec) { - return config(codecs::codec_pair(key_codec, value_codec)); + return config(codecs::codec_pair(std::forward(key_codec), std::forward(value_codec))); } // Uses key type K and value type V to create configuration from @@ -1425,7 +1432,7 @@ template > class } sqlitemap(configuration config) - : _config(std::move(config)) + : _config(std::forward>(config)) { log().set_level(_config.log_level()); if (_config.log_impl()) diff --git a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp index 5189e04..d1406fb 100644 --- a/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_codecs_tests.cpp @@ -268,10 +268,9 @@ TEST_CASE("codecs can be definded by functor objects", "[codecs]") }; // clang-format on - auto kc = key_codec(std::move(key_codec_encode_functor{}), key_codec_decode_functor{}); + auto kc = key_codec(key_codec_encode_functor{}, key_codec_decode_functor{}); auto vc = value_codec(value_codec_encode_functor{}, value_codec_decode_functor{}); - - auto sm = sqlitemap(config(kc, vc)); + sqlitemap sm(config(kc, vc)); REQUIRE_NOTHROW(sm.set(42, 0.1234)); REQUIRE(sm.get(42) == Catch::Approx(0.1234)); From 82058b70dc6cf9b41043924028c7646dfed17041 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Fri, 14 Nov 2025 01:18:07 +0100 Subject: [PATCH 06/13] improve configuration forwarding --- include/bw/sqlitemap/sqlitemap.hpp | 150 ++++++--- test/catch2/unit_tests/conversion_functor.hpp | 116 +++++-- .../unit_tests/sqlitemap_core_tests.cpp | 308 ++++++++++++++++++ 3 files changed, 505 insertions(+), 69 deletions(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index 43f38c4..220526b 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -594,17 +594,6 @@ template auto taged_codec_from() template struct codec_pair { - codec_pair(KC k, VC v) - : key_codec(k) - , value_codec(v) - { - static_assert(is_key_codec>::value, - "KC must be a specialization of key_codec"); - - static_assert(is_value_codec>::value, - "VC must be a specialization of value_codec"); - } - using key_in_type = typename KC::in_type; using key_out_type = typename KC::out_type; using value_in_type = typename VC::in_type; @@ -612,8 +601,23 @@ template struct codec_pair KC key_codec; VC value_codec; + + template + codec_pair(KCT&& k, VCT&& v) + : key_codec(std::forward(k)) + , value_codec(std::forward(v)) + { + static_assert(is_key_codec>::value, + "KC must be a specialization of key_codec"); + + static_assert(is_value_codec>::value, + "VC must be a specialization of value_codec"); + } }; +template +codec_pair(KCT&&, VCT&&) -> codec_pair, std::decay_t>; + template struct is_codec_pair : std::false_type { // Default case, not a codec_pair specialization @@ -691,8 +695,8 @@ constexpr log_level default_log_level = log_level::off; template class configuration { public: - configuration(CODEC_PAIR codecs) - : _codecs(codecs) + configuration(CODEC_PAIR&& codecs) + : _codecs(std::forward(codecs)) { static_assert(codecs::is_codec_pair>::value, "CODEC_PAIR must be a specialization of codec_pair"); @@ -703,95 +707,135 @@ template class configuration return _codecs; } - configuration& filename(std::string filename) + configuration& filename(std::string filename) & { - _filename = filename; + _filename = std::move(filename); return *this; } + configuration&& filename(std::string filename) && + { + _filename = std::move(filename); + return std::move(*this); + } + std::string filename() const { return _filename; } - configuration& table(std::string table) + configuration& table(std::string table) & { - _table = table; + _table = std::move(table); return *this; } + configuration&& table(std::string table) && + { + _table = std::move(table); + return std::move(*this); + } + std::string table() const { return _table; } - configuration& mode(operation_mode mode) + configuration& mode(operation_mode mode) & { _mode = mode; return *this; } + configuration&& mode(operation_mode mode) && + { + _mode = mode; + return std::move(*this); + } + operation_mode mode() const { return _mode; } - configuration& auto_commit(bool auto_commit) + configuration& auto_commit(bool auto_commit) & { _auto_commit = auto_commit; return *this; } + configuration&& auto_commit(bool auto_commit) && + { + _auto_commit = auto_commit; + return std::move(*this); + } + bool auto_commit() const { return _auto_commit; } - configuration& log_level(log_level log_level) + configuration& log_level(bw::sqlitemap::log_level log_level) & { _log_level = log_level; return *this; } + configuration&& log_level(bw::sqlitemap::log_level log_level) && + { + _log_level = log_level; + return std::move(*this); + } bw::sqlitemap::log_level log_level() const { return _log_level; } - configuration& log_impl(logger::log_function log_impl) + configuration& log_impl(logger::log_function log_impl) & { - _log_impl = log_impl; + _log_impl = std::move(log_impl); return *this; } + configuration&& log_impl(logger::log_function log_impl) && + { + _log_impl = std::move(log_impl); + return std::move(*this); + } + logger::log_function log_impl() const { return _log_impl; } - configuration& pragma(const std::string& flag, int value) + configuration& pragma(std::string flag, int value) & { - return pragma(flag, std::to_string(value)); + return pragma(std::move(flag), std::to_string(value)); } - configuration& pragma(const std::string& flag, const std::string& value) + configuration&& pragma(std::string flag, int value) && { - return pragma("PRAGMA " + flag + " = " + value); + return std::move(pragma(std::move(flag), std::to_string(value))); } - configuration& pragma(const std::string& statement) + configuration& pragma(std::string flag, std::string value) & { - std::string prefix = "PRAGMA "; - if (statement.size() < prefix.size() || - !std::equal(prefix.begin(), prefix.end(), statement.begin(), - [](char a, char b) { return std::tolower(a) == std::tolower(b); })) - { - _pragma_statements.push_back(prefix + statement); - return *this; - } + return pragma("PRAGMA " + std::move(flag) + " = " + std::move(value)); + } - _pragma_statements.push_back(statement); - return *this; + configuration&& pragma(std::string flag, std::string value) && + { + return std::move(pragma("PRAGMA " + std::move(flag) + " = " + std::move(value))); + } + + configuration& pragma(std::string statement) & + { + return add_pragma_statement(std::move(statement)); + } + + configuration&& pragma(std::string statement) && + { + return std::move(add_pragma_statement(std::move(statement))); } const std::vector& pragmas() const @@ -800,6 +844,21 @@ template class configuration } private: + configuration& add_pragma_statement(std::string statement) + { + static constexpr std::string_view prefix = "PRAGMA "; + bool has_prefix = + statement.size() >= prefix.size() && + std::equal(prefix.begin(), prefix.end(), statement.begin(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }); + + if (!has_prefix) + statement = std::string(prefix) + std::move(statement); + + _pragma_statements.push_back(std::move(statement)); + return *this; + } + CODEC_PAIR _codecs; std::string _filename = default_filename; std::string _table = default_table; @@ -831,7 +890,7 @@ template auto config(CODEC_ARG codec) } } -template auto config(KC key_codec, VC value_codec) +template auto config(KC&& key_codec, VC&& value_codec) { return config(codecs::codec_pair(std::forward(key_codec), std::forward(value_codec))); } @@ -1431,8 +1490,19 @@ template > class { } - sqlitemap(configuration config) - : _config(std::forward>(config)) + sqlitemap(const configuration& config) + : _config(config) // copies + { + init_from_config(); + } + + sqlitemap(configuration&& config) + : _config(std::move(config)) // moves + { + init_from_config(); + } + + void init_from_config() { log().set_level(_config.log_level()); if (_config.log_impl()) diff --git a/test/catch2/unit_tests/conversion_functor.hpp b/test/catch2/unit_tests/conversion_functor.hpp index e519754..825dd3b 100644 --- a/test/catch2/unit_tests/conversion_functor.hpp +++ b/test/catch2/unit_tests/conversion_functor.hpp @@ -11,56 +11,114 @@ namespace bw::testhelper { -template struct conversion_functor +struct counts { - explicit conversion_functor(const std::string& name, - std::function&& convert) - : name(name) - , convert(std::move(convert)) + int default_ctor_count = 0; + int copy_ctor_count = 0; + int move_ctor_count = 0; + int copy_assign_count = 0; + int move_assign_count = 0; + int dtor_count = 0; +}; + +template struct instance_counter +{ + std::string instance_name; + counts* counts_ptr; + bool was_moved = false; + + instance_counter(const std::string& name = "Instance", counts* c_ptr = nullptr) + : instance_name(name) + , counts_ptr(c_ptr) + { + if (!counts_ptr) + { + static counts default__global_counts; + counts_ptr = &default__global_counts; + } + ++counts_ptr->default_ctor_count; + std::cout << instance_name << " constructed count:" << counts_ptr->default_ctor_count + << "\n"; + } + + instance_counter(const instance_counter& other) + : instance_name(other.instance_name) + , counts_ptr(other.counts_ptr) { - std::cout << name << " constructed\n"; + if (counts_ptr) + ++counts_ptr->copy_ctor_count; + std::cout << instance_name << " copy constructed count:" << counts_ptr->copy_ctor_count + << "\n"; } - conversion_functor(const conversion_functor& other) - : name(other.name) - , convert(other.convert) + instance_counter(instance_counter&& other) noexcept + : instance_name(std::move(other.instance_name)) + , counts_ptr(other.counts_ptr) { - std::cout << name << " copy-constructed\n"; + if (counts_ptr) + ++counts_ptr->move_ctor_count; + std::cout << instance_name << " move constructed count:" << counts_ptr->move_ctor_count + << "\n"; + was_moved = true; } - conversion_functor(conversion_functor&& other) noexcept - : name(std::move(other.name)) - , convert(std::move(other.convert)) + instance_counter& operator=(const instance_counter& other) { - std::cout << name << " move-constructed\n"; + instance_name = other.instance_name; + counts_ptr = other.counts_ptr; + if (counts_ptr) + ++counts_ptr->copy_assign_count; + std::cout << instance_name << " copy assigned count:" << counts_ptr->copy_assign_count + << "\n"; + return static_cast(*this); } - conversion_functor& operator=(const conversion_functor& other) + instance_counter& operator=(instance_counter&& other) noexcept { - name = other.name; - convert = other.convert; - std::cout << name << " copy-assigned\n"; - return *this; + instance_name = std::move(other.instance_name); + counts_ptr = other.counts_ptr; + if (counts_ptr) + ++counts_ptr->move_assign_count; + std::cout << instance_name << " move assigned count:" << counts_ptr->move_assign_count + << "\n"; + + was_moved = true; + + return static_cast(*this); } - conversion_functor& operator=(conversion_functor&& other) noexcept + ~instance_counter() { - name = std::move(other.name); - convert = std::move(other.convert); - std::cout << name << " move-assigned\n"; - return *this; + if (was_moved) + return; // avoid counting destructors for moved-from instances + + if (counts_ptr) + ++counts_ptr->dtor_count; + + std::cout << instance_name << " destroyed count:" << counts_ptr->dtor_count << "\n"; } +}; - ~conversion_functor() +// --- Conversion Functor with per-instance tracking and name --- +template +struct conversion_functor : public instance_counter> +{ + using counter = instance_counter>; + + explicit conversion_functor(const std::string& name, + std::function&& convert, + counts* counts_ptr = nullptr) + : counter(name, counts_ptr) + , name(name.empty() ? "ConversionFunctor" : name) + , convert(std::move(convert)) { - std::cout << name << " destroyed\n"; } OUT_T operator()(const IN_T& value) const { - auto converted = convert(value); - std::cout << name << " converted " << value << " to " << converted << "\n"; - return converted; + auto result = convert(value); + std::cout << name << " converted " << value << " to " << result << "\n"; + return result; } std::string name; diff --git a/test/catch2/unit_tests/sqlitemap_core_tests.cpp b/test/catch2/unit_tests/sqlitemap_core_tests.cpp index ca92edf..bd7776d 100644 --- a/test/catch2/unit_tests/sqlitemap_core_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_core_tests.cpp @@ -8,6 +8,8 @@ #include +#include "conversion_functor.hpp" + using namespace bw::sqlitemap; using namespace bw::tempdir; namespace fs = std::filesystem; @@ -147,6 +149,312 @@ TEST_CASE("sqlitemap can be stored in a std::map") REQUIRE((db2["k2"] == "v2")); } +TEST_CASE("sqlitemap tries to avoid copies of configuration and codecs") +{ + using namespace bw::testhelper; + + // clang-format off + struct key_codec_encode_functor : public conversion_functor + { + key_codec_encode_functor(counts* counts_ptr = nullptr): conversion_functor( + "KEY_ENCODE_FUNCTOR", [](int key){ return "key-" + std::to_string(key); }, counts_ptr) + {} + }; + + struct key_codec_decode_functor : public conversion_functor + { + key_codec_decode_functor(counts* counts_ptr = nullptr): conversion_functor( + "KEY_DECODE_FUNCTOR", [](std::string key_str){ return std::atoi(key_str.substr(4).c_str()); }, counts_ptr) + {} + }; + + struct value_codec_encode_functor : public conversion_functor + { + value_codec_encode_functor(counts* counts_ptr = nullptr): conversion_functor( + "VALUE_ENCODE_FUNCTOR", [](double value){ return "value-" + std::to_string(value); }, counts_ptr) + {} + }; + + struct value_codec_decode_functor : public conversion_functor + { + value_codec_decode_functor(counts* counts_ptr = nullptr): conversion_functor( + "VALUE_DECODE_FUNCTOR", [](std::string value_str){ return std::atof(value_str.substr(6).c_str()); }, counts_ptr) + {} + }; + + struct test_config: public instance_counter, + public configuration, value_codec_t>> + { + using counter = instance_counter; + + test_config(counts* counts_ptr = nullptr) + : kc_encode_counts() + , kc_decode_counts() + , vc_encode_counts() + , vc_decode_counts() + , counter("######### TEST_CONFIG", counts_ptr) + , configuration(config( + key_codec( + key_codec_encode_functor{&kc_encode_counts}, + key_codec_decode_functor{&kc_decode_counts}), + value_codec( + value_codec_encode_functor{&vc_encode_counts}, + value_codec_decode_functor{&vc_decode_counts}))) + { + } + + counts kc_encode_counts; + counts kc_decode_counts; + counts vc_encode_counts; + counts vc_decode_counts; + }; + + // clang-format on + + { // check that copies are avoided when passing configuration as rvalue reference + counts test_config_counts; + { + sqlitemap sm(test_config{&test_config_counts}); + sm.set(42, 3.1415); + REQUIRE(sm.get(42) == Catch::Approx(3.1415)); + } + + REQUIRE(test_config_counts.default_ctor_count == 1); + REQUIRE(test_config_counts.copy_ctor_count == 0); + REQUIRE(test_config_counts.copy_assign_count == 0); + REQUIRE(test_config_counts.dtor_count == 1); + } + + { // check that copies are avoided when passing configuration and codecs as rvalue references + counts kc_encode_counts; + counts kc_decode_counts; + counts vc_encode_counts; + counts vc_decode_counts; + + { + sqlitemap sm(config(key_codec(key_codec_encode_functor{&kc_encode_counts}, + key_codec_decode_functor{&kc_decode_counts}), + value_codec(value_codec_encode_functor{&vc_encode_counts}, + value_codec_decode_functor{&vc_decode_counts}))); + sm.set(42, 3.1415); + sm.set(24, 2.4); + sm.set(1, 42); + sm.commit(); + + REQUIRE(sm.get(42) == Catch::Approx(3.1415)); + REQUIRE(sm.get(24) == Catch::Approx(2.4)); + REQUIRE(sm.get(1) == Catch::Approx(42)); + + for (const auto& [key, value] : sm) + { + std::cout << "key:" << key << " value:" << value << "\n"; + } + + auto res = std::find_if(sm.begin(), sm.end(), [](auto kv) { return kv.second > 10; }); + REQUIRE(res != sm.end()); + REQUIRE(res->first == 1); + } + + REQUIRE(kc_encode_counts.default_ctor_count == 1); + REQUIRE(kc_decode_counts.default_ctor_count == 1); + REQUIRE(vc_encode_counts.default_ctor_count == 1); + REQUIRE(vc_decode_counts.default_ctor_count == 1); + + REQUIRE(kc_encode_counts.copy_ctor_count == 0); + REQUIRE(kc_decode_counts.copy_ctor_count == 0); + REQUIRE(vc_encode_counts.copy_ctor_count == 0); + REQUIRE(vc_decode_counts.copy_ctor_count == 0); + + REQUIRE(kc_encode_counts.copy_assign_count == 0); + REQUIRE(kc_decode_counts.copy_assign_count == 0); + REQUIRE(vc_encode_counts.copy_assign_count == 0); + REQUIRE(vc_decode_counts.copy_assign_count == 0); + + REQUIRE(kc_encode_counts.dtor_count == 1); + REQUIRE(kc_decode_counts.dtor_count == 1); + REQUIRE(vc_encode_counts.dtor_count == 1); + REQUIRE(vc_decode_counts.dtor_count == 1); + } + + { // check that copies are avoided when passing configuration + // as rvalue and codecs as lvalue references + counts kc_encode_counts; + counts kc_decode_counts; + counts vc_encode_counts; + counts vc_decode_counts; + + { + auto kc = key_codec(key_codec_encode_functor{&kc_encode_counts}, + key_codec_decode_functor{&kc_decode_counts}); + + auto vc = value_codec(value_codec_encode_functor{&vc_encode_counts}, + value_codec_decode_functor{&vc_decode_counts}); + + sqlitemap sm(config(kc, vc)); // only copy of codecs, configuration is passed as rvalue + sm.set(42, 3.1415); + sm.set(24, 2.4); + sm.set(1, 42); + sm.commit(); + + REQUIRE(sm.get(42) == Catch::Approx(3.1415)); + REQUIRE(sm.get(24) == Catch::Approx(2.4)); + REQUIRE(sm.get(1) == Catch::Approx(42)); + + for (const auto& [key, value] : sm) + { + std::cout << "key:" << key << " value:" << value << "\n"; + } + + auto res = std::find_if(sm.begin(), sm.end(), [](auto kv) { return kv.second > 10; }); + REQUIRE(res != sm.end()); + REQUIRE(res->first == 1); + } + + REQUIRE(kc_encode_counts.default_ctor_count == 1); + REQUIRE(kc_decode_counts.default_ctor_count == 1); + REQUIRE(vc_encode_counts.default_ctor_count == 1); + REQUIRE(vc_decode_counts.default_ctor_count == 1); + + REQUIRE(kc_encode_counts.copy_ctor_count == 1); + REQUIRE(kc_decode_counts.copy_ctor_count == 1); + REQUIRE(vc_encode_counts.copy_ctor_count == 1); + REQUIRE(vc_decode_counts.copy_ctor_count == 1); + + REQUIRE(kc_encode_counts.copy_assign_count == 0); + REQUIRE(kc_decode_counts.copy_assign_count == 0); + REQUIRE(vc_encode_counts.copy_assign_count == 0); + REQUIRE(vc_decode_counts.copy_assign_count == 0); + + // 2 d'tor calls: 1 for original, 1 copy + REQUIRE(kc_encode_counts.dtor_count == 2); + REQUIRE(kc_decode_counts.dtor_count == 2); + REQUIRE(vc_encode_counts.dtor_count == 2); + REQUIRE(vc_decode_counts.dtor_count == 2); + } + + { // check that copies are avoided when passing configuration and codecs as lvalue references + counts kc_encode_counts; + counts kc_decode_counts; + counts vc_encode_counts; + counts vc_decode_counts; + + { + auto kc = key_codec(key_codec_encode_functor{&kc_encode_counts}, + key_codec_decode_functor{&kc_decode_counts}); + + auto vc = value_codec(value_codec_encode_functor{&vc_encode_counts}, + value_codec_decode_functor{&vc_decode_counts}); + + auto cfg = config(kc, vc); // first copy of codecs + + sqlitemap sm(cfg); // second copy of codecs, as the whole configuration is copied + sm.set(42, 3.1415); + sm.set(24, 2.4); + sm.set(1, 42); + sm.commit(); + + REQUIRE(sm.get(42) == Catch::Approx(3.1415)); + REQUIRE(sm.get(24) == Catch::Approx(2.4)); + REQUIRE(sm.get(1) == Catch::Approx(42)); + + for (const auto& [key, value] : sm) + { + std::cout << "key:" << key << " value:" << value << "\n"; + } + + auto res = std::find_if(sm.begin(), sm.end(), [](auto kv) { return kv.second > 10; }); + REQUIRE(res != sm.end()); + REQUIRE(res->first == 1); + } + + REQUIRE(kc_encode_counts.default_ctor_count == 1); + REQUIRE(kc_decode_counts.default_ctor_count == 1); + REQUIRE(vc_encode_counts.default_ctor_count == 1); + REQUIRE(vc_decode_counts.default_ctor_count == 1); + + REQUIRE(kc_encode_counts.copy_ctor_count == 2); + REQUIRE(kc_decode_counts.copy_ctor_count == 2); + REQUIRE(vc_encode_counts.copy_ctor_count == 2); + REQUIRE(vc_decode_counts.copy_ctor_count == 2); + + REQUIRE(kc_encode_counts.copy_assign_count == 0); + REQUIRE(kc_decode_counts.copy_assign_count == 0); + REQUIRE(vc_encode_counts.copy_assign_count == 0); + REQUIRE(vc_decode_counts.copy_assign_count == 0); + + // 3 d'tor calls: 1 for original, 2 copies + REQUIRE(kc_encode_counts.dtor_count == 3); + REQUIRE(kc_decode_counts.dtor_count == 3); + REQUIRE(vc_encode_counts.dtor_count == 3); + REQUIRE(vc_decode_counts.dtor_count == 3); + } + + { + counts kc_encode_counts; + counts kc_decode_counts; + counts vc_encode_counts; + counts vc_decode_counts; + + struct db_wrapper + { + using kc_t = key_codec_t; + using vc_t = value_codec_t; + using DB = sqlitemap_t; + + db_wrapper(std::string table_name, // + counts* kc_encode_counts, counts* kc_decode_counts, // + counts* vc_encode_counts, counts* vc_decode_counts) + : _db(config(key_codec(key_codec_encode_functor{kc_encode_counts}, + key_codec_decode_functor{kc_decode_counts}), + value_codec(value_codec_encode_functor{vc_encode_counts}, + value_codec_decode_functor{vc_decode_counts})) + .table(table_name)) + { + _db.set(42, 3.1415); + _db.set(24, 2.4); + _db.set(1, 42); + _db.commit(); + } + + int stored_records() const + { + return static_cast(_db.size()); + } + + private: + DB _db; + }; + + { + db_wrapper db("test_table", // + &kc_encode_counts, &kc_decode_counts, // + &vc_encode_counts, &vc_decode_counts); + REQUIRE(db.stored_records() == 3); + } + + REQUIRE(kc_encode_counts.default_ctor_count == 1); + REQUIRE(kc_decode_counts.default_ctor_count == 1); + REQUIRE(vc_encode_counts.default_ctor_count == 1); + REQUIRE(vc_decode_counts.default_ctor_count == 1); + + REQUIRE(kc_encode_counts.copy_ctor_count == 0); + REQUIRE(kc_decode_counts.copy_ctor_count == 0); + REQUIRE(vc_encode_counts.copy_ctor_count == 0); + REQUIRE(vc_decode_counts.copy_ctor_count == 0); + + REQUIRE(kc_encode_counts.copy_assign_count == 0); + REQUIRE(kc_decode_counts.copy_assign_count == 0); + REQUIRE(vc_encode_counts.copy_assign_count == 0); + REQUIRE(vc_decode_counts.copy_assign_count == 0); + + REQUIRE(kc_encode_counts.dtor_count == 1); + REQUIRE(kc_decode_counts.dtor_count == 1); + REQUIRE(vc_encode_counts.dtor_count == 1); + REQUIRE(vc_decode_counts.dtor_count == 1); + } +} + TEST_CASE("sqlitemap can be represented as string") { sqlitemap sm(config().filename(":memory:")); From bc48dbea14c6525bfcc553ffc0bab62f188481d9 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Fri, 14 Nov 2025 01:52:18 +0100 Subject: [PATCH 07/13] test configurations fluent api --- .../unit_tests/sqlitemap_core_tests.cpp | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/catch2/unit_tests/sqlitemap_core_tests.cpp b/test/catch2/unit_tests/sqlitemap_core_tests.cpp index bd7776d..2c569c5 100644 --- a/test/catch2/unit_tests/sqlitemap_core_tests.cpp +++ b/test/catch2/unit_tests/sqlitemap_core_tests.cpp @@ -455,6 +455,49 @@ TEST_CASE("sqlitemap tries to avoid copies of configuration and codecs") } } +TEST_CASE("sqlitemap configuration offers a fluent interface") +{ + // configure all options using rvalue method calls + auto cfg = config() + .filename("test_db.sqlite") + .table("test_table") + .mode(operation_mode::w) + .auto_commit(true) + .log_level(log_level::debug) + .log_impl([](auto level, auto msg) {}) + .pragma("journal_mode", "WAL") + .pragma("cache_size", -64000) + .pragma("temp_store = 2"); + + REQUIRE(cfg.filename() == "test_db.sqlite"); + REQUIRE(cfg.table() == "test_table"); + REQUIRE(cfg.mode() == operation_mode::w); + REQUIRE(cfg.auto_commit()); + REQUIRE(cfg.log_level() == log_level::debug); + REQUIRE(cfg.pragmas() == std::vector{"PRAGMA journal_mode = WAL", + "PRAGMA cache_size = -64000", + "PRAGMA temp_store = 2"}); + + // configure all options using lvalue method calls + auto cfg2 = config(); + cfg2.filename("test_db2.sqlite"); + cfg2.table("test_table2"); + cfg2.mode(operation_mode::r); + cfg2.auto_commit(false); + cfg2.log_level(log_level::error); + cfg2.log_impl([](auto level, auto msg) {}); + cfg2.pragma("cache_size", 2000); + cfg2.pragma("synchronous", "OFF"); + + REQUIRE(cfg2.filename() == "test_db2.sqlite"); + REQUIRE(cfg2.table() == "test_table2"); + REQUIRE(cfg2.mode() == operation_mode::r); + REQUIRE_FALSE(cfg2.auto_commit()); + REQUIRE(cfg2.log_level() == log_level::error); + REQUIRE(cfg2.pragmas() == + std::vector{"PRAGMA cache_size = 2000", "PRAGMA synchronous = OFF"}); +} + TEST_CASE("sqlitemap can be represented as string") { sqlitemap sm(config().filename(":memory:")); From 8412a47c839566092b0cfe555fd01946de978fc4 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Fri, 14 Nov 2025 02:03:40 +0100 Subject: [PATCH 08/13] bump version --- include/bw/sqlitemap/sqlitemap.hpp | 2 +- vcpkg.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index 220526b..f2f4464 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -1,5 +1,5 @@ // sqlitemap — Persistent Map Backed by SQLite -// version 1.1.0 +// version 1.2.0 // https://github.com/bw-hro/sqlitemap // SPDX-FileCopyrightText: 2024-present Benno Waldhauer diff --git a/vcpkg.json b/vcpkg.json index 037f3f0..458ff3d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "sqlitemap", - "version-string": "1.1.0", + "version-string": "1.2.0", "dependencies": [ "bw-tempdir", "catch2", From 5cef12e8d0be490c6ab562ec2b255b03a8163832 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sat, 15 Nov 2025 19:53:59 +0100 Subject: [PATCH 09/13] commit on exit --- examples/lines_to_sqlitemap.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/lines_to_sqlitemap.cpp b/examples/lines_to_sqlitemap.cpp index 1fef670..e91e936 100644 --- a/examples/lines_to_sqlitemap.cpp +++ b/examples/lines_to_sqlitemap.cpp @@ -77,6 +77,7 @@ class processor void exit() { std::cout << "Existing..." << std::endl; + db.commit(); db.close(); } From f0da328cf19725375576ed1ea743efe6ce4c2d16 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sat, 15 Nov 2025 19:58:43 +0100 Subject: [PATCH 10/13] use type aliases --- examples/sqlitemap_tiles.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/sqlitemap_tiles.cpp b/examples/sqlitemap_tiles.cpp index 23a4786..f8ee8cc 100644 --- a/examples/sqlitemap_tiles.cpp +++ b/examples/sqlitemap_tiles.cpp @@ -52,14 +52,14 @@ class tiles { using key_type = tile_location; using value_type = tile_bitmap; - using key_codec_t = sm::codecs::key_codec; - using value_codec_t = sm::codecs::value_codec; - using db = sm::sqlitemap>; + using key_codec = sm::key_codec_t; + using value_codec = sm::value_codec_t; + using db = sm::sqlitemap_t; public: tiles() - : data(sm::config(key_codec_t{to_blob, from_blob}, - value_codec_t{to_blob, from_blob}) + : data(sm::config(key_codec{to_blob, from_blob}, + value_codec{to_blob, from_blob}) .log_level(sm::log_level::debug)) { data.set(tile_location{0, 0, 0}, tile_bitmap{1, 1, 0, 0, // From 9dcd8ec8c60957a59c7646cc35b015972753a276 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sat, 15 Nov 2025 20:00:59 +0100 Subject: [PATCH 11/13] improve documentation of type aliases --- include/bw/sqlitemap/sqlitemap.hpp | 138 +++++++++++++++++++++++++++++ readme.md | 69 +++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index f2f4464..26ffe02 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -1402,6 +1402,11 @@ template struct sqlitemap_node_type * @tparam CODEC_PAIR The codec pair type used for encoding and decoding keys and values. * Defaults to the codec pair from the global config(). * + * @note To simplify construction of `sqlitemap` instances with different combinations of + * codecs or native types, the helper alias `sqlitemap_t` is provided. It allows users + * to create `sqlitemap` types without specifying the full codec pair explicitly, + * reducing boilerplate and improving readability—especially when working with custom + * codecs, inferred types, or mixed key/value configurations. * * @note Only single-pass iteration is supported to enable lazy evaluation and caching of query * results. As a result, iterators returned by STL-like operations are intended solely for data @@ -2502,6 +2507,63 @@ template struct sqlitemap_t_helper using type = typename sqlitemap_alias_2_arg::type; }; +/** + * @brief Helper alias for constructing `sqlitemap` types with flexible template arguments. + * + * `sqlitemap_t` is a variadic template alias that selects the appropriate `sqlitemap` + * instantiation based on the number and nature of its template arguments. It eliminates + * the need to manually specify codec pairs and provides a concise, user-friendly way to + * construct `sqlitemap` types from: + * + * - no arguments (use default key/value codecs from `config()`) + * - a single codec pair, key codec, value codec, or native C++ type + * - two arguments describing key codec/type and value codec/type + * + * The alias delegates to internal helper specializations that analyze the template + * arguments and derive the correct codec pair. This allows users to write: + * + * @code + * sqlitemap_t<> // uses default configured codecs + * sqlitemap_t // key: int (native), default value codec + * sqlitemap_t>() // custom key codec + * sqlitemap_t // both native types, identity codecs generated + * @endcode + * + * without manually constructing: + * + * @code + * sqlitemap, codecs::value_codec<...>>> + * @endcode + * + * Supported usage patterns: + * + * 1. **No arguments** + * Uses the default codec pair derived from `config().codecs()`. + * + * 2. **Single argument** + * - If it is a codec pair → use it directly. + * - If it is a key codec → pair with default value codec. + * - If it is a value codec → pair with default key codec. + * - If it is a native type → identity codec if supported by SQLite, otherwise fallback + * value codec with storage type `std::string`. + * + * 3. **Two arguments** + * - If both are codecs → use them directly. + * - If one is native and one is a codec → wrap the native type + * in the appropriate identity codec. + * - If both are native types → construct corresponding identity codecs automatically. + * + * @tparam Ts Template parameters controlling how the final `sqlitemap` type is derived: + * - `<>` → default configuration + * - `` → codec pair, key codec, value codec, or native type + * - `` → explicit key/value codecs or native types + * + * @note This alias performs compile-time selection of codecs. Native types must have native + * SQLite support (e.g., integral types, floating-point types, std::string, blob, + * nullptr_t). Types without native support must be wrapped in custom codecs. + * + * @see sqlitemap, key_codec, value_codec, codec_pair + */ template using sqlitemap_t = typename sqlitemap_t_helper::type; // Helper alias templates for value_codec @@ -2523,6 +2585,44 @@ template struct value_codec_t_helper; }; +/** + * @brief Convenience alias for constructing value codec types with flexible template arguments. + * + * `value_codec_t` simplifies the creation of `codecs::value_codec` types by supporting + * multiple usage patterns with zero, one, or two template parameters. + * + * **Usage patterns** + * + * 1. **No template arguments** + * Uses the library's `default_value_codec`. + * @code + * using vc = value_codec_t<>; // default value codec + * @endcode + * + * 2. **Single template argument** + * Interpreted as both the input and output type of the codec. + * @code + * using vc = value_codec_t; // value_codec + * @endcode + * + * 3. **Two template arguments** + * Explicitly specifies the input and output types. + * @code + * using vc = value_codec_t; // value_codec + * @endcode + * + * **Summary** + * + * This alias reduces boilerplate when defining value codecs, improves readability, + * and ensures consistent construction of `codecs::value_codec` types throughout the codebase. + * + * @tparam Ts + * - `<>` → use default value codec + * - `` → create `value_codec` + * - `` → create `value_codec` + * + * @see codecs::value_codec + */ template using value_codec_t = typename value_codec_t_helper::type; // Helper alias templates for key_codec @@ -2544,6 +2644,44 @@ template struct key_codec_t_helper using type = codecs::key_codec; }; +/** + * @brief Convenience alias for constructing key codec types with flexible template arguments. + * + * `key_codec_t` simplifies the creation of `codecs::key_codec` types by supporting + * multiple usage patterns with zero, one, or two template parameters. + * + * **Usage patterns** + * + * 1. **No template arguments** + * Uses the library's `default_key_codec`. + * @code + * using kc = key_codec_t<>; // default key codec + * @endcode + * + * 2. **Single template argument** + * Interpreted as both the input and output type of the codec. + * @code + * using kc = key_codec_t; // key_codec + * @endcode + * + * 3. **Two template arguments** + * Explicitly specifies the input and output types. + * @code + * using kc = key_codec_t; // key_codec + * @endcode + * + * **Summary** + * + * This alias reduces boilerplate when defining key codecs, improves readability, + * and ensures consistent construction of `codecs::key_codec` types throughout the codebase. + * + * @tparam Ts + * - `<>` → use default key codec + * - `` → create `key_codec` + * - `` → create `key_codec` + * + * @see codecs::key_codec + */ template using key_codec_t = typename key_codec_t_helper::type; } // namespace bw::sqlitemap \ No newline at end of file diff --git a/readme.md b/readme.md index d55eb82..8deadc0 100644 --- a/readme.md +++ b/readme.md @@ -422,6 +422,75 @@ int main() - [sqlitemap_zlib.cpp](examples/sqlitemap_zlib.cpp) demonstrates how to use **sqlitemap** to store compressed values using [zlib](https://github.com/madler/zlib). - Please make sure to also inspect [sqlitemap_codecs_tests.cpp](test/catch2/unit_tests/sqlitemap_codecs_tests.cpp) were further details regarding encoding/decoding using codecs are covered. +#### Using `sqlitemap_t` for convenience with sqlitemap type construction + +To reduce boilerplate when creating `sqlitemap` instances, the helper alias **`sqlitemap_t`** automatically selects the appropriate codec configuration based on the template arguments you provide. + +```c++ +#include + +using namespace bw::sqlitemap; + +// Default key/value types (std::string identity) +sqlitemap_t<> db; // same as just sqlitemap + +// Specify only the value type (key codec defaults to std::string identity) +sqlitemap_t db; +sqlitemap_t> db; + +// Specify only the key type (value codec defaults to std::string identity) +sqlitemap_t> db; + +// Specify both key and value types (identity codecs inferred) +sqlitemap_t db; + +// Use explicit codec types directly +sqlitemap_t, value_codec_t db; +``` + +Example showing how `sqlitemap_t`, `key_codec_t` and `value_codec_t` support convenient application-specific type aliases and improve readability: + +```c++ + using namespace bw::sqlitemap; + + template blob to_blob(const T& data){...} + template T from_blob(blob blob){...} + + struct tile_location ... + struct tile_bitmap ... + + class tiles + { + public: + using location_codec = key_codec_t; + using bitmap_codec = value_codec_t; + using tiles_db = sqlitemap_t; + + tiles() + : data(std::make_unique( + config(location_codec{to_blob, from_blob}, + bitmap_codec{to_blob, from_blob}) + .filename("tiles.sqlite") + .table("tiles"))) + { + } + + void save(const tile_location& location, const tile_bitmap& bitmap) + { + data->set(location, bitmap); + } + + tile_bitmap tile_for(const tile_location& location) const + { + return data->get(location); + } + + private: + std::unique_ptr data; + }; + +``` + ## Tests / Examples / Additional Documentation - **sqlitemap** is extensively covered by [unit tests](test), which also serve as documentation and usage examples. From 70352fab7124b86617bae489ec65c5ed81e50a93 Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sat, 15 Nov 2025 20:22:53 +0100 Subject: [PATCH 12/13] reformat example snippet --- readme.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/readme.md b/readme.md index 8deadc0..0822110 100644 --- a/readme.md +++ b/readme.md @@ -451,43 +451,43 @@ sqlitemap_t, value_codec_t db; Example showing how `sqlitemap_t`, `key_codec_t` and `value_codec_t` support convenient application-specific type aliases and improve readability: ```c++ - using namespace bw::sqlitemap; +using namespace bw::sqlitemap; + +template blob to_blob(const T& data){...} +template T from_blob(blob blob){...} - template blob to_blob(const T& data){...} - template T from_blob(blob blob){...} +struct tile_location ... +struct tile_bitmap ... - struct tile_location ... - struct tile_bitmap ... +class tiles +{ + public: + using location_codec = key_codec_t; + using bitmap_codec = value_codec_t; + using tiles_db = sqlitemap_t; + + tiles() + : data(std::make_unique( + config(location_codec{to_blob, from_blob}, + bitmap_codec{to_blob, from_blob}) + .filename("tiles.sqlite") + .table("tiles"))) + { + } - class tiles + void save(const tile_location& location, const tile_bitmap& bitmap) { - public: - using location_codec = key_codec_t; - using bitmap_codec = value_codec_t; - using tiles_db = sqlitemap_t; - - tiles() - : data(std::make_unique( - config(location_codec{to_blob, from_blob}, - bitmap_codec{to_blob, from_blob}) - .filename("tiles.sqlite") - .table("tiles"))) - { - } - - void save(const tile_location& location, const tile_bitmap& bitmap) - { - data->set(location, bitmap); - } - - tile_bitmap tile_for(const tile_location& location) const - { - return data->get(location); - } - - private: - std::unique_ptr data; - }; + data->set(location, bitmap); + } + + tile_bitmap tile_for(const tile_location& location) const + { + return data->get(location); + } + + private: + std::unique_ptr data; +}; ``` From 9223c7ca691dd41ab9cee0d2a832f8b576cd5bdf Mon Sep 17 00:00:00 2001 From: Benno Waldhauer Date: Sat, 15 Nov 2025 23:20:46 +0100 Subject: [PATCH 13/13] switch back to single config accepting c'tor --- include/bw/sqlitemap/sqlitemap.hpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/include/bw/sqlitemap/sqlitemap.hpp b/include/bw/sqlitemap/sqlitemap.hpp index 26ffe02..da19877 100644 --- a/include/bw/sqlitemap/sqlitemap.hpp +++ b/include/bw/sqlitemap/sqlitemap.hpp @@ -1495,19 +1495,8 @@ template > class { } - sqlitemap(const configuration& config) - : _config(config) // copies - { - init_from_config(); - } - - sqlitemap(configuration&& config) - : _config(std::move(config)) // moves - { - init_from_config(); - } - - void init_from_config() + sqlitemap(configuration config) + : _config(std::move(config)) { log().set_level(_config.log_level()); if (_config.log_impl())