drizzle_cli/commands/
introspect.rs1use 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 #[arg(long = "init")]
13 pub init_metadata: bool,
14
15 #[arg(long)]
17 pub casing: Option<IntrospectCasing>,
18
19 #[arg(long)]
21 pub out: Option<std::path::PathBuf>,
22
23 #[arg(long)]
25 pub breakpoints: Option<bool>,
26
27 #[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
38pub 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 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 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
152fn 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
181pub(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
210fn 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}