6#include <userver/cache/base_postgres_cache_fwd.hpp>
12#include <unordered_map>
14#include <fmt/format.h>
16#include <userver/cache/cache_statistics.hpp>
17#include <userver/cache/caching_component_base.hpp>
18#include <userver/components/component_config.hpp>
19#include <userver/components/component_context.hpp>
21#include <userver/storages/postgres/cluster.hpp>
22#include <userver/storages/postgres/component.hpp>
23#include <userver/storages/postgres/io/chrono.hpp>
25#include <userver/compiler/demangle.hpp>
26#include <userver/engine/sleep.hpp>
27#include <userver/logging/log.hpp>
28#include <userver/tracing/span.hpp>
29#include <userver/utils/assert.hpp>
30#include <userver/utils/cpu_relax.hpp>
31#include <userver/utils/meta.hpp>
32#include <userver/utils/void_t.hpp>
33#include <userver/yaml_config/merge_schemas.hpp>
35USERVER_NAMESPACE_BEGIN
126namespace pg_cache::detail {
129using ValueType =
typename T::ValueType;
131inline constexpr bool kHasValueType = meta::IsDetected<ValueType, T>;
134using RawValueTypeImpl =
typename T::RawValueType;
136inline constexpr bool kHasRawValueType = meta::IsDetected<RawValueTypeImpl, T>;
138using RawValueType = meta::DetectedOr<ValueType<T>, RawValueTypeImpl, T>;
140template <
typename PostgreCachePolicy>
141auto ExtractValue(RawValueType<PostgreCachePolicy>&& raw) {
142 if constexpr (kHasRawValueType<PostgreCachePolicy>) {
143 return Convert(std::move(raw), formats::parse::To<ValueType<PostgreCachePolicy>>());
145 return std::move(raw);
151using HasNameImpl = std::enable_if_t<!std::string_view{T::kName}.empty()>;
153inline constexpr bool kHasName = meta::IsDetected<HasNameImpl, T>;
157using HasQueryImpl =
decltype(T::kQuery);
159inline constexpr bool kHasQuery = meta::IsDetected<HasQueryImpl, T>;
163using HasGetQueryImpl =
decltype(T::GetQuery());
165inline constexpr bool kHasGetQuery = meta::IsDetected<HasGetQueryImpl, T>;
169using HasWhere =
decltype(T::kWhere);
171inline constexpr bool kHasWhere = meta::IsDetected<HasWhere, T>;
175using HasOrderBy =
decltype(T::kOrderBy);
177inline constexpr bool kHasOrderBy = meta::IsDetected<HasOrderBy, T>;
181using HasUpdatedField =
decltype(T::kUpdatedField);
183inline constexpr bool kHasUpdatedField = meta::IsDetected<HasUpdatedField, T>;
186using WantIncrementalUpdates = std::enable_if_t<!std::string_view{T::kUpdatedField}.empty()>;
188inline constexpr bool kWantIncrementalUpdates = meta::IsDetected<WantIncrementalUpdates, T>;
192using KeyMemberTypeImpl = std::decay_t<std::invoke_result_t<
decltype(T::kKeyMember), ValueType<T>>>;
194inline constexpr bool kHasKeyMember = meta::IsDetected<KeyMemberTypeImpl, T>;
196using KeyMemberType = meta::DetectedType<KeyMemberTypeImpl, T>;
200using SizeMethodInvokeResultImpl =
decltype(std::declval<T>().size());
202inline constexpr bool kHasSizeMethod =
203 meta::IsDetected<SizeMethodInvokeResultImpl, T> &&
204 std::is_convertible_v<SizeMethodInvokeResultImpl<T>, std::size_t>;
208using InsertOrAssignMethodInvokeResultImpl =
209 decltype(std::declval<
typename T::CacheContainer>()
210 .insert_or_assign(std::declval<KeyMemberTypeImpl<T>>(), std::declval<ValueType<T>>()));
212inline constexpr bool kHasInsertOrAssignMethod = meta::IsDetected<InsertOrAssignMethodInvokeResultImpl, T>;
216using CacheInsertOrAssignFunctionInvokeResultImpl =
decltype(CacheInsertOrAssign(
217 std::declval<
typename T::CacheContainer&>(),
218 std::declval<ValueType<T>>(),
219 std::declval<KeyMemberTypeImpl<T>>()
223 kHasCacheInsertOrAssignFunction = meta::IsDetected<CacheInsertOrAssignFunctionInvokeResultImpl, T>;
226template <
typename T,
typename = USERVER_NAMESPACE::utils::void_t<>>
227struct DataCacheContainer {
229 meta::kIsStdHashable<KeyMemberType<T>>,
230 "With default CacheContainer, key type must be std::hash-able"
233 using type = std::unordered_map<KeyMemberType<T>, ValueType<T>>;
237struct DataCacheContainer<T, USERVER_NAMESPACE::utils::void_t<
typename T::CacheContainer>> {
238 static_assert(kHasSizeMethod<
typename T::CacheContainer>,
"Custom CacheContainer must provide `size` method");
240 kHasInsertOrAssignMethod<T> || kHasCacheInsertOrAssignFunction<T>,
241 "Custom CacheContainer must provide `insert_or_assign` method similar to std::unordered_map's "
242 "one or CacheInsertOrAssign function"
245 using type =
typename T::CacheContainer;
249using DataCacheContainerType =
typename DataCacheContainer<T>::type;
254inline constexpr bool kIsContainerCopiedByElement =
255 meta::kIsInstantiationOf<std::unordered_map, T> || meta::kIsInstantiationOf<std::map, T>;
258std::unique_ptr<T> CopyContainer(
260 [[maybe_unused]] std::size_t cpu_relax_iterations,
261 tracing::ScopeTime& scope
263 if constexpr (kIsContainerCopiedByElement<T>) {
264 auto copy = std::make_unique<T>();
265 if constexpr (meta::kIsReservable<T>) {
266 copy->reserve(container.size());
269 utils::
CpuRelax relax
{cpu_relax_iterations
, &scope
};
270 for (
const auto& kv : container) {
276 return std::make_unique<T>(container);
280template <
typename Container,
typename Value,
typename KeyMember,
typename... Args>
281void CacheInsertOrAssign(Container& container, Value&& value,
const KeyMember& key_member, Args&&... ) {
283 static_assert(
sizeof...(Args) == 0);
285 auto key = std::invoke(key_member, value);
286 container.insert_or_assign(std::move(key), std::forward<Value>(value));
290using HasOnWritesDoneImpl =
decltype(std::declval<T&>().OnWritesDone());
293void OnWritesDone(T& container) {
294 if constexpr (meta::IsDetected<HasOnWritesDoneImpl, T>) {
295 container.OnWritesDone();
300using HasCustomUpdatedImpl =
decltype(T::GetLastKnownUpdated(std::declval<DataCacheContainerType<T>>()));
303inline constexpr bool kHasCustomUpdated = meta::IsDetected<HasCustomUpdatedImpl, T>;
306using UpdatedFieldTypeImpl =
typename T::UpdatedFieldType;
308inline constexpr bool kHasUpdatedFieldType = meta::IsDetected<UpdatedFieldTypeImpl, T>;
310using UpdatedFieldType = meta::DetectedOr<storages::postgres::TimePointTz, UpdatedFieldTypeImpl, T>;
313constexpr bool CheckUpdatedFieldType() {
314 if constexpr (kHasUpdatedFieldType<T>) {
315#if USERVER_POSTGRES_ENABLE_LEGACY_TIMESTAMP
317 std::is_same_v<
typename T::UpdatedFieldType, storages::postgres::TimePointTz> ||
318 std::is_same_v<
typename T::UpdatedFieldType, storages::postgres::TimePointWithoutTz> ||
319 std::is_same_v<
typename T::UpdatedFieldType, storages::postgres::TimePoint> || kHasCustomUpdated<T>,
320 "Invalid UpdatedFieldType, must be either TimePointTz or "
322 "or (legacy) system_clock::time_point"
326 std::is_same_v<
typename T::UpdatedFieldType, storages::postgres::TimePointTz> ||
327 std::is_same_v<
typename T::UpdatedFieldType, storages::postgres::TimePointWithoutTz> ||
328 kHasCustomUpdated<T>,
329 "Invalid UpdatedFieldType, must be either TimePointTz or "
335 !kWantIncrementalUpdates<T>,
336 "UpdatedFieldType must be explicitly specified when using "
337 "incremental updates"
345using HasClusterHostTypeImpl =
decltype(T::kClusterHostType);
348constexpr storages::postgres::ClusterHostTypeFlags ClusterHostType() {
349 if constexpr (meta::IsDetected<HasClusterHostTypeImpl, T>) {
350 return T::kClusterHostType;
352 return storages::postgres::ClusterHostType::kSlave;
358using HasMayReturnNull =
decltype(T::kMayReturnNull);
361constexpr bool MayReturnNull() {
362 if constexpr (meta::IsDetected<HasMayReturnNull, T>) {
363 return T::kMayReturnNull;
369template <
typename PostgreCachePolicy>
370struct PolicyChecker {
372 static_assert(kHasName<PostgreCachePolicy>,
"The PosgreSQL cache policy must contain a static member `kName`");
373 static_assert(kHasValueType<PostgreCachePolicy>,
"The PosgreSQL cache policy must define a type alias `ValueType`");
375 kHasKeyMember<PostgreCachePolicy>,
376 "The PostgreSQL cache policy must contain a static member `kKeyMember` "
377 "with a pointer to a data or a function member with the object's key"
380 kHasQuery<PostgreCachePolicy> || kHasGetQuery<PostgreCachePolicy>,
381 "The PosgreSQL cache policy must contain a static data member "
382 "`kQuery` with a select statement or a static member function "
383 "`GetQuery` returning the query"
386 !(kHasQuery<PostgreCachePolicy> && kHasGetQuery<PostgreCachePolicy>),
387 "The PosgreSQL cache policy must define `kQuery` or "
388 "`GetQuery`, not both"
391 kHasUpdatedField<PostgreCachePolicy>,
392 "The PosgreSQL cache policy must contain a static member "
393 "`kUpdatedField`. If you don't want to use incremental updates, "
394 "please set its value to `nullptr`"
396 static_assert(CheckUpdatedFieldType<PostgreCachePolicy>());
399 ClusterHostType<PostgreCachePolicy>() & storages::postgres::kClusterHostRolesMask,
400 "Cluster host role must be specified for caching component, "
401 "please be more specific"
404 static storages::postgres::Query GetQuery() {
405 if constexpr (kHasGetQuery<PostgreCachePolicy>) {
406 return PostgreCachePolicy::GetQuery();
408 return PostgreCachePolicy::kQuery;
415inline constexpr std::chrono::minutes kDefaultFullUpdateTimeout{1};
416inline constexpr std::chrono::seconds kDefaultIncrementalUpdateTimeout{1};
417inline constexpr std::chrono::milliseconds kStatementTimeoutOff{0};
418inline constexpr std::chrono::milliseconds kCpuRelaxThreshold{10};
419inline constexpr std::chrono::milliseconds kCpuRelaxInterval{2};
421inline constexpr std::string_view kCopyStage =
"copy_data";
422inline constexpr std::string_view kFetchStage =
"fetch";
423inline constexpr std::string_view kParseStage =
"parse";
425inline constexpr std::size_t kDefaultChunkSize = 1000;
426inline constexpr std::chrono::milliseconds kDefaultSleepBetweenChunks{0};
434template <
typename PostgreCachePolicy>
435class PostgreCache
final :
public pg_cache::detail::PolicyChecker<PostgreCachePolicy>::BaseType {
438 using PolicyType = PostgreCachePolicy;
439 using ValueType = pg_cache::detail::ValueType<PolicyType>;
440 using RawValueType = pg_cache::detail::RawValueType<PolicyType>;
441 using DataType = pg_cache::detail::DataCacheContainerType<PolicyType>;
442 using PolicyCheckerType = pg_cache::detail::PolicyChecker<PostgreCachePolicy>;
443 using UpdatedFieldType = pg_cache::detail::UpdatedFieldType<PostgreCachePolicy>;
444 using BaseType =
typename PolicyCheckerType::BaseType;
447 constexpr static bool kIncrementalUpdates = pg_cache::detail::kWantIncrementalUpdates<PolicyType>;
448 constexpr static auto kClusterHostTypeFlags = pg_cache::detail::ClusterHostType<PolicyType>();
449 constexpr static auto kName = PolicyType::kName;
451 PostgreCache(
const ComponentConfig&,
const ComponentContext&);
453 static yaml_config::Schema GetStaticConfigSchema();
456 using CachedData = std::unique_ptr<DataType>;
458 UpdatedFieldType GetLastUpdated(std::chrono::system_clock::time_point last_update,
const DataType& cache)
const;
461 cache::UpdateType type,
462 const std::chrono::system_clock::time_point& last_update,
463 const std::chrono::system_clock::time_point& now,
464 cache::UpdateStatisticsScope& stats_scope
467 bool MayReturnNull()
const override;
469 CachedData GetDataSnapshot(cache::UpdateType type, tracing::ScopeTime& scope);
471 storages::postgres::ResultSet res,
472 CachedData& data_cache,
473 cache::UpdateStatisticsScope& stats_scope,
474 tracing::ScopeTime& scope
477 static storages::postgres::Query GetAllQuery();
478 static storages::postgres::Query GetDeltaQuery();
479 static std::string GetWhereClause();
480 static std::string GetDeltaWhereClause();
481 static std::string GetOrderByClause();
483 std::chrono::milliseconds ParseCorrection(
const ComponentConfig& config);
485 std::vector<storages::postgres::ClusterPtr> clusters_;
487 const std::chrono::system_clock::duration correction_;
488 const std::chrono::milliseconds full_update_timeout_;
489 const std::chrono::milliseconds incremental_update_timeout_;
490 const std::size_t chunk_size_;
491 const std::chrono::milliseconds sleep_between_chunks_;
492 std::size_t cpu_relax_iterations_parse_{0};
493 std::size_t cpu_relax_iterations_copy_{0};
496template <
typename PostgreCachePolicy>
497inline constexpr bool kHasValidate<PostgreCache<PostgreCachePolicy>> =
true;
499template <
typename PostgreCachePolicy>
500PostgreCache<PostgreCachePolicy>::PostgreCache(
const ComponentConfig& config,
const ComponentContext& context)
501 : BaseType{config, context},
502 correction_{ParseCorrection(config)},
503 full_update_timeout_{
504 config[
"full-update-op-timeout"].As<std::chrono::milliseconds>(pg_cache::detail::kDefaultFullUpdateTimeout)
506 incremental_update_timeout_{
507 config[
"incremental-update-op-timeout"]
508 .As<std::chrono::milliseconds>(pg_cache::detail::kDefaultIncrementalUpdateTimeout)
510 chunk_size_{config[
"chunk-size"].As<size_t>(pg_cache::detail::kDefaultChunkSize)},
511 sleep_between_chunks_{
512 config[
"sleep-between-chunks"].As<std::chrono::milliseconds>(pg_cache::detail::kDefaultSleepBetweenChunks)
516 !chunk_size_ || storages::postgres::Portal::IsSupportedByDriver(),
517 "Either set 'chunk-size' to 0, or enable PostgreSQL portals by building "
518 "the framework with CMake option USERVER_FEATURE_PATCH_LIBPQ set to ON."
521 if (
this->GetAllowedUpdateTypes() == cache::AllowedUpdateTypes::kFullAndIncremental && !kIncrementalUpdates) {
522 throw std::logic_error(
523 "Incremental update support is requested in config but no update field "
524 "name is specified in traits of '" +
525 config.Name() +
"' cache"
528 if (correction_.count() < 0) {
529 throw std::logic_error(
530 "Refusing to set forward (negative) update correction requested in "
532 config.Name() +
"' cache"
536 const auto pg_alias = config[
"pgcomponent"].As<std::string>(
"");
537 if (pg_alias.empty()) {
538 throw storages::postgres::InvalidConfig{
"No `pgcomponent` entry in configuration"};
540 auto& pg_cluster_comp = context.FindComponent<components::Postgres>(pg_alias);
541 const auto shard_count = pg_cluster_comp.GetShardCount();
542 clusters_.resize(shard_count);
543 for (size_t i = 0; i < shard_count; ++i) {
544 clusters_[i] = pg_cluster_comp.GetClusterForShard(i);
548 <<
"Cache " << kName <<
" full update query `" << GetAllQuery().GetStatementView()
549 <<
"` incremental update query `" << GetDeltaQuery().GetStatementView() <<
"`";
552template <
typename PostgreCachePolicy>
553std::string PostgreCache<PostgreCachePolicy>::GetWhereClause() {
554 if constexpr (pg_cache::detail::kHasWhere<PostgreCachePolicy>) {
555 return fmt::format(FMT_COMPILE(
"where {}"), PostgreCachePolicy::kWhere);
561template <
typename PostgreCachePolicy>
562std::string PostgreCache<PostgreCachePolicy>::GetDeltaWhereClause() {
563 if constexpr (pg_cache::detail::kHasWhere<PostgreCachePolicy>) {
565 FMT_COMPILE(
"where ({}) and {} >= $1"),
566 PostgreCachePolicy::kWhere,
567 PostgreCachePolicy::kUpdatedField
570 return fmt::format(FMT_COMPILE(
"where {} >= $1"), PostgreCachePolicy::kUpdatedField);
574template <
typename PostgreCachePolicy>
575std::string PostgreCache<PostgreCachePolicy>::GetOrderByClause() {
576 if constexpr (pg_cache::detail::kHasOrderBy<PostgreCachePolicy>) {
577 return fmt::format(FMT_COMPILE(
"order by {}"), PostgreCachePolicy::kOrderBy);
583template <
typename PostgreCachePolicy>
584storages::postgres::Query PostgreCache<PostgreCachePolicy>::GetAllQuery() {
585 const storages::postgres::Query query = PolicyCheckerType::GetQuery();
586 return fmt::format(
"{} {} {}", query.GetStatementView(), GetWhereClause(), GetOrderByClause());
589template <
typename PostgreCachePolicy>
590storages::postgres::Query PostgreCache<PostgreCachePolicy>::GetDeltaQuery() {
591 if constexpr (kIncrementalUpdates) {
592 const storages::postgres::Query query = PolicyCheckerType::GetQuery();
593 return storages::postgres::Query{
594 fmt::format(
"{} {} {}", query.GetStatementView(), GetDeltaWhereClause(), GetOrderByClause()),
595 query.GetOptionalName(),
598 return GetAllQuery();
602template <
typename PostgreCachePolicy>
603std::chrono::milliseconds PostgreCache<PostgreCachePolicy>::ParseCorrection(
const ComponentConfig& config) {
604 static constexpr std::string_view kUpdateCorrection =
"update-correction";
605 if (pg_cache::detail::kHasCustomUpdated<PostgreCachePolicy> ||
606 this->GetAllowedUpdateTypes() == cache::AllowedUpdateTypes::kOnlyFull)
608 return config[kUpdateCorrection].As<std::chrono::milliseconds>(0);
610 return config[kUpdateCorrection].As<std::chrono::milliseconds>();
614template <
typename PostgreCachePolicy>
615typename PostgreCache<PostgreCachePolicy>::UpdatedFieldType PostgreCache<PostgreCachePolicy>::GetLastUpdated(
616 [[maybe_unused]] std::chrono::system_clock::time_point last_update,
617 const DataType& cache
619 if constexpr (pg_cache::detail::kHasCustomUpdated<PostgreCachePolicy>) {
620 return PostgreCachePolicy::GetLastKnownUpdated(cache);
622 return UpdatedFieldType{last_update - correction_};
626template <
typename PostgreCachePolicy>
627void PostgreCache<PostgreCachePolicy>::Update(
628 cache::UpdateType type,
629 const std::chrono::system_clock::time_point& last_update,
630 const std::chrono::system_clock::time_point& ,
631 cache::UpdateStatisticsScope& stats_scope
633 namespace pg = storages::postgres;
634 if constexpr (!kIncrementalUpdates) {
635 type = cache::UpdateType::kFull;
637 const auto query = (type == cache::UpdateType::kFull) ? GetAllQuery() : GetDeltaQuery();
638 const std::chrono::milliseconds
639 timeout = (type == cache::UpdateType::kFull) ? full_update_timeout_ : incremental_update_timeout_;
642 auto scope = tracing::Span::CurrentSpan().CreateScopeTime(std::string{pg_cache::detail::kCopyStage});
643 auto data_cache = GetDataSnapshot(type, scope);
644 [[maybe_unused]]
const auto old_size = data_cache->size();
646 scope.Reset(std::string{pg_cache::detail::kFetchStage});
650 for (
auto& cluster : clusters_) {
651 if (chunk_size_ > 0) {
652 auto trx = cluster->Begin(
653 kClusterHostTypeFlags,
655 pg::CommandControl{timeout, pg_cache::detail::kStatementTimeoutOff}
657 auto portal = trx.MakePortal(query, GetLastUpdated(last_update, *data_cache));
659 scope.Reset(std::string{pg_cache::detail::kFetchStage});
660 auto res = portal.Fetch(chunk_size_);
663 scope.Reset(std::string{pg_cache::detail::kParseStage});
664 CacheResults(res, data_cache, stats_scope, scope);
665 changes += res.Size();
666 if (sleep_between_chunks_.count() > 0) {
672 const bool has_parameter = query.GetStatementView().find(
'$') != std::string::npos;
676 kClusterHostTypeFlags,
677 pg::CommandControl{timeout, pg_cache::detail::kStatementTimeoutOff},
679 GetLastUpdated(last_update, *data_cache)
682 kClusterHostTypeFlags,
683 pg::CommandControl{timeout, pg_cache::detail::kStatementTimeoutOff},
688 scope.Reset(std::string{pg_cache::detail::kParseStage});
689 CacheResults(res, data_cache, stats_scope, scope);
690 changes += res.Size();
696 if constexpr (pg_cache::detail::kIsContainerCopiedByElement<DataType>) {
698 const auto elapsed_copy = scope.ElapsedTotal(std::string{pg_cache::detail::kCopyStage});
699 if (elapsed_copy > pg_cache::detail::kCpuRelaxThreshold) {
700 cpu_relax_iterations_copy_ =
static_cast<
701 std::size_t>(
static_cast<
double>(old_size) / (elapsed_copy / pg_cache::detail::kCpuRelaxInterval));
703 <<
"Elapsed time for copying " << kName <<
" " << elapsed_copy.count() <<
" for " << changes
704 <<
" data items is over threshold. Will relax CPU every " << cpu_relax_iterations_parse_
711 const auto elapsed_parse = scope.ElapsedTotal(std::string{pg_cache::detail::kParseStage});
712 if (elapsed_parse > pg_cache::detail::kCpuRelaxThreshold) {
713 cpu_relax_iterations_parse_ =
static_cast<
714 std::size_t>(
static_cast<
double>(changes) / (elapsed_parse / pg_cache::detail::kCpuRelaxInterval));
716 <<
"Elapsed time for parsing " << kName <<
" " << elapsed_parse.count() <<
" for " << changes
717 <<
" data items is over threshold. Will relax CPU every " << cpu_relax_iterations_parse_
721 if (changes > 0 || type == cache::UpdateType::kFull) {
723 pg_cache::detail::OnWritesDone(*data_cache);
725 this->Set(std::move(data_cache));
731template <
typename PostgreCachePolicy>
732bool PostgreCache<PostgreCachePolicy>::MayReturnNull()
const {
733 return pg_cache::detail::MayReturnNull<PolicyType>();
736template <
typename PostgreCachePolicy>
737void PostgreCache<PostgreCachePolicy>::CacheResults(
738 storages::postgres::ResultSet res,
739 CachedData& data_cache,
740 cache::UpdateStatisticsScope& stats_scope,
741 tracing::ScopeTime& scope
743 auto values = res.AsSetOf<RawValueType>(storages::postgres::kRowTag);
744 utils::
CpuRelax relax
{cpu_relax_iterations_parse_
, &scope
};
745 for (
auto p = values.begin(); p != values.end(); ++p) {
748 using pg_cache::detail::CacheInsertOrAssign;
751 pg_cache::detail::ExtractValue<PostgreCachePolicy>(*p),
752 PostgreCachePolicy::kKeyMember
754 }
catch (
const std::exception& e) {
757 <<
"Error parsing data row in cache '" << kName <<
"' to '" << compiler::GetTypeName<ValueType>()
758 <<
"': " << e.what();
763template <
typename PostgreCachePolicy>
764typename PostgreCache<PostgreCachePolicy>::CachedData PostgreCache<
765 PostgreCachePolicy>::GetDataSnapshot(cache::UpdateType type, tracing::ScopeTime& scope) {
766 if (type == cache::UpdateType::kIncremental) {
767 auto data =
this->Get();
769 return pg_cache::detail::CopyContainer(*data, cpu_relax_iterations_copy_, scope);
772 return std::make_unique<DataType>();
777std::string GetPostgreCacheSchema();
781template <
typename PostgreCachePolicy>
782yaml_config::Schema PostgreCache<PostgreCachePolicy>::GetStaticConfigSchema() {
783 using ParentType =
typename pg_cache::detail::PolicyChecker<PostgreCachePolicy>::BaseType;
784 return yaml_config::MergeSchemas<ParentType>(impl::GetPostgreCacheSchema());
789namespace utils::impl::projected_set {
791template <
typename Set,
typename Value,
typename KeyMember>
792void CacheInsertOrAssign(Set& set, Value&& value,
const KeyMember& ) {
793 DoInsert(set, std::forward<Value>(value));