fraiseql_server/routes/api/
query.rs1use axum::{Json, extract::State};
9use fraiseql_core::db::traits::DatabaseAdapter;
10use serde::{Deserialize, Serialize};
11
12use crate::routes::{
13 api::types::{ApiError, ApiResponse},
14 graphql::AppState,
15};
16
17#[derive(Debug, Deserialize)]
19pub struct ExplainRequest {
20 pub query: String,
22}
23
24#[derive(Debug, Serialize)]
26pub struct ExplainResponse {
27 pub query: String,
29 pub sql: Option<String>,
31 pub complexity: ComplexityInfo,
33 pub warnings: Vec<String>,
35 pub estimated_cost: usize,
37}
38
39#[derive(Debug, Serialize, Clone, Copy)]
41pub struct ComplexityInfo {
42 pub depth: usize,
44 pub field_count: usize,
46 pub score: usize,
48}
49
50#[derive(Debug, Deserialize)]
52pub struct ValidateRequest {
53 pub query: String,
55}
56
57#[derive(Debug, Serialize)]
59pub struct ValidateResponse {
60 pub valid: bool,
62 pub errors: Vec<String>,
64}
65
66#[derive(Debug, Serialize)]
68pub struct StatsResponse {
69 pub total_queries: usize,
71 pub successful_queries: usize,
73 pub failed_queries: usize,
75 pub average_latency_ms: f64,
77}
78
79pub async fn explain_handler<A: DatabaseAdapter>(
89 State(_state): State<AppState<A>>,
90 Json(req): Json<ExplainRequest>,
91) -> Result<Json<ApiResponse<ExplainResponse>>, ApiError> {
92 if req.query.trim().is_empty() {
94 return Err(ApiError::validation_error("Query cannot be empty"));
95 }
96
97 let complexity = calculate_complexity(&req.query);
99
100 let warnings = generate_warnings(&complexity);
102
103 let sql = generate_mock_sql(&req.query);
105
106 let response = ExplainResponse {
107 query: req.query,
108 sql,
109 complexity,
110 warnings,
111 estimated_cost: estimate_cost(&complexity),
112 };
113
114 Ok(Json(ApiResponse {
115 status: "success".to_string(),
116 data: response,
117 }))
118}
119
120pub async fn validate_handler<A: DatabaseAdapter>(
125 State(_state): State<AppState<A>>,
126 Json(req): Json<ValidateRequest>,
127) -> Result<Json<ApiResponse<ValidateResponse>>, ApiError> {
128 if req.query.trim().is_empty() {
129 return Ok(Json(ApiResponse {
130 status: "success".to_string(),
131 data: ValidateResponse {
132 valid: false,
133 errors: vec!["Query cannot be empty".to_string()],
134 },
135 }));
136 }
137
138 let errors = validate_query_syntax(&req.query);
140 let valid = errors.is_empty();
141
142 let response = ValidateResponse { valid, errors };
143
144 Ok(Json(ApiResponse {
145 status: "success".to_string(),
146 data: response,
147 }))
148}
149
150pub async fn stats_handler<A: DatabaseAdapter>(
164 State(state): State<AppState<A>>,
165) -> Result<Json<ApiResponse<StatsResponse>>, ApiError> {
166 let total_queries = state.metrics.queries_total.load(std::sync::atomic::Ordering::Relaxed);
168 let successful_queries =
169 state.metrics.queries_success.load(std::sync::atomic::Ordering::Relaxed);
170 let failed_queries = state.metrics.queries_error.load(std::sync::atomic::Ordering::Relaxed);
171 let total_duration_us =
172 state.metrics.queries_duration_us.load(std::sync::atomic::Ordering::Relaxed);
173
174 let average_latency_ms = if total_queries > 0 {
176 (total_duration_us as f64 / total_queries as f64) / 1000.0
177 } else {
178 0.0
179 };
180
181 let response = StatsResponse {
182 total_queries: total_queries as usize,
183 successful_queries: successful_queries as usize,
184 failed_queries: failed_queries as usize,
185 average_latency_ms,
186 };
187
188 Ok(Json(ApiResponse {
189 status: "success".to_string(),
190 data: response,
191 }))
192}
193
194fn calculate_complexity(query: &str) -> ComplexityInfo {
198 let depth = calculate_depth(query);
199 let field_count = count_fields(query);
200 let score = depth.saturating_mul(field_count);
201
202 ComplexityInfo {
203 depth,
204 field_count,
205 score,
206 }
207}
208
209fn calculate_depth(query: &str) -> usize {
211 let mut max_depth = 0;
212 let mut current_depth = 0;
213
214 for ch in query.chars() {
215 match ch {
216 '{' => {
217 current_depth += 1;
218 max_depth = max_depth.max(current_depth);
219 },
220 '}' => {
221 if current_depth > 0 {
222 current_depth -= 1;
223 }
224 },
225 _ => {},
226 }
227 }
228
229 max_depth
230}
231
232fn count_fields(query: &str) -> usize {
235 let mut count = 1; let mut in_braces = 0;
237
238 for ch in query.chars() {
239 match ch {
240 '{' => in_braces += 1,
241 '}' => {
242 if in_braces > 0 {
243 in_braces -= 1;
244 }
245 },
246 ',' => {
247 if in_braces > 0 {
248 count += 1;
249 }
250 },
251 '\n' if in_braces > 0 => {
252 if !query.contains(',') {
254 count += 1;
255 }
256 },
257 _ => {},
258 }
259 }
260
261 count.max(1)
262}
263
264fn generate_warnings(complexity: &ComplexityInfo) -> Vec<String> {
266 let mut warnings = vec![];
267
268 if complexity.depth > 10 {
270 warnings.push(format!(
271 "Query nesting depth is {} (threshold: 10). Consider using aliases or fragments.",
272 complexity.depth
273 ));
274 }
275
276 if complexity.score > 500 {
278 warnings.push(format!(
279 "Query complexity score is {} (threshold: 500). This may take longer to execute.",
280 complexity.score
281 ));
282 }
283
284 if complexity.field_count > 50 {
286 warnings.push(format!(
287 "Query requests {} fields (threshold: 50). Consider requesting only necessary fields.",
288 complexity.field_count
289 ));
290 }
291
292 warnings
293}
294
295fn estimate_cost(complexity: &ComplexityInfo) -> usize {
297 let base_cost = 50;
299 let depth_cost = complexity.depth.saturating_mul(10);
300 let field_cost = complexity.field_count.saturating_mul(5);
301
302 base_cost + depth_cost + field_cost
303}
304
305fn generate_mock_sql(_query: &str) -> Option<String> {
308 Some("SELECT * FROM generated_view".to_string())
314}
315
316fn validate_query_syntax(query: &str) -> Vec<String> {
319 let mut errors = vec![];
320
321 if !query.contains('{') || !query.contains('}') {
323 errors.push("Query must contain opening and closing braces".to_string());
324 }
325
326 let open_braces = query.matches('{').count();
328 let close_braces = query.matches('}').count();
329 if open_braces != close_braces {
330 errors
331 .push(format!("Mismatched braces: {} opening, {} closing", open_braces, close_braces));
332 }
333
334 let has_operation =
336 query.contains("query") || query.contains("mutation") || query.contains("subscription");
337
338 if !has_operation {
339 errors.push("Query must contain query, mutation, or subscription operation".to_string());
340 }
341
342 errors
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_calculate_depth_simple() {
351 let depth = calculate_depth("query { users { id } }");
352 assert_eq!(depth, 2);
353 }
354
355 #[test]
356 fn test_calculate_depth_nested() {
357 let depth = calculate_depth("query { users { posts { comments { text } } } }");
358 assert_eq!(depth, 4);
359 }
360
361 #[test]
362 fn test_count_fields_single() {
363 let count = count_fields("query { users { id } }");
364 assert!(count >= 1);
365 }
366
367 #[test]
368 fn test_generate_warnings_deep() {
369 let complexity = ComplexityInfo {
370 depth: 15,
371 field_count: 5,
372 score: 75,
373 };
374 let warnings = generate_warnings(&complexity);
375 assert!(!warnings.is_empty());
376 assert!(warnings[0].contains("depth"));
377 }
378
379 #[test]
380 fn test_generate_warnings_high_score() {
381 let complexity = ComplexityInfo {
382 depth: 3,
383 field_count: 200,
384 score: 600,
385 };
386 let warnings = generate_warnings(&complexity);
387 assert!(!warnings.is_empty());
388 assert!(warnings.iter().any(|w| w.contains("complexity")));
389 }
390
391 #[test]
392 fn test_estimate_cost() {
393 let complexity = ComplexityInfo {
394 depth: 2,
395 field_count: 3,
396 score: 6,
397 };
398 let cost = estimate_cost(&complexity);
399 assert!(cost > 0);
400 }
401
402 #[test]
403 fn test_validate_empty_query() {
404 let errors = validate_query_syntax("");
405 assert!(!errors.is_empty());
406 }
407
408 #[test]
409 fn test_validate_mismatched_braces() {
410 let errors = validate_query_syntax("query { users { id }");
411 assert!(!errors.is_empty());
412 assert!(errors[0].contains("Mismatched"));
413 }
414
415 #[test]
416 fn test_validate_valid_query() {
417 let errors = validate_query_syntax("query { users { id } }");
418 assert!(errors.is_empty());
419 }
420
421 #[test]
422 fn test_stats_response_structure() {
423 let response = StatsResponse {
425 total_queries: 100,
426 successful_queries: 95,
427 failed_queries: 5,
428 average_latency_ms: 42.5,
429 };
430
431 assert_eq!(response.total_queries, 100);
432 assert_eq!(response.successful_queries, 95);
433 assert_eq!(response.failed_queries, 5);
434 assert!(response.average_latency_ms > 0.0);
435 }
436
437 #[test]
438 fn test_explain_response_structure() {
439 let response = ExplainResponse {
441 query: "query { users { id } }".to_string(),
442 sql: Some("SELECT id FROM users".to_string()),
443 complexity: ComplexityInfo {
444 depth: 2,
445 field_count: 1,
446 score: 2,
447 },
448 warnings: vec![],
449 estimated_cost: 50,
450 };
451
452 assert!(!response.query.is_empty());
453 assert!(response.sql.is_some());
454 assert_eq!(response.complexity.depth, 2);
455 assert_eq!(response.estimated_cost, 50);
456 }
457
458 #[test]
459 fn test_complexity_info_score_calculation() {
460 let complexity = ComplexityInfo {
462 depth: 3,
463 field_count: 4,
464 score: 12,
465 };
466
467 assert_eq!(complexity.score, 3 * 4);
468 }
469
470 #[test]
471 fn test_validate_request_structure() {
472 let request = ValidateRequest {
473 query: "query { users { id } }".to_string(),
474 };
475
476 assert!(!request.query.is_empty());
477 }
478
479 #[test]
480 fn test_explain_request_structure() {
481 let request = ExplainRequest {
482 query: "query { users { id } }".to_string(),
483 };
484
485 assert!(!request.query.is_empty());
486 }
487}