data_modelling_core/export/
sql.rs

1//! SQL exporter for generating CREATE TABLE statements from data models.
2//!
3//! # Security
4//!
5//! All identifiers (table names, column names, schema names) are properly quoted
6//! and escaped to prevent SQL injection. Internal quote characters are escaped
7//! by doubling them according to SQL standards.
8
9use crate::export::{ExportError, ExportResult};
10use crate::models::{DataModel, Table};
11
12/// Exporter for SQL CREATE TABLE format.
13pub struct SQLExporter;
14
15impl SQLExporter {
16    /// Export a table to SQL CREATE TABLE statement.
17    ///
18    /// # Arguments
19    ///
20    /// * `table` - The table to export
21    /// * `dialect` - Optional SQL dialect ("postgres", "mysql", "sqlserver", etc.)
22    ///
23    /// # Returns
24    ///
25    /// A SQL CREATE TABLE statement as a string, with proper identifier quoting
26    /// and escaping based on the dialect.
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// use data_modelling_core::export::sql::SQLExporter;
32    /// use data_modelling_core::models::{Table, Column};
33    ///
34    /// let table = Table::new(
35    ///     "users".to_string(),
36    ///     vec![Column::new("id".to_string(), "INT".to_string())],
37    /// );
38    ///
39    /// let sql = SQLExporter::export_table(&table, Some("postgres"));
40    /// // Returns: CREATE TABLE "users" (\n  "id" INT\n);
41    /// ```
42    pub fn export_table(table: &Table, dialect: Option<&str>) -> String {
43        let dialect = dialect.unwrap_or("standard");
44        let mut sql = String::new();
45
46        // CREATE TABLE statement
47        sql.push_str(&format!(
48            "CREATE TABLE {}",
49            Self::quote_identifier(&table.name, dialect)
50        ));
51
52        // Build fully-qualified table name based on catalog and schema
53        sql = match (&table.catalog_name, &table.schema_name) {
54            (Some(catalog), Some(schema)) => {
55                format!(
56                    "CREATE TABLE {}.{}.{}",
57                    Self::quote_identifier(catalog, dialect),
58                    Self::quote_identifier(schema, dialect),
59                    Self::quote_identifier(&table.name, dialect)
60                )
61            }
62            (Some(catalog), None) => {
63                format!(
64                    "CREATE TABLE {}.{}",
65                    Self::quote_identifier(catalog, dialect),
66                    Self::quote_identifier(&table.name, dialect)
67                )
68            }
69            (None, Some(schema)) => {
70                format!(
71                    "CREATE TABLE {}.{}",
72                    Self::quote_identifier(schema, dialect),
73                    Self::quote_identifier(&table.name, dialect)
74                )
75            }
76            (None, None) => sql, // Keep default "CREATE TABLE tablename"
77        };
78
79        sql.push_str(" (\n");
80
81        // Column definitions
82        let mut column_defs = Vec::new();
83        for column in &table.columns {
84            let mut col_def = format!("  {}", Self::quote_identifier(&column.name, dialect));
85            col_def.push(' ');
86            col_def.push_str(&column.data_type);
87
88            if !column.nullable {
89                col_def.push_str(" NOT NULL");
90            }
91
92            if column.primary_key {
93                col_def.push_str(" PRIMARY KEY");
94            }
95
96            if !column.description.is_empty() {
97                // Add comment (dialect-specific)
98                match dialect {
99                    "postgres" | "postgresql" => {
100                        col_def.push_str(&format!(" -- {}", column.description));
101                    }
102                    "mysql" => {
103                        col_def.push_str(&format!(
104                            " COMMENT '{}'",
105                            column.description.replace('\'', "''")
106                        ));
107                    }
108                    _ => {
109                        col_def.push_str(&format!(" -- {}", column.description));
110                    }
111                }
112            }
113
114            column_defs.push(col_def);
115        }
116
117        sql.push_str(&column_defs.join(",\n"));
118        sql.push_str("\n);\n");
119
120        // Add table comment if available (from odcl_metadata)
121        if let Some(desc) = table
122            .odcl_metadata
123            .get("description")
124            .and_then(|v| v.as_str())
125        {
126            match dialect {
127                "postgres" | "postgresql" => {
128                    sql.push_str(&format!(
129                        "COMMENT ON TABLE {} IS '{}';\n",
130                        Self::quote_identifier(&table.name, dialect),
131                        desc.replace('\'', "''")
132                    ));
133                }
134                "mysql" => {
135                    sql.push_str(&format!(
136                        "ALTER TABLE {} COMMENT = '{}';\n",
137                        Self::quote_identifier(&table.name, dialect),
138                        desc.replace("'", "''")
139                    ));
140                }
141                _ => {
142                    // Default: SQL comment
143                    sql.push_str(&format!("-- Table: {}\n", table.name));
144                    sql.push_str(&format!("-- Description: {}\n", desc));
145                }
146            }
147        }
148
149        sql
150    }
151
152    /// Export tables to SQL CREATE TABLE statements (SDK interface).
153    ///
154    /// # Arguments
155    ///
156    /// * `tables` - Slice of tables to export
157    /// * `dialect` - Optional SQL dialect
158    ///
159    /// # Returns
160    ///
161    /// An `ExportResult` containing the SQL statements for all tables.
162    ///
163    /// # Example
164    ///
165    /// ```rust
166    /// use data_modelling_core::export::sql::SQLExporter;
167    /// use data_modelling_core::models::{Table, Column};
168    ///
169    /// let tables = vec![
170    ///     Table::new("users".to_string(), vec![Column::new("id".to_string(), "INT".to_string())]),
171    ///     Table::new("orders".to_string(), vec![Column::new("id".to_string(), "INT".to_string())]),
172    /// ];
173    ///
174    /// let exporter = SQLExporter;
175    /// let result = exporter.export(&tables, Some("postgres")).unwrap();
176    /// assert_eq!(result.format, "sql");
177    /// ```
178    pub fn export(
179        &self,
180        tables: &[Table],
181        dialect: Option<&str>,
182    ) -> Result<ExportResult, ExportError> {
183        let mut sql = String::new();
184        for table in tables {
185            sql.push_str(&Self::export_table(table, dialect));
186            sql.push('\n');
187        }
188        Ok(ExportResult {
189            content: sql,
190            format: "sql".to_string(),
191        })
192    }
193
194    /// Export a data model to SQL CREATE TABLE statements (legacy method for compatibility).
195    pub fn export_model(
196        model: &DataModel,
197        table_ids: Option<&[uuid::Uuid]>,
198        dialect: Option<&str>,
199    ) -> String {
200        let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
201            model
202                .tables
203                .iter()
204                .filter(|t| ids.contains(&t.id))
205                .collect()
206        } else {
207            model.tables.iter().collect()
208        };
209
210        let mut sql = String::new();
211
212        for table in tables_to_export {
213            sql.push_str(&Self::export_table(table, dialect));
214            sql.push('\n');
215        }
216
217        sql
218    }
219
220    /// Quote and escape identifier based on SQL dialect.
221    ///
222    /// # Security
223    ///
224    /// This function properly escapes quote characters within the identifier
225    /// by doubling them, preventing SQL injection attacks.
226    ///
227    /// # Dialects
228    ///
229    /// - **PostgreSQL**: Uses double quotes (`"identifier"`)
230    /// - **MySQL**: Uses backticks (`` `identifier` ``)
231    /// - **SQL Server**: Uses brackets (`[identifier]`)
232    /// - **Standard SQL**: Uses double quotes
233    ///
234    /// # Example
235    ///
236    /// ```rust,no_run
237    /// use data_modelling_core::export::sql::SQLExporter;
238    ///
239    /// // PostgreSQL style
240    /// // let quoted = SQLExporter::quote_identifier("user-name", "postgres");
241    /// // Returns: "user-name"
242    ///
243    /// // MySQL style
244    /// // let quoted = SQLExporter::quote_identifier("user-name", "mysql");
245    /// // Returns: `user-name`
246    /// ```
247    fn quote_identifier(identifier: &str, dialect: &str) -> String {
248        match dialect {
249            "mysql" => {
250                // MySQL uses backticks; escape internal backticks by doubling
251                format!("`{}`", identifier.replace('`', "``"))
252            }
253            "postgres" | "postgresql" => {
254                // PostgreSQL uses double quotes; escape by doubling
255                format!("\"{}\"", identifier.replace('"', "\"\""))
256            }
257            "sqlserver" | "mssql" => {
258                // SQL Server uses brackets; escape ] by doubling
259                format!("[{}]", identifier.replace(']', "]]"))
260            }
261            _ => {
262                // Standard SQL: use double quotes
263                format!("\"{}\"", identifier.replace('"', "\"\""))
264            }
265        }
266    }
267}