Skip to main content

fraiseql_core/compiler/
lowering.rs

1//! SQL template generator - lowers IR to database-specific SQL.
2//!
3//! # Overview
4//!
5//! Transforms validated IR into SQL templates for each query/mutation.
6//! Supports multiple database backends with dialect-specific generation.
7//!
8//! # Template Syntax
9//!
10//! Templates use named placeholders in the format `{param_name}` which are
11//! replaced at runtime with actual parameter values:
12//!
13//! - `{where_clause}` - Optional WHERE clause (empty if no filters)
14//! - `{order_by}` - Optional ORDER BY clause
15//! - `{limit}` - Optional LIMIT value
16//! - `{offset}` - Optional OFFSET value
17//! - `{arg_name}` - Query argument placeholders
18//!
19//! # Example
20//!
21//! ```ignore
22//! use fraiseql_core::compiler::lowering::{SqlTemplateGenerator, DatabaseTarget};
23//!
24//! let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
25//! let templates = generator.generate(&ir)?;
26//!
27//! for template in templates {
28//!     println!("{}: {}", template.name, template.template);
29//! }
30//! ```
31
32use super::ir::{AuthoringIR, IRArgument, IRMutation, IRQuery, MutationOperation};
33use crate::error::Result;
34
35/// Database target for SQL generation.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum DatabaseTarget {
38    /// PostgreSQL database.
39    #[default]
40    PostgreSQL,
41    /// MySQL database.
42    MySQL,
43    /// SQLite database.
44    SQLite,
45    /// SQL Server database.
46    SQLServer,
47}
48
49impl DatabaseTarget {
50    /// Get the placeholder format for this database.
51    fn placeholder(self, index: usize) -> String {
52        match self {
53            Self::PostgreSQL => format!("${index}"),
54            Self::MySQL | Self::SQLite => "?".to_string(),
55            Self::SQLServer => format!("@p{index}"),
56        }
57    }
58
59    /// Get the identifier quoting character for this database.
60    fn quote_identifier(self, name: &str) -> String {
61        match self {
62            Self::PostgreSQL | Self::SQLite => format!("\"{name}\""),
63            Self::MySQL => format!("`{name}`"),
64            Self::SQLServer => format!("[{name}]"),
65        }
66    }
67
68    /// Get the JSONB path extraction syntax for this database.
69    fn jsonb_extract(self, column: &str, path: &str) -> String {
70        match self {
71            Self::PostgreSQL => format!("{column}->'{path}'"),
72            Self::MySQL => format!("JSON_EXTRACT({column}, '$.{path}')"),
73            Self::SQLite => format!("json_extract({column}, '$.{path}')"),
74            Self::SQLServer => format!("JSON_VALUE({column}, '$.{path}')"),
75        }
76    }
77
78    /// Get the LIMIT/OFFSET syntax for this database.
79    fn limit_offset(self, limit: Option<&str>, offset: Option<&str>) -> String {
80        match self {
81            Self::PostgreSQL | Self::SQLite | Self::MySQL => {
82                let mut parts = Vec::new();
83                if let Some(lim) = limit {
84                    parts.push(format!("LIMIT {lim}"));
85                }
86                if let Some(off) = offset {
87                    parts.push(format!("OFFSET {off}"));
88                }
89                parts.join(" ")
90            },
91            Self::SQLServer => {
92                // SQL Server uses OFFSET...FETCH
93                let mut parts = Vec::new();
94                if let Some(off) = offset {
95                    parts.push(format!("OFFSET {off} ROWS"));
96                    if let Some(lim) = limit {
97                        parts.push(format!("FETCH NEXT {lim} ROWS ONLY"));
98                    }
99                } else if let Some(lim) = limit {
100                    // Without OFFSET, use TOP in SELECT
101                    parts.push(format!("TOP {lim}"));
102                }
103                parts.join(" ")
104            },
105        }
106    }
107}
108
109/// Template kind for categorizing generated templates.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum TemplateKind {
112    /// SELECT query template.
113    Query,
114    /// INSERT mutation template.
115    Insert,
116    /// UPDATE mutation template.
117    Update,
118    /// DELETE mutation template.
119    Delete,
120    /// Custom mutation template.
121    Custom,
122}
123
124/// SQL template for a query/mutation.
125#[derive(Debug, Clone)]
126pub struct SqlTemplate {
127    /// Template name (query/mutation name).
128    pub name:                String,
129    /// Template kind (query, insert, update, delete).
130    pub kind:                TemplateKind,
131    /// SQL template with placeholders.
132    pub template:            String,
133    /// Parameter names in order of appearance.
134    pub parameters:          Vec<String>,
135    /// Whether this template supports dynamic WHERE clauses.
136    pub supports_where:      bool,
137    /// Whether this template supports dynamic ORDER BY.
138    pub supports_order_by:   bool,
139    /// Whether this template supports LIMIT/OFFSET.
140    pub supports_pagination: bool,
141}
142
143impl SqlTemplate {
144    /// Create a new query template.
145    fn query(name: String, template: String, parameters: Vec<String>) -> Self {
146        Self {
147            name,
148            kind: TemplateKind::Query,
149            template,
150            parameters,
151            supports_where: false,
152            supports_order_by: false,
153            supports_pagination: false,
154        }
155    }
156
157    /// Enable WHERE clause support.
158    #[must_use]
159    pub fn with_where(mut self) -> Self {
160        self.supports_where = true;
161        self
162    }
163
164    /// Enable ORDER BY support.
165    #[must_use]
166    pub fn with_order_by(mut self) -> Self {
167        self.supports_order_by = true;
168        self
169    }
170
171    /// Enable pagination support.
172    #[must_use]
173    pub fn with_pagination(mut self) -> Self {
174        self.supports_pagination = true;
175        self
176    }
177}
178
179/// SQL template generator.
180pub struct SqlTemplateGenerator {
181    target: DatabaseTarget,
182}
183
184impl SqlTemplateGenerator {
185    /// Create new SQL template generator.
186    #[must_use]
187    pub fn new(target: DatabaseTarget) -> Self {
188        Self { target }
189    }
190
191    /// Generate SQL templates from IR.
192    ///
193    /// # Arguments
194    ///
195    /// * `ir` - Validated IR
196    ///
197    /// # Returns
198    ///
199    /// SQL templates for all queries/mutations
200    ///
201    /// # Errors
202    ///
203    /// Returns error if SQL generation fails.
204    pub fn generate(&self, ir: &AuthoringIR) -> Result<Vec<SqlTemplate>> {
205        let mut templates = Vec::new();
206
207        // Generate SQL templates for each query
208        for query in &ir.queries {
209            templates.push(self.generate_query_template(query));
210        }
211
212        // Generate SQL templates for each mutation
213        for mutation in &ir.mutations {
214            templates.push(self.generate_mutation_template(mutation));
215        }
216
217        Ok(templates)
218    }
219
220    /// Generate a query template.
221    fn generate_query_template(&self, query: &IRQuery) -> SqlTemplate {
222        let table = query.sql_source.as_deref().unwrap_or(&query.return_type);
223        let quoted_table = self.target.quote_identifier(table);
224
225        // Build parameter list from arguments
226        let parameters: Vec<String> = query.arguments.iter().map(|a| a.name.clone()).collect();
227
228        // Build the base SELECT template
229        let mut template = format!("SELECT data FROM {quoted_table}");
230
231        // Add placeholder for dynamic WHERE if supported
232        if query.auto_params.has_where || !query.arguments.is_empty() {
233            template.push_str(" {{where_clause}}");
234        }
235
236        // Add placeholder for ORDER BY if supported
237        if query.auto_params.has_order_by {
238            template.push_str(" {{order_by}}");
239        }
240
241        // Add placeholder for LIMIT/OFFSET if supported
242        if query.auto_params.has_limit || query.auto_params.has_offset {
243            template.push_str(" {{pagination}}");
244        }
245
246        let mut sql_template = SqlTemplate::query(query.name.clone(), template, parameters);
247
248        if query.auto_params.has_where || !query.arguments.is_empty() {
249            sql_template = sql_template.with_where();
250        }
251        if query.auto_params.has_order_by {
252            sql_template = sql_template.with_order_by();
253        }
254        if query.auto_params.has_limit || query.auto_params.has_offset {
255            sql_template = sql_template.with_pagination();
256        }
257
258        sql_template
259    }
260
261    /// Generate a mutation template.
262    fn generate_mutation_template(&self, mutation: &IRMutation) -> SqlTemplate {
263        // Infer table name from return type (lowercase)
264        let table = mutation.return_type.to_lowercase();
265        let quoted_table = self.target.quote_identifier(&table);
266
267        // Build parameter list from arguments
268        let parameters: Vec<String> = mutation.arguments.iter().map(|a| a.name.clone()).collect();
269
270        let (template, kind) = match mutation.operation {
271            MutationOperation::Create => {
272                self.generate_insert_template(&quoted_table, &mutation.arguments)
273            },
274            MutationOperation::Update => {
275                self.generate_update_template(&quoted_table, &mutation.arguments)
276            },
277            MutationOperation::Delete => {
278                self.generate_delete_template(&quoted_table, &mutation.arguments)
279            },
280            MutationOperation::Custom => {
281                (format!("-- Custom mutation: {}", mutation.name), TemplateKind::Custom)
282            },
283        };
284
285        SqlTemplate {
286            name: mutation.name.clone(),
287            kind,
288            template,
289            parameters,
290            supports_where: false,
291            supports_order_by: false,
292            supports_pagination: false,
293        }
294    }
295
296    /// Generate an INSERT template.
297    fn generate_insert_template(
298        &self,
299        quoted_table: &str,
300        arguments: &[IRArgument],
301    ) -> (String, TemplateKind) {
302        if arguments.is_empty() {
303            return (
304                format!("INSERT INTO {quoted_table} (data) VALUES ({{data}}) RETURNING data"),
305                TemplateKind::Insert,
306            );
307        }
308
309        // For mutations with arguments, we expect an "input" argument containing the data
310        let has_input = arguments.iter().any(|a| a.name == "input");
311
312        if has_input {
313            // Standard input pattern
314            (
315                format!("INSERT INTO {quoted_table} (data) VALUES ({{input}}) RETURNING data"),
316                TemplateKind::Insert,
317            )
318        } else {
319            // Build column list from arguments
320            let columns: Vec<&str> = arguments.iter().map(|a| a.name.as_str()).collect();
321            let placeholders: Vec<String> =
322                (1..=columns.len()).map(|i| self.target.placeholder(i)).collect();
323
324            (
325                format!(
326                    "INSERT INTO {quoted_table} ({}) VALUES ({}) RETURNING data",
327                    columns.join(", "),
328                    placeholders.join(", ")
329                ),
330                TemplateKind::Insert,
331            )
332        }
333    }
334
335    /// Generate an UPDATE template.
336    fn generate_update_template(
337        &self,
338        quoted_table: &str,
339        arguments: &[IRArgument],
340    ) -> (String, TemplateKind) {
341        // Find the id argument (usually "id" or "where")
342        let id_arg = arguments.iter().find(|a| a.name == "id" || a.name == "where");
343
344        // Find the input argument
345        let input_arg = arguments.iter().find(|a| a.name == "input" || a.name == "data");
346
347        match (id_arg, input_arg) {
348            (Some(id), Some(_)) => (
349                format!(
350                    "UPDATE {quoted_table} SET data = {{input}} WHERE {} = {{{}}} RETURNING data",
351                    self.target.jsonb_extract("data", "id"),
352                    id.name
353                ),
354                TemplateKind::Update,
355            ),
356            (Some(id), None) => {
357                // Update with individual fields
358                let set_clauses: Vec<String> = arguments
359                    .iter()
360                    .filter(|a| a.name != "id" && a.name != "where")
361                    .map(|a| {
362                        format!("data = jsonb_set(data, '{{{{{}}}}}', {{{}}})", a.name, a.name)
363                    })
364                    .collect();
365
366                if set_clauses.is_empty() {
367                    (
368                        format!(
369                            "UPDATE {quoted_table} SET data = {{data}} WHERE {} = {{{}}} RETURNING data",
370                            self.target.jsonb_extract("data", "id"),
371                            id.name
372                        ),
373                        TemplateKind::Update,
374                    )
375                } else {
376                    (
377                        format!(
378                            "UPDATE {quoted_table} SET {} WHERE {} = {{{}}} RETURNING data",
379                            set_clauses.join(", "),
380                            self.target.jsonb_extract("data", "id"),
381                            id.name
382                        ),
383                        TemplateKind::Update,
384                    )
385                }
386            },
387            _ => (
388                format!(
389                    "UPDATE {quoted_table} SET data = {{data}} WHERE {{where_clause}} RETURNING data"
390                ),
391                TemplateKind::Update,
392            ),
393        }
394    }
395
396    /// Generate a DELETE template.
397    fn generate_delete_template(
398        &self,
399        quoted_table: &str,
400        arguments: &[IRArgument],
401    ) -> (String, TemplateKind) {
402        // Find the id argument
403        let id_arg = arguments.iter().find(|a| a.name == "id" || a.name == "where");
404
405        if let Some(id) = id_arg {
406            (
407                format!(
408                    "DELETE FROM {quoted_table} WHERE {} = {{{}}} RETURNING data",
409                    self.target.jsonb_extract("data", "id"),
410                    id.name
411                ),
412                TemplateKind::Delete,
413            )
414        } else {
415            (
416                format!("DELETE FROM {quoted_table} WHERE {{where_clause}} RETURNING data"),
417                TemplateKind::Delete,
418            )
419        }
420    }
421
422    /// Get database target.
423    #[must_use]
424    pub const fn target(&self) -> DatabaseTarget {
425        self.target
426    }
427
428    /// Expand a template with actual parameter values.
429    ///
430    /// This method replaces template placeholders with actual values:
431    /// - `{param}` -> actual parameter value
432    /// - `{where_clause}` -> WHERE clause or empty
433    /// - `{order_by}` -> ORDER BY clause or empty
434    /// - `{pagination}` -> LIMIT/OFFSET or empty
435    pub fn expand_template(
436        &self,
437        template: &SqlTemplate,
438        params: &std::collections::HashMap<String, serde_json::Value>,
439        where_clause: Option<&str>,
440        order_by: Option<&str>,
441        limit: Option<u64>,
442        offset: Option<u64>,
443    ) -> String {
444        let mut sql = template.template.clone();
445
446        // Replace parameter placeholders
447        for (i, param_name) in template.parameters.iter().enumerate() {
448            if let Some(value) = params.get(param_name) {
449                let placeholder = format!("{{{param_name}}}");
450                let replacement = self.value_to_sql(value);
451                sql = sql.replace(&placeholder, &replacement);
452            } else {
453                // Replace with positional placeholder for unbound params
454                let placeholder = format!("{{{param_name}}}");
455                sql = sql.replace(&placeholder, &self.target.placeholder(i + 1));
456            }
457        }
458
459        // Replace WHERE clause placeholder
460        if template.supports_where {
461            let where_sql = where_clause.map(|w| format!("WHERE {w}")).unwrap_or_default();
462            sql = sql.replace("{{where_clause}}", &where_sql);
463        }
464
465        // Replace ORDER BY placeholder
466        if template.supports_order_by {
467            let order_sql = order_by.map(|o| format!("ORDER BY {o}")).unwrap_or_default();
468            sql = sql.replace("{{order_by}}", &order_sql);
469        }
470
471        // Replace pagination placeholder
472        if template.supports_pagination {
473            let limit_str = limit.map(|l| l.to_string());
474            let offset_str = offset.map(|o| o.to_string());
475            let pagination_sql =
476                self.target.limit_offset(limit_str.as_deref(), offset_str.as_deref());
477            sql = sql.replace("{{pagination}}", &pagination_sql);
478        }
479
480        // Clean up any remaining empty placeholders
481        sql = sql.replace("{{where_clause}}", "");
482        sql = sql.replace("{{order_by}}", "");
483        sql = sql.replace("{{pagination}}", "");
484
485        // Clean up extra whitespace
486        sql.split_whitespace().collect::<Vec<_>>().join(" ")
487    }
488
489    /// Convert a JSON value to SQL literal.
490    fn value_to_sql(&self, value: &serde_json::Value) -> String {
491        match value {
492            serde_json::Value::Null => "NULL".to_string(),
493            serde_json::Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
494            serde_json::Value::Number(n) => n.to_string(),
495            serde_json::Value::String(s) => format!("'{}'", s.replace('\'', "''")),
496            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
497                format!("'{}'", value.to_string().replace('\'', "''"))
498            },
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::{
506        super::ir::{AutoParams, IRArgument},
507        *,
508    };
509
510    #[test]
511    fn test_sql_template_generator_new() {
512        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
513        assert_eq!(generator.target(), DatabaseTarget::PostgreSQL);
514    }
515
516    #[test]
517    fn test_database_target_equality() {
518        assert_eq!(DatabaseTarget::PostgreSQL, DatabaseTarget::PostgreSQL);
519        assert_ne!(DatabaseTarget::PostgreSQL, DatabaseTarget::MySQL);
520    }
521
522    #[test]
523    fn test_database_target_placeholder() {
524        assert_eq!(DatabaseTarget::PostgreSQL.placeholder(1), "$1");
525        assert_eq!(DatabaseTarget::PostgreSQL.placeholder(5), "$5");
526        assert_eq!(DatabaseTarget::MySQL.placeholder(1), "?");
527        assert_eq!(DatabaseTarget::SQLite.placeholder(1), "?");
528        assert_eq!(DatabaseTarget::SQLServer.placeholder(1), "@p1");
529    }
530
531    #[test]
532    fn test_database_target_quote_identifier() {
533        assert_eq!(DatabaseTarget::PostgreSQL.quote_identifier("users"), "\"users\"");
534        assert_eq!(DatabaseTarget::MySQL.quote_identifier("users"), "`users`");
535        assert_eq!(DatabaseTarget::SQLite.quote_identifier("users"), "\"users\"");
536        assert_eq!(DatabaseTarget::SQLServer.quote_identifier("users"), "[users]");
537    }
538
539    #[test]
540    fn test_database_target_jsonb_extract() {
541        assert_eq!(DatabaseTarget::PostgreSQL.jsonb_extract("data", "name"), "data->'name'");
542        assert_eq!(
543            DatabaseTarget::MySQL.jsonb_extract("data", "name"),
544            "JSON_EXTRACT(data, '$.name')"
545        );
546        assert_eq!(
547            DatabaseTarget::SQLite.jsonb_extract("data", "name"),
548            "json_extract(data, '$.name')"
549        );
550        assert_eq!(
551            DatabaseTarget::SQLServer.jsonb_extract("data", "name"),
552            "JSON_VALUE(data, '$.name')"
553        );
554    }
555
556    #[test]
557    fn test_database_target_limit_offset() {
558        // PostgreSQL/MySQL/SQLite
559        assert_eq!(DatabaseTarget::PostgreSQL.limit_offset(Some("10"), None), "LIMIT 10");
560        assert_eq!(
561            DatabaseTarget::PostgreSQL.limit_offset(Some("10"), Some("5")),
562            "LIMIT 10 OFFSET 5"
563        );
564        assert_eq!(DatabaseTarget::PostgreSQL.limit_offset(None, Some("5")), "OFFSET 5");
565
566        // SQL Server
567        assert_eq!(DatabaseTarget::SQLServer.limit_offset(Some("10"), None), "TOP 10");
568        assert_eq!(
569            DatabaseTarget::SQLServer.limit_offset(Some("10"), Some("5")),
570            "OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY"
571        );
572    }
573
574    #[test]
575    fn test_generate_query_template_basic() {
576        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
577
578        let query = IRQuery {
579            name:         "users".to_string(),
580            return_type:  "User".to_string(),
581            returns_list: true,
582            nullable:     false,
583            arguments:    vec![],
584            sql_source:   Some("v_user".to_string()),
585            description:  None,
586            auto_params:  AutoParams::default(),
587        };
588
589        let template = generator.generate_query_template(&query);
590
591        assert_eq!(template.name, "users");
592        assert_eq!(template.kind, TemplateKind::Query);
593        assert_eq!(template.template, "SELECT data FROM \"v_user\"");
594        assert!(!template.supports_where);
595        assert!(!template.supports_order_by);
596        assert!(!template.supports_pagination);
597    }
598
599    #[test]
600    fn test_generate_query_template_with_auto_params() {
601        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
602
603        let query = IRQuery {
604            name:         "users".to_string(),
605            return_type:  "User".to_string(),
606            returns_list: true,
607            nullable:     false,
608            arguments:    vec![],
609            sql_source:   Some("v_user".to_string()),
610            description:  None,
611            auto_params:  AutoParams {
612                has_where:    true,
613                has_order_by: true,
614                has_limit:    true,
615                has_offset:   true,
616            },
617        };
618
619        let template = generator.generate_query_template(&query);
620
621        assert!(template.template.contains("{{where_clause}}"));
622        assert!(template.template.contains("{{order_by}}"));
623        assert!(template.template.contains("{{pagination}}"));
624        assert!(template.supports_where);
625        assert!(template.supports_order_by);
626        assert!(template.supports_pagination);
627    }
628
629    #[test]
630    fn test_generate_query_template_with_arguments() {
631        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
632
633        let query = IRQuery {
634            name:         "user".to_string(),
635            return_type:  "User".to_string(),
636            returns_list: false,
637            nullable:     true,
638            arguments:    vec![IRArgument {
639                name:          "id".to_string(),
640                arg_type:      "ID!".to_string(),
641                nullable:      false,
642                default_value: None,
643                description:   None,
644            }],
645            sql_source:   Some("v_user".to_string()),
646            description:  None,
647            auto_params:  AutoParams::default(),
648        };
649
650        let template = generator.generate_query_template(&query);
651
652        assert_eq!(template.parameters, vec!["id"]);
653        assert!(template.supports_where); // Arguments imply WHERE support
654    }
655
656    #[test]
657    fn test_generate_mutation_template_insert() {
658        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
659
660        let mutation = IRMutation {
661            name:        "createUser".to_string(),
662            return_type: "User".to_string(),
663            nullable:    false,
664            arguments:   vec![IRArgument {
665                name:          "input".to_string(),
666                arg_type:      "CreateUserInput!".to_string(),
667                nullable:      false,
668                default_value: None,
669                description:   None,
670            }],
671            description: None,
672            operation:   MutationOperation::Create,
673        };
674
675        let template = generator.generate_mutation_template(&mutation);
676
677        assert_eq!(template.name, "createUser");
678        assert_eq!(template.kind, TemplateKind::Insert);
679        assert!(template.template.contains("INSERT INTO"));
680        assert!(template.template.contains("{input}"));
681        assert!(template.template.contains("RETURNING data"));
682    }
683
684    #[test]
685    fn test_generate_mutation_template_update() {
686        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
687
688        let mutation = IRMutation {
689            name:        "updateUser".to_string(),
690            return_type: "User".to_string(),
691            nullable:    false,
692            arguments:   vec![
693                IRArgument {
694                    name:          "id".to_string(),
695                    arg_type:      "ID!".to_string(),
696                    nullable:      false,
697                    default_value: None,
698                    description:   None,
699                },
700                IRArgument {
701                    name:          "input".to_string(),
702                    arg_type:      "UpdateUserInput!".to_string(),
703                    nullable:      false,
704                    default_value: None,
705                    description:   None,
706                },
707            ],
708            description: None,
709            operation:   MutationOperation::Update,
710        };
711
712        let template = generator.generate_mutation_template(&mutation);
713
714        assert_eq!(template.name, "updateUser");
715        assert_eq!(template.kind, TemplateKind::Update);
716        assert!(template.template.contains("UPDATE"));
717        assert!(template.template.contains("{id}"));
718        assert!(template.template.contains("RETURNING data"));
719    }
720
721    #[test]
722    fn test_generate_mutation_template_delete() {
723        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
724
725        let mutation = IRMutation {
726            name:        "deleteUser".to_string(),
727            return_type: "User".to_string(),
728            nullable:    false,
729            arguments:   vec![IRArgument {
730                name:          "id".to_string(),
731                arg_type:      "ID!".to_string(),
732                nullable:      false,
733                default_value: None,
734                description:   None,
735            }],
736            description: None,
737            operation:   MutationOperation::Delete,
738        };
739
740        let template = generator.generate_mutation_template(&mutation);
741
742        assert_eq!(template.name, "deleteUser");
743        assert_eq!(template.kind, TemplateKind::Delete);
744        assert!(template.template.contains("DELETE FROM"));
745        assert!(template.template.contains("{id}"));
746    }
747
748    #[test]
749    fn test_generate_all_templates() {
750        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
751        let mut ir = AuthoringIR::new();
752
753        ir.queries.push(IRQuery {
754            name:         "users".to_string(),
755            return_type:  "User".to_string(),
756            returns_list: true,
757            nullable:     false,
758            arguments:    vec![],
759            sql_source:   Some("v_user".to_string()),
760            description:  None,
761            auto_params:  AutoParams::default(),
762        });
763
764        ir.mutations.push(IRMutation {
765            name:        "createUser".to_string(),
766            return_type: "User".to_string(),
767            nullable:    false,
768            arguments:   vec![],
769            description: None,
770            operation:   MutationOperation::Create,
771        });
772
773        let templates = generator.generate(&ir).unwrap();
774
775        assert_eq!(templates.len(), 2);
776        assert_eq!(templates[0].name, "users");
777        assert_eq!(templates[1].name, "createUser");
778    }
779
780    #[test]
781    fn test_expand_template_basic() {
782        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
783
784        let template = SqlTemplate::query(
785            "users".to_string(),
786            "SELECT data FROM \"v_user\"".to_string(),
787            vec![],
788        );
789
790        let params = std::collections::HashMap::new();
791        let sql = generator.expand_template(&template, &params, None, None, None, None);
792
793        assert_eq!(sql, "SELECT data FROM \"v_user\"");
794    }
795
796    #[test]
797    fn test_expand_template_with_where() {
798        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
799
800        let template = SqlTemplate::query(
801            "users".to_string(),
802            "SELECT data FROM \"v_user\" {{where_clause}}".to_string(),
803            vec![],
804        )
805        .with_where();
806
807        let params = std::collections::HashMap::new();
808        let sql = generator.expand_template(
809            &template,
810            &params,
811            Some("data->>'status' = 'active'"),
812            None,
813            None,
814            None,
815        );
816
817        assert_eq!(sql, "SELECT data FROM \"v_user\" WHERE data->>'status' = 'active'");
818    }
819
820    #[test]
821    fn test_expand_template_with_pagination() {
822        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
823
824        let template = SqlTemplate::query(
825            "users".to_string(),
826            "SELECT data FROM \"v_user\" {{pagination}}".to_string(),
827            vec![],
828        )
829        .with_pagination();
830
831        let params = std::collections::HashMap::new();
832        let sql = generator.expand_template(&template, &params, None, None, Some(10), Some(5));
833
834        assert_eq!(sql, "SELECT data FROM \"v_user\" LIMIT 10 OFFSET 5");
835    }
836
837    #[test]
838    fn test_expand_template_with_order_by() {
839        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
840
841        let template = SqlTemplate::query(
842            "users".to_string(),
843            "SELECT data FROM \"v_user\" {{order_by}}".to_string(),
844            vec![],
845        )
846        .with_order_by();
847
848        let params = std::collections::HashMap::new();
849        let sql = generator.expand_template(
850            &template,
851            &params,
852            None,
853            Some("data->>'name' ASC"),
854            None,
855            None,
856        );
857
858        assert_eq!(sql, "SELECT data FROM \"v_user\" ORDER BY data->>'name' ASC");
859    }
860
861    #[test]
862    fn test_value_to_sql() {
863        let generator = SqlTemplateGenerator::new(DatabaseTarget::PostgreSQL);
864
865        assert_eq!(generator.value_to_sql(&serde_json::Value::Null), "NULL");
866        assert_eq!(generator.value_to_sql(&serde_json::json!(true)), "TRUE");
867        assert_eq!(generator.value_to_sql(&serde_json::json!(false)), "FALSE");
868        assert_eq!(generator.value_to_sql(&serde_json::json!(42)), "42");
869        assert_eq!(generator.value_to_sql(&serde_json::json!(1.5)), "1.5");
870        assert_eq!(generator.value_to_sql(&serde_json::json!("hello")), "'hello'");
871        assert_eq!(generator.value_to_sql(&serde_json::json!("it's")), "'it''s'"); // SQL escaping
872    }
873
874    #[test]
875    fn test_mysql_query_template() {
876        let generator = SqlTemplateGenerator::new(DatabaseTarget::MySQL);
877
878        let query = IRQuery {
879            name:         "users".to_string(),
880            return_type:  "User".to_string(),
881            returns_list: true,
882            nullable:     false,
883            arguments:    vec![],
884            sql_source:   Some("v_user".to_string()),
885            description:  None,
886            auto_params:  AutoParams::default(),
887        };
888
889        let template = generator.generate_query_template(&query);
890
891        // MySQL uses backticks for identifiers
892        assert!(template.template.contains("`v_user`"));
893    }
894
895    #[test]
896    fn test_sqlserver_query_template() {
897        let generator = SqlTemplateGenerator::new(DatabaseTarget::SQLServer);
898
899        let query = IRQuery {
900            name:         "users".to_string(),
901            return_type:  "User".to_string(),
902            returns_list: true,
903            nullable:     false,
904            arguments:    vec![],
905            sql_source:   Some("v_user".to_string()),
906            description:  None,
907            auto_params:  AutoParams::default(),
908        };
909
910        let template = generator.generate_query_template(&query);
911
912        // SQL Server uses square brackets for identifiers
913        assert!(template.template.contains("[v_user]"));
914    }
915}