Skip to main content

hydracache_sqlx/
lib.rs

1//! SQLx-facing integration crate for HydraCache database result caching.
2//!
3//! The database-neutral query cache API lives in `hydracache-db`. This crate
4//! keeps SQLx users on a convenient import path while avoiding a hard conceptual
5//! dependency between the generic adapter and SQLx itself.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use hydracache::HydraCache;
11//! use hydracache_sqlx::{DbCache, HydraCacheEntity, SqlxQueryExt};
12//!
13//! #[derive(serde::Serialize, serde::Deserialize, HydraCacheEntity)]
14//! #[hydracache(entity = "user", collection = "users", id = i64)]
15//! struct User {
16//!     id: i64,
17//!     name: String,
18//! }
19//!
20//! # async fn example(pool: sqlx::PgPool) -> hydracache_sqlx::Result<()> {
21//! let local = HydraCache::local().build();
22//!
23//! // SQLx users may import DbCache from this crate, but the type itself is
24//! // database-neutral and comes from hydracache-db.
25//! let queries = DbCache::new(local, "db");
26//!
27//! let user: User = queries
28//!     .for_entity::<User>(42)
29//!     .fetch_with(move || async move {
30//!         let (id, name): (i64, String) =
31//!             sqlx::query_as("select id, name from users where id = $1")
32//!                 .bind(42_i64)
33//!                 .fetch_one(&pool)
34//!                 .await?;
35//!
36//!         Ok::<_, sqlx::Error>(User { id, name })
37//!     })
38//!     .await?;
39//!
40//! assert_eq!(user.id, 42);
41//! assert!(!user.name.is_empty());
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! Use [`DbQuery::fetch_with`] when you need SQLx macros, transactions, or a
47//! repository function instead of a pool-like executor.
48//!
49//! [`QueryCachePolicy`] is also re-exported for SQLx users, but the policy type
50//! is database-neutral and lives in `hydracache-db`.
51
52extern crate self as hydracache_sqlx;
53
54mod error;
55mod query_ext;
56
57pub use error::{Result, SqlxCacheError};
58pub use hydracache_db::{
59    CacheEntity, DbCache, DbCacheError, DbQuery, HydraCacheEntity, QueryCachePolicy,
60    Result as DbResult,
61};
62pub use query_ext::SqlxQueryExt;
63
64/// SQLx-specific compatibility name for [`DbCache`].
65pub type SqlxCache<C = hydracache::PostcardCodec> = DbCache<C>;
66
67/// SQLx-specific compatibility name for [`DbQuery`].
68pub type SqlxQuery<T, C = hydracache::PostcardCodec> = DbQuery<T, C>;
69
70/// Re-export the SQLx crate used by this adapter.
71///
72/// This lets downstream users keep one adapter-aligned SQLx version in examples
73/// and integration code without hiding SQLx behind HydraCache abstractions.
74pub use sqlx;
75
76#[cfg(test)]
77mod tests {
78    use hydracache::HydraCache;
79    use serde::{Deserialize, Serialize};
80    use sqlx::postgres::PgPoolOptions;
81
82    use crate::{DbCache, QueryCachePolicy, SqlxCache, SqlxQueryExt};
83
84    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85    struct User {
86        id: u64,
87    }
88
89    #[tokio::test]
90    async fn sqlx_cache_alias_matches_database_cache_api() {
91        let query = SqlxCache::new(HydraCache::local().build(), "sqlx")
92            .cached::<User>()
93            .key("user:1");
94
95        assert_eq!(query.physical_key(), Some("sqlx:user:1".to_owned()));
96    }
97
98    #[tokio::test]
99    async fn db_cache_reexport_is_available_from_sqlx_crate() {
100        let query = DbCache::new(HydraCache::local().build(), "db")
101            .cached::<User>()
102            .key("user:1");
103
104        assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
105    }
106
107    #[tokio::test]
108    async fn query_cache_policy_reexport_is_available_from_sqlx_crate() {
109        let policy = QueryCachePolicy::new().key("user:1").tag("user:1");
110        let query = DbCache::new(HydraCache::local().build(), "db").cached_with::<User>(policy);
111
112        assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
113        assert_eq!(query.tags_value(), &["user:1".to_owned()]);
114    }
115
116    #[tokio::test]
117    async fn sqlx_helper_missing_key_returns_sqlx_cache_error() {
118        let pool = PgPoolOptions::new()
119            .connect_lazy("postgres://postgres:postgres@localhost/postgres")
120            .unwrap();
121
122        let result = DbCache::new(HydraCache::local().build(), "db")
123            .cached::<(i64,)>()
124            .fetch_one(pool, sqlx::query_as("select 1"))
125            .await;
126
127        let error = result.unwrap_err();
128        assert_eq!(
129            error.to_string(),
130            "database cached operation `db:unnamed` is missing an explicit cache key"
131        );
132    }
133
134    #[tokio::test]
135    async fn sqlx_cache_error_wraps_db_cache_errors() {
136        let error = hydracache_db::DbCacheError::MissingKey {
137            operation: "load-user".to_owned(),
138        };
139        let error = crate::SqlxCacheError::from(error);
140
141        assert_eq!(
142            error.to_string(),
143            "database cached operation `load-user` is missing an explicit cache key"
144        );
145    }
146}