flowscope_cli/
schema.rs

1//! Schema loading from DDL files.
2
3use anyhow::{bail, Context, Result};
4use flowscope_core::{
5    analyze, AnalyzeRequest, ColumnSchema, Dialect, FileSource, SchemaMetadata, SchemaTable,
6    Severity,
7};
8use std::path::Path;
9
10/// Load schema from a DDL file containing CREATE TABLE statements.
11///
12/// Parses the DDL using flowscope-core's analyzer to extract table/column definitions.
13pub fn load_schema_from_ddl(path: &Path, dialect: Dialect) -> Result<SchemaMetadata> {
14    let content = std::fs::read_to_string(path)
15        .with_context(|| format!("Failed to read schema file: {}", path.display()))?;
16
17    parse_schema_ddl(&content, dialect)
18}
19
20/// Parse DDL content to extract schema metadata.
21fn parse_schema_ddl(content: &str, dialect: Dialect) -> Result<SchemaMetadata> {
22    let request = AnalyzeRequest {
23        sql: String::new(),
24        files: Some(vec![FileSource {
25            name: "schema.sql".to_string(),
26            content: content.to_string(),
27        }]),
28        dialect,
29        source_name: None,
30        options: None,
31        schema: Some(SchemaMetadata {
32            allow_implied: true,
33            ..Default::default()
34        }),
35        #[cfg(feature = "templating")]
36        template_config: None,
37    };
38
39    let result = analyze(&request);
40
41    // Check for parsing errors
42    if result.summary.has_errors {
43        let error_messages: Vec<String> = result
44            .issues
45            .iter()
46            .filter(|issue| issue.severity == Severity::Error)
47            .map(|issue| format!("[{}] {}", issue.code, issue.message))
48            .collect();
49
50        bail!("Failed to parse schema DDL:\n{}", error_messages.join("\n"));
51    }
52
53    // Extract schema from resolved_schema
54    let resolved = result
55        .resolved_schema
56        .context("Schema DDL produced no table definitions")?;
57
58    let schema = SchemaMetadata {
59        default_catalog: None,
60        default_schema: None,
61        search_path: None,
62        case_sensitivity: None,
63        tables: resolved
64            .tables
65            .into_iter()
66            .map(|t| SchemaTable {
67                catalog: t.catalog,
68                schema: t.schema,
69                name: t.name,
70                columns: t
71                    .columns
72                    .into_iter()
73                    .map(|c| ColumnSchema {
74                        name: c.name,
75                        data_type: c.data_type,
76                        is_primary_key: c.is_primary_key,
77                        foreign_key: c.foreign_key,
78                    })
79                    .collect(),
80            })
81            .collect(),
82        allow_implied: false, // Don't allow further implied tables in main analysis
83    };
84
85    Ok(schema)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_parse_simple_ddl() {
94        let ddl = r#"
95            CREATE TABLE users (
96                id INT,
97                name VARCHAR(100),
98                email VARCHAR(255)
99            );
100        "#;
101
102        let schema = parse_schema_ddl(ddl, Dialect::Generic).unwrap();
103        assert!(!schema.tables.is_empty());
104
105        let users_table = schema.tables.iter().find(|t| t.name == "users");
106        assert!(users_table.is_some());
107
108        let table = users_table.unwrap();
109        assert_eq!(table.columns.len(), 3);
110    }
111
112    #[test]
113    fn test_parse_multiple_tables() {
114        let ddl = r#"
115            CREATE TABLE users (id INT, name VARCHAR);
116            CREATE TABLE orders (id INT, user_id INT, total DECIMAL);
117        "#;
118
119        let schema = parse_schema_ddl(ddl, Dialect::Generic).unwrap();
120        assert!(schema.tables.len() >= 2);
121    }
122
123    #[test]
124    fn test_parse_invalid_ddl_returns_error() {
125        let ddl = "THIS IS NOT VALID SQL AT ALL ;;;";
126
127        let result = parse_schema_ddl(ddl, Dialect::Generic);
128        assert!(result.is_err());
129
130        let err_msg = result.unwrap_err().to_string();
131        assert!(
132            err_msg.contains("Failed to parse schema DDL"),
133            "Expected error message to mention parsing failure, got: {}",
134            err_msg
135        );
136    }
137}