Skip to main content

drizzle_cli/commands/
introspect.rs

1//! Introspect command implementation
2//!
3//! Introspects an existing database and generates a snapshot/schema.
4
5use crate::commands::overrides::{self, ConnectionOverrides, FilterArgs};
6use crate::config::{Config, Dialect, IntrospectCasing};
7use crate::error::CliError;
8use crate::output;
9
10#[derive(clap::Args, Debug, Clone)]
11pub struct IntrospectOptions {
12    /// Initialize migration metadata after introspecting
13    #[arg(long = "init")]
14    pub init_metadata: bool,
15
16    /// Casing for introspected identifiers (camel or preserve)
17    #[arg(long)]
18    pub casing: Option<IntrospectCasing>,
19
20    /// Override output directory
21    #[arg(long)]
22    pub out: Option<std::path::PathBuf>,
23
24    /// Override breakpoints setting
25    #[arg(long)]
26    pub breakpoints: Option<bool>,
27
28    /// Override dialect from config
29    #[arg(long)]
30    pub dialect: Option<Dialect>,
31
32    #[command(flatten)]
33    pub filters: FilterArgs,
34
35    #[command(flatten)]
36    pub connection: ConnectionOverrides,
37}
38
39/// Run the introspect command.
40///
41/// # Errors
42///
43/// Returns [`CliError`] if the requested database cannot be resolved,
44/// credentials are missing or invalid, connecting to the database fails, or
45/// writing the generated Rust schema files fails.
46pub fn run(
47    config: &Config,
48    db_name: Option<&str>,
49    opts: &IntrospectOptions,
50) -> Result<(), CliError> {
51    let db = config.database(db_name)?;
52
53    // CLI flags override config
54    let effective_casing = opts
55        .casing
56        .unwrap_or_else(|| db.effective_introspect_casing());
57    let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
58    let effective_out = opts.out.as_deref().unwrap_or_else(|| db.migrations_dir());
59    let effective_breakpoints = opts.breakpoints.unwrap_or(db.breakpoints);
60
61    if effective_dialect != Dialect::Postgresql {
62        if opts
63            .filters
64            .schema_filters
65            .as_ref()
66            .is_some_and(|v| !v.is_empty())
67        {
68            println!(
69                "{}",
70                output::warning("Ignoring --schemaFilters: only supported for postgresql")
71            );
72        }
73        if opts
74            .filters
75            .extensions_filters
76            .as_ref()
77            .is_some_and(|v| !v.is_empty())
78        {
79            println!(
80                "{}",
81                output::warning("Ignoring --extensionsFilters: only supported for postgresql")
82            );
83        }
84    }
85
86    let filters = crate::db::SnapshotFilters {
87        tables: overrides::resolve_filter_list(
88            opts.filters.tables_filter.as_deref(),
89            db.tables_filter.as_ref(),
90        ),
91        schemas: overrides::resolve_schema_filters(
92            effective_dialect,
93            opts.filters.schema_filters.as_deref(),
94            db.schema_filter.as_ref(),
95        ),
96        extensions: overrides::resolve_extensions_filter(
97            opts.filters.extensions_filters.as_deref(),
98            db.extensions_filters.as_deref(),
99        ),
100    };
101
102    println!("{}", output::heading("Introspecting database..."));
103    println!();
104
105    crate::commands::harness::print_db_header(config, db_name);
106
107    println!(
108        "  {}: {}",
109        output::label("Dialect"),
110        effective_dialect.as_str()
111    );
112    if let Some(ref driver) = db.driver {
113        println!("  {}: {:?}", output::label("Driver"), driver);
114    }
115    println!("  {}: {}", output::label("Output"), effective_out.display());
116
117    if opts.init_metadata {
118        println!("  {}: enabled", output::label("Init metadata"));
119    }
120    println!();
121
122    // Get credentials
123    let credentials = overrides::resolve_credentials(db, effective_dialect, &opts.connection)?;
124
125    let Some(credentials) = credentials else {
126        print_missing_credentials_help(effective_dialect);
127        return Ok(());
128    };
129
130    // Run introspection
131    let result = crate::db::run_introspection(
132        &credentials,
133        effective_dialect,
134        effective_out,
135        opts.init_metadata,
136        effective_breakpoints,
137        Some(effective_casing),
138        &filters,
139        db.migrations_table(),
140        db.migrations_schema(),
141    )?;
142
143    print_introspection_summary(&result, opts.init_metadata);
144    Ok(())
145}
146
147/// Print a helpful message when no database credentials are configured.
148fn print_missing_credentials_help(effective_dialect: Dialect) {
149    println!("{}", output::warning("No database credentials configured."));
150    println!();
151    println!("Add credentials to your drizzle.config.toml:");
152    println!();
153    println!("  {}", output::muted("[dbCredentials]"));
154    match effective_dialect.to_base() {
155        drizzle_types::Dialect::SQLite => {
156            println!("  {}", output::muted("url = \"./dev.db\""));
157        }
158        drizzle_types::Dialect::PostgreSQL => {
159            println!(
160                "  {}",
161                output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
162            );
163        }
164        drizzle_types::Dialect::MySQL => {
165            // drizzle-cli doesn't currently support MySQL end-to-end, but the base
166            // dialect type includes it, so keep the match exhaustive.
167            println!(
168                "  {}",
169                output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
170            );
171        }
172    }
173    println!();
174    println!("Or use an environment variable:");
175    println!();
176    println!("  {}", output::muted("[dbCredentials]"));
177    println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
178}
179
180/// Print the final summary after introspection completes.
181fn print_introspection_summary(result: &crate::db::IntrospectResult, init_metadata: bool) {
182    println!();
183    println!(
184        "  {} {} table(s), {} index(es)",
185        output::success("Found"),
186        result.table_count,
187        result.index_count
188    );
189
190    if result.view_count > 0 {
191        println!(
192            "  {} {} view(s)",
193            output::success("Found"),
194            result.view_count
195        );
196    }
197
198    println!();
199    println!(
200        "{} Snapshot saved to {}",
201        output::success("Done!"),
202        result.snapshot_path.display()
203    );
204
205    if init_metadata {
206        println!();
207        println!(
208            "  {} Migration metadata initialized in database.",
209            output::label("Note:")
210        );
211        println!("  The current database state is now the baseline for future migrations.");
212    }
213}