Skip to main content

hydracache_db/
policy.rs

1use std::time::Duration;
2
3use hydracache::{CacheKeyBuilder, CacheOptions, RefreshOptions, TagSet};
4
5use crate::CacheEntity;
6
7const SHORT_LIVED_TTL: Duration = Duration::from_secs(30);
8const READ_MOSTLY_TTL: Duration = Duration::from_secs(300);
9const PER_ENTITY_TTL: Duration = Duration::from_secs(300);
10const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(30);
11
12/// Reusable cache metadata for one database query result.
13///
14/// `QueryCachePolicy` contains the database-neutral parts of query result
15/// caching: diagnostic name, logical key, invalidation tags, and optional TTL.
16/// It is intentionally independent of SQLx, Diesel, SeaORM, or any other
17/// database client.
18///
19/// # Example
20///
21/// ```rust
22/// use std::time::Duration;
23///
24/// use hydracache_db::QueryCachePolicy;
25///
26/// let policy = QueryCachePolicy::named("load-user")
27///     .key("user:42")
28///     .tag("user:42")
29///     .ttl(Duration::from_secs(60));
30///
31/// assert_eq!(policy.name(), Some("load-user"));
32/// assert_eq!(policy.key_value(), Some("user:42"));
33/// assert_eq!(policy.tags_value(), &["user:42".to_owned()]);
34/// assert_eq!(policy.ttl_value(), Some(Duration::from_secs(60)));
35/// ```
36///
37/// The [`query_cache_policy!`](crate::query_cache_policy) macro provides a
38/// shorter declarative form when the policy is known at the call site.
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct QueryCachePolicy {
41    name: Option<String>,
42    key: Option<String>,
43    tags: TagSet,
44    ttl: Option<Duration>,
45    refresh: Option<RefreshOptions>,
46    required_dimensions: Vec<String>,
47}
48
49impl QueryCachePolicy {
50    /// Create an empty cache policy.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Create a short-lived policy for values that should smooth brief bursts.
56    ///
57    /// The preset uses a 30 second TTL and leaves key/tags to the caller.
58    ///
59    /// # Example
60    ///
61    /// ```rust
62    /// use std::time::Duration;
63    ///
64    /// use hydracache_db::QueryCachePolicy;
65    ///
66    /// let policy = QueryCachePolicy::short_lived().key("user:42");
67    ///
68    /// assert_eq!(policy.ttl_value(), Some(Duration::from_secs(30)));
69    /// assert_eq!(policy.key_value(), Some("user:42"));
70    /// ```
71    pub fn short_lived() -> Self {
72        Self::new().ttl(SHORT_LIVED_TTL)
73    }
74
75    /// Create a read-mostly policy for values that change rarely.
76    ///
77    /// The preset uses a 5 minute TTL. Pair it with entity or collection tags
78    /// so writes can still invalidate cached results explicitly.
79    pub fn read_mostly() -> Self {
80        Self::new().ttl(READ_MOSTLY_TTL)
81    }
82
83    /// Create a policy intended for one entity-shaped result.
84    ///
85    /// The preset uses a 5 minute TTL and expects the caller to add an entity
86    /// key/tag with [`QueryCachePolicy::for_entity`] or
87    /// [`QueryCachePolicy::for_cache_entity`].
88    pub fn per_entity() -> Self {
89        Self::new().ttl(PER_ENTITY_TTL)
90    }
91
92    /// Create a policy for explicit-invalidation-only values.
93    ///
94    /// No TTL is configured. The value remains cached until the caller
95    /// invalidates a key/tag, removes it, flushes the cache, or the backend
96    /// evicts it due to capacity pressure.
97    pub fn no_ttl_explicit_invalidation() -> Self {
98        Self::new()
99    }
100
101    /// Create a policy for caching negative lookups briefly.
102    ///
103    /// Use this for `Option<T>` or domain-specific "not found" results where
104    /// repeated misses are expensive but long-lived absence would be unsafe.
105    /// The preset uses a 30 second TTL.
106    pub fn negative_cache() -> Self {
107        Self::new().ttl(NEGATIVE_CACHE_TTL)
108    }
109
110    /// Create a cache policy with a diagnostic operation name.
111    pub fn named(name: impl Into<String>) -> Self {
112        Self::new().with_name(name)
113    }
114
115    /// Return the optional diagnostic operation name.
116    pub fn name(&self) -> Option<&str> {
117        self.name.as_deref()
118    }
119
120    /// Return the logical key, if one has been configured.
121    pub fn key_value(&self) -> Option<&str> {
122        self.key.as_deref()
123    }
124
125    /// Return configured invalidation tags.
126    pub fn tags_value(&self) -> &[String] {
127        self.tags.as_slice()
128    }
129
130    /// Return the optional per-entry TTL.
131    pub fn ttl_value(&self) -> Option<Duration> {
132        self.ttl
133    }
134
135    /// Return the optional refresh/stale policy.
136    pub fn refresh_policy_value(&self) -> Option<RefreshOptions> {
137        self.refresh
138    }
139
140    /// Return statically declared key dimensions required by this policy.
141    ///
142    /// These labels are diagnostics only; values are intentionally not stored.
143    pub fn required_dimensions_value(&self) -> &[String] {
144        &self.required_dimensions
145    }
146
147    /// Set or replace the diagnostic operation name.
148    pub fn with_name(mut self, name: impl Into<String>) -> Self {
149        self.name = Some(name.into());
150        self
151    }
152
153    /// Set the logical cache key.
154    pub fn key(mut self, key: impl Into<String>) -> Self {
155        self.key = Some(key.into());
156        self
157    }
158
159    /// Set the logical cache key from a segmented key builder.
160    pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
161        self.key(key.build_string())
162    }
163
164    /// Set the logical key and add the same entity invalidation tag.
165    pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
166        let key = entity_key(kind, id);
167        self.key = Some(key.clone());
168        self.tags = self.tags.tag(key);
169        self
170    }
171
172    /// Set the logical key and tags from [`CacheEntity`] metadata.
173    pub fn for_cache_entity<T>(mut self, id: T::Id) -> Self
174    where
175        T: CacheEntity,
176    {
177        let key = T::cache_key_for(&id);
178        self.key = Some(key);
179        self.tags = self.tags.tag(T::entity_tag_for(&id));
180        self.tags = append_optional_tag(self.tags, T::collection_tag());
181        self
182    }
183
184    /// Set the logical key and invalidation tag for a collection result.
185    pub fn collection(mut self, name: impl ToString) -> Self {
186        let tag = collection_tag(name);
187        self.key = Some(tag.clone());
188        self.tags = self.tags.tag(tag);
189        self
190    }
191
192    /// Add one invalidation tag.
193    pub fn tag(mut self, tag: impl Into<String>) -> Self {
194        self.tags = self.tags.tag(tag);
195        self
196    }
197
198    /// Add a collection invalidation tag from one escaped key segment.
199    pub fn collection_tag(mut self, name: impl ToString) -> Self {
200        self.tags = self.tags.tag(collection_tag(name));
201        self
202    }
203
204    /// Add several invalidation tags.
205    pub fn tags<I, S>(mut self, tags: I) -> Self
206    where
207        I: IntoIterator<Item = S>,
208        S: Into<String>,
209    {
210        self.tags = self.tags.tags(tags);
211        self
212    }
213
214    /// Replace invalidation tags from a reusable [`TagSet`].
215    pub fn tag_set(mut self, tags: TagSet) -> Self {
216        self.tags = tags;
217        self
218    }
219
220    /// Set a per-entry TTL.
221    pub fn ttl(mut self, ttl: Duration) -> Self {
222        self.ttl = Some(ttl);
223        self
224    }
225
226    /// Set refresh/stale behavior for this query result.
227    pub fn refresh_policy(mut self, refresh: RefreshOptions) -> Self {
228        self.refresh = Some(refresh);
229        self
230    }
231
232    /// Store statically declared key dimensions for diagnostics and review.
233    pub fn required_dimensions<I, S>(mut self, dimensions: I) -> Self
234    where
235        I: IntoIterator<Item = S>,
236        S: Into<String>,
237    {
238        self.required_dimensions = dimensions.into_iter().map(Into::into).collect();
239        self
240    }
241
242    /// Add one statically declared key dimension for diagnostics and review.
243    pub fn required_dimension(mut self, dimension: impl Into<String>) -> Self {
244        self.required_dimensions.push(dimension.into());
245        self
246    }
247
248    pub(crate) fn cache_options(&self) -> CacheOptions {
249        let mut options = CacheOptions::new().tag_set(self.tags.clone());
250        if let Some(ttl) = self.ttl {
251            options = options.ttl(ttl);
252        }
253        options
254    }
255}
256
257pub(crate) fn entity_key(kind: impl ToString, id: impl ToString) -> String {
258    CacheKeyBuilder::new().entity(kind, id).build_string()
259}
260
261pub(crate) fn collection_tag(name: impl ToString) -> String {
262    CacheKeyBuilder::from_segment(name).build_string()
263}
264
265fn append_optional_tag(tags: TagSet, tag: Option<String>) -> TagSet {
266    match tag {
267        Some(tag) => tags.tag(tag),
268        None => tags,
269    }
270}