Skip to content

Commit

Permalink
WIP: [DataMapper] RecordCache API
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Parpart <[email protected]>
  • Loading branch information
christianparpart committed Jan 30, 2025
1 parent f0f7c1c commit 0d013d2
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/Lightweight/DataMapper/DataMapper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "HasManyThrough.hpp"
#include "HasOneThrough.hpp"
#include "Record.hpp"
#include "RecordCache.hpp"
#include "RecordId.hpp"

#include <reflection-cpp/reflection.hpp>
Expand Down Expand Up @@ -581,6 +582,12 @@ std::optional<Record> DataMapper::QuerySingle(PrimaryKeyTypes&&... primaryKeys)
{
static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");

#if defined(LIGHTWEIGHT_USE_RECORD_CACHE) // TODO
auto& cache = RecordCache<Record>::Instance();
if (auto cachedRecord = cache.Lookup(primaryKeys...); cachedRecord)
return { *cachedRecord };
#endif

auto queryBuilder = _connection.Query(RecordTableName<Record>).Select();

Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
Expand Down
16 changes: 15 additions & 1 deletion src/Lightweight/DataMapper/Field.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ struct Field
namespace detail
{

template <typename T>
struct IsAutoAssignPrimaryKeyField: std::false_type {};

template <typename T, auto P>
struct IsAutoAssignPrimaryKeyField<Field<T, PrimaryKey::AutoAssign, P>>: std::true_type {};

template <typename T, auto P>
struct IsAutoAssignPrimaryKeyField<Field<T, P, PrimaryKey::AutoAssign>>: std::true_type {};

template <typename T>
struct IsAutoIncrementPrimaryKeyField: std::false_type {};

Expand All @@ -162,7 +171,12 @@ struct IsFieldType<Field<T, P1, P2>>: std::true_type {};
} // namespace detail
// clang-format on

// Requires that T satisfies to be a field with storage and is considered a primary key.
/// Tests if T is a Field<> that is a primary key.
template <typename T>
constexpr bool IsPrimaryKey =
detail::IsAutoAssignPrimaryKeyField<T>::value || detail::IsAutoIncrementPrimaryKeyField<T>::value;

/// Requires that T satisfies to be a field with storage and is considered a primary key.
template <typename T>
constexpr bool IsAutoIncrementPrimaryKey = detail::IsAutoIncrementPrimaryKeyField<T>::value;

Expand Down
40 changes: 40 additions & 0 deletions src/Lightweight/DataMapper/Record.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

#pragma once

#include "Field.hpp"

#include <reflection-cpp/reflection.hpp>

#include <concepts>

/// @brief Represents a record type that can be used with the DataMapper.
Expand All @@ -12,3 +16,39 @@
/// @ingroup DataMapper
template <typename Record>
concept DataMapperRecord = std::is_aggregate_v<Record>;

namespace detail
{

template <std::size_t I, typename Record>
constexpr std::optional<size_t> FindPrimaryKeyIndex()
{
static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
if constexpr (I < Reflection::CountMembers<Record>)
{
if constexpr (IsPrimaryKey<Reflection::MemberTypeOf<I, Record>>)
return { I };
else
return FindPrimaryKeyIndex<I + 1, Record>();
}
return std::nullopt;
}

} // namespace detail

/// Declare RecordPrimaryKeyIndex<Record> to retrieve the primary key index of the given record.
template <typename Record>
constexpr size_t RecordPrimaryKeyIndex = detail::FindPrimaryKeyIndex<0, Record>().value_or(static_cast<size_t>(-1));

/// Retrieves a reference to the given record's primary key.
template <typename Record>
decltype(auto) RecordPrimaryKeyOf(Record&& record)
{
// static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
// static_assert(RecordPrimaryKeyIndex<Record> != static_cast<size_t>(-1), "Record must have a primary key");
return Reflection::GetMemberAt<RecordPrimaryKeyIndex<std::remove_cvref_t<Record>>>(std::forward<Record>(record));
}

/// Reflects the primary key type of the given record.
template <typename Record>
using RecordPrimaryKeyType = typename Reflection::MemberTypeOf<RecordPrimaryKeyIndex<Record>, Record>::ValueType;
73 changes: 73 additions & 0 deletions src/Lightweight/DataMapper/RecordCache.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include "../Api.hpp"
#include "Record.hpp"

#include <reflection-cpp/reflection.hpp>

#include <unordered_map>

/// Manages the cache of the records of the given type.
template <typename Record>
class RecordCache
{
public:
using PrimaryKey = RecordPrimaryKeyType<Record>;

explicit RecordCache(size_t capacity):
_cache()
{
std::ignore = capacity; // TODO: used when moving to LRU cache
}

LIGHTWEIGHT_API static RecordCache& Instance()
{
static RecordCache instance;
return instance;
}

/// Clears the cache.
void Clear();

/// Clears the record with the given primary key from the cache.
void Clear(PrimaryKey const& key);

/// Emplaces the record in the cache and returns a reference to the cached record.
Record& Emplace(Record&& record);

/// Looks up the record in the cache.
Record* Lookup(PrimaryKey const& key);

private:
// TODO: Use an LRU-based internal cache store here later
std::unordered_map<PrimaryKey, Record> _cache {};
};

template <typename Record>
inline void RecordCache<Record>::Clear()
{
_cache.clear();
}

template <typename Record>
inline void RecordCache<Record>::Clear(PrimaryKey const& key)
{
_cache.erase(key);
}

template <typename Record>
inline Record& RecordCache<Record>::Emplace(Record&& record)
{
auto const& primaryKey = RecordPrimaryKeyOf(record);
auto [it, inserted] = _cache.try_emplace(primaryKey, std::move(record));
return it->second;
}

template <typename Record>
inline Record* RecordCache<Record>::Lookup(PrimaryKey const& key)
{
if (auto it = _cache.find(key); it != _cache.end())
return &it->second;
return nullptr;
}
49 changes: 49 additions & 0 deletions src/tests/DataMapperTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,55 @@ TEST_CASE_METHOD(SqlTestFixture, "SQL entity naming (namespace)", "[DataMapper]"
CHECK(RecordTableName<Models::NamingTest2> == "NamingTest2_aliased"sv);
}

struct PKTest0
{
Field<int, PrimaryKey::AutoAssign> pk;
Field<char> field;
};

static_assert(IsPrimaryKey<Reflection::MemberTypeOf<0, PKTest0>>);
static_assert(!IsPrimaryKey<Reflection::MemberTypeOf<1, PKTest0>>);
static_assert(RecordPrimaryKeyIndex<PKTest0> == 0);
static_assert(std::same_as<RecordPrimaryKeyType<PKTest0>, int>);

struct PKTest1
{
Field<int> field;
Field<SqlTrimmedFixedString<10>, PrimaryKey::AutoAssign> pk;
};

static_assert(!IsPrimaryKey<Reflection::MemberTypeOf<0, PKTest1>>);
static_assert(IsPrimaryKey<Reflection::MemberTypeOf<1, PKTest1>>);
static_assert(RecordPrimaryKeyIndex<PKTest1> == 1);
static_assert(std::same_as<RecordPrimaryKeyType<PKTest1>, SqlTrimmedFixedString<10>>);

struct PKTestMulti
{
Field<int> fieldA;
Field<char> fieldB;
Field<SqlTrimmedFixedString<10>, PrimaryKey::AutoAssign> pk1;
Field<double, PrimaryKey::AutoAssign> pk2;
};

static_assert(RecordPrimaryKeyIndex<PKTestMulti> == 2,
"Primary key index should be the first primary key field in the record");
static_assert(std::same_as<RecordPrimaryKeyType<PKTestMulti>, SqlTrimmedFixedString<10>>);

TEST_CASE_METHOD(SqlTestFixture, "Primary key access", "[DataMapper]")
{
PKTest0 pk0;
RecordPrimaryKeyOf(pk0) = 42;
CHECK(pk0.pk.Value() == 42);

PKTest1 pk1;
RecordPrimaryKeyOf(pk1) = "Hello";
CHECK(pk1.pk.Value() == "Hello");

PKTestMulti pkMulti;
RecordPrimaryKeyOf(pkMulti) = "World";
CHECK(pkMulti.pk1.Value() == "World");
}

struct Person
{
Field<SqlGuid, PrimaryKey::AutoAssign> id;
Expand Down

0 comments on commit 0d013d2

Please sign in to comment.