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#[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}