#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Sqlite,
Postgres,
MySql,
}
impl std::fmt::Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Sqlite => "sqlite",
Self::Postgres => "postgres",
Self::MySql => "mysql",
})
}
}
impl Backend {
pub fn from_url(url: &str) -> Self {
let u = url.trim().to_ascii_lowercase();
if u.starts_with("mysql") || u.starts_with("mariadb") {
Self::MySql
} else if u.starts_with("postgres") {
Self::Postgres
} else {
Self::Sqlite
}
}
pub fn id_text(&self) -> &'static str {
match self {
Self::MySql => "VARCHAR(128)",
_ => "TEXT",
}
}
pub fn id_short(&self) -> &'static str {
match self {
Self::MySql => "VARCHAR(45)",
_ => "TEXT",
}
}
pub fn text(&self, n: usize) -> String {
match self {
Self::MySql => format!("VARCHAR({n})"),
_ => "TEXT".to_string(),
}
}
pub const ID_MAX: usize = 128;
pub const ID_SHORT_MAX: usize = 45;
pub fn q(&self, template: &str) -> String {
let s = template
.replace("{INS}", self.insert_ignore())
.replace("{NOCONFLICT}", self.no_conflict());
self.placeholders(&s)
}
fn insert_ignore(&self) -> &'static str {
match self {
Self::MySql => "INSERT IGNORE INTO",
_ => "INSERT INTO",
}
}
fn no_conflict(&self) -> &'static str {
match self {
Self::MySql => "",
_ => "ON CONFLICT DO NOTHING",
}
}
fn placeholders(&self, sql: &str) -> String {
if *self == Self::MySql {
return sql.to_string();
}
let mut out = String::with_capacity(sql.len() + 8);
let mut n = 0;
for c in sql.chars() {
if c == '?' {
n += 1;
out.push('$');
out.push_str(&n.to_string());
} else {
out.push(c);
}
}
out
}
pub fn create_index(&self, name: &str, table: &str, cols: &str) -> Option<String> {
match self {
Self::MySql => None,
_ => Some(format!(
"CREATE INDEX IF NOT EXISTS {name} ON {table}({cols})"
)),
}
}
pub fn inline_index(&self, name: &str, cols: &str) -> String {
match self {
Self::MySql => format!(", KEY {name} ({cols})"),
_ => String::new(),
}
}
pub fn skip_locked(&self) -> &'static str {
match self {
Self::Sqlite => "",
_ => " FOR UPDATE SKIP LOCKED",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TooLong {
pub col: &'static str,
pub len: usize,
pub max: usize,
}
impl std::fmt::Display for TooLong {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} 超长:{} 个字符,上限 {}(MySQL 上这一列是 VARCHAR({}))",
self.col, self.len, self.max, self.max
)
}
}
impl std::error::Error for TooLong {}
pub fn check_len(col: &'static str, val: &str, max: usize) -> Result<(), TooLong> {
let len = val.chars().count();
if len > max {
return Err(TooLong { col, len, max });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 从url认后端() {
assert_eq!(Backend::from_url("sqlite::memory:"), Backend::Sqlite);
assert_eq!(Backend::from_url("sqlite:/tmp/a.db"), Backend::Sqlite);
assert_eq!(Backend::from_url("postgres://u:p@h/db"), Backend::Postgres);
assert_eq!(
Backend::from_url("postgresql://u:p@h/db"),
Backend::Postgres
);
assert_eq!(
Backend::from_url("mysql://root:x@h:3306/db"),
Backend::MySql
);
assert_eq!(Backend::from_url("MySQL://ROOT@H/DB"), Backend::MySql);
assert_eq!(Backend::from_url("mariadb://root@h/db"), Backend::MySql);
}
#[test]
fn 占位符按方言转换() {
let t = "INSERT INTO t (a,b,c) VALUES (?,?,?)";
assert_eq!(
Backend::Postgres.q(t),
"INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
);
assert_eq!(
Backend::Sqlite.q(t),
"INSERT INTO t (a,b,c) VALUES ($1,$2,$3)"
);
assert_eq!(Backend::MySql.q(t), t);
}
#[test]
fn 冲突忽略按方言展开() {
let t = "{INS} t (k) VALUES (?) {NOCONFLICT}";
assert_eq!(
Backend::Postgres.q(t),
"INSERT INTO t (k) VALUES ($1) ON CONFLICT DO NOTHING"
);
assert_eq!(Backend::MySql.q(t), "INSERT IGNORE INTO t (k) VALUES (?) ");
}
#[test]
fn 自由文本列在mysql上必须是varchar() {
assert_eq!(Backend::MySql.text(8192), "VARCHAR(8192)");
assert_eq!(Backend::Postgres.text(8192), "TEXT");
assert_eq!(Backend::Sqlite.text(8192), "TEXT");
}
#[test]
fn 主键列类型按方言() {
assert_eq!(Backend::MySql.id_text(), "VARCHAR(128)");
assert_eq!(Backend::Postgres.id_text(), "TEXT");
assert_eq!(Backend::Sqlite.id_text(), "TEXT");
}
#[test]
fn 索引方式按方言二选一() {
assert!(Backend::Postgres.create_index("i", "t", "a,b").is_some());
assert_eq!(Backend::Postgres.inline_index("i", "a,b"), "");
assert!(Backend::MySql.create_index("i", "t", "a,b").is_none());
assert_eq!(Backend::MySql.inline_index("i", "a,b"), ", KEY i (a,b)");
}
#[test]
fn 编号从1开始且连续() {
let t = "UPDATE t SET a=?, b=? WHERE c=? AND d=?";
assert_eq!(
Backend::Postgres.q(t),
"UPDATE t SET a=$1, b=$2 WHERE c=$3 AND d=$4"
);
}
}