Skip to main content

ecat_data/
dialect.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3/// 数据库方言标识。
4///
5/// 放在 `ecat-data` 而非 `ecat-orm`:`ecat-data-sqlx` 需要上报自己的方言,
6/// 而它不能依赖 `ecat-orm`。
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Dialect {
9    /// ANSI 近似(双引号标识符、`?` 占位符、`LIMIT`)。
10    /// 第三方 `SqlExecutor` 实现未声明方言时的回退值。
11    Standard,
12    Sqlite,
13    Postgres,
14    MySql,
15    Mssql,
16}
17
18impl Dialect {
19    /// 从连接串推断方言,无法识别时返回 [`Dialect::Standard`]。
20    ///
21    /// scheme 的大小写,以及前导/尾随的 C0 控制字符与空格(U+0000–U+0020),
22    /// 均被忽略(与 sqlx 底层 `url` crate 的判定一致)。
23    /// 同时兼容有 `://` 的形式(`postgres://host/db`)与 sqlite 的无 authority
24    /// 形式(`sqlite:app.db`)。
25    pub fn from_url(url: &str) -> Self {
26        // 谓词必须与 sqlx 底层 url crate 一致:url-2.5.8/src/parser.rs:1745-1747 的
27        // `c0_control_or_space` 是 `ch <= ' '`(U+0000–U+0020,含 C0 控制字符)。
28        // 不能用 str::trim():它走 Unicode White_Space 属性,不含 NUL 等 C0 控制字符,
29        // 会让 "\0postgres://host/db" 落回 Standard —— sqlx 侧却能连通,又是静默错答。
30        let scheme = url
31            .trim_matches(|c: char| c <= ' ')
32            .split("://")
33            .next()
34            .unwrap_or("")
35            .split(':')
36            .next()
37            .unwrap_or("")
38            .to_ascii_lowercase();
39        match scheme.as_str() {
40            "postgres" | "postgresql" => Self::Postgres,
41            "mysql" | "mariadb" => Self::MySql,
42            "sqlite" => Self::Sqlite,
43            "mssql" | "sqlserver" => Self::Mssql,
44            _ => Self::Standard,
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn from_url_recognizes_each_scheme() {
55        assert_eq!(
56            Dialect::from_url("postgres://localhost/db"),
57            Dialect::Postgres
58        );
59        assert_eq!(
60            Dialect::from_url("postgresql://localhost/db"),
61            Dialect::Postgres
62        );
63        assert_eq!(Dialect::from_url("mysql://localhost/db"), Dialect::MySql);
64        assert_eq!(Dialect::from_url("mariadb://localhost/db"), Dialect::MySql);
65        assert_eq!(Dialect::from_url("sqlite::memory:"), Dialect::Sqlite);
66        assert_eq!(Dialect::from_url("sqlite:app.db"), Dialect::Sqlite);
67        assert_eq!(Dialect::from_url("mssql://host:1433/db"), Dialect::Mssql);
68        assert_eq!(
69            Dialect::from_url("sqlserver://host:1433/db"),
70            Dialect::Mssql
71        );
72        assert_eq!(Dialect::from_url("postgres"), Dialect::Postgres);
73    }
74
75    #[test]
76    fn from_url_unknown_scheme_is_standard() {
77        assert_eq!(Dialect::from_url("oracle://host/db"), Dialect::Standard);
78        assert_eq!(Dialect::from_url(""), Dialect::Standard);
79        assert_eq!(Dialect::from_url("nonsense"), Dialect::Standard);
80    }
81
82    /// RFC 3986 §3.1:scheme 大小写不敏感。sqlx 侧同样会小写化,
83    /// 不归一化就会在能连通的情况下静默给出 Standard。
84    #[test]
85    fn from_url_is_case_insensitive() {
86        assert_eq!(
87            Dialect::from_url("POSTGRES://localhost/db"),
88            Dialect::Postgres
89        );
90        assert_eq!(Dialect::from_url("MySQL://localhost/db"), Dialect::MySql);
91        assert_eq!(Dialect::from_url("SQLite:app.db"), Dialect::Sqlite);
92    }
93
94    /// url crate 会 trim 首尾 U+0000–U+0020,故带空白的 URL 能连通 sqlx;
95    /// 不 trim 就会静默返回 Standard。
96    #[test]
97    fn from_url_tolerates_surrounding_whitespace() {
98        assert_eq!(Dialect::from_url(" postgres://host/db"), Dialect::Postgres);
99        assert_eq!(Dialect::from_url("\tmysql://host/db"), Dialect::MySql);
100        assert_eq!(Dialect::from_url("sqlite:app.db\n"), Dialect::Sqlite);
101        // 谓词与 url crate 一致(U+0000–U+0020):NUL/控制字符同样被 trim。
102        assert_eq!(Dialect::from_url("\0postgres://host/db"), Dialect::Postgres);
103        assert_eq!(Dialect::from_url("\u{1}mysql://host/db"), Dialect::MySql);
104    }
105
106    /// sqlite 的 URL 没有 `://`,是最容易写错的一类,单独钉住。
107    #[test]
108    fn from_url_handles_sqlite_without_authority() {
109        assert_eq!(
110            Dialect::from_url("sqlite:ecat-test.db?mode=memory"),
111            Dialect::Sqlite
112        );
113    }
114}