use crate::core::{
AggregateQuery, BulkInsertQuery, BulkUpdateQuery, ConflictClause, CountQuery, DeleteQuery,
FieldSchema, FieldType, InsertQuery, ModelSchema, SelectQuery, UpdateQuery,
};
use super::writers::{
write_aggregate, write_bulk_insert, write_count, write_delete, write_insert, write_select,
write_update, Sql,
};
use super::{CompiledStatement, Dialect, SqlError};
pub(crate) const SKIPPED_INSERT_ID: u64 = u64::MAX;
#[derive(Debug, Default, Clone, Copy)]
pub struct MySql;
#[cfg(feature = "mysql")]
pub static DIALECT: &MySql = &MySql;
#[cfg(feature = "mysql")]
impl MySql {
pub(crate) fn compile_insert_auto_id(
&self,
query: &InsertQuery,
) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::with_capacity(self, query.values.len());
super::writers::write_insert_rows(&mut b, query)?;
Ok(b.finish())
}
}
impl Dialect for MySql {
fn name(&self) -> &'static str {
"mysql"
}
fn default_values_clause(&self) -> &'static str {
" () VALUES ()"
}
fn supports_nulls_order(&self) -> bool {
false
}
fn random_fn(&self) -> &'static str {
"RAND"
}
fn offset_without_limit_clause(&self) -> Option<&'static str> {
Some(" LIMIT 18446744073709551615")
}
fn in_subquery_limit_needs_derived_table(&self) -> bool {
true
}
fn quote_ident(&self, name: &str) -> String {
super::dialect::quote_with(name, '`')
}
fn quote_literal(&self, text: &str) -> String {
format!("'{}'", text.replace('\\', "\\\\").replace('\'', "''"))
}
fn write_inline_column_comment(&self, comment: &str) -> Option<String> {
Some(format!(" COMMENT {}", self.quote_literal(comment)))
}
fn write_inline_table_comment(&self, comment: &str) -> Option<String> {
Some(format!(" COMMENT={}", self.quote_literal(comment)))
}
fn serial_type(&self, field_type: FieldType) -> &'static str {
match field_type {
FieldType::I32 => "INT AUTO_INCREMENT",
_ => "BIGINT AUTO_INCREMENT",
}
}
fn cast_aggregate_to_int(&self, expr: &str) -> String {
format!("CAST({expr} AS SIGNED)")
}
fn cast_aggregate_to_float(&self, expr: &str) -> String {
format!("CAST({expr} AS DOUBLE)")
}
fn cast_type(&self, ty: FieldType) -> Option<&'static str> {
Some(match ty {
FieldType::I16 | FieldType::I32 | FieldType::I64 => "SIGNED",
FieldType::F32 => "FLOAT",
FieldType::F64 => "DOUBLE",
FieldType::Bool => "UNSIGNED",
FieldType::String => "CHAR",
FieldType::DateTime => "DATETIME",
FieldType::Date => "DATE",
FieldType::Time => "TIME",
FieldType::Decimal => "DECIMAL(65, 28)",
FieldType::Binary => "BINARY",
FieldType::Json => "JSON",
FieldType::Uuid
| FieldType::Array(_)
| FieldType::Range(_)
| FieldType::HStore
| FieldType::Vector(_)
| FieldType::Geometry(_) => return None,
})
}
fn column_type(&self, ty: FieldType, max_length: Option<u32>) -> String {
match ty {
FieldType::I16 => "SMALLINT".into(),
FieldType::I32 => "INT".into(),
FieldType::I64 => "BIGINT".into(),
FieldType::F32 => "FLOAT".into(),
FieldType::F64 => "DOUBLE".into(),
FieldType::Bool => "TINYINT(1)".into(),
FieldType::String => match max_length {
Some(n) => format!("VARCHAR({n})"),
None => "LONGTEXT".into(),
},
FieldType::DateTime => "DATETIME(6)".into(),
FieldType::Date => "DATE".into(),
FieldType::Uuid => "CHAR(36)".into(),
FieldType::Json => "JSON".into(),
FieldType::Decimal => "DECIMAL(65, 28)".into(),
FieldType::Binary => "LONGBLOB".into(),
FieldType::Time => "TIME(6)".into(),
FieldType::Array(_) => "TEXT".into(),
FieldType::Range(_) => "TEXT".into(),
FieldType::HStore => "TEXT".into(),
FieldType::Vector(_) => "TEXT".into(),
FieldType::Geometry(_) => "TEXT".into(),
}
}
fn ci_text_type(&self, max_length: Option<u32>) -> String {
match max_length {
Some(n) => format!("VARCHAR({n}) COLLATE utf8mb4_general_ci"),
None => "LONGTEXT COLLATE utf8mb4_general_ci".to_owned(),
}
}
fn default_collation_sql(&self) -> Option<&'static str> {
Some(
"SELECT CAST(DEFAULT_COLLATION_NAME AS CHAR) FROM information_schema.SCHEMATA \
WHERE SCHEMA_NAME = DATABASE()",
)
}
fn translate_default_expr(&self, expr: &str, ty: &str, max_length: Option<u32>) -> String {
let mut out = expr.trim().to_owned();
match out.as_str() {
"now()" | "NOW()" | "current_timestamp" | "CURRENT_TIMESTAMP" => {
out = "CURRENT_TIMESTAMP(6)".to_owned();
}
e if super::dialect::is_uuid_expr(e) => return "(UUID())".to_owned(),
_ => {
if let Some(idx) = out.rfind("::") {
let suffix = &out[idx + 2..];
if !suffix.is_empty()
&& suffix
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
out.truncate(idx);
}
}
}
}
let renders_as_lob = ty.eq_ignore_ascii_case("json")
|| ty.eq_ignore_ascii_case("binary")
|| (ty.eq_ignore_ascii_case("string") && max_length.is_none());
if renders_as_lob {
if !(out.starts_with('(') && out.ends_with(')')) {
out = format!("({out})");
}
}
out
}
fn index_method_clause(&self, method: &str) -> String {
match method {
"hash" => " USING HASH".to_owned(),
_ => String::new(),
}
}
fn supports_create_index_if_not_exists(&self) -> bool {
false
}
fn supports_partial_index(&self) -> bool {
false
}
fn supports_on_delete_set_default(&self) -> bool {
false
}
fn drop_check_constraint_sql(&self, table: &str, name: &str) -> Option<String> {
Some(format!(
"ALTER TABLE {} DROP CHECK {}",
self.quote_ident(table),
self.quote_ident(name)
))
}
fn drop_foreign_key_sql(&self, table: &str, name: &str) -> Option<String> {
Some(format!(
"ALTER TABLE {} DROP FOREIGN KEY {}",
self.quote_ident(table),
self.quote_ident(name)
))
}
fn defer_foreign_keys_sql(&self) -> Option<&'static str> {
None
}
fn clear_tables_sql(&self, tables: &[&str]) -> Vec<String> {
tables
.iter()
.map(|t| {
format!(
"DELETE /*+ SET_VAR(foreign_key_checks=0) */ FROM {}",
self.quote_ident(t)
)
})
.collect()
}
fn foreign_key_names_sql(&self) -> Option<&'static str> {
Some(
"SELECT CAST(k.CONSTRAINT_NAME AS CHAR) FROM information_schema.KEY_COLUMN_USAGE k \
WHERE k.TABLE_SCHEMA = DATABASE() AND k.TABLE_NAME = ? AND k.COLUMN_NAME = ? \
AND k.REFERENCED_TABLE_NAME IS NOT NULL AND NOT EXISTS (\
SELECT 1 FROM information_schema.KEY_COLUMN_USAGE o \
WHERE o.TABLE_SCHEMA = k.TABLE_SCHEMA AND o.TABLE_NAME = k.TABLE_NAME \
AND o.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND o.ORDINAL_POSITION > 1)",
)
}
fn sole_leading_column_sql(&self) -> Option<&'static str> {
Some(
"SELECT CAST(s.COLUMN_NAME AS CHAR) FROM information_schema.STATISTICS s \
WHERE s.TABLE_SCHEMA = DATABASE() AND s.TABLE_NAME = ? AND s.INDEX_NAME = ? \
AND s.SEQ_IN_INDEX = 1 AND NOT EXISTS (\
SELECT 1 FROM information_schema.STATISTICS o \
WHERE o.TABLE_SCHEMA = s.TABLE_SCHEMA AND o.TABLE_NAME = s.TABLE_NAME \
AND o.COLUMN_NAME = s.COLUMN_NAME AND o.SEQ_IN_INDEX = 1 \
AND o.INDEX_NAME <> s.INDEX_NAME \
AND o.SUB_PART IS NULL AND o.INDEX_TYPE = 'BTREE')",
)
}
fn composite_fks_needing_index_sql(&self) -> Option<&'static str> {
Some(
"SELECT CAST(k.CONSTRAINT_NAME AS CHAR) FROM information_schema.KEY_COLUMN_USAGE k \
WHERE k.TABLE_SCHEMA = DATABASE() AND k.TABLE_NAME = ? \
AND k.REFERENCED_TABLE_NAME IS NOT NULL AND k.ORDINAL_POSITION = 2 \
AND NOT EXISTS (SELECT 1 FROM information_schema.KEY_COLUMN_USAGE f \
WHERE f.TABLE_SCHEMA = k.TABLE_SCHEMA AND f.TABLE_NAME = k.TABLE_NAME \
AND f.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND NOT EXISTS (\
SELECT 1 FROM information_schema.STATISTICS s \
WHERE s.TABLE_SCHEMA = f.TABLE_SCHEMA AND s.TABLE_NAME = f.TABLE_NAME \
AND s.INDEX_NAME = ? AND s.SEQ_IN_INDEX = f.ORDINAL_POSITION \
AND s.COLUMN_NAME = f.COLUMN_NAME AND s.SUB_PART IS NULL)) \
AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS j \
WHERE j.TABLE_SCHEMA = k.TABLE_SCHEMA AND j.TABLE_NAME = k.TABLE_NAME \
AND j.INDEX_NAME <> ? AND j.SEQ_IN_INDEX = 1 \
AND NOT EXISTS (SELECT 1 FROM information_schema.KEY_COLUMN_USAGE f \
WHERE f.TABLE_SCHEMA = k.TABLE_SCHEMA AND f.TABLE_NAME = k.TABLE_NAME \
AND f.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND NOT EXISTS (\
SELECT 1 FROM information_schema.STATISTICS s \
WHERE s.TABLE_SCHEMA = j.TABLE_SCHEMA AND s.TABLE_NAME = j.TABLE_NAME \
AND s.INDEX_NAME = j.INDEX_NAME AND s.SEQ_IN_INDEX = f.ORDINAL_POSITION \
AND s.COLUMN_NAME = f.COLUMN_NAME AND s.SUB_PART IS NULL \
AND s.INDEX_TYPE = 'BTREE')))",
)
}
fn modifies_whole_column(&self) -> bool {
true
}
fn unique_index_names_sql(&self) -> Option<&'static str> {
Some(
"SELECT CAST(s.INDEX_NAME AS CHAR) FROM information_schema.STATISTICS s \
WHERE s.TABLE_SCHEMA = DATABASE() AND s.TABLE_NAME = ? AND s.COLUMN_NAME = ? \
AND s.NON_UNIQUE = 0 AND s.INDEX_NAME <> 'PRIMARY' AND NOT EXISTS (\
SELECT 1 FROM information_schema.STATISTICS o \
WHERE o.TABLE_SCHEMA = s.TABLE_SCHEMA AND o.TABLE_NAME = s.TABLE_NAME \
AND o.INDEX_NAME = s.INDEX_NAME AND o.SEQ_IN_INDEX > 1)",
)
}
fn drop_unique_index_sql(&self, table: &str, name: &str) -> Option<String> {
Some(format!(
"ALTER TABLE {} DROP INDEX {}",
self.quote_ident(table),
self.quote_ident(name)
))
}
fn insert_on_conflict_skip(&self, conflict_cols: &[&str]) -> String {
if conflict_cols.is_empty() {
return String::new();
}
let pivot = conflict_cols[0];
format!("ON DUPLICATE KEY UPDATE {pivot} = {pivot}")
}
fn bool_literal(&self, b: bool) -> &'static str {
if b {
"1"
} else {
"0"
}
}
fn write_ilike(&self, sql: &mut String, qualified_col: &str, placeholder: &str, negated: bool) {
sql.push_str("LOWER(");
sql.push_str(qualified_col);
sql.push_str(if negated {
") NOT LIKE LOWER("
} else {
") LIKE LOWER("
});
sql.push_str(placeholder);
sql.push(')');
}
fn write_regex(
&self,
sql: &mut String,
qualified_col: &str,
placeholder: &str,
case_sensitive: bool,
negated: bool,
) {
let kw = if negated { " NOT REGEXP " } else { " REGEXP " };
if case_sensitive {
sql.push_str(qualified_col);
sql.push_str(kw);
sql.push_str(placeholder);
} else {
sql.push_str("LOWER(");
sql.push_str(qualified_col);
sql.push(')');
sql.push_str(kw);
sql.push_str("LOWER(");
sql.push_str(placeholder);
sql.push(')');
}
}
fn write_trigram_similar(
&self,
_sql: &mut String,
_qualified_col: &str,
_placeholder: &str,
word: bool,
) -> Result<(), super::SqlError> {
Err(super::SqlError::OpNotSupportedInDialect {
op: if word {
"trigram_word_similar (%>) — pg_trgm is Postgres-only"
} else {
"trigram_similar (%) — pg_trgm is Postgres-only"
},
dialect: "mysql",
})
}
fn write_search(
&self,
_sql: &mut String,
_qualified_col: &str,
_placeholder: &str,
) -> Result<(), super::SqlError> {
Err(super::SqlError::OpNotSupportedInDialect {
op: "search (__search) — full-text search shape is Postgres-only; \
use MySQL `MATCH … AGAINST` via a raw predicate",
dialect: "mysql",
})
}
fn write_array_op(
&self,
_sql: &mut String,
_qualified_col: &str,
_placeholder: &str,
_op: &'static str,
) -> Result<(), super::SqlError> {
Err(super::SqlError::OpNotSupportedInDialect {
op: "array operators (@>, <@, &&) — PG ArrayField is Postgres-only; \
use JSON columns + JSON-shape operators on MySQL",
dialect: "mysql",
})
}
fn write_range_op(
&self,
_sql: &mut String,
_qualified_col: &str,
_placeholder: &str,
_op: &'static str,
) -> Result<(), super::SqlError> {
Err(super::SqlError::OpNotSupportedInDialect {
op: "range operators (@>, <@, &&, <<, >>, -|-) — PG RangeField is Postgres-only; \
store lo/hi as separate columns on MySQL",
dialect: "mysql",
})
}
fn write_null_safe_eq(
&self,
sql: &mut String,
qualified_col: &str,
placeholder: &str,
distinct: bool,
) {
if distinct {
sql.push_str("NOT (");
}
sql.push_str(qualified_col);
sql.push_str(" <=> ");
sql.push_str(placeholder);
if distinct {
sql.push(')');
}
}
fn write_json_param(&self, sql: &mut String, placeholder: &str) {
sql.push_str("CAST(");
sql.push_str(placeholder);
sql.push_str(" AS JSON)");
}
fn write_json_contains(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
sql.push_str("JSON_CONTAINS(");
sql.push_str(qualified_col);
sql.push_str(", ");
sql.push_str(placeholder);
sql.push(')');
}
fn write_json_contained_by(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
sql.push_str("JSON_CONTAINS(");
sql.push_str(placeholder);
sql.push_str(", ");
sql.push_str(qualified_col);
sql.push(')');
}
fn write_json_has_key(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
sql.push_str("JSON_CONTAINS_PATH(");
sql.push_str(qualified_col);
sql.push_str(", 'one', CONCAT('$.', ");
sql.push_str(placeholder);
sql.push_str("))");
}
fn write_json_has_any_keys(
&self,
sql: &mut String,
qualified_col: &str,
placeholders: &[String],
) {
write_my_json_has_keys(sql, qualified_col, placeholders, "one");
}
fn write_json_has_all_keys(
&self,
sql: &mut String,
qualified_col: &str,
placeholders: &[String],
) {
write_my_json_has_keys(sql, qualified_col, placeholders, "all");
}
fn write_conflict_clause(
&self,
sql: &mut String,
model: &ModelSchema,
conflict: &ConflictClause,
) -> Result<(), SqlError> {
match conflict {
ConflictClause::DoNothing => {
sql.push_str(" ON DUPLICATE KEY UPDATE ");
write_my_skip_marker(sql, model, &SKIPPED_INSERT_ID.to_string())?;
}
ConflictClause::DoUpdate {
target,
update_columns,
} => {
let _ = target;
if update_columns.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
sql.push_str(" ON DUPLICATE KEY UPDATE ");
let mut id = model.primary_key().filter(|f| f.is_serial()).map(|pk| {
let mut id = String::new();
write_my_ident(&mut id, pk.column);
id
});
for (i, col) in update_columns.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
let mut value = String::from("VALUES(");
write_my_ident(&mut value, col);
value.push(')');
write_my_ident(sql, col);
sql.push_str(" = ");
match id.take() {
Some(id) => write_my_set_insert_id(sql, &id, &value),
None => sql.push_str(&value),
}
}
}
}
Ok(())
}
fn acquire_session_lock_sql(&self) -> Option<String> {
Some(format!(
"SELECT GET_LOCK({}, 0)",
my_db_lock_name(self.placeholder(1))
))
}
fn release_session_lock_sql(&self) -> Option<String> {
Some(format!(
"SELECT RELEASE_LOCK({})",
my_db_lock_name(self.placeholder(1))
))
}
fn compile_select(&self, query: &SelectQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_select(&mut b, query)?;
Ok(b.finish())
}
fn compile_count(&self, query: &CountQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_count(&mut b, query)?;
Ok(b.finish())
}
fn compile_aggregate(&self, query: &AggregateQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_aggregate(&mut b, query)?;
Ok(b.finish())
}
fn compile_insert(&self, query: &InsertQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::with_capacity(self, query.values.len());
write_insert(&mut b, query)?;
Ok(b.finish())
}
fn compile_bulk_insert(&self, query: &BulkInsertQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::with_capacity(self, query.columns.len() * query.rows.len());
write_bulk_insert(&mut b, query)?;
Ok(b.finish())
}
fn compile_update(&self, query: &UpdateQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_update(&mut b, query)?;
Ok(b.finish())
}
fn compile_delete(&self, query: &DeleteQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_delete(&mut b, query)?;
Ok(b.finish())
}
fn compile_bulk_update(&self, query: &BulkUpdateQuery) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(self);
write_mysql_bulk_update(&mut b, query)?;
Ok(b.finish())
}
}
fn my_db_lock_name(prefix: impl std::fmt::Display) -> String {
format!("CONCAT({prefix}, SHA1(COALESCE(DATABASE(), '')))")
}
fn write_my_ident(sql: &mut String, name: &str) {
sql.push('`');
for ch in name.chars() {
if ch == '`' {
sql.push_str("``");
} else {
sql.push(ch);
}
}
sql.push('`');
}
fn write_my_skip_marker(sql: &mut String, model: &ModelSchema, id: &str) -> Result<(), SqlError> {
let plain = |f: &&FieldSchema| !f.is_serial() && f.generated_as.is_none();
let pivot = model
.primary_key()
.filter(plain)
.or_else(|| model.fields.iter().find(plain))
.or_else(|| model.primary_key())
.ok_or(SqlError::MissingPrimaryKey)?
.column;
let mut col = String::new();
write_my_ident(&mut col, pivot);
sql.push_str(&col);
sql.push_str(" = ");
write_my_set_insert_id(sql, id, &col);
Ok(())
}
fn write_my_set_insert_id(sql: &mut String, id: &str, value: &str) {
sql.push_str("IF(LAST_INSERT_ID(");
sql.push_str(id);
sql.push_str("), ");
sql.push_str(value);
sql.push_str(", ");
sql.push_str(value);
sql.push(')');
}
fn write_my_json_has_keys(
sql: &mut String,
qualified_col: &str,
placeholders: &[String],
mode: &'static str,
) {
sql.push_str("JSON_CONTAINS_PATH(");
sql.push_str(qualified_col);
sql.push_str(", '");
sql.push_str(mode);
sql.push('\'');
for p in placeholders {
sql.push_str(", CONCAT('$.', ");
sql.push_str(p);
sql.push(')');
}
sql.push(')');
}
fn write_mysql_bulk_update(
b: &mut crate::sql::writers::Sql<'_>,
query: &crate::core::BulkUpdateQuery,
) -> Result<(), SqlError> {
use std::fmt::Write as _;
if query.rows.is_empty() {
return Err(SqlError::EmptyBulkInsert);
}
if query.update_columns.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
let pk_field = query
.model
.primary_key()
.ok_or(SqlError::MissingPrimaryKey)?;
b.sql.push_str("UPDATE ");
b.write_ident(query.model.table);
b.sql.push_str(" INNER JOIN (VALUES ");
let mut first_row = true;
for row in &query.rows {
if !first_row {
b.sql.push_str(", ");
}
first_row = false;
b.sql.push_str("ROW(");
for (i, val) in row.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.params.push(val.clone());
let _ = write!(b.sql, "{}", b.d.placeholder(b.params.len()));
}
b.sql.push(')');
}
b.sql.push_str(") AS __data(");
b.write_ident(pk_field.column);
for col in &query.update_columns {
b.sql.push_str(", ");
b.write_ident(col);
}
b.sql.push_str(") ON ");
b.write_ident(query.model.table);
b.sql.push('.');
b.write_ident(pk_field.column);
b.sql.push_str(" = __data.");
b.write_ident(pk_field.column);
b.sql.push_str(" SET ");
let mut first = true;
for col in &query.update_columns {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(query.model.table);
b.sql.push('.');
b.write_ident(col);
b.sql.push_str(" = __data.");
b.write_ident(col);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::FieldType;
#[test]
fn name_is_mysql() {
assert_eq!(MySql.name(), "mysql");
}
#[test]
fn quote_literal_escapes_backslash_and_quote() {
assert_eq!(MySql.quote_literal(r"a\'b"), r"'a\\''b'");
assert_eq!(crate::sql::Postgres.quote_literal(r"a\'b"), r"'a\''b'");
}
#[test]
fn quote_ident_uses_backticks() {
assert_eq!(MySql.quote_ident("col"), "`col`");
assert_eq!(MySql.quote_ident("schema.table"), "`schema.table`");
}
#[test]
fn quote_ident_escapes_embedded_backticks() {
assert_eq!(MySql.quote_ident("a`b"), "`a``b`");
}
#[test]
fn placeholder_is_question_mark() {
assert_eq!(MySql.placeholder(1), "?");
assert_eq!(MySql.placeholder(7), "?");
}
#[test]
fn serial_type_uses_auto_increment() {
assert_eq!(MySql.serial_type(FieldType::I32), "INT AUTO_INCREMENT");
assert_eq!(MySql.serial_type(FieldType::I64), "BIGINT AUTO_INCREMENT");
}
#[test]
fn unbounded_string_is_longtext() {
assert_eq!(MySql.column_type(FieldType::String, None), "LONGTEXT");
assert_eq!(
MySql.column_type(FieldType::String, Some(64)),
"VARCHAR(64)"
);
assert_eq!(
MySql.ci_text_type(None),
"LONGTEXT COLLATE utf8mb4_general_ci"
);
}
#[test]
fn bool_literal_uses_one_zero() {
assert_eq!(MySql.bool_literal(true), "1");
assert_eq!(MySql.bool_literal(false), "0");
}
#[test]
fn null_cast_returns_none() {
assert!(MySql.null_cast(FieldType::I32).is_none());
assert!(MySql.null_cast(FieldType::String).is_none());
}
#[test]
fn does_not_support_returning() {
assert!(!MySql.supports_returning());
}
#[test]
fn does_not_support_concurrent_index() {
assert!(!MySql.supports_concurrent_index());
}
#[test]
fn default_on_lob_columns_uses_expression_form() {
assert_eq!(
MySql.translate_default_expr("'{}'", "string", None),
"('{}')"
);
assert_eq!(MySql.translate_default_expr("'{}'", "json", None), "('{}')");
assert_eq!(MySql.translate_default_expr("''", "binary", None), "('')");
assert_eq!(
MySql.translate_default_expr("''", "string", Some(500)),
"''"
);
assert_eq!(MySql.translate_default_expr("0", "i64", None), "0");
assert_eq!(
MySql.translate_default_expr("now()", "datetime", None),
"CURRENT_TIMESTAMP(6)"
);
}
#[test]
fn supports_op_accepts_every_operator_after_batch4() {
use crate::core::Op;
for op in [
Op::Eq,
Op::Ne,
Op::Lt,
Op::Lte,
Op::Gt,
Op::Gte,
Op::In,
Op::NotIn,
Op::Like,
Op::NotLike,
Op::ILike,
Op::NotILike,
Op::Between,
Op::IsNull,
Op::IsDistinctFrom,
Op::IsNotDistinctFrom,
Op::JsonContains,
Op::JsonContainedBy,
Op::JsonHasKey,
Op::JsonHasAnyKey,
Op::JsonHasAllKeys,
] {
assert!(MySql.supports_op(op), "expected {op:?} to be supported");
}
}
#[test]
fn session_lock_uses_get_lock() {
let acq = MySql.acquire_session_lock_sql().unwrap();
assert!(acq.contains("GET_LOCK"));
assert!(acq.contains("?"));
assert!(acq.contains("DATABASE()"), "{acq}");
let rel = MySql.release_session_lock_sql().unwrap();
assert!(rel.contains("RELEASE_LOCK"));
assert!(rel.contains("DATABASE()"), "{rel}");
}
#[test]
fn xact_lock_is_none() {
assert!(MySql.acquire_xact_lock_sql().is_none());
}
#[test]
fn conflict_do_nothing_assigns_the_pk_and_marks_the_skip() {
let model = with_pk(
empty_model_with("t", &[("code", FieldType::String), ("n", FieldType::I64)]),
"code",
false,
);
let mut sql = String::new();
MySql
.write_conflict_clause(&mut sql, model, &ConflictClause::DoNothing)
.unwrap();
assert_eq!(
sql,
" ON DUPLICATE KEY UPDATE `code` = IF(LAST_INSERT_ID(18446744073709551615), `code`, `code`)"
);
}
#[test]
fn conflict_do_update_on_auto_pk_reports_the_updated_id() {
let model = with_pk(
empty_model_with("t", &[("id", FieldType::I64), ("a", FieldType::String)]),
"id",
true,
);
let mut sql = String::new();
MySql
.write_conflict_clause(
&mut sql,
model,
&ConflictClause::DoUpdate {
target: vec![],
update_columns: vec!["a"],
},
)
.unwrap();
assert_eq!(
sql,
" ON DUPLICATE KEY UPDATE `a` = IF(LAST_INSERT_ID(`id`), VALUES(`a`), VALUES(`a`))"
);
}
#[test]
fn conflict_do_nothing_on_auto_pk_assigns_another_column() {
let model = with_pk(
empty_model_with("t", &[("id", FieldType::I64), ("code", FieldType::String)]),
"id",
true,
);
let mut sql = String::new();
MySql
.write_conflict_clause(&mut sql, model, &ConflictClause::DoNothing)
.unwrap();
assert_eq!(
sql,
" ON DUPLICATE KEY UPDATE `code` = IF(LAST_INSERT_ID(18446744073709551615), `code`, `code`)"
);
}
#[test]
fn conflict_do_update_with_empty_target_translates() {
let model = empty_model_with("t", &[("a", FieldType::String), ("b", FieldType::String)]);
let mut sql = String::new();
MySql
.write_conflict_clause(
&mut sql,
model,
&ConflictClause::DoUpdate {
target: vec![],
update_columns: vec!["a", "b"],
},
)
.unwrap();
assert_eq!(
sql,
" ON DUPLICATE KEY UPDATE `a` = VALUES(`a`), `b` = VALUES(`b`)"
);
}
#[test]
fn conflict_do_update_with_target_silently_ignores_target() {
let model = empty_model_with("t", &[("id", FieldType::I64), ("a", FieldType::String)]);
let mut sql = String::new();
MySql
.write_conflict_clause(
&mut sql,
model,
&ConflictClause::DoUpdate {
target: vec!["id"],
update_columns: vec!["a"],
},
)
.expect("MySQL should accept target and silently ignore it");
assert_eq!(sql, " ON DUPLICATE KEY UPDATE `a` = VALUES(`a`)");
}
#[test]
fn ilike_translates_to_lower_like_lower() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("users", &[("name", FieldType::String)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "name",
op: Op::ILike,
value: SqlValue::String("%Alice%".into()),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert_eq!(
stmt.sql,
"SELECT `name` FROM `users` WHERE LOWER(`name`) LIKE LOWER(?)"
);
assert_eq!(stmt.params.len(), 1);
}
#[test]
fn not_ilike_translates_to_not_like() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("users", &[("name", FieldType::String)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "name",
op: Op::NotILike,
value: SqlValue::String("%bot%".into()),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt.sql.contains("LOWER(`name`) NOT LIKE LOWER(?)"));
}
#[test]
fn is_distinct_from_translates_to_not_null_safe_eq() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("users", &[("email", FieldType::String)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "email",
op: Op::IsDistinctFrom,
value: SqlValue::String("a@b".into()),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt.sql.contains("NOT (`email` <=> ?)"));
}
#[test]
fn is_not_distinct_from_translates_to_null_safe_eq() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("users", &[("email", FieldType::String)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "email",
op: Op::IsNotDistinctFrom,
value: SqlValue::Null,
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt.sql.contains("`email` <=> ?"));
assert!(!stmt.sql.contains("NOT"));
}
#[test]
fn json_contains_translates_to_json_contains_function() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("posts", &[("meta", FieldType::Json)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "meta",
op: Op::JsonContains,
value: SqlValue::Json(serde_json::json!({"k": "v"})),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt.sql.contains("JSON_CONTAINS(`meta`, ?)"));
assert!(!stmt.sql.contains("@>"));
}
#[test]
fn json_contained_by_swaps_argument_order() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("posts", &[("meta", FieldType::Json)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "meta",
op: Op::JsonContainedBy,
value: SqlValue::Json(serde_json::json!({"k": "v"})),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt.sql.contains("JSON_CONTAINS(?, `meta`)"));
}
#[test]
fn json_has_key_translates_to_contains_path_with_concat() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("posts", &[("meta", FieldType::Json)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "meta",
op: Op::JsonHasKey,
value: SqlValue::String("title".into()),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt
.sql
.contains("JSON_CONTAINS_PATH(`meta`, 'one', CONCAT('$.', ?))"));
}
#[test]
fn json_has_any_keys_translates_to_contains_path_one() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("posts", &[("meta", FieldType::Json)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "meta",
op: Op::JsonHasAnyKey,
value: SqlValue::List(vec![
SqlValue::String("title".into()),
SqlValue::String("body".into()),
]),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt
.sql
.contains("JSON_CONTAINS_PATH(`meta`, 'one', CONCAT('$.', ?), CONCAT('$.', ?))"));
assert_eq!(stmt.params.len(), 2);
}
#[test]
fn json_has_all_keys_uses_all_mode() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with("posts", &[("meta", FieldType::Json)]);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "meta",
op: Op::JsonHasAllKeys,
value: SqlValue::List(vec![
SqlValue::String("a".into()),
SqlValue::String("b".into()),
]),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert!(stmt
.sql
.contains("JSON_CONTAINS_PATH(`meta`, 'all', CONCAT('$.', ?), CONCAT('$.', ?))"));
}
#[test]
fn bulk_update_translates_to_inner_join_values_row() {
use crate::core::{BulkUpdateQuery, SqlValue};
let model = empty_model_with(
"users",
&[("id", FieldType::I64), ("name", FieldType::String)],
);
let pk_model = with_pk(model, "id", false);
let q = BulkUpdateQuery {
model: pk_model,
update_columns: vec!["name"],
rows: vec![
vec![SqlValue::I64(1), SqlValue::String("Alice".into())],
vec![SqlValue::I64(2), SqlValue::String("Bob".into())],
],
};
let stmt = MySql.compile_bulk_update(&q).unwrap();
assert!(stmt.sql.starts_with("UPDATE `users` INNER JOIN (VALUES "));
assert!(stmt.sql.contains("ROW(?, ?), ROW(?, ?)"));
assert!(stmt.sql.contains(") AS __data(`id`, `name`)"));
assert!(stmt.sql.contains("ON `users`.`id` = __data.`id`"));
assert!(stmt.sql.contains("SET `users`.`name` = __data.`name`"));
assert_eq!(stmt.params.len(), 4);
}
fn with_pk(
model: &'static crate::core::ModelSchema,
pk_col: &'static str,
auto: bool,
) -> &'static crate::core::ModelSchema {
let new_fields: Vec<crate::core::FieldSchema> = model
.fields
.iter()
.map(|f| {
let mut f = *f;
if f.column == pk_col {
f.primary_key = true;
f.auto = auto;
}
f
})
.collect();
let leaked: &'static [crate::core::FieldSchema] = Box::leak(new_fields.into_boxed_slice());
Box::leak(Box::new(crate::core::ModelSchema {
fields: leaked,
..*model
}))
}
#[test]
fn select_emits_backticks_and_question_marks() {
use crate::core::{Filter, Op, SelectQuery, SqlValue, WhereExpr};
let model = empty_model_with(
"users",
&[("id", FieldType::I64), ("name", FieldType::String)],
);
let q = SelectQuery {
model,
joins: vec![],
subquery_joins: vec![],
where_clause: WhereExpr::Predicate(Filter {
column: "name",
op: Op::Eq,
value: SqlValue::String("alice".into()),
}),
search: None,
order_by: vec![],
limit: None,
offset: None,
lock_mode: None,
compound: vec![],
projection: None,
distinct: None,
compound_order_by: vec![],
compound_limit: None,
compound_offset: None,
select_related: vec![],
};
let stmt = MySql.compile_select(&q).unwrap();
assert_eq!(
stmt.sql,
"SELECT `id`, `name` FROM `users` WHERE `name` = ?"
);
assert_eq!(stmt.params.len(), 1);
}
#[test]
fn insert_with_returning_errors() {
use crate::core::{InsertQuery, SqlValue};
let model = empty_model_with(
"users",
&[("id", FieldType::I64), ("name", FieldType::String)],
);
let q = InsertQuery {
model,
columns: vec!["name"],
values: vec![SqlValue::String("alice".into())],
returning: vec!["id"],
on_conflict: None,
};
let err = MySql.compile_insert(&q).unwrap_err();
assert!(matches!(
err,
SqlError::OperatorNotSupportedInDialect {
op: "RETURNING",
dialect: "mysql"
}
));
}
fn empty_model_with(
table: &'static str,
fields: &[(&'static str, FieldType)],
) -> &'static crate::core::ModelSchema {
let field_vec: Vec<crate::core::FieldSchema> = fields
.iter()
.map(|(col, ty)| crate::core::FieldSchema {
name: col,
column: col,
ty: *ty,
nullable: false,
primary_key: false,
relation: None,
max_length: None,
min: None,
max: None,
default: None,
auto: false,
auto_now: false,
unique: false,
generated_as: None,
help_text: None,
choices: None,
db_comment: None,
verbose_name: None,
editable: true,
blank: false,
case_insensitive: false,
fk_on_delete: None,
validators: &[],
})
.collect();
let leaked: &'static [crate::core::FieldSchema] = Box::leak(field_vec.into_boxed_slice());
Box::leak(Box::new(crate::core::ModelSchema {
name: table,
table,
fields: leaked,
display: None,
app_label: None,
admin: None,
soft_delete_column: None,
permissions: false,
audit_track: None,
m2m: &[],
indexes: &[],
check_constraints: &[],
exclusion_constraints: &[],
default_permissions: &[],
composite_relations: &[],
generic_relations: &[],
scope: crate::core::ModelScope::Tenant,
default_order: &[],
is_view: false,
verbose_name: None,
verbose_name_plural: None,
managed: true,
db_table_comment: None,
default_related_name: None,
base_manager_name: None,
required_db_vendor: None,
required_db_features: &[],
order_with_respect_to: None,
proxy: false,
get_latest_by: None,
extra_permissions: &[],
global_scopes: &[],
}))
}
}