use std::num::NonZeroUsize;
use crate::advisory::Advisory;
use crate::dialect::Dialect;
use crate::form::{Form, KeyPart};
use crate::insert::{Placeholder, Spelling, quote_into};
use crate::lease::Lease;
use crate::opening::{Isolation, Opening, Opens, level};
use crate::outbox::OutboxDialect;
use crate::row_lock::RowLock;
use crate::spec::TableSpec;
use crate::statement::{ClaimShape, NameLimit, Param, Statement, StatementError};
use crate::table_name::TableName;
use crate::writer::{BuiltIn, OutboxWriter, Probe, SqlWriter};
const DATABASE_NOW: &str = "UTC_TIMESTAMP(6)";
const LOCK: &str = " FOR UPDATE SKIP LOCKED";
const BEGIN_CLAIM: &str = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION";
const BEGIN_READ_UNCOMMITTED: &str =
"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION";
const BEGIN_REPEATABLE_READ: &str =
"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION";
const BEGIN_SERIALIZABLE: &str = "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION";
const TRY_LOCK: &str = "SELECT CAST(COALESCE(GET_LOCK(?, 0), 0) AS SIGNED)";
const UNLOCK: &str = "SELECT CAST(COALESCE(RELEASE_LOCK(?), 0) AS SIGNED)";
#[derive(Debug, Clone, Copy)]
struct Floor {
release: (u32, u32, u32),
name: &'static str,
}
const MYSQL_FLOOR: Floor = Floor {
release: (8, 0, 1),
name: "MySQL 8.0.1",
};
const MARIADB_FLOOR: Floor = Floor {
release: (10, 6, 0),
name: "MariaDB 10.6",
};
pub(crate) const SPELLING: Spelling = Spelling {
dialect: "mysql",
quote: b'`',
placeholder: Placeholder::Question,
default_row: " () VALUES ()",
name_limit: Some(NameLimit::Characters(64)),
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct MySql;
impl BuiltIn for MySql {
const SPELLING: Spelling = SPELLING;
const ROW_LOCKS: bool = true;
const BACKSLASH_ESCAPES: bool = true;
const UPDATE_RETURNS: bool = false;
const PROBE: Probe = Probe::Check("IS_USED_LOCK(", ") IS NULL");
fn database_now(&self) -> &'static str {
DATABASE_NOW
}
fn database_later(&self, sql: &mut SqlWriter<'_, Self>) {
sql.push(DATABASE_NOW)
.push(" + INTERVAL ")
.param(Param::Delay)
.push(" MICROSECOND");
}
fn render_lock_key(
&self,
sql: &mut SqlWriter<'_, Self>,
schema: Option<&str>,
key: &[KeyPart<'_>],
) {
let text = |sql: &mut SqlWriter<'_, Self>| {
sql.push("CONCAT_WS('', LOWER(");
match schema {
Some(schema) => sql.literal(schema),
None => sql.push("DATABASE()"),
};
sql.push("), '.', ")
.key_parts(key, ", ", |sql, column| {
sql.ident(column);
})
.push(")");
};
sql.push("IF(CHAR_LENGTH(");
text(sql);
sql.push(") BETWEEN 1 AND 64, ");
text(sql);
sql.push(", SHA2(");
text(sql);
sql.push(", 256))");
}
}
impl Dialect for MySql {
fn name(&self) -> &'static str {
"mysql"
}
fn quote_into(&self, ident: &str, out: &mut String) {
quote_into(SPELLING.quote, ident, out);
}
fn placeholder_into(&self, _: NonZeroUsize, out: &mut String) {
out.push('?');
}
fn fetch(&self, _: &TableSpec<'_>) -> Result<Statement, StatementError> {
Err(StatementError::UnsupportedFetch {
dialect: self.name(),
})
}
fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.finish_statement(spec)
}
fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> {
self.retry_statement(spec)
}
fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.retry_after_statement(spec)
}
fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.finish_statement(spec)
}
fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.dead_letter_group_statement(spec)
}
fn dead_letter_table(
&self,
spec: &TableSpec<'_>,
target: TableName<'_>,
) -> Result<Vec<Statement>, StatementError> {
self.copy_then_delete(spec, target)
}
fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.insert_statement(spec)
}
fn server_version(&self) -> Option<&'static str> {
Some("SELECT VERSION()")
}
fn check_server(&self, _: &TableSpec<'_>, version: &str) -> Result<(), StatementError> {
let floor = if is_mariadb(version) {
MARIADB_FLOOR
} else {
MYSQL_FLOOR
};
if release(version).is_some_and(|release| release >= floor.release) {
return Ok(());
}
Err(StatementError::ServerTooOld {
dialect: self.name(),
server: version.to_owned(),
required: floor.name,
})
}
fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
match opening {
Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => {
Ok(Some(BEGIN_CLAIM))
}
Opening::Isolation(Isolation::ReadUncommitted) => Ok(Some(BEGIN_READ_UNCOMMITTED)),
Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(BEGIN_REPEATABLE_READ)),
Opening::Isolation(Isolation::Serializable) => Ok(Some(BEGIN_SERIALIZABLE)),
Opening::Mode(_) => Err(opening.refused(self.name())),
}
}
fn fifo_guard(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> {
if !self.guards_group(spec)? {
return Ok(None);
}
if matches!(spec.form(), Form::RowLock)
&& spec.opening() == Opening::Isolation(Isolation::Serializable)
{
return Err(StatementError::FifoAtSerializable {
dialect: self.name(),
});
}
let mut sql = SqlWriter::new(self);
sql.push("SELECT CAST((")
.group_count(spec, "")
.push(") = (")
.group_count(spec, LOCK)
.push(") AS SIGNED)");
Ok(Some(sql.finish()))
}
}
impl OutboxDialect for MySql {
fn outbox_fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.outbox_fetch_statement(spec)
}
fn outbox_mark(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.outbox_mark_statement(spec)
}
fn outbox_recover(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.outbox_recover_statement(spec)
}
}
impl Opens<level::ReadUncommitted> for MySql {}
impl Opens<level::ReadCommitted> for MySql {}
impl Opens<level::RepeatableRead> for MySql {}
impl Opens<level::Serializable> for MySql {}
impl RowLock for MySql {
fn lock_claim(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Statement, StatementError> {
self.locked(spec, "lock_claim")?;
let mut sql = SqlWriter::new(self);
sql.claim(spec, shape, LOCK);
Ok(sql.finish())
}
}
impl Advisory for MySql {
fn advisory_claim(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.advisory_claim_statement(spec)
}
fn lock(&self) -> Option<Statement> {
Some(Statement::new(TRY_LOCK, [Param::Key]))
}
fn unlock(&self) -> Option<Statement> {
Some(Statement::new(UNLOCK, [Param::Key]))
}
fn take(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Vec<Statement>, StatementError> {
self.take_statements(spec, shape)
}
}
impl Lease for MySql {
fn lease_claim(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Statement, StatementError> {
self.leased(spec, "lease_claim")?;
let mut sql = SqlWriter::new(self);
sql.claim(spec, shape, LOCK);
Ok(sql.finish())
}
fn extend(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.extend_statement(spec)
}
fn stamp(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
self.stamp_statement(spec)
}
fn claim_writes_lease(&self) -> bool {
false
}
fn begin_lease_claim(&self) -> Option<&'static str> {
Some(BEGIN_CLAIM)
}
}
fn is_mariadb(version: &str) -> bool {
const MARIADB: &[u8] = b"mariadb";
version
.as_bytes()
.windows(MARIADB.len())
.any(|window| window.eq_ignore_ascii_case(MARIADB))
}
fn release(version: &str) -> Option<(u32, u32, u32)> {
let digits = version
.split(|character: char| !character.is_ascii_digit() && character != '.')
.next()
.unwrap_or_default();
let mut numbers = digits.split('.').map(|number| number.parse().ok());
let mut next = || numbers.next().flatten();
Some((next()?, next()?, next()?))
}