1use std::path::PathBuf;
2
3use crate::config::{
4 Credentials, DatabaseConfig, Dialect, Driver, Extension, Filter, MySQLCreds, MySQLSslMode,
5 PostgresCreds, PostgresSslMode,
6};
7use crate::error::CliError;
8
9#[derive(clap::Args, Clone, Default)]
15pub struct ConnectionOverrides {
16 #[arg(long)]
18 pub driver: Option<Driver>,
19
20 #[arg(long)]
22 pub url: Option<String>,
23
24 #[arg(long)]
26 pub host: Option<String>,
27
28 #[arg(long)]
30 pub port: Option<u16>,
31
32 #[arg(long)]
34 pub user: Option<String>,
35
36 #[arg(long)]
38 pub password: Option<String>,
39
40 #[arg(long)]
42 pub database: Option<String>,
43
44 #[arg(long)]
46 pub ssl: Option<String>,
47
48 #[arg(long = "authToken", alias = "auth-token")]
50 pub auth_token: Option<String>,
51}
52
53impl std::fmt::Debug for ConnectionOverrides {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("ConnectionOverrides")
56 .field("driver", &self.driver)
57 .field("url", &self.url.as_ref().map(|_| "[REDACTED]"))
58 .field("host", &self.host)
59 .field("port", &self.port)
60 .field("user", &self.user)
61 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
62 .field("database", &self.database)
63 .field("ssl", &self.ssl)
64 .field(
65 "auth_token",
66 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
67 )
68 .finish()
69 }
70}
71
72#[derive(clap::Args, Debug, Clone, Default)]
77pub struct FilterArgs {
78 #[arg(long = "tablesFilter", value_delimiter = ',')]
80 pub tables_filter: Option<Vec<String>>,
81
82 #[arg(long = "schemaFilters", alias = "schemaFilter", value_delimiter = ',')]
84 pub schema_filters: Option<Vec<String>>,
85
86 #[arg(long = "extensionsFilters", value_delimiter = ',', value_parser = parse_extension_arg)]
88 pub extensions_filters: Option<Vec<Extension>>,
89}
90
91fn parse_extension_arg(s: &str) -> Result<Extension, String> {
92 s.parse()
93}
94
95impl ConnectionOverrides {
96 #[must_use]
97 pub const fn has_any(&self) -> bool {
98 self.url.is_some()
99 || self.host.is_some()
100 || self.port.is_some()
101 || self.user.is_some()
102 || self.password.is_some()
103 || self.database.is_some()
104 || self.ssl.is_some()
105 || self.auth_token.is_some()
106 }
107}
108
109#[derive(Debug, Clone)]
113pub struct ResolvedConnection {
114 pub dialect: Dialect,
115 pub driver: Driver,
116 pub credentials: Credentials,
117}
118
119#[must_use]
120pub fn resolve_dialect(db: &DatabaseConfig, override_dialect: Option<Dialect>) -> Dialect {
121 override_dialect.unwrap_or(db.dialect)
122}
123
124pub fn resolve_driver(
133 db: &DatabaseConfig,
134 dialect: Dialect,
135 driver_override: Option<Driver>,
136) -> Result<Option<Driver>, CliError> {
137 let driver = driver_override.or(db.driver);
138 if let Some(driver) = driver
139 && !driver.is_valid_for(dialect)
140 {
141 return Err(CliError::Other(format!(
142 "driver '{driver}' invalid for {dialect} dialect"
143 )));
144 }
145 Ok(driver)
146}
147
148pub fn resolve_credentials(
157 db: &DatabaseConfig,
158 dialect: Dialect,
159 overrides: &ConnectionOverrides,
160) -> Result<Option<Credentials>, CliError> {
161 if !overrides.has_any() {
162 if dialect != db.dialect {
163 return Err(CliError::Other(format!(
164 "--dialect={dialect} requires matching credential flags (--url/--host/--database/etc)"
165 )));
166 }
167 return db.credentials().map_err(Into::into);
168 }
169
170 let creds = match dialect {
171 Dialect::Sqlite => {
172 if overrides.host.is_some()
173 || overrides.port.is_some()
174 || overrides.user.is_some()
175 || overrides.password.is_some()
176 || overrides.database.is_some()
177 || overrides.ssl.is_some()
178 || overrides.auth_token.is_some()
179 {
180 return Err(CliError::Other(
181 "sqlite credentials only support --url for local database path".into(),
182 ));
183 }
184
185 let path = overrides
186 .url
187 .clone()
188 .ok_or_else(|| CliError::Other("sqlite requires --url".into()))?;
189
190 Credentials::Sqlite {
191 path: path.into_boxed_str(),
192 }
193 }
194 Dialect::Turso => {
195 if overrides.host.is_some()
196 || overrides.port.is_some()
197 || overrides.user.is_some()
198 || overrides.password.is_some()
199 || overrides.database.is_some()
200 || overrides.ssl.is_some()
201 {
202 return Err(CliError::Other(
203 "turso credentials support --url and optional --authToken".into(),
204 ));
205 }
206
207 let url = overrides
208 .url
209 .clone()
210 .ok_or_else(|| CliError::Other("turso requires --url".into()))?;
211
212 Credentials::Turso {
213 url: url.into_boxed_str(),
214 auth_token: overrides.auth_token.clone().map(String::into_boxed_str),
215 }
216 }
217 Dialect::Postgresql => {
218 if overrides.auth_token.is_some() {
219 return Err(CliError::Other(
220 "postgresql does not support --authToken (use --password or --url)".into(),
221 ));
222 }
223
224 if let Some(url) = overrides.url.clone() {
225 if overrides.host.is_some()
226 || overrides.port.is_some()
227 || overrides.user.is_some()
228 || overrides.password.is_some()
229 || overrides.database.is_some()
230 || overrides.ssl.is_some()
231 {
232 return Err(CliError::Other(
233 "postgresql credentials: use either --url OR --host/--database[/--port/...], not both"
234 .into(),
235 ));
236 }
237
238 Credentials::Postgres(PostgresCreds::Url(url.into_boxed_str()))
239 } else {
240 let host = overrides.host.clone().ok_or_else(|| {
241 CliError::Other("postgresql host credentials require --host".into())
242 })?;
243 let database = overrides.database.clone().ok_or_else(|| {
244 CliError::Other("postgresql host credentials require --database".into())
245 })?;
246
247 Credentials::Postgres(PostgresCreds::Host {
248 host: host.into_boxed_str(),
249 port: overrides.port.unwrap_or(5432),
250 user: overrides.user.clone().map(String::into_boxed_str),
251 password: overrides.password.clone().map(String::into_boxed_str),
252 database: database.into_boxed_str(),
253 ssl: parse_ssl_override(overrides.ssl.as_deref())?
254 .unwrap_or(PostgresSslMode::Disable),
255 })
256 }
257 }
258 Dialect::Mysql => {
259 if overrides.auth_token.is_some() {
260 return Err(CliError::Other(
261 "mysql does not support --authToken (use --password or --url)".into(),
262 ));
263 }
264
265 if let Some(url) = overrides.url.clone() {
266 if overrides.host.is_some()
267 || overrides.port.is_some()
268 || overrides.user.is_some()
269 || overrides.password.is_some()
270 || overrides.database.is_some()
271 || overrides.ssl.is_some()
272 {
273 return Err(CliError::Other(
274 "mysql credentials: use either --url OR --host/--database[/--port/...], not both"
275 .into(),
276 ));
277 }
278 if !url.starts_with("mysql://") {
279 return Err(CliError::Other(
280 "mysql --url must start with mysql://".into(),
281 ));
282 }
283 Credentials::MySQL(MySQLCreds::Url(url.into_boxed_str()))
284 } else {
285 let host = overrides.host.clone().ok_or_else(|| {
286 CliError::Other("mysql host credentials require --host".into())
287 })?;
288 let database = overrides.database.clone().ok_or_else(|| {
289 CliError::Other("mysql host credentials require --database".into())
290 })?;
291
292 Credentials::MySQL(MySQLCreds::Host {
293 host: host.into_boxed_str(),
294 port: overrides.port.unwrap_or(3306),
295 user: overrides.user.clone().map(String::into_boxed_str),
296 password: overrides.password.clone().map(String::into_boxed_str),
297 database: database.into_boxed_str(),
298 ssl: parse_mysql_ssl_override(overrides.ssl.as_deref())?
299 .unwrap_or(MySQLSslMode::Disable),
300 })
301 }
302 }
303 };
304
305 Ok(Some(creds))
306}
307
308pub fn resolve_connection(
311 db: &DatabaseConfig,
312 dialect: Dialect,
313 overrides: &ConnectionOverrides,
314) -> Result<Option<ResolvedConnection>, CliError> {
315 let credentials = resolve_credentials(db, dialect, overrides)?;
316 let Some(credentials) = credentials else {
317 return Ok(None);
318 };
319 let configured_driver = overrides
320 .driver
321 .or_else(|| (dialect == db.dialect).then_some(db.driver).flatten());
322 let driver = configured_driver.unwrap_or_else(|| default_driver(dialect, &credentials));
323 if !driver.is_valid_for(dialect) {
324 return Err(CliError::Other(format!(
325 "driver '{driver}' invalid for {dialect} dialect"
326 )));
327 }
328 if !driver_accepts_credentials(driver, &credentials) {
329 return Err(CliError::Other(format!(
330 "driver '{driver}' is incompatible with the resolved {dialect} credentials"
331 )));
332 }
333 Ok(Some(ResolvedConnection {
334 dialect,
335 driver,
336 credentials,
337 }))
338}
339
340fn driver_accepts_credentials(driver: Driver, credentials: &Credentials) -> bool {
341 matches!(
342 (driver, credentials),
343 (Driver::Rusqlite, Credentials::Sqlite { .. })
344 | (Driver::Libsql | Driver::Turso, Credentials::Turso { .. })
345 | (
346 Driver::PostgresSync | Driver::TokioPostgres,
347 Credentials::Postgres(_)
348 )
349 | (
350 Driver::MysqlSync | Driver::MysqlAsync,
351 Credentials::MySQL(_)
352 )
353 | (Driver::D1Http, Credentials::D1 { .. })
354 | (Driver::AwsDataApi, Credentials::AwsDataApi { .. })
355 )
356}
357
358fn default_driver(dialect: Dialect, credentials: &Credentials) -> Driver {
359 match (dialect, credentials) {
360 (Dialect::Sqlite, Credentials::D1 { .. }) => Driver::D1Http,
361 (Dialect::Postgresql, Credentials::AwsDataApi { .. }) => Driver::AwsDataApi,
362 (Dialect::Sqlite, _) => Driver::Rusqlite,
363 (Dialect::Turso, Credentials::Turso { url, .. }) => {
364 if is_local_libsql_url(url) {
365 Driver::Libsql
366 } else {
367 Driver::Turso
368 }
369 }
370 (Dialect::Postgresql, _) => Driver::PostgresSync,
371 (Dialect::Mysql, _) => Driver::MysqlSync,
372 (Dialect::Turso, _) => Driver::Turso,
375 }
376}
377
378fn is_local_libsql_url(url: &str) -> bool {
379 !url.starts_with("libsql://") && !url.starts_with("http://") && !url.starts_with("https://")
380}
381
382fn parse_ssl_override(ssl: Option<&str>) -> Result<Option<PostgresSslMode>, CliError> {
383 let Some(raw) = ssl else {
384 return Ok(None);
385 };
386
387 PostgresSslMode::parse(raw).map(Some).map_err(|_| {
388 CliError::Other(format!(
389 "invalid --ssl value '{raw}'; expected one of: true,false,require,allow,prefer,verify-full,verify-ca,disable"
390 ))
391 })
392}
393
394fn parse_mysql_ssl_override(ssl: Option<&str>) -> Result<Option<MySQLSslMode>, CliError> {
395 let Some(raw) = ssl else {
396 return Ok(None);
397 };
398
399 MySQLSslMode::parse(raw).map(Some).map_err(|_| {
400 CliError::Other(format!(
401 "invalid --ssl value '{raw}' for mysql; expected one of: true,false,required,verify-ca,verify-identity,disable"
402 ))
403 })
404}
405
406#[must_use]
407pub fn resolve_filter_list(cli: Option<&[String]>, config: Option<&Filter>) -> Option<Vec<String>> {
408 if let Some(values) = cli {
409 if values.is_empty() {
410 return None;
411 }
412 return Some(values.to_vec());
413 }
414
415 config.map(|f| f.iter().map(ToOwned::to_owned).collect())
416}
417
418#[must_use]
419pub fn resolve_schema_filters(
420 dialect: Dialect,
421 cli: Option<&[String]>,
422 config: Option<&Filter>,
423) -> Option<Vec<String>> {
424 if dialect != Dialect::Postgresql {
425 return None;
426 }
427 let resolved = resolve_filter_list(cli, config);
428 if resolved.is_some() {
429 return resolved;
430 }
431
432 if matches!(dialect, Dialect::Postgresql) {
433 Some(vec!["public".to_string()])
434 } else {
435 None
436 }
437}
438
439#[must_use]
440pub fn resolve_extensions_filter(
441 dialect: Dialect,
442 cli: Option<&[Extension]>,
443 config: Option<&[Extension]>,
444) -> Option<Vec<Extension>> {
445 if dialect != Dialect::Postgresql {
446 return None;
447 }
448 if let Some(values) = cli {
449 if values.is_empty() {
450 return None;
451 }
452 return Some(values.to_vec());
453 }
454
455 config.map(<[Extension]>::to_vec)
456}
457
458#[must_use]
459pub fn resolve_schema_display(db: &DatabaseConfig, schema_override: Option<&[String]>) -> String {
460 match schema_override {
461 Some(v) if !v.is_empty() => v.join(", "),
462 _ => db.schema_display(),
463 }
464}
465
466pub fn resolve_schema_files(
475 db: &DatabaseConfig,
476 schema_override: Option<&[String]>,
477) -> Result<Vec<PathBuf>, CliError> {
478 let Some(schema_patterns) = schema_override else {
479 return db.schema_files().map_err(Into::into);
480 };
481
482 if schema_patterns.is_empty() {
483 return Err(CliError::NoSchemaFiles("(empty schema override)".into()));
484 }
485
486 let files = crate::config::resolve_schema_patterns(schema_patterns.iter().map(String::as_str))?;
487
488 if files.is_empty() {
489 return Err(CliError::NoSchemaFiles(
490 schema_patterns
491 .iter()
492 .map(std::string::String::as_str)
493 .collect::<Vec<_>>()
494 .join(", "),
495 ));
496 }
497
498 Ok(files)
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use crate::config::Config;
505 use std::path::PathBuf;
506 use tempfile::TempDir;
507
508 fn load_db(config_toml: &str) -> (TempDir, DatabaseConfig) {
509 let dir = TempDir::new().expect("temp dir");
510 let path = dir.path().join("drizzle.config.toml");
511 std::fs::write(&path, config_toml).expect("write config");
512 let config = Config::load_from(&path).expect("load config");
513 let db = config.default_database().expect("default db").clone();
514 (dir, db)
515 }
516
517 #[test]
518 fn resolve_filter_list_prefers_cli_values() {
519 let config = Filter::Many(vec!["from_config".to_string()]);
520 let cli = vec!["from_cli".to_string()];
521
522 let resolved = resolve_filter_list(Some(&cli), Some(&config));
523 assert_eq!(resolved, Some(vec!["from_cli".to_string()]));
524 }
525
526 #[test]
527 fn resolve_filter_list_uses_config_when_cli_missing() {
528 let config = Filter::Many(vec!["public".to_string(), "dev".to_string()]);
529 let resolved = resolve_filter_list(None, Some(&config));
530 assert_eq!(
531 resolved,
532 Some(vec!["public".to_string(), "dev".to_string()])
533 );
534 }
535
536 #[test]
537 fn resolve_schema_filters_defaults_to_public_for_postgres() {
538 let resolved = resolve_schema_filters(Dialect::Postgresql, None, None);
539 assert_eq!(resolved, Some(vec!["public".to_string()]));
540 }
541
542 #[test]
543 fn resolve_schema_filters_does_not_default_for_sqlite() {
544 let resolved = resolve_schema_filters(Dialect::Sqlite, None, None);
545 assert_eq!(resolved, None);
546 }
547
548 #[test]
549 fn non_postgres_filters_are_ignored_after_cli_warning() {
550 let schemas = vec!["public".to_string()];
551 let extensions = vec![Extension::Postgis];
552 for dialect in [Dialect::Sqlite, Dialect::Turso, Dialect::Mysql] {
553 assert_eq!(resolve_schema_filters(dialect, Some(&schemas), None), None);
554 assert_eq!(
555 resolve_extensions_filter(dialect, Some(&extensions), None),
556 None
557 );
558 }
559 }
560
561 #[test]
562 fn resolve_extensions_filter_prefers_cli_values() {
563 let cli = vec![Extension::Postgis];
564 let config = vec![];
565
566 let resolved = resolve_extensions_filter(Dialect::Postgresql, Some(&cli), Some(&config));
567 assert_eq!(resolved, Some(vec![Extension::Postgis]));
568 }
569
570 #[test]
571 fn resolve_driver_rejects_invalid_override() {
572 let (_dir, db) = load_db(
573 r#"
574dialect = "sqlite"
575schema = "src/schema.rs"
576"#,
577 );
578
579 let err = resolve_driver(&db, Dialect::Sqlite, Some(Driver::TokioPostgres))
580 .expect_err("driver should be rejected");
581 assert_eq!(
582 err.to_string(),
583 "driver 'tokio-postgres' invalid for sqlite dialect"
584 );
585 }
586
587 #[test]
588 fn resolve_credentials_requires_overrides_for_dialect_switch() {
589 let (_dir, db) = load_db(
590 r#"
591dialect = "sqlite"
592[dbCredentials]
593url = "./dev.db"
594"#,
595 );
596
597 let err = resolve_credentials(&db, Dialect::Postgresql, &ConnectionOverrides::default())
598 .expect_err("dialect switch should require explicit credentials");
599 assert_eq!(
600 err.to_string(),
601 "--dialect=postgresql requires matching credential flags (--url/--host/--database/etc)"
602 );
603 }
604
605 #[test]
606 fn resolve_credentials_sqlite_rejects_host_fields() {
607 let (_dir, db) = load_db(
608 r#"
609dialect = "sqlite"
610"#,
611 );
612
613 let overrides = ConnectionOverrides {
614 host: Some("localhost".to_string()),
615 ..Default::default()
616 };
617
618 let err = resolve_credentials(&db, Dialect::Sqlite, &overrides)
619 .expect_err("sqlite should reject host-style credentials");
620 assert_eq!(
621 err.to_string(),
622 "sqlite credentials only support --url for local database path"
623 );
624 }
625
626 #[test]
627 fn resolve_credentials_postgres_rejects_mixed_url_and_host_fields() {
628 let (_dir, db) = load_db(
629 r#"
630dialect = "postgresql"
631"#,
632 );
633
634 let overrides = ConnectionOverrides {
635 url: Some("postgres://u:p@localhost:5432/db".to_string()),
636 host: Some("localhost".to_string()),
637 database: Some("db".to_string()),
638 ..Default::default()
639 };
640
641 let err = resolve_credentials(&db, Dialect::Postgresql, &overrides)
642 .expect_err("postgres should reject mixed credentials");
643 assert_eq!(
644 err.to_string(),
645 "postgresql credentials: use either --url OR --host/--database[/--port/...], not both"
646 );
647 }
648
649 #[test]
650 fn resolve_credentials_postgres_requires_database_for_host_mode() {
651 let (_dir, db) = load_db(
652 r#"
653dialect = "postgresql"
654"#,
655 );
656
657 let overrides = ConnectionOverrides {
658 host: Some("localhost".to_string()),
659 ..Default::default()
660 };
661
662 let err = resolve_credentials(&db, Dialect::Postgresql, &overrides)
663 .expect_err("postgres host credentials require database");
664 assert_eq!(
665 err.to_string(),
666 "postgresql host credentials require --database"
667 );
668 }
669
670 #[test]
671 fn resolve_credentials_turso_accepts_url_with_optional_token() {
672 let (_dir, db) = load_db(
673 r#"
674dialect = "turso"
675"#,
676 );
677
678 let overrides = ConnectionOverrides {
679 url: Some("libsql://example.turso.io".to_string()),
680 auth_token: Some("secret".to_string()),
681 ..Default::default()
682 };
683
684 let creds = resolve_credentials(&db, Dialect::Turso, &overrides)
685 .expect("resolve creds")
686 .expect("some creds");
687
688 match creds {
689 Credentials::Turso { url, auth_token } => {
690 assert_eq!(url.as_ref(), "libsql://example.turso.io");
691 assert_eq!(auth_token.as_deref(), Some("secret"));
692 }
693 _ => panic!("expected turso credentials"),
694 }
695 }
696
697 #[test]
698 fn resolve_credentials_postgres_host_mode_accepts_ssl_modes() {
699 let (_dir, db) = load_db(
700 r#"
701dialect = "postgresql"
702"#,
703 );
704
705 let require_ssl = ConnectionOverrides {
706 host: Some("localhost".to_string()),
707 database: Some("db".to_string()),
708 ssl: Some("require".to_string()),
709 ..Default::default()
710 };
711 let creds = resolve_credentials(&db, Dialect::Postgresql, &require_ssl)
712 .expect("resolve")
713 .expect("creds");
714 match creds {
715 Credentials::Postgres(PostgresCreds::Host { ssl, .. }) => {
716 assert_eq!(ssl, PostgresSslMode::Require)
717 }
718 _ => panic!("expected postgres host creds"),
719 }
720
721 let disable_ssl = ConnectionOverrides {
722 host: Some("localhost".to_string()),
723 database: Some("db".to_string()),
724 ssl: Some("disable".to_string()),
725 ..Default::default()
726 };
727 let creds = resolve_credentials(&db, Dialect::Postgresql, &disable_ssl)
728 .expect("resolve")
729 .expect("creds");
730 match creds {
731 Credentials::Postgres(PostgresCreds::Host { ssl, .. }) => {
732 assert_eq!(ssl, PostgresSslMode::Disable)
733 }
734 _ => panic!("expected postgres host creds"),
735 }
736 }
737
738 #[test]
739 fn resolve_credentials_postgres_host_mode_rejects_invalid_ssl_value() {
740 let (_dir, db) = load_db(
741 r#"
742dialect = "postgresql"
743"#,
744 );
745
746 let overrides = ConnectionOverrides {
747 host: Some("localhost".to_string()),
748 database: Some("db".to_string()),
749 ssl: Some("maybe".to_string()),
750 ..Default::default()
751 };
752
753 let err = resolve_credentials(&db, Dialect::Postgresql, &overrides)
754 .expect_err("invalid ssl should fail");
755 assert_eq!(
756 err.to_string(),
757 "invalid --ssl value 'maybe'; expected one of: true,false,require,allow,prefer,verify-full,verify-ca,disable"
758 );
759 }
760
761 #[test]
762 fn resolve_connection_preserves_explicit_mysql_async_driver() {
763 let (_dir, db) = load_db(
764 r#"
765dialect = "mysql"
766driver = "mysql-sync"
767"#,
768 );
769 let overrides = ConnectionOverrides {
770 driver: Some(Driver::MysqlAsync),
771 host: Some("localhost".to_string()),
772 database: Some("app".to_string()),
773 ssl: Some("verify-ca".to_string()),
774 ..Default::default()
775 };
776 let target = resolve_connection(&db, Dialect::Mysql, &overrides)
777 .expect("resolve")
778 .expect("target");
779 assert_eq!(target.driver, Driver::MysqlAsync);
780 assert_eq!(target.dialect, Dialect::Mysql);
781 match target.credentials {
782 Credentials::MySQL(MySQLCreds::Host { port, ssl, .. }) => {
783 assert_eq!(port, 3306);
784 assert_eq!(ssl, MySQLSslMode::VerifyCa);
785 }
786 other => panic!("unexpected credentials: {other:?}"),
787 }
788 }
789
790 #[test]
791 fn resolve_connection_defaults_mysql_to_sync() {
792 let (_dir, db) = load_db(
793 r#"
794dialect = "mysql"
795[dbCredentials]
796url = "mysql://localhost/app"
797"#,
798 );
799 let target = resolve_connection(&db, Dialect::Mysql, &ConnectionOverrides::default())
800 .expect("resolve")
801 .expect("target");
802 assert_eq!(target.driver, Driver::MysqlSync);
803 }
804
805 #[test]
806 fn resolve_connection_rejects_driver_with_wrong_credential_shape() {
807 let (_dir, db) = load_db(
808 r#"
809dialect = "postgresql"
810[dbCredentials]
811url = "postgres://localhost/app"
812"#,
813 );
814 let overrides = ConnectionOverrides {
815 driver: Some(Driver::AwsDataApi),
816 ..Default::default()
817 };
818
819 let error = resolve_connection(&db, Dialect::Postgresql, &overrides)
820 .expect_err("AWS adapter cannot consume TCP credentials");
821 assert!(error.to_string().contains("incompatible"));
822 }
823
824 #[test]
825 fn dialect_switch_does_not_reuse_original_driver() {
826 let (_dir, db) = load_db(
827 r#"
828dialect = "sqlite"
829driver = "rusqlite"
830"#,
831 );
832 let overrides = ConnectionOverrides {
833 url: Some("mysql://localhost/app".to_string()),
834 ..Default::default()
835 };
836 let target = resolve_connection(&db, Dialect::Mysql, &overrides)
837 .expect("resolve switch")
838 .expect("target");
839 assert_eq!(target.driver, Driver::MysqlSync);
840 }
841
842 #[test]
843 fn mysql_override_rejects_postgres_url_and_auth_token() {
844 let (_dir, db) = load_db("dialect = \"mysql\"");
845 let bad_url = ConnectionOverrides {
846 url: Some("postgres://localhost/app".to_string()),
847 ..Default::default()
848 };
849 assert!(
850 resolve_credentials(&db, Dialect::Mysql, &bad_url)
851 .expect_err("wrong scheme")
852 .to_string()
853 .contains("mysql://")
854 );
855
856 let auth_token = ConnectionOverrides {
857 host: Some("localhost".to_string()),
858 database: Some("app".to_string()),
859 auth_token: Some("unsupported".to_string()),
860 ..Default::default()
861 };
862 assert!(
863 resolve_credentials(&db, Dialect::Mysql, &auth_token)
864 .expect_err("auth token")
865 .to_string()
866 .contains("does not support --authToken")
867 );
868 }
869
870 #[test]
871 fn resolve_schema_filters_defaults_to_public_in_multi_db_postgres() {
872 let dir = TempDir::new().expect("temp dir");
873 let path = dir.path().join("drizzle.config.toml");
874 std::fs::write(
875 &path,
876 r#"
877[databases.pg]
878dialect = "postgresql"
879
880[databases.pg.dbCredentials]
881url = "postgres://localhost/db"
882
883[databases.sqlite]
884dialect = "sqlite"
885
886[databases.sqlite.dbCredentials]
887url = "./dev.db"
888"#,
889 )
890 .expect("write config");
891
892 let config = Config::load_from(&path).expect("load config");
893 let db = config.database(Some("pg")).expect("pg db");
894
895 let resolved = resolve_schema_filters(Dialect::Postgresql, None, db.schema_filter.as_ref());
896 assert_eq!(resolved, Some(vec!["public".to_string()]));
897 }
898
899 #[test]
900 fn resolve_schema_files_uses_override_glob() {
901 let (dir, db) = load_db(
902 r#"
903dialect = "sqlite"
904schema = "src/schema.rs"
905"#,
906 );
907
908 let a = dir.path().join("a.schema.rs");
909 let b = dir.path().join("b.schema.rs");
910 std::fs::write(&a, "pub struct A;").expect("write a");
911 std::fs::write(&b, "pub struct B;").expect("write b");
912
913 let pattern = format!("{}/*.schema.rs", dir.path().display()).replace('\\', "/");
914 let override_patterns = vec![pattern];
915 let files = resolve_schema_files(&db, Some(&override_patterns)).expect("resolve files");
916
917 let paths: Vec<PathBuf> = files;
918 assert_eq!(paths.len(), 2);
919 assert!(paths.iter().any(|p| p.ends_with("a.schema.rs")));
920 assert!(paths.iter().any(|p| p.ends_with("b.schema.rs")));
921 }
922
923 #[test]
924 fn connection_overrides_debug_redacts_secrets() {
925 let overrides = ConnectionOverrides {
926 url: Some("postgres://alice:url-secret@localhost/app".into()),
927 password: Some("password-secret".into()),
928 auth_token: Some("token-secret".into()),
929 ..ConnectionOverrides::default()
930 };
931
932 let rendered = format!("{overrides:?}");
933 assert!(rendered.contains("[REDACTED]"));
934 assert!(!rendered.contains("url-secret"));
935 assert!(!rendered.contains("password-secret"));
936 assert!(!rendered.contains("token-secret"));
937 }
938}