Skip to main content

heliosdb_proxy/graphql/
engine.rs

1//! GraphQL Engine
2//!
3//! Main entry point for GraphQL query execution.
4
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::Instant;
8
9use super::sql_generator::FilterOperator;
10use super::{
11    ErrorCode, ExecutionContext, Filter, GraphQLConfig, GraphQLMetrics, GraphQLSchema,
12    OperationType, QueryPlan, QueryValidator, Selection, SqlGenerator,
13};
14
15/// GraphQL request
16#[derive(Debug, Clone)]
17pub struct GraphQLRequest {
18    /// GraphQL query string
19    pub query: String,
20    /// Operation name (for multi-operation documents)
21    pub operation_name: Option<String>,
22    /// Query variables
23    pub variables: Option<HashMap<String, serde_json::Value>>,
24    /// Extensions
25    pub extensions: Option<HashMap<String, serde_json::Value>>,
26}
27
28impl GraphQLRequest {
29    /// Create a new GraphQL request
30    pub fn new(query: impl Into<String>) -> Self {
31        Self {
32            query: query.into(),
33            operation_name: None,
34            variables: None,
35            extensions: None,
36        }
37    }
38
39    /// Set operation name
40    pub fn with_operation(mut self, name: impl Into<String>) -> Self {
41        self.operation_name = Some(name.into());
42        self
43    }
44
45    /// Set variables
46    pub fn with_variables(mut self, vars: HashMap<String, serde_json::Value>) -> Self {
47        self.variables = Some(vars);
48        self
49    }
50
51    /// Add a variable
52    pub fn var(mut self, name: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
53        let vars = self.variables.get_or_insert_with(HashMap::new);
54        vars.insert(name.into(), value.into());
55        self
56    }
57}
58
59/// GraphQL response
60#[derive(Debug, Clone)]
61pub struct GraphQLResponse {
62    /// Response data
63    pub data: Option<serde_json::Value>,
64    /// Errors
65    pub errors: Option<Vec<GraphQLError>>,
66    /// Extensions (timing, tracing, etc.)
67    pub extensions: Option<HashMap<String, serde_json::Value>>,
68}
69
70impl GraphQLResponse {
71    /// Create a successful response
72    pub fn success(data: serde_json::Value) -> Self {
73        Self {
74            data: Some(data),
75            errors: None,
76            extensions: None,
77        }
78    }
79
80    /// Create an error response
81    pub fn error(error: GraphQLError) -> Self {
82        Self {
83            data: None,
84            errors: Some(vec![error]),
85            extensions: None,
86        }
87    }
88
89    /// Create a response with multiple errors
90    pub fn errors(errors: Vec<GraphQLError>) -> Self {
91        Self {
92            data: None,
93            errors: Some(errors),
94            extensions: None,
95        }
96    }
97
98    /// Add extension data
99    pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
100        let extensions = self.extensions.get_or_insert_with(HashMap::new);
101        extensions.insert(key.into(), value);
102        self
103    }
104
105    /// Check if the response has errors
106    pub fn has_errors(&self) -> bool {
107        self.errors.as_ref().map(|e| !e.is_empty()).unwrap_or(false)
108    }
109
110    /// Convert to JSON
111    pub fn to_json(&self) -> serde_json::Value {
112        let mut result = serde_json::Map::new();
113
114        if let Some(ref data) = self.data {
115            result.insert("data".to_string(), data.clone());
116        }
117
118        if let Some(ref errors) = self.errors {
119            let error_array: Vec<_> = errors.iter().map(|e| e.to_json()).collect();
120            result.insert("errors".to_string(), serde_json::Value::Array(error_array));
121        }
122
123        if let Some(ref extensions) = self.extensions {
124            result.insert(
125                "extensions".to_string(),
126                serde_json::Value::Object(
127                    extensions
128                        .iter()
129                        .map(|(k, v)| (k.clone(), v.clone()))
130                        .collect(),
131                ),
132            );
133        }
134
135        serde_json::Value::Object(result)
136    }
137}
138
139/// GraphQL error
140#[derive(Debug, Clone)]
141pub struct GraphQLError {
142    /// Error message
143    pub message: String,
144    /// Error locations in the query
145    pub locations: Option<Vec<ErrorLocation>>,
146    /// Path to the field that caused the error
147    pub path: Option<Vec<PathSegment>>,
148    /// Error extensions
149    pub extensions: Option<HashMap<String, serde_json::Value>>,
150    /// Error code
151    pub code: ErrorCode,
152}
153
154impl GraphQLError {
155    /// Create a new error
156    pub fn new(message: impl Into<String>, code: ErrorCode) -> Self {
157        Self {
158            message: message.into(),
159            locations: None,
160            path: None,
161            extensions: None,
162            code,
163        }
164    }
165
166    /// Create a parse error
167    pub fn parse_error(message: impl Into<String>) -> Self {
168        Self::new(message, ErrorCode::ParseError)
169    }
170
171    /// Create a validation error
172    pub fn validation_error(message: impl Into<String>) -> Self {
173        Self::new(message, ErrorCode::ValidationError)
174    }
175
176    /// Create an authorization error
177    pub fn unauthorized(message: impl Into<String>) -> Self {
178        Self::new(message, ErrorCode::Unauthorized)
179    }
180
181    /// Create a not found error
182    pub fn not_found(message: impl Into<String>) -> Self {
183        Self::new(message, ErrorCode::NotFound)
184    }
185
186    /// Create an internal error
187    pub fn internal(message: impl Into<String>) -> Self {
188        Self::new(message, ErrorCode::InternalError)
189    }
190
191    /// Set location
192    pub fn with_location(mut self, line: u32, column: u32) -> Self {
193        self.locations = Some(vec![ErrorLocation { line, column }]);
194        self
195    }
196
197    /// Set path
198    pub fn with_path(mut self, path: Vec<PathSegment>) -> Self {
199        self.path = Some(path);
200        self
201    }
202
203    /// Add extension
204    pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
205        let extensions = self.extensions.get_or_insert_with(HashMap::new);
206        extensions.insert(key.into(), value);
207        self
208    }
209
210    /// Convert to JSON
211    pub fn to_json(&self) -> serde_json::Value {
212        let mut result = serde_json::Map::new();
213        result.insert(
214            "message".to_string(),
215            serde_json::Value::String(self.message.clone()),
216        );
217
218        if let Some(ref locations) = self.locations {
219            let loc_array: Vec<_> = locations
220                .iter()
221                .map(|l| {
222                    let mut loc = serde_json::Map::new();
223                    loc.insert("line".to_string(), serde_json::Value::Number(l.line.into()));
224                    loc.insert(
225                        "column".to_string(),
226                        serde_json::Value::Number(l.column.into()),
227                    );
228                    serde_json::Value::Object(loc)
229                })
230                .collect();
231            result.insert("locations".to_string(), serde_json::Value::Array(loc_array));
232        }
233
234        if let Some(ref path) = self.path {
235            let path_array: Vec<_> = path.iter().map(|s| s.to_json()).collect();
236            result.insert("path".to_string(), serde_json::Value::Array(path_array));
237        }
238
239        let mut extensions = self.extensions.clone().unwrap_or_default();
240        extensions.insert(
241            "code".to_string(),
242            serde_json::Value::String(format!("{:?}", self.code)),
243        );
244        result.insert(
245            "extensions".to_string(),
246            serde_json::Value::Object(extensions.into_iter().collect()),
247        );
248
249        serde_json::Value::Object(result)
250    }
251}
252
253impl std::fmt::Display for GraphQLError {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(f, "{}", self.message)
256    }
257}
258
259impl std::error::Error for GraphQLError {}
260
261/// Error location in the GraphQL document
262#[derive(Debug, Clone, Copy)]
263pub struct ErrorLocation {
264    /// Line number (1-based)
265    pub line: u32,
266    /// Column number (1-based)
267    pub column: u32,
268}
269
270/// Path segment (field name or array index)
271#[derive(Debug, Clone)]
272pub enum PathSegment {
273    /// Field name
274    Field(String),
275    /// Array index
276    Index(usize),
277}
278
279impl PathSegment {
280    /// Convert to JSON value
281    pub fn to_json(&self) -> serde_json::Value {
282        match self {
283            PathSegment::Field(name) => serde_json::Value::String(name.clone()),
284            PathSegment::Index(idx) => serde_json::Value::Number((*idx).into()),
285        }
286    }
287}
288
289/// Parsed GraphQL document
290#[derive(Debug, Clone)]
291pub struct ParsedDocument {
292    /// Operation type
293    pub operation_type: OperationType,
294    /// Operation name
295    pub operation_name: Option<String>,
296    /// Selection set
297    pub selections: Vec<ParsedSelection>,
298    /// Variable definitions
299    pub variable_definitions: Vec<VariableDefinition>,
300    /// Fragment definitions
301    pub fragments: HashMap<String, FragmentDefinition>,
302}
303
304/// Parsed field selection
305#[derive(Debug, Clone)]
306pub struct ParsedSelection {
307    /// Field name
308    pub name: String,
309    /// Alias
310    pub alias: Option<String>,
311    /// Arguments
312    pub arguments: HashMap<String, serde_json::Value>,
313    /// Nested selections
314    pub selections: Vec<ParsedSelection>,
315    /// Directives
316    pub directives: Vec<Directive>,
317}
318
319impl ParsedSelection {
320    /// Get the response key (alias or name)
321    pub fn response_key(&self) -> &str {
322        self.alias.as_deref().unwrap_or(&self.name)
323    }
324}
325
326/// Variable definition
327#[derive(Debug, Clone)]
328pub struct VariableDefinition {
329    /// Variable name (without $)
330    pub name: String,
331    /// Type string
332    pub var_type: String,
333    /// Default value
334    pub default_value: Option<serde_json::Value>,
335}
336
337/// Fragment definition
338#[derive(Debug, Clone)]
339pub struct FragmentDefinition {
340    /// Fragment name
341    pub name: String,
342    /// Type condition
343    pub type_condition: String,
344    /// Selections
345    pub selections: Vec<ParsedSelection>,
346}
347
348/// GraphQL directive
349#[derive(Debug, Clone)]
350pub struct Directive {
351    /// Directive name
352    pub name: String,
353    /// Arguments
354    pub arguments: HashMap<String, serde_json::Value>,
355}
356
357/// GraphQL Engine
358///
359/// Main entry point for GraphQL query execution.
360#[derive(Debug)]
361pub struct GraphQLEngine {
362    /// Configuration
363    config: Arc<GraphQLConfig>,
364    /// Schema
365    schema: Arc<GraphQLSchema>,
366    /// SQL generator
367    sql_generator: Arc<SqlGenerator>,
368    /// Query validator
369    validator: QueryValidator,
370    /// Metrics
371    metrics: Arc<GraphQLMetrics>,
372    /// Backend the generated SQL is executed against. `None` => the engine runs
373    /// in offline mode and returns empty results (used in tests).
374    backend: Option<crate::backend::BackendConfig>,
375}
376
377impl GraphQLEngine {
378    /// Create a new GraphQL engine
379    pub fn new(config: GraphQLConfig, schema: GraphQLSchema) -> Self {
380        let config = Arc::new(config);
381        let schema = Arc::new(schema);
382
383        Self {
384            sql_generator: Arc::new(SqlGenerator::new(schema.clone())),
385            validator: QueryValidator::new(config.clone()),
386            metrics: Arc::new(GraphQLMetrics::new()),
387            config,
388            schema,
389            backend: None,
390        }
391    }
392
393    /// Attach the backend the generated SQL runs against.
394    pub fn with_backend(mut self, backend: crate::backend::BackendConfig) -> Self {
395        self.backend = Some(backend);
396        self
397    }
398
399    /// Execute a GraphQL request
400    pub async fn execute(&self, request: GraphQLRequest) -> GraphQLResponse {
401        self.execute_with_context(request, ExecutionContext::default())
402            .await
403    }
404
405    /// Execute a GraphQL request with context
406    pub async fn execute_with_context(
407        &self,
408        request: GraphQLRequest,
409        context: ExecutionContext,
410    ) -> GraphQLResponse {
411        let start = Instant::now();
412
413        // 1. Parse the query
414        let document = match self.parse(&request.query) {
415            Ok(doc) => doc,
416            Err(e) => {
417                self.metrics.record_error(&e);
418                return GraphQLResponse::error(e);
419            }
420        };
421
422        // 2. Validate the query
423        if let Err(e) = self.validate(&document) {
424            self.metrics.record_error(&e);
425            return GraphQLResponse::error(e);
426        }
427
428        // 3. Check authorization
429        if let Err(e) = self.authorize(&document, &context) {
430            self.metrics.record_error(&e);
431            return GraphQLResponse::error(e);
432        }
433
434        // 4. Plan query execution
435        let plan = match self.plan(&document, &request.variables) {
436            Ok(p) => p,
437            Err(e) => {
438                self.metrics.record_error(&e);
439                return GraphQLResponse::error(e);
440            }
441        };
442
443        // 5. Generate SQL
444        let sql_queries = match self.sql_generator.generate(&plan) {
445            Ok(queries) => queries,
446            Err(e) => {
447                let error = GraphQLError::internal(format!("SQL generation failed: {}", e));
448                self.metrics.record_error(&error);
449                return GraphQLResponse::error(error);
450            }
451        };
452
453        // 6. Execute SQL (mock for now - would use actual database connection)
454        let results = match self.execute_queries(&sql_queries, &context).await {
455            Ok(r) => r,
456            Err(e) => {
457                self.metrics.record_error(&e);
458                return GraphQLResponse::error(e);
459            }
460        };
461
462        // 7. Shape response
463        let data = match self.shape_response(&document, &results) {
464            Ok(d) => d,
465            Err(e) => {
466                self.metrics.record_error(&e);
467                return GraphQLResponse::error(e);
468            }
469        };
470
471        let elapsed = start.elapsed();
472        self.metrics.record_query(elapsed, document.operation_type);
473
474        GraphQLResponse::success(data).with_extension(
475            "timing",
476            serde_json::json!({
477                "durationMs": elapsed.as_millis()
478            }),
479        )
480    }
481
482    /// Parse a GraphQL query string
483    #[allow(clippy::result_large_err)]
484    fn parse(&self, query: &str) -> Result<ParsedDocument, GraphQLError> {
485        // Simple parser for basic queries
486        // In production, would use a proper GraphQL parser
487        let query = query.trim();
488
489        // Detect operation type
490        let (operation_type, remaining) = if query.starts_with("mutation") {
491            (
492                OperationType::Mutation,
493                query.strip_prefix("mutation").unwrap_or(query),
494            )
495        } else if query.starts_with("subscription") {
496            (
497                OperationType::Subscription,
498                query.strip_prefix("subscription").unwrap_or(query),
499            )
500        } else if query.starts_with("query") {
501            (
502                OperationType::Query,
503                query.strip_prefix("query").unwrap_or(query),
504            )
505        } else if query.starts_with("{") {
506            (OperationType::Query, query)
507        } else {
508            return Err(GraphQLError::parse_error("Invalid query format"));
509        };
510
511        // Extract operation name if present
512        let remaining = remaining.trim();
513        let (operation_name, remaining) = if remaining.starts_with('{') {
514            (None, remaining)
515        } else if let Some(brace_pos) = remaining.find('{') {
516            let name_part = remaining[..brace_pos].trim();
517            // Handle variables in name (e.g., "GetUser($id: ID!)")
518            let name = name_part.split('(').next().unwrap_or(name_part).trim();
519            if name.is_empty() {
520                (None, &remaining[brace_pos..])
521            } else {
522                (Some(name.to_string()), &remaining[brace_pos..])
523            }
524        } else {
525            return Err(GraphQLError::parse_error("Missing selection set"));
526        };
527
528        // Parse selection set
529        let selections = self.parse_selection_set(remaining)?;
530
531        Ok(ParsedDocument {
532            operation_type,
533            operation_name,
534            selections,
535            variable_definitions: Vec::new(),
536            fragments: HashMap::new(),
537        })
538    }
539
540    /// Parse a selection set
541    #[allow(clippy::result_large_err)]
542    fn parse_selection_set(&self, input: &str) -> Result<Vec<ParsedSelection>, GraphQLError> {
543        let input = input.trim();
544
545        if !input.starts_with('{') {
546            return Err(GraphQLError::parse_error("Expected '{'"));
547        }
548
549        // Find matching closing brace
550        let mut depth = 0;
551        let mut end_pos = 0;
552        for (i, c) in input.chars().enumerate() {
553            match c {
554                '{' => depth += 1,
555                '}' => {
556                    depth -= 1;
557                    if depth == 0 {
558                        end_pos = i;
559                        break;
560                    }
561                }
562                _ => {}
563            }
564        }
565
566        if depth != 0 {
567            return Err(GraphQLError::parse_error("Unmatched braces"));
568        }
569
570        let inner = &input[1..end_pos].trim();
571        self.parse_fields(inner)
572    }
573
574    /// Parse fields in a selection set
575    #[allow(clippy::result_large_err)]
576    fn parse_fields(&self, input: &str) -> Result<Vec<ParsedSelection>, GraphQLError> {
577        let mut selections = Vec::new();
578        let mut current_pos = 0;
579        let chars: Vec<char> = input.chars().collect();
580
581        while current_pos < chars.len() {
582            // Skip whitespace
583            while current_pos < chars.len() && chars[current_pos].is_whitespace() {
584                current_pos += 1;
585            }
586
587            if current_pos >= chars.len() {
588                break;
589            }
590
591            // Parse field name (possibly with alias)
592            let field_start = current_pos;
593            while current_pos < chars.len()
594                && (chars[current_pos].is_alphanumeric() || chars[current_pos] == '_')
595            {
596                current_pos += 1;
597            }
598
599            if current_pos == field_start {
600                current_pos += 1;
601                continue;
602            }
603
604            let mut field_name: String = chars[field_start..current_pos].iter().collect();
605            let mut alias = None;
606
607            // Check for alias
608            while current_pos < chars.len() && chars[current_pos].is_whitespace() {
609                current_pos += 1;
610            }
611
612            if current_pos < chars.len() && chars[current_pos] == ':' {
613                alias = Some(field_name);
614                current_pos += 1;
615
616                // Skip whitespace
617                while current_pos < chars.len() && chars[current_pos].is_whitespace() {
618                    current_pos += 1;
619                }
620
621                // Parse actual field name
622                let name_start = current_pos;
623                while current_pos < chars.len()
624                    && (chars[current_pos].is_alphanumeric() || chars[current_pos] == '_')
625                {
626                    current_pos += 1;
627                }
628                field_name = chars[name_start..current_pos].iter().collect();
629            }
630
631            // Skip whitespace
632            while current_pos < chars.len() && chars[current_pos].is_whitespace() {
633                current_pos += 1;
634            }
635
636            // Parse arguments if present
637            let mut arguments = HashMap::new();
638            if current_pos < chars.len() && chars[current_pos] == '(' {
639                let args_start = current_pos;
640                let mut depth = 1;
641                current_pos += 1;
642
643                while current_pos < chars.len() && depth > 0 {
644                    match chars[current_pos] {
645                        '(' => depth += 1,
646                        ')' => depth -= 1,
647                        _ => {}
648                    }
649                    current_pos += 1;
650                }
651
652                let args_str: String = chars[args_start + 1..current_pos - 1].iter().collect();
653                arguments = self.parse_arguments(&args_str)?;
654            }
655
656            // Skip whitespace
657            while current_pos < chars.len() && chars[current_pos].is_whitespace() {
658                current_pos += 1;
659            }
660
661            // Parse nested selection set if present
662            let nested_selections = if current_pos < chars.len() && chars[current_pos] == '{' {
663                let nested_start = current_pos;
664                let mut depth = 1;
665                current_pos += 1;
666
667                while current_pos < chars.len() && depth > 0 {
668                    match chars[current_pos] {
669                        '{' => depth += 1,
670                        '}' => depth -= 1,
671                        _ => {}
672                    }
673                    current_pos += 1;
674                }
675
676                let nested_str: String = chars[nested_start..current_pos].iter().collect();
677                self.parse_selection_set(&nested_str)?
678            } else {
679                Vec::new()
680            };
681
682            selections.push(ParsedSelection {
683                name: field_name,
684                alias,
685                arguments,
686                selections: nested_selections,
687                directives: Vec::new(),
688            });
689        }
690
691        Ok(selections)
692    }
693
694    /// Parse arguments
695    #[allow(clippy::result_large_err)]
696    fn parse_arguments(
697        &self,
698        input: &str,
699    ) -> Result<HashMap<String, serde_json::Value>, GraphQLError> {
700        let mut arguments = HashMap::new();
701
702        for part in input.split(',') {
703            let part = part.trim();
704            if part.is_empty() {
705                continue;
706            }
707
708            if let Some(colon_pos) = part.find(':') {
709                let key = part[..colon_pos].trim().to_string();
710                let value_str = part[colon_pos + 1..].trim();
711
712                let value = self.parse_value(value_str)?;
713                arguments.insert(key, value);
714            }
715        }
716
717        Ok(arguments)
718    }
719
720    /// Parse a GraphQL value
721    #[allow(clippy::result_large_err)]
722    fn parse_value(&self, input: &str) -> Result<serde_json::Value, GraphQLError> {
723        let input = input.trim();
724
725        if input == "null" {
726            Ok(serde_json::Value::Null)
727        } else if input == "true" {
728            Ok(serde_json::Value::Bool(true))
729        } else if input == "false" {
730            Ok(serde_json::Value::Bool(false))
731        } else if input.starts_with('"') && input.ends_with('"') {
732            Ok(serde_json::Value::String(
733                input[1..input.len() - 1].to_string(),
734            ))
735        } else if let Ok(n) = input.parse::<i64>() {
736            Ok(serde_json::Value::Number(n.into()))
737        } else if let Ok(n) = input.parse::<f64>() {
738            Ok(serde_json::json!(n))
739        } else {
740            // Treat as enum value or variable reference
741            Ok(serde_json::Value::String(input.to_string()))
742        }
743    }
744
745    /// Validate a parsed document
746    #[allow(clippy::result_large_err)]
747    fn validate(&self, document: &ParsedDocument) -> Result<(), GraphQLError> {
748        self.validator.validate(document, &self.schema)
749    }
750
751    /// Check authorization
752    #[allow(clippy::result_large_err)]
753    fn authorize(
754        &self,
755        _document: &ParsedDocument,
756        _context: &ExecutionContext,
757    ) -> Result<(), GraphQLError> {
758        // Authorization checks would go here
759        // For now, allow all queries
760        Ok(())
761    }
762
763    /// Plan query execution
764    #[allow(clippy::result_large_err)]
765    fn plan(
766        &self,
767        document: &ParsedDocument,
768        _variables: &Option<HashMap<String, serde_json::Value>>,
769    ) -> Result<QueryPlan, GraphQLError> {
770        // Convert parsed document to query plan
771        let selections: Vec<_> = document
772            .selections
773            .iter()
774            .map(|s| self.selection_to_plan(s))
775            .collect();
776
777        Ok(QueryPlan::Multiple { plans: selections })
778    }
779
780    /// Convert a selection to a plan
781    fn selection_to_plan(&self, selection: &ParsedSelection) -> QueryPlan {
782        // Get filters from arguments
783        let filters = self.extract_filters(&selection.arguments);
784
785        // Get limit/offset from arguments
786        let limit = selection
787            .arguments
788            .get("limit")
789            .and_then(|v| v.as_u64())
790            .map(|v| v as u32);
791        let offset = selection
792            .arguments
793            .get("offset")
794            .and_then(|v| v.as_u64())
795            .map(|v| v as u32);
796
797        // Build selection
798        let sel = Selection {
799            table_name: super::to_snake_case(&selection.name),
800            fields: selection
801                .selections
802                .iter()
803                .filter(|s| s.selections.is_empty())
804                .map(|s| s.name.clone())
805                .collect(),
806            relationships: selection
807                .selections
808                .iter()
809                .filter(|s| !s.selections.is_empty())
810                .map(|s| (s.name.clone(), self.selection_to_plan(s)))
811                .collect(),
812        };
813
814        QueryPlan::Single {
815            selection: sel,
816            filters,
817            limit,
818            offset,
819        }
820    }
821
822    /// Extract filters from arguments
823    fn extract_filters(&self, arguments: &HashMap<String, serde_json::Value>) -> Vec<Filter> {
824        let mut filters = Vec::new();
825
826        // Handle 'id' argument
827        if let Some(id) = arguments.get("id") {
828            filters.push(Filter {
829                field: "id".to_string(),
830                operator: FilterOperator::Eq,
831                value: id.clone(),
832            });
833        }
834
835        // Handle 'where' argument
836        if let Some(where_obj) = arguments.get("where") {
837            if let Some(obj) = where_obj.as_object() {
838                for (field, condition) in obj {
839                    if let Some(cond_obj) = condition.as_object() {
840                        for (op, value) in cond_obj {
841                            let operator = match op.as_str() {
842                                "eq" => FilterOperator::Eq,
843                                "ne" => FilterOperator::Ne,
844                                "gt" => FilterOperator::Gt,
845                                "gte" => FilterOperator::Gte,
846                                "lt" => FilterOperator::Lt,
847                                "lte" => FilterOperator::Lte,
848                                "contains" => FilterOperator::Contains,
849                                "startsWith" => FilterOperator::StartsWith,
850                                "endsWith" => FilterOperator::EndsWith,
851                                "in" => FilterOperator::In,
852                                _ => continue,
853                            };
854
855                            filters.push(Filter {
856                                field: field.clone(),
857                                operator,
858                                value: value.clone(),
859                            });
860                        }
861                    }
862                }
863            }
864        }
865
866        filters
867    }
868
869    /// Execute the generated SQL against the configured backend, returning one
870    /// vector of row-objects (`{column: value}`) per query. With no backend
871    /// attached the engine returns empty result sets.
872    async fn execute_queries(
873        &self,
874        queries: &[super::SqlQuery],
875        _context: &ExecutionContext,
876    ) -> Result<Vec<Vec<serde_json::Value>>, GraphQLError> {
877        let Some(bcfg) = self.backend.clone() else {
878            return Ok(queries.iter().map(|_| Vec::new()).collect());
879        };
880        use crate::backend::BackendClient;
881
882        let mut out = Vec::with_capacity(queries.len());
883        for q in queries {
884            let mut client = BackendClient::connect(&bcfg)
885                .await
886                .map_err(|e| GraphQLError::internal(format!("backend connect: {}", e)))?;
887            let qr = client
888                .simple_query(&q.sql)
889                .await
890                .map_err(|e| GraphQLError::internal(format!("backend query: {}", e)))?;
891            let rows: Vec<serde_json::Value> = qr
892                .rows
893                .iter()
894                .map(|row| {
895                    let mut obj = serde_json::Map::new();
896                    for (i, c) in qr.columns.iter().enumerate() {
897                        let v = row
898                            .get(i)
899                            .map(graphql_cell_to_json)
900                            .unwrap_or(serde_json::Value::Null);
901                        obj.insert(c.name.clone(), v);
902                    }
903                    serde_json::Value::Object(obj)
904                })
905                .collect();
906            out.push(rows);
907        }
908        Ok(out)
909    }
910
911    /// Shape the response: each top-level selection is keyed by its response
912    /// key and carries the rows from its corresponding query as an array.
913    /// (Flat top-level selections; nested-relationship shaping is a follow-on.)
914    #[allow(clippy::result_large_err)]
915    fn shape_response(
916        &self,
917        document: &ParsedDocument,
918        results: &[Vec<serde_json::Value>],
919    ) -> Result<serde_json::Value, GraphQLError> {
920        let mut data = serde_json::Map::new();
921        for (i, selection) in document.selections.iter().enumerate() {
922            let key = selection.response_key().to_string();
923            let rows = results.get(i).cloned().unwrap_or_default();
924            data.insert(key, serde_json::Value::Array(rows));
925        }
926        Ok(serde_json::Value::Object(data))
927    }
928
929    /// Get the schema
930    pub fn schema(&self) -> &GraphQLSchema {
931        &self.schema
932    }
933
934    /// Get the configuration
935    pub fn config(&self) -> &GraphQLConfig {
936        &self.config
937    }
938
939    /// Get metrics
940    pub fn metrics(&self) -> &GraphQLMetrics {
941        &self.metrics
942    }
943
944    /// Generate SDL (Schema Definition Language)
945    pub fn generate_sdl(&self) -> String {
946        self.schema.to_sdl()
947    }
948}
949
950/// Convert a backend text-protocol cell to a JSON value.
951fn graphql_cell_to_json(v: &crate::backend::TextValue) -> serde_json::Value {
952    match v {
953        crate::backend::TextValue::Null => serde_json::Value::Null,
954        crate::backend::TextValue::Text(s) => serde_json::Value::String(s.clone()),
955    }
956}
957
958impl Clone for GraphQLEngine {
959    fn clone(&self) -> Self {
960        Self {
961            config: self.config.clone(),
962            schema: self.schema.clone(),
963            sql_generator: self.sql_generator.clone(),
964            validator: QueryValidator::new(self.config.clone()),
965            metrics: self.metrics.clone(),
966            backend: self.backend.clone(),
967        }
968    }
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974    use crate::graphql::introspector::GraphQLSchema;
975
976    fn create_test_engine() -> GraphQLEngine {
977        let config = GraphQLConfig::default();
978        let schema = GraphQLSchema::new();
979        GraphQLEngine::new(config, schema)
980    }
981
982    #[test]
983    fn test_parse_simple_query() {
984        let engine = create_test_engine();
985        let query = "query { users { id name } }";
986
987        let doc = engine.parse(query).unwrap();
988        assert_eq!(doc.operation_type, OperationType::Query);
989        assert_eq!(doc.selections.len(), 1);
990        assert_eq!(doc.selections[0].name, "users");
991        assert_eq!(doc.selections[0].selections.len(), 2);
992    }
993
994    #[tokio::test]
995    async fn offline_execute_shapes_field_key() {
996        use crate::graphql::introspector::{ColumnDefinition, SchemaIntrospector, TableDefinition};
997        let tabledef = TableDefinition {
998            name: "gqlitem".to_string(),
999            schema: "public".to_string(),
1000            columns: vec![ColumnDefinition {
1001                name: "id".to_string(),
1002                data_type: "text".to_string(),
1003                nullable: true,
1004                is_primary_key: true,
1005                has_default: false,
1006            }],
1007            foreign_keys: Vec::new(),
1008        };
1009        let schema = SchemaIntrospector::new().build_schema(&[tabledef]);
1010        // No backend attached -> offline mode -> empty result sets, but the
1011        // pipeline (parse -> plan -> generate -> shape) must still key each
1012        // top-level selection under its response key (an array).
1013        let engine = GraphQLEngine::new(GraphQLConfig::default(), schema);
1014        let resp = engine
1015            .execute(GraphQLRequest::new("{ gqlitems { id } }"))
1016            .await;
1017        assert!(
1018            resp.errors.is_none(),
1019            "unexpected errors: {:?}",
1020            resp.errors
1021        );
1022        let data = resp.data.expect("data present");
1023        assert!(
1024            data.get("gqlitems").is_some(),
1025            "missing field key: {}",
1026            data
1027        );
1028        assert!(data["gqlitems"].is_array(), "field should be an array");
1029    }
1030
1031    #[test]
1032    fn test_parse_named_query() {
1033        let engine = create_test_engine();
1034        let query = "query GetUsers { users { id } }";
1035
1036        let doc = engine.parse(query).unwrap();
1037        assert_eq!(doc.operation_name, Some("GetUsers".to_string()));
1038    }
1039
1040    #[test]
1041    fn test_parse_mutation() {
1042        let engine = create_test_engine();
1043        let query = "mutation { createUser(name: \"test\") { id } }";
1044
1045        let doc = engine.parse(query).unwrap();
1046        assert_eq!(doc.operation_type, OperationType::Mutation);
1047    }
1048
1049    #[test]
1050    fn test_parse_with_arguments() {
1051        let engine = create_test_engine();
1052        let query = "{ user(id: \"123\") { name } }";
1053
1054        let doc = engine.parse(query).unwrap();
1055        let user_selection = &doc.selections[0];
1056        assert_eq!(user_selection.name, "user");
1057        assert!(user_selection.arguments.contains_key("id"));
1058    }
1059
1060    #[test]
1061    fn test_parse_with_alias() {
1062        let engine = create_test_engine();
1063        let query = "{ myUser: user(id: \"123\") { name } }";
1064
1065        let doc = engine.parse(query).unwrap();
1066        let selection = &doc.selections[0];
1067        assert_eq!(selection.alias, Some("myUser".to_string()));
1068        assert_eq!(selection.name, "user");
1069        assert_eq!(selection.response_key(), "myUser");
1070    }
1071
1072    #[test]
1073    fn test_graphql_request_builder() {
1074        let request = GraphQLRequest::new("{ users { id } }")
1075            .with_operation("GetUsers")
1076            .var("limit", 10);
1077
1078        assert_eq!(request.query, "{ users { id } }");
1079        assert_eq!(request.operation_name, Some("GetUsers".to_string()));
1080        assert!(request.variables.unwrap().contains_key("limit"));
1081    }
1082
1083    #[test]
1084    fn test_graphql_response_success() {
1085        let response = GraphQLResponse::success(serde_json::json!({"users": []}));
1086
1087        assert!(response.data.is_some());
1088        assert!(!response.has_errors());
1089    }
1090
1091    #[test]
1092    fn test_graphql_response_error() {
1093        let error = GraphQLError::parse_error("Syntax error");
1094        let response = GraphQLResponse::error(error);
1095
1096        assert!(response.data.is_none());
1097        assert!(response.has_errors());
1098    }
1099
1100    #[test]
1101    fn test_graphql_error_to_json() {
1102        let error = GraphQLError::validation_error("Field not found")
1103            .with_location(1, 10)
1104            .with_path(vec![PathSegment::Field("users".to_string())]);
1105
1106        let json = error.to_json();
1107        assert_eq!(json["message"], "Field not found");
1108        assert!(json["locations"].is_array());
1109        assert!(json["path"].is_array());
1110    }
1111
1112    #[tokio::test]
1113    async fn test_execute_simple_query() {
1114        let engine = create_test_engine();
1115        let request = GraphQLRequest::new("{ users { id name } }");
1116
1117        let response = engine.execute(request).await;
1118
1119        // Should succeed even with mock results
1120        assert!(!response.has_errors());
1121        assert!(response.data.is_some());
1122    }
1123
1124    #[test]
1125    fn test_parse_nested_selections() {
1126        let engine = create_test_engine();
1127        let query = "{ users { id posts { title comments { content } } } }";
1128
1129        let doc = engine.parse(query).unwrap();
1130        let users = &doc.selections[0];
1131        assert_eq!(users.selections.len(), 2); // id, posts
1132
1133        let posts = &users.selections[1];
1134        assert_eq!(posts.name, "posts");
1135        assert_eq!(posts.selections.len(), 2); // title, comments
1136    }
1137
1138    #[test]
1139    fn test_parse_value() {
1140        let engine = create_test_engine();
1141
1142        assert_eq!(engine.parse_value("null").unwrap(), serde_json::Value::Null);
1143        assert_eq!(
1144            engine.parse_value("true").unwrap(),
1145            serde_json::Value::Bool(true)
1146        );
1147        assert_eq!(
1148            engine.parse_value("false").unwrap(),
1149            serde_json::Value::Bool(false)
1150        );
1151        assert_eq!(
1152            engine.parse_value("\"hello\"").unwrap(),
1153            serde_json::Value::String("hello".to_string())
1154        );
1155        assert_eq!(engine.parse_value("42").unwrap(), serde_json::json!(42));
1156    }
1157}