Skip to main content

drizzle_cli/commands/
export.rs

1//! `drizzle export`: prints the schema as `CREATE` SQL (no database needed).
2
3use std::path::PathBuf;
4
5use crate::commands::overrides;
6use crate::config::Config;
7use crate::config::Dialect;
8use crate::error::CliError;
9use crate::output;
10use drizzle_migrations::schema::Snapshot;
11
12#[derive(clap::Args, Debug, Clone)]
13pub struct ExportOptions {
14    /// Output SQL to a file (default: stdout)
15    #[arg(long = "sql")]
16    pub output_path: Option<PathBuf>,
17
18    /// Override dialect from config
19    #[arg(long)]
20    pub dialect: Option<Dialect>,
21
22    /// Override schema path(s)
23    #[arg(long, value_delimiter = ',')]
24    pub schema: Option<Vec<String>>,
25}
26
27/// Runs `drizzle export`: parses the schema files and prints the SQL that
28/// creates the whole schema from scratch (the same statements a first
29/// `drizzle generate` would write), or writes it to the `--sql` file.
30///
31/// Statements are joined with `--> statement-breakpoint` lines when the
32/// config's `breakpoints` is on (the default), otherwise with blank lines.
33///
34/// # Errors
35///
36/// Returns [`CliError`] if `db_name` does not match the config, no schema
37/// files are found, a schema file cannot be read or has parse errors, the
38/// SQL cannot be planned, or the output file cannot be written.
39pub fn run(config: &Config, db_name: Option<&str>, opts: ExportOptions) -> Result<(), CliError> {
40    use drizzle_migrations::parser::SchemaParser;
41
42    let db = config.database(db_name)?;
43    let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
44
45    crate::commands::harness::print_db_header(config, db_name);
46
47    println!("{}", output::heading("Exporting schema as SQL..."));
48    println!();
49
50    println!(
51        "  {}: {}",
52        output::label("Dialect"),
53        effective_dialect.as_str()
54    );
55
56    // Parse schema files
57    let schema_files = overrides::resolve_schema_files(db, opts.schema.as_deref())?;
58    if schema_files.is_empty() {
59        return Err(CliError::NoSchemaFiles(overrides::resolve_schema_display(
60            db,
61            opts.schema.as_deref(),
62        )));
63    }
64
65    println!(
66        "  {} {} schema file(s)",
67        output::label("Parsing"),
68        schema_files.len()
69    );
70
71    let mut combined_code = String::new();
72    for path in &schema_files {
73        let code = std::fs::read_to_string(path)
74            .map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
75        combined_code.push_str(&code);
76        combined_code.push('\n');
77    }
78
79    let parse_result = SchemaParser::parse(&combined_code);
80    crate::snapshot::surface_parse_diagnostics(&parse_result)?;
81
82    println!(
83        "  {} {} table(s), {} index(es), {} enum(s), {} view(s)",
84        output::label("Found"),
85        parse_result.tables.len(),
86        parse_result.indexes.len(),
87        parse_result.enums.len(),
88        parse_result.views.len(),
89    );
90
91    // Build snapshot from parsed schema (use config dialect)
92    let dialect = effective_dialect.to_base();
93    let snapshot = Snapshot::from_parse_result(&parse_result, dialect, db.casing);
94    if snapshot.is_empty() {
95        println!("{}", output::warning("No schema entities found."));
96        return Ok(());
97    }
98
99    // Use the public planner so export has the same validation, dependency
100    // ordering, and SQL rendering call stack as generated migrations.
101    let plan = generate_create_plan(&snapshot)?;
102    let sql_statements = &plan.statements;
103
104    if sql_statements.is_empty() {
105        println!("{}", output::warning("No SQL statements generated."));
106        return Ok(());
107    }
108
109    let sql_content = if db.breakpoints {
110        plan.to_sql()
111    } else {
112        sql_statements.join("\n\n")
113    };
114
115    // Output to file or stdout
116    if let Some(path) = opts.output_path {
117        std::fs::write(&path, &sql_content)
118            .map_err(|e| CliError::IoError(format!("Failed to write {}: {}", path.display(), e)))?;
119        println!();
120        println!(
121            "{}",
122            output::success(&format!(
123                "Exported {} SQL statement(s) to {}",
124                sql_statements.len(),
125                path.display()
126            ))
127        );
128    } else {
129        println!();
130        println!("{}", output::muted("-- Generated SQL --"));
131        println!();
132        println!("{sql_content}");
133        println!();
134        println!("{}", output::muted("-- End of SQL --"));
135    }
136
137    Ok(())
138}
139
140/// Plan CREATE SQL from an empty snapshot through the shared dialect entrypoint.
141fn generate_create_plan(
142    snapshot: &drizzle_migrations::schema::Snapshot,
143) -> Result<drizzle_migrations::Plan, CliError> {
144    let empty = Snapshot::empty(snapshot.dialect());
145    drizzle_migrations::diff(&empty, snapshot)
146        .map_err(|error| CliError::MigrationError(error.to_string()))
147}