fraiseql_core/apq/storage.rs
1//! Storage trait and types for APQ backends
2//!
3//! This module defines the abstract storage interface that all APQ backends must implement,
4//! allowing for pluggable storage backends (memory, `PostgreSQL`, etc.).
5
6use async_trait::async_trait;
7use serde_json::json;
8
9/// Storage backend for persisted queries
10///
11/// Implementations of this trait provide different storage strategies:
12/// - Memory: In-process LRU cache (single instance, fast)
13/// - `PostgreSQL`: Distributed storage (multi-instance, persistent)
14#[async_trait]
15pub trait ApqStorage: Send + Sync {
16 /// Get query by hash
17 ///
18 /// # Arguments
19 ///
20 /// * `hash` - The SHA-256 hash of the query (hexadecimal)
21 ///
22 /// # Returns
23 ///
24 /// * `Ok(Some(query))` if query found
25 /// * `Ok(None)` if query not found
26 /// * `Err(e)` if storage access fails
27 async fn get(&self, hash: &str) -> Result<Option<String>, ApqError>;
28
29 /// Store query with hash
30 ///
31 /// # Arguments
32 ///
33 /// * `hash` - The SHA-256 hash of the query
34 /// * `query` - The full GraphQL query string
35 ///
36 /// # Returns
37 ///
38 /// * `Ok(())` on success
39 /// * `Err(e)` if storage fails
40 async fn set(&self, hash: String, query: String) -> Result<(), ApqError>;
41
42 /// Check if query exists
43 ///
44 /// # Arguments
45 ///
46 /// * `hash` - The SHA-256 hash to check
47 ///
48 /// # Returns
49 ///
50 /// * `Ok(true)` if query exists
51 /// * `Ok(false)` if not found
52 /// * `Err(e)` if check fails
53 async fn exists(&self, hash: &str) -> Result<bool, ApqError>;
54
55 /// Remove query from storage
56 ///
57 /// # Arguments
58 ///
59 /// * `hash` - The hash to remove
60 ///
61 /// # Returns
62 ///
63 /// * `Ok(())` on success
64 /// * `Err(e)` if removal fails
65 async fn remove(&self, hash: &str) -> Result<(), ApqError>;
66
67 /// Get storage statistics
68 ///
69 /// # Returns
70 ///
71 /// Statistics about the storage backend
72 async fn stats(&self) -> Result<ApqStats, ApqError>;
73
74 /// Clear all stored queries
75 ///
76 /// # Returns
77 ///
78 /// * `Ok(())` on success
79 /// * `Err(e)` if clear fails
80 async fn clear(&self) -> Result<(), ApqError>;
81}
82
83/// Statistics about APQ storage
84#[derive(Debug, Clone)]
85pub struct ApqStats {
86 /// Total number of stored queries
87 pub total_queries: usize,
88
89 /// Storage backend name
90 pub backend: String,
91
92 /// Additional backend-specific stats (as JSON)
93 pub extra: serde_json::Value,
94}
95
96impl ApqStats {
97 /// Create new statistics
98 #[must_use]
99 pub fn new(total_queries: usize, backend: String) -> Self {
100 Self {
101 total_queries,
102 backend,
103 extra: json!({}),
104 }
105 }
106
107 /// Create new statistics with extra data
108 #[must_use]
109 pub const fn with_extra(
110 total_queries: usize,
111 backend: String,
112 extra: serde_json::Value,
113 ) -> Self {
114 Self {
115 total_queries,
116 backend,
117 extra,
118 }
119 }
120}
121
122/// APQ errors
123#[derive(Debug, thiserror::Error)]
124pub enum ApqError {
125 /// Query not found in storage
126 #[error("Query not found")]
127 NotFound,
128
129 /// Query size exceeded limit (100KB)
130 #[error("Query size exceeds maximum limit (100KB)")]
131 QueryTooLarge,
132
133 /// Storage backend error
134 #[error("Storage error: {0}")]
135 StorageError(String),
136
137 /// Serialization/deserialization error
138 #[error("Serialization error: {0}")]
139 SerializationError(String),
140
141 /// Database error (for `PostgreSQL` backend)
142 #[error("Database error: {0}")]
143 DatabaseError(String),
144
145 /// Configuration error
146 #[error("Configuration error: {0}")]
147 ConfigError(String),
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_apq_stats_creation() {
156 let stats = ApqStats::new(100, "memory".to_string());
157 assert_eq!(stats.total_queries, 100);
158 assert_eq!(stats.backend, "memory");
159 assert_eq!(stats.extra, json!({}));
160 }
161
162 #[test]
163 fn test_apq_stats_with_extra() {
164 let extra = json!({
165 "hits": 500,
166 "misses": 50,
167 "hit_rate": 0.909
168 });
169
170 let stats = ApqStats::with_extra(100, "postgresql".to_string(), extra.clone());
171 assert_eq!(stats.total_queries, 100);
172 assert_eq!(stats.backend, "postgresql");
173 assert_eq!(stats.extra, extra);
174 }
175
176 #[test]
177 fn test_apq_error_display() {
178 let err = ApqError::QueryTooLarge;
179 assert_eq!(err.to_string(), "Query size exceeds maximum limit (100KB)");
180
181 let err = ApqError::StorageError("connection failed".to_string());
182 assert!(err.to_string().contains("connection failed"));
183 }
184}