use super::{Dialect, DialectKind, SqlType};
use crate::transaction::IsolationLevel;
#[derive(Debug, Clone, Copy, Default)]
pub struct MySqlDialect;
impl MySqlDialect {
pub const fn new() -> Self {
Self
}
}
impl Dialect for MySqlDialect {
fn name(&self) -> &'static str {
"mysql"
}
fn kind(&self) -> DialectKind {
DialectKind::Mysql
}
fn quote_identifier(&self, identifier: &str, out: &mut String) {
out.push('`');
for ch in identifier.chars() {
if ch == '`' {
out.push('`');
}
out.push(ch);
}
out.push('`');
}
fn placeholder(&self, _index: usize, out: &mut String) {
out.push('?');
}
fn supports_returning(&self) -> bool {
false
}
fn max_bind_params(&self) -> usize {
65535
}
fn escape_string_literal(&self, value: &str, out: &mut String) {
crate::dialect::writer::quote_string_literal_mysql(value, out);
}
fn acquire_migration_lock_sql(&self, key: i64) -> Option<String> {
Some(format!("SELECT GET_LOCK('tork_migration_{key}', 60)"))
}
fn release_migration_lock_sql(&self, key: i64) -> Option<String> {
Some(format!("SELECT RELEASE_LOCK('tork_migration_{key}')"))
}
fn map_sql_type(&self, ty: SqlType, out: &mut String) {
match ty {
SqlType::Boolean => out.push_str("TINYINT(1)"),
SqlType::Integer => out.push_str("INT"),
SqlType::BigInt => out.push_str("BIGINT"),
SqlType::Real => out.push_str("DOUBLE"),
SqlType::Text => out.push_str("TEXT"),
SqlType::Varchar(length) => {
out.push_str("VARCHAR(");
out.push_str(&length.to_string());
out.push(')');
}
SqlType::Timestamp => out.push_str("DATETIME"),
SqlType::Blob => out.push_str("BLOB"),
SqlType::Json => out.push_str("JSON"),
SqlType::Uuid => out.push_str("CHAR(36)"),
SqlType::Array(_) => out.push_str("TEXT"),
SqlType::Enum { variants, .. } => {
out.push_str("ENUM(");
for (index, variant) in variants.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.escape_string_literal(variant, out);
}
out.push(')');
}
}
}
fn begin_sql(&self) -> &'static str {
"START TRANSACTION"
}
fn isolation_setup_sql(&self, level: IsolationLevel) -> Option<String> {
level
.standard_sql()
.map(|name| format!("SET TRANSACTION ISOLATION LEVEL {name}"))
}
fn begin_with_sql(&self, _level: IsolationLevel) -> String {
"START TRANSACTION".to_string()
}
fn release_sql(&self, name: &str) -> String {
format!("RELEASE SAVEPOINT {name}")
}
fn rollback_to_sql(&self, name: &str) -> String {
format!("ROLLBACK TO SAVEPOINT {name}")
}
fn supports_filter_clause(&self) -> bool {
false
}
fn supports_full_join(&self) -> bool {
false
}
fn supports_lock_modifiers(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn type_str(ty: SqlType) -> String {
let mut out = String::new();
MySqlDialect::new().map_sql_type(ty, &mut out);
out
}
#[test]
fn mysql_escapes_backslashes_in_string_literals() {
let mut out = String::new();
MySqlDialect::new().escape_string_literal("a\\'b", &mut out);
assert_eq!(out, "'a\\\\''b'");
}
#[test]
fn sqlite_does_not_escape_backslashes() {
let mut out = String::new();
crate::dialect::SqliteDialect::new().escape_string_literal("a\\'b", &mut out);
assert_eq!(out, "'a\\''b'");
}
#[test]
fn function_names_cannot_inject_sql() {
use crate::dialect::render_expr;
use crate::query::expr::Expr;
let expr = Expr::func("evil'); DROP TABLE users; --", vec![]);
let (sql, _) = render_expr(&MySqlDialect::new(), &expr);
assert!(!sql.contains('\''), "sql: {sql}");
assert!(!sql.contains(';'), "sql: {sql}");
assert!(!sql.contains("--"), "sql: {sql}");
}
#[test]
fn quotes_identifiers_with_backticks() {
let dialect = MySqlDialect::new();
assert_eq!(dialect.quoted("users"), "`users`");
assert_eq!(dialect.quoted("a`b"), "`a``b`");
}
#[test]
fn placeholders_are_positional_question_marks() {
let dialect = MySqlDialect::new();
let mut out = String::new();
dialect.placeholder(0, &mut out);
dialect.placeholder(7, &mut out);
assert_eq!(out, "??");
}
#[test]
fn maps_types_to_mysql_spellings() {
assert_eq!(type_str(SqlType::Boolean), "TINYINT(1)");
assert_eq!(type_str(SqlType::Integer), "INT");
assert_eq!(type_str(SqlType::BigInt), "BIGINT");
assert_eq!(type_str(SqlType::Real), "DOUBLE");
assert_eq!(type_str(SqlType::Text), "TEXT");
assert_eq!(type_str(SqlType::Varchar(50)), "VARCHAR(50)");
assert_eq!(type_str(SqlType::Timestamp), "DATETIME");
assert_eq!(type_str(SqlType::Blob), "BLOB");
assert_eq!(type_str(SqlType::Json), "JSON");
assert_eq!(type_str(SqlType::Uuid), "CHAR(36)");
}
#[test]
fn lacks_returning_filter_and_full_join() {
let dialect = MySqlDialect::new();
assert!(!dialect.supports_returning());
assert!(!dialect.supports_filter_clause());
assert!(!dialect.supports_full_join());
}
#[test]
fn savepoint_release_uses_savepoint_keyword() {
let dialect = MySqlDialect::new();
assert_eq!(dialect.release_sql("sp1"), "RELEASE SAVEPOINT sp1");
assert_eq!(dialect.rollback_to_sql("sp1"), "ROLLBACK TO SAVEPOINT sp1");
}
#[test]
fn standard_isolation_levels_use_a_set_statement() {
use crate::transaction::IsolationLevel;
let dialect = MySqlDialect::new();
assert_eq!(
dialect
.isolation_setup_sql(IsolationLevel::Serializable)
.as_deref(),
Some("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
);
assert_eq!(dialect.isolation_setup_sql(IsolationLevel::Deferred), None);
}
#[test]
fn uses_named_user_locks_for_migrations() {
let dialect = MySqlDialect::new();
assert_eq!(
dialect.acquire_migration_lock_sql(42).as_deref(),
Some("SELECT GET_LOCK('tork_migration_42', 60)")
);
assert_eq!(
dialect.release_migration_lock_sql(42).as_deref(),
Some("SELECT RELEASE_LOCK('tork_migration_42')")
);
assert_eq!(dialect.max_bind_params(), 65535);
}
}