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 /// Create a runtime query from a static prepared policy.
418 ///
419 /// Entity-id policies should usually use [`PreparedDbQuery::for_id`] so the
420 /// dynamic id becomes part of the key and entity invalidation tag.
421 pub fn to_query(&self) -> DbQuery<T, C> {
422 self.query_from_policy(self.policy.to_policy())
423 }
424
425 /// Bind an id and create a runtime query.
426 pub fn for_id(&self, id: impl ToString) -> DbQuery<T, C> {
427 self.query_from_policy(self.policy.bind_id(id))
428 }
429
430 /// Fetch a cached value from a static prepared policy or run the loader.
431 pub async fn load<E, F, Fut>(&self, loader: F) -> Result<T>
432 where
433 T: Serialize + DeserializeOwned + Send + 'static,
434 E: Error + Send + Sync + 'static,
435 F: FnOnce() -> Fut + Send + 'static,
436 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
437 {
438 self.to_query().load(loader).await
439 }
440
441 /// Bind an id, fetch a cached value, or run the loader.
442 pub async fn load_id<E, F, Fut>(&self, id: impl ToString, 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.for_id(id).load(loader).await
450 }
451
452 /// Fetch a static prepared value with an output type chosen by an adapter.
453 pub async fn fetch_value_with<U, E, F, Fut>(&self, loader: F) -> Result<U>
454 where
455 U: Serialize + DeserializeOwned + Send + 'static,
456 E: Error + Send + Sync + 'static,
457 F: FnOnce() -> Fut + Send + 'static,
458 Fut: Future<Output = std::result::Result<U, E>> + Send + 'static,
459 {
460 self.to_query().fetch_value_with(loader).await
461 }
462
463 /// Bind an id and fetch a value with an output type chosen by an adapter.
464 pub async fn fetch_value_with_id<U, E, F, Fut>(&self, id: impl ToString, 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.for_id(id).fetch_value_with(loader).await
472 }
473
474 fn query_from_policy(&self, policy: QueryCachePolicy) -> DbQuery<T, C> {
475 DbQuery {
476 cache: self.cache.clone(),
477 namespace: self.namespace.clone(),
478 policy,
479 value: PhantomData,
480 }
481 }
482}
483
484impl<T, C> Clone for DbQuery<T, C>
485where
486 C: CacheCodec,
487{
488 fn clone(&self) -> Self {
489 Self {
490 cache: self.cache.clone(),
491 namespace: self.namespace.clone(),
492 policy: self.policy.clone(),
493 value: PhantomData,
494 }
495 }
496}
497
498impl<T, C> fmt::Debug for DbQuery<T, C>
499where
500 C: CacheCodec,
501{
502 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
503 formatter
504 .debug_struct("DbQuery")
505 .field("namespace", &self.namespace)
506 .field("policy", &self.policy)
507 .finish_non_exhaustive()
508 }
509}
510
511impl<T, C> DbQuery<T, C>
512where
513 C: CacheCodec,
514{
515 /// Return the optional diagnostic operation name.
516 pub fn name(&self) -> Option<&str> {
517 self.policy.name()
518 }
519
520 /// Set or replace the diagnostic operation name.
521 pub fn with_name(mut self, name: impl Into<String>) -> Self {
522 self.policy = self.policy.with_name(name);
523 self
524 }
525
526 /// Return the reusable cache policy backing this descriptor.
527 pub fn cache_policy(&self) -> &QueryCachePolicy {
528 &self.policy
529 }
530
531 /// Replace the current cache policy.
532 ///
533 /// This is the lowest-friction way to reuse one policy across SQLx,
534 /// Diesel, SeaORM, or repository-style call sites while keeping the loader
535 /// itself fully caller-controlled.
536 pub fn with_policy(mut self, policy: QueryCachePolicy) -> Self {
537 self.policy = policy;
538 self
539 }
540
541 /// Return the namespace used for physical cache keys.
542 pub fn namespace(&self) -> &str {
543 &self.namespace
544 }
545
546 /// Return the logical key, if one has been configured.
547 pub fn key_value(&self) -> Option<&str> {
548 self.policy.key_value()
549 }
550
551 /// Return the physical cache key, including the adapter namespace.
552 pub fn physical_key(&self) -> Option<String> {
553 let key = self.key_value()?;
554 Some(physical_key(&self.namespace, key))
555 }
556
557 /// Return the configured tags.
558 pub fn tags_value(&self) -> &[String] {
559 self.policy.tags_value()
560 }
561
562 /// Return the configured per-entry TTL.
563 pub fn ttl_value(&self) -> Option<Duration> {
564 self.policy.ttl_value()
565 }
566
567 /// Set the logical cache key for this query result.
568 pub fn key(mut self, key: impl Into<String>) -> Self {
569 self.policy = self.policy.key(key);
570 self
571 }
572
573 /// Set the logical cache key from a segmented key builder.
574 pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
575 self.key(key.build_string())
576 }
577
578 /// Set the logical key and add an entity invalidation tag.
579 ///
580 /// `for_entity("user", 42)` sets the key to `user:42` and adds the tag
581 /// `user:42`. Both segments are escaped with [`CacheKeyBuilder`], so `:` and
582 /// `%` inside one segment cannot accidentally create extra key segments.
583 ///
584 /// # Example
585 ///
586 /// ```rust
587 /// use hydracache::HydraCache;
588 /// use hydracache_db::DbCache;
589 /// use serde::{Deserialize, Serialize};
590 ///
591 /// #[derive(Debug, Clone, Serialize, Deserialize)]
592 /// struct User {
593 /// id: i64,
594 /// }
595 ///
596 /// let queries = DbCache::new(HydraCache::local().build(), "db");
597 /// let query = queries
598 /// .cached::<User>()
599 /// .tag("users")
600 /// .for_entity("user", 42);
601 ///
602 /// assert_eq!(query.key_value(), Some("user:42"));
603 /// assert_eq!(
604 /// query.tags_value(),
605 /// &["users".to_owned(), "user:42".to_owned()]
606 /// );
607 /// ```
608 pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
609 self.policy = self.policy.for_entity(kind, id);
610 self
611 }
612
613 /// Set the logical key and tags from [`CacheEntity`] metadata.
614 ///
615 /// This is the metadata-driven equivalent of [`DbQuery::for_entity`]. It
616 /// preserves any existing tags, then adds the entity tag and optional
617 /// collection tag defined by `T`.
618 ///
619 /// # Example
620 ///
621 /// ```rust
622 /// use hydracache::HydraCache;
623 /// use hydracache_db::{CacheEntity, DbCache};
624 /// use serde::{Deserialize, Serialize};
625 ///
626 /// #[derive(Debug, Clone, Serialize, Deserialize)]
627 /// struct User {
628 /// id: i64,
629 /// }
630 ///
631 /// impl CacheEntity for User {
632 /// type Id = i64;
633 ///
634 /// const ENTITY: &'static str = "user";
635 /// const COLLECTION: Option<&'static str> = Some("users");
636 /// }
637 ///
638 /// let queries = DbCache::new(HydraCache::local().build(), "db");
639 /// let query = queries
640 /// .cached::<User>()
641 /// .tag("tenant:7")
642 /// .for_cache_entity(42);
643 ///
644 /// assert_eq!(query.key_value(), Some("user:42"));
645 /// assert_eq!(
646 /// query.tags_value(),
647 /// &[
648 /// "tenant:7".to_owned(),
649 /// "user:42".to_owned(),
650 /// "users".to_owned()
651 /// ]
652 /// );
653 /// ```
654 pub fn for_cache_entity(mut self, id: T::Id) -> Self
655 where
656 T: CacheEntity,
657 {
658 self.policy = self.policy.for_cache_entity::<T>(id);
659 self
660 }
661
662 /// Set the logical key and invalidation tag for a collection result.
663 pub fn collection(mut self, name: impl ToString) -> Self {
664 self.policy = self.policy.collection(name);
665 self
666 }
667
668 /// Add one invalidation tag.
669 pub fn tag(mut self, tag: impl Into<String>) -> Self {
670 self.policy = self.policy.tag(tag);
671 self
672 }
673
674 /// Add a collection invalidation tag from one escaped key segment.
675 ///
676 /// Use this with [`DbCache::entity`] or [`DbQuery::for_entity`] when one
677 /// entity result also belongs to a broader list or query group.
678 ///
679 /// # Example
680 ///
681 /// ```rust
682 /// use hydracache::HydraCache;
683 /// use hydracache_db::DbCache;
684 /// use serde::{Deserialize, Serialize};
685 ///
686 /// #[derive(Debug, Clone, Serialize, Deserialize)]
687 /// struct User {
688 /// id: i64,
689 /// }
690 ///
691 /// let queries = DbCache::new(HydraCache::local().build(), "db");
692 /// let query = queries
693 /// .entity::<User>("user", 42)
694 /// .collection_tag("users:active");
695 ///
696 /// assert_eq!(
697 /// query.tags_value(),
698 /// &["user:42".to_owned(), "users%3Aactive".to_owned()]
699 /// );
700 /// ```
701 pub fn collection_tag(mut self, name: impl ToString) -> Self {
702 self.policy = self.policy.collection_tag(name);
703 self
704 }
705
706 /// Add several invalidation tags.
707 pub fn tags<I, S>(mut self, tags: I) -> Self
708 where
709 I: IntoIterator<Item = S>,
710 S: Into<String>,
711 {
712 self.policy = self.policy.tags(tags);
713 self
714 }
715
716 /// Replace invalidation tags from a reusable [`TagSet`].
717 pub fn tag_set(mut self, tags: TagSet) -> Self {
718 self.policy = self.policy.tag_set(tags);
719 self
720 }
721
722 /// Set a per-entry TTL for this query result.
723 pub fn ttl(mut self, ttl: Duration) -> Self {
724 self.policy = self.policy.ttl(ttl);
725 self
726 }
727
728 /// Fetch a cached value or run the supplied repository/database loader on
729 /// miss.
730 ///
731 /// This is a short alias for [`DbQuery::fetch_with`]. It reads more
732 /// naturally when a call site is wrapping a repository method rather than a
733 /// raw SQL query.
734 pub async fn load<E, F, Fut>(self, loader: F) -> Result<T>
735 where
736 T: Serialize + DeserializeOwned + Send + 'static,
737 E: Error + Send + Sync + 'static,
738 F: FnOnce() -> Fut + Send + 'static,
739 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
740 {
741 self.fetch_with(loader).await
742 }
743
744 /// Fetch a cached value or run the supplied database loader on miss.
745 ///
746 /// The loader is intentionally caller-supplied so the database library
747 /// remains responsible for pools, transactions, compile-time checked
748 /// queries, and row mapping. HydraCache owns only the cache boundary.
749 pub async fn fetch_with<E, F, Fut>(self, loader: F) -> Result<T>
750 where
751 T: Serialize + DeserializeOwned + Send + 'static,
752 E: Error + Send + Sync + 'static,
753 F: FnOnce() -> Fut + Send + 'static,
754 Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
755 {
756 self.fetch_value_with(loader).await
757 }
758
759 /// Fetch a cached value with an output type chosen by an adapter.
760 ///
761 /// Most application code should use [`DbQuery::fetch_with`]. This method is
762 /// intended for adapter crates that keep the descriptor type focused on a
763 /// database row while caching shapes such as `Option<T>` or `Vec<T>`.
764 pub async fn fetch_value_with<U, E, F, Fut>(self, loader: F) -> Result<U>
765 where
766 U: Serialize + DeserializeOwned + Send + 'static,
767 E: Error + Send + Sync + 'static,
768 F: FnOnce() -> Fut + Send + 'static,
769 Fut: Future<Output = std::result::Result<U, E>> + Send + 'static,
770 {
771 let key = self.required_physical_key()?;
772
773 self.cache
774 .get_or_load(&key, self.options(), loader)
775 .await
776 .map_err(DbCacheError::from)
777 }
778
779 fn options(&self) -> CacheOptions {
780 self.policy.cache_options()
781 }
782
783 fn required_physical_key(&self) -> Result<String> {
784 self.physical_key().ok_or_else(|| DbCacheError::MissingKey {
785 operation: self.operation_label(),
786 })
787 }
788
789 fn operation_label(&self) -> String {
790 self.name()
791 .map(str::to_owned)
792 .unwrap_or_else(|| default_operation_label(&self.namespace))
793 }
794}
795
796fn physical_key(namespace: &str, key: &str) -> String {
797 if namespace.is_empty() {
798 key.to_owned()
799 } else {
800 format!("{namespace}:{key}")
801 }
802}
803
804fn default_operation_label(namespace: &str) -> String {
805 if namespace.is_empty() {
806 "unnamed".to_owned()
807 } else {
808 format!("{namespace}:unnamed")
809 }
810}