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 ([`CliError::MissingCredentials`]) or invalid,
45/// connecting to the database fails, or writing the generated Rust schema
46/// files fails.
47pub fn run(
48    config: &Config,
49    db_name: Option<&str>,
50    opts: &IntrospectOptions,
51) -> Result<(), CliError> {
52    let db = config.database(db_name)?;
53
54    // CLI flags override config
55    let effective_casing = opts
56        .casing
57        .unwrap_or_else(|| db.effective_introspect_casing());
58    let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
59    let effective_out = opts.out.as_deref().unwrap_or_else(|| db.migrations_dir());
60    let effective_breakpoints = opts.breakpoints.unwrap_or(db.breakpoints);
61
62    if effective_dialect != Dialect::Postgresql {
63        if opts
64            .filters
65            .schema_filters
66            .as_ref()
67            .is_some_and(|v| !v.is_empty())
68        {
69            println!(
70                "{}",
71                output::warning("Ignoring --schemaFilters: only supported for postgresql")
72            );
73        }
74        if opts
75            .filters
76            .extensions_filters
77            .as_ref()
78            .is_some_and(|v| !v.is_empty())
79        {
80            println!(
81                "{}",
82                output::warning("Ignoring --extensionsFilters: only supported for postgresql")
83            );
84        }
85    }
86
87    let filters = crate::db::SnapshotFilters {
88        tables: overrides::resolve_filter_list(
89            opts.filters.tables_filter.as_deref(),
90            db.tables_filter.as_ref(),
91        ),
92        schemas: overrides::resolve_schema_filters(
93            effective_dialect,
94            opts.filters.schema_filters.as_deref(),
95            db.schema_filter.as_ref(),
96        ),
97        extensions: overrides::resolve_extensions_filter(
98            effective_dialect,
99            opts.filters.extensions_filters.as_deref(),
100            db.extensions_filters.as_deref(),
101        ),
102        roles: Some(db.effective_entities().roles),
103    };
104
105    println!("{}", output::heading("Introspecting database..."));
106    println!();
107
108    crate::commands::harness::print_db_header(config, db_name);
109
110    println!(
111        "  {}: {}",
112        output::label("Dialect"),
113        effective_dialect.as_str()
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    let connection = overrides::resolve_connection(db, effective_dialect, &opts.connection)?;
123    let Some(connection) = connection else {
124        print_missing_credentials_help(effective_dialect);
125        return Err(CliError::MissingCredentials("introspect"));
126    };
127    println!("  {}: {}", output::label("Driver"), connection.driver);
128
129    // Run introspection
130    let result = crate::db::run_introspection(
131        &connection,
132        effective_out,
133        opts.init_metadata,
134        effective_breakpoints,
135        Some(effective_casing),
136        &filters,
137        db.migrations_table(),
138        db.migrations_schema(),
139    )?;
140
141    print_introspection_summary(&result, opts.init_metadata);
142    Ok(())
143}
144
145/// Print how to configure credentials; the caller reports the failure.
146fn print_missing_credentials_help(effective_dialect: Dialect) {
147    println!("Add credentials to your drizzle.config.toml:");
148    println!();
149    println!("  {}", output::muted("[dbCredentials]"));
150    match effective_dialect.to_base() {
151        drizzle_types::Dialect::SQLite => {
152            println!("  {}", output::muted("url = \"./dev.db\""));
153        }
154        drizzle_types::Dialect::PostgreSQL => {
155            println!(
156                "  {}",
157                output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
158            );
159        }
160        drizzle_types::Dialect::MySQL => {
161            println!(
162                "  {}",
163                output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
164            );
165        }
166    }
167    println!();
168    println!("Or use an environment variable:");
169    println!();
170    println!("  {}", output::muted("[dbCredentials]"));
171    println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
172}
173
174/// Print the final summary after introspection completes.
175fn print_introspection_summary(result: &crate::db::IntrospectResult, init_metadata: bool) {
176    println!();
177    println!(
178        "  {} {} table(s), {} index(es)",
179        output::success("Found"),
180        result.table_count,
181        result.index_count
182    );
183
184    if result.view_count > 0 {
185        println!(
186            "  {} {} view(s)",
187            output::success("Found"),
188            result.view_count
189        );
190    }
191
192    // Objects the generated schema could not express, and similar notes.
193    if !result.warnings.is_empty() {
194        println!();
195        println!("{}", output::warning("Warnings:"));
196        for warning in &result.warnings {
197            println!("  {} {}", output::warning("-"), warning);
198        }
199    }
200
201    println!();
202    println!(
203        "{} Snapshot saved to {}",
204        output::success("Done!"),
205        result.snapshot_path.display()
206    );
207
208    if init_metadata {
209        println!();
210        println!(
211            "  {} Migration metadata initialized in database.",
212            output::label("Note:")
213        );
214        println!("  The current database state is now the baseline for future migrations.");
215    }
216}