1use crate::commands::overrides::{self, ConnectionOverrides, FilterArgs};
6use crate::config::{Casing, Config, Dialect};
7use crate::error::CliError;
8use crate::output;
9use drizzle_migrations::schema::Snapshot;
10
11#[derive(clap::Args, Debug, Clone)]
12pub struct PushOptions {
13 #[arg(long)]
15 pub verbose: bool,
16
17 #[arg(long)]
19 pub force: bool,
20
21 #[arg(long)]
23 pub explain: bool,
24
25 #[arg(long)]
27 pub casing: Option<Casing>,
28
29 #[arg(long)]
31 pub dialect: Option<Dialect>,
32
33 #[arg(long, value_delimiter = ',')]
35 pub schema: Option<Vec<String>>,
36
37 #[command(flatten)]
38 pub filters: FilterArgs,
39
40 #[command(flatten)]
41 pub connection: ConnectionOverrides,
42
43 #[command(flatten)]
44 pub hints: crate::commands::renames::HintArgs,
45}
46
47pub fn run(config: &Config, db_name: Option<&str>, opts: &PushOptions) -> Result<(), CliError> {
70 let db = config.database(db_name)?;
71
72 let verbose = opts.verbose || db.verbose;
74 let explain = opts.explain;
75 let effective_casing = opts.casing.or(db.casing);
76 let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
77
78 warn_unsupported_pg_filters(effective_dialect, opts);
79
80 crate::commands::harness::print_db_header(config, db_name);
81
82 println!("{}", output::heading("Pushing schema to database..."));
83 println!();
84
85 println!(
86 " {}: {}",
87 output::label("Dialect"),
88 effective_dialect.as_str()
89 );
90
91 let connection = overrides::resolve_connection(db, effective_dialect, &opts.connection)?;
92 let Some(connection) = connection else {
93 print_missing_credentials_help(effective_dialect);
94 return Err(CliError::MissingCredentials("push"));
95 };
96 println!(" {}: {}", output::label("Driver"), connection.driver);
97
98 let parse_result = parse_schema_files(db, opts.schema.as_deref())?;
100
101 if parse_result.tables.is_empty() && parse_result.indexes.is_empty() {
102 println!(
103 "{}",
104 output::warning("No tables or indexes found in schema files.")
105 );
106 return Ok(());
107 }
108
109 println!(
110 " {} {} table(s), {} index(es)",
111 output::label("Found"),
112 parse_result.tables.len(),
113 parse_result.indexes.len()
114 );
115
116 let dialect = effective_dialect.to_base();
118 let mut desired_snapshot =
119 Snapshot::from_parse_result(&parse_result, dialect, effective_casing);
120
121 let filters = crate::db::SnapshotFilters {
122 tables: overrides::resolve_filter_list(
123 opts.filters.tables_filter.as_deref(),
124 db.tables_filter.as_ref(),
125 ),
126 schemas: overrides::resolve_schema_filters(
127 effective_dialect,
128 opts.filters.schema_filters.as_deref(),
129 db.schema_filter.as_ref(),
130 ),
131 extensions: overrides::resolve_extensions_filter(
132 effective_dialect,
133 opts.filters.extensions_filters.as_deref(),
134 db.extensions_filters.as_deref(),
135 ),
136 roles: Some(db.effective_entities().roles),
137 };
138 crate::db::apply_snapshot_filters(&mut desired_snapshot, effective_dialect, &filters)?;
139
140 let plan = crate::db::plan_push_with_renames(
144 &connection,
145 &desired_snapshot,
146 db.breakpoints,
147 &filters,
148 db.migrations_table(),
149 &mut |current, desired, options| {
150 crate::commands::renames::resolve_for_command(current, desired, options, &opts.hints)
151 },
152 )?;
153
154 if !plan.warnings.is_empty() {
155 println!("{}", output::warning("Warnings:"));
156 for w in &plan.warnings {
157 println!(" {} {}", output::warning("-"), w);
158 }
159 println!();
160 }
161
162 if explain || verbose {
164 if plan.sql_statements.is_empty() {
165 println!("{}", output::success("No schema changes detected."));
166 return Ok(());
167 }
168
169 println!("{}", output::muted("--- Planned SQL ---"));
170 println!();
171 for stmt in &plan.sql_statements {
172 println!("{stmt}\n");
173 }
174 println!("{}", output::muted("--- End SQL ---"));
175 println!();
176 }
177
178 if explain {
180 return Ok(());
181 }
182
183 if plan.sql_statements.is_empty() {
184 println!("{}", output::success("No schema changes detected."));
185 return Ok(());
186 }
187
188 crate::db::apply_push(&connection, &plan, opts.force)?;
190
191 println!("{}", output::success("Push complete!"));
192
193 Ok(())
194}
195
196fn warn_unsupported_pg_filters(effective_dialect: Dialect, opts: &PushOptions) {
199 if effective_dialect == Dialect::Postgresql {
200 return;
201 }
202 if opts
203 .filters
204 .schema_filters
205 .as_ref()
206 .is_some_and(|v| !v.is_empty())
207 {
208 println!(
209 "{}",
210 output::warning("Ignoring --schemaFilters: only supported for postgresql")
211 );
212 }
213 if opts
214 .filters
215 .extensions_filters
216 .as_ref()
217 .is_some_and(|v| !v.is_empty())
218 {
219 println!(
220 "{}",
221 output::warning("Ignoring --extensionsFilters: only supported for postgresql")
222 );
223 }
224}
225
226fn print_missing_credentials_help(effective_dialect: Dialect) {
228 println!("Add credentials to your drizzle.config.toml:");
229 println!();
230 println!(" {}", output::muted("[dbCredentials]"));
231 match effective_dialect.to_base() {
232 drizzle_types::Dialect::SQLite => {
233 println!(" {}", output::muted("url = \"./dev.db\""));
234 }
235 drizzle_types::Dialect::PostgreSQL => {
236 println!(
237 " {}",
238 output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
239 );
240 }
241 drizzle_types::Dialect::MySQL => {
242 println!(
243 " {}",
244 output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
245 );
246 }
247 }
248 println!();
249 println!("Or use an environment variable:");
250 println!();
251 println!(" {}", output::muted("[dbCredentials]"));
252 println!(" {}", output::muted("url = { env = \"DATABASE_URL\" }"));
253}
254
255fn parse_schema_files(
257 db: &crate::config::DatabaseConfig,
258 schema_override: Option<&[String]>,
259) -> Result<drizzle_migrations::parser::ParseResult, CliError> {
260 use drizzle_migrations::parser::SchemaParser;
261
262 let schema_files = overrides::resolve_schema_files(db, schema_override)?;
263 if schema_files.is_empty() {
264 return Err(CliError::NoSchemaFiles(overrides::resolve_schema_display(
265 db,
266 schema_override,
267 )));
268 }
269
270 println!(
271 " {} {} schema file(s)",
272 output::label("Parsing"),
273 schema_files.len()
274 );
275
276 let mut combined_code = String::new();
277 for path in &schema_files {
278 let code = std::fs::read_to_string(path)
279 .map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
280 combined_code.push_str(&code);
281 combined_code.push('\n');
282 }
283
284 let parse_result = SchemaParser::parse(&combined_code);
285 crate::snapshot::surface_parse_diagnostics(&parse_result)?;
286 Ok(parse_result)
287}