Skip to main content

gatekeep_sqlx/fragment/
driver.rs

1use url::Url;
2
3use super::GatekeepSqlxBackend;
4
5/// Supported `SQLx` database driver.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum SqlxDriver {
9    /// Postgres `SQLx` driver.
10    Postgres,
11    /// `SQLite` `SQLx` driver.
12    Sqlite,
13    /// `MySQL` `SQLx` driver.
14    MySql,
15}
16
17impl SqlxDriver {
18    /// Stable driver name.
19    #[must_use]
20    pub const fn name(self) -> &'static str {
21        match self {
22            Self::Postgres => "postgres",
23            Self::Sqlite => "sqlite",
24            Self::MySql => "mysql",
25        }
26    }
27
28    /// Whether this crate was compiled with the matching backend feature.
29    #[must_use]
30    pub const fn is_enabled(self) -> bool {
31        match self {
32            Self::Postgres => cfg!(feature = "postgres"),
33            Self::Sqlite => cfg!(feature = "sqlite"),
34            Self::MySql => cfg!(feature = "mysql"),
35        }
36    }
37}
38
39/// Database driver configuration error.
40#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
41#[non_exhaustive]
42pub enum SqlxDriverError {
43    /// The URL scheme is not recognized as a `SQLx` database driver.
44    #[error("unsupported SQLx database URL scheme {scheme:?}")]
45    UnsupportedUrlScheme {
46        /// Validated scheme from an explicit `scheme://` URL; absent for
47        /// ambiguous prefixes that could contain private credentials.
48        scheme: Option<String>,
49    },
50
51    /// The URL selects a driver whose feature was not enabled.
52    #[error("SQLx driver {driver} is not enabled for gatekeep-sqlx")]
53    DriverNotEnabled {
54        /// Driver inferred from the URL.
55        driver: &'static str,
56    },
57
58    /// The configured driver does not match the selected backend.
59    #[error("SQLx backend mismatch: expected {expected}, found {actual}")]
60    BackendMismatch {
61        /// Backend expected by the selected lowerer.
62        expected: &'static str,
63        /// Driver inferred from runtime configuration.
64        actual: &'static str,
65    },
66}
67
68/// Infers the `SQLx` driver from a database URL or `SQLx`-style `SQLite` memory URL.
69///
70/// # Errors
71///
72/// Returns [`SqlxDriverError`] when the URL scheme is unsupported or when the
73/// inferred driver was not enabled at compile time.
74pub fn infer_enabled_driver_from_url(database_url: &str) -> Result<SqlxDriver, SqlxDriverError> {
75    let driver = infer_driver_from_url(database_url)?;
76    if driver.is_enabled() {
77        Ok(driver)
78    } else {
79        Err(SqlxDriverError::DriverNotEnabled {
80            driver: driver.name(),
81        })
82    }
83}
84
85/// Validates that a database URL matches a selected backend.
86///
87/// # Errors
88///
89/// Returns [`SqlxDriverError`] when the URL is unsupported, names a disabled
90/// driver, or names a different driver from `B`.
91pub fn validate_database_url_for_backend<B>(database_url: &str) -> Result<(), SqlxDriverError>
92where
93    B: GatekeepSqlxBackend,
94{
95    let actual = infer_enabled_driver_from_url(database_url)?;
96    if actual == B::DRIVER {
97        Ok(())
98    } else {
99        Err(SqlxDriverError::BackendMismatch {
100            expected: B::NAME,
101            actual: actual.name(),
102        })
103    }
104}
105
106fn infer_driver_from_url(database_url: &str) -> Result<SqlxDriver, SqlxDriverError> {
107    if database_url.starts_with("sqlite:") {
108        return Ok(SqlxDriver::Sqlite);
109    }
110
111    let Some((scheme, rest)) = database_url.split_once(':') else {
112        return Err(SqlxDriverError::UnsupportedUrlScheme { scheme: None });
113    };
114
115    match scheme {
116        "postgres" | "postgresql" => Ok(SqlxDriver::Postgres),
117        "mysql" | "mariadb" => Ok(SqlxDriver::MySql),
118        "sqlite" => Ok(SqlxDriver::Sqlite),
119        _ => Err(SqlxDriverError::UnsupportedUrlScheme {
120            scheme: Url::parse(database_url)
121                .ok()
122                .filter(|url| rest.starts_with("//") && url.scheme().eq_ignore_ascii_case(scheme))
123                .map(|_| scheme.to_owned()),
124        }),
125    }
126}