sz_orm_explain/parsers/
oracle.rs1use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
18
19#[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 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; }
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 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 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}