1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Backend {
30 Sqlite,
31 Postgres,
32 MySql,
33}
34
35impl std::fmt::Display for Backend {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str(match self {
38 Self::Sqlite => "sqlite",
39 Self::Postgres => "postgres",
40 Self::MySql => "mysql",
41 })
42 }
43}
44
45impl Backend {
46 pub fn from_url(url: &str) -> Self {
47 let u = url.trim().to_ascii_lowercase();
48 if u.starts_with("mysql") || u.starts_with("mariadb") {
49 Self::MySql
50 } else if u.starts_with("postgres") {
51 Self::Postgres
52 } else {
53 Self::Sqlite
54 }
55 }
56
57 pub fn id_text(&self) -> &'static str {
62 match self {
63 Self::MySql => "VARCHAR(128)",
64 _ => "TEXT",
65 }
66 }
67
68 pub fn id_short(&self) -> &'static str {
70 match self {
71 Self::MySql => "VARCHAR(45)",
72 _ => "TEXT",
73 }
74 }
75
76 pub fn text(&self, n: usize) -> String {
86 match self {
87 Self::MySql => format!("VARCHAR({n})"),
88 _ => "TEXT".to_string(),
89 }
90 }
91
92 pub const ID_MAX: usize = 128;
94
95 pub const ID_SHORT_MAX: usize = 45;
97
98 pub fn q(&self, template: &str) -> String {
105 let s = template
106 .replace("{INS}", self.insert_ignore())
107 .replace("{NOCONFLICT}", self.no_conflict());
108 self.placeholders(&s)
109 }
110
111 fn insert_ignore(&self) -> &'static str {
112 match self {
113 Self::MySql => "INSERT IGNORE INTO",
114 _ => "INSERT INTO",
115 }
116 }
117
118 fn no_conflict(&self) -> &'static str {
119 match self {
120 Self::MySql => "",
122 _ => "ON CONFLICT DO NOTHING",
123 }
124 }
125
126 fn placeholders(&self, sql: &str) -> String {
127 if *self == Self::MySql {
128 return sql.to_string();
129 }
130 let mut out = String::with_capacity(sql.len() + 8);
131 let mut n = 0;
132 for c in sql.chars() {
133 if c == '?' {
134 n += 1;
135 out.push('$');
136 out.push_str(&n.to_string());
137 } else {
138 out.push(c);
139 }
140 }
141 out
142 }
143
144 pub fn create_index(&self, name: &str, table: &str, cols: &str) -> Option<String> {
147 match self {
148 Self::MySql => None,
149 _ => Some(format!(
150 "CREATE INDEX IF NOT EXISTS {name} ON {table}({cols})"
151 )),
152 }
153 }
154
155 pub fn inline_index(&self, name: &str, cols: &str) -> String {
157 match self {
158 Self::MySql => format!(", KEY {name} ({cols})"),
159 _ => String::new(),
160 }
161 }
162
163 pub fn skip_locked(&self) -> &'static str {
179 match self {
180 Self::Sqlite => "",
181 _ => " FOR UPDATE SKIP LOCKED",
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct TooLong {
189 pub col: &'static str,
190 pub len: usize,
191 pub max: usize,
192}
193
194impl std::fmt::Display for TooLong {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(
197 f,
198 "{} 超长:{} 个字符,上限 {}(MySQL 上这一列是 VARCHAR({}))",
199 self.col, self.len, self.max, self.max
200 )
201 }
202}
203
204impl std::error::Error for TooLong {}
205
206pub fn check_len(col: &'static str, val: &str, max: usize) -> Result<(), TooLong> {
224 let len = val.chars().count();
225 if len > max {
226 return Err(TooLong { col, len, max });
227 }
228 Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn 从url认后端() {
237 assert_eq!(Backend::from_url("sqlite::memory:"), Backend::Sqlite);
238 assert_eq!(Backend::from_url("sqlite:/tmp/a.db"), Backend::Sqlite);
239 assert_eq!(Backend::from_url("postgres://u:p@h/db"), Backend::Postgres);
240 assert_eq!(
241 Backend::from_url("postgresql://u:p@h/db"),
242 Backend::Postgres
243 );
244 assert_eq!(
245 Backend::from_url("mysql://root:x@h:3306/db"),
246 Backend::MySql
247 );
248 assert_eq!(Backend::from_url("MySQL://ROOT@H/DB"), Backend::MySql);
249 assert_eq!(Backend::from_url("mariadb://root@h/db"), Backend::MySql);
250 }
251
252 #[test]
253 fn 占位符按方言转换() {
254 let t = "INSERT INTO t (a,b,c) VALUES (?,?,?)";
255 assert_eq!(
256 Backend::Postgres.q(t),
257 "INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
258 );
259 assert_eq!(
260 Backend::Sqlite.q(t),
261 "INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
262 );
263 assert_eq!(Backend::MySql.q(t), t);
265 }
266
267 #[test]
268 fn 冲突忽略按方言展开() {
269 let t = "{INS} t (k) VALUES (?) {NOCONFLICT}";
270 assert_eq!(
271 Backend::Postgres.q(t),
272 "INSERT INTO t (k) VALUES ($1) ON CONFLICT DO NOTHING"
273 );
274 assert_eq!(Backend::MySql.q(t), "INSERT IGNORE INTO t (k) VALUES (?) ");
275 }
276
277 #[test]
278 fn 自由文本列在mysql上必须是varchar() {
279 assert_eq!(Backend::MySql.text(8192), "VARCHAR(8192)");
281 assert_eq!(Backend::Postgres.text(8192), "TEXT");
282 assert_eq!(Backend::Sqlite.text(8192), "TEXT");
283 }
284
285 #[test]
286 fn 主键列类型按方言() {
287 assert_eq!(Backend::MySql.id_text(), "VARCHAR(128)");
289 assert_eq!(Backend::Postgres.id_text(), "TEXT");
290 assert_eq!(Backend::Sqlite.id_text(), "TEXT");
291 }
292
293 #[test]
294 fn 索引方式按方言二选一() {
295 assert!(Backend::Postgres.create_index("i", "t", "a,b").is_some());
297 assert_eq!(Backend::Postgres.inline_index("i", "a,b"), "");
298 assert!(Backend::MySql.create_index("i", "t", "a,b").is_none());
300 assert_eq!(Backend::MySql.inline_index("i", "a,b"), ", KEY i (a,b)");
301 }
302
303 #[test]
304 fn 编号从1开始且连续() {
305 let t = "UPDATE t SET a=?, b=? WHERE c=? AND d=?";
306 assert_eq!(
307 Backend::Postgres.q(t),
308 "UPDATE t SET a=$1, b=$2 WHERE c=$3 AND d=$4"
309 );
310 }
311}