hydracache_db/query.rs
1use std::error::Error;
2use std::fmt;
3use std::future::Future;
4use std::marker::PhantomData;
5use std::time::Duration;
6
7use hydracache::{CacheKeyBuilder, CacheOptions, HydraCache, PostcardCodec, TagSet};
8use hydracache_core::CacheCodec;
9use serde::{de::DeserializeOwned, Serialize};
10
11use crate::{CacheEntity, DbCacheError, PreparedQueryPolicy, QueryCachePolicy, Result};
12
13/// A database-oriented view over a [`HydraCache`] instance.
14///
15/// `DbCache` groups query result keys under a namespace while keeping all
16/// cache storage, single-flight, tags, TTL, and stats in the shared local cache.
17///
18/// # Example
19///
20/// ```rust
21/// use std::time::Duration;
22///
23/// use hydracache::HydraCache;
24/// use hydracache_db::DbCache;
25/// use serde::{Deserialize, Serialize};
26///
27/// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28/// struct User {
29/// id: i64,
30/// name: String,
31/// }
32///
33/// # #[tokio::main]
34/// # async fn main() -> hydracache_db::Result<()> {
35/// let local = HydraCache::local().build();
36/// let queries = DbCache::new(local, "db");
37///
38/// let user = queries
39/// .entity::<User>("user", 42)
40/// // Later, invalidate_tag("user:42") removes this result.
41/// .collection_tag("users")
42/// .ttl(Duration::from_secs(60))
43/// .fetch_with(|| async {
44/// // Replace this block with code from sqlx, diesel, sea-orm, or any
45/// // other database client. It is called only when the cache does not
46/// // already contain "db:user:42" or when the cached value has expired.
47/// Ok::<_, std::io::Error>(User {
48/// id: 42,
49/// name: "Ada".to_owned(),
50/// })
51/// })
52/// .await?;
53///
54/// assert_eq!(user.id, 42);
55/// # Ok(())
56/// # }
57/// ```
58pub struct DbCache<C = PostcardCodec>
59where
60 C: CacheCodec,
61{
62 cache: HydraCache<C>,
63 namespace: String,
64}
65
66impl<C> Clone for DbCache<C>
67where
68 C: CacheCodec,
69{
70 fn clone(&self) -> Self {
71 Self {
72 cache: self.cache.clone(),
73 namespace: self.namespace.clone(),
74 }
75 }
76}
77
78impl<C> fmt::Debug for DbCache<C>
79where
80 C: CacheCodec,
81{
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter
84 .debug_struct("DbCache")
85 .field("namespace", &self.namespace)
86 .finish_non_exhaustive()
87 }
88}
89
90impl<C> DbCache<C>
91where
92 C: CacheCodec,
93{
94 /// Create a database query cache adapter over an existing local cache.
95 pub fn new(cache: HydraCache<C>, namespace: impl Into<String>) -> Self {
96 Self {
97 cache,
98 namespace: namespace.into(),
99 }
100 }
101
102 /// Return the namespace used for physical cache keys.
103 pub fn namespace(&self) -> &str {
104 &self.namespace
105 }
106
107 /// Return the underlying local cache.
108 pub fn cache(&self) -> &HydraCache<C> {
109 &self.cache
110 }
111
112 /// Start describing a cacheable database-loaded value.
113 ///
114 /// This is the preferred entry point when the query is already visible
115 /// inside the `fetch_with` loader through a database client, ORM, or
116 /// repository method.
117 pub fn cached<T>(&self) -> DbQuery<T, C> {
118 DbQuery {
119 cache: self.cache.clone(),
120 namespace: self.namespace.clone(),
121 policy: QueryCachePolicy::new(),
122 value: PhantomData,
123 }
124 }
125
126 /// Start describing a cacheable database-loaded value with a reusable
127 /// [`QueryCachePolicy`].
128 ///
129 /// This is useful when the same key/tag/TTL pattern is shared by a
130 /// repository method, a SQLx call site, and a future ORM adapter.
131 pub fn cached_with<T>(&self, policy: QueryCachePolicy) -> DbQuery<T, C> {
132 self.cached::<T>().with_policy(policy)
133 }
134
135 /// Prepare a reusable database query descriptor from stable metadata.
136 ///
137 /// Use this when a repository method runs many times and only a small part
138 /// of the cache metadata changes per call, such as the entity id.
139 ///
140 /// # Example
141 ///
142 /// ```rust
143 /// use std::time::Duration;
144 ///
145 /// use hydracache::HydraCache;
146 /// use hydracache_db::{CacheEntity, DbCache, PreparedQueryPolicy};
147 /// use serde::{Deserialize, Serialize};
148 ///
149 /// #[derive(Debug, Clone, Serialize, Deserialize)]
150 /// struct User {
151 /// id: i64,
152 /// }
153 ///
154 /// impl CacheEntity for User {
155 /// type Id = i64;
156 ///
157 /// const ENTITY: &'static str = "user";
158 /// const COLLECTION: Option<&'static str> = Some("users");
159 /// }
160 ///
161 /// let queries = DbCache::new(HydraCache::local().build(), "db");
162 /// let load_user = queries.prepare::<User>(
163 /// PreparedQueryPolicy::for_cache_entity::<User>()
164 /// .with_name("load-user")
165 /// .ttl(Duration::from_secs(60)),
166 /// );
167 ///
168 /// let query = load_user.for_id(42);
169 /// assert_eq!(query.physical_key(), Some("db:user:42".to_owned()));
170 /// assert_eq!(query.tags_value(), &["users".to_owned(), "user:42".to_owned()]);
171 /// ```
172 pub fn prepare<T>(&self, policy: PreparedQueryPolicy) -> PreparedDbQuery<T, C> {
173 PreparedDbQuery {
174 cache: self.cache.clone(),
175 namespace: self.namespace.clone(),
176 policy,
177 value: PhantomData,
178 }
179 }
180
181 /// Prepare a reusable entity-id descriptor from [`CacheEntity`] metadata.
182 pub fn prepare_entity<T>(&self) -> PreparedDbQuery<T, C>
183 where
184 T: CacheEntity,
185 {
186 self.prepare(PreparedQueryPolicy::for_cache_entity::<T>())
187 }
188
189 /// Start describing an entity-shaped cached value.
190 ///
191 /// This is a convenience layer over [`DbCache::cached`] that sets both the
192 /// logical key and the entity invalidation tag from escaped key segments.
193 /// For example, `entity::<User>("user", 42)` creates key `user:42` and tag
194 /// `user:42`; with namespace `db`, the physical cache key is `db:user:42`.
195 ///
196 /// # Example
197 ///
198 /// ```rust
199 /// use hydracache::HydraCache;
200 /// use hydracache_db::DbCache;
201 /// use serde::{Deserialize, Serialize};
202 ///
203 /// #[derive(Debug, Clone, Serialize, Deserialize)]
204 /// struct User {
205 /// id: i64,
206 /// }
207 ///
208 /// let queries = DbCache::new(HydraCache::local().build(), "db");
209 /// let query = queries.entity::<User>("user", 42);
210 ///
211 /// assert_eq!(query.key_value(), Some("user:42"));
212 /// assert_eq!(query.tags_value(), &["user:42".to_owned()]);
213 /// assert_eq!(query.physical_key(), Some("db:user:42".to_owned()));
214 /// ```
215 pub fn entity<T>(&self, kind: impl ToString, id: impl ToString) -> DbQuery<T, C> {
216 self.cached::<T>().for_entity(kind, id)
217 }
218
219 /// Start describing an entity-shaped cached value from [`CacheEntity`]
220 /// metadata.
221 ///
222 /// This helper removes repeated entity and collection literals from call
223 /// sites. It sets the logical key, entity tag, and optional collection tag
224 /// defined by `T`.
225 ///
226 /// # Example
227 ///
228 /// ```rust
229 /// use hydracache::HydraCache;
230 /// use hydracache_db::{CacheEntity, DbCache};
231 /// use serde::{Deserialize, Serialize};
232 ///
233 /// #[derive(Debug, Clone, Serialize, Deserialize)]
234 /// struct User {
235 /// id: i64,
236 /// }
237 ///
238 /// impl CacheEntity for User {
239 /// type Id = i64;
240 ///
241 /// const ENTITY: &'static str = "user";
242 /// const COLLECTION: Option<&'static str> = Some("users");
243 /// }
244 ///
245 /// let queries = DbCache::new(HydraCache::local().build(), "db");
246 /// let query = queries.for_entity::<User>(42);
247 ///
248 /// assert_eq!(query.key_value(), Some("user:42"));
249 /// assert_eq!(
250 /// query.tags_value(),
251 /// &["user:42".to_owned(), "users".to_owned()]
252 /// );
253 /// ```
254 pub fn for_entity<T>(&self, id: T::Id) -> DbQuery<T, C>
255 where
256 T: CacheEntity,
257 {
258 self.cached::<T>().for_cache_entity(id)
259 }
260
261 /// Start describing a collection-shaped cached value.
262 ///
263 /// This sets both the logical key and the collection invalidation tag to
264 /// the escaped collection name. For example, `collection::<User>("users")`
265 /// creates key `users` and tag `users`.
266 ///
267 /// # Example
268 ///
269 /// ```rust
270 /// use hydracache::HydraCache;
271 /// use hydracache_db::DbCache;
272 /// use serde::{Deserialize, Serialize};
273 ///
274 /// #[derive(Debug, Clone, Serialize, Deserialize)]
275 /// struct User {
276 /// id: i64,
277 /// }
278 ///
279 /// let queries = DbCache::new(HydraCache::local().build(), "db");
280 /// let query = queries.collection::<User>("users:active");
281 ///
282 /// assert_eq!(query.key_value(), Some("users%3Aactive"));
283 /// assert_eq!(query.tags_value(), &["users%3Aactive".to_owned()]);
284 /// assert_eq!(query.physical_key(), Some("db:users%3Aactive".to_owned()));
285 /// ```
286 pub fn collection<T>(&self, name: impl ToString) -> DbQuery<T, C> {
287 self.cached::<T>().collection(name)
288 }
289
290 /// Start describing a cacheable database-loaded value with a diagnostic name.
291 pub fn named<T>(&self, name: impl Into<String>) -> DbQuery<T, C> {
292 DbQuery {
293 cache: self.cache.clone(),
294 namespace: self.namespace.clone(),
295 policy: QueryCachePolicy::named(name),
296 value: PhantomData,
297 }
298 }
299
300 /// Start describing a cacheable SQL query result.
301 ///
302 /// Prefer [`DbCache::cached`] or [`DbCache::named`] when writing new code.
303 /// This method remains useful if you want the SQL text itself to be the
304 /// diagnostic label for errors and logs.
305 pub fn query_as<T>(&self, sql: impl Into<String>) -> DbQuery<T, C> {
306 self.named(sql)
307 }
308}
309
310/// A cacheable database query descriptor.
311///
312/// The descriptor is deliberately explicit: callers choose the key, tags, and
313/// TTL that match their freshness model. An operation name is optional and used
314/// only for diagnostics. `fetch_with` executes the supplied loader only on a
315/// cache miss.
316pub struct DbQuery<T, C = PostcardCodec>
317where
318 C: CacheCodec,
319{
320 cache: HydraCache<C>,
321 namespace: String,
322 policy: QueryCachePolicy,
323 value: PhantomData<fn() -> T>,
324}
325
326/// A prepared database query descriptor.
327///
328/// `PreparedDbQuery` keeps adapter state and stable query-cache metadata close
329/// together. It can cheaply create ordinary [`DbQuery`] values for each call,
330/// or execute loaders directly through `load_id`/`fetch_with_id`.
331pub struct PreparedDbQuery<T, C = PostcardCodec>
332where
333 C: CacheCodec,
334{
335 cache: HydraCache<C>,
336 namespace: String,
337 policy: PreparedQueryPolicy,
338 value: PhantomData<fn() -> T>,
339}
340
341impl<T, C> Clone for PreparedDbQuery<T, C>
342where
343 C: CacheCodec,
344{
345 fn clone(&self) -> Self {
346 Self {
347 cache: self.cache.clone(),
348 namespace: self.namespace.clone(),
349 policy: self.policy.clone(),
350 value: PhantomData,
351 }
352 }
353}
354
355impl<T, C> fmt::Debug for PreparedDbQuery<T, C>
356where
357 C: CacheCodec,
358{
359 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
360 formatter
361 .debug_struct("PreparedDbQuery")
362 .field("namespace", &self.namespace)
363 .field("policy", &self.policy)
364 .finish_non_exhaustive()
365 }
366}
367
368impl<T, C> PreparedDbQuery<T, C>
369where
370 C: CacheCodec,
371{
372 /// Return the namespace used for physical cache keys.
373 pub fn namespace(&self) -> &str {
374 &self.namespace
375 }
376
377 /// Return the underlying local cache.
378 pub fn cache(&self) -> &HydraCache<C> {
379 &self.cache
380 }
381
382 /// Return the prepared policy backing this descriptor.
383 pub fn prepared_policy(&self) -> &PreparedQueryPolicy {
384 &self.policy
385 }
386
387 /// Return the optional diagnostic operation name.
388 pub fn name(&self) -> Option<&str> {
389 self.policy.name()
390 }
391
392 /// Return whether this descriptor needs an id binding before execution.
393 pub fn requires_id(&self) -> bool {
394 self.policy.requires_id()
395 }
396
397 /// Return the static logical key, if this descriptor has one.
398 pub fn static_key_value(&self) -> Option<&str> {
399 self.policy.static_key_value()
400 }
401
402 /// Return the precomputed entity key prefix, if this is an entity policy.
403 pub fn entity_key_prefix(&self) -> Option<&str> {
404 self.policy.entity_key_prefix()
405 }
406
407 /// Return the precomputed tags.
408 pub fn tags_value(&self) -> &[String] {
409 self.policy.tags_value()
410 }
411
412 /// Return the configured per-entry TTL.
413 pub fn ttl_value(&self) -> Option<Duration> {
414 self.policy.ttl_value()
415 }
416
417 /// Return the configured refresh/stale policy.
418 pub fn refresh_policy_value(&self) -> Option<hydracache::RefreshOptions> {
419 self.policy.refresh_policy_value()
420 }
421
422 /// Replace refresh/stale behavior for this prepared descriptor.
423 pub fn refresh_policy(mut self, refresh: hydracache::RefreshOptions) -> Self {
424 self.policy = self.policy.refresh_policy(refresh);
425 self
426 }
427
428 /// Create a runtime query from a static prepared policy.
429 ///
430 /// Entity-id policies should usually use [`PreparedDbQuery::for_id`] so the
431 /// dynamic id becomes part of the key and entity invalidation tag.
432 pub fn to_query(&self) -> DbQuery<T, C> {
433 self.query_from_policy(self.policy.to_policy())
434 }
435
436 /// Bind an id and create a runtime query.
437 pub fn for_id(&self, id: impl ToString) -> DbQuery<T, C> {
438 self.query_from_policy(self.policy.bind_id(id))
439 }
440
441 /// Fetch a cached value from a static prepared policy or run the loader.
442 pub async fn load<E, F, Fut>(&self, loader: F) -> Result<T>
443 where
444 T: Serialize + DeserializeOwned + Send + 'static,
445 E: Error + Send + Sync + 'static,
446 F: FnOnce() -> Fut + Send + 'static,
447 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
448 {
449 self.to_query().load(loader).await
450 }
451
452 /// Bind an id, fetch a cached value, or run the loader.
453 pub async fn load_id<E, F, Fut>(&self, id: impl ToString, loader: F) -> Result<T>
454 where
455 T: Serialize + DeserializeOwned + Send + 'static,
456 E: Error + Send + Sync + 'static,
457 F: FnOnce() -> Fut + Send + 'static,
458 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
459 {
460 self.for_id(id).load(loader).await
461 }
462
463 /// Fetch a static prepared value with an output type chosen by an adapter.
464 pub async fn fetch_value_with<U, E, F, Fut>(&self, loader: F) -> Result<U>
465 where
466 U: Serialize + DeserializeOwned + Send + 'static,
467 E: Error + Send + Sync + 'static,
468 F: FnOnce() -> Fut + Send + 'static,
469 Fut: Future<Output = std::result::Result<U, E>> + Send + 'static,
470 {
471 self.to_query().fetch_value_with(loader).await
472 }
473
474 /// Bind an id and fetch a value with an output type chosen by an adapter.
475 pub async fn fetch_value_with_id<U, E, F, Fut>(&self, id: impl ToString, loader: F) -> Result<U>
476 where
477 U: Serialize + DeserializeOwned + Send + 'static,
478 E: Error + Send + Sync + 'static,
479 F: FnOnce() -> Fut + Send + 'static,
480 Fut: Future<Output = std::result::Result<U, E>> + Send + 'static,
481 {
482 self.for_id(id).fetch_value_with(loader).await
483 }
484
485 fn query_from_policy(&self, policy: QueryCachePolicy) -> DbQuery<T, C> {
486 DbQuery {
487 cache: self.cache.clone(),
488 namespace: self.namespace.clone(),
489 policy,
490 value: PhantomData,
491 }
492 }
493}
494
495impl<T, C> Clone for DbQuery<T, C>
496where
497 C: CacheCodec,
498{
499 fn clone(&self) -> Self {
500 Self {
501 cache: self.cache.clone(),
502 namespace: self.namespace.clone(),
503 policy: self.policy.clone(),
504 value: PhantomData,
505 }
506 }
507}
508
509impl<T, C> fmt::Debug for DbQuery<T, C>
510where
511 C: CacheCodec,
512{
513 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
514 formatter
515 .debug_struct("DbQuery")
516 .field("namespace", &self.namespace)
517 .field("policy", &self.policy)
518 .finish_non_exhaustive()
519 }
520}
521
522impl<T, C> DbQuery<T, C>
523where
524 C: CacheCodec,
525{
526 /// Return the optional diagnostic operation name.
527 pub fn name(&self) -> Option<&str> {
528 self.policy.name()
529 }
530
531 /// Set or replace the diagnostic operation name.
532 pub fn with_name(mut self, name: impl Into<String>) -> Self {
533 self.policy = self.policy.with_name(name);
534 self
535 }
536
537 /// Return the reusable cache policy backing this descriptor.
538 pub fn cache_policy(&self) -> &QueryCachePolicy {
539 &self.policy
540 }
541
542 /// Replace the current cache policy.
543 ///
544 /// This is the lowest-friction way to reuse one policy across SQLx,
545 /// Diesel, SeaORM, or repository-style call sites while keeping the loader
546 /// itself fully caller-controlled.
547 pub fn with_policy(mut self, policy: QueryCachePolicy) -> Self {
548 self.policy = policy;
549 self
550 }
551
552 /// Return the namespace used for physical cache keys.
553 pub fn namespace(&self) -> &str {
554 &self.namespace
555 }
556
557 /// Return the logical key, if one has been configured.
558 pub fn key_value(&self) -> Option<&str> {
559 self.policy.key_value()
560 }
561
562 /// Return the physical cache key, including the adapter namespace.
563 pub fn physical_key(&self) -> Option<String> {
564 let key = self.key_value()?;
565 Some(physical_key(&self.namespace, key))
566 }
567
568 /// Return the configured tags.
569 pub fn tags_value(&self) -> &[String] {
570 self.policy.tags_value()
571 }
572
573 /// Return the configured per-entry TTL.
574 pub fn ttl_value(&self) -> Option<Duration> {
575 self.policy.ttl_value()
576 }
577
578 /// Return the configured refresh/stale policy.
579 pub fn refresh_policy_value(&self) -> Option<hydracache::RefreshOptions> {
580 self.policy.refresh_policy_value()
581 }
582
583 /// Set the logical cache key for this query result.
584 pub fn key(mut self, key: impl Into<String>) -> Self {
585 self.policy = self.policy.key(key);
586 self
587 }
588
589 /// Set the logical cache key from a segmented key builder.
590 pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
591 self.key(key.build_string())
592 }
593
594 /// Set the logical key and add an entity invalidation tag.
595 ///
596 /// `for_entity("user", 42)` sets the key to `user:42` and adds the tag
597 /// `user:42`. Both segments are escaped with [`CacheKeyBuilder`], so `:` and
598 /// `%` inside one segment cannot accidentally create extra key segments.
599 ///
600 /// # Example
601 ///
602 /// ```rust
603 /// use hydracache::HydraCache;
604 /// use hydracache_db::DbCache;
605 /// use serde::{Deserialize, Serialize};
606 ///
607 /// #[derive(Debug, Clone, Serialize, Deserialize)]
608 /// struct User {
609 /// id: i64,
610 /// }
611 ///
612 /// let queries = DbCache::new(HydraCache::local().build(), "db");
613 /// let query = queries
614 /// .cached::<User>()
615 /// .tag("users")
616 /// .for_entity("user", 42);
617 ///
618 /// assert_eq!(query.key_value(), Some("user:42"));
619 /// assert_eq!(
620 /// query.tags_value(),
621 /// &["users".to_owned(), "user:42".to_owned()]
622 /// );
623 /// ```
624 pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
625 self.policy = self.policy.for_entity(kind, id);
626 self
627 }
628
629 /// Set the logical key and tags from [`CacheEntity`] metadata.
630 ///
631 /// This is the metadata-driven equivalent of [`DbQuery::for_entity`]. It
632 /// preserves any existing tags, then adds the entity tag and optional
633 /// collection tag defined by `T`.
634 ///
635 /// # Example
636 ///
637 /// ```rust
638 /// use hydracache::HydraCache;
639 /// use hydracache_db::{CacheEntity, DbCache};
640 /// use serde::{Deserialize, Serialize};
641 ///
642 /// #[derive(Debug, Clone, Serialize, Deserialize)]
643 /// struct User {
644 /// id: i64,
645 /// }
646 ///
647 /// impl CacheEntity for User {
648 /// type Id = i64;
649 ///
650 /// const ENTITY: &'static str = "user";
651 /// const COLLECTION: Option<&'static str> = Some("users");
652 /// }
653 ///
654 /// let queries = DbCache::new(HydraCache::local().build(), "db");
655 /// let query = queries
656 /// .cached::<User>()
657 /// .tag("tenant:7")
658 /// .for_cache_entity(42);
659 ///
660 /// assert_eq!(query.key_value(), Some("user:42"));
661 /// assert_eq!(
662 /// query.tags_value(),
663 /// &[
664 /// "tenant:7".to_owned(),
665 /// "user:42".to_owned(),
666 /// "users".to_owned()
667 /// ]
668 /// );
669 /// ```
670 pub fn for_cache_entity(mut self, id: T::Id) -> Self
671 where
672 T: CacheEntity,
673 {
674 self.policy = self.policy.for_cache_entity::<T>(id);
675 self
676 }
677
678 /// Set the logical key and invalidation tag for a collection result.
679 pub fn collection(mut self, name: impl ToString) -> Self {
680 self.policy = self.policy.collection(name);
681 self
682 }
683
684 /// Add one invalidation tag.
685 pub fn tag(mut self, tag: impl Into<String>) -> Self {
686 self.policy = self.policy.tag(tag);
687 self
688 }
689
690 /// Add a collection invalidation tag from one escaped key segment.
691 ///
692 /// Use this with [`DbCache::entity`] or [`DbQuery::for_entity`] when one
693 /// entity result also belongs to a broader list or query group.
694 ///
695 /// # Example
696 ///
697 /// ```rust
698 /// use hydracache::HydraCache;
699 /// use hydracache_db::DbCache;
700 /// use serde::{Deserialize, Serialize};
701 ///
702 /// #[derive(Debug, Clone, Serialize, Deserialize)]
703 /// struct User {
704 /// id: i64,
705 /// }
706 ///
707 /// let queries = DbCache::new(HydraCache::local().build(), "db");
708 /// let query = queries
709 /// .entity::<User>("user", 42)
710 /// .collection_tag("users:active");
711 ///
712 /// assert_eq!(
713 /// query.tags_value(),
714 /// &["user:42".to_owned(), "users%3Aactive".to_owned()]
715 /// );
716 /// ```
717 pub fn collection_tag(mut self, name: impl ToString) -> Self {
718 self.policy = self.policy.collection_tag(name);
719 self
720 }
721
722 /// Add several invalidation tags.
723 pub fn tags<I, S>(mut self, tags: I) -> Self
724 where
725 I: IntoIterator<Item = S>,
726 S: Into<String>,
727 {
728 self.policy = self.policy.tags(tags);
729 self
730 }
731
732 /// Replace invalidation tags from a reusable [`TagSet`].
733 pub fn tag_set(mut self, tags: TagSet) -> Self {
734 self.policy = self.policy.tag_set(tags);
735 self
736 }
737
738 /// Set a per-entry TTL for this query result.
739 pub fn ttl(mut self, ttl: Duration) -> Self {
740 self.policy = self.policy.ttl(ttl);
741 self
742 }
743
744 /// Set refresh/stale behavior for this query result.
745 pub fn refresh_policy(mut self, refresh: hydracache::RefreshOptions) -> Self {
746 self.policy = self.policy.refresh_policy(refresh);
747 self
748 }
749
750 /// Fetch a cached value or run the supplied repository/database loader on
751 /// miss.
752 ///
753 /// This is a short alias for [`DbQuery::fetch_with`]. It reads more
754 /// naturally when a call site is wrapping a repository method rather than a
755 /// raw SQL query.
756 pub async fn load<E, F, Fut>(self, loader: F) -> Result<T>
757 where
758 T: Serialize + DeserializeOwned + Send + 'static,
759 E: Error + Send + Sync + 'static,
760 F: FnOnce() -> Fut + Send + 'static,
761 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
762 {
763 self.fetch_with(loader).await
764 }
765
766 /// Fetch a cached value or run the supplied database loader on miss.
767 ///
768 /// The loader is intentionally caller-supplied so the database library
769 /// remains responsible for pools, transactions, compile-time checked
770 /// queries, and row mapping. HydraCache owns only the cache boundary.
771 pub async fn fetch_with<E, F, Fut>(self, loader: F) -> Result<T>
772 where
773 T: Serialize + DeserializeOwned + Send + 'static,
774 E: Error + Send + Sync + 'static,
775 F: FnOnce() -> Fut + Send + 'static,
776 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
777 {
778 self.fetch_value_with(loader).await
779 }
780
781 /// Fetch a cached value with an output type chosen by an adapter.
782 ///
783 /// Most application code should use [`DbQuery::fetch_with`]. This method is
784 /// intended for adapter crates that keep the descriptor type focused on a
785 /// database row while caching shapes such as `Option<T>` or `Vec<T>`.
786 pub async fn fetch_value_with<U, E, F, Fut>(self, loader: F) -> Result<U>
787 where
788 U: Serialize + DeserializeOwned + Send + 'static,
789 E: Error + Send + Sync + 'static,
790 F: FnOnce() -> Fut + Send + 'static,
791 Fut: Future<Output = std::result::Result<U, E>> + Send + 'static,
792 {
793 let key = self.required_physical_key()?;
794 let options = self.options();
795
796 match self.policy.refresh_policy_value() {
797 Some(refresh) => self
798 .cache
799 .get_or_load_with_refresh(&key, options, refresh, loader)
800 .await
801 .map_err(DbCacheError::from),
802 None => self
803 .cache
804 .get_or_load(&key, options, loader)
805 .await
806 .map_err(DbCacheError::from),
807 }
808 }
809
810 fn options(&self) -> CacheOptions {
811 self.policy.cache_options()
812 }
813
814 fn required_physical_key(&self) -> Result<String> {
815 self.physical_key().ok_or_else(|| DbCacheError::MissingKey {
816 operation: self.operation_label(),
817 })
818 }
819
820 fn operation_label(&self) -> String {
821 self.name()
822 .map(str::to_owned)
823 .unwrap_or_else(|| default_operation_label(&self.namespace))
824 }
825}
826
827fn physical_key(namespace: &str, key: &str) -> String {
828 if namespace.is_empty() {
829 key.to_owned()
830 } else {
831 format!("{namespace}:{key}")
832 }
833}
834
835fn default_operation_label(namespace: &str) -> String {
836 if namespace.is_empty() {
837 "unnamed".to_owned()
838 } else {
839 format!("{namespace}:unnamed")
840 }
841}