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(
54 config: &Config,
55 db_name: Option<&str>,
56 opts: &IntrospectOptions,
57) -> Result<(), CliError> {
58 let db = config.database(db_name)?;
59
60 let effective_casing = opts
62 .casing
63 .unwrap_or_else(|| db.effective_introspect_casing());
64 let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
65 let effective_out = opts.out.as_deref().unwrap_or_else(|| db.migrations_dir());
66 let effective_breakpoints = opts.breakpoints.unwrap_or(db.breakpoints);
67
68 if effective_dialect != Dialect::Postgresql {
69 if opts
70 .filters
71 .schema_filters
72 .as_ref()
73 .is_some_and(|v| !v.is_empty())
74 {
75 println!(
76 "{}",
77 output::warning("Ignoring --schemaFilters: only supported for postgresql")
78 );
79 }
80 if opts
81 .filters
82 .extensions_filters
83 .as_ref()
84 .is_some_and(|v| !v.is_empty())
85 {
86 println!(
87 "{}",
88 output::warning("Ignoring --extensionsFilters: only supported for postgresql")
89 );
90 }
91 }
92
93 let filters = crate::db::SnapshotFilters {
94 tables: overrides::resolve_filter_list(
95 opts.filters.tables_filter.as_deref(),
96 db.tables_filter.as_ref(),
97 ),
98 schemas: overrides::resolve_schema_filters(
99 effective_dialect,
100 opts.filters.schema_filters.as_deref(),
101 db.schema_filter.as_ref(),
102 ),
103 extensions: overrides::resolve_extensions_filter(
104 effective_dialect,
105 opts.filters.extensions_filters.as_deref(),
106 db.extensions_filters.as_deref(),
107 ),
108 roles: Some(db.effective_entities().roles),
109 };
110
111 println!("{}", output::heading("Introspecting database..."));
112 println!();
113
114 crate::commands::harness::print_db_header(config, db_name);
115
116 println!(
117 " {}: {}",
118 output::label("Dialect"),
119 effective_dialect.as_str()
120 );
121 println!(" {}: {}", output::label("Output"), effective_out.display());
122
123 if opts.init_metadata {
124 println!(" {}: enabled", output::label("Init metadata"));
125 }
126 println!();
127
128 let connection = overrides::resolve_connection(db, effective_dialect, &opts.connection)?;
129 let Some(connection) = connection else {
130 print_missing_credentials_help(effective_dialect);
131 return Err(CliError::MissingCredentials("introspect"));
132 };
133 println!(" {}: {}", output::label("Driver"), connection.driver);
134
135 let result = crate::db::run_introspection(
137 &connection,
138 effective_out,
139 opts.init_metadata,
140 effective_breakpoints,
141 Some(effective_casing),
142 &filters,
143 db.migrations_table(),
144 db.migrations_schema(),
145 )?;
146
147 print_introspection_summary(&result, opts.init_metadata);
148 Ok(())
149}
150
151fn print_missing_credentials_help(effective_dialect: Dialect) {
153 println!("Add credentials to your drizzle.config.toml:");
154 println!();
155 println!(" {}", output::muted("[dbCredentials]"));
156 match effective_dialect.to_base() {
157 drizzle_types::Dialect::SQLite => {
158 println!(" {}", output::muted("url = \"./dev.db\""));
159 }
160 drizzle_types::Dialect::PostgreSQL => {
161 println!(
162 " {}",
163 output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
164 );
165 }
166 drizzle_types::Dialect::MySQL => {
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
180pub(crate) fn summary_lines(
184 table_count: usize,
185 index_count: usize,
186 view_count: usize,
187 warnings: &[String],
188) -> Vec<String> {
189 let mut lines = vec![format!(
190 " {} {table_count} table(s), {index_count} index(es)",
191 output::success("Found"),
192 )];
193 if view_count > 0 {
194 lines.push(format!(
195 " {} {view_count} view(s)",
196 output::success("Found")
197 ));
198 }
199 if !warnings.is_empty() {
200 lines.push(String::new());
201 lines.push(output::warning("Warnings:"));
202 for warning in warnings {
203 lines.push(format!(" {} {warning}", output::warning("-")));
204 }
205 }
206 lines
207}
208
209fn print_introspection_summary(result: &crate::db::IntrospectResult, init_metadata: bool) {
211 println!();
212 for line in summary_lines(
213 result.table_count,
214 result.index_count,
215 result.view_count,
216 &result.warnings,
217 ) {
218 println!("{line}");
219 }
220
221 println!();
222 println!(
223 "{} Snapshot saved to {}",
224 output::success("Done!"),
225 result.snapshot_path.display()
226 );
227
228 if init_metadata {
229 println!();
230 println!(
231 " {} Migration metadata initialized in database.",
232 output::label("Note:")
233 );
234 println!(" The current database state is now the baseline for future migrations.");
235 }
236}