Skip to main content

sz_orm_explain/parsers/
postgres.rs

1//! PostgreSQL EXPLAIN 文本格式解析器
2//!
3//! 输入示例(`EXPLAIN [VERBOSE] SELECT ...` 输出):
4//!
5//! ```text
6//! Seq Scan on users  (cost=0.00..21.50 rows=1150 width=36)
7//!   Filter: (age > 18)
8//! Index Scan using users_pkey on users  (cost=0.29..8.31 rows=1 width=36)
9//!   Index Cond: (id = 1)
10//! ```
11
12use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
13
14/// PostgreSQL EXPLAIN 解析器
15#[derive(Debug)]
16pub struct PostgresParser;
17
18impl ExplainParser for PostgresParser {
19    fn parse(&self, raw: &str) -> Result<ExplainPlan, ExplainError> {
20        // 顶层节点为第一个非空行
21        let top = raw
22            .lines()
23            .map(str::trim)
24            .find(|l| !l.is_empty())
25            .ok_or_else(|| ExplainError::Unparseable {
26                reason: "empty explain output".into(),
27            })?;
28
29        let (scan_type, table, index) = classify_operation(top);
30        let rows = crate::parsers::extract_rows_from_kv(raw).unwrap_or(0);
31        let extra = collect_extra(raw);
32
33        Ok(ExplainPlan {
34            scan_type,
35            table,
36            index,
37            rows,
38            extra,
39        })
40    }
41}
42
43/// 根据顶层操作符文本分类扫描类型,并提取表名/索引名
44fn classify_operation(top: &str) -> (ScanType, String, Option<String>) {
45    // 模式一:`Index Scan using <index> on <table>`
46    if let Some(rest) = strip_prefix_ci(top, "Index Scan using") {
47        let (index, table) = split_index_table(rest);
48        return (ScanType::IndexRange, table, Some(index));
49    }
50    // 模式二:`Index Only Scan using <index> on <table>`
51    if let Some(rest) = strip_prefix_ci(top, "Index Only Scan using") {
52        let (index, table) = split_index_table(rest);
53        return (ScanType::IndexRange, table, Some(index));
54    }
55    // 模式三:`Seq Scan on <table>`
56    if let Some(rest) = strip_prefix_ci(top, "Seq Scan on") {
57        return (ScanType::FullTable, extract_table(rest), None);
58    }
59    // 模式四:`Bitmap Heap Scan on <table>`(索引驱动,无法直接判断索引)
60    if let Some(rest) = strip_prefix_ci(top, "Bitmap Heap Scan on") {
61        return (ScanType::Other, extract_table(rest), None);
62    }
63    // 模式五:`Bitmap Index Scan on <index>`
64    if let Some(rest) = strip_prefix_ci(top, "Bitmap Index Scan on") {
65        return (
66            ScanType::IndexRange,
67            "<unknown>".into(),
68            Some(extract_name(rest)),
69        );
70    }
71    // 其他(Sort/Hash Join/Nested Loop/Aggregate 等)
72    (
73        ScanType::Other,
74        top.split_whitespace()
75            .nth(2)
76            .unwrap_or("<unknown>")
77            .to_string(),
78        None,
79    )
80}
81
82/// 提取 `using <index> on <table>` 中的索引名与表名
83fn split_index_table(rest: &str) -> (String, String) {
84    let lower = rest.to_ascii_lowercase();
85    if let Some(pos) = lower.find(" on ") {
86        let index = rest[..pos].trim().to_string();
87        let table = extract_table(&rest[pos + 4..]);
88        (index, table)
89    } else {
90        (rest.trim().to_string(), "<unknown>".into())
91    }
92}
93
94/// 提取 `on <table> (cost=...)` 中的表名
95fn extract_table(rest: &str) -> String {
96    let name = rest.split_whitespace().next().unwrap_or("<unknown>");
97    if name.starts_with('(') {
98        "<unknown>".into()
99    } else {
100        name.to_string()
101    }
102}
103
104/// 提取 `on <index>` 中的索引名
105fn extract_name(rest: &str) -> String {
106    rest.split_whitespace()
107        .next()
108        .unwrap_or("<unknown>")
109        .to_string()
110}
111
112fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
113    if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
114        Some(&s[prefix.len()..])
115    } else {
116        None
117    }
118}
119
120/// 收集子节点信息(Filter/Index Cond 等)为 extra
121fn collect_extra(raw: &str) -> Vec<String> {
122    let mut extra = Vec::new();
123    for line in raw.lines() {
124        let t = line.trim();
125        for key in ["Filter:", "Index Cond:", "Heap Blocks:", "Buffers:"] {
126            if let Some(pos) = t.find(key) {
127                extra.push(t[pos..].to_string());
128            }
129        }
130    }
131    extra
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    const SEQ_SCAN: &str = "\
139Seq Scan on users  (cost=0.00..21.50 rows=1150 width=36)
140  Filter: (age > 18)
141";
142
143    const INDEX_SCAN: &str = "\
144Index Scan using users_pkey on users  (cost=0.29..8.31 rows=1 width=36)
145  Index Cond: (id = 1)
146";
147
148    const INDEX_ONLY: &str = "\
149Index Only Scan using orders_user_idx on orders  (cost=0.42..8.44 rows=1 width=8)
150  Index Cond: (user_id = 42)
151";
152
153    const JOIN_PLAN: &str = "\
154Hash Join  (cost=25.31..49.02 rows=1150 width=72)
155  Hash Cond: (o.user_id = u.id)
156  ->  Seq Scan on orders  (cost=0.00..16.50 rows=1150 width=36)
157  ->  Hash  (cost=21.50..21.50 rows=1150 width=36)
158        ->  Seq Scan on users  (cost=0.00..21.50 rows=1150 width=36)
159";
160
161    #[test]
162    fn parses_seq_scan() {
163        let plan = PostgresParser.parse(SEQ_SCAN).expect("parse ok");
164        assert_eq!(plan.scan_type, ScanType::FullTable);
165        assert_eq!(plan.table, "users");
166        assert!(plan.index.is_none());
167        assert_eq!(plan.rows, 1150);
168        assert!(plan.extra.iter().any(|e| e.contains("Filter")));
169        assert!(plan.missing_index(1000));
170    }
171
172    #[test]
173    fn parses_index_scan() {
174        let plan = PostgresParser.parse(INDEX_SCAN).expect("parse ok");
175        assert_eq!(plan.scan_type, ScanType::IndexRange);
176        assert_eq!(plan.index.as_deref(), Some("users_pkey"));
177        assert_eq!(plan.table, "users");
178        assert_eq!(plan.rows, 1);
179    }
180
181    #[test]
182    fn parses_index_only_scan() {
183        let plan = PostgresParser.parse(INDEX_ONLY).expect("parse ok");
184        assert_eq!(plan.scan_type, ScanType::IndexRange);
185        assert_eq!(plan.index.as_deref(), Some("orders_user_idx"));
186        assert_eq!(plan.table, "orders");
187    }
188
189    #[test]
190    fn top_level_join_is_other() {
191        let plan = PostgresParser.parse(JOIN_PLAN).expect("parse ok");
192        assert_eq!(plan.scan_type, ScanType::Other);
193        assert_eq!(plan.rows, 1150);
194    }
195
196    #[test]
197    fn rejects_empty_input() {
198        let err = PostgresParser.parse("   \n  \n").unwrap_err();
199        assert!(matches!(err, ExplainError::Unparseable { .. }));
200    }
201}