1extern crate self as hydracache_sqlx;
91
92mod error;
93mod query_ext;
94mod transaction;
95
96pub use error::{Result, SqlxCacheError, SqlxTransactionError, TransactionResult};
97pub use hydracache_db::{
98 prepared_query_policy, query_cache_policy, CacheEntity, CacheKeyBuilder, DbAdapterKind,
99 DbCache, DbCacheError, DbOperationContext, DbQuery, DbResultShape, HydraCacheEntity,
100 InvalidationCollector, InvalidationPlan, InvalidationReport, PgNotifyIntent,
101 PgNotifyIntentSource, PreparedDbQuery, PreparedQueryPolicy, QueryCachePolicy, RefreshPolicy,
102 Result as DbResult, SqlxInvalidationOutbox, OUTBOX_SCHEMA_VERSION,
103};
104pub use query_ext::SqlxQueryExt;
105pub use transaction::{
106 SqlxTransactionCompanion, SqlxTransactionDiagnostics, SqlxTransactionExt,
107 SqlxTransactionFuture, SqlxTransactionReport,
108};
109
110pub type SqlxCache<C = hydracache::PostcardCodec> = DbCache<C>;
112
113pub type SqlxQuery<T, C = hydracache::PostcardCodec> = DbQuery<T, C>;
115
116pub use sqlx;
121
122#[cfg(test)]
123mod tests {
124 use hydracache::HydraCache;
125 use serde::{Deserialize, Serialize};
126 use sqlx::postgres::PgPoolOptions;
127
128 use crate::{
129 prepared_query_policy, query_cache_policy, CacheKeyBuilder, DbCache, InvalidationPlan,
130 PreparedQueryPolicy, QueryCachePolicy, RefreshPolicy, SqlxCache, SqlxQueryExt,
131 };
132
133 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134 struct User {
135 id: u64,
136 }
137
138 #[tokio::test]
139 async fn sqlx_cache_alias_matches_database_cache_api() {
140 let query = SqlxCache::new(HydraCache::local().build(), "sqlx")
141 .cached::<User>()
142 .key("user:1");
143
144 assert_eq!(query.physical_key(), Some("sqlx:user:1".to_owned()));
145 }
146
147 #[tokio::test]
148 async fn db_cache_reexport_is_available_from_sqlx_crate() {
149 let query = DbCache::new(HydraCache::local().build(), "db")
150 .cached::<User>()
151 .key("user:1");
152
153 assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
154
155 let pending = InvalidationPlan::new().tag("user:1");
156 assert_eq!(pending.tag_values().collect::<Vec<_>>(), vec!["user:1"]);
157 }
158
159 #[tokio::test]
160 async fn query_cache_policy_reexport_is_available_from_sqlx_crate() {
161 let refresh =
162 RefreshPolicy::new().stale_while_revalidate(std::time::Duration::from_secs(5));
163 let policy = QueryCachePolicy::new()
164 .key("user:1")
165 .tag("user:1")
166 .refresh_policy(refresh);
167 let query = DbCache::new(HydraCache::local().build(), "db").cached_with::<User>(policy);
168
169 assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
170 assert_eq!(query.tags_value(), &["user:1".to_owned()]);
171 assert_eq!(query.refresh_policy_value(), Some(refresh));
172 }
173
174 #[test]
175 fn query_policy_segment_macro_reexport_uses_sqlx_crate_paths() {
176 let policy = query_cache_policy!(
177 name = "search-users",
178 key_segments = ["tenant", 7_u64, "q", "ada:lovelace"],
179 tag_segments = [["tenant", 7_u64], ["users"]],
180 ttl_secs = 30,
181 );
182 let expected_key = CacheKeyBuilder::new()
183 .segment("tenant")
184 .segment(7_u64)
185 .segment("q")
186 .segment("ada:lovelace")
187 .build_string();
188
189 assert_eq!(policy.name(), Some("search-users"));
190 assert_eq!(policy.key_value(), Some(expected_key.as_str()));
191 assert_eq!(
192 policy.tags_value(),
193 &["tenant:7".to_owned(), "users".to_owned()]
194 );
195 }
196
197 #[tokio::test]
198 async fn prepared_query_policy_reexport_is_available_from_sqlx_crate() {
199 let policy = prepared_query_policy!(
200 entity = "user",
201 name = "load-user",
202 collection_tag = "users",
203 );
204 let expected = PreparedQueryPolicy::for_entity("user")
205 .with_name("load-user")
206 .collection_tag("users");
207 assert_eq!(policy, expected);
208
209 let prepared = DbCache::new(HydraCache::local().build(), "db").prepare::<User>(policy);
210
211 let query = prepared.for_id(1);
212 assert_eq!(query.name(), Some("load-user"));
213 assert_eq!(query.physical_key(), Some("db:user:1".to_owned()));
214 assert_eq!(
215 query.tags_value(),
216 &["users".to_owned(), "user:1".to_owned()]
217 );
218 }
219
220 #[tokio::test]
221 async fn sqlx_helper_missing_key_returns_sqlx_cache_error() {
222 let pool = PgPoolOptions::new()
223 .connect_lazy("postgres://postgres:postgres@localhost/postgres")
224 .unwrap();
225
226 let result = DbCache::new(HydraCache::local().build(), "db")
227 .cached::<(i64,)>()
228 .sqlx_one(pool, sqlx::query_as("select 1"))
229 .await;
230
231 let error = result.unwrap_err();
232 assert_eq!(
233 error.to_string(),
234 "database cached operation `db:unnamed` is missing an explicit cache key (adapter=sqlx, namespace=db, result_shape=one)"
235 );
236 }
237
238 #[tokio::test]
239 async fn sqlx_cache_error_wraps_db_cache_errors() {
240 let error = hydracache_db::DbCacheError::MissingKey {
241 operation: "load-user".to_owned(),
242 adapter: hydracache_db::DbAdapterKind::Sqlx,
243 namespace: "db".to_owned(),
244 result_shape: hydracache_db::DbResultShape::One,
245 };
246 let error = crate::SqlxCacheError::from(error);
247
248 assert_eq!(
249 error.to_string(),
250 "database cached operation `load-user` is missing an explicit cache key (adapter=sqlx, namespace=db, result_shape=one)"
251 );
252 }
253}