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