hydracache_db/lib.rs
1//! Database-neutral query result caching helpers for HydraCache.
2//!
3//! This crate is intentionally a thin runtime adapter. It does not replace a
4//! database client, ORM, or query builder. Callers keep their database library
5//! as the query authority and provide an explicit cache key, tags, and TTL
6//! around the operation they want to cache.
7//!
8//! # Example
9//!
10//! ```rust
11//! use hydracache::HydraCache;
12//! use hydracache_db::{
13//! DbCache, HydraCacheEntity, PreparedQueryPolicy, QueryCachePolicy, RefreshPolicy,
14//! };
15//! use serde::{Deserialize, Serialize};
16//!
17//! #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
18//! #[hydracache(entity = "user", collection = "users")]
19//! struct User {
20//! #[hydracache(id)]
21//! id: i64,
22//! name: String,
23//! }
24//!
25//! # #[tokio::main]
26//! # async fn main() -> hydracache_db::Result<()> {
27//! let local = HydraCache::local().build();
28//!
29//! // The adapter wraps the local HydraCache instance. The namespace becomes
30//! // part of the physical cache key, so key("user:42") is stored as
31//! // "db:user:42".
32//! let queries = DbCache::new(local, "db");
33//!
34//! let policy = QueryCachePolicy::read_mostly()
35//! // Metadata helper: key "user:42", tag "user:42", and tag "users".
36//! .for_cache_entity::<User>(42)
37//! .with_name("load-user")
38//! .refresh_policy(
39//! RefreshPolicy::new()
40//! .refresh_ahead(std::time::Duration::from_secs(10))
41//! .stale_while_revalidate(std::time::Duration::from_secs(300)),
42//! );
43//!
44//! let user = queries
45//! .cached_with::<User>(policy)
46//! .load(|| async {
47//! // This loader runs only on a cache miss. On a cache hit, HydraCache
48//! // returns the cached User and this database code is not executed.
49//! Ok::<_, std::io::Error>(User {
50//! id: 42,
51//! name: "Ada".to_owned(),
52//! })
53//! })
54//! .await?;
55//!
56//! assert_eq!(user.id, 42);
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! For hot repository methods, prepare stable metadata once and bind only the
62//! dynamic id on each call:
63//!
64//! ```rust
65//! use hydracache::HydraCache;
66//! use hydracache_db::{DbCache, HydraCacheEntity, PreparedQueryPolicy};
67//! use serde::{Deserialize, Serialize};
68//!
69//! #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
70//! #[hydracache(entity = "user", collection = "users")]
71//! struct User {
72//! #[hydracache(id)]
73//! id: i64,
74//! name: String,
75//! }
76//!
77//! # #[tokio::main]
78//! # async fn main() -> hydracache_db::Result<()> {
79//! let queries = DbCache::new(HydraCache::local().build(), "db");
80//! let load_user = queries.prepare::<User>(
81//! PreparedQueryPolicy::per_entity()
82//! .cache_entity::<User>()
83//! .with_name("load-user"),
84//! );
85//!
86//! let user = load_user
87//! .load_id(42, || async {
88//! Ok::<_, std::io::Error>(User {
89//! id: 42,
90//! name: "Ada".to_owned(),
91//! })
92//! })
93//! .await?;
94//!
95//! assert_eq!(user.id, 42);
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! For compact policy construction, use [`query_cache_policy!`]:
101//!
102//! ```rust
103//! use hydracache_db::{query_cache_policy, CacheEntity};
104//!
105//! struct User;
106//!
107//! impl CacheEntity for User {
108//! type Id = i64;
109//!
110//! const ENTITY: &'static str = "user";
111//! const COLLECTION: Option<&'static str> = Some("users");
112//! }
113//!
114//! let user_id = 42_i64;
115//! let policy = query_cache_policy!(
116//! preset = read_mostly,
117//! name = "load-user",
118//! entity = User,
119//! id = user_id,
120//! refresh_ahead_secs = 10,
121//! stale_while_revalidate_secs = 300,
122//! );
123//!
124//! assert_eq!(policy.name(), Some("load-user"));
125//! assert_eq!(policy.key_value(), Some("user:42"));
126//! assert!(policy.refresh_policy_value().is_some());
127//!
128//! let search = query_cache_policy!(
129//! name = "search-users",
130//! key_segments = ["tenant", 7_u64, "q", "ada:lovelace", "page", 1_u32],
131//! tag_segments = [["tenant", 7_u64], ["users"]],
132//! ttl_secs = 30,
133//! );
134//!
135//! assert_eq!(
136//! search.key_value(),
137//! Some("tenant:7:q:ada%3Alovelace:page:1")
138//! );
139//! assert_eq!(search.tags_value(), &["tenant:7".to_owned(), "users".to_owned()]);
140//! ```
141//!
142//! For write paths, stage invalidations during repository work and execute them
143//! only after the database transaction commits:
144//!
145//! ```rust
146//! use hydracache::HydraCache;
147//! use hydracache_db::{HydraCacheEntity, InvalidationPlan};
148//! use serde::{Deserialize, Serialize};
149//!
150//! #[derive(Debug, Clone, Serialize, Deserialize, HydraCacheEntity)]
151//! #[hydracache(entity = "user", collection = "users")]
152//! struct User {
153//! #[hydracache(id)]
154//! id: i64,
155//! }
156//!
157//! # #[tokio::main]
158//! # async fn main() -> hydracache::CacheResult<()> {
159//! let cache = HydraCache::local().build();
160//! let pending = InvalidationPlan::new().cache_entity::<User>(42);
161//!
162//! // tx.update_user(42).await?;
163//! // tx.commit().await?;
164//!
165//! let report = pending.execute(&cache).await?;
166//! assert_eq!(report.tag_count, 2);
167//! # Ok(())
168//! # }
169//! ```
170
171extern crate self as hydracache_db;
172
173mod entity;
174mod error;
175mod hooks;
176mod invalidation;
177mod lint;
178mod outbox;
179mod policy;
180mod prepared;
181mod profiles;
182mod query;
183mod reconcile;
184#[cfg(feature = "sqlx-outbox")]
185mod sqlx_outbox;
186mod transaction;
187
188pub use entity::CacheEntity;
189pub use error::{DbAdapterKind, DbCacheError, DbOperationContext, DbResultShape, Result};
190pub use hooks::{
191 HookDialect, HookError, HookInvalidationTarget, HookOperation, HookPlan, HookSchemaVersion,
192 HOOK_SCHEMA_ARTIFACT, HOOK_SCHEMA_VERSION,
193};
194pub use hydracache::CacheKeyBuilder;
195pub use hydracache_macros::{prepared_query_policy, query_cache_policy, HydraCacheEntity};
196pub use invalidation::{InvalidationPlan, InvalidationReport};
197pub use lint::{
198 schema_table, table, DeclaredLintMode, DeclaredRelation, LintFinding, LintSuppression,
199 PolicyLintMetadata,
200};
201pub use outbox::{
202 CommitPosition, ConsistencyMode, InMemoryInvalidationOutbox, InvalidationApplier,
203 InvalidationIntent, InvalidationIntentBatch, InvalidationOutbox, InvalidationOutboxWorker,
204 InvalidationReceipt, InvalidationTargetHash, InvalidationWait, InvalidationWaitDiagnostics,
205 InvalidationWaitOutcome, OutboxPublishReport, OutboxRow, OutboxState, OutboxStatus,
206 OutboxWorkerDiagnostics,
207};
208pub use policy::QueryCachePolicy;
209pub use prepared::PreparedQueryPolicy;
210pub use profiles::{
211 CustomProfile, DimensionAllow, DimensionAllowError, DimensionProfile, DimensionRequirement,
212 DimensionValidationMode, ProfileValidation,
213};
214pub use query::{DbCache, DbQuery, PreparedDbQuery};
215#[cfg(feature = "sqlx-outbox")]
216pub use reconcile::sqlite_hook_drift;
217pub use reconcile::{
218 CdcOffsetLag, DriftReason, DriftStatus, GenerationDrift, HookDrift, OutboxLag, OutboxLagPolicy,
219 ReconciliationReport,
220};
221#[cfg(feature = "sqlx-outbox")]
222pub use sqlx_outbox::{
223 PgNotifyIntent, PgNotifyIntentSource, SqlxInvalidationOutbox, OUTBOX_SCHEMA_VERSION,
224};
225pub use transaction::{CollectedInvalidationReport, CollectedInvalidations, InvalidationCollector};
226
227/// Database-facing alias for local cache refresh/stale behavior.
228pub type RefreshPolicy = hydracache::RefreshOptions;
229
230#[cfg(test)]
231mod tests;