Skip to main content

heliosdb_proxy/graphql/
sql_generator.rs

1//! SQL Generator
2//!
3//! Generates optimized SQL from GraphQL queries.
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use super::{to_snake_case, GraphQLSchema, RelationType};
9
10/// SQL query with parameters
11#[derive(Debug, Clone)]
12pub struct SqlQuery {
13    /// SQL statement
14    pub sql: String,
15    /// Query parameters
16    pub params: Vec<serde_json::Value>,
17    /// Source table
18    pub table: Option<String>,
19    /// Is a count query
20    pub is_count: bool,
21}
22
23impl SqlQuery {
24    /// Create a new SQL query
25    pub fn new(sql: impl Into<String>) -> Self {
26        Self {
27            sql: sql.into(),
28            params: Vec::new(),
29            table: None,
30            is_count: false,
31        }
32    }
33
34    /// Add a parameter
35    pub fn param(mut self, value: serde_json::Value) -> Self {
36        self.params.push(value);
37        self
38    }
39
40    /// Set the source table
41    pub fn from_table(mut self, table: impl Into<String>) -> Self {
42        self.table = Some(table.into());
43        self
44    }
45
46    /// Mark as count query
47    pub fn count(mut self) -> Self {
48        self.is_count = true;
49        self
50    }
51
52    /// Get the parameter placeholder for the given index
53    pub fn placeholder(index: usize) -> String {
54        format!("${}", index + 1)
55    }
56}
57
58/// Query plan for SQL generation
59#[derive(Debug, Clone)]
60pub enum QueryPlan {
61    /// Single table query
62    Single {
63        /// Selection
64        selection: Selection,
65        /// Filters
66        filters: Vec<Filter>,
67        /// Limit
68        limit: Option<u32>,
69        /// Offset
70        offset: Option<u32>,
71    },
72    /// Query with relationship (JOIN or LATERAL)
73    Relationship {
74        /// Parent selection
75        parent: Selection,
76        /// Child selection
77        child: Selection,
78        /// Relationship type
79        relation_type: RelationType,
80        /// Join condition
81        join_column: String,
82        /// Parent join column
83        parent_column: String,
84    },
85    /// Batch multiple queries
86    Batch {
87        /// Individual queries to batch
88        queries: Vec<QueryPlan>,
89    },
90    /// Multiple independent plans
91    Multiple {
92        /// Plans
93        plans: Vec<QueryPlan>,
94    },
95}
96
97/// Field selection
98#[derive(Debug, Clone)]
99pub struct Selection {
100    /// Table name
101    pub table_name: String,
102    /// Selected fields
103    pub fields: Vec<String>,
104    /// Nested relationships
105    pub relationships: Vec<(String, QueryPlan)>,
106}
107
108impl Selection {
109    /// Create a new selection
110    pub fn new(table_name: impl Into<String>) -> Self {
111        Self {
112            table_name: table_name.into(),
113            fields: Vec::new(),
114            relationships: Vec::new(),
115        }
116    }
117
118    /// Add a field
119    pub fn field(mut self, name: impl Into<String>) -> Self {
120        self.fields.push(name.into());
121        self
122    }
123
124    /// Add multiple fields
125    pub fn fields(mut self, names: Vec<String>) -> Self {
126        self.fields.extend(names);
127        self
128    }
129
130    /// Add a relationship
131    pub fn relationship(mut self, name: impl Into<String>, plan: QueryPlan) -> Self {
132        self.relationships.push((name.into(), plan));
133        self
134    }
135
136    /// Get the table name
137    pub fn table_name(&self) -> &str {
138        &self.table_name
139    }
140
141    /// Get the primary key column (assuming "id" for now)
142    pub fn primary_key(&self) -> &str {
143        "id"
144    }
145}
146
147/// Filter condition
148#[derive(Debug, Clone)]
149pub struct Filter {
150    /// Field name
151    pub field: String,
152    /// Operator
153    pub operator: FilterOperator,
154    /// Value
155    pub value: serde_json::Value,
156}
157
158impl Filter {
159    /// Create a new filter
160    pub fn new(
161        field: impl Into<String>,
162        operator: FilterOperator,
163        value: serde_json::Value,
164    ) -> Self {
165        Self {
166            field: field.into(),
167            operator,
168            value,
169        }
170    }
171
172    /// Create an equality filter
173    pub fn eq(field: impl Into<String>, value: serde_json::Value) -> Self {
174        Self::new(field, FilterOperator::Eq, value)
175    }
176
177    /// Create a not-equal filter
178    pub fn ne(field: impl Into<String>, value: serde_json::Value) -> Self {
179        Self::new(field, FilterOperator::Ne, value)
180    }
181
182    /// Create a greater-than filter
183    pub fn gt(field: impl Into<String>, value: serde_json::Value) -> Self {
184        Self::new(field, FilterOperator::Gt, value)
185    }
186
187    /// Create an IN filter
188    pub fn in_values(field: impl Into<String>, values: Vec<serde_json::Value>) -> Self {
189        Self::new(field, FilterOperator::In, serde_json::Value::Array(values))
190    }
191}
192
193/// Filter operator
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum FilterOperator {
196    /// Equals
197    Eq,
198    /// Not equals
199    Ne,
200    /// Greater than
201    Gt,
202    /// Greater than or equal
203    Gte,
204    /// Less than
205    Lt,
206    /// Less than or equal
207    Lte,
208    /// Contains (LIKE %value%)
209    Contains,
210    /// Starts with (LIKE value%)
211    StartsWith,
212    /// Ends with (LIKE %value)
213    EndsWith,
214    /// In list
215    In,
216    /// Not in list
217    NotIn,
218    /// Is null
219    IsNull,
220    /// Is not null
221    IsNotNull,
222}
223
224impl FilterOperator {
225    /// Get the SQL operator
226    pub fn to_sql(&self) -> &'static str {
227        match self {
228            FilterOperator::Eq => "=",
229            FilterOperator::Ne => "<>",
230            FilterOperator::Gt => ">",
231            FilterOperator::Gte => ">=",
232            FilterOperator::Lt => "<",
233            FilterOperator::Lte => "<=",
234            FilterOperator::Contains => "LIKE",
235            FilterOperator::StartsWith => "LIKE",
236            FilterOperator::EndsWith => "LIKE",
237            FilterOperator::In => "IN",
238            FilterOperator::NotIn => "NOT IN",
239            FilterOperator::IsNull => "IS NULL",
240            FilterOperator::IsNotNull => "IS NOT NULL",
241        }
242    }
243
244    /// Check if operator needs a value
245    pub fn needs_value(&self) -> bool {
246        !matches!(self, FilterOperator::IsNull | FilterOperator::IsNotNull)
247    }
248
249    /// Check if operator uses LIKE patterns
250    pub fn is_like(&self) -> bool {
251        matches!(
252            self,
253            FilterOperator::Contains | FilterOperator::StartsWith | FilterOperator::EndsWith
254        )
255    }
256}
257
258/// SQL Generator
259#[derive(Debug)]
260pub struct SqlGenerator {
261    /// Schema reference
262    #[allow(dead_code)]
263    schema: Arc<GraphQLSchema>,
264    /// Quote identifier character
265    quote_char: char,
266    /// Parameter style
267    param_style: ParamStyle,
268}
269
270/// Parameter placeholder style
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum ParamStyle {
273    /// Positional ($1, $2, ...)
274    Positional,
275    /// Named (:name)
276    Named,
277    /// Question mark (?)
278    QuestionMark,
279}
280
281impl SqlGenerator {
282    /// Create a new SQL generator
283    pub fn new(schema: Arc<GraphQLSchema>) -> Self {
284        Self {
285            schema,
286            quote_char: '"',
287            param_style: ParamStyle::Positional,
288        }
289    }
290
291    /// Set the quote character for identifiers
292    pub fn with_quote_char(mut self, char: char) -> Self {
293        self.quote_char = char;
294        self
295    }
296
297    /// Set the parameter style
298    pub fn with_param_style(mut self, style: ParamStyle) -> Self {
299        self.param_style = style;
300        self
301    }
302
303    /// Generate SQL from a query plan
304    pub fn generate(&self, plan: &QueryPlan) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
305        match plan {
306            QueryPlan::Single {
307                selection,
308                filters,
309                limit,
310                offset,
311            } => Ok(vec![
312                self.generate_single(selection, filters, *limit, *offset)?
313            ]),
314            QueryPlan::Relationship {
315                parent,
316                child,
317                relation_type,
318                join_column,
319                parent_column,
320            } => self.generate_relationship(
321                parent,
322                child,
323                *relation_type,
324                join_column,
325                parent_column,
326            ),
327            QueryPlan::Batch { queries } => self.generate_batch(queries),
328            QueryPlan::Multiple { plans } => {
329                let mut results = Vec::new();
330                for p in plans {
331                    results.extend(self.generate(p)?);
332                }
333                Ok(results)
334            }
335        }
336    }
337
338    /// Generate SQL for a single table query
339    fn generate_single(
340        &self,
341        selection: &Selection,
342        filters: &[Filter],
343        limit: Option<u32>,
344        offset: Option<u32>,
345    ) -> Result<SqlQuery, SqlGeneratorError> {
346        let mut params = Vec::new();
347        let mut param_index = 0;
348
349        // Build SELECT clause
350        let columns = if selection.fields.is_empty() {
351            "*".to_string()
352        } else {
353            selection
354                .fields
355                .iter()
356                .map(|f| self.quote_identifier(&to_snake_case(f)))
357                .collect::<Vec<_>>()
358                .join(", ")
359        };
360
361        // Build FROM clause
362        let table = self.quote_identifier(&selection.table_name);
363
364        // Build WHERE clause
365        let where_clause = if filters.is_empty() {
366            String::new()
367        } else {
368            let conditions: Vec<String> = filters
369                .iter()
370                .map(|f| {
371                    let col = self.quote_identifier(&to_snake_case(&f.field));
372                    if f.operator.needs_value() {
373                        param_index += 1;
374                        params.push(self.prepare_value(&f.operator, &f.value));
375                        format!(
376                            "{} {} {}",
377                            col,
378                            f.operator.to_sql(),
379                            self.placeholder(param_index - 1)
380                        )
381                    } else {
382                        format!("{} {}", col, f.operator.to_sql())
383                    }
384                })
385                .collect();
386            format!(" WHERE {}", conditions.join(" AND "))
387        };
388
389        // Build LIMIT/OFFSET
390        let mut limit_offset = String::new();
391        if let Some(l) = limit {
392            limit_offset.push_str(&format!(" LIMIT {}", l));
393        }
394        if let Some(o) = offset {
395            limit_offset.push_str(&format!(" OFFSET {}", o));
396        }
397
398        let sql = format!(
399            "SELECT {} FROM {}{}{}",
400            columns, table, where_clause, limit_offset
401        );
402
403        Ok(SqlQuery {
404            sql,
405            params,
406            table: Some(selection.table_name.clone()),
407            is_count: false,
408        })
409    }
410
411    /// Generate SQL for a relationship query
412    fn generate_relationship(
413        &self,
414        parent: &Selection,
415        child: &Selection,
416        relation_type: RelationType,
417        join_column: &str,
418        parent_column: &str,
419    ) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
420        match relation_type {
421            RelationType::OneToOne | RelationType::ManyToOne => {
422                // Use JOIN for *-to-one relationships
423                Ok(vec![self.generate_with_join(
424                    parent,
425                    child,
426                    join_column,
427                    parent_column,
428                )?])
429            }
430            RelationType::OneToMany | RelationType::ManyToMany => {
431                // Use LATERAL for *-to-many relationships
432                Ok(vec![self.generate_with_lateral(
433                    parent,
434                    child,
435                    join_column,
436                    parent_column,
437                )?])
438            }
439        }
440    }
441
442    /// Generate SQL with JOIN
443    fn generate_with_join(
444        &self,
445        parent: &Selection,
446        child: &Selection,
447        join_column: &str,
448        parent_column: &str,
449    ) -> Result<SqlQuery, SqlGeneratorError> {
450        let parent_alias = "p";
451        let child_alias = "c";
452
453        let parent_cols: Vec<String> = if parent.fields.is_empty() {
454            vec![format!("{}.*", parent_alias)]
455        } else {
456            parent
457                .fields
458                .iter()
459                .map(|f| {
460                    format!(
461                        "{}.{}",
462                        parent_alias,
463                        self.quote_identifier(&to_snake_case(f))
464                    )
465                })
466                .collect()
467        };
468
469        let child_cols: Vec<String> = if child.fields.is_empty() {
470            vec![format!("{}.*", child_alias)]
471        } else {
472            child
473                .fields
474                .iter()
475                .map(|f| {
476                    format!(
477                        "{}.{}",
478                        child_alias,
479                        self.quote_identifier(&to_snake_case(f))
480                    )
481                })
482                .collect()
483        };
484
485        let all_cols = [parent_cols, child_cols].concat();
486
487        let sql = format!(
488            "SELECT {} FROM {} {} LEFT JOIN {} {} ON {}.{} = {}.{}",
489            all_cols.join(", "),
490            self.quote_identifier(&parent.table_name),
491            parent_alias,
492            self.quote_identifier(&child.table_name),
493            child_alias,
494            child_alias,
495            self.quote_identifier(&to_snake_case(join_column)),
496            parent_alias,
497            self.quote_identifier(&to_snake_case(parent_column))
498        );
499
500        Ok(SqlQuery::new(sql).from_table(&parent.table_name))
501    }
502
503    /// Generate SQL with LATERAL subquery
504    fn generate_with_lateral(
505        &self,
506        parent: &Selection,
507        child: &Selection,
508        join_column: &str,
509        parent_column: &str,
510    ) -> Result<SqlQuery, SqlGeneratorError> {
511        let parent_alias = "p";
512        let child_alias = "c";
513
514        let parent_cols: Vec<String> = if parent.fields.is_empty() {
515            vec![format!("{}.*", parent_alias)]
516        } else {
517            parent
518                .fields
519                .iter()
520                .map(|f| {
521                    format!(
522                        "{}.{}",
523                        parent_alias,
524                        self.quote_identifier(&to_snake_case(f))
525                    )
526                })
527                .collect()
528        };
529
530        let child_cols: Vec<String> = if child.fields.is_empty() {
531            vec!["*".to_string()]
532        } else {
533            child
534                .fields
535                .iter()
536                .map(|f| self.quote_identifier(&to_snake_case(f)))
537                .collect()
538        };
539
540        let sql = format!(
541            "SELECT {}, LATERAL (
542                SELECT json_agg(sub.*) FROM (
543                    SELECT {} FROM {} {} WHERE {}.{} = {}.{}
544                ) sub
545            ) AS {}
546            FROM {} {}",
547            parent_cols.join(", "),
548            child_cols.join(", "),
549            self.quote_identifier(&child.table_name),
550            child_alias,
551            child_alias,
552            self.quote_identifier(&to_snake_case(join_column)),
553            parent_alias,
554            self.quote_identifier(&to_snake_case(parent_column)),
555            self.quote_identifier(&to_snake_case(&child.table_name)),
556            self.quote_identifier(&parent.table_name),
557            parent_alias
558        );
559
560        Ok(SqlQuery::new(sql).from_table(&parent.table_name))
561    }
562
563    /// Generate batched SQL queries
564    fn generate_batch(&self, queries: &[QueryPlan]) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
565        // For now, just generate individual queries
566        // In production, could use UNION ALL or multi-statement
567        let mut results = Vec::new();
568        for query in queries {
569            results.extend(self.generate(query)?);
570        }
571        Ok(results)
572    }
573
574    /// Quote an identifier
575    fn quote_identifier(&self, name: &str) -> String {
576        format!("{}{}{}", self.quote_char, name, self.quote_char)
577    }
578
579    /// Get parameter placeholder
580    fn placeholder(&self, index: usize) -> String {
581        match self.param_style {
582            ParamStyle::Positional => format!("${}", index + 1),
583            ParamStyle::Named => format!(":p{}", index),
584            ParamStyle::QuestionMark => "?".to_string(),
585        }
586    }
587
588    /// Prepare a value for a filter
589    fn prepare_value(
590        &self,
591        operator: &FilterOperator,
592        value: &serde_json::Value,
593    ) -> serde_json::Value {
594        match operator {
595            FilterOperator::Contains => {
596                if let serde_json::Value::String(s) = value {
597                    serde_json::Value::String(format!("%{}%", s))
598                } else {
599                    value.clone()
600                }
601            }
602            FilterOperator::StartsWith => {
603                if let serde_json::Value::String(s) = value {
604                    serde_json::Value::String(format!("{}%", s))
605                } else {
606                    value.clone()
607                }
608            }
609            FilterOperator::EndsWith => {
610                if let serde_json::Value::String(s) = value {
611                    serde_json::Value::String(format!("%{}", s))
612                } else {
613                    value.clone()
614                }
615            }
616            _ => value.clone(),
617        }
618    }
619
620    /// Generate a count query
621    pub fn generate_count(
622        &self,
623        table: &str,
624        filters: &[Filter],
625    ) -> Result<SqlQuery, SqlGeneratorError> {
626        let mut params = Vec::new();
627        let mut param_index = 0;
628
629        let where_clause = if filters.is_empty() {
630            String::new()
631        } else {
632            let conditions: Vec<String> = filters
633                .iter()
634                .map(|f| {
635                    let col = self.quote_identifier(&to_snake_case(&f.field));
636                    if f.operator.needs_value() {
637                        param_index += 1;
638                        params.push(self.prepare_value(&f.operator, &f.value));
639                        format!(
640                            "{} {} {}",
641                            col,
642                            f.operator.to_sql(),
643                            self.placeholder(param_index - 1)
644                        )
645                    } else {
646                        format!("{} {}", col, f.operator.to_sql())
647                    }
648                })
649                .collect();
650            format!(" WHERE {}", conditions.join(" AND "))
651        };
652
653        let sql = format!(
654            "SELECT COUNT(*) FROM {}{}",
655            self.quote_identifier(table),
656            where_clause
657        );
658
659        Ok(SqlQuery {
660            sql,
661            params,
662            table: Some(table.to_string()),
663            is_count: true,
664        })
665    }
666
667    /// Generate an INSERT query
668    pub fn generate_insert(
669        &self,
670        table: &str,
671        values: &HashMap<String, serde_json::Value>,
672    ) -> Result<SqlQuery, SqlGeneratorError> {
673        if values.is_empty() {
674            return Err(SqlGeneratorError::EmptyValues);
675        }
676
677        let columns: Vec<String> = values
678            .keys()
679            .map(|k| self.quote_identifier(&to_snake_case(k)))
680            .collect();
681
682        let placeholders: Vec<String> = (0..values.len()).map(|i| self.placeholder(i)).collect();
683
684        let params: Vec<serde_json::Value> = values.values().cloned().collect();
685
686        let sql = format!(
687            "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
688            self.quote_identifier(table),
689            columns.join(", "),
690            placeholders.join(", ")
691        );
692
693        Ok(SqlQuery {
694            sql,
695            params,
696            table: Some(table.to_string()),
697            is_count: false,
698        })
699    }
700
701    /// Generate an UPDATE query
702    pub fn generate_update(
703        &self,
704        table: &str,
705        id: &serde_json::Value,
706        values: &HashMap<String, serde_json::Value>,
707    ) -> Result<SqlQuery, SqlGeneratorError> {
708        if values.is_empty() {
709            return Err(SqlGeneratorError::EmptyValues);
710        }
711
712        let set_clauses: Vec<String> = values
713            .keys()
714            .enumerate()
715            .map(|(i, k)| {
716                format!(
717                    "{} = {}",
718                    self.quote_identifier(&to_snake_case(k)),
719                    self.placeholder(i)
720                )
721            })
722            .collect();
723
724        let mut params: Vec<serde_json::Value> = values.values().cloned().collect();
725        params.push(id.clone());
726
727        let id_placeholder = self.placeholder(params.len() - 1);
728
729        let sql = format!(
730            "UPDATE {} SET {} WHERE {} = {} RETURNING *",
731            self.quote_identifier(table),
732            set_clauses.join(", "),
733            self.quote_identifier("id"),
734            id_placeholder
735        );
736
737        Ok(SqlQuery {
738            sql,
739            params,
740            table: Some(table.to_string()),
741            is_count: false,
742        })
743    }
744
745    /// Generate a DELETE query
746    pub fn generate_delete(
747        &self,
748        table: &str,
749        id: &serde_json::Value,
750    ) -> Result<SqlQuery, SqlGeneratorError> {
751        let sql = format!(
752            "DELETE FROM {} WHERE {} = {} RETURNING {}",
753            self.quote_identifier(table),
754            self.quote_identifier("id"),
755            self.placeholder(0),
756            self.quote_identifier("id")
757        );
758
759        Ok(SqlQuery {
760            sql,
761            params: vec![id.clone()],
762            table: Some(table.to_string()),
763            is_count: false,
764        })
765    }
766}
767
768/// SQL generator error
769#[derive(Debug, Clone)]
770pub enum SqlGeneratorError {
771    /// Empty values for insert/update
772    EmptyValues,
773    /// Invalid filter
774    InvalidFilter(String),
775    /// Unknown table
776    UnknownTable(String),
777    /// Invalid relationship
778    InvalidRelationship(String),
779}
780
781impl std::fmt::Display for SqlGeneratorError {
782    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
783        match self {
784            SqlGeneratorError::EmptyValues => write!(f, "No values provided"),
785            SqlGeneratorError::InvalidFilter(msg) => write!(f, "Invalid filter: {}", msg),
786            SqlGeneratorError::UnknownTable(table) => write!(f, "Unknown table: {}", table),
787            SqlGeneratorError::InvalidRelationship(msg) => {
788                write!(f, "Invalid relationship: {}", msg)
789            }
790        }
791    }
792}
793
794impl std::error::Error for SqlGeneratorError {}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use crate::graphql::introspector::GraphQLSchema;
800
801    fn create_generator() -> SqlGenerator {
802        let schema = Arc::new(GraphQLSchema::new());
803        SqlGenerator::new(schema)
804    }
805
806    #[test]
807    fn test_generate_simple_select() {
808        let generator = create_generator();
809        let selection = Selection::new("users").field("id").field("name");
810
811        let plan = QueryPlan::Single {
812            selection,
813            filters: vec![],
814            limit: None,
815            offset: None,
816        };
817
818        let queries = generator.generate(&plan).unwrap();
819        assert_eq!(queries.len(), 1);
820        assert!(queries[0].sql.contains("SELECT"));
821        assert!(queries[0].sql.contains("\"users\""));
822    }
823
824    #[test]
825    fn test_generate_with_filters() {
826        let generator = create_generator();
827        let selection = Selection::new("users");
828
829        let plan = QueryPlan::Single {
830            selection,
831            filters: vec![Filter::eq("id", serde_json::json!("123"))],
832            limit: None,
833            offset: None,
834        };
835
836        let queries = generator.generate(&plan).unwrap();
837        assert_eq!(queries.len(), 1);
838        assert!(queries[0].sql.contains("WHERE"));
839        assert!(queries[0].sql.contains("$1"));
840        assert_eq!(queries[0].params.len(), 1);
841    }
842
843    #[test]
844    fn test_generate_with_limit_offset() {
845        let generator = create_generator();
846        let selection = Selection::new("users");
847
848        let plan = QueryPlan::Single {
849            selection,
850            filters: vec![],
851            limit: Some(10),
852            offset: Some(20),
853        };
854
855        let queries = generator.generate(&plan).unwrap();
856        assert!(queries[0].sql.contains("LIMIT 10"));
857        assert!(queries[0].sql.contains("OFFSET 20"));
858    }
859
860    #[test]
861    fn test_generate_join() {
862        let generator = create_generator();
863        let parent = Selection::new("users").field("id").field("name");
864        let child = Selection::new("profiles").field("bio");
865
866        let plan = QueryPlan::Relationship {
867            parent,
868            child,
869            relation_type: RelationType::OneToOne,
870            join_column: "user_id".to_string(),
871            parent_column: "id".to_string(),
872        };
873
874        let queries = generator.generate(&plan).unwrap();
875        assert_eq!(queries.len(), 1);
876        assert!(queries[0].sql.contains("LEFT JOIN"));
877    }
878
879    #[test]
880    fn test_generate_lateral() {
881        let generator = create_generator();
882        let parent = Selection::new("users").field("id");
883        let child = Selection::new("posts").field("title");
884
885        let plan = QueryPlan::Relationship {
886            parent,
887            child,
888            relation_type: RelationType::OneToMany,
889            join_column: "user_id".to_string(),
890            parent_column: "id".to_string(),
891        };
892
893        let queries = generator.generate(&plan).unwrap();
894        assert_eq!(queries.len(), 1);
895        assert!(queries[0].sql.contains("LATERAL"));
896        assert!(queries[0].sql.contains("json_agg"));
897    }
898
899    #[test]
900    fn test_generate_count() {
901        let generator = create_generator();
902        let query = generator.generate_count("users", &[]).unwrap();
903
904        assert!(query.sql.contains("COUNT(*)"));
905        assert!(query.is_count);
906    }
907
908    #[test]
909    fn test_generate_insert() {
910        let generator = create_generator();
911        let mut values = HashMap::new();
912        values.insert("name".to_string(), serde_json::json!("John"));
913        values.insert("email".to_string(), serde_json::json!("john@example.com"));
914
915        let query = generator.generate_insert("users", &values).unwrap();
916
917        assert!(query.sql.contains("INSERT INTO"));
918        assert!(query.sql.contains("RETURNING"));
919        assert_eq!(query.params.len(), 2);
920    }
921
922    #[test]
923    fn test_generate_update() {
924        let generator = create_generator();
925        let mut values = HashMap::new();
926        values.insert("name".to_string(), serde_json::json!("Jane"));
927
928        let query = generator
929            .generate_update("users", &serde_json::json!("123"), &values)
930            .unwrap();
931
932        assert!(query.sql.contains("UPDATE"));
933        assert!(query.sql.contains("SET"));
934        assert!(query.sql.contains("WHERE"));
935        assert!(query.sql.contains("RETURNING"));
936    }
937
938    #[test]
939    fn test_generate_delete() {
940        let generator = create_generator();
941        let query = generator
942            .generate_delete("users", &serde_json::json!("123"))
943            .unwrap();
944
945        assert!(query.sql.contains("DELETE FROM"));
946        assert!(query.sql.contains("WHERE"));
947        assert!(query.sql.contains("RETURNING"));
948    }
949
950    #[test]
951    fn test_filter_operators() {
952        assert_eq!(FilterOperator::Eq.to_sql(), "=");
953        assert_eq!(FilterOperator::Ne.to_sql(), "<>");
954        assert_eq!(FilterOperator::Gt.to_sql(), ">");
955        assert_eq!(FilterOperator::Contains.to_sql(), "LIKE");
956        assert_eq!(FilterOperator::In.to_sql(), "IN");
957        assert_eq!(FilterOperator::IsNull.to_sql(), "IS NULL");
958    }
959
960    #[test]
961    fn test_like_pattern_preparation() {
962        let generator = create_generator();
963
964        let contains =
965            generator.prepare_value(&FilterOperator::Contains, &serde_json::json!("test"));
966        assert_eq!(contains, serde_json::json!("%test%"));
967
968        let starts =
969            generator.prepare_value(&FilterOperator::StartsWith, &serde_json::json!("test"));
970        assert_eq!(starts, serde_json::json!("test%"));
971
972        let ends = generator.prepare_value(&FilterOperator::EndsWith, &serde_json::json!("test"));
973        assert_eq!(ends, serde_json::json!("%test"));
974    }
975
976    #[test]
977    fn test_empty_insert_error() {
978        let generator = create_generator();
979        let result = generator.generate_insert("users", &HashMap::new());
980        assert!(matches!(result, Err(SqlGeneratorError::EmptyValues)));
981    }
982
983    #[test]
984    fn test_param_styles() {
985        let schema = Arc::new(GraphQLSchema::new());
986
987        let positional = SqlGenerator::new(schema.clone());
988        assert_eq!(positional.placeholder(0), "$1");
989        assert_eq!(positional.placeholder(2), "$3");
990
991        let question = SqlGenerator::new(schema.clone()).with_param_style(ParamStyle::QuestionMark);
992        assert_eq!(question.placeholder(0), "?");
993
994        let named = SqlGenerator::new(schema).with_param_style(ParamStyle::Named);
995        assert_eq!(named.placeholder(0), ":p0");
996    }
997}