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