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, SqlxQueryExt};
12//!
13//! # async fn example(pool: sqlx::PgPool) -> hydracache_sqlx::Result<()> {
14//! let local = HydraCache::local().build();
15//!
16//! // SQLx users may import DbCache from this crate, but the type itself is
17//! // database-neutral and comes from hydracache-db.
18//! let queries = DbCache::new(local, "db");
19//!
20//! let (id, name): (i64, String) = queries
21//!     .cached::<(i64, String)>()
22//!     .key("user:42")
23//!     .tag("user:42")
24//!     .fetch_one(
25//!         pool.clone(),
26//!         sqlx::query_as("select id, name from users where id = $1").bind(42_i64),
27//!     )
28//!     .await?;
29//!
30//! assert_eq!(id, 42);
31//! assert!(!name.is_empty());
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! Use [`DbQuery::fetch_with`] when you need SQLx macros, transactions, or a
37//! repository function instead of a pool-like executor.
38
39mod error;
40mod query_ext;
41
42pub use error::{Result, SqlxCacheError};
43pub use hydracache_db::{DbCache, DbCacheError, DbQuery, Result as DbResult};
44pub use query_ext::SqlxQueryExt;
45
46/// SQLx-specific compatibility name for [`DbCache`].
47pub type SqlxCache<C = hydracache::PostcardCodec> = DbCache<C>;
48
49/// SQLx-specific compatibility name for [`DbQuery`].
50pub type SqlxQuery<T, C = hydracache::PostcardCodec> = DbQuery<T, C>;
51
52/// Re-export the SQLx crate used by this adapter.
53///
54/// This lets downstream users keep one adapter-aligned SQLx version in examples
55/// and integration code without hiding SQLx behind HydraCache abstractions.
56pub use sqlx;
57
58#[cfg(test)]
59mod tests {
60    use hydracache::HydraCache;
61    use serde::{Deserialize, Serialize};
62    use sqlx::postgres::PgPoolOptions;
63
64    use crate::{DbCache, SqlxCache, SqlxQueryExt};
65
66    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67    struct User {
68        id: u64,
69    }
70
71    #[tokio::test]
72    async fn sqlx_cache_alias_matches_database_cache_api() {
73        let query = SqlxCache::new(HydraCache::local().build(), "sqlx")
74            .cached::<User>()
75            .key("user:1");
76
77        assert_eq!(query.physical_key(), Some("sqlx:user:1".to_owned()));
78    }
79
80    #[tokio::test]
81    async fn db_cache_reexport_is_available_from_sqlx_crate() {
82        let query = DbCache::new(HydraCache::local().build(), "db")
83            .cached::<User>()
84            .key("user:1");
85
86        assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
87    }
88
89    #[tokio::test]
90    async fn sqlx_helper_missing_key_returns_sqlx_cache_error() {
91        let pool = PgPoolOptions::new()
92            .connect_lazy("postgres://postgres:postgres@localhost/postgres")
93            .unwrap();
94
95        let result = DbCache::new(HydraCache::local().build(), "db")
96            .cached::<(i64,)>()
97            .fetch_one(pool, sqlx::query_as("select 1"))
98            .await;
99
100        let error = result.unwrap_err();
101        assert_eq!(
102            error.to_string(),
103            "database cached operation `db:unnamed` is missing an explicit cache key"
104        );
105    }
106
107    #[tokio::test]
108    async fn sqlx_cache_error_wraps_db_cache_errors() {
109        let error = hydracache_db::DbCacheError::MissingKey {
110            operation: "load-user".to_owned(),
111        };
112        let error = crate::SqlxCacheError::from(error);
113
114        assert_eq!(
115            error.to_string(),
116            "database cached operation `load-user` is missing an explicit cache key"
117        );
118    }
119}