diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index 9450fcf55..9cff582d9 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -16,7 +16,8 @@ use super::{ expr::{translate_condition_expr, translate_expr, ConditionMetadata}, order_by::{order_by_sorter_insert, sorter_insert}, plan::{ - IterationDirection, Search, SelectPlan, SelectQueryType, SourceOperator, TableReference, + IterationDirection, JoinAwareConditionExpr, Operation, Search, SelectPlan, SelectQueryType, + TableReference, }, }; @@ -46,118 +47,48 @@ pub struct LoopLabels { pub fn init_loop( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, - source: &SourceOperator, + tables: &[TableReference], mode: &OperationMode, ) -> Result<()> { - let operator_id = source.id(); - let loop_labels = LoopLabels { - next: program.allocate_label(), - loop_start: program.allocate_label(), - loop_end: program.allocate_label(), - }; - t_ctx.labels_main_loop.insert(operator_id, loop_labels); + for (table_index, table) in tables.iter().enumerate() { + let loop_labels = LoopLabels { + next: program.allocate_label(), + loop_start: program.allocate_label(), + loop_end: program.allocate_label(), + }; + t_ctx.labels_main_loop.push(loop_labels); - match source { - SourceOperator::Subquery { .. } => Ok(()), - SourceOperator::Join { - id, - left, - right, - outer, - .. - } => { - if *outer { + // Initialize bookkeeping for OUTER JOIN + if let Some(join_info) = table.join_info.as_ref() { + if join_info.outer { let lj_metadata = LeftJoinMetadata { reg_match_flag: program.alloc_register(), label_match_flag_set_true: program.allocate_label(), label_match_flag_check_value: program.allocate_label(), }; - t_ctx.meta_left_joins.insert(*id, lj_metadata); + t_ctx.meta_left_joins.insert(table_index, lj_metadata); } - init_loop(program, t_ctx, left, mode)?; - init_loop(program, t_ctx, right, mode)?; - - Ok(()) } - SourceOperator::Scan { - table_reference, .. - } => { - let cursor_id = program.alloc_cursor_id( - Some(table_reference.table_identifier.clone()), - CursorType::BTreeTable(table_reference.btree().unwrap().clone()), - ); - let root_page = table_reference.table.get_root_page(); - - match mode { - OperationMode::SELECT => { - program.emit_insn(Insn::OpenReadAsync { - cursor_id, - root_page, - }); - program.emit_insn(Insn::OpenReadAwait {}); - } - OperationMode::DELETE => { - program.emit_insn(Insn::OpenWriteAsync { - cursor_id, - root_page, - }); - program.emit_insn(Insn::OpenWriteAwait {}); - } - _ => { - unimplemented!() - } - } - - Ok(()) - } - SourceOperator::Search { - table_reference, - search, - .. - } => { - let table_cursor_id = program.alloc_cursor_id( - Some(table_reference.table_identifier.clone()), - CursorType::BTreeTable(table_reference.btree().unwrap().clone()), - ); - - match mode { - OperationMode::SELECT => { - program.emit_insn(Insn::OpenReadAsync { - cursor_id: table_cursor_id, - root_page: table_reference.table.get_root_page(), - }); - program.emit_insn(Insn::OpenReadAwait {}); - } - OperationMode::DELETE => { - program.emit_insn(Insn::OpenWriteAsync { - cursor_id: table_cursor_id, - root_page: table_reference.table.get_root_page(), - }); - program.emit_insn(Insn::OpenWriteAwait {}); - } - _ => { - unimplemented!() - } - } - - if let Search::IndexSearch { index, .. } = search { - let index_cursor_id = program.alloc_cursor_id( - Some(index.name.clone()), - CursorType::BTreeIndex(index.clone()), + match &table.op { + Operation::Scan { .. } => { + let cursor_id = program.alloc_cursor_id( + Some(table.identifier.clone()), + CursorType::BTreeTable(table.btree().unwrap().clone()), ); + let root_page = table.table.get_root_page(); match mode { OperationMode::SELECT => { program.emit_insn(Insn::OpenReadAsync { - cursor_id: index_cursor_id, - root_page: index.root_page, + cursor_id, + root_page, }); - program.emit_insn(Insn::OpenReadAwait); + program.emit_insn(Insn::OpenReadAwait {}); } OperationMode::DELETE => { program.emit_insn(Insn::OpenWriteAsync { - cursor_id: index_cursor_id, - root_page: index.root_page, + cursor_id, + root_page, }); program.emit_insn(Insn::OpenWriteAwait {}); } @@ -166,11 +97,64 @@ pub fn init_loop( } } } + Operation::Search(search) => { + let table_cursor_id = program.alloc_cursor_id( + Some(table.identifier.clone()), + CursorType::BTreeTable(table.btree().unwrap().clone()), + ); - Ok(()) + match mode { + OperationMode::SELECT => { + program.emit_insn(Insn::OpenReadAsync { + cursor_id: table_cursor_id, + root_page: table.table.get_root_page(), + }); + program.emit_insn(Insn::OpenReadAwait {}); + } + OperationMode::DELETE => { + program.emit_insn(Insn::OpenWriteAsync { + cursor_id: table_cursor_id, + root_page: table.table.get_root_page(), + }); + program.emit_insn(Insn::OpenWriteAwait {}); + } + _ => { + unimplemented!() + } + } + + if let Search::IndexSearch { index, .. } = search { + let index_cursor_id = program.alloc_cursor_id( + Some(index.name.clone()), + CursorType::BTreeIndex(index.clone()), + ); + + match mode { + OperationMode::SELECT => { + program.emit_insn(Insn::OpenReadAsync { + cursor_id: index_cursor_id, + root_page: index.root_page, + }); + program.emit_insn(Insn::OpenReadAwait); + } + OperationMode::DELETE => { + program.emit_insn(Insn::OpenWriteAsync { + cursor_id: index_cursor_id, + root_page: index.root_page, + }); + program.emit_insn(Insn::OpenWriteAwait {}); + } + _ => { + unimplemented!() + } + } + } + } + _ => {} } - SourceOperator::Nothing { .. } => Ok(()), } + + Ok(()) } /// Set up the main query execution loop @@ -179,52 +163,64 @@ pub fn init_loop( pub fn open_loop( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, - source: &mut SourceOperator, - referenced_tables: &[TableReference], + tables: &[TableReference], + predicates: &[JoinAwareConditionExpr], ) -> Result<()> { - match source { - SourceOperator::Subquery { - id, - predicates, - plan, - .. - } => { - let (yield_reg, coroutine_implementation_start) = match &plan.query_type { - SelectQueryType::Subquery { - yield_reg, - coroutine_implementation_start, - } => (*yield_reg, *coroutine_implementation_start), - _ => unreachable!("Subquery operator with non-subquery query type"), - }; - // In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop. - program.emit_insn(Insn::InitCoroutine { - yield_reg, - jump_on_definition: BranchOffset::Offset(0), - start_offset: coroutine_implementation_start, - }); - let LoopLabels { - loop_start, - loop_end, - next, - } = *t_ctx - .labels_main_loop - .get(id) - .expect("subquery has no loop labels"); - program.resolve_label(loop_start, program.offset()); - // A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor, - // it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row. - // When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack, - // which in this case is the end of the Yield-Goto loop in the parent query. - program.emit_insn(Insn::Yield { - yield_reg, - end_offset: loop_end, - }); + for (table_index, table) in tables.iter().enumerate() { + let LoopLabels { + loop_start, + loop_end, + next, + } = *t_ctx + .labels_main_loop + .get(table_index) + .expect("table has no loop labels"); - // These are predicates evaluated outside of the subquery, - // so they are translated here. - // E.g. SELECT foo FROM (SELECT bar as foo FROM t1) sub WHERE sub.foo > 10 - if let Some(preds) = predicates { - for expr in preds { + // Each OUTER JOIN has a "match flag" that is initially set to false, + // and is set to true when a match is found for the OUTER JOIN. + // This is used to determine whether to emit actual columns or NULLs for the columns of the right table. + if let Some(join_info) = table.join_info.as_ref() { + if join_info.outer { + let lj_meta = t_ctx.meta_left_joins.get(&table_index).unwrap(); + program.emit_insn(Insn::Integer { + value: 0, + dest: lj_meta.reg_match_flag, + }); + } + } + + match &table.op { + Operation::Subquery { plan, .. } => { + let (yield_reg, coroutine_implementation_start) = match &plan.query_type { + SelectQueryType::Subquery { + yield_reg, + coroutine_implementation_start, + } => (*yield_reg, *coroutine_implementation_start), + _ => unreachable!("Subquery operator with non-subquery query type"), + }; + // In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop. + program.emit_insn(Insn::InitCoroutine { + yield_reg, + jump_on_definition: BranchOffset::Offset(0), + start_offset: coroutine_implementation_start, + }); + program.resolve_label(loop_start, program.offset()); + // A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor, + // it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row. + // When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack, + // which in this case is the end of the Yield-Goto loop in the parent query. + program.emit_insn(Insn::Yield { + yield_reg, + end_offset: loop_end, + }); + + // These are predicates evaluated outside of the subquery, + // so they are translated here. + // E.g. SELECT foo FROM (SELECT bar as foo FROM t1) sub WHERE sub.foo > 10 + for cond in predicates + .iter() + .filter(|cond| cond.eval_at_loop == table_index) + { let jump_target_when_true = program.allocate_label(); let condition_metadata = ConditionMetadata { jump_if_condition_is_true: false, @@ -233,325 +229,253 @@ pub fn open_loop( }; translate_condition_expr( program, - referenced_tables, - expr, + tables, + &cond.expr, condition_metadata, &t_ctx.resolver, )?; program.resolve_label(jump_target_when_true, program.offset()); } } + Operation::Scan { iter_dir } => { + let cursor_id = program.resolve_cursor_id(&table.identifier); + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::LastAsync { cursor_id }); + } else { + program.emit_insn(Insn::RewindAsync { cursor_id }); + } + program.emit_insn( + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + Insn::LastAwait { + cursor_id, + pc_if_empty: loop_end, + } + } else { + Insn::RewindAwait { + cursor_id, + pc_if_empty: loop_end, + } + }, + ); + program.resolve_label(loop_start, program.offset()); - Ok(()) - } - SourceOperator::Join { - id, - left, - right, - predicates, - outer, - .. - } => { - open_loop(program, t_ctx, left, referenced_tables)?; - - let LoopLabels { next, .. } = *t_ctx - .labels_main_loop - .get(&right.id()) - .expect("right side of join has no loop labels"); - - let mut jump_target_when_false = next; - - if *outer { - let lj_meta = t_ctx.meta_left_joins.get(id).unwrap(); - program.emit_insn(Insn::Integer { - value: 0, - dest: lj_meta.reg_match_flag, - }); - jump_target_when_false = lj_meta.label_match_flag_check_value; - } - - open_loop(program, t_ctx, right, referenced_tables)?; - - if let Some(predicates) = predicates { - let jump_target_when_true = program.allocate_label(); - let condition_metadata = ConditionMetadata { - jump_if_condition_is_true: false, - jump_target_when_true, - jump_target_when_false, - }; - for predicate in predicates.iter() { + for cond in predicates + .iter() + .filter(|cond| cond.eval_at_loop == table_index) + { + let jump_target_when_true = program.allocate_label(); + let condition_metadata = ConditionMetadata { + jump_if_condition_is_true: false, + jump_target_when_true, + jump_target_when_false: next, + }; translate_condition_expr( program, - referenced_tables, - predicate, + tables, + &cond.expr, condition_metadata, &t_ctx.resolver, )?; + program.resolve_label(jump_target_when_true, program.offset()); } - program.resolve_label(jump_target_when_true, program.offset()); } + Operation::Search(search) => { + let table_cursor_id = program.resolve_cursor_id(&table.identifier); + // Open the loop for the index search. + // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, since it is a single row lookup. + if !matches!(search, Search::RowidEq { .. }) { + let index_cursor_id = if let Search::IndexSearch { index, .. } = search { + Some(program.resolve_cursor_id(&index.name)) + } else { + None + }; + let cmp_reg = program.alloc_register(); + let (cmp_expr, cmp_op) = match search { + Search::IndexSearch { + cmp_expr, cmp_op, .. + } => (cmp_expr, cmp_op), + Search::RowidSearch { cmp_expr, cmp_op } => (cmp_expr, cmp_op), + Search::RowidEq { .. } => unreachable!(), + }; - if *outer { - let lj_meta = t_ctx.meta_left_joins.get(id).unwrap(); + // TODO this only handles ascending indexes + match cmp_op { + ast::Operator::Equals + | ast::Operator::Greater + | ast::Operator::GreaterEquals => { + translate_expr( + program, + Some(tables), + &cmp_expr.expr, + cmp_reg, + &t_ctx.resolver, + )?; + } + ast::Operator::Less | ast::Operator::LessEquals => { + program.emit_insn(Insn::Null { + dest: cmp_reg, + dest_end: None, + }); + } + _ => unreachable!(), + } + // If we try to seek to a key that is not present in the table/index, we exit the loop entirely. + program.emit_insn(match cmp_op { + ast::Operator::Equals | ast::Operator::GreaterEquals => Insn::SeekGE { + is_index: index_cursor_id.is_some(), + cursor_id: index_cursor_id.unwrap_or(table_cursor_id), + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }, + ast::Operator::Greater + | ast::Operator::Less + | ast::Operator::LessEquals => Insn::SeekGT { + is_index: index_cursor_id.is_some(), + cursor_id: index_cursor_id.unwrap_or(table_cursor_id), + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }, + _ => unreachable!(), + }); + if *cmp_op == ast::Operator::Less || *cmp_op == ast::Operator::LessEquals { + translate_expr( + program, + Some(tables), + &cmp_expr.expr, + cmp_reg, + &t_ctx.resolver, + )?; + } + + program.resolve_label(loop_start, program.offset()); + // TODO: We are currently only handling ascending indexes. + // For conditions like index_key > 10, we have already seeked to the first key greater than 10, and can just scan forward. + // For conditions like index_key < 10, we are at the beginning of the index, and will scan forward and emit IdxGE(10) with a conditional jump to the end. + // For conditions like index_key = 10, we have already seeked to the first key greater than or equal to 10, and can just scan forward and emit IdxGT(10) with a conditional jump to the end. + // For conditions like index_key >= 10, we have already seeked to the first key greater than or equal to 10, and can just scan forward. + // For conditions like index_key <= 10, we are at the beginning of the index, and will scan forward and emit IdxGT(10) with a conditional jump to the end. + // For conditions like index_key != 10, TODO. probably the optimal way is not to use an index at all. + // + // For primary key searches we emit RowId and then compare it to the seek value. + + match cmp_op { + ast::Operator::Equals | ast::Operator::LessEquals => { + if let Some(index_cursor_id) = index_cursor_id { + program.emit_insn(Insn::IdxGT { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn(Insn::Gt { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: loop_end, + flags: CmpInsFlags::default(), + }); + } + } + ast::Operator::Less => { + if let Some(index_cursor_id) = index_cursor_id { + program.emit_insn(Insn::IdxGE { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: loop_end, + }); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn(Insn::Ge { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: loop_end, + flags: CmpInsFlags::default(), + }); + } + } + _ => {} + } + + if let Some(index_cursor_id) = index_cursor_id { + program.emit_insn(Insn::DeferredSeek { + index_cursor_id, + table_cursor_id, + }); + } + } + + if let Search::RowidEq { cmp_expr } = search { + let src_reg = program.alloc_register(); + translate_expr( + program, + Some(tables), + &cmp_expr.expr, + src_reg, + &t_ctx.resolver, + )?; + program.emit_insn(Insn::SeekRowid { + cursor_id: table_cursor_id, + src_reg, + target_pc: next, + }); + } + for cond in predicates + .iter() + .filter(|cond| cond.eval_at_loop == table_index) + { + let jump_target_when_true = program.allocate_label(); + let condition_metadata = ConditionMetadata { + jump_if_condition_is_true: false, + jump_target_when_true, + jump_target_when_false: next, + }; + translate_condition_expr( + program, + tables, + &cond.expr, + condition_metadata, + &t_ctx.resolver, + )?; + program.resolve_label(jump_target_when_true, program.offset()); + } + } + } + + // Set the match flag to true if this is a LEFT JOIN. + // At this point of execution we are going to emit columns for the left table, + // and either emit columns or NULLs for the right table, depending on whether the null_flag is set + // for the right table's cursor. + if let Some(join_info) = table.join_info.as_ref() { + if join_info.outer { + let lj_meta = t_ctx.meta_left_joins.get(&table_index).unwrap(); program.resolve_label(lj_meta.label_match_flag_set_true, program.offset()); program.emit_insn(Insn::Integer { value: 1, dest: lj_meta.reg_match_flag, }); } - - Ok(()) } - SourceOperator::Scan { - id, - table_reference, - predicates, - iter_dir, - } => { - let cursor_id = program.resolve_cursor_id(&table_reference.table_identifier); - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::LastAsync { cursor_id }); - } else { - program.emit_insn(Insn::RewindAsync { cursor_id }); - } - let LoopLabels { - loop_start, - loop_end, - next, - } = *t_ctx - .labels_main_loop - .get(id) - .expect("scan has no loop labels"); - program.emit_insn( - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - Insn::LastAwait { - cursor_id, - pc_if_empty: loop_end, - } - } else { - Insn::RewindAwait { - cursor_id, - pc_if_empty: loop_end, - } - }, - ); - program.resolve_label(loop_start, program.offset()); - - if let Some(preds) = predicates { - for expr in preds { - let jump_target_when_true = program.allocate_label(); - let condition_metadata = ConditionMetadata { - jump_if_condition_is_true: false, - jump_target_when_true, - jump_target_when_false: next, - }; - translate_condition_expr( - program, - referenced_tables, - expr, - condition_metadata, - &t_ctx.resolver, - )?; - program.resolve_label(jump_target_when_true, program.offset()); - } - } - - Ok(()) - } - SourceOperator::Search { - id, - table_reference, - search, - predicates, - .. - } => { - let table_cursor_id = program.resolve_cursor_id(&table_reference.table_identifier); - let LoopLabels { - loop_start, - loop_end, - next, - } = *t_ctx - .labels_main_loop - .get(id) - .expect("search has no loop labels"); - // Open the loop for the index search. - // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, since it is a single row lookup. - if !matches!(search, Search::RowidEq { .. }) { - let index_cursor_id = if let Search::IndexSearch { index, .. } = search { - Some(program.resolve_cursor_id(&index.name)) - } else { - None - }; - let cmp_reg = program.alloc_register(); - let (cmp_expr, cmp_op) = match search { - Search::IndexSearch { - cmp_expr, cmp_op, .. - } => (cmp_expr, cmp_op), - Search::RowidSearch { cmp_expr, cmp_op } => (cmp_expr, cmp_op), - Search::RowidEq { .. } => unreachable!(), - }; - // TODO this only handles ascending indexes - match cmp_op { - ast::Operator::Equals - | ast::Operator::Greater - | ast::Operator::GreaterEquals => { - translate_expr( - program, - Some(referenced_tables), - cmp_expr, - cmp_reg, - &t_ctx.resolver, - )?; - } - ast::Operator::Less | ast::Operator::LessEquals => { - program.emit_insn(Insn::Null { - dest: cmp_reg, - dest_end: None, - }); - } - _ => unreachable!(), - } - // If we try to seek to a key that is not present in the table/index, we exit the loop entirely. - program.emit_insn(match cmp_op { - ast::Operator::Equals | ast::Operator::GreaterEquals => Insn::SeekGE { - is_index: index_cursor_id.is_some(), - cursor_id: index_cursor_id.unwrap_or(table_cursor_id), - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }, - ast::Operator::Greater | ast::Operator::Less | ast::Operator::LessEquals => { - Insn::SeekGT { - is_index: index_cursor_id.is_some(), - cursor_id: index_cursor_id.unwrap_or(table_cursor_id), - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - } - } - _ => unreachable!(), - }); - if *cmp_op == ast::Operator::Less || *cmp_op == ast::Operator::LessEquals { - translate_expr( - program, - Some(referenced_tables), - cmp_expr, - cmp_reg, - &t_ctx.resolver, - )?; - } - - program.resolve_label(loop_start, program.offset()); - // TODO: We are currently only handling ascending indexes. - // For conditions like index_key > 10, we have already seeked to the first key greater than 10, and can just scan forward. - // For conditions like index_key < 10, we are at the beginning of the index, and will scan forward and emit IdxGE(10) with a conditional jump to the end. - // For conditions like index_key = 10, we have already seeked to the first key greater than or equal to 10, and can just scan forward and emit IdxGT(10) with a conditional jump to the end. - // For conditions like index_key >= 10, we have already seeked to the first key greater than or equal to 10, and can just scan forward. - // For conditions like index_key <= 10, we are at the beginning of the index, and will scan forward and emit IdxGT(10) with a conditional jump to the end. - // For conditions like index_key != 10, TODO. probably the optimal way is not to use an index at all. - // - // For primary key searches we emit RowId and then compare it to the seek value. - - match cmp_op { - ast::Operator::Equals | ast::Operator::LessEquals => { - if let Some(index_cursor_id) = index_cursor_id { - program.emit_insn(Insn::IdxGT { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }); - } else { - let rowid_reg = program.alloc_register(); - program.emit_insn(Insn::RowId { - cursor_id: table_cursor_id, - dest: rowid_reg, - }); - program.emit_insn(Insn::Gt { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: loop_end, - flags: CmpInsFlags::default(), - }); - } - } - ast::Operator::Less => { - if let Some(index_cursor_id) = index_cursor_id { - program.emit_insn(Insn::IdxGE { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: loop_end, - }); - } else { - let rowid_reg = program.alloc_register(); - program.emit_insn(Insn::RowId { - cursor_id: table_cursor_id, - dest: rowid_reg, - }); - program.emit_insn(Insn::Ge { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: loop_end, - flags: CmpInsFlags::default(), - }); - } - } - _ => {} - } - - if let Some(index_cursor_id) = index_cursor_id { - program.emit_insn(Insn::DeferredSeek { - index_cursor_id, - table_cursor_id, - }); - } - } - - if let Search::RowidEq { cmp_expr } = search { - let src_reg = program.alloc_register(); - translate_expr( - program, - Some(referenced_tables), - cmp_expr, - src_reg, - &t_ctx.resolver, - )?; - program.emit_insn(Insn::SeekRowid { - cursor_id: table_cursor_id, - src_reg, - target_pc: next, - }); - } - if let Some(predicates) = predicates { - for predicate in predicates.iter() { - let jump_target_when_true = program.allocate_label(); - let condition_metadata = ConditionMetadata { - jump_if_condition_is_true: false, - jump_target_when_true, - jump_target_when_false: next, - }; - translate_condition_expr( - program, - referenced_tables, - predicate, - condition_metadata, - &t_ctx.resolver, - )?; - program.resolve_label(jump_target_when_true, program.offset()); - } - } - - Ok(()) - } - SourceOperator::Nothing { .. } => Ok(()), } + + Ok(()) } /// SQLite (and so Limbo) processes joins as a nested loop. @@ -620,7 +544,7 @@ fn emit_loop_source( cur_reg += 1; translate_expr( program, - Some(&plan.referenced_tables), + Some(&plan.table_references), expr, key_reg, &t_ctx.resolver, @@ -639,7 +563,7 @@ fn emit_loop_source( cur_reg += 1; translate_expr( program, - Some(&plan.referenced_tables), + Some(&plan.table_references), expr, agg_reg, &t_ctx.resolver, @@ -676,7 +600,7 @@ fn emit_loop_source( let reg = start_reg + i; translate_aggregation_step( program, - &plan.referenced_tables, + &plan.table_references, agg, reg, &t_ctx.resolver, @@ -692,7 +616,7 @@ fn emit_loop_source( let reg = start_reg + num_aggs + i; translate_expr( program, - Some(&plan.referenced_tables), + Some(&plan.table_references), &rc.expr, reg, &t_ctx.resolver, @@ -705,16 +629,17 @@ fn emit_loop_source( plan.aggregates.is_empty(), "We should not get here with aggregates" ); - let loop_labels = *t_ctx + let offset_jump_to = t_ctx .labels_main_loop - .get(&plan.source.id()) - .expect("source has no loop labels"); + .get(0) + .map(|l| l.next) + .or_else(|| t_ctx.label_main_loop_end); emit_select_result( program, t_ctx, plan, t_ctx.label_main_loop_end, - Some(loop_labels.next), + offset_jump_to, )?; Ok(()) @@ -728,33 +653,85 @@ fn emit_loop_source( pub fn close_loop( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, - source: &SourceOperator, + tables: &[TableReference], ) -> Result<()> { - let loop_labels = *t_ctx - .labels_main_loop - .get(&source.id()) - .expect("source has no loop labels"); - match source { - SourceOperator::Subquery { .. } => { - program.resolve_label(loop_labels.next, program.offset()); - // A subquery has no cursor to call NextAsync on, so it just emits a Goto - // to the Yield instruction, which in turn jumps back to the main loop of the subquery, - // so that the next row from the subquery can be read. - program.emit_insn(Insn::Goto { - target_pc: loop_labels.loop_start, - }); - } - SourceOperator::Join { - id, - left, - right, - outer, - .. - } => { - close_loop(program, t_ctx, right)?; + // We close the loops for all tables in reverse order, i.e. innermost first. + // OPEN t1 + // OPEN t2 + // OPEN t3 + // + // CLOSE t3 + // CLOSE t2 + // CLOSE t1 + for (idx, table) in tables.iter().rev().enumerate() { + let table_index = tables.len() - idx - 1; + let loop_labels = *t_ctx + .labels_main_loop + .get(table_index) + .expect("source has no loop labels"); - if *outer { - let lj_meta = t_ctx.meta_left_joins.get(id).unwrap(); + match &table.op { + Operation::Subquery { .. } => { + program.resolve_label(loop_labels.next, program.offset()); + // A subquery has no cursor to call NextAsync on, so it just emits a Goto + // to the Yield instruction, which in turn jumps back to the main loop of the subquery, + // so that the next row from the subquery can be read. + program.emit_insn(Insn::Goto { + target_pc: loop_labels.loop_start, + }); + } + Operation::Scan { iter_dir, .. } => { + program.resolve_label(loop_labels.next, program.offset()); + let cursor_id = program.resolve_cursor_id(&table.identifier); + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::PrevAsync { cursor_id }); + } else { + program.emit_insn(Insn::NextAsync { cursor_id }); + } + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::PrevAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } else { + program.emit_insn(Insn::NextAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } + } + Operation::Search(search) => { + program.resolve_label(loop_labels.next, program.offset()); + // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a NextAsync instruction. + if !matches!(search, Search::RowidEq { .. }) { + let cursor_id = match search { + Search::IndexSearch { index, .. } => program.resolve_cursor_id(&index.name), + Search::RowidSearch { .. } => program.resolve_cursor_id(&table.identifier), + Search::RowidEq { .. } => unreachable!(), + }; + + program.emit_insn(Insn::NextAsync { cursor_id }); + program.emit_insn(Insn::NextAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } + } + } + + program.resolve_label(loop_labels.loop_end, program.offset()); + + // Handle OUTER JOIN logic. The reason this comes after the "loop end" mark is that we may need to still jump back + // and emit a row with NULLs for the right table, and then jump back to the next row of the left table. + if let Some(join_info) = table.join_info.as_ref() { + if join_info.outer { + let lj_meta = t_ctx.meta_left_joins.get(&table_index).unwrap(); // The left join match flag is set to 1 when there is any match on the right table // (e.g. SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a). // If the left join match flag has been set to 1, we jump to the next row on the outer table, @@ -770,13 +747,9 @@ pub fn close_loop( // but since it's a LEFT JOIN, we still need to emit a row with NULLs for the right table. // In that case, we now enter the routine that does exactly that. // First we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL - let right_cursor_id = match right.as_ref() { - SourceOperator::Scan { - table_reference, .. - } => program.resolve_cursor_id(&table_reference.table_identifier), - SourceOperator::Search { - table_reference, .. - } => program.resolve_cursor_id(&table_reference.table_identifier), + let right_cursor_id = match &table.op { + Operation::Scan { .. } => program.resolve_cursor_id(&table.identifier), + Operation::Search { .. } => program.resolve_cursor_id(&table.identifier), _ => unreachable!(), }; program.emit_insn(Insn::NullRow { @@ -794,66 +767,7 @@ pub fn close_loop( assert_eq!(program.offset(), jump_offset); } - - close_loop(program, t_ctx, left)?; } - SourceOperator::Scan { - table_reference, - iter_dir, - .. - } => { - program.resolve_label(loop_labels.next, program.offset()); - let cursor_id = program.resolve_cursor_id(&table_reference.table_identifier); - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::PrevAsync { cursor_id }); - } else { - program.emit_insn(Insn::NextAsync { cursor_id }); - } - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::PrevAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }); - } else { - program.emit_insn(Insn::NextAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }); - } - } - SourceOperator::Search { - table_reference, - search, - .. - } => { - program.resolve_label(loop_labels.next, program.offset()); - if matches!(search, Search::RowidEq { .. }) { - // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a NextAsync instruction. - return Ok(()); - } - let cursor_id = match search { - Search::IndexSearch { index, .. } => program.resolve_cursor_id(&index.name), - Search::RowidSearch { .. } => { - program.resolve_cursor_id(&table_reference.table_identifier) - } - Search::RowidEq { .. } => unreachable!(), - }; - - program.emit_insn(Insn::NextAsync { cursor_id }); - program.emit_insn(Insn::NextAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }); - } - SourceOperator::Nothing { .. } => {} - }; - - program.resolve_label(loop_labels.loop_end, program.offset()); + } Ok(()) }