drizzle_cli/commands/
introspect.rs1use 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 #[arg(long = "init")]
14 pub init_metadata: bool,
15
16 #[arg(long)]
18 pub casing: Option<IntrospectCasing>,
19
20 #[arg(long)]
22 pub out: Option<std::path::PathBuf>,
23
24 #[arg(long)]
26 pub breakpoints: Option<bool>,
27
28 #[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
39pub 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 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 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
145fn 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
174fn 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 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}