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