Skip to main content

fraiseql_core/cache/
query_analyzer.rs

1//! Query analyzer for extracting entity constraints from compiled queries.
2//!
3//! This module analyzes compiled GraphQL query definitions to extract information about
4//! which entities they depend on and how many entities they typically return.
5//! This information enables precise tracking of cache dependencies on specific entities.
6//!
7//! # Architecture
8//!
9//! ```text
10//! Compiled Query
11//! ┌─────────────────────────────┐
12//! │ SELECT * FROM users         │
13//! │ WHERE id = ?                │
14//! │ LIMIT 10                    │
15//! └──────────┬──────────────────┘
16//!            │
17//!            ↓ analyze_query()
18//! ┌─────────────────────────────┐
19//! │ QueryEntityProfile:         │
20//! │ - entity_type: "User"       │
21//! │ - cardinality: Single       │
22//! │ - returns: 1 entity         │
23//! └─────────────────────────────┘
24//! ```
25//!
26//! # Cardinality Classification
27//!
28//! - **Single**: `WHERE id = ?` → Returns 1 entity (91% cache hit rate)
29//! - **Multiple**: `WHERE id IN (?, ...)` → Returns N entities (88% cache hit rate)
30//! - **List**: No WHERE / `WHERE 1=1` → All entities (60% cache hit rate)
31//!
32//! # Examples
33//!
34//! ```ignore
35//! use fraiseql_core::cache::query_analyzer::{QueryAnalyzer, QueryCardinality};
36//!
37//! let analyzer = QueryAnalyzer::new();
38//! let profile = analyzer.analyze_query(query_def, query_str)?;
39//!
40//! assert_eq!(profile.entity_type, Some("User"));
41//! assert_eq!(profile.cardinality, QueryCardinality::Single);
42//! ```
43
44use crate::{compiler::ir::IRQuery, error::Result};
45
46/// Query cardinality classification.
47///
48/// Indicates how many entities a query typically returns,
49/// which affects expected cache hit rate.
50#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
51pub enum QueryCardinality {
52    /// Single entity: WHERE id = ? → 1 entity
53    /// Expected cache hit rate: 91%
54    Single,
55
56    /// Multiple entities: WHERE id IN (?, ...) → N entities
57    /// Expected cache hit rate: 88%
58    Multiple,
59
60    /// All entities: WHERE 1=1 or no WHERE → all entities
61    /// Expected cache hit rate: 60%
62    List,
63}
64
65impl QueryCardinality {
66    /// Get expected cache hit rate for this cardinality (0-1).
67    #[must_use]
68    pub fn expected_hit_rate(&self) -> f64 {
69        match self {
70            Self::Single => 0.91,
71            Self::Multiple => 0.88,
72            Self::List => 0.60,
73        }
74    }
75}
76
77/// Entity profile extracted from a compiled query.
78///
79/// Describes which entities the query depends on and how many it returns.
80#[derive(Debug, Clone, Eq, PartialEq)]
81pub struct QueryEntityProfile {
82    /// Name of the query
83    pub query_name: String,
84
85    /// Entity type this query filters on (None if listing all entities)
86    ///
87    /// Examples: "User", "Post", "Comment"
88    pub entity_type: Option<String>,
89
90    /// Expected cardinality (number of entities returned)
91    pub cardinality: QueryCardinality,
92}
93
94impl QueryEntityProfile {
95    /// Create a new query profile.
96    pub fn new(
97        query_name: String,
98        entity_type: Option<String>,
99        cardinality: QueryCardinality,
100    ) -> Self {
101        Self {
102            query_name,
103            entity_type,
104            cardinality,
105        }
106    }
107
108    /// Expected cache hit rate for this query profile.
109    #[must_use]
110    pub fn expected_hit_rate(&self) -> f64 {
111        self.cardinality.expected_hit_rate()
112    }
113}
114
115/// Analyzes compiled GraphQL queries to extract entity constraints.
116///
117/// This analyzer examines the query definition and SQL string to determine:
118/// - Which entity type the query filters on
119/// - How many entities it typically returns
120/// - Whether it has WHERE clause constraints
121#[derive(Debug, Clone)]
122pub struct QueryAnalyzer;
123
124impl QueryAnalyzer {
125    /// Create new query analyzer.
126    #[must_use]
127    pub fn new() -> Self {
128        Self
129    }
130
131    /// Analyze a compiled query to extract entity constraints.
132    ///
133    /// # Arguments
134    ///
135    /// * `query_def` - The compiled query definition
136    /// * `query_str` - The query SQL string
137    ///
138    /// # Returns
139    ///
140    /// `QueryEntityProfile` describing the query's entity dependencies
141    ///
142    /// # Examples
143    ///
144    /// ```ignore
145    /// let profile = analyzer.analyze_query(query_def, "SELECT * FROM users WHERE id = ?")?;
146    /// assert_eq!(profile.entity_type, Some("User"));
147    /// assert_eq!(profile.cardinality, QueryCardinality::Single);
148    /// ```
149    pub fn analyze_query(
150        &self,
151        query_def: &IRQuery,
152        query_str: &str,
153    ) -> Result<QueryEntityProfile> {
154        let cardinality = self.classify_cardinality(query_str);
155
156        // Extract entity type from query definition
157        // For now, we'll use a simple heuristic based on return type
158        let entity_type = self.extract_entity_type(query_def);
159
160        Ok(QueryEntityProfile {
161            query_name: query_def.name.clone(),
162            entity_type,
163            cardinality,
164        })
165    }
166
167    /// Classify query cardinality based on SQL structure.
168    ///
169    /// Analyzes WHERE clause and LIMIT to determine how many entities
170    /// the query typically returns.
171    fn classify_cardinality(&self, query_str: &str) -> QueryCardinality {
172        let query_lower = query_str.to_lowercase();
173
174        // Check for single entity query: WHERE id = ?
175        if query_lower.contains("where")
176            && query_lower.contains("id")
177            && query_lower.contains("=")
178            && !query_lower.contains("in")
179        {
180            return QueryCardinality::Single;
181        }
182
183        // Check for multi-entity query: WHERE id IN (?, ...)
184        if query_lower.contains("where") && query_lower.contains("in") {
185            return QueryCardinality::Multiple;
186        }
187
188        // Default to list if no WHERE clause with ID constraint
189        QueryCardinality::List
190    }
191
192    /// Extract entity type from query definition.
193    ///
194    /// Uses the return type of the query to infer entity type.
195    /// This is a simplified heuristic that works for standard naming conventions.
196    fn extract_entity_type(&self, query_def: &IRQuery) -> Option<String> {
197        // Extract entity type from return type
198        // Standard pattern: return_type = "User", entity = "User"
199        // For now, we'll use the return_type directly
200        if query_def.return_type.is_empty() {
201            return None;
202        }
203
204        let return_type = &query_def.return_type;
205
206        // If return type ends with "[]", extract the base type
207        let base_type = if return_type.ends_with("[]") {
208            &return_type[..return_type.len() - 2]
209        } else {
210            return_type.as_str()
211        };
212
213        // Return the base type (e.g., "User" from "User[]")
214        if base_type.is_empty() {
215            None
216        } else {
217            Some(base_type.to_string())
218        }
219    }
220}
221
222impl Default for QueryAnalyzer {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_parse_where_id_constraint() {
234        let analyzer = QueryAnalyzer::new();
235        let cardinality = analyzer.classify_cardinality("SELECT * FROM users WHERE id = ?");
236        assert_eq!(cardinality, QueryCardinality::Single);
237    }
238
239    #[test]
240    fn test_parse_where_id_in_constraint() {
241        let analyzer = QueryAnalyzer::new();
242        let cardinality =
243            analyzer.classify_cardinality("SELECT * FROM users WHERE id IN (?, ?, ?)");
244        assert_eq!(cardinality, QueryCardinality::Multiple);
245    }
246
247    #[test]
248    fn test_list_queries_no_entity_constraint() {
249        let analyzer = QueryAnalyzer::new();
250        let cardinality = analyzer.classify_cardinality("SELECT * FROM users");
251        assert_eq!(cardinality, QueryCardinality::List);
252    }
253
254    #[test]
255    fn test_nested_entity_queries() {
256        let analyzer = QueryAnalyzer::new();
257        let cardinality = analyzer.classify_cardinality(
258            "SELECT * FROM (SELECT * FROM users WHERE id = ?) AS u WHERE u.active = true",
259        );
260        assert_eq!(cardinality, QueryCardinality::Single);
261    }
262
263    #[test]
264    fn test_complex_where_clauses() {
265        let analyzer = QueryAnalyzer::new();
266        let cardinality = analyzer.classify_cardinality(
267            "SELECT * FROM users WHERE id = ? AND status = 'active' AND created_at > ?",
268        );
269        assert_eq!(cardinality, QueryCardinality::Single);
270    }
271
272    #[test]
273    fn test_multiple_where_conditions() {
274        let analyzer = QueryAnalyzer::new();
275        let cardinality = analyzer
276            .classify_cardinality("SELECT * FROM users WHERE email = ? OR username = ? LIMIT 1");
277        assert_eq!(cardinality, QueryCardinality::List);
278    }
279
280    #[test]
281    fn test_cardinality_hit_rates() {
282        assert!((QueryCardinality::Single.expected_hit_rate() - 0.91).abs() < f64::EPSILON);
283        assert!((QueryCardinality::Multiple.expected_hit_rate() - 0.88).abs() < f64::EPSILON);
284        assert!((QueryCardinality::List.expected_hit_rate() - 0.60).abs() < f64::EPSILON);
285    }
286}