1use super::matcher::QueryMatch;
4use crate::{
5 error::Result,
6 graphql::FieldSelection,
7 runtime::{JsonbOptimizationOptions, JsonbStrategy},
8};
9
10#[derive(Debug, Clone)]
12pub struct ExecutionPlan {
13 pub sql: String,
15
16 pub parameters: Vec<(String, serde_json::Value)>,
18
19 pub is_cached: bool,
21
22 pub estimated_cost: usize,
24
25 pub projection_fields: Vec<String>,
27
28 pub jsonb_strategy: JsonbStrategy,
30}
31
32pub struct QueryPlanner {
34 cache_enabled: bool,
36
37 jsonb_options: JsonbOptimizationOptions,
39}
40
41impl QueryPlanner {
42 #[must_use]
44 pub fn new(cache_enabled: bool) -> Self {
45 Self::with_jsonb_options(cache_enabled, JsonbOptimizationOptions::default())
46 }
47
48 #[must_use]
50 pub fn with_jsonb_options(
51 cache_enabled: bool,
52 jsonb_options: JsonbOptimizationOptions,
53 ) -> Self {
54 Self {
55 cache_enabled,
56 jsonb_options,
57 }
58 }
59
60 pub fn plan(&self, query_match: &QueryMatch) -> Result<ExecutionPlan> {
82 let sql = self.generate_sql(query_match);
87 let parameters = self.extract_parameters(query_match);
88
89 let projection_fields = self.extract_projection_fields(&query_match.selections);
92
93 let jsonb_strategy = self.choose_jsonb_strategy(&projection_fields);
95
96 Ok(ExecutionPlan {
97 sql,
98 parameters,
99 is_cached: false,
100 estimated_cost: self.estimate_cost(query_match),
101 projection_fields,
102 jsonb_strategy,
103 })
104 }
105
106 fn choose_jsonb_strategy(&self, projection_fields: &[String]) -> JsonbStrategy {
108 let estimated_total_fields = projection_fields.len().max(10); self.jsonb_options
112 .choose_strategy(projection_fields.len(), estimated_total_fields)
113 }
114
115 fn extract_projection_fields(&self, selections: &[FieldSelection]) -> Vec<String> {
119 if let Some(root_selection) = selections.first() {
121 root_selection
122 .nested_fields
123 .iter()
124 .map(|f| f.response_key().to_string())
125 .collect()
126 } else {
127 Vec::new()
128 }
129 }
130
131 fn generate_sql(&self, query_match: &QueryMatch) -> String {
133 let table = query_match.query_def.sql_source.as_ref().map_or("unknown", String::as_str);
135
136 let fields_sql = if query_match.fields.is_empty() {
138 "data".to_string()
139 } else {
140 "data".to_string()
142 };
143
144 format!("SELECT {fields_sql} FROM {table}")
145 }
146
147 fn extract_parameters(&self, query_match: &QueryMatch) -> Vec<(String, serde_json::Value)> {
149 query_match.arguments.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
150 }
151
152 fn estimate_cost(&self, query_match: &QueryMatch) -> usize {
154 let base_cost = 100;
156 let field_cost = query_match.fields.len() * 10;
157 let arg_cost = query_match.arguments.len() * 5;
158
159 base_cost + field_cost + arg_cost
160 }
161
162 #[must_use]
164 pub const fn cache_enabled(&self) -> bool {
165 self.cache_enabled
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use std::collections::HashMap;
172
173 use super::*;
174 use crate::{
175 graphql::{FieldSelection, ParsedQuery},
176 schema::{AutoParams, QueryDefinition},
177 };
178
179 fn test_query_match() -> QueryMatch {
180 QueryMatch {
181 query_def: QueryDefinition {
182 name: "users".to_string(),
183 return_type: "User".to_string(),
184 returns_list: true,
185 nullable: false,
186 arguments: Vec::new(),
187 sql_source: Some("v_user".to_string()),
188 description: None,
189 auto_params: AutoParams::default(),
190 deprecation: None,
191 jsonb_column: "data".to_string(),
192 },
193 fields: vec!["id".to_string(), "name".to_string()],
194 selections: vec![FieldSelection {
195 name: "users".to_string(),
196 alias: None,
197 arguments: vec![],
198 nested_fields: vec![
199 FieldSelection {
200 name: "id".to_string(),
201 alias: None,
202 arguments: vec![],
203 nested_fields: vec![],
204 directives: vec![],
205 },
206 FieldSelection {
207 name: "name".to_string(),
208 alias: None,
209 arguments: vec![],
210 nested_fields: vec![],
211 directives: vec![],
212 },
213 ],
214 directives: vec![],
215 }],
216 arguments: HashMap::new(),
217 operation_name: Some("users".to_string()),
218 parsed_query: ParsedQuery {
219 operation_type: "query".to_string(),
220 operation_name: Some("users".to_string()),
221 root_field: "users".to_string(),
222 selections: vec![],
223 variables: vec![],
224 fragments: vec![],
225 source: "{ users { id name } }".to_string(),
226 },
227 }
228 }
229
230 #[test]
231 fn test_planner_new() {
232 let planner = QueryPlanner::new(true);
233 assert!(planner.cache_enabled());
234
235 let planner = QueryPlanner::new(false);
236 assert!(!planner.cache_enabled());
237 }
238
239 #[test]
240 fn test_generate_sql() {
241 let planner = QueryPlanner::new(true);
242 let query_match = test_query_match();
243
244 let sql = planner.generate_sql(&query_match);
245 assert_eq!(sql, "SELECT data FROM v_user");
246 }
247
248 #[test]
249 fn test_extract_parameters() {
250 let planner = QueryPlanner::new(true);
251 let mut query_match = test_query_match();
252 query_match.arguments.insert("id".to_string(), serde_json::json!("123"));
253 query_match.arguments.insert("limit".to_string(), serde_json::json!(10));
254
255 let params = planner.extract_parameters(&query_match);
256 assert_eq!(params.len(), 2);
257 }
258
259 #[test]
260 fn test_estimate_cost() {
261 let planner = QueryPlanner::new(true);
262 let query_match = test_query_match();
263
264 let cost = planner.estimate_cost(&query_match);
265 assert_eq!(cost, 120);
267 }
268
269 #[test]
270 fn test_plan() {
271 let planner = QueryPlanner::new(true);
272 let query_match = test_query_match();
273
274 let plan = planner.plan(&query_match).unwrap();
275 assert!(!plan.sql.is_empty());
276 assert_eq!(plan.projection_fields.len(), 2);
277 assert!(!plan.is_cached);
278 assert_eq!(plan.estimated_cost, 120);
279 assert_eq!(plan.jsonb_strategy, JsonbStrategy::Project);
280 }
281
282 #[test]
287 fn test_plan_includes_jsonb_strategy() {
288 let planner = QueryPlanner::new(true);
289 let query_match = test_query_match();
290
291 let plan = planner.plan(&query_match).unwrap();
292 assert_eq!(plan.jsonb_strategy, JsonbStrategy::Project);
294 }
295
296 #[test]
297 fn test_planner_with_custom_jsonb_options() {
298 let custom_options = JsonbOptimizationOptions {
299 default_strategy: JsonbStrategy::Stream,
300 auto_threshold_percent: 50,
301 };
302 let planner = QueryPlanner::with_jsonb_options(true, custom_options);
303 let query_match = test_query_match();
304
305 let plan = planner.plan(&query_match).unwrap();
306 assert_eq!(plan.jsonb_strategy, JsonbStrategy::Stream);
308 }
309
310 #[test]
311 fn test_choose_jsonb_strategy_below_threshold() {
312 let options = JsonbOptimizationOptions {
313 default_strategy: JsonbStrategy::Project,
314 auto_threshold_percent: 80,
315 };
316 let planner = QueryPlanner::with_jsonb_options(true, options);
317
318 let strategy = planner.choose_jsonb_strategy(&["id".to_string(), "name".to_string()]);
320 assert_eq!(strategy, JsonbStrategy::Project);
321 }
322
323 #[test]
324 fn test_choose_jsonb_strategy_at_threshold() {
325 let options = JsonbOptimizationOptions {
326 default_strategy: JsonbStrategy::Project,
327 auto_threshold_percent: 80,
328 };
329 let planner = QueryPlanner::with_jsonb_options(true, options);
330
331 let many_fields = (0..9).map(|i| format!("field_{}", i)).collect::<Vec<_>>();
333 let strategy = planner.choose_jsonb_strategy(&many_fields);
334 assert_eq!(strategy, JsonbStrategy::Stream);
335 }
336
337 #[test]
338 fn test_choose_jsonb_strategy_respects_default() {
339 let options = JsonbOptimizationOptions {
340 default_strategy: JsonbStrategy::Stream,
341 auto_threshold_percent: 80,
342 };
343 let planner = QueryPlanner::with_jsonb_options(true, options);
344
345 let strategy = planner.choose_jsonb_strategy(&["id".to_string()]);
347 assert_eq!(strategy, JsonbStrategy::Stream);
348 }
349}