Skip to main content

sz_orm_explain/
lib.rs

1//! # sz-orm-explain — 跨方言 EXPLAIN 结果解析器
2//!
3//! 将 MySQL / PostgreSQL / SQLite / Oracle / MSSQL 的 `EXPLAIN` 输出解析为
4//! 统一的 [`ExplainPlan`],用于:
5//!
6//! - 全表扫描检测(`ScanType::FullTable`)
7//! - 缺失索引检测(`index == None` 且扫描行数超阈值)
8//! - 执行计划回归检测([`crate::regression`],需 `explain-analyzer` feature)
9//!
10//! 典型用法(配合 `sz-orm-macros` 的 `query!` 宏 db-verify 模式):
11//!
12//! ```rust,ignore
13//! let plan = sz_orm_explain::parser_for(ExplainDialect::Postgres)?.parse(explain_raw)?;
14//! if plan.scan_type == ScanType::FullTable {
15//!     // 编译期/CI 警告:全表扫描
16//! }
17//! ```
18//!
19//! 解析失败返回 [`ExplainError::Unparseable`],不 panic,可降级为"无警告"。
20//!
21//! 注意:本包刻意零依赖 `sz-orm-core`,通过自带的 [`ExplainDialect`] 枚举
22//! 描述方言,避免 core → macros → explain → core 依赖循环。
23
24pub mod dialect;
25pub mod parsers;
26#[cfg(feature = "perf-baseline")]
27pub mod perf_baseline;
28#[cfg(feature = "explain-analyzer")]
29pub mod regression;
30
31/// 支持的 EXPLAIN 解析方言(与数据库引擎对应,家族内格式兼容)
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ExplainDialect {
34    /// MySQL(及 MariaDB/TiDB/PolarDB/OceanBase)
35    MySql,
36    /// PostgreSQL(及 CockroachDB/YugabyteDB)
37    Postgres,
38    /// SQLite(及 DuckDB)
39    Sqlite,
40    /// Oracle(及 Dameng)
41    Oracle,
42    /// SQL Server(及 Sybase)
43    Mssql,
44}
45
46/// 扫描类型分类(各方言扫描方式的统一抽象)
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[cfg_attr(
49    feature = "explain-analyzer",
50    derive(serde::Serialize, serde::Deserialize)
51)]
52pub enum ScanType {
53    /// 全表扫描(MySQL `ALL` / PG `Seq Scan` / SQLite `SCAN` / Oracle `TABLE ACCESS FULL` / MSSQL `Table Scan`)
54    FullTable,
55    /// 索引范围扫描(MySQL `range` / PG `Index Scan` / SQLite `SEARCH ... USING INDEX` / Oracle `INDEX RANGE SCAN` / MSSQL `Index Scan`)
56    IndexRange,
57    /// 索引等值/点查(MySQL `ref`/`eq_ref` / Oracle `INDEX UNIQUE SCAN`)
58    IndexLookup,
59    /// 主键/唯一索引点查(MySQL `const` / PG `Index Scan ... pkey` / MSSQL `Index Seek`)
60    UniqueLookup,
61    /// 其他(物化、排序、聚合等)
62    Other,
63}
64
65impl ScanType {
66    /// 是否为全表扫描(性能警告触发条件)
67    pub fn is_full_table_scan(&self) -> bool {
68        matches!(self, ScanType::FullTable)
69    }
70}
71
72/// 解析后的执行计划摘要
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[cfg_attr(
75    feature = "explain-analyzer",
76    derive(serde::Serialize, serde::Deserialize)
77)]
78pub struct ExplainPlan {
79    /// 扫描类型
80    pub scan_type: ScanType,
81    /// 涉及的表名(未知时为 `"<unknown>"`)
82    pub table: String,
83    /// 使用的索引名(全表扫描/未知时为 `None`)
84    pub index: Option<String>,
85    /// 预估扫描行数
86    pub rows: u64,
87    /// 额外信息(filter/using where 等)
88    pub extra: Vec<String>,
89}
90
91impl ExplainPlan {
92    /// 缺失索引判断:全表扫描或行数超阈值且未使用索引
93    pub fn missing_index(&self, row_threshold: u64) -> bool {
94        self.index.is_none() && (self.scan_type.is_full_table_scan() || self.rows >= row_threshold)
95    }
96}
97
98/// EXPLAIN 解析错误
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum ExplainError {
101    /// 无法解析(非 EXPLAIN 输出 / 格式不识别)
102    Unparseable { reason: String },
103    /// 方言不受支持(无对应解析器)
104    UnsupportedDialect,
105}
106
107impl std::fmt::Display for ExplainError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            ExplainError::Unparseable { reason } => {
111                write!(f, "unparseable explain output: {reason}")
112            }
113            ExplainError::UnsupportedDialect => {
114                write!(f, "unsupported dialect for explain parsing")
115            }
116        }
117    }
118}
119
120impl std::error::Error for ExplainError {}
121
122/// 方言 EXPLAIN 解析器 trait
123pub trait ExplainParser: Send + Sync + std::fmt::Debug {
124    /// 解析原始 EXPLAIN 输出为统一 [`ExplainPlan`]
125    fn parse(&self, raw: &str) -> Result<ExplainPlan, ExplainError>;
126}
127
128/// 按方言分派解析器
129pub fn parser_for(dialect: ExplainDialect) -> Result<Box<dyn ExplainParser>, ExplainError> {
130    match dialect {
131        ExplainDialect::MySql => Ok(Box::new(parsers::mysql::MySqlParser)),
132        ExplainDialect::Postgres => Ok(Box::new(parsers::postgres::PostgresParser)),
133        ExplainDialect::Sqlite => Ok(Box::new(parsers::sqlite::SqliteParser)),
134        ExplainDialect::Oracle => Ok(Box::new(parsers::oracle::OracleParser)),
135        ExplainDialect::Mssql => Ok(Box::new(parsers::mssql::MssqlParser)),
136    }
137}