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
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct TooLong {
167 pub col: &'static str,
168 pub len: usize,
169 pub max: usize,
170}
171
172impl std::fmt::Display for TooLong {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(
175 f,
176 "{} 超长:{} 个字符,上限 {}(MySQL 上这一列是 VARCHAR({}))",
177 self.col, self.len, self.max, self.max
178 )
179 }
180}
181
182impl std::error::Error for TooLong {}
183
184pub fn check_len(col: &'static str, val: &str, max: usize) -> Result<(), TooLong> {
202 let len = val.chars().count();
203 if len > max {
204 return Err(TooLong { col, len, max });
205 }
206 Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn 从url认后端() {
215 assert_eq!(Backend::from_url("sqlite::memory:"), Backend::Sqlite);
216 assert_eq!(Backend::from_url("sqlite:/tmp/a.db"), Backend::Sqlite);
217 assert_eq!(Backend::from_url("postgres://u:p@h/db"), Backend::Postgres);
218 assert_eq!(
219 Backend::from_url("postgresql://u:p@h/db"),
220 Backend::Postgres
221 );
222 assert_eq!(
223 Backend::from_url("mysql://root:x@h:3306/db"),
224 Backend::MySql
225 );
226 assert_eq!(Backend::from_url("MySQL://ROOT@H/DB"), Backend::MySql);
227 assert_eq!(Backend::from_url("mariadb://root@h/db"), Backend::MySql);
228 }
229
230 #[test]
231 fn 占位符按方言转换() {
232 let t = "INSERT INTO t (a,b,c) VALUES (?,?,?)";
233 assert_eq!(
234 Backend::Postgres.q(t),
235 "INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
236 );
237 assert_eq!(
238 Backend::Sqlite.q(t),
239 "INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
240 );
241 assert_eq!(Backend::MySql.q(t), t);
243 }
244
245 #[test]
246 fn 冲突忽略按方言展开() {
247 let t = "{INS} t (k) VALUES (?) {NOCONFLICT}";
248 assert_eq!(
249 Backend::Postgres.q(t),
250 "INSERT INTO t (k) VALUES ($1) ON CONFLICT DO NOTHING"
251 );
252 assert_eq!(Backend::MySql.q(t), "INSERT IGNORE INTO t (k) VALUES (?) ");
253 }
254
255 #[test]
256 fn 自由文本列在mysql上必须是varchar() {
257 assert_eq!(Backend::MySql.text(8192), "VARCHAR(8192)");
259 assert_eq!(Backend::Postgres.text(8192), "TEXT");
260 assert_eq!(Backend::Sqlite.text(8192), "TEXT");
261 }
262
263 #[test]
264 fn 主键列类型按方言() {
265 assert_eq!(Backend::MySql.id_text(), "VARCHAR(128)");
267 assert_eq!(Backend::Postgres.id_text(), "TEXT");
268 assert_eq!(Backend::Sqlite.id_text(), "TEXT");
269 }
270
271 #[test]
272 fn 索引方式按方言二选一() {
273 assert!(Backend::Postgres.create_index("i", "t", "a,b").is_some());
275 assert_eq!(Backend::Postgres.inline_index("i", "a,b"), "");
276 assert!(Backend::MySql.create_index("i", "t", "a,b").is_none());
278 assert_eq!(Backend::MySql.inline_index("i", "a,b"), ", KEY i (a,b)");
279 }
280
281 #[test]
282 fn 编号从1开始且连续() {
283 let t = "UPDATE t SET a=?, b=? WHERE c=? AND d=?";
284 assert_eq!(
285 Backend::Postgres.q(t),
286 "UPDATE t SET a=$1, b=$2 WHERE c=$3 AND d=$4"
287 );
288 }
289}