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//! [`query_cache_policy!`] is re-exported for the same convenience.
52
53extern crate self as hydracache_sqlx;
54
55mod error;
56mod query_ext;
57
58pub use error::{Result, SqlxCacheError};
59pub use hydracache_db::{
60 query_cache_policy, CacheEntity, DbCache, DbCacheError, DbQuery, HydraCacheEntity,
61 QueryCachePolicy, Result as DbResult,
62};
63pub use query_ext::SqlxQueryExt;
64
65/// SQLx-specific compatibility name for [`DbCache`].
66pub type SqlxCache<C = hydracache::PostcardCodec> = DbCache<C>;
67
68/// SQLx-specific compatibility name for [`DbQuery`].
69pub type SqlxQuery<T, C = hydracache::PostcardCodec> = DbQuery<T, C>;
70
71/// Re-export the SQLx crate used by this adapter.
72///
73/// This lets downstream users keep one adapter-aligned SQLx version in examples
74/// and integration code without hiding SQLx behind HydraCache abstractions.
75pub use sqlx;
76
77#[cfg(test)]
78mod tests {
79 use hydracache::HydraCache;
80 use serde::{Deserialize, Serialize};
81 use sqlx::postgres::PgPoolOptions;
82
83 use crate::{DbCache, QueryCachePolicy, SqlxCache, SqlxQueryExt};
84
85 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86 struct User {
87 id: u64,
88 }
89
90 #[tokio::test]
91 async fn sqlx_cache_alias_matches_database_cache_api() {
92 let query = SqlxCache::new(HydraCache::local().build(), "sqlx")
93 .cached::<User>()
94 .key("user:1");
95
96 assert_eq!(query.physical_key(), Some("sqlx:user:1".to_owned()));
97 }
98
99 #[tokio::test]
100 async fn db_cache_reexport_is_available_from_sqlx_crate() {
101 let query = DbCache::new(HydraCache::local().build(), "db")
102 .cached::<User>()
103 .key("user:1");
104
105 assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
106 }
107
108 #[tokio::test]
109 async fn query_cache_policy_reexport_is_available_from_sqlx_crate() {
110 let policy = QueryCachePolicy::new().key("user:1").tag("user:1");
111 let query = DbCache::new(HydraCache::local().build(), "db").cached_with::<User>(policy);
112
113 assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
114 assert_eq!(query.tags_value(), &["user:1".to_owned()]);
115 }
116
117 #[tokio::test]
118 async fn sqlx_helper_missing_key_returns_sqlx_cache_error() {
119 let pool = PgPoolOptions::new()
120 .connect_lazy("postgres://postgres:postgres@localhost/postgres")
121 .unwrap();
122
123 let result = DbCache::new(HydraCache::local().build(), "db")
124 .cached::<(i64,)>()
125 .fetch_one(pool, sqlx::query_as("select 1"))
126 .await;
127
128 let error = result.unwrap_err();
129 assert_eq!(
130 error.to_string(),
131 "database cached operation `db:unnamed` is missing an explicit cache key"
132 );
133 }
134
135 #[tokio::test]
136 async fn sqlx_cache_error_wraps_db_cache_errors() {
137 let error = hydracache_db::DbCacheError::MissingKey {
138 operation: "load-user".to_owned(),
139 };
140 let error = crate::SqlxCacheError::from(error);
141
142 assert_eq!(
143 error.to_string(),
144 "database cached operation `load-user` is missing an explicit cache key"
145 );
146 }
147}