#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AllowedMySqlQuery {
DigestSnapshot,
ThreadsSnapshot,
MetadataLocksSnapshot,
BufferPoolSnapshot,
}
impl AllowedMySqlQuery {
pub const ALL: [AllowedMySqlQuery; 4] = [
Self::DigestSnapshot,
Self::ThreadsSnapshot,
Self::MetadataLocksSnapshot,
Self::BufferPoolSnapshot,
];
pub fn sql(&self) -> &'static str {
match self {
Self::DigestSnapshot => {
"SELECT \
UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
MD5(DIGEST) AS digest_id, \
COUNT_STAR AS calls, \
SUM_TIMER_WAIT / 1000000000.0 AS total_exec_time_ms \
FROM performance_schema.events_statements_summary_by_digest \
WHERE DIGEST IS NOT NULL"
}
Self::ThreadsSnapshot => {
"SELECT \
UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
COALESCE(PROCESSLIST_STATE, 'None') AS wait_event_type, \
COALESCE(PROCESSLIST_COMMAND, 'None') AS wait_event, \
COUNT(*) AS n_threads \
FROM performance_schema.threads \
WHERE PROCESSLIST_ID IS NOT NULL \
GROUP BY PROCESSLIST_STATE, PROCESSLIST_COMMAND"
}
Self::MetadataLocksSnapshot => {
"SELECT \
UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
COALESCE(LOCK_STATUS, 'None') AS lock_status, \
COALESCE(LOCK_TYPE, 'None') AS lock_type, \
COUNT(*) AS n_waiters \
FROM performance_schema.metadata_locks \
GROUP BY LOCK_STATUS, LOCK_TYPE"
}
Self::BufferPoolSnapshot => {
"SELECT \
UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
POOL_ID AS pool_id, \
PAGES_DATA AS pages_data, \
PAGES_MISC AS pages_misc, \
PAGES_FREE AS pages_free, \
PAGES_MADE_YOUNG AS pages_made_young, \
PAGES_READ AS pages_read, \
PAGES_CREATED AS pages_created, \
PAGES_WRITTEN AS pages_written \
FROM information_schema.innodb_buffer_pool_stats"
}
}
}
pub fn sql_concat_for_lock() -> String {
let mut s = String::new();
for (i, q) in Self::ALL.iter().enumerate() {
if i > 0 {
s.push('\n');
}
s.push_str(q.sql());
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_variants_enumerated_once() {
let n = AllowedMySqlQuery::ALL.len();
let unique: std::collections::HashSet<_> =
AllowedMySqlQuery::ALL.iter().copied().collect();
assert_eq!(n, unique.len(), "duplicate variant in AllowedMySqlQuery::ALL");
}
#[test]
fn every_variant_is_pure_select() {
for q in AllowedMySqlQuery::ALL {
let sql = q.sql();
let head = sql.trim_start().to_uppercase();
assert!(
head.starts_with("SELECT"),
"AllowedMySqlQuery::{:?} does not start with SELECT: {}",
q,
sql
);
let tokens: Vec<&str> = head
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.filter(|t| !t.is_empty())
.collect();
for kw in &[
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
"GRANT", "REVOKE", "TRUNCATE", "LOCK", "UNLOCK", "CALL",
"LOAD", "HANDLER",
] {
assert!(
!tokens.iter().any(|t| t == kw),
"AllowedMySqlQuery::{:?} contains forbidden keyword {}: {}",
q,
kw,
sql
);
}
}
}
#[test]
fn sql_concat_is_deterministic() {
let a = AllowedMySqlQuery::sql_concat_for_lock();
let b = AllowedMySqlQuery::sql_concat_for_lock();
assert_eq!(a, b);
}
}