Skip to main content

sz_orm_explain/parsers/
sqlite.rs

1//! SQLite EXPLAIN QUERY PLAN 解析器
2//!
3//! 输入示例(`EXPLAIN QUERY PLAN SELECT ...` 输出,两种格式均支持):
4//!
5//! ```text
6//! QUERY PLAN
7//! |--SCAN users
8//! `--SEARCH users USING INDEX users_email_idx (email=?)
9//! ```
10//!
11//! ```text
12//! 0 0 0 SCAN TABLE users
13//! 0 1 0 SEARCH TABLE users USING INDEX users_email_idx (email=?)
14//! ```
15//!
16//! 注意:SQLite 的 EXPLAIN QUERY PLAN 不输出预估行数,`rows` 固定为 0。
17
18use crate::{ExplainError, ExplainParser, ExplainPlan, ScanType};
19
20/// SQLite EXPLAIN 解析器
21#[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            // 去掉 `|--` / `` `-- `` 前缀
33            let body = strip_prefix(line);
34            // 数字前缀格式:`0 0 0 SCAN TABLE users` → 跳过前 3 个数字列
35            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            // 其他(LIST SUBQUERY/CO-ROUTINE 等)忽略
61        }
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
79/// 去掉树形前缀(`|--`/`` `-- ``)与 `TABLE` 关键字
80fn 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
90/// 提取 `SCAN TABLE <t>` 或 `SCAN <t>` 中的表名
91fn 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
98/// 分类 `SEARCH ... USING INDEX <idx>` / `SEARCH ... USING PRIMARY KEY` / `SEARCH ... USING COVERING INDEX <idx>`
99fn 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}