1use std::collections::{HashMap, HashSet};
10
11use fsqlite_types::opcode::{Opcode, P4};
12use fsqlite_vdbe::VdbeProgram;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ExplainRow {
21 pub addr: i32,
23 pub opcode: String,
25 pub p1: i32,
27 pub p2: i32,
29 pub p3: i32,
31 pub p4: String,
33 pub p5: u16,
35 pub comment: String,
37}
38
39#[must_use]
44pub fn explain_program(program: &VdbeProgram) -> Vec<ExplainRow> {
45 program
46 .ops()
47 .iter()
48 .enumerate()
49 .map(|(i, op)| {
50 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
51 let addr = i as i32;
52 ExplainRow {
53 addr,
54 opcode: format!("{:?}", op.opcode),
55 p1: op.p1,
56 p2: op.p2,
57 p3: op.p3,
58 p4: format!("{:?}", op.p4),
59 p5: op.p5,
60 comment: opcode_comment(op.opcode, op.p1, op.p2, op.p3),
61 }
62 })
63 .collect()
64}
65
66fn opcode_comment(opcode: Opcode, p1: i32, p2: i32, p3: i32) -> String {
68 match opcode {
69 Opcode::Init => format!("start at {p2}"),
70 Opcode::Goto => format!("goto {p2}"),
71 Opcode::Halt => {
72 if p1 == 0 {
73 String::new()
74 } else {
75 format!("error code {p1}")
76 }
77 }
78 Opcode::Transaction => {
79 if p2 == 0 {
80 "read transaction".to_owned()
81 } else {
82 "write transaction".to_owned()
83 }
84 }
85 Opcode::OpenRead | Opcode::OpenWrite => format!("root={p2}"),
86 Opcode::Column => format!("r[{p3}]=cursor[{p1}].column[{p2}]"),
87 Opcode::ResultRow => format!("output r[{p1}..{p1}+{p2}]"),
88 Opcode::Rewind => format!("if eof goto {p2}"),
89 Opcode::Next => format!("goto {p2} if more rows"),
90 Opcode::Close => format!("close cursor {p1}"),
91 _ => String::new(),
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct EqpRow {
102 pub id: i32,
104 pub parent: i32,
106 pub notused: i32,
108 pub detail: String,
110}
111
112#[must_use]
118pub fn explain_query_plan(program: &VdbeProgram) -> Vec<EqpRow> {
119 let ops = program.ops();
120 let mut rows = Vec::new();
121 let mut next_id = 1_i32;
122 let mut index_step_by_cursor: HashMap<i32, usize> = HashMap::new();
123 let mut index_name_by_cursor: HashMap<i32, String> = HashMap::new();
124 let mut index_used = HashSet::new();
125 let mut index_with_table_lookup = HashSet::new();
126 let mut last_idxrowid_cursor = None;
127 let mut saw_sorter = false;
128
129 for op in ops {
131 match op.opcode {
132 Opcode::OpenRead | Opcode::OpenWrite => {
133 let scan_type = if op.opcode == Opcode::OpenRead {
134 "SCAN"
135 } else {
136 "SEARCH"
137 };
138 let detail = match &op.p4 {
139 P4::Table(name) => format!("{scan_type} {name}"),
140 P4::Index(name) => format!("{scan_type} INDEX {name}"),
141 _ => format!("{scan_type} {:?}", op.p4),
142 };
143 rows.push(EqpRow {
144 id: next_id,
145 parent: 0,
146 notused: 0,
147 detail,
148 });
149 if let P4::Index(name) = &op.p4 {
150 index_step_by_cursor.insert(op.p1, rows.len() - 1);
151 index_name_by_cursor.insert(op.p1, name.clone());
152 }
153 next_id += 1;
154 }
155 Opcode::SeekGE
156 | Opcode::SeekLE
157 | Opcode::SeekGT
158 | Opcode::SeekLT
159 | Opcode::IdxGE
160 | Opcode::IdxGT
161 | Opcode::IdxLE
162 | Opcode::IdxLT
163 | Opcode::Rewind
164 | Opcode::Last
165 | Opcode::Next
166 | Opcode::Prev => {
167 if index_name_by_cursor.contains_key(&op.p1) {
168 index_used.insert(op.p1);
169 }
170 }
171 Opcode::IdxRowid => {
172 if index_name_by_cursor.contains_key(&op.p1) {
173 index_used.insert(op.p1);
174 last_idxrowid_cursor = Some(op.p1);
175 }
176 }
177 Opcode::SeekRowid => {
178 if let Some(index_cursor) = last_idxrowid_cursor {
179 index_with_table_lookup.insert(index_cursor);
180 }
181 last_idxrowid_cursor = None;
182 }
183 Opcode::SorterOpen | Opcode::SorterInsert | Opcode::SorterSort => {
184 saw_sorter = true;
185 }
186 _ => {
187 last_idxrowid_cursor = None;
188 }
189 }
190 }
191
192 for index_cursor in index_used {
194 let Some(step_idx) = index_step_by_cursor.get(&index_cursor).copied() else {
195 continue;
196 };
197 let Some(index_name) = index_name_by_cursor.get(&index_cursor) else {
198 continue;
199 };
200 let usage = if index_with_table_lookup.contains(&index_cursor) {
201 format!(" USING INDEX {index_name}")
202 } else {
203 format!(" USING COVERING INDEX {index_name}")
204 };
205 if !rows[step_idx].detail.contains("USING ") {
206 rows[step_idx].detail.push_str(&usage);
207 }
208 }
209
210 if saw_sorter {
211 rows.push(EqpRow {
212 id: next_id,
213 parent: 0,
214 notused: 0,
215 detail: "USE TEMP B-TREE FOR ORDER BY".to_owned(),
216 });
217 }
218
219 if rows.is_empty() {
221 rows.push(EqpRow {
222 id: 1,
223 parent: 0,
224 notused: 0,
225 detail: "SCAN CONSTANT ROW".to_owned(),
226 });
227 }
228
229 rows
230}
231
232#[cfg(test)]
237mod tests {
238 use super::*;
239 use fsqlite_types::opcode::{Opcode, P4};
240 use fsqlite_vdbe::ProgramBuilder;
241
242 fn build_simple_select_program() -> VdbeProgram {
243 let mut b = ProgramBuilder::new();
244 let end_label = b.emit_label();
245 let done_label = b.emit_label();
246
247 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
248 b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
249 b.emit_op(Opcode::OpenRead, 0, 2, 0, P4::Table("t".to_owned()), 0);
250 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done_label, P4::None, 0);
251 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
252 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
253 b.emit_op(Opcode::Next, 0, 4, 0, P4::None, 0);
254 b.resolve_label(done_label);
255 b.emit_op(Opcode::Close, 0, 0, 0, P4::None, 0);
256 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
257 b.resolve_label(end_label);
258
259 b.finish().unwrap()
260 }
261
262 #[test]
264 fn test_explain_returns_bytecode() {
265 let prog = build_simple_select_program();
266 let rows = explain_program(&prog);
267
268 assert!(!rows.is_empty());
270 assert_eq!(rows[0].addr, 0);
271
272 let init_row = &rows[0];
274 assert_eq!(init_row.opcode, "Init");
275 assert_eq!(init_row.p5, 0);
277 assert!(init_row.comment.contains("start at"));
279 }
280
281 #[test]
283 fn test_explain_query_plan_columns() {
284 let prog = build_simple_select_program();
285 let rows = explain_query_plan(&prog);
286
287 assert!(!rows.is_empty());
288 let row = &rows[0];
289 assert!(row.id > 0);
291 assert_eq!(row.parent, 0);
292 assert_eq!(row.notused, 0);
293 assert!(!row.detail.is_empty());
294 }
295
296 #[test]
298 fn test_explain_query_plan_shows_index() {
299 let mut b = ProgramBuilder::new();
301 let end_label = b.emit_label();
302 let done_label = b.emit_label();
303 let skip_label = b.emit_label();
304
305 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
306 b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
307 b.emit_op(Opcode::OpenRead, 0, 2, 0, P4::Table("t".to_owned()), 0);
308 b.emit_op(
309 Opcode::OpenRead,
310 1,
311 3,
312 0,
313 P4::Index("idx_t_a".to_owned()),
314 0,
315 );
316 b.emit_jump_to_label(Opcode::Rewind, 1, 0, done_label, P4::None, 0);
317 b.emit_op(Opcode::IdxRowid, 1, 2, 0, P4::None, 0);
318 b.emit_jump_to_label(Opcode::SeekRowid, 0, 2, skip_label, P4::None, 0);
319 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
320 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
321 b.resolve_label(skip_label);
322 b.emit_op(Opcode::Next, 1, 5, 0, P4::None, 0);
323 b.resolve_label(done_label);
324 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
325 b.resolve_label(end_label);
326
327 let prog = b.finish().unwrap();
328 let rows = explain_query_plan(&prog);
329
330 let has_index = rows.iter().any(|r| r.detail.contains("USING INDEX"));
332 assert!(has_index, "EQP should show index usage, got: {rows:?}");
333 }
334
335 #[test]
337 fn test_explain_query_plan_tree_structure() {
338 let prog = build_simple_select_program();
339 let rows = explain_query_plan(&prog);
340
341 for row in &rows {
343 if row.parent == 0 {
344 assert!(row.id > 0);
346 } else {
347 assert!(rows.iter().any(|r| r.id == row.parent));
349 }
350 }
351
352 let mut ids: Vec<i32> = rows.iter().map(|r| r.id).collect();
354 ids.sort_unstable();
355 ids.dedup();
356 assert_eq!(ids.len(), rows.len());
357 }
358
359 #[test]
360 fn test_explain_query_plan_shows_covering_index() {
361 let mut b = ProgramBuilder::new();
362 let end_label = b.emit_label();
363 let done_label = b.emit_label();
364
365 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
366 b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
367 b.emit_op(
368 Opcode::OpenRead,
369 1,
370 3,
371 0,
372 P4::Index("idx_t_b".to_owned()),
373 0,
374 );
375 b.emit_jump_to_label(Opcode::Rewind, 1, 0, done_label, P4::None, 0);
376 b.emit_op(Opcode::Column, 1, 0, 1, P4::None, 0);
377 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
378 b.emit_op(Opcode::Next, 1, 4, 0, P4::None, 0);
379 b.resolve_label(done_label);
380 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
381 b.resolve_label(end_label);
382
383 let prog = b.finish().unwrap();
384 let rows = explain_query_plan(&prog);
385 let has_covering = rows
386 .iter()
387 .any(|row| row.detail.contains("USING COVERING INDEX idx_t_b"));
388 assert!(
389 has_covering,
390 "EQP should show covering-index usage, got: {rows:?}"
391 );
392 }
393
394 #[test]
395 fn test_explain_query_plan_shows_temp_btree_for_order_by() {
396 let mut b = ProgramBuilder::new();
397 let end_label = b.emit_label();
398 let done_label = b.emit_label();
399
400 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
401 b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
402 b.emit_op(Opcode::OpenRead, 0, 2, 0, P4::Table("t".to_owned()), 0);
403 b.emit_op(Opcode::SorterOpen, 1, 1, 0, P4::Str("+".to_owned()), 0);
404 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done_label, P4::None, 0);
405 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
406 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
407 b.emit_op(Opcode::SorterInsert, 1, 3, 0, P4::None, 0);
408 b.resolve_label(done_label);
409 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
410 b.resolve_label(end_label);
411
412 let prog = b.finish().unwrap();
413 let rows = explain_query_plan(&prog);
414 let has_temp_btree = rows
415 .iter()
416 .any(|row| row.detail == "USE TEMP B-TREE FOR ORDER BY");
417 assert!(
418 has_temp_btree,
419 "EQP should include temp B-tree marker for sorter plans, got: {rows:?}"
420 );
421 }
422}