From 9548545007a1b5b5af2c0c386a53336dcc08f9b7 Mon Sep 17 00:00:00 2001 From: Scott Fries Date: Mon, 9 Mar 2026 00:18:58 -0600 Subject: [PATCH] #124 Adding support for invalidating cache objects --- docs/cache.md | 38 +++++++++++++++++++++++++++++++++++ include/sqlgen/cache.hpp | 11 ++++++++++ tests/duckdb/test_cache.cpp | 36 ++++++++++++++++++++------------- tests/mysql/test_cache.cpp | 36 ++++++++++++++++++++------------- tests/postgres/test_cache.cpp | 36 ++++++++++++++++++++------------- tests/sqlite/test_cache.cpp | 36 ++++++++++++++++++++------------- 6 files changed, 137 insertions(+), 56 deletions(-) diff --git a/docs/cache.md b/docs/cache.md index d359cf79..aaac77b8 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -50,6 +50,44 @@ To create a cache with a virtually unlimited size, you can specify a `max_size` const auto cached_query = sqlgen::cache<0>(query); ``` +### Cache Invalidation + +The cache has no mechanism for determining if a cached result is still valid/up-to-date. Because of this, the cache should be explicitly `clear`ed before use any time another query is made that invalidates the cache. + +```cpp +#include + +struct User { + std::string name; + int age; +}; + +const auto conn = sqlgen::sqlite::connect(); + +const auto user = User{.name = "John", .age = 30}; +sqlgen::write(conn, user); + +const auto user_b = User{.name = "Mary", .age = 25}; +sqlgen::write(conn, user_b); + +const auto query = sqlgen::read>; +const auto cached_query = sqlgen::cache<100>(query); + +const auto users1 = cached_query(conn).value(); +// The cache size will now contain a result consisting of John & Mary + +const auto user_c = User{.name = "Bill", .age = 50}; +sqlgen::write(conn, user_c); + +// Because the query was previously cached, user2 will still only contain John & Mary while Bill will be absent. +const auto users2 = cached_query(conn).value(); + +cached_query.clear(conn); + +// Now, the query will be executed again since it's no longer cached. Afterwards, the cache will again store an up-to-date result and users3 will contain John, Mary, & Bill. +const auto users3 = cached_query(conn).value(); +``` + ### Thread Safety and Concurrency The cache is thread-safe and can be accessed from multiple threads concurrently. A `std::shared_mutex` is used to protect the cache from data races. diff --git a/include/sqlgen/cache.hpp b/include/sqlgen/cache.hpp index 45de3807..60446a09 100644 --- a/include/sqlgen/cache.hpp +++ b/include/sqlgen/cache.hpp @@ -65,6 +65,11 @@ class CacheImpl { }); } + static void clear() { + std::unique_lock write_lock(mtx_); + cache_.clear(); + } + static const auto& cache() { return cache_; } private: @@ -107,6 +112,12 @@ struct Cache { _max_size>::cache(); } + template + requires is_connection + static void clear(const Result>&) { + CacheImpl, _max_size>::clear(); + } + QueryT query_; }; diff --git a/tests/duckdb/test_cache.cpp b/tests/duckdb/test_cache.cpp index 322442b7..11ee96b5 100644 --- a/tests/duckdb/test_cache.cpp +++ b/tests/duckdb/test_cache.cpp @@ -26,20 +26,28 @@ TEST(duckdb, test_cache) { const auto cached_query = sqlgen::cache<100>(query); - const auto user1 = conn.and_then(cache<100>(query)).value(); - - EXPECT_EQ(cached_query.cache(conn).size(), 1); - - const auto user2 = cached_query(conn).value(); - const auto user3 = cached_query(conn).value(); - - EXPECT_EQ(user1.name, "John"); - EXPECT_EQ(user1.age, 30); - EXPECT_EQ(user2.name, "John"); - EXPECT_EQ(user2.age, 30); - EXPECT_EQ(cached_query.cache(conn).size(), 1); - EXPECT_EQ(user3.name, "John"); - EXPECT_EQ(user3.age, 30); + auto test_cache_population = [&]() { + const auto user1 = conn.and_then(cache<100>(query)).value(); + + EXPECT_EQ(cached_query.cache(conn).size(), 1); + + const auto user2 = cached_query(conn).value(); + const auto user3 = cached_query(conn).value(); + + EXPECT_EQ(user1.name, "John"); + EXPECT_EQ(user1.age, 30); + EXPECT_EQ(user2.name, "John"); + EXPECT_EQ(user2.age, 30); + EXPECT_EQ(cached_query.cache(conn).size(), 1); + EXPECT_EQ(user3.name, "John"); + EXPECT_EQ(user3.age, 30); + }; + test_cache_population(); + + // Test cache invalidation + cached_query.clear(conn); + EXPECT_EQ(cached_query.cache(conn).size(), 0); + test_cache_population(); } } // namespace test_cache diff --git a/tests/mysql/test_cache.cpp b/tests/mysql/test_cache.cpp index b088e10b..67444457 100644 --- a/tests/mysql/test_cache.cpp +++ b/tests/mysql/test_cache.cpp @@ -33,20 +33,28 @@ TEST(mysql, test_cache) { const auto cached_query = sqlgen::cache<100>(query); - const auto user1 = conn.and_then(cache<100>(query)).value(); - - EXPECT_EQ(cached_query.cache(conn).size(), 1); - - const auto user2 = cached_query(conn).value(); - const auto user3 = cached_query(conn).value(); - - EXPECT_EQ(user1.name, "John"); - EXPECT_EQ(user1.age, 30); - EXPECT_EQ(user2.name, "John"); - EXPECT_EQ(user2.age, 30); - EXPECT_EQ(cached_query.cache(conn).size(), 1); - EXPECT_EQ(user3.name, "John"); - EXPECT_EQ(user3.age, 30); + auto test_cache_population = [&]() { + const auto user1 = conn.and_then(cache<100>(query)).value(); + + EXPECT_EQ(cached_query.cache(conn).size(), 1); + + const auto user2 = cached_query(conn).value(); + const auto user3 = cached_query(conn).value(); + + EXPECT_EQ(user1.name, "John"); + EXPECT_EQ(user1.age, 30); + EXPECT_EQ(user2.name, "John"); + EXPECT_EQ(user2.age, 30); + EXPECT_EQ(cached_query.cache(conn).size(), 1); + EXPECT_EQ(user3.name, "John"); + EXPECT_EQ(user3.age, 30); + }; + test_cache_population(); + + // Test cache invalidation + cached_query.clear(conn); + EXPECT_EQ(cached_query.cache(conn).size(), 0); + test_cache_population(); } } // namespace test_cache diff --git a/tests/postgres/test_cache.cpp b/tests/postgres/test_cache.cpp index c38d2a08..34145e28 100644 --- a/tests/postgres/test_cache.cpp +++ b/tests/postgres/test_cache.cpp @@ -33,20 +33,28 @@ TEST(postgres, test_cache) { const auto cached_query = sqlgen::cache<100>(query); - const auto user1 = conn.and_then(cache<100>(query)).value(); - - EXPECT_EQ(cached_query.cache(conn).size(), 1); - - const auto user2 = cached_query(conn).value(); - const auto user3 = cached_query(conn).value(); - - EXPECT_EQ(user1.name, "John"); - EXPECT_EQ(user1.age, 30); - EXPECT_EQ(user2.name, "John"); - EXPECT_EQ(user2.age, 30); - EXPECT_EQ(cached_query.cache(conn).size(), 1); - EXPECT_EQ(user3.name, "John"); - EXPECT_EQ(user3.age, 30); + auto test_cache_population = [&]() { + const auto user1 = conn.and_then(cache<100>(query)).value(); + + EXPECT_EQ(cached_query.cache(conn).size(), 1); + + const auto user2 = cached_query(conn).value(); + const auto user3 = cached_query(conn).value(); + + EXPECT_EQ(user1.name, "John"); + EXPECT_EQ(user1.age, 30); + EXPECT_EQ(user2.name, "John"); + EXPECT_EQ(user2.age, 30); + EXPECT_EQ(cached_query.cache(conn).size(), 1); + EXPECT_EQ(user3.name, "John"); + EXPECT_EQ(user3.age, 30); + }; + test_cache_population(); + + // Test cache invalidation + cached_query.clear(conn); + EXPECT_EQ(cached_query.cache(conn).size(), 0); + test_cache_population(); } } // namespace test_cache diff --git a/tests/sqlite/test_cache.cpp b/tests/sqlite/test_cache.cpp index 86b63667..4cf58141 100644 --- a/tests/sqlite/test_cache.cpp +++ b/tests/sqlite/test_cache.cpp @@ -26,20 +26,28 @@ TEST(sqlite, test_cache) { const auto cached_query = sqlgen::cache<100>(query); - const auto user1 = conn.and_then(cache<100>(query)).value(); - - EXPECT_EQ(cached_query.cache(conn).size(), 1); - - const auto user2 = cached_query(conn).value(); - const auto user3 = cached_query(conn).value(); - - EXPECT_EQ(user1.name, "John"); - EXPECT_EQ(user1.age, 30); - EXPECT_EQ(user2.name, "John"); - EXPECT_EQ(user2.age, 30); - EXPECT_EQ(cached_query.cache(conn).size(), 1); - EXPECT_EQ(user3.name, "John"); - EXPECT_EQ(user3.age, 30); + auto test_cache_population = [&]() { + const auto user1 = conn.and_then(cache<100>(query)).value(); + + EXPECT_EQ(cached_query.cache(conn).size(), 1); + + const auto user2 = cached_query(conn).value(); + const auto user3 = cached_query(conn).value(); + + EXPECT_EQ(user1.name, "John"); + EXPECT_EQ(user1.age, 30); + EXPECT_EQ(user2.name, "John"); + EXPECT_EQ(user2.age, 30); + EXPECT_EQ(cached_query.cache(conn).size(), 1); + EXPECT_EQ(user3.name, "John"); + EXPECT_EQ(user3.age, 30); + }; + test_cache_population(); + + // Test cache invalidation + cached_query.clear(conn); + EXPECT_EQ(cached_query.cache(conn).size(), 0); + test_cache_population(); } } // namespace test_cache