Skip to main content

dsfb_database/live_mysql/
queries.rs

1//! Query allow-list for the live MySQL adapter.
2//!
3//! This file is the MySQL analogue of `src/live/queries.rs`. Every
4//! SQL statement the live-mysql adapter can ever issue is enumerated
5//! by [`AllowedMySqlQuery`] and maps to a `'static` SQL string via
6//! [`AllowedMySqlQuery::sql`]. The concatenated SQL texts are
7//! SHA-256-pinned by `tests/live_query_allowlist_lock_mysql.rs`: any
8//! edit — even an added comment — forces an intentional lock bump
9//! that must be co-authored with the paper's §Live-Eval MySQL
10//! subsection and the `spec/permissions.mysql.sql` manifest.
11//!
12//! All four variants are pure `SELECT` against `performance_schema`
13//! and `information_schema`. None touch user tables. None issue DDL,
14//! DML, or advisory locks. This is the statement-level layer of the
15//! code-audit contract documented in [`crate::live_mysql`].
16//!
17//! The residual-class mapping mirrors the PostgreSQL path:
18//!   * DigestSnapshot  -> PlanRegression / WorkloadPhase
19//!   * ThreadsSnapshot -> Contention (wait-event samples)
20//!   * MetadataLocksSnapshot -> Contention (per-object lock waits)
21//!   * BufferPoolSnapshot    -> CacheIo
22
23/// Closed enumeration of every SQL statement the live-mysql adapter
24/// will ever execute. Adding a variant is a reviewable change that
25/// simultaneously breaks [`Self::sql_concat_for_lock`] and therefore
26/// the allow-list lock test.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum AllowedMySqlQuery {
29    /// Per-digest cumulative latency / call / row counters.
30    /// Source: `performance_schema.events_statements_summary_by_digest`
31    /// (MySQL 5.6+; column names stable through 8.4).
32    /// Residual classes emitted: PlanRegression, WorkloadPhase.
33    DigestSnapshot,
34    /// Per-thread wait-event sample.
35    /// Source: `performance_schema.threads` (MySQL 5.6+).
36    /// Residual class emitted: Contention.
37    ThreadsSnapshot,
38    /// Per-object metadata-lock wait snapshot.
39    /// Source: `performance_schema.metadata_locks` (MySQL 5.7+).
40    /// Residual class emitted: Contention (no PostgreSQL analog).
41    MetadataLocksSnapshot,
42    /// Per-pool InnoDB buffer-pool cumulative counters.
43    /// Source: `information_schema.innodb_buffer_pool_stats`
44    /// (MySQL 5.6+).
45    /// Residual class emitted: CacheIo.
46    BufferPoolSnapshot,
47}
48
49impl AllowedMySqlQuery {
50    /// Every variant the adapter knows about. Used by the lock test
51    /// and by the scraper to enumerate a full poll cycle in a
52    /// deterministic order.
53    pub const ALL: [AllowedMySqlQuery; 4] = [
54        Self::DigestSnapshot,
55        Self::ThreadsSnapshot,
56        Self::MetadataLocksSnapshot,
57        Self::BufferPoolSnapshot,
58    ];
59
60    /// Pinned SQL text for this variant. Every string is a `SELECT`
61    /// against `performance_schema` or `information_schema`. Any edit
62    /// invalidates the lock test and forces a paired paper update.
63    pub fn sql(&self) -> &'static str {
64        match self {
65            Self::DigestSnapshot => {
66                // MD5 of the digest canonicalises the identifier so
67                // the emitted residual stream contains no raw digest
68                // text. Analogous to the md5(queryid::text) choice in
69                // the PostgreSQL path.
70                "SELECT \
71                   UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
72                   MD5(DIGEST) AS digest_id, \
73                   COUNT_STAR AS calls, \
74                   SUM_TIMER_WAIT / 1000000000.0 AS total_exec_time_ms \
75                 FROM performance_schema.events_statements_summary_by_digest \
76                 WHERE DIGEST IS NOT NULL"
77            }
78            Self::ThreadsSnapshot => {
79                // No query-text columns, no client-host columns.
80                // Counts wait-event samples per category in the
81                // distiller, so the PII surface collapses to "how
82                // many sessions are waiting on which wait-event
83                // class".
84                "SELECT \
85                   UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
86                   COALESCE(PROCESSLIST_STATE, 'None') AS wait_event_type, \
87                   COALESCE(PROCESSLIST_COMMAND, 'None') AS wait_event, \
88                   COUNT(*) AS n_threads \
89                 FROM performance_schema.threads \
90                 WHERE PROCESSLIST_ID IS NOT NULL \
91                 GROUP BY PROCESSLIST_STATE, PROCESSLIST_COMMAND"
92            }
93            Self::MetadataLocksSnapshot => {
94                // Aggregates per-status counts to avoid emitting raw
95                // object identifiers. A pure read: performance_schema
96                // exposes lock metadata via SELECT, not a lock-taking
97                // call.
98                "SELECT \
99                   UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
100                   COALESCE(LOCK_STATUS, 'None') AS lock_status, \
101                   COALESCE(LOCK_TYPE, 'None') AS lock_type, \
102                   COUNT(*) AS n_waiters \
103                 FROM performance_schema.metadata_locks \
104                 GROUP BY LOCK_STATUS, LOCK_TYPE"
105            }
106            Self::BufferPoolSnapshot => {
107                // Per-pool cache counters. InnoDB exposes them via
108                // information_schema; the adapter consumes the delta
109                // between adjacent snapshots in the distiller.
110                "SELECT \
111                   UNIX_TIMESTAMP(NOW(6)) AS snapshot_t, \
112                   POOL_ID AS pool_id, \
113                   PAGES_DATA AS pages_data, \
114                   PAGES_MISC AS pages_misc, \
115                   PAGES_FREE AS pages_free, \
116                   PAGES_MADE_YOUNG AS pages_made_young, \
117                   PAGES_READ AS pages_read, \
118                   PAGES_CREATED AS pages_created, \
119                   PAGES_WRITTEN AS pages_written \
120                 FROM information_schema.innodb_buffer_pool_stats"
121            }
122        }
123    }
124
125    /// Concatenation of every variant's SQL text, in `ALL`-order,
126    /// separated by a single newline. The SHA-256 of this string is
127    /// pinned by `tests/live_query_allowlist_lock_mysql.rs`.
128    pub fn sql_concat_for_lock() -> String {
129        let mut s = String::new();
130        for (i, q) in Self::ALL.iter().enumerate() {
131            if i > 0 {
132                s.push('\n');
133            }
134            s.push_str(q.sql());
135        }
136        s
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn all_variants_enumerated_once() {
146        let n = AllowedMySqlQuery::ALL.len();
147        let unique: std::collections::HashSet<_> =
148            AllowedMySqlQuery::ALL.iter().copied().collect();
149        assert_eq!(n, unique.len(), "duplicate variant in AllowedMySqlQuery::ALL");
150    }
151
152    #[test]
153    fn every_variant_is_pure_select() {
154        for q in AllowedMySqlQuery::ALL {
155            let sql = q.sql();
156            let head = sql.trim_start().to_uppercase();
157            assert!(
158                head.starts_with("SELECT"),
159                "AllowedMySqlQuery::{:?} does not start with SELECT: {}",
160                q,
161                sql
162            );
163            let tokens: Vec<&str> = head
164                .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
165                .filter(|t| !t.is_empty())
166                .collect();
167            for kw in &[
168                "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
169                "GRANT", "REVOKE", "TRUNCATE", "LOCK", "UNLOCK", "CALL",
170                "LOAD", "HANDLER",
171            ] {
172                assert!(
173                    !tokens.iter().any(|t| t == kw),
174                    "AllowedMySqlQuery::{:?} contains forbidden keyword {}: {}",
175                    q,
176                    kw,
177                    sql
178                );
179            }
180        }
181    }
182
183    #[test]
184    fn sql_concat_is_deterministic() {
185        let a = AllowedMySqlQuery::sql_concat_for_lock();
186        let b = AllowedMySqlQuery::sql_concat_for_lock();
187        assert_eq!(a, b);
188    }
189}