Skip to main content

drizzle_cli/commands/
export.rs

1//! Export command implementation
2//!
3//! Exports the schema as SQL statements.
4
5use std::path::PathBuf;
6
7use crate::commands::overrides;
8use crate::config::Config;
9use crate::config::Dialect;
10use crate::error::CliError;
11use crate::output;
12use drizzle_migrations::schema::Snapshot;
13
14#[derive(clap::Args, Debug, Clone)]
15pub struct ExportOptions {
16    /// Output SQL to a file (default: stdout)
17    #[arg(long = "sql")]
18    pub output_path: Option<PathBuf>,
19
20    /// Override dialect from config
21    #[arg(long)]
22    pub dialect: Option<Dialect>,
23
24    /// Override schema path(s)
25    #[arg(long, value_delimiter = ',')]
26    pub schema: Option<Vec<String>>,
27}
28
29/// Run the export command.
30///
31/// # Errors
32///
33/// Returns [`CliError`] if the requested database cannot be resolved, the
34/// schema files cannot be read/parsed, the resolved snapshot cannot be
35/// generated, or if writing the output SQL file fails.
36pub fn run(config: &Config, db_name: Option<&str>, opts: ExportOptions) -> Result<(), CliError> {
37    use drizzle_migrations::parser::SchemaParser;
38
39    let db = config.database(db_name)?;
40    let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
41
42    crate::commands::harness::print_db_header(config, db_name);
43
44    println!("{}", output::heading("Exporting schema as SQL..."));
45    println!();
46
47    println!(
48        "  {}: {}",
49        output::label("Dialect"),
50        effective_dialect.as_str()
51    );
52
53    // Parse schema files
54    let schema_files = overrides::resolve_schema_files(db, opts.schema.as_deref())?;
55    if schema_files.is_empty() {
56        return Err(CliError::NoSchemaFiles(overrides::resolve_schema_display(
57            db,
58            opts.schema.as_deref(),
59        )));
60    }
61
62    println!(
63        "  {} {} schema file(s)",
64        output::label("Parsing"),
65        schema_files.len()
66    );
67
68    let mut combined_code = String::new();
69    for path in &schema_files {
70        let code = std::fs::read_to_string(path)
71            .map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
72        combined_code.push_str(&code);
73        combined_code.push('\n');
74    }
75
76    let parse_result = SchemaParser::parse(&combined_code);
77    crate::snapshot::surface_parse_diagnostics(&parse_result)?;
78
79    if parse_result.tables.is_empty() && parse_result.indexes.is_empty() {
80        println!(
81            "{}",
82            output::warning("No tables or indexes found in schema files.")
83        );
84        return Ok(());
85    }
86
87    println!(
88        "  {} {} table(s), {} index(es)",
89        output::label("Found"),
90        parse_result.tables.len(),
91        parse_result.indexes.len()
92    );
93
94    // Build snapshot from parsed schema (use config dialect)
95    let dialect = effective_dialect.to_base();
96    let snapshot = Snapshot::from_parse_result(&parse_result, dialect, db.casing);
97
98    // Generate SQL from snapshot (create statements for all entities)
99    let sql_statements = generate_create_sql(&snapshot, db.breakpoints);
100
101    if sql_statements.is_empty() {
102        println!("{}", output::warning("No SQL statements generated."));
103        return Ok(());
104    }
105
106    let sql_content = sql_statements.join("\n\n");
107
108    // Output to file or stdout
109    if let Some(path) = opts.output_path {
110        std::fs::write(&path, &sql_content)
111            .map_err(|e| CliError::IoError(format!("Failed to write {}: {}", path.display(), e)))?;
112        println!();
113        println!(
114            "{}",
115            output::success(&format!(
116                "Exported {} SQL statement(s) to {}",
117                sql_statements.len(),
118                path.display()
119            ))
120        );
121    } else {
122        println!();
123        println!("{}", output::muted("-- Generated SQL --"));
124        println!();
125        println!("{sql_content}");
126        println!();
127        println!("{}", output::muted("-- End of SQL --"));
128    }
129
130    Ok(())
131}
132
133/// Generate CREATE SQL statements from a snapshot
134fn generate_create_sql(
135    snapshot: &drizzle_migrations::schema::Snapshot,
136    breakpoints: bool,
137) -> Vec<String> {
138    match snapshot {
139        Snapshot::Sqlite(snap) => {
140            use drizzle_migrations::sqlite::SQLiteSnapshot;
141            use drizzle_migrations::sqlite::diff_snapshots;
142            use drizzle_migrations::sqlite::statements::Generator as SqliteGenerator;
143
144            // Diff against empty snapshot to get all CREATE statements
145            let empty = SQLiteSnapshot::new();
146            let diff = diff_snapshots(&empty, snap);
147            let generator = SqliteGenerator::new().with_breakpoints(breakpoints);
148            generator.generate_migration(&diff)
149        }
150        Snapshot::Postgres(snap) => {
151            use drizzle_migrations::postgres::PostgresSnapshot;
152            use drizzle_migrations::postgres::diff_full_snapshots;
153            use drizzle_migrations::postgres::statements::Generator as PostgresGenerator;
154
155            // Diff against empty snapshot to get all CREATE statements
156            let empty = PostgresSnapshot::new();
157            let diff = diff_full_snapshots(&empty, snap);
158            let generator = PostgresGenerator::new().with_breakpoints(breakpoints);
159            generator.generate(&diff.diffs)
160        }
161    }
162}