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