limbo_core/translate/main_loop.rs
1use limbo_ext::VTabKind;
2use limbo_sqlite3_parser::ast::{self, SortOrder};
3
4use std::sync::Arc;
5
6use crate::{
7 schema::{Affinity, Index, IndexColumn, Table},
8 translate::{
9 plan::{DistinctCtx, Distinctness},
10 result_row::emit_select_result,
11 },
12 types::SeekOp,
13 vdbe::{
14 builder::{CursorKey, CursorType, ProgramBuilder},
15 insn::{CmpInsFlags, IdxInsertFlags, Insn},
16 BranchOffset, CursorID,
17 },
18 Result,
19};
20
21use super::{
22 aggregation::translate_aggregation_step,
23 emitter::{OperationMode, TranslateCtx},
24 expr::{
25 translate_condition_expr, translate_expr, translate_expr_no_constant_opt,
26 ConditionMetadata, NoConstantOptReason,
27 },
28 group_by::{group_by_agg_phase, GroupByMetadata, GroupByRowSource},
29 optimizer::Optimizable,
30 order_by::{order_by_sorter_insert, sorter_insert},
31 plan::{
32 convert_where_to_vtab_constraint, Aggregate, GroupBy, IterationDirection, JoinOrderMember,
33 Operation, QueryDestination, Search, SeekDef, SelectPlan, TableReferences, WhereTerm,
34 },
35};
36
37// Metadata for handling LEFT JOIN operations
38#[derive(Debug)]
39pub struct LeftJoinMetadata {
40 // integer register that holds a flag that is set to true if the current row has a match for the left join
41 pub reg_match_flag: usize,
42 // label for the instruction that sets the match flag to true
43 pub label_match_flag_set_true: BranchOffset,
44 // label for the instruction that checks if the match flag is true
45 pub label_match_flag_check_value: BranchOffset,
46}
47
48/// Jump labels for each loop in the query's main execution loop
49#[derive(Debug, Clone, Copy)]
50pub struct LoopLabels {
51 /// jump to the start of the loop body
52 pub loop_start: BranchOffset,
53 /// jump to the Next instruction (or equivalent)
54 pub next: BranchOffset,
55 /// jump to the end of the loop, exiting it
56 pub loop_end: BranchOffset,
57}
58
59impl LoopLabels {
60 pub fn new(program: &mut ProgramBuilder) -> Self {
61 Self {
62 loop_start: program.allocate_label(),
63 next: program.allocate_label(),
64 loop_end: program.allocate_label(),
65 }
66 }
67}
68
69pub fn init_distinct(program: &mut ProgramBuilder, plan: &mut SelectPlan) {
70 if let Distinctness::Distinct { ctx } = &mut plan.distinctness {
71 assert!(
72 ctx.is_none(),
73 "distinctness context should not be allocated yet"
74 );
75 let index_name = format!("distinct_{}", program.offset().to_offset_int()); // we don't really care about the name that much, just enough that we don't get name collisions
76 let index = Arc::new(Index {
77 name: index_name.clone(),
78 table_name: String::new(),
79 ephemeral: true,
80 root_page: 0,
81 columns: plan
82 .result_columns
83 .iter()
84 .enumerate()
85 .map(|(i, col)| IndexColumn {
86 name: col.expr.to_string(),
87 order: SortOrder::Asc,
88 pos_in_table: i,
89 collation: None, // FIXME: this should be determined based on the result column expression!
90 default: None, // FIXME: this should be determined based on the result column expression!
91 })
92 .collect(),
93 unique: false,
94 has_rowid: false,
95 });
96 let cursor_id = program.alloc_cursor_id(CursorType::BTreeIndex(index.clone()));
97 *ctx = Some(DistinctCtx {
98 cursor_id,
99 ephemeral_index_name: index_name,
100 label_on_conflict: program.allocate_label(),
101 });
102
103 program.emit_insn(Insn::OpenEphemeral {
104 cursor_id,
105 is_table: false,
106 });
107 }
108}
109
110/// Initialize resources needed for the source operators (tables, joins, etc)
111pub fn init_loop(
112 program: &mut ProgramBuilder,
113 t_ctx: &mut TranslateCtx,
114 tables: &TableReferences,
115 aggregates: &mut [Aggregate],
116 group_by: Option<&GroupBy>,
117 mode: OperationMode,
118) -> Result<()> {
119 assert!(
120 t_ctx.meta_left_joins.len() == tables.joined_tables().len(),
121 "meta_left_joins length does not match tables length"
122 );
123 // Initialize ephemeral indexes for distinct aggregates
124 for (i, agg) in aggregates
125 .iter_mut()
126 .enumerate()
127 .filter(|(_, agg)| agg.is_distinct())
128 {
129 assert!(
130 agg.args.len() == 1,
131 "DISTINCT aggregate functions must have exactly one argument"
132 );
133 let index_name = format!("distinct_agg_{}_{}", i, agg.args[0]);
134 let index = Arc::new(Index {
135 name: index_name.clone(),
136 table_name: String::new(),
137 ephemeral: true,
138 root_page: 0,
139 columns: vec![IndexColumn {
140 name: agg.args[0].to_string(),
141 order: SortOrder::Asc,
142 pos_in_table: 0,
143 collation: None, // FIXME: this should be inferred from the expression
144 default: None, // FIXME: this should be inferred from the expression
145 }],
146 has_rowid: false,
147 unique: false,
148 });
149 let cursor_id = program.alloc_cursor_id(CursorType::BTreeIndex(index.clone()));
150 if group_by.is_none() {
151 // In GROUP BY, the ephemeral index is reinitialized for every group
152 // in the clear accumulator subroutine, so we only do it here if there is no GROUP BY.
153 program.emit_insn(Insn::OpenEphemeral {
154 cursor_id,
155 is_table: false,
156 });
157 }
158 agg.distinctness = Distinctness::Distinct {
159 ctx: Some(DistinctCtx {
160 cursor_id,
161 ephemeral_index_name: index_name,
162 label_on_conflict: program.allocate_label(),
163 }),
164 };
165 }
166 for (table_index, table) in tables.joined_tables().iter().enumerate() {
167 // Initialize bookkeeping for OUTER JOIN
168 if let Some(join_info) = table.join_info.as_ref() {
169 if join_info.outer {
170 let lj_metadata = LeftJoinMetadata {
171 reg_match_flag: program.alloc_register(),
172 label_match_flag_set_true: program.allocate_label(),
173 label_match_flag_check_value: program.allocate_label(),
174 };
175 t_ctx.meta_left_joins[table_index] = Some(lj_metadata);
176 }
177 }
178 let (table_cursor_id, index_cursor_id) = table.open_cursors(program, mode)?;
179 match &table.op {
180 Operation::Scan { index, .. } => match (mode, &table.table) {
181 (OperationMode::SELECT, Table::BTree(btree)) => {
182 let root_page = btree.root_page;
183 if let Some(cursor_id) = table_cursor_id {
184 program.emit_insn(Insn::OpenRead {
185 cursor_id,
186 root_page,
187 });
188 }
189 if let Some(index_cursor_id) = index_cursor_id {
190 program.emit_insn(Insn::OpenRead {
191 cursor_id: index_cursor_id,
192 root_page: index.as_ref().unwrap().root_page,
193 });
194 }
195 }
196 (OperationMode::DELETE, Table::BTree(btree)) => {
197 let root_page = btree.root_page;
198 program.emit_insn(Insn::OpenWrite {
199 cursor_id: table_cursor_id
200 .expect("table cursor is always opened in OperationMode::DELETE"),
201 root_page: root_page.into(),
202 name: btree.name.clone(),
203 });
204 if let Some(index_cursor_id) = index_cursor_id {
205 program.emit_insn(Insn::OpenWrite {
206 cursor_id: index_cursor_id,
207 root_page: index.as_ref().unwrap().root_page.into(),
208 name: index.as_ref().unwrap().name.clone(),
209 });
210 }
211 // For delete, we need to open all the other indexes too for writing
212 if let Some(indexes) = t_ctx.resolver.schema.indexes.get(&btree.name) {
213 for index in indexes {
214 if table
215 .op
216 .index()
217 .is_some_and(|table_index| table_index.name == index.name)
218 {
219 continue;
220 }
221 let cursor_id = program.alloc_cursor_id_keyed(
222 CursorKey::index(table.internal_id, index.clone()),
223 CursorType::BTreeIndex(index.clone()),
224 );
225 program.emit_insn(Insn::OpenWrite {
226 cursor_id,
227 root_page: index.root_page.into(),
228 name: index.name.clone(),
229 });
230 }
231 }
232 }
233 (OperationMode::UPDATE, Table::BTree(btree)) => {
234 let root_page = btree.root_page;
235 program.emit_insn(Insn::OpenWrite {
236 cursor_id: table_cursor_id
237 .expect("table cursor is always opened in OperationMode::UPDATE"),
238 root_page: root_page.into(),
239 name: btree.name.clone(),
240 });
241 if let Some(index_cursor_id) = index_cursor_id {
242 program.emit_insn(Insn::OpenWrite {
243 cursor_id: index_cursor_id,
244 root_page: index.as_ref().unwrap().root_page.into(),
245 name: index.as_ref().unwrap().name.clone(),
246 });
247 }
248 }
249 (_, Table::Virtual(_)) => {
250 if let Some(cursor_id) = table_cursor_id {
251 program.emit_insn(Insn::VOpen { cursor_id });
252 }
253 }
254 _ => {}
255 },
256 Operation::Search(search) => {
257 match mode {
258 OperationMode::SELECT => {
259 if let Some(table_cursor_id) = table_cursor_id {
260 program.emit_insn(Insn::OpenRead {
261 cursor_id: table_cursor_id,
262 root_page: table.table.get_root_page(),
263 });
264 }
265 }
266 OperationMode::DELETE | OperationMode::UPDATE => {
267 let table_cursor_id = table_cursor_id.expect("table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE");
268 program.emit_insn(Insn::OpenWrite {
269 cursor_id: table_cursor_id,
270 root_page: table.table.get_root_page().into(),
271 name: table.table.get_name().to_string(),
272 });
273 // For DELETE, we need to open all the indexes for writing
274 // UPDATE opens these in emit_program_for_update() separately
275 if mode == OperationMode::DELETE {
276 if let Some(indexes) =
277 t_ctx.resolver.schema.indexes.get(table.table.get_name())
278 {
279 for index in indexes {
280 if table
281 .op
282 .index()
283 .is_some_and(|table_index| table_index.name == index.name)
284 {
285 continue;
286 }
287 let cursor_id = program.alloc_cursor_id_keyed(
288 CursorKey::index(table.internal_id, index.clone()),
289 CursorType::BTreeIndex(index.clone()),
290 );
291 program.emit_insn(Insn::OpenWrite {
292 cursor_id,
293 root_page: index.root_page.into(),
294 name: index.name.clone(),
295 });
296 }
297 }
298 }
299 }
300 _ => {
301 unimplemented!()
302 }
303 }
304
305 if let Search::Seek {
306 index: Some(index), ..
307 } = search
308 {
309 // Ephemeral index cursor are opened ad-hoc when needed.
310 if !index.ephemeral {
311 match mode {
312 OperationMode::SELECT => {
313 program.emit_insn(Insn::OpenRead {
314 cursor_id: index_cursor_id
315 .expect("index cursor is always opened in Seek with index"),
316 root_page: index.root_page,
317 });
318 }
319 OperationMode::UPDATE | OperationMode::DELETE => {
320 program.emit_insn(Insn::OpenWrite {
321 cursor_id: index_cursor_id
322 .expect("index cursor is always opened in Seek with index"),
323 root_page: index.root_page.into(),
324 name: index.name.clone(),
325 });
326 }
327 _ => {
328 unimplemented!()
329 }
330 }
331 }
332 }
333 }
334 }
335 }
336
337 Ok(())
338}
339
340/// Set up the main query execution loop
341/// For example in the case of a nested table scan, this means emitting the Rewind instruction
342/// for all tables involved, outermost first.
343pub fn open_loop(
344 program: &mut ProgramBuilder,
345 t_ctx: &mut TranslateCtx,
346 table_references: &TableReferences,
347 join_order: &[JoinOrderMember],
348 predicates: &[WhereTerm],
349) -> Result<()> {
350 for (join_index, join) in join_order.iter().enumerate() {
351 let joined_table_index = join.original_idx;
352 let table = &table_references.joined_tables()[joined_table_index];
353 let LoopLabels {
354 loop_start,
355 loop_end,
356 next,
357 } = *t_ctx
358 .labels_main_loop
359 .get(joined_table_index)
360 .expect("table has no loop labels");
361
362 // Each OUTER JOIN has a "match flag" that is initially set to false,
363 // and is set to true when a match is found for the OUTER JOIN.
364 // This is used to determine whether to emit actual columns or NULLs for the columns of the right table.
365 if let Some(join_info) = table.join_info.as_ref() {
366 if join_info.outer {
367 let lj_meta = t_ctx.meta_left_joins[joined_table_index].as_ref().unwrap();
368 program.emit_insn(Insn::Integer {
369 value: 0,
370 dest: lj_meta.reg_match_flag,
371 });
372 }
373 }
374
375 let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program)?;
376
377 match &table.op {
378 Operation::Scan { iter_dir, .. } => {
379 match &table.table {
380 Table::BTree(_) => {
381 let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
382 table_cursor_id.expect("Either index or table cursor must be opened")
383 });
384 if *iter_dir == IterationDirection::Backwards {
385 program.emit_insn(Insn::Last {
386 cursor_id: iteration_cursor_id,
387 pc_if_empty: loop_end,
388 });
389 } else {
390 program.emit_insn(Insn::Rewind {
391 cursor_id: iteration_cursor_id,
392 pc_if_empty: loop_end,
393 });
394 }
395 program.preassign_label_to_next_insn(loop_start);
396 }
397 Table::Virtual(vtab) => {
398 let (start_reg, count, maybe_idx_str, maybe_idx_int) =
399 if vtab.kind.eq(&VTabKind::VirtualTable) {
400 // Virtual‑table (non‑TVF) modules can receive constraints via xBestIndex.
401 // They return information with which to pass to VFilter operation.
402 // We forward every predicate that touches vtab columns.
403 //
404 // vtab.col = literal (always usable)
405 // vtab.col = outer_table.col (usable, because outer_table is already positioned)
406 // vtab.col = later_table.col (forwarded with usable = false)
407 //
408 // xBestIndex decides which ones it wants by setting argvIndex and whether the
409 // core layer may omit them (omit = true).
410 // We then materialise the RHS/LHS into registers before issuing VFilter.
411 let converted_constraints = predicates
412 .iter()
413 .filter(|p| p.should_eval_at_loop(join_index, join_order))
414 .enumerate()
415 .filter_map(|(i, p)| {
416 // Build ConstraintInfo from the predicates
417 convert_where_to_vtab_constraint(
418 p,
419 joined_table_index,
420 i,
421 join_order,
422 )
423 .unwrap_or(None)
424 })
425 .collect::<Vec<_>>();
426 // TODO: get proper order_by information to pass to the vtab.
427 // maybe encode more info on t_ctx? we need: [col_idx, is_descending]
428 let index_info = vtab.best_index(&converted_constraints, &[]);
429
430 // Determine the number of VFilter arguments (constraints with an argv_index).
431 let args_needed = index_info
432 .constraint_usages
433 .iter()
434 .filter(|u| u.argv_index.is_some())
435 .count();
436 let start_reg = program.alloc_registers(args_needed);
437
438 // For each constraint used by best_index, translate the opposite side.
439 for (i, usage) in index_info.constraint_usages.iter().enumerate() {
440 if let Some(argv_index) = usage.argv_index {
441 if let Some(cinfo) = converted_constraints.get(i) {
442 let (pred_idx, is_rhs) = cinfo.unpack_plan_info();
443 if let ast::Expr::Binary(lhs, _, rhs) =
444 &predicates[pred_idx].expr
445 {
446 // translate the opposite side of the referenced vtab column
447 let expr = if is_rhs { lhs } else { rhs };
448 // argv_index is 1-based; adjust to get the proper register offset.
449 if argv_index == 0 {
450 // invalid since argv_index is 1-based
451 continue;
452 }
453 let target_reg =
454 start_reg + (argv_index - 1) as usize;
455 translate_expr(
456 program,
457 Some(table_references),
458 expr,
459 target_reg,
460 &t_ctx.resolver,
461 )?;
462 if cinfo.usable && usage.omit {
463 predicates[pred_idx].consumed.set(true);
464 }
465 }
466 }
467 }
468 }
469
470 // If best_index provided an idx_str, translate it.
471 let maybe_idx_str = if let Some(idx_str) = index_info.idx_str {
472 let reg = program.alloc_register();
473 program.emit_insn(Insn::String8 {
474 dest: reg,
475 value: idx_str,
476 });
477 Some(reg)
478 } else {
479 None
480 };
481 (
482 start_reg,
483 args_needed,
484 maybe_idx_str,
485 Some(index_info.idx_num),
486 )
487 } else {
488 // For table-valued functions: translate the table args.
489 let args = match vtab.args.as_ref() {
490 Some(args) => args,
491 None => &vec![],
492 };
493 let start_reg = program.alloc_registers(args.len());
494 let mut cur_reg = start_reg;
495 for arg in args {
496 let reg = cur_reg;
497 cur_reg += 1;
498 let _ = translate_expr(
499 program,
500 Some(table_references),
501 arg,
502 reg,
503 &t_ctx.resolver,
504 )?;
505 }
506 (start_reg, args.len(), None, None)
507 };
508
509 // Emit VFilter with the computed arguments.
510 program.emit_insn(Insn::VFilter {
511 cursor_id: table_cursor_id
512 .expect("Virtual tables do not support covering indexes"),
513 arg_count: count,
514 args_reg: start_reg,
515 idx_str: maybe_idx_str,
516 idx_num: maybe_idx_int.unwrap_or(0) as usize,
517 pc_if_empty: loop_end,
518 });
519 program.preassign_label_to_next_insn(loop_start);
520 }
521 Table::FromClauseSubquery(from_clause_subquery) => {
522 let (yield_reg, coroutine_implementation_start) =
523 match &from_clause_subquery.plan.query_destination {
524 QueryDestination::CoroutineYield {
525 yield_reg,
526 coroutine_implementation_start,
527 } => (*yield_reg, *coroutine_implementation_start),
528 _ => unreachable!("Subquery table with non-subquery query type"),
529 };
530 // In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop.
531 program.emit_insn(Insn::InitCoroutine {
532 yield_reg,
533 jump_on_definition: BranchOffset::Offset(0),
534 start_offset: coroutine_implementation_start,
535 });
536 program.preassign_label_to_next_insn(loop_start);
537 // A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor,
538 // it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row.
539 // When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack,
540 // which in this case is the end of the Yield-Goto loop in the parent query.
541 program.emit_insn(Insn::Yield {
542 yield_reg,
543 end_offset: loop_end,
544 });
545 }
546 Table::Pseudo(_) => panic!("Pseudo tables should not loop"),
547 }
548
549 if let Some(table_cursor_id) = table_cursor_id {
550 if let Some(index_cursor_id) = index_cursor_id {
551 program.emit_insn(Insn::DeferredSeek {
552 index_cursor_id,
553 table_cursor_id,
554 });
555 }
556 }
557
558 for cond in predicates
559 .iter()
560 .filter(|cond| cond.should_eval_at_loop(join_index, join_order))
561 {
562 let jump_target_when_true = program.allocate_label();
563 let condition_metadata = ConditionMetadata {
564 jump_if_condition_is_true: false,
565 jump_target_when_true,
566 jump_target_when_false: next,
567 };
568 translate_condition_expr(
569 program,
570 table_references,
571 &cond.expr,
572 condition_metadata,
573 &t_ctx.resolver,
574 )?;
575 program.preassign_label_to_next_insn(jump_target_when_true);
576 }
577 }
578 Operation::Search(search) => {
579 assert!(
580 !matches!(table.table, Table::FromClauseSubquery(_)),
581 "Subqueries do not support index seeks"
582 );
583 // Open the loop for the index search.
584 // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, since it is a single row lookup.
585 if let Search::RowidEq { cmp_expr } = search {
586 let src_reg = program.alloc_register();
587 translate_expr(
588 program,
589 Some(table_references),
590 cmp_expr,
591 src_reg,
592 &t_ctx.resolver,
593 )?;
594 program.emit_insn(Insn::SeekRowid {
595 cursor_id: table_cursor_id
596 .expect("Search::RowidEq requires a table cursor"),
597 src_reg,
598 target_pc: next,
599 });
600 } else {
601 // Otherwise, it's an index/rowid scan, i.e. first a seek is performed and then a scan until the comparison expression is not satisfied anymore.
602 if let Search::Seek {
603 index: Some(index), ..
604 } = search
605 {
606 if index.ephemeral {
607 let table_has_rowid = if let Table::BTree(btree) = &table.table {
608 btree.has_rowid
609 } else {
610 false
611 };
612 Some(emit_autoindex(
613 program,
614 &index,
615 table_cursor_id
616 .expect("an ephemeral index must have a source table cursor"),
617 index_cursor_id
618 .expect("an ephemeral index must have an index cursor"),
619 table_has_rowid,
620 )?)
621 } else {
622 index_cursor_id
623 }
624 } else {
625 index_cursor_id
626 };
627
628 let is_index = index_cursor_id.is_some();
629 let seek_cursor_id = index_cursor_id.unwrap_or_else(|| {
630 table_cursor_id.expect("Either index or table cursor must be opened")
631 });
632 let Search::Seek { seek_def, .. } = search else {
633 unreachable!("Rowid equality point lookup should have been handled above");
634 };
635
636 let start_reg = program.alloc_registers(seek_def.key.len());
637 emit_seek(
638 program,
639 table_references,
640 seek_def,
641 t_ctx,
642 seek_cursor_id,
643 start_reg,
644 loop_end,
645 is_index,
646 )?;
647 emit_seek_termination(
648 program,
649 table_references,
650 seek_def,
651 t_ctx,
652 seek_cursor_id,
653 start_reg,
654 loop_start,
655 loop_end,
656 is_index,
657 )?;
658
659 if let Some(index_cursor_id) = index_cursor_id {
660 if let Some(table_cursor_id) = table_cursor_id {
661 // Don't do a btree table seek until it's actually necessary to read from the table.
662 program.emit_insn(Insn::DeferredSeek {
663 index_cursor_id,
664 table_cursor_id,
665 });
666 }
667 }
668 }
669
670 for cond in predicates
671 .iter()
672 .filter(|cond| cond.should_eval_at_loop(join_index, join_order))
673 {
674 let jump_target_when_true = program.allocate_label();
675 let condition_metadata = ConditionMetadata {
676 jump_if_condition_is_true: false,
677 jump_target_when_true,
678 jump_target_when_false: next,
679 };
680 translate_condition_expr(
681 program,
682 table_references,
683 &cond.expr,
684 condition_metadata,
685 &t_ctx.resolver,
686 )?;
687 program.preassign_label_to_next_insn(jump_target_when_true);
688 }
689 }
690 }
691
692 // Set the match flag to true if this is a LEFT JOIN.
693 // At this point of execution we are going to emit columns for the left table,
694 // and either emit columns or NULLs for the right table, depending on whether the null_flag is set
695 // for the right table's cursor.
696 if let Some(join_info) = table.join_info.as_ref() {
697 if join_info.outer {
698 let lj_meta = t_ctx.meta_left_joins[joined_table_index].as_ref().unwrap();
699 program.resolve_label(lj_meta.label_match_flag_set_true, program.offset());
700 program.emit_insn(Insn::Integer {
701 value: 1,
702 dest: lj_meta.reg_match_flag,
703 });
704 }
705 }
706 }
707
708 Ok(())
709}
710
711/// SQLite (and so Limbo) processes joins as a nested loop.
712/// The loop may emit rows to various destinations depending on the query:
713/// - a GROUP BY sorter (grouping is done by sorting based on the GROUP BY keys and aggregating while the GROUP BY keys match)
714/// - a GROUP BY phase with no sorting (when the rows are already in the order required by the GROUP BY keys)
715/// - an ORDER BY sorter (when there is no GROUP BY, but there is an ORDER BY)
716/// - an AggStep (the columns are collected for aggregation, which is finished later)
717/// - a QueryResult (there is none of the above, so the loop either emits a ResultRow, or if it's a subquery, yields to the parent query)
718enum LoopEmitTarget {
719 GroupBy,
720 OrderBySorter,
721 AggStep,
722 QueryResult,
723}
724
725/// Emits the bytecode for the inner loop of a query.
726/// At this point the cursors for all tables have been opened and rewound.
727pub fn emit_loop<'a>(
728 program: &mut ProgramBuilder,
729 t_ctx: &mut TranslateCtx<'a>,
730 plan: &'a SelectPlan,
731) -> Result<()> {
732 // if we have a group by, we emit a record into the group by sorter,
733 // or if the rows are already sorted, we do the group by aggregation phase directly.
734 if plan.group_by.is_some() {
735 return emit_loop_source(program, t_ctx, plan, LoopEmitTarget::GroupBy);
736 }
737 // if we DONT have a group by, but we have aggregates, we emit without ResultRow.
738 // we also do not need to sort because we are emitting a single row.
739 if !plan.aggregates.is_empty() {
740 return emit_loop_source(program, t_ctx, plan, LoopEmitTarget::AggStep);
741 }
742 // if we DONT have a group by, but we have an order by, we emit a record into the order by sorter.
743 if plan.order_by.is_some() {
744 return emit_loop_source(program, t_ctx, plan, LoopEmitTarget::OrderBySorter);
745 }
746 // if we have neither, we emit a ResultRow. In that case, if we have a Limit, we handle that with DecrJumpZero.
747 emit_loop_source(program, t_ctx, plan, LoopEmitTarget::QueryResult)
748}
749
750/// This is a helper function for inner_loop_emit,
751/// which does a different thing depending on the emit target.
752/// See the InnerLoopEmitTarget enum for more details.
753fn emit_loop_source<'a>(
754 program: &mut ProgramBuilder,
755 t_ctx: &mut TranslateCtx<'a>,
756 plan: &'a SelectPlan,
757 emit_target: LoopEmitTarget,
758) -> Result<()> {
759 match emit_target {
760 LoopEmitTarget::GroupBy => {
761 // This function either:
762 // - creates a sorter for GROUP BY operations by allocating registers and translating expressions for three types of columns:
763 // 1) GROUP BY columns (used as sorting keys)
764 // 2) non-aggregate, non-GROUP BY columns
765 // 3) aggregate function arguments
766 // - or if the rows produced by the loop are already sorted in the order required by the GROUP BY keys,
767 // the group by comparisons are done directly inside the main loop.
768 let group_by = plan.group_by.as_ref().unwrap();
769 let aggregates = &plan.aggregates;
770
771 let GroupByMetadata {
772 row_source,
773 registers,
774 ..
775 } = t_ctx.meta_group_by.as_ref().unwrap();
776
777 let start_reg = registers.reg_group_by_source_cols_start;
778 let mut cur_reg = start_reg;
779
780 // Step 1: Process GROUP BY columns first
781 // These will be the first columns in the sorter and serve as sort keys
782 for expr in group_by.exprs.iter() {
783 let key_reg = cur_reg;
784 cur_reg += 1;
785 translate_expr(
786 program,
787 Some(&plan.table_references),
788 expr,
789 key_reg,
790 &t_ctx.resolver,
791 )?;
792 }
793
794 // Step 2: Process columns that aren't part of GROUP BY and don't contain aggregates
795 // Example: SELECT col1, col2, SUM(col3) FROM table GROUP BY col1
796 // Here col2 would be processed in this loop if it's in the result set
797 for expr in plan.non_group_by_non_agg_columns() {
798 let key_reg = cur_reg;
799 cur_reg += 1;
800 translate_expr(
801 program,
802 Some(&plan.table_references),
803 expr,
804 key_reg,
805 &t_ctx.resolver,
806 )?;
807 }
808
809 // Step 3: Process arguments for all aggregate functions
810 // For each aggregate, translate all its argument expressions
811 for agg in aggregates.iter() {
812 // For a query like: SELECT group_col, SUM(val1), AVG(val2) FROM table GROUP BY group_col
813 // we'll process val1 and val2 here, storing them in the sorter so they're available
814 // when computing the aggregates after sorting by group_col
815 for expr in agg.args.iter() {
816 let agg_reg = cur_reg;
817 cur_reg += 1;
818 translate_expr(
819 program,
820 Some(&plan.table_references),
821 expr,
822 agg_reg,
823 &t_ctx.resolver,
824 )?;
825 }
826 }
827
828 match row_source {
829 GroupByRowSource::Sorter {
830 sort_cursor,
831 sorter_column_count,
832 reg_sorter_key,
833 ..
834 } => {
835 sorter_insert(
836 program,
837 start_reg,
838 *sorter_column_count,
839 *sort_cursor,
840 *reg_sorter_key,
841 );
842 }
843 GroupByRowSource::MainLoop { .. } => group_by_agg_phase(program, t_ctx, plan)?,
844 }
845
846 Ok(())
847 }
848 LoopEmitTarget::OrderBySorter => {
849 order_by_sorter_insert(
850 program,
851 &t_ctx.resolver,
852 t_ctx
853 .meta_sort
854 .as_ref()
855 .expect("sort metadata must exist for ORDER BY"),
856 &mut t_ctx.result_column_indexes_in_orderby_sorter,
857 plan,
858 )?;
859
860 if let Distinctness::Distinct { ctx } = &plan.distinctness {
861 let distinct_ctx = ctx.as_ref().expect("distinct context must exist");
862 program.preassign_label_to_next_insn(distinct_ctx.label_on_conflict);
863 }
864
865 Ok(())
866 }
867 LoopEmitTarget::AggStep => {
868 let start_reg = t_ctx
869 .reg_agg_start
870 .expect("aggregate registers must be initialized");
871
872 // In planner.rs, we have collected all aggregates from the SELECT clause, including ones where the aggregate is embedded inside
873 // a more complex expression. Some examples: length(sum(x)), sum(x) + avg(y), sum(x) + 1, etc.
874 // The result of those more complex expressions depends on the final result of the aggregate, so we don't translate the complete expressions here.
875 // Instead, we accumulate the intermediate results of all aggreagates, and evaluate any expressions that do not contain aggregates.
876 for (i, agg) in plan.aggregates.iter().enumerate() {
877 let reg = start_reg + i;
878 translate_aggregation_step(
879 program,
880 &plan.table_references,
881 agg,
882 reg,
883 &t_ctx.resolver,
884 )?;
885 if let Distinctness::Distinct { ctx } = &agg.distinctness {
886 let ctx = ctx
887 .as_ref()
888 .expect("distinct aggregate context not populated");
889 program.preassign_label_to_next_insn(ctx.label_on_conflict);
890 }
891 }
892
893 let label_emit_nonagg_only_once = if let Some(flag) = t_ctx.reg_nonagg_emit_once_flag {
894 let if_label = program.allocate_label();
895 program.emit_insn(Insn::If {
896 reg: flag,
897 target_pc: if_label,
898 jump_if_null: false,
899 });
900 Some(if_label)
901 } else {
902 None
903 };
904
905 let col_start = t_ctx.reg_result_cols_start.unwrap();
906
907 // Process only non-aggregate columns
908 let non_agg_columns = plan
909 .result_columns
910 .iter()
911 .enumerate()
912 .filter(|(_, rc)| !rc.contains_aggregates);
913
914 for (i, rc) in non_agg_columns {
915 let reg = col_start + i;
916
917 translate_expr(
918 program,
919 Some(&plan.table_references),
920 &rc.expr,
921 reg,
922 &t_ctx.resolver,
923 )?;
924 }
925 if let Some(label) = label_emit_nonagg_only_once {
926 program.resolve_label(label, program.offset());
927 let flag = t_ctx.reg_nonagg_emit_once_flag.unwrap();
928 program.emit_int(1, flag);
929 }
930
931 Ok(())
932 }
933 LoopEmitTarget::QueryResult => {
934 assert!(
935 plan.aggregates.is_empty(),
936 "We should not get here with aggregates"
937 );
938 let offset_jump_to = t_ctx
939 .labels_main_loop
940 .first()
941 .map(|l| l.next)
942 .or(t_ctx.label_main_loop_end);
943 emit_select_result(
944 program,
945 &t_ctx.resolver,
946 plan,
947 t_ctx.label_main_loop_end,
948 offset_jump_to,
949 t_ctx.reg_nonagg_emit_once_flag,
950 t_ctx.reg_offset,
951 t_ctx.reg_result_cols_start.unwrap(),
952 t_ctx.limit_ctx,
953 )?;
954
955 if let Distinctness::Distinct { ctx } = &plan.distinctness {
956 let distinct_ctx = ctx.as_ref().expect("distinct context must exist");
957 program.preassign_label_to_next_insn(distinct_ctx.label_on_conflict);
958 }
959
960 Ok(())
961 }
962 }
963}
964
965/// Closes the loop for a given source operator.
966/// For example in the case of a nested table scan, this means emitting the Next instruction
967/// for all tables involved, innermost first.
968pub fn close_loop(
969 program: &mut ProgramBuilder,
970 t_ctx: &mut TranslateCtx,
971 tables: &TableReferences,
972 join_order: &[JoinOrderMember],
973) -> Result<()> {
974 // We close the loops for all tables in reverse order, i.e. innermost first.
975 // OPEN t1
976 // OPEN t2
977 // OPEN t3
978 // <do stuff>
979 // CLOSE t3
980 // CLOSE t2
981 // CLOSE t1
982 for join in join_order.iter().rev() {
983 let table_index = join.original_idx;
984 let table = &tables.joined_tables()[table_index];
985 let loop_labels = *t_ctx
986 .labels_main_loop
987 .get(table_index)
988 .expect("source has no loop labels");
989
990 let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program)?;
991
992 match &table.op {
993 Operation::Scan { iter_dir, .. } => {
994 program.resolve_label(loop_labels.next, program.offset());
995 match &table.table {
996 Table::BTree(_) => {
997 let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
998 table_cursor_id.expect("Either index or table cursor must be opened")
999 });
1000 if *iter_dir == IterationDirection::Backwards {
1001 program.emit_insn(Insn::Prev {
1002 cursor_id: iteration_cursor_id,
1003 pc_if_prev: loop_labels.loop_start,
1004 });
1005 } else {
1006 program.emit_insn(Insn::Next {
1007 cursor_id: iteration_cursor_id,
1008 pc_if_next: loop_labels.loop_start,
1009 });
1010 }
1011 }
1012 Table::Virtual(_) => {
1013 program.emit_insn(Insn::VNext {
1014 cursor_id: table_cursor_id
1015 .expect("Virtual tables do not support covering indexes"),
1016 pc_if_next: loop_labels.loop_start,
1017 });
1018 }
1019 Table::FromClauseSubquery(_) => {
1020 // A subquery has no cursor to call Next on, so it just emits a Goto
1021 // to the Yield instruction, which in turn jumps back to the main loop of the subquery,
1022 // so that the next row from the subquery can be read.
1023 program.emit_insn(Insn::Goto {
1024 target_pc: loop_labels.loop_start,
1025 });
1026 }
1027 other => unreachable!("Unsupported table reference type: {:?}", other),
1028 }
1029 program.preassign_label_to_next_insn(loop_labels.loop_end);
1030 }
1031 Operation::Search(search) => {
1032 assert!(
1033 !matches!(table.table, Table::FromClauseSubquery(_)),
1034 "Subqueries do not support index seeks"
1035 );
1036 program.resolve_label(loop_labels.next, program.offset());
1037 let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
1038 table_cursor_id.expect("Either index or table cursor must be opened")
1039 });
1040 // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a Next instruction.
1041 if !matches!(search, Search::RowidEq { .. }) {
1042 let iter_dir = match search {
1043 Search::Seek { seek_def, .. } => seek_def.iter_dir,
1044 Search::RowidEq { .. } => unreachable!(),
1045 };
1046
1047 if iter_dir == IterationDirection::Backwards {
1048 program.emit_insn(Insn::Prev {
1049 cursor_id: iteration_cursor_id,
1050 pc_if_prev: loop_labels.loop_start,
1051 });
1052 } else {
1053 program.emit_insn(Insn::Next {
1054 cursor_id: iteration_cursor_id,
1055 pc_if_next: loop_labels.loop_start,
1056 });
1057 }
1058 }
1059 program.preassign_label_to_next_insn(loop_labels.loop_end);
1060 }
1061 }
1062
1063 // Handle OUTER JOIN logic. The reason this comes after the "loop end" mark is that we may need to still jump back
1064 // and emit a row with NULLs for the right table, and then jump back to the next row of the left table.
1065 if let Some(join_info) = table.join_info.as_ref() {
1066 if join_info.outer {
1067 let lj_meta = t_ctx.meta_left_joins[table_index].as_ref().unwrap();
1068 // The left join match flag is set to 1 when there is any match on the right table
1069 // (e.g. SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a).
1070 // If the left join match flag has been set to 1, we jump to the next row on the outer table,
1071 // i.e. continue to the next row of t1 in our example.
1072 program.resolve_label(lj_meta.label_match_flag_check_value, program.offset());
1073 let label_when_right_table_notnull = program.allocate_label();
1074 program.emit_insn(Insn::IfPos {
1075 reg: lj_meta.reg_match_flag,
1076 target_pc: label_when_right_table_notnull,
1077 decrement_by: 0,
1078 });
1079 // If the left join match flag is still 0, it means there was no match on the right table,
1080 // but since it's a LEFT JOIN, we still need to emit a row with NULLs for the right table.
1081 // In that case, we now enter the routine that does exactly that.
1082 // First we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL.
1083 // This needs to be set for both the table and the index cursor, if present,
1084 // since even if the iteration cursor is the index cursor, it might fetch values from the table cursor.
1085 [table_cursor_id, index_cursor_id]
1086 .iter()
1087 .filter_map(|maybe_cursor_id| maybe_cursor_id.as_ref())
1088 .for_each(|cursor_id| {
1089 program.emit_insn(Insn::NullRow {
1090 cursor_id: *cursor_id,
1091 });
1092 });
1093 // Then we jump to setting the left join match flag to 1 again,
1094 // but this time the right table cursor will set everything to null.
1095 // This leads to emitting a row with cols from the left + nulls from the right,
1096 // and we will end up back in the IfPos instruction above, which will then
1097 // check the match flag again, and since it is now 1, we will jump to the
1098 // next row in the left table.
1099 program.emit_insn(Insn::Goto {
1100 target_pc: lj_meta.label_match_flag_set_true,
1101 });
1102 program.preassign_label_to_next_insn(label_when_right_table_notnull);
1103 }
1104 }
1105 }
1106 Ok(())
1107}
1108
1109/// Emits instructions for an index seek. See e.g. [crate::translate::plan::SeekDef]
1110/// for more details about the seek definition.
1111///
1112/// Index seeks always position the cursor to the first row that matches the seek key,
1113/// and then continue to emit rows until the termination condition is reached,
1114/// see [emit_seek_termination] below.
1115///
1116/// If either 1. the seek finds no rows or 2. the termination condition is reached,
1117/// the loop for that given table/index is fully exited.
1118#[allow(clippy::too_many_arguments)]
1119fn emit_seek(
1120 program: &mut ProgramBuilder,
1121 tables: &TableReferences,
1122 seek_def: &SeekDef,
1123 t_ctx: &mut TranslateCtx,
1124 seek_cursor_id: usize,
1125 start_reg: usize,
1126 loop_end: BranchOffset,
1127 is_index: bool,
1128) -> Result<()> {
1129 let Some(seek) = seek_def.seek.as_ref() else {
1130 // If there is no seek key, we start from the first or last row of the index,
1131 // depending on the iteration direction.
1132 match seek_def.iter_dir {
1133 IterationDirection::Forwards => {
1134 program.emit_insn(Insn::Rewind {
1135 cursor_id: seek_cursor_id,
1136 pc_if_empty: loop_end,
1137 });
1138 }
1139 IterationDirection::Backwards => {
1140 program.emit_insn(Insn::Last {
1141 cursor_id: seek_cursor_id,
1142 pc_if_empty: loop_end,
1143 });
1144 }
1145 }
1146 return Ok(());
1147 };
1148 // We allocated registers for the full index key, but our seek key might not use the full index key.
1149 // See [crate::translate::optimizer::build_seek_def] for more details about in which cases we do and don't use the full index key.
1150 for i in 0..seek_def.key.len() {
1151 let reg = start_reg + i;
1152 if i >= seek.len {
1153 if seek.null_pad {
1154 program.emit_insn(Insn::Null {
1155 dest: reg,
1156 dest_end: None,
1157 });
1158 }
1159 } else {
1160 let expr = &seek_def.key[i].0;
1161 translate_expr_no_constant_opt(
1162 program,
1163 Some(tables),
1164 &expr,
1165 reg,
1166 &t_ctx.resolver,
1167 NoConstantOptReason::RegisterReuse,
1168 )?;
1169 // If the seek key column is not verifiably non-NULL, we need check whether it is NULL,
1170 // and if so, jump to the loop end.
1171 // This is to avoid returning rows for e.g. SELECT * FROM t WHERE t.x > NULL,
1172 // which would erroneously return all rows from t, as NULL is lower than any non-NULL value in index key comparisons.
1173 if !expr.is_nonnull(tables) {
1174 program.emit_insn(Insn::IsNull {
1175 reg,
1176 target_pc: loop_end,
1177 });
1178 }
1179 }
1180 }
1181 let num_regs = if seek.null_pad {
1182 seek_def.key.len()
1183 } else {
1184 seek.len
1185 };
1186 match seek.op {
1187 SeekOp::GE { eq_only } => program.emit_insn(Insn::SeekGE {
1188 is_index,
1189 cursor_id: seek_cursor_id,
1190 start_reg,
1191 num_regs,
1192 target_pc: loop_end,
1193 eq_only,
1194 }),
1195 SeekOp::GT => program.emit_insn(Insn::SeekGT {
1196 is_index,
1197 cursor_id: seek_cursor_id,
1198 start_reg,
1199 num_regs,
1200 target_pc: loop_end,
1201 }),
1202 SeekOp::LE { eq_only } => program.emit_insn(Insn::SeekLE {
1203 is_index,
1204 cursor_id: seek_cursor_id,
1205 start_reg,
1206 num_regs,
1207 target_pc: loop_end,
1208 eq_only,
1209 }),
1210 SeekOp::LT => program.emit_insn(Insn::SeekLT {
1211 is_index,
1212 cursor_id: seek_cursor_id,
1213 start_reg,
1214 num_regs,
1215 target_pc: loop_end,
1216 }),
1217 };
1218
1219 Ok(())
1220}
1221
1222/// Emits instructions for an index seek termination. See e.g. [crate::translate::plan::SeekDef]
1223/// for more details about the seek definition.
1224///
1225/// Index seeks always position the cursor to the first row that matches the seek key
1226/// (see [emit_seek] above), and then continue to emit rows until the termination condition
1227/// (if any) is reached.
1228///
1229/// If the termination condition is not present, the cursor is fully scanned to the end.
1230#[allow(clippy::too_many_arguments)]
1231fn emit_seek_termination(
1232 program: &mut ProgramBuilder,
1233 tables: &TableReferences,
1234 seek_def: &SeekDef,
1235 t_ctx: &mut TranslateCtx,
1236 seek_cursor_id: usize,
1237 start_reg: usize,
1238 loop_start: BranchOffset,
1239 loop_end: BranchOffset,
1240 is_index: bool,
1241) -> Result<()> {
1242 let Some(termination) = seek_def.termination.as_ref() else {
1243 program.preassign_label_to_next_insn(loop_start);
1244 return Ok(());
1245 };
1246
1247 // How many non-NULL values were used for seeking.
1248 let seek_len = seek_def.seek.as_ref().map_or(0, |seek| seek.len);
1249
1250 // How many values will be used for the termination condition.
1251 let num_regs = if termination.null_pad {
1252 seek_def.key.len()
1253 } else {
1254 termination.len
1255 };
1256 for i in 0..seek_def.key.len() {
1257 let reg = start_reg + i;
1258 let is_last = i == seek_def.key.len() - 1;
1259
1260 // For all index key values apart from the last one, we are guaranteed to use the same values
1261 // as were used for the seek, so we don't need to emit them again.
1262 if i < seek_len && !is_last {
1263 continue;
1264 }
1265 // For the last index key value, we need to emit a NULL if the termination condition is NULL-padded.
1266 // See [SeekKey::null_pad] and [crate::translate::optimizer::build_seek_def] for why this is the case.
1267 if i >= termination.len && !termination.null_pad {
1268 continue;
1269 }
1270 if is_last && termination.null_pad {
1271 program.emit_insn(Insn::Null {
1272 dest: reg,
1273 dest_end: None,
1274 });
1275 // if the seek key is shorter than the termination key, we need to translate the remaining suffix of the termination key.
1276 // if not, we just reuse what was emitted for the seek.
1277 } else if seek_len < termination.len {
1278 translate_expr_no_constant_opt(
1279 program,
1280 Some(tables),
1281 &seek_def.key[i].0,
1282 reg,
1283 &t_ctx.resolver,
1284 NoConstantOptReason::RegisterReuse,
1285 )?;
1286 }
1287 }
1288 program.preassign_label_to_next_insn(loop_start);
1289 let mut rowid_reg = None;
1290 let mut affinity = None;
1291 if !is_index {
1292 rowid_reg = Some(program.alloc_register());
1293 program.emit_insn(Insn::RowId {
1294 cursor_id: seek_cursor_id,
1295 dest: rowid_reg.unwrap(),
1296 });
1297
1298 affinity = if let Some(table_ref) = tables
1299 .joined_tables()
1300 .iter()
1301 .find(|t| t.columns().iter().any(|c| c.is_rowid_alias))
1302 {
1303 if let Some(rowid_col_idx) = table_ref.columns().iter().position(|c| c.is_rowid_alias) {
1304 Some(table_ref.columns()[rowid_col_idx].affinity())
1305 } else {
1306 Some(Affinity::Numeric)
1307 }
1308 } else {
1309 Some(Affinity::Numeric)
1310 };
1311 }
1312 match (is_index, termination.op) {
1313 (true, SeekOp::GE { .. }) => program.emit_insn(Insn::IdxGE {
1314 cursor_id: seek_cursor_id,
1315 start_reg,
1316 num_regs,
1317 target_pc: loop_end,
1318 }),
1319 (true, SeekOp::GT) => program.emit_insn(Insn::IdxGT {
1320 cursor_id: seek_cursor_id,
1321 start_reg,
1322 num_regs,
1323 target_pc: loop_end,
1324 }),
1325 (true, SeekOp::LE { .. }) => program.emit_insn(Insn::IdxLE {
1326 cursor_id: seek_cursor_id,
1327 start_reg,
1328 num_regs,
1329 target_pc: loop_end,
1330 }),
1331 (true, SeekOp::LT) => program.emit_insn(Insn::IdxLT {
1332 cursor_id: seek_cursor_id,
1333 start_reg,
1334 num_regs,
1335 target_pc: loop_end,
1336 }),
1337 (false, SeekOp::GE { .. }) => program.emit_insn(Insn::Ge {
1338 lhs: rowid_reg.unwrap(),
1339 rhs: start_reg,
1340 target_pc: loop_end,
1341 flags: CmpInsFlags::default()
1342 .jump_if_null()
1343 .with_affinity(affinity.unwrap()),
1344 collation: program.curr_collation(),
1345 }),
1346 (false, SeekOp::GT) => program.emit_insn(Insn::Gt {
1347 lhs: rowid_reg.unwrap(),
1348 rhs: start_reg,
1349 target_pc: loop_end,
1350 flags: CmpInsFlags::default()
1351 .jump_if_null()
1352 .with_affinity(affinity.unwrap()),
1353 collation: program.curr_collation(),
1354 }),
1355 (false, SeekOp::LE { .. }) => program.emit_insn(Insn::Le {
1356 lhs: rowid_reg.unwrap(),
1357 rhs: start_reg,
1358 target_pc: loop_end,
1359 flags: CmpInsFlags::default()
1360 .jump_if_null()
1361 .with_affinity(affinity.unwrap()),
1362 collation: program.curr_collation(),
1363 }),
1364 (false, SeekOp::LT) => program.emit_insn(Insn::Lt {
1365 lhs: rowid_reg.unwrap(),
1366 rhs: start_reg,
1367 target_pc: loop_end,
1368 flags: CmpInsFlags::default()
1369 .jump_if_null()
1370 .with_affinity(affinity.unwrap()),
1371 collation: program.curr_collation(),
1372 }),
1373 };
1374
1375 Ok(())
1376}
1377
1378/// Open an ephemeral index cursor and build an automatic index on a table.
1379/// This is used as a last-resort to avoid a nested full table scan
1380/// Returns the cursor id of the ephemeral index cursor.
1381fn emit_autoindex(
1382 program: &mut ProgramBuilder,
1383 index: &Arc<Index>,
1384 table_cursor_id: CursorID,
1385 index_cursor_id: CursorID,
1386 table_has_rowid: bool,
1387) -> Result<CursorID> {
1388 assert!(index.ephemeral, "Index {} is not ephemeral", index.name);
1389 let label_ephemeral_build_end = program.allocate_label();
1390 // Since this typically happens in an inner loop, we only build it once.
1391 program.emit_insn(Insn::Once {
1392 target_pc_when_reentered: label_ephemeral_build_end,
1393 });
1394 program.emit_insn(Insn::OpenAutoindex {
1395 cursor_id: index_cursor_id,
1396 });
1397 // Rewind source table
1398 let label_ephemeral_build_loop_start = program.allocate_label();
1399 program.emit_insn(Insn::Rewind {
1400 cursor_id: table_cursor_id,
1401 pc_if_empty: label_ephemeral_build_loop_start,
1402 });
1403 program.preassign_label_to_next_insn(label_ephemeral_build_loop_start);
1404 // Emit all columns from source table that are needed in the ephemeral index.
1405 // Also reserve a register for the rowid if the source table has rowids.
1406 let num_regs_to_reserve = index.columns.len() + table_has_rowid as usize;
1407 let ephemeral_cols_start_reg = program.alloc_registers(num_regs_to_reserve);
1408 for (i, col) in index.columns.iter().enumerate() {
1409 let reg = ephemeral_cols_start_reg + i;
1410 program.emit_column(table_cursor_id, col.pos_in_table, reg);
1411 }
1412 if table_has_rowid {
1413 program.emit_insn(Insn::RowId {
1414 cursor_id: table_cursor_id,
1415 dest: ephemeral_cols_start_reg + index.columns.len(),
1416 });
1417 }
1418 let record_reg = program.alloc_register();
1419 program.emit_insn(Insn::MakeRecord {
1420 start_reg: ephemeral_cols_start_reg,
1421 count: num_regs_to_reserve,
1422 dest_reg: record_reg,
1423 index_name: Some(index.name.clone()),
1424 });
1425 program.emit_insn(Insn::IdxInsert {
1426 cursor_id: index_cursor_id,
1427 record_reg,
1428 unpacked_start: Some(ephemeral_cols_start_reg),
1429 unpacked_count: Some(num_regs_to_reserve as u16),
1430 flags: IdxInsertFlags::new().use_seek(false),
1431 });
1432 program.emit_insn(Insn::Next {
1433 cursor_id: table_cursor_id,
1434 pc_if_next: label_ephemeral_build_loop_start,
1435 });
1436 program.preassign_label_to_next_insn(label_ephemeral_build_end);
1437 Ok(index_cursor_id)
1438}