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