Skip to main content

sz_orm_explain/parsers/
mssql.rs

1//! MSSQL SHOWPLAN 解析器
2//!
3//! 输入示例(`SET SHOWPLAN_TEXT ON` 后执行查询的输出):
4//!
5//! ```text
6//! |--Table Scan(OBJECT:([db].[dbo].[users]), WHERE:([age]>(18)))
7//! |--Index Seek(OBJECT:([db].[dbo].[users].[PK__users__3214EC07]), SEEK:([id]=@1) ORDERED FORWARD)
8//! |--Index Scan(OBJECT:([db].[dbo].[orders].[idx_orders_user]))
9//! |--Hash Match(Inner Join, HASH:([o].[user_id])=([u].[id]), RESIDUAL:...)
10//! ```
11//!
12//! 注意:SHOWPLAN_TEXT 不输出预估行数,`rows` 固定为 0。
13
14use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
15
16/// MSSQL EXPLAIN 解析器
17#[derive(Debug)]
18pub struct MssqlParser;
19
20impl ExplainParser for MssqlParser {
21    fn parse(&self, raw: &str) -> Result<ExplainPlan, ExplainError> {
22        let mut best: Option<(ScanType, String, Option<String>)> = None;
23        // 优先级:Table Scan > Index Scan > Index Seek > 其他
24        let rank = |s: ScanType| match s {
25            ScanType::FullTable => 4,
26            ScanType::IndexRange => 3,
27            ScanType::UniqueLookup => 2,
28            _ => 1,
29        };
30
31        for line in raw.lines() {
32            let line = line.trim();
33            if line.is_empty() || line.starts_with("StmtText") || line.starts_with("----") {
34                continue;
35            }
36            // 去掉树形前缀 `|--`
37            let body = line.strip_prefix("|--").unwrap_or(line).trim();
38            let upper = body.to_ascii_uppercase();
39
40            let candidate: Option<(ScanType, String, Option<String>)> =
41                if upper.starts_with("TABLE SCAN(") || upper.starts_with("CLUSTERED INDEX SCAN(") {
42                    Some((ScanType::FullTable, extract_object(body), None))
43                } else if upper.starts_with("INDEX SCAN(") {
44                    Some((
45                        ScanType::IndexRange,
46                        "<unknown>".into(),
47                        Some(extract_object(body)),
48                    ))
49                } else if upper.starts_with("INDEX SEEK(")
50                    || upper.starts_with("CLUSTERED INDEX SEEK(")
51                {
52                    Some((
53                        ScanType::UniqueLookup,
54                        "<unknown>".into(),
55                        Some(extract_object(body)),
56                    ))
57                } else {
58                    None
59                };
60            if let Some(c) = candidate {
61                if best.is_none() || rank(c.0) > rank(best.as_ref().unwrap().0) {
62                    best = Some(c);
63                }
64            }
65        }
66        let (scan_type, table, index) = best.ok_or_else(|| ExplainError::Unparseable {
67            reason: "no recognizable plan operator".into(),
68        })?;
69        Ok(ExplainPlan {
70            scan_type,
71            table,
72            index,
73            rows: 0,
74            extra: Vec::new(),
75        })
76    }
77}
78
79/// 提取 `OBJECT:([db].[dbo].[users])` 中的对象名(取最后一段)
80fn extract_object(body: &str) -> String {
81    let upper = body.to_ascii_uppercase();
82    if let Some(pos) = upper.find("OBJECT:(") {
83        let rest = &body[pos + 8..];
84        let inner = rest.split(')').next().unwrap_or("");
85        let trimmed = inner.trim_start_matches('[').trim_end_matches(']');
86        // `[db].[dbo].[users]` → 取最后一段
87        trimmed
88            .split("].[")
89            .last()
90            .map(|s| s.trim_matches('[').trim_matches(']').to_string())
91            .unwrap_or_else(|| trimmed.to_string())
92    } else {
93        "<unknown>".into()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    const TABLE_SCAN: &str = "\
102|--Table Scan(OBJECT:([szdb].[dbo].[users]), WHERE:([age]>(18)))
103";
104
105    const INDEX_SEEK: &str = "\
106|--Index Seek(OBJECT:([szdb].[dbo].[users].[PK__users__3214EC07]), SEEK:([id]=@1) ORDERED FORWARD)
107";
108
109    const INDEX_SCAN: &str = "\
110|--Index Scan(OBJECT:([szdb].[dbo].[orders].[idx_orders_user]))
111";
112
113    const JOIN_PLAN: &str = "\
114  |--Nested Loops(Inner Join, OUTER REFERENCES:([o].[user_id]))
115       |--Index Seek(OBJECT:([szdb].[dbo].[users].[PK__users__3214EC07]), SEEK:([u].[id]=[o].[user_id]) ORDERED FORWARD)
116       |--Table Scan(OBJECT:([szdb].[dbo].[orders]))
117";
118
119    #[test]
120    fn parses_table_scan() {
121        let plan = MssqlParser.parse(TABLE_SCAN).expect("parse ok");
122        assert_eq!(plan.scan_type, ScanType::FullTable);
123        assert_eq!(plan.table, "users");
124        assert!(plan.index.is_none());
125        assert_eq!(plan.rows, 0);
126        assert!(plan.missing_index(0));
127    }
128
129    #[test]
130    fn parses_index_seek() {
131        let plan = MssqlParser.parse(INDEX_SEEK).expect("parse ok");
132        assert_eq!(plan.scan_type, ScanType::UniqueLookup);
133        assert!(plan.index.as_deref().unwrap_or("").contains("PK__users"));
134    }
135
136    #[test]
137    fn parses_index_scan() {
138        let plan = MssqlParser.parse(INDEX_SCAN).expect("parse ok");
139        assert_eq!(plan.scan_type, ScanType::IndexRange);
140        assert!(plan
141            .index
142            .as_deref()
143            .unwrap_or("")
144            .contains("idx_orders_user"));
145    }
146
147    #[test]
148    fn full_scan_beats_seek_in_join() {
149        // 连接计划中同时出现 Table Scan 与 Index Seek,优先报告全表扫描
150        let plan = MssqlParser.parse(JOIN_PLAN).expect("parse ok");
151        assert_eq!(plan.scan_type, ScanType::FullTable);
152        assert_eq!(plan.table, "orders");
153    }
154
155    #[test]
156    fn rejects_unknown_input() {
157        let err = MssqlParser.parse("nothing here").unwrap_err();
158        assert!(matches!(err, ExplainError::Unparseable { .. }));
159    }
160}