use super::types::SnapshotColumn;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrationDialect {
Postgres,
MySql,
Sqlite,
}
impl MigrationDialect {
pub fn quote(&self, ident: &str) -> String {
match self {
MigrationDialect::Postgres | MigrationDialect::Sqlite => quote_qualified(ident, '"'),
MigrationDialect::MySql => quote_qualified(ident, '`'),
}
}
pub fn map_column_type(&self, col: &SnapshotColumn) -> String {
if let Some(explicit) = &col.column_type {
if !explicit.trim().is_empty() {
return explicit.trim().to_string();
}
}
let tn = inner_type_name(&col.type_name);
if col.is_auto_increment {
if tn.ends_with("i32") {
return match self {
MigrationDialect::Postgres => "SERIAL".into(),
MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
MigrationDialect::Sqlite => "INTEGER".into(),
};
}
if tn.ends_with("i64") {
return match self {
MigrationDialect::Postgres => "BIGSERIAL".into(),
MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
MigrationDialect::Sqlite => "INTEGER".into(),
};
}
}
if col.is_sequence {
if tn.ends_with("i32") {
return match self {
MigrationDialect::Postgres => "INTEGER".into(),
MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
MigrationDialect::Sqlite => "INTEGER".into(),
};
}
if tn.ends_with("i64") {
return match self {
MigrationDialect::Postgres => "BIGINT".into(),
MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
MigrationDialect::Sqlite => "INTEGER".into(),
};
}
}
let base: &str = if tn.ends_with("i16") {
"SMALLINT"
} else if tn.ends_with("i32") {
"INTEGER"
} else if tn.ends_with("i64") {
"BIGINT"
} else if tn.ends_with("f32") {
"REAL"
} else if tn.ends_with("f64") {
"DOUBLE PRECISION"
} else if tn.ends_with("bool") {
"BOOLEAN"
} else if tn.ends_with("String") {
if col.is_fixed_length {
return format!("CHAR({})", col.max_length.unwrap_or(1));
}
return match col.max_length {
Some(n) => format!("VARCHAR({})", n),
None => "TEXT".into(),
};
} else if tn.ends_with("Vec<u8>") {
return match self {
MigrationDialect::Postgres => "BYTEA".into(),
MigrationDialect::MySql | MigrationDialect::Sqlite => "BLOB".into(),
};
} else if tn.contains("NaiveDateTime") {
return match (self, col.precision) {
(MigrationDialect::Postgres, Some(p)) => format!("TIMESTAMP({p})"),
(MigrationDialect::MySql, Some(p)) => format!("DATETIME({p})"),
(MigrationDialect::Postgres, None) => "TIMESTAMP".into(),
(MigrationDialect::MySql, None) => "DATETIME".into(),
(MigrationDialect::Sqlite, _) => "TEXT".into(),
};
} else if tn.contains("NaiveDate") {
return match self {
MigrationDialect::Postgres => "DATE".into(),
MigrationDialect::MySql => "DATE".into(),
MigrationDialect::Sqlite => "TEXT".into(),
};
} else if tn.contains("DateTime") {
return match (self, col.precision) {
(MigrationDialect::Postgres, Some(p)) => format!("TIMESTAMPTZ({p})"),
(MigrationDialect::MySql, Some(p)) => format!("DATETIME({p})"),
(MigrationDialect::Postgres, None) => "TIMESTAMPTZ".into(),
(MigrationDialect::MySql, None) => "DATETIME".into(),
(MigrationDialect::Sqlite, _) => "TEXT".into(),
};
} else if tn.contains("Uuid") {
return match self {
MigrationDialect::Postgres => "UUID".into(),
MigrationDialect::MySql => "CHAR(36)".into(),
MigrationDialect::Sqlite => "TEXT".into(),
};
} else if tn.contains("Decimal") {
let (p, s) = (col.precision.unwrap_or(38), col.scale.unwrap_or(18));
return match self {
MigrationDialect::Postgres => {
if col.precision.is_some() {
format!("NUMERIC({p},{s})")
} else {
"NUMERIC".into()
}
}
MigrationDialect::MySql => format!("DECIMAL({p},{s})"),
MigrationDialect::Sqlite => "TEXT".into(),
};
} else {
"TEXT"
};
base.to_string()
}
}
pub(crate) fn quote_qualified(ident: &str, quote: char) -> String {
ident
.split('.')
.map(|part| format!("{quote}{part}{quote}"))
.collect::<Vec<_>>()
.join(".")
}
fn inner_type_name(type_name: &str) -> &str {
if let Some(pos) = type_name.find("Option<") {
let rest = &type_name[pos + "Option<".len()..];
return rest.strip_suffix('>').unwrap_or(rest);
}
type_name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn option_string_uses_inner_type_and_max_length() {
let col = SnapshotColumn {
type_name: "core::option::Option<alloc::string::String>".into(),
max_length: Some(32),
..Default::default()
};
assert_eq!(MigrationDialect::MySql.map_column_type(&col), "VARCHAR(32)");
assert_eq!(
MigrationDialect::Postgres.map_column_type(&col),
"VARCHAR(32)"
);
}
#[test]
fn option_uuid_and_decimal_use_inner_type() {
let uuid = SnapshotColumn {
type_name: "core::option::Option<uuid::Uuid>".into(),
..Default::default()
};
assert_eq!(MigrationDialect::MySql.map_column_type(&uuid), "CHAR(36)");
let decimal = SnapshotColumn {
type_name: "core::option::Option<rust_decimal::Decimal>".into(),
..Default::default()
};
assert_eq!(
MigrationDialect::MySql.map_column_type(&decimal),
"DECIMAL(38,18)"
);
}
}