1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! Query classification — determines operation type for routing.
use std::collections::HashMap;
use super::super::{Executor, QueryType};
use crate::{
db::traits::DatabaseAdapter,
error::{FraiseQLError, Result},
graphql::parse_query,
};
impl<A: DatabaseAdapter> Executor<A> {
/// Classify a GraphQL query into its operation type for routing.
///
/// This is the first phase of query execution. It determines which handler
/// to invoke based on the query structure and conventions:
///
/// - **Introspection** (`__schema`, `__type`) → Uses pre-built responses (zero-cost)
/// - **Federation** (`_service`, `_entities`) → Fed-specific logic
/// - **Relay node** (`node(id: "...")`) → Global ID lookup
/// - **Mutations** (`mutation { ... }`) → Write operations
/// - **Aggregates** (root field ends with `_aggregate`) → Analytics queries
/// - **Windows** (root field ends with `_window`) → Time-series queries
/// - **Regular** (default) → Standard field selections
///
/// # Errors
///
/// Returns `FraiseQLError::Parse` if the query string is malformed GraphQL.
///
/// # Example
///
/// ```text
/// // Illustrative — classify_query() is internal.
/// // Use executor.execute(query, None).await? for the public API.
///
/// // Regular query → Regular
/// // Mutation → Mutation → execute_mutation_query()
/// // __schema → Introspection
/// // _entities → Federation
/// ```
pub(in crate::runtime::executor) fn classify_query(&self, query: &str) -> Result<QueryType> {
self.classify_query_with_parse(query).map(|(qt, _)| qt)
}
/// Classify a query and simultaneously return the parsed AST for `Regular`
/// queries, avoiding a redundant parse in the multi-root pipeline path.
///
/// Returns `(QueryType, Some(ParsedQuery))` for `Regular` queries and
/// `(QueryType, None)` for all other types (introspection, federation, etc.).
///
/// # Errors
///
/// Returns [`FraiseQLError::Parse`] if the query string is malformed GraphQL.
pub(in crate::runtime::executor) fn classify_query_with_parse(
&self,
query: &str,
) -> Result<(QueryType, Option<crate::graphql::ParsedQuery>)> {
// Parse the query once; the AST is the canonical source of truth.
// Substring scans on the raw string produce false-positives on aliases,
// comments, and string argument values (e.g. `{ search(q: "_service") }`
// would be mis-routed as a federation query by a text scan).
let parsed = parse_query(query).map_err(|e| FraiseQLError::Parse {
message: e.to_string(),
location: "query".to_string(),
})?;
let root_field = &parsed.root_field;
// Introspection (highest priority): `__schema` or `__type`.
// These are meta-fields defined by the GraphQL spec — always a root query.
if root_field == "__schema" {
return Ok((QueryType::IntrospectionSchema, None));
}
if root_field == "__type" {
let type_name = extract_root_string_arg(&parsed, "name");
return Ok((QueryType::IntrospectionType(type_name.unwrap_or_default()), None));
}
// Federation (Apollo Federation v1/v2 entry-points).
if root_field == "_service" || root_field == "_entities" {
return Ok((QueryType::Federation(root_field.clone()), None));
}
// Relay global node lookup: root field is exactly "node" on a query.
// Extract selections from inline fragments (... on TypeName { fields })
// so the execution layer can project only requested fields.
if parsed.operation_type == "query" && root_field == "node" {
let selections = parsed
.selections
.first()
.map(|node_sel| {
// Flatten inline fragments: `node { ... on Booking { id startDate } }`
// Inline fragments are represented as FieldSelection with name "...on TypeName"
let mut fields = Vec::new();
for sel in &node_sel.nested_fields {
if sel.name.starts_with("...") {
// Inline fragment — lift its nested_fields up
fields.extend(sel.nested_fields.clone());
} else {
fields.push(sel.clone());
}
}
fields
})
.unwrap_or_default();
return Ok((QueryType::NodeQuery { selections }, None));
}
// Mutations are routed by operation type.
if parsed.operation_type == "mutation" {
let type_selections: HashMap<String, Vec<String>> = parsed
.selections
.first()
.map(|s| {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
for f in &s.nested_fields {
if let Some(type_name) = f.name.strip_prefix("...on ") {
// Inline fragment: collect fields under the type name
let fields: Vec<String> = f
.nested_fields
.iter()
.map(|nf| nf.response_key().to_string())
.collect();
map.entry(type_name.to_string()).or_default().extend(fields);
} else {
// Common field (outside any inline fragment)
map.entry(String::new())
.or_default()
.push(f.response_key().to_string());
}
}
map
})
.unwrap_or_default();
return Ok((
QueryType::Mutation {
name: root_field.clone(),
type_selections,
},
None,
));
}
// Aggregate queries (root field ends with `_aggregate`).
if root_field.ends_with("_aggregate") {
return Ok((QueryType::Aggregate(root_field.clone()), None));
}
// Window queries (root field ends with `_window`).
if root_field.ends_with("_window") {
return Ok((QueryType::Window(root_field.clone()), None));
}
// Regular query — return the already-parsed AST to avoid re-parsing in
// the multi-root pipeline path.
Ok((QueryType::Regular, Some(parsed)))
}
}
/// Extract the value of a named string argument from the first (root) field of
/// a parsed query.
///
/// For `{ __type(name: "User") { ... } }`, calling `extract_root_string_arg(parsed, "name")`
/// returns `Some("User".to_string())`.
///
/// Returns `None` if the argument is absent or is not a JSON string literal.
fn extract_root_string_arg(parsed: &crate::graphql::ParsedQuery, arg_name: &str) -> Option<String> {
let root_field = parsed.selections.first()?;
let arg = root_field.arguments.iter().find(|a| a.name == arg_name)?;
// value_json is serialized as `"TypeName"` (with surrounding quotes) for
// string values. Strip the outer quotes to get the raw string.
let json = &arg.value_json;
if json.starts_with('"') && json.ends_with('"') && json.len() >= 2 {
Some(json[1..json.len() - 1].replace("\\\"", "\""))
} else {
None
}
}