#124 Adding support for invalidating cache objects (#125)

Co-authored-by: Scott Fries <Scott@ScottFries.com>
This commit is contained in:
Scott Fries
2026-03-10 11:02:00 -06:00
committed by GitHub
parent 5768fb9aa2
commit 7e5e6d11bf
6 changed files with 125 additions and 44 deletions
+38
View File
@@ -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 <sqlgen.hpp>
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<std::vector<User>>;
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.
+11
View File
@@ -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 <class Connection>
requires is_connection<Connection>
static void clear(const Result<Ref<Connection>>&) {
CacheImpl<QueryT, std::remove_cvref_t<Connection>, _max_size>::clear();
}
QueryT query_;
};
+19 -11
View File
@@ -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();
auto test_cache_population = [&]() {
const auto user1 = conn.and_then(cache<100>(query)).value();
EXPECT_EQ(cached_query.cache(conn).size(), 1);
EXPECT_EQ(cached_query.cache(conn).size(), 1);
const auto user2 = cached_query(conn).value();
const auto user3 = cached_query(conn).value();
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);
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
+19 -11
View File
@@ -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();
auto test_cache_population = [&]() {
const auto user1 = conn.and_then(cache<100>(query)).value();
EXPECT_EQ(cached_query.cache(conn).size(), 1);
EXPECT_EQ(cached_query.cache(conn).size(), 1);
const auto user2 = cached_query(conn).value();
const auto user3 = cached_query(conn).value();
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);
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
+19 -11
View File
@@ -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();
auto test_cache_population = [&]() {
const auto user1 = conn.and_then(cache<100>(query)).value();
EXPECT_EQ(cached_query.cache(conn).size(), 1);
EXPECT_EQ(cached_query.cache(conn).size(), 1);
const auto user2 = cached_query(conn).value();
const auto user3 = cached_query(conn).value();
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);
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
+19 -11
View File
@@ -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();
auto test_cache_population = [&]() {
const auto user1 = conn.and_then(cache<100>(query)).value();
EXPECT_EQ(cached_query.cache(conn).size(), 1);
EXPECT_EQ(cached_query.cache(conn).size(), 1);
const auto user2 = cached_query(conn).value();
const auto user3 = cached_query(conn).value();
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);
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