Skip to main content

sz_orm_explain/parsers/
oracle.rs

1//! Oracle EXPLAIN PLAN 解析器
2//!
3//! 输入示例(`EXPLAIN PLAN FOR ...` 后查询 `PLAN_TABLE`/`DBMS_XPLAN.DISPLAY` 输出):
4//!
5//! ```text
6//! Plan hash value: 1234567890
7//!
8//! ---------------------------------------------------------------------------
9//! | Id  | Operation                    | Name      | Rows  | Bytes | Cost   |
10//! ---------------------------------------------------------------------------
11//! |   0 | SELECT STATEMENT             |           |     1 |    38 |     3  |
12//! |   1 |  TABLE ACCESS BY INDEX ROWID | USERS     |     1 |    38 |     2  |
13//! |*  2 |   INDEX UNIQUE SCAN          | USERS_PK  |     1 |       |     1  |
14//! ---------------------------------------------------------------------------
15//! ```
16
17use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
18
19/// Oracle EXPLAIN 解析器
20#[derive(Debug)]
21pub struct OracleParser;
22
23impl ExplainParser for OracleParser {
24    fn parse(&self, raw: &str) -> Result<ExplainPlan, ExplainError> {
25        let mut best: Option<(ScanType, String, Option<String>, u64)> = None;
26        // 优先级:FullTable > IndexRange > UniqueLookup > IndexLookup > Other
27        let rank = |s: ScanType| match s {
28            ScanType::FullTable => 5,
29            ScanType::IndexRange => 4,
30            ScanType::UniqueLookup => 3,
31            ScanType::IndexLookup => 2,
32            ScanType::Other => 1,
33        };
34
35        for line in raw.lines() {
36            let line = line.trim();
37            if !line.starts_with('|') {
38                continue;
39            }
40            let cells: Vec<String> = line
41                .split('|')
42                .map(|c| c.trim().to_string())
43                .filter(|c| !c.is_empty())
44                .collect();
45            if cells.len() < 2 {
46                continue;
47            }
48            if cells.iter().any(|c| c.eq_ignore_ascii_case("Operation")) {
49                continue; // 表头
50            }
51            let operation = cells
52                .get(1)
53                .map(|s| s.to_ascii_uppercase())
54                .unwrap_or_default();
55            let name = cells.get(2).cloned().unwrap_or_default();
56            let rows = cells
57                .get(3)
58                .and_then(|s| crate::parsers::parse_u64(s))
59                .unwrap_or(0);
60
61            let candidate: Option<(ScanType, String, Option<String>, u64)> =
62                if operation.contains("TABLE ACCESS FULL") {
63                    Some((ScanType::FullTable, name.clone(), None, rows))
64                } else if operation.contains("INDEX RANGE SCAN") {
65                    Some((
66                        ScanType::IndexRange,
67                        "<unknown>".into(),
68                        Some(name.clone()),
69                        rows,
70                    ))
71                } else if operation.contains("INDEX UNIQUE SCAN") {
72                    Some((
73                        ScanType::UniqueLookup,
74                        "<unknown>".into(),
75                        Some(name.clone()),
76                        rows,
77                    ))
78                } else if operation.contains("INDEX") && operation.contains("SCAN") {
79                    Some((
80                        ScanType::IndexLookup,
81                        "<unknown>".into(),
82                        Some(name.clone()),
83                        rows,
84                    ))
85                } else if operation.contains("TABLE ACCESS") {
86                    Some((ScanType::Other, name.clone(), None, rows))
87                } else {
88                    None
89                };
90            if let Some(c) = candidate {
91                if best.is_none() || rank(c.0) > rank(best.as_ref().unwrap().0) {
92                    best = Some(c);
93                }
94            }
95        }
96        let (scan_type, table, index, rows) = best.ok_or_else(|| ExplainError::Unparseable {
97            reason: "no recognizable operation row".into(),
98        })?;
99        Ok(ExplainPlan {
100            scan_type,
101            table,
102            index,
103            rows,
104            extra: Vec::new(),
105        })
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    const FULL_SCAN: &str = "\
114---------------------------------------------------------------------------
115| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
116---------------------------------------------------------------------------
117|   0 | SELECT STATEMENT   |       |  1150 | 40250 |    22  (0)| 00:00:01 |
118|   1 |  TABLE ACCESS FULL | USERS |  1150 | 40250 |    21  (0)| 00:00:01 |
119---------------------------------------------------------------------------
120";
121
122    const PK_LOOKUP: &str = "\
123---------------------------------------------------------------------------
124| Id  | Operation                    | Name      | Rows  | Bytes | Cost   |
125---------------------------------------------------------------------------
126|   0 | SELECT STATEMENT             |           |     1 |    38 |     3  |
127|   1 |  TABLE ACCESS BY INDEX ROWID | USERS     |     1 |    38 |     2  |
128|*  2 |   INDEX UNIQUE SCAN          | USERS_PK  |     1 |       |     1  |
129---------------------------------------------------------------------------
130";
131
132    const RANGE_SCAN: &str = "\
133---------------------------------------------------------------------------
134| Id  | Operation          | Name          | Rows  | Bytes | Cost   |
135---------------------------------------------------------------------------
136|   0 | SELECT STATEMENT   |               |    42 |  1512 |     4  |
137|   1 |  TABLE ACCESS FULL | ORDERS        |    42 |  1512 |     3  |
138|*  2 |   INDEX RANGE SCAN | IDX_ORDERS_UA |    42 |       |     1  |
139---------------------------------------------------------------------------
140";
141
142    #[test]
143    fn parses_full_table_access() {
144        let plan = OracleParser.parse(FULL_SCAN).expect("parse ok");
145        assert_eq!(plan.scan_type, ScanType::FullTable);
146        assert_eq!(plan.table, "USERS");
147        assert!(plan.index.is_none());
148        assert_eq!(plan.rows, 1150);
149        assert!(plan.missing_index(1000));
150    }
151
152    #[test]
153    fn unique_scan_beats_rowid_access() {
154        // 同一计划中含 TABLE ACCESS + INDEX UNIQUE SCAN,应选中 UniqueLookup
155        let plan = OracleParser.parse(PK_LOOKUP).expect("parse ok");
156        assert_eq!(plan.scan_type, ScanType::UniqueLookup);
157        assert_eq!(plan.index.as_deref(), Some("USERS_PK"));
158        assert_eq!(plan.table, "<unknown>");
159    }
160
161    #[test]
162    fn full_scan_beats_range_scan() {
163        // 同时出现 TABLE ACCESS FULL 与 INDEX RANGE SCAN 时(复杂计划),
164        // 全表扫描是更严重的信号,优先报告
165        let plan = OracleParser.parse(RANGE_SCAN).expect("parse ok");
166        assert_eq!(plan.scan_type, ScanType::FullTable);
167        assert_eq!(plan.table, "ORDERS");
168    }
169
170    #[test]
171    fn rejects_unknown_input() {
172        let err = OracleParser.parse("no plan here").unwrap_err();
173        assert!(matches!(err, ExplainError::Unparseable { .. }));
174    }
175}