@@ -526,6 +526,9 @@ pub trait Backend: Send + Sync + DynClone {
526526
527527dyn_clone:: clone_trait_object!( Backend ) ;
528528
529+ /// Regular expression to match PostgreSQL key-value pairs in a connection string.
530+ const PG_KEY_PAIR_RE : & str = r"(hostaddr|host|dbname|user|port)\s*=" ;
531+
529532/// Connection specification. Contains the name of a database backend
530533/// and the backend-specific connection string. See [`connect`]
531534/// to make a [`Connection`] from a `ConnectionSpec`.
@@ -549,7 +552,7 @@ impl ConnectionSpec {
549552 contents. push ( '\n' ) ;
550553 f. write_all ( contents. as_bytes ( ) ) . map_err ( |e| e. into ( ) )
551554 }
552- /// Load a previously saved connection spec
555+ /// Load a previously saved connection spec.
553556 pub fn load ( path : impl AsRef < Path > ) -> Result < Self > {
554557 let path = conn_complete_if_dir ( path. as_ref ( ) ) ;
555558 serde_json:: from_reader ( fs:: File :: open ( path) ?) . map_err ( |e| e. into ( ) )
@@ -560,28 +563,115 @@ impl ConnectionSpec {
560563 None => Err ( crate :: Error :: UnknownBackend ( self . backend_name . clone ( ) ) ) ,
561564 }
562565 }
566+ /// Get the backend name.
567+ pub const fn backend_name ( & self ) -> & String {
568+ & self . backend_name
569+ }
570+ /// Get the database connection string.
571+ pub const fn connection_string ( & self ) -> & String {
572+ & self . conn_str
573+ }
574+ /// Get the connection string URI if it is a URI.
575+ /// Returns None if the connection string is not a URI.
576+ pub fn connection_string_uri ( & self ) -> Option < url:: Url > {
577+ url:: Url :: parse ( self . connection_string ( ) ) . ok ( )
578+ }
579+
580+ fn is_pg_key_value_pairs ( connection_string : & str ) -> bool {
581+ // host and hostaddr are optional in Postgresql, however this prevents connecting
582+ // without one of them: https://github.com/sfackler/rust-postgres/issues/1239
583+ let re = regex:: Regex :: new ( PG_KEY_PAIR_RE ) . unwrap ( ) ;
584+ re. is_match ( connection_string)
585+ }
586+
587+ /// Add a query parameter to the connection string.
588+ pub fn add_parameter ( & mut self , name : & str , value : & str ) -> Result < ( ) > {
589+ if self . backend_name ( ) == "pg" && Self :: is_pg_key_value_pairs ( & self . conn_str ) {
590+ // Append using PostgreSQL key-value pair syntax.
591+ self . conn_str . push_str ( & format ! ( " {}={}" , name, value) ) ;
592+ return Ok ( ( ) ) ;
593+ }
594+ if self . connection_string_uri ( ) . is_none ( ) {
595+ return Err ( crate :: Error :: UnknownConnectString (
596+ "Cannot add query parameter to connection string that is not a postgres or URI"
597+ . to_string ( ) ,
598+ ) ) ;
599+ }
600+
601+ if self . conn_str . contains ( '?' ) {
602+ self . conn_str . push_str ( & format ! ( "&{}={}" , name, value) ) ;
603+ } else {
604+ self . conn_str . push_str ( & format ! ( "?{}={}" , name, value) ) ;
605+ }
606+ Ok ( ( ) )
607+ }
563608}
564609
610+ /// Convert a string to a connection spec.
611+ ///
612+ /// The string may be any connection string supported by the database backend.
613+ /// If it is a URL with a scheme of "sqlite", the backend will be set to "sqlite",
614+ /// and the scheme will be set to "file" as required by the sqlite engine.
615+ /// If it is a URI with a scheme of "file", the backend will be set to "sqlite".
616+ /// If it does not have a scheme, it will be set to "pg" if it contains any of
617+ /// `host=`, `hostaddr=`, `dbname=`, or `user=`.
618+ /// Otherwise it will default to "sqlite".
565619impl TryFrom < & str > for ConnectionSpec {
566620 type Error = crate :: Error ;
567621 fn try_from ( value : & str ) -> Result < Self > {
568- let parsed = url:: Url :: parse ( value) ?;
569- if parsed. scheme ( ) == "sqlite" {
570- let path = value. trim_start_matches ( "sqlite://" ) ;
571- Ok ( ConnectionSpec {
572- backend_name : "sqlite" . to_string ( ) ,
573- conn_str : path. to_string ( ) ,
574- } )
575- } else if [ "postgres" , "postgresql" ] . contains ( & parsed. scheme ( ) ) {
576- Ok ( ConnectionSpec {
577- backend_name : "pg" . to_string ( ) ,
578- conn_str : value. to_string ( ) ,
579- } )
580- } else {
581- Ok ( ConnectionSpec {
582- backend_name : parsed. scheme ( ) . to_string ( ) ,
583- conn_str : value. to_string ( ) ,
584- } )
622+ match url:: Url :: parse ( value) {
623+ Ok ( parsed) => {
624+ match parsed. scheme ( ) {
625+ "sqlite" => {
626+ // SQLite uses file: URLs, but we want to accept sqlite:
627+ // in order that the URIs are more expressive.
628+ let value = value. replacen ( "sqlite:" , "file:" , 1 ) ;
629+ Ok ( ConnectionSpec {
630+ backend_name : "sqlite" . to_string ( ) ,
631+ conn_str : value,
632+ } )
633+ }
634+ "file" => Ok ( ConnectionSpec {
635+ backend_name : "sqlite" . to_string ( ) ,
636+ conn_str : value. to_string ( ) ,
637+ } ) ,
638+ "postgres" | "postgresql" => Ok ( ConnectionSpec {
639+ backend_name : "pg" . to_string ( ) ,
640+ conn_str : value. to_string ( ) ,
641+ } ) ,
642+ _ => Ok ( ConnectionSpec {
643+ backend_name : parsed. scheme ( ) . to_string ( ) ,
644+ conn_str : value. to_string ( ) ,
645+ } ) ,
646+ }
647+ }
648+ Err ( url:: ParseError :: InvalidPort ) => {
649+ // This occurs when using a PostgreSQL multi-host connection string.
650+ if value. starts_with ( "postgres" ) {
651+ Ok ( ConnectionSpec {
652+ backend_name : "pg" . to_string ( ) ,
653+ conn_str : value. to_string ( ) ,
654+ } )
655+ } else {
656+ Ok ( ConnectionSpec {
657+ backend_name : "sqlite" . to_string ( ) ,
658+ conn_str : value. to_string ( ) ,
659+ } )
660+ }
661+ }
662+ Err ( _) => {
663+ // Spaces are allowed between the key and the equals sign in a PostgreSQL connection string.
664+ if Self :: is_pg_key_value_pairs ( value) {
665+ return Ok ( ConnectionSpec {
666+ backend_name : "pg" . to_string ( ) ,
667+ conn_str : value. to_string ( ) ,
668+ } ) ;
669+ }
670+ Ok ( ConnectionSpec {
671+ backend_name : "sqlite" . to_string ( ) ,
672+ conn_str : value. to_string ( ) ,
673+ } )
674+ }
585675 }
586676 }
587677}
0 commit comments