data_modelling_sdk/import/
sql.rs1use super::{ColumnData, ImportError, ImportResult, TableData};
15use crate::validation::input::{validate_column_name, validate_data_type, validate_table_name};
16use anyhow::Result;
17use sqlparser::ast::{ColumnDef, ColumnOption, ObjectName, Statement, TableConstraint};
18use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
19use sqlparser::parser::Parser;
20
21pub struct SQLImporter {
23 pub dialect: String,
25}
26
27impl Default for SQLImporter {
28 fn default() -> Self {
29 Self {
30 dialect: "generic".to_string(),
31 }
32 }
33}
34
35impl SQLImporter {
36 pub fn new(dialect: &str) -> Self {
50 Self {
51 dialect: dialect.to_string(),
52 }
53 }
54
55 pub fn parse(&self, sql: &str) -> Result<ImportResult> {
76 let dialect = self.dialect_impl();
77 let statements = match Parser::parse_sql(dialect.as_ref(), sql) {
78 Ok(stmts) => stmts,
79 Err(e) => {
80 return Ok(ImportResult {
81 tables: Vec::new(),
82 tables_requiring_name: Vec::new(),
83 errors: vec![ImportError::ParseError(e.to_string())],
84 ai_suggestions: None,
85 });
86 }
87 };
88
89 let mut tables = Vec::new();
90 let mut errors = Vec::new();
91
92 for (idx, stmt) in statements.into_iter().enumerate() {
93 if let Statement::CreateTable(create) = stmt {
94 match self.parse_create_table(
95 idx,
96 &create.name,
97 &create.columns,
98 &create.constraints,
99 ) {
100 Ok(t) => tables.push(t),
101 Err(e) => errors.push(ImportError::ParseError(e)),
102 }
103 }
104 }
107
108 Ok(ImportResult {
109 tables,
110 tables_requiring_name: Vec::new(),
111 errors,
112 ai_suggestions: None,
113 })
114 }
115
116 pub fn parse_liquibase(&self, sql: &str) -> Result<ImportResult> {
143 let cleaned = sql
148 .lines()
149 .filter(|l| {
150 let t = l.trim_start();
151 if !t.starts_with("--") {
152 return true;
153 }
154 false
156 })
157 .collect::<Vec<_>>()
158 .join("\n");
159
160 self.parse(&cleaned)
161 }
162
163 fn dialect_impl(&self) -> Box<dyn Dialect + Send + Sync> {
164 match self.dialect.to_lowercase().as_str() {
165 "postgres" | "postgresql" => Box::new(PostgreSqlDialect {}),
166 "mysql" => Box::new(MySqlDialect {}),
167 "sqlite" => Box::new(SQLiteDialect {}),
168 _ => Box::new(GenericDialect {}),
169 }
170 }
171
172 fn object_name_to_string(name: &ObjectName) -> String {
173 name.0
175 .last()
176 .map(|ident| ident.value.clone())
177 .unwrap_or_else(|| name.to_string())
178 }
179
180 fn parse_create_table(
181 &self,
182 table_index: usize,
183 name: &ObjectName,
184 columns: &[ColumnDef],
185 constraints: &[TableConstraint],
186 ) -> std::result::Result<TableData, String> {
187 let table_name = Self::object_name_to_string(name);
188
189 if let Err(e) = validate_table_name(&table_name) {
191 tracing::warn!("Table name validation warning: {}", e);
193 }
194
195 let mut pk_cols = std::collections::HashSet::<String>::new();
197 for c in constraints {
198 if let TableConstraint::PrimaryKey { columns, .. } = c {
199 for col in columns {
200 pk_cols.insert(col.value.clone());
201 }
202 }
203 }
204
205 let mut out_cols = Vec::new();
206 for col in columns {
207 let mut nullable = true;
208 let mut is_pk = false;
209
210 for opt_def in &col.options {
211 match &opt_def.option {
212 ColumnOption::NotNull => nullable = false,
213 ColumnOption::Null => nullable = true,
214 ColumnOption::Unique { is_primary, .. } => {
215 if *is_primary {
216 is_pk = true;
217 }
218 }
219 _ => {}
220 }
221 }
222
223 if pk_cols.contains(&col.name.value) {
224 is_pk = true;
225 }
226
227 let col_name = col.name.value.clone();
228 let data_type = col.data_type.to_string();
229
230 if let Err(e) = validate_column_name(&col_name) {
232 tracing::warn!("Column name validation warning for '{}': {}", col_name, e);
233 }
234 if let Err(e) = validate_data_type(&data_type) {
235 tracing::warn!("Data type validation warning for '{}': {}", data_type, e);
236 }
237
238 out_cols.push(ColumnData {
239 name: col_name,
240 data_type,
241 nullable,
242 primary_key: is_pk,
243 description: None,
244 quality: None,
245 ref_path: None,
246 });
247 }
248
249 Ok(TableData {
250 table_index,
251 name: Some(table_name),
252 columns: out_cols,
253 })
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_sql_importer_default() {
263 let importer = SQLImporter::default();
264 assert_eq!(importer.dialect, "generic");
265 }
266
267 #[test]
268 fn test_sql_importer_parse_basic() {
269 let importer = SQLImporter::new("postgres");
270 let result = importer
271 .parse("CREATE TABLE test (id INT PRIMARY KEY, name TEXT NOT NULL);")
272 .unwrap();
273 assert!(result.errors.is_empty());
274 assert_eq!(result.tables.len(), 1);
275 let t = &result.tables[0];
276 assert_eq!(t.name.as_deref(), Some("test"));
277 assert_eq!(t.columns.len(), 2);
278 assert!(t.columns.iter().any(|c| c.name == "id" && c.primary_key));
279 assert!(t.columns.iter().any(|c| c.name == "name" && !c.nullable));
280 }
281
282 #[test]
283 fn test_sql_importer_parse_table_pk_constraint() {
284 let importer = SQLImporter::new("postgres");
285 let result = importer
286 .parse("CREATE TABLE t (id INT, name TEXT, CONSTRAINT pk PRIMARY KEY (id));")
287 .unwrap();
288 assert!(result.errors.is_empty());
289 assert_eq!(result.tables.len(), 1);
290 let t = &result.tables[0];
291 assert!(t.columns.iter().any(|c| c.name == "id" && c.primary_key));
292 }
293
294 #[test]
295 fn test_sql_importer_parse_liquibase_formatted_sql() {
296 let importer = SQLImporter::new("postgres");
297 let result = importer
298 .parse_liquibase(
299 "--liquibase formatted sql\n--changeset user:1\nCREATE TABLE test (id INT);\n",
300 )
301 .unwrap();
302 assert!(result.errors.is_empty());
303 assert_eq!(result.tables.len(), 1);
304 }
305}