sz_orm_explain/parsers/
sqlite.rs1use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
19
20#[derive(Debug)]
22pub struct SqliteParser;
23
24impl ExplainParser for SqliteParser {
25 fn parse(&self, raw: &str) -> Result<ExplainPlan, ExplainError> {
26 let mut plans: Vec<(ScanType, String, Option<String>)> = Vec::new();
27 for line in raw.lines() {
28 let line = line.trim();
29 if line.is_empty() || line.eq_ignore_ascii_case("QUERY PLAN") {
30 continue;
31 }
32 let body = strip_prefix(line);
34 let body = if body
36 .split_whitespace()
37 .next()
38 .unwrap_or("")
39 .chars()
40 .all(|c| c.is_ascii_digit())
41 {
42 body.split_whitespace()
43 .skip(3)
44 .collect::<Vec<_>>()
45 .join(" ")
46 } else {
47 body
48 };
49 if body.is_empty() {
50 continue;
51 }
52 if let Some(rest) = strip_prefix_ci(&body, "SCAN") {
53 plans.push((ScanType::FullTable, extract_table(rest), None));
54 } else if let Some(rest) = strip_prefix_ci(&body, "SEARCH") {
55 let (scan_type, table, index) = classify_search(rest);
56 plans.push((scan_type, table, index));
57 } else if strip_prefix_ci(&body, "USE TEMP B-TREE").is_some() {
58 plans.push((ScanType::Other, "<temp>".into(), None));
59 }
60 }
62 let (scan_type, table, index) =
63 plans
64 .first()
65 .cloned()
66 .ok_or_else(|| ExplainError::Unparseable {
67 reason: "no SCAN/SEARCH row found".into(),
68 })?;
69 Ok(ExplainPlan {
70 scan_type,
71 table,
72 index,
73 rows: 0,
74 extra: Vec::new(),
75 })
76 }
77}
78
79fn strip_prefix(line: &str) -> String {
81 let mut s = line.to_string();
82 for p in ["|--", "`--", "--"] {
83 if let Some(rest) = s.strip_prefix(p) {
84 s = rest.trim().to_string();
85 }
86 }
87 s
88}
89
90fn extract_table(rest: &str) -> String {
92 rest.split_whitespace()
93 .find(|w| !w.eq_ignore_ascii_case("TABLE") && !w.starts_with('('))
94 .unwrap_or("<unknown>")
95 .to_string()
96}
97
98fn classify_search(rest: &str) -> (ScanType, String, Option<String>) {
100 let lower = rest.to_ascii_lowercase();
101 let table = extract_table(rest);
102 let Some(pos) = lower.find("using") else {
103 return (ScanType::Other, table, None);
104 };
105 let using = lower[pos + 5..].trim_start();
106 if using.starts_with("primary key") {
107 return (ScanType::UniqueLookup, table, None);
108 }
109 if let Some(idx) = using.strip_prefix("covering index ") {
110 let idx = idx.split_whitespace().next().unwrap_or("<unknown>");
111 return (ScanType::IndexRange, table, Some(idx.to_string()));
112 }
113 if let Some(idx) = using.strip_prefix("index ") {
114 let idx = idx.split_whitespace().next().unwrap_or("<unknown>");
115 return (ScanType::IndexRange, table, Some(idx.to_string()));
116 }
117 (ScanType::Other, table, None)
118}
119
120fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
121 if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
122 Some(&s[prefix.len()..])
123 } else {
124 None
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const TREE_FORMAT: &str = "\
133QUERY PLAN
134|--SCAN users
135`--SEARCH orders USING INDEX idx_orders_user (user_id=?)
136";
137
138 const TABLE_FORMAT: &str = "\
1390 0 0 SCAN TABLE users
1400 1 0 SEARCH TABLE orders USING INDEX idx_orders_user (user_id=?)
141";
142
143 const PK_LOOKUP: &str = "\
144QUERY PLAN
145|--SEARCH users USING PRIMARY KEY (rowid=?)
146";
147
148 const COVERING: &str = "\
149QUERY PLAN
150`--SEARCH orders USING COVERING INDEX idx_orders_user (user_id=?)
151";
152
153 #[test]
154 fn parses_tree_format() {
155 let plan = SqliteParser.parse(TREE_FORMAT).expect("parse ok");
156 assert_eq!(plan.scan_type, ScanType::FullTable);
157 assert_eq!(plan.table, "users");
158 assert!(plan.index.is_none());
159 assert_eq!(plan.rows, 0);
160 }
161
162 #[test]
163 fn parses_table_format() {
164 let plan = SqliteParser.parse(TABLE_FORMAT).expect("parse ok");
165 assert_eq!(plan.scan_type, ScanType::FullTable);
166 assert_eq!(plan.table, "users");
167 }
168
169 #[test]
170 fn parses_primary_key_search() {
171 let plan = SqliteParser.parse(PK_LOOKUP).expect("parse ok");
172 assert_eq!(plan.scan_type, ScanType::UniqueLookup);
173 assert_eq!(plan.table, "users");
174 }
175
176 #[test]
177 fn parses_covering_index() {
178 let plan = SqliteParser.parse(COVERING).expect("parse ok");
179 assert_eq!(plan.scan_type, ScanType::IndexRange);
180 assert_eq!(plan.index.as_deref(), Some("idx_orders_user"));
181 }
182
183 #[test]
184 fn rejects_unknown_input() {
185 let err = SqliteParser.parse("hello world").unwrap_err();
186 assert!(matches!(err, ExplainError::Unparseable { .. }));
187 }
188}