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}