Skip to main content

drizzle_cli/commands/
introspect.rs

1//! `drizzle introspect` / `drizzle pull`: reads a live database and writes a
2//! Rust schema file (and optionally initial migration metadata).
3
4use crate::commands::overrides::{self, ConnectionOverrides, FilterArgs};
5use crate::config::{Config, Dialect, IntrospectCasing};
6use crate::error::CliError;
7use crate::output;
8
9#[derive(clap::Args, Debug, Clone)]
10pub struct IntrospectOptions {
11    /// Initialize migration metadata after introspecting
12    #[arg(long = "init")]
13    pub init_metadata: bool,
14
15    /// Casing for introspected identifiers (camel or preserve)
16    #[arg(long)]
17    pub casing: Option<IntrospectCasing>,
18
19    /// Override output directory
20    #[arg(long)]
21    pub out: Option<std::path::PathBuf>,
22
23    /// Override breakpoints setting
24    #[arg(long)]
25    pub breakpoints: Option<bool>,
26
27    /// Override dialect from config
28    #[arg(long)]
29    pub dialect: Option<Dialect>,
30
31    #[command(flatten)]
32    pub filters: FilterArgs,
33
34    #[command(flatten)]
35    pub connection: ConnectionOverrides,
36}
37
38/// Runs `drizzle introspect` (alias `drizzle pull`): reads the live schema
39/// and writes it as Rust to `<out>/schema.rs`, plus a baseline migration
40/// folder `<out>/<tag>/` whose `migration.sql` creates that schema.
41///
42/// The migrations tracking table is left out, and the table, schema,
43/// extension, and role filters are applied. With `--init`, the baseline is
44/// also recorded as applied in the database, which is allowed only when the
45/// tracking table is empty and the folder holds no other migration. Without
46/// it, the baseline's SQL is commented out, as drizzle-kit writes it.
47///
48/// # Errors
49///
50/// Returns [`CliError`] if `db_name` does not match the config, there are no
51/// credentials ([`CliError::MissingCredentials`]) or no driver for them,
52/// connecting or reading the catalog fails, the folder uses the legacy
53/// journal layout, files cannot be written, or `--init` is refused.
54pub fn run(
55    config: &Config,
56    db_name: Option<&str>,
57    opts: &IntrospectOptions,
58) -> Result<(), CliError> {
59    let db = config.database(db_name)?;
60
61    // CLI flags override config
62    let effective_casing = opts
63        .casing
64        .unwrap_or_else(|| db.effective_introspect_casing());
65    let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
66    let effective_out = opts.out.as_deref().unwrap_or_else(|| db.migrations_dir());
67    let effective_breakpoints = opts.breakpoints.unwrap_or(db.breakpoints);
68
69    if effective_dialect != Dialect::Postgresql {
70        if opts
71            .filters
72            .schema_filters
73            .as_ref()
74            .is_some_and(|v| !v.is_empty())
75        {
76            println!(
77                "{}",
78                output::warning("Ignoring --schemaFilters: only supported for postgresql")
79            );
80        }
81        if opts
82            .filters
83            .extensions_filters
84            .as_ref()
85            .is_some_and(|v| !v.is_empty())
86        {
87            println!(
88                "{}",
89                output::warning("Ignoring --extensionsFilters: only supported for postgresql")
90            );
91        }
92    }
93
94    let filters = crate::db::SnapshotFilters {
95        tables: overrides::resolve_filter_list(
96            opts.filters.tables_filter.as_deref(),
97            db.tables_filter.as_ref(),
98        ),
99        schemas: overrides::resolve_schema_filters(
100            effective_dialect,
101            opts.filters.schema_filters.as_deref(),
102            db.schema_filter.as_ref(),
103        ),
104        extensions: overrides::resolve_extensions_filter(
105            effective_dialect,
106            opts.filters.extensions_filters.as_deref(),
107            db.extensions_filters.as_deref(),
108        ),
109        roles: Some(db.effective_entities().roles),
110    };
111
112    println!("{}", output::heading("Introspecting database..."));
113    println!();
114
115    crate::commands::harness::print_db_header(config, db_name);
116
117    println!(
118        "  {}: {}",
119        output::label("Dialect"),
120        effective_dialect.as_str()
121    );
122    println!("  {}: {}", output::label("Output"), effective_out.display());
123
124    if opts.init_metadata {
125        println!("  {}: enabled", output::label("Init metadata"));
126    }
127    println!();
128
129    let connection = overrides::resolve_connection(db, effective_dialect, &opts.connection)?;
130    let Some(connection) = connection else {
131        print_missing_credentials_help(effective_dialect);
132        return Err(CliError::MissingCredentials("introspect"));
133    };
134    println!("  {}: {}", output::label("Driver"), connection.driver);
135
136    // Run introspection
137    let result = crate::db::run_introspection(
138        &connection,
139        effective_out,
140        opts.init_metadata,
141        effective_breakpoints,
142        Some(effective_casing),
143        &filters,
144        db.migrations_table(),
145        db.migrations_schema(),
146    )?;
147
148    print_introspection_summary(&result, opts.init_metadata);
149    Ok(())
150}
151
152/// Print how to configure credentials; the caller reports the failure.
153fn print_missing_credentials_help(effective_dialect: Dialect) {
154    println!("Add credentials to your drizzle.config.toml:");
155    println!();
156    println!("  {}", output::muted("[dbCredentials]"));
157    match effective_dialect.to_base() {
158        drizzle_types::Dialect::SQLite => {
159            println!("  {}", output::muted("url = \"./dev.db\""));
160        }
161        drizzle_types::Dialect::PostgreSQL => {
162            println!(
163                "  {}",
164                output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
165            );
166        }
167        drizzle_types::Dialect::MySQL => {
168            println!(
169                "  {}",
170                output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
171            );
172        }
173    }
174    println!();
175    println!("Or use an environment variable:");
176    println!();
177    println!("  {}", output::muted("[dbCredentials]"));
178    println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
179}
180
181/// Lines reporting what a generated schema holds, plus the codegen warnings
182/// (objects the schema could not express, and similar notes). Shared with
183/// `drizzle import`.
184pub(crate) fn summary_lines(
185    table_count: usize,
186    index_count: usize,
187    view_count: usize,
188    warnings: &[String],
189) -> Vec<String> {
190    let mut lines = vec![format!(
191        "  {} {table_count} table(s), {index_count} index(es)",
192        output::success("Found"),
193    )];
194    if view_count > 0 {
195        lines.push(format!(
196            "  {} {view_count} view(s)",
197            output::success("Found")
198        ));
199    }
200    if !warnings.is_empty() {
201        lines.push(String::new());
202        lines.push(output::warning("Warnings:"));
203        for warning in warnings {
204            lines.push(format!("  {} {warning}", output::warning("-")));
205        }
206    }
207    lines
208}
209
210/// Print the final summary after introspection completes.
211fn print_introspection_summary(result: &crate::db::IntrospectResult, init_metadata: bool) {
212    println!();
213    for line in summary_lines(
214        result.table_count,
215        result.index_count,
216        result.view_count,
217        &result.warnings,
218    ) {
219        println!("{line}");
220    }
221
222    println!();
223    println!(
224        "{} Snapshot saved to {}",
225        output::success("Done!"),
226        result.snapshot_path.display()
227    );
228
229    if init_metadata {
230        println!();
231        println!(
232            "  {} Migration metadata initialized in database.",
233            output::label("Note:")
234        );
235        println!("  The current database state is now the baseline for future migrations.");
236    }
237}