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