Skip to main content

fraiseql_core/runtime/
matcher.rs

1//! Query pattern matching - matches incoming GraphQL queries to compiled templates.
2
3use std::collections::HashMap;
4
5use crate::{
6    error::{FraiseQLError, Result},
7    graphql::{DirectiveEvaluator, FieldSelection, FragmentResolver, ParsedQuery, parse_query},
8    schema::{CompiledSchema, QueryDefinition},
9};
10
11/// A matched query with extracted information.
12#[derive(Debug, Clone)]
13pub struct QueryMatch {
14    /// The matched query definition from compiled schema.
15    pub query_def: QueryDefinition,
16
17    /// Requested fields (selection set) - now includes full field info.
18    pub fields: Vec<String>,
19
20    /// Parsed and processed field selections (after fragment/directive resolution).
21    pub selections: Vec<FieldSelection>,
22
23    /// Query arguments/variables.
24    pub arguments: HashMap<String, serde_json::Value>,
25
26    /// Query operation name (if provided).
27    pub operation_name: Option<String>,
28
29    /// The parsed query (for access to fragments, variables, etc.).
30    pub parsed_query: ParsedQuery,
31}
32
33/// Query pattern matcher.
34///
35/// Matches incoming GraphQL queries against the compiled schema to determine
36/// which pre-compiled SQL template to execute.
37pub struct QueryMatcher {
38    schema: CompiledSchema,
39}
40
41impl QueryMatcher {
42    /// Create new query matcher.
43    #[must_use]
44    pub fn new(schema: CompiledSchema) -> Self {
45        Self { schema }
46    }
47
48    /// Match a GraphQL query to a compiled template.
49    ///
50    /// # Arguments
51    ///
52    /// * `query` - GraphQL query string
53    /// * `variables` - Query variables (optional)
54    ///
55    /// # Returns
56    ///
57    /// QueryMatch with query definition and extracted information
58    ///
59    /// # Errors
60    ///
61    /// Returns error if:
62    /// - Query syntax is invalid
63    /// - Query references undefined operation
64    /// - Query structure doesn't match schema
65    /// - Fragment resolution fails
66    /// - Directive evaluation fails
67    ///
68    /// # Example
69    ///
70    /// ```rust,ignore
71    /// let matcher = QueryMatcher::new(schema);
72    /// let query = "query { users { id name } }";
73    /// let matched = matcher.match_query(query, None)?;
74    /// assert_eq!(matched.query_def.name, "users");
75    /// ```
76    pub fn match_query(
77        &self,
78        query: &str,
79        variables: Option<&serde_json::Value>,
80    ) -> Result<QueryMatch> {
81        // 1. Parse GraphQL query using proper parser
82        let parsed = parse_query(query).map_err(|e| FraiseQLError::Parse {
83            message:  e.to_string(),
84            location: "query".to_string(),
85        })?;
86
87        // 2. Build variables map for directive evaluation
88        let variables_map = self.build_variables_map(variables);
89
90        // 3. Resolve fragment spreads
91        let resolver = FragmentResolver::new(&parsed.fragments);
92        let resolved_selections = resolver.resolve_spreads(&parsed.selections).map_err(|e| {
93            FraiseQLError::Validation {
94                message: e.to_string(),
95                path:    Some("fragments".to_string()),
96            }
97        })?;
98
99        // 4. Evaluate directives (@skip, @include) and filter selections
100        let final_selections =
101            DirectiveEvaluator::filter_selections(&resolved_selections, &variables_map).map_err(
102                |e| FraiseQLError::Validation {
103                    message: e.to_string(),
104                    path:    Some("directives".to_string()),
105                },
106            )?;
107
108        // 5. Find matching query definition using root field
109        let query_def = self
110            .schema
111            .find_query(&parsed.root_field)
112            .ok_or_else(|| FraiseQLError::Validation {
113                message: format!("Query '{}' not found in schema", parsed.root_field),
114                path:    None,
115            })?
116            .clone();
117
118        // 6. Extract field names for backward compatibility
119        let fields = self.extract_field_names(&final_selections);
120
121        // 7. Extract arguments from variables
122        let arguments = self.extract_arguments(variables);
123
124        Ok(QueryMatch {
125            query_def,
126            fields,
127            selections: final_selections,
128            arguments,
129            operation_name: parsed.operation_name.clone(),
130            parsed_query: parsed,
131        })
132    }
133
134    /// Build a variables map from JSON value for directive evaluation.
135    fn build_variables_map(
136        &self,
137        variables: Option<&serde_json::Value>,
138    ) -> HashMap<String, serde_json::Value> {
139        if let Some(serde_json::Value::Object(map)) = variables {
140            map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
141        } else {
142            HashMap::new()
143        }
144    }
145
146    /// Extract field names from selections (for backward compatibility).
147    fn extract_field_names(&self, selections: &[FieldSelection]) -> Vec<String> {
148        selections.iter().map(|s| s.name.clone()).collect()
149    }
150
151    /// Extract arguments from variables.
152    fn extract_arguments(
153        &self,
154        variables: Option<&serde_json::Value>,
155    ) -> HashMap<String, serde_json::Value> {
156        if let Some(serde_json::Value::Object(map)) = variables {
157            map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
158        } else {
159            HashMap::new()
160        }
161    }
162
163    /// Get the compiled schema.
164    #[must_use]
165    pub const fn schema(&self) -> &CompiledSchema {
166        &self.schema
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::schema::{CompiledSchema, QueryDefinition};
174
175    fn test_schema() -> CompiledSchema {
176        let mut schema = CompiledSchema::new();
177        schema.queries.push(QueryDefinition {
178            name:         "users".to_string(),
179            return_type:  "User".to_string(),
180            returns_list: true,
181            nullable:     false,
182            arguments:    Vec::new(),
183            sql_source:   Some("v_user".to_string()),
184            description:  None,
185            auto_params:  crate::schema::AutoParams::default(),
186            deprecation:  None,
187            jsonb_column: "data".to_string(),
188        });
189        schema
190    }
191
192    #[test]
193    fn test_matcher_new() {
194        let schema = test_schema();
195        let matcher = QueryMatcher::new(schema.clone());
196        assert_eq!(matcher.schema().queries.len(), 1);
197    }
198
199    #[test]
200    fn test_match_simple_query() {
201        let schema = test_schema();
202        let matcher = QueryMatcher::new(schema);
203
204        let query = "{ users { id name } }";
205        let result = matcher.match_query(query, None).unwrap();
206
207        assert_eq!(result.query_def.name, "users");
208        assert_eq!(result.fields.len(), 1); // "users" is the root field
209        assert!(result.selections[0].nested_fields.len() >= 2); // id, name
210    }
211
212    #[test]
213    fn test_match_query_with_operation_name() {
214        let schema = test_schema();
215        let matcher = QueryMatcher::new(schema);
216
217        let query = "query GetUsers { users { id name } }";
218        let result = matcher.match_query(query, None).unwrap();
219
220        assert_eq!(result.query_def.name, "users");
221        assert_eq!(result.operation_name, Some("GetUsers".to_string()));
222    }
223
224    #[test]
225    fn test_match_query_with_fragment() {
226        let schema = test_schema();
227        let matcher = QueryMatcher::new(schema);
228
229        let query = r"
230            fragment UserFields on User {
231                id
232                name
233            }
234            query { users { ...UserFields } }
235        ";
236        let result = matcher.match_query(query, None).unwrap();
237
238        assert_eq!(result.query_def.name, "users");
239        // Fragment should be resolved - nested fields should contain id, name
240        let root_selection = &result.selections[0];
241        assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
242        assert!(root_selection.nested_fields.iter().any(|f| f.name == "name"));
243    }
244
245    #[test]
246    fn test_match_query_with_skip_directive() {
247        let schema = test_schema();
248        let matcher = QueryMatcher::new(schema);
249
250        let query = r"{ users { id name @skip(if: true) } }";
251        let result = matcher.match_query(query, None).unwrap();
252
253        assert_eq!(result.query_def.name, "users");
254        // "name" should be skipped due to @skip(if: true)
255        let root_selection = &result.selections[0];
256        assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
257        assert!(!root_selection.nested_fields.iter().any(|f| f.name == "name"));
258    }
259
260    #[test]
261    fn test_match_query_with_include_directive_variable() {
262        let schema = test_schema();
263        let matcher = QueryMatcher::new(schema);
264
265        let query =
266            r"query($includeEmail: Boolean!) { users { id email @include(if: $includeEmail) } }";
267        let variables = serde_json::json!({ "includeEmail": false });
268        let result = matcher.match_query(query, Some(&variables)).unwrap();
269
270        assert_eq!(result.query_def.name, "users");
271        // "email" should be excluded because $includeEmail is false
272        let root_selection = &result.selections[0];
273        assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
274        assert!(!root_selection.nested_fields.iter().any(|f| f.name == "email"));
275    }
276
277    #[test]
278    fn test_match_query_unknown_query() {
279        let schema = test_schema();
280        let matcher = QueryMatcher::new(schema);
281
282        let query = "{ unknown { id } }";
283        let result = matcher.match_query(query, None);
284
285        assert!(result.is_err());
286    }
287
288    #[test]
289    fn test_extract_arguments_none() {
290        let schema = test_schema();
291        let matcher = QueryMatcher::new(schema);
292
293        let args = matcher.extract_arguments(None);
294        assert!(args.is_empty());
295    }
296
297    #[test]
298    fn test_extract_arguments_some() {
299        let schema = test_schema();
300        let matcher = QueryMatcher::new(schema);
301
302        let variables = serde_json::json!({
303            "id": "123",
304            "limit": 10
305        });
306
307        let args = matcher.extract_arguments(Some(&variables));
308        assert_eq!(args.len(), 2);
309        assert_eq!(args.get("id"), Some(&serde_json::json!("123")));
310        assert_eq!(args.get("limit"), Some(&serde_json::json!(10)));
311    }
312}