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