Skip to main content

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// Reason: used as dyn Trait (Arc<dyn ApqStorage>); async_trait ensures Send bounds and
15// dyn-compatibility async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
16#[async_trait]
17pub trait ApqStorage: Send + Sync {
18    /// Get query by hash
19    ///
20    /// # Arguments
21    ///
22    /// * `hash` - The SHA-256 hash of the query (hexadecimal)
23    ///
24    /// # Returns
25    ///
26    /// * `Ok(Some(query))` if query found
27    /// * `Ok(None)` if query not found
28    /// * `Err(e)` if storage access fails
29    async fn get(&self, hash: &str) -> Result<Option<String>, ApqError>;
30
31    /// Store query with hash
32    ///
33    /// # Arguments
34    ///
35    /// * `hash` - The SHA-256 hash of the query
36    /// * `query` - The full GraphQL query string
37    ///
38    /// # Returns
39    ///
40    /// * `Ok(())` on success
41    /// * `Err(e)` if storage fails
42    async fn set(&self, hash: String, query: String) -> Result<(), ApqError>;
43
44    /// Check if query exists
45    ///
46    /// # Arguments
47    ///
48    /// * `hash` - The SHA-256 hash to check
49    ///
50    /// # Returns
51    ///
52    /// * `Ok(true)` if query exists
53    /// * `Ok(false)` if not found
54    /// * `Err(e)` if check fails
55    async fn exists(&self, hash: &str) -> Result<bool, ApqError>;
56
57    /// Remove query from storage
58    ///
59    /// # Arguments
60    ///
61    /// * `hash` - The hash to remove
62    ///
63    /// # Returns
64    ///
65    /// * `Ok(())` on success
66    /// * `Err(e)` if removal fails
67    async fn remove(&self, hash: &str) -> Result<(), ApqError>;
68
69    /// Get storage statistics
70    ///
71    /// # Returns
72    ///
73    /// Statistics about the storage backend
74    async fn stats(&self) -> Result<ApqStats, ApqError>;
75
76    /// Clear all stored queries
77    ///
78    /// # Returns
79    ///
80    /// * `Ok(())` on success
81    /// * `Err(e)` if clear fails
82    async fn clear(&self) -> Result<(), ApqError>;
83}
84
85/// Statistics about APQ storage
86#[derive(Debug, Clone)]
87pub struct ApqStats {
88    /// Total number of stored queries
89    pub total_queries: usize,
90
91    /// Storage backend name
92    pub backend: String,
93
94    /// Additional backend-specific stats (as JSON)
95    pub extra: serde_json::Value,
96}
97
98impl ApqStats {
99    /// Create new statistics
100    #[must_use]
101    pub fn new(total_queries: usize, backend: String) -> Self {
102        Self {
103            total_queries,
104            backend,
105            extra: json!({}),
106        }
107    }
108
109    /// Create new statistics with extra data
110    #[must_use]
111    pub const fn with_extra(
112        total_queries: usize,
113        backend: String,
114        extra: serde_json::Value,
115    ) -> Self {
116        Self {
117            total_queries,
118            backend,
119            extra,
120        }
121    }
122}
123
124/// APQ errors
125#[derive(Debug, thiserror::Error)]
126#[non_exhaustive]
127pub enum ApqError {
128    /// Query not found in storage
129    #[error("Query not found")]
130    NotFound,
131
132    /// Query size exceeded limit (100KB)
133    #[error("Query size exceeds maximum limit (100KB)")]
134    QueryTooLarge,
135
136    /// Storage backend error
137    #[error("Storage error: {0}")]
138    StorageError(String),
139
140    /// Serialization/deserialization error
141    #[error("Serialization error: {0}")]
142    SerializationError(String),
143
144    /// Database error (for `PostgreSQL` backend)
145    #[error("Database error: {0}")]
146    DatabaseError(String),
147
148    /// Configuration error
149    #[error("Configuration error: {0}")]
150    ConfigError(String),
151}
152
153/// Type alias for arc-wrapped dynamic APQ storage.
154///
155/// Used for thread-safe, reference-counted storage of APQ backends.
156pub type ArcApqStorage = std::sync::Arc<dyn ApqStorage>;
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_apq_stats_creation() {
164        let stats = ApqStats::new(100, "memory".to_string());
165        assert_eq!(stats.total_queries, 100);
166        assert_eq!(stats.backend, "memory");
167        assert_eq!(stats.extra, json!({}));
168    }
169
170    #[test]
171    fn test_apq_stats_with_extra() {
172        let extra = json!({
173            "hits": 500,
174            "misses": 50,
175            "hit_rate": 0.909
176        });
177
178        let stats = ApqStats::with_extra(100, "postgresql".to_string(), extra.clone());
179        assert_eq!(stats.total_queries, 100);
180        assert_eq!(stats.backend, "postgresql");
181        assert_eq!(stats.extra, extra);
182    }
183
184    #[test]
185    fn test_apq_error_display() {
186        let err = ApqError::QueryTooLarge;
187        assert_eq!(err.to_string(), "Query size exceeds maximum limit (100KB)");
188
189        let err = ApqError::StorageError("connection failed".to_string());
190        assert!(err.to_string().contains("connection failed"));
191    }
192}