From f02da18acdd85f28652ee66a829461f6c09e4c6e Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 29 Sep 2024 19:13:50 +0300 Subject: [PATCH 01/32] index scan wip foo doesnt work yet --- core/pseudo.rs | 8 ++ core/schema.rs | 14 ++- core/storage/btree.rs | 176 ++++++++++++++++++++++++++++++- core/translate/emitter.rs | 199 +++++++++++++++++++++++++++++++++--- core/translate/insert.rs | 1 + core/translate/optimizer.rs | 116 +++++++++++++++++++-- core/translate/plan.rs | 35 ++++++- core/translate/planner.rs | 9 ++ core/types.rs | 2 + core/vdbe/builder.rs | 16 +++ core/vdbe/explain.rs | 68 ++++++++++++ core/vdbe/mod.rs | 168 +++++++++++++++++++++++++++++- core/vdbe/sorter.rs | 8 ++ testing/testing.db | Bin 1118208 -> 1212416 bytes 14 files changed, 792 insertions(+), 28 deletions(-) diff --git a/core/pseudo.rs b/core/pseudo.rs index 431fc83d1..78a80afc6 100644 --- a/core/pseudo.rs +++ b/core/pseudo.rs @@ -52,6 +52,14 @@ impl Cursor for PseudoCursor { unimplemented!(); } + fn seek_ge(&mut self, _: &OwnedRecord) -> Result> { + unimplemented!(); + } + + fn seek_gt(&mut self, _: &OwnedRecord) -> Result> { + unimplemented!(); + } + fn seek_to_last(&mut self) -> Result> { unimplemented!(); } diff --git a/core/schema.rs b/core/schema.rs index f6d5c0434..6a440ffa8 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -46,6 +46,7 @@ impl Schema { #[derive(Clone, Debug)] pub enum Table { BTree(Rc), + Index(Rc), Pseudo(Rc), } @@ -57,6 +58,7 @@ impl Table { pub fn get_rowid_alias_column(&self) -> Option<(usize, &Column)> { match self { Table::BTree(table) => table.get_rowid_alias_column(), + Table::Index(_) => None, Table::Pseudo(_) => None, } } @@ -64,6 +66,7 @@ impl Table { pub fn column_is_rowid_alias(&self, col: &Column) -> bool { match self { Table::BTree(table) => table.column_is_rowid_alias(col), + Table::Index(_) => false, Table::Pseudo(_) => false, } } @@ -71,6 +74,7 @@ impl Table { pub fn get_name(&self) -> &str { match self { Table::BTree(table) => &table.name, + Table::Index(index) => &index.name, Table::Pseudo(_) => "", } } @@ -81,6 +85,10 @@ impl Table { Some(column) => Some(&column.name), None => None, }, + Table::Index(i) => match i.columns.get(index) { + Some(column) => Some(&column.name), + None => None, + }, Table::Pseudo(table) => match table.columns.get(index) { Some(column) => Some(&column.name), None => None, @@ -91,6 +99,7 @@ impl Table { pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> { match self { Table::BTree(table) => table.get_column(name), + Table::Index(index) => unimplemented!(), Table::Pseudo(table) => table.get_column(name), } } @@ -98,6 +107,7 @@ impl Table { pub fn get_column_at(&self, index: usize) -> &Column { match self { Table::BTree(table) => table.columns.get(index).unwrap(), + Table::Index(index) => unimplemented!(), Table::Pseudo(table) => table.columns.get(index).unwrap(), } } @@ -105,6 +115,7 @@ impl Table { pub fn columns(&self) -> &Vec { match self { Table::BTree(table) => &table.columns, + Table::Index(index) => unimplemented!(), Table::Pseudo(table) => &table.columns, } } @@ -112,7 +123,8 @@ impl Table { pub fn has_rowid(&self) -> bool { match self { Table::BTree(table) => table.has_rowid, - Table::Pseudo(_) => todo!(), + Table::Index(_) => unimplemented!(), + Table::Pseudo(_) => unimplemented!(), } } } diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 3f6cea18a..dfb9d313c 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -11,7 +11,7 @@ use crate::Result; use std::cell::{Ref, RefCell}; use std::rc::Rc; -use super::sqlite3_ondisk::{write_varint_to_vec, OverflowCell}; +use super::sqlite3_ondisk::{write_varint_to_vec, IndexInteriorCell, IndexLeafCell, OverflowCell}; /* These are offsets of fields in the header of a b-tree page. @@ -23,6 +23,12 @@ const BTREE_HEADER_OFFSET_CELL_CONTENT: usize = 5; /* pointer to first byte of c const BTREE_HEADER_OFFSET_FRAGMENTED: usize = 7; /* number of fragmented bytes -> u8 */ const BTREE_HEADER_OFFSET_RIGHTMOST: usize = 8; /* if internalnode, pointer right most pointer (saved separately from cells) -> u32 */ +#[derive(Clone)] +pub enum IndexSeekOp { + GT, + GE, +} + pub struct MemPage { parent: Option>, page_idx: usize, @@ -145,14 +151,75 @@ impl BTreeCursor { let record = crate::storage::sqlite3_ondisk::read_record(_payload)?; return Ok(CursorResult::Ok((Some(*_rowid), Some(record)))); } - BTreeCell::IndexInteriorCell(_) => { - unimplemented!(); + BTreeCell::IndexInteriorCell(IndexInteriorCell { + left_child_page, .. + }) => { + mem_page.advance(); + let mem_page = + MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + continue; + } + BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { + mem_page.advance(); + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + let rowid = match record.values[1] { + OwnedValue::Integer(rowid) => rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } + } + } + } + + fn btree_index_seek( + &mut self, + key: &OwnedRecord, + op: IndexSeekOp, + ) -> Result, Option)>> { + self.move_to_index_leaf(key, op.clone())?; + + let mem_page = self.get_mem_page(); + let page_idx = mem_page.page_idx; + let page = self.pager.read_page(page_idx)?; + let page = RefCell::borrow(&page); + if page.is_locked() { + return Ok(CursorResult::IO); + } + + let page = page.contents.read().unwrap(); + let page = page.as_ref().unwrap(); + + for cell_idx in 0..page.cell_count() { + match &page.cell_get( + cell_idx, + self.pager.clone(), + self.max_local(page.page_type()), + self.min_local(page.page_type()), + self.usable_space(), + )? { + BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { + mem_page.advance(); + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + let comparison = match op { + IndexSeekOp::GT => record > *key, + IndexSeekOp::GE => record >= *key, + }; + if comparison { + let rowid = match record.values.get(1) { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } } - BTreeCell::IndexLeafCell(_) => { - unimplemented!(); + cell_type => { + unreachable!("unexpected cell type: {:?}", cell_type); } } } + Ok(CursorResult::Ok((None, None))) } fn btree_seek_rowid( @@ -337,6 +404,81 @@ impl BTreeCursor { } } + fn move_to_index_leaf( + &mut self, + key: &OwnedRecord, + cmp: IndexSeekOp, + ) -> Result> { + self.move_to_root(); + loop { + let mem_page = self.get_mem_page(); + let page_idx = mem_page.page_idx; + let page = self.pager.read_page(page_idx)?; + let page = RefCell::borrow(&page); + if page.is_locked() { + return Ok(CursorResult::IO); + } + + let page = page.contents.read().unwrap(); + let page = page.as_ref().unwrap(); + if page.is_leaf() { + return Ok(CursorResult::Ok(())); + } + + let mut found_cell = false; + for cell_idx in 0..page.cell_count() { + match &page.cell_get( + cell_idx, + self.pager.clone(), + self.max_local(page.page_type()), + self.min_local(page.page_type()), + self.usable_space(), + )? { + BTreeCell::IndexInteriorCell(IndexInteriorCell { + left_child_page, + payload, + .. + }) => { + // get the logic for this from btree_index_seek + + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + let comparison = match cmp { + IndexSeekOp::GT => record > *key, + IndexSeekOp::GE => record >= *key, + }; + if comparison { + mem_page.advance(); + let mem_page = + MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + found_cell = true; + break; + } + } + _ => { + unreachable!( + "we don't iterate leaf cells while trying to move to a leaf cell" + ); + } + } + } + + if !found_cell { + let parent = mem_page.clone(); + match page.rightmost_pointer() { + Some(right_most_pointer) => { + let mem_page = MemPage::new(Some(parent), right_most_pointer as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + continue; + } + None => { + unreachable!("we shall not go back up! The only way is down the slope"); + } + } + } + } + } + fn insert_to_page( &mut self, key: &OwnedValue, @@ -1296,6 +1438,30 @@ impl Cursor for BTreeCursor { } } + fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { + match self.btree_index_seek(key, IndexSeekOp::GE)? { + CursorResult::Ok((rowid, record)) => { + self.rowid.replace(rowid); + println!("seek_ge: {:?}", record); + self.record.replace(record); + Ok(CursorResult::Ok(rowid.is_some())) + } + CursorResult::IO => Ok(CursorResult::IO), + } + } + + fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { + match self.btree_index_seek(key, IndexSeekOp::GT)? { + CursorResult::Ok((rowid, record)) => { + self.rowid.replace(rowid); + println!("seek_gt: {:?}", record); + self.record.replace(record); + Ok(CursorResult::Ok(rowid.is_some())) + } + CursorResult::IO => Ok(CursorResult::IO), + } + } + fn record(&self) -> Result>> { Ok(self.record.borrow()) } diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index e0d22a986..0a22d7047 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -2,6 +2,8 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use sqlite3_parser::ast; + use crate::schema::{BTreeTable, Column, PseudoTable, Table}; use crate::storage::sqlite3_ondisk::DatabaseHeader; use crate::translate::expr::resolve_ident_pseudo_table; @@ -251,6 +253,165 @@ impl Emitter for Operator { _ => Ok(OpStepResult::Done), } } + Operator::IndexScan { + table, + table_identifier, + index, + seek_cmp, + seek_expr, + predicates, + step, + id, + .. + } => { + *step += 1; + const INDEX_SCAN_OPEN_AND_SEEK: usize = 1; + const INDEX_SCAN_NEXT: usize = 2; + match *step { + INDEX_SCAN_OPEN_AND_SEEK => { + let table_cursor_id = program.alloc_cursor_id( + Some(table_identifier.clone()), + Some(Table::BTree(table.clone())), + ); + let index_cursor_id = program.alloc_cursor_id( + Some(index.name.clone()), + Some(Table::Index(index.clone())), + ); + let next_row_label = program.allocate_label(); + m.next_row_labels.insert(*id, next_row_label); + let rewind_label = program.allocate_label(); + m.rewind_labels.push(rewind_label); + program.emit_insn(Insn::OpenReadAsync { + cursor_id: table_cursor_id, + root_page: table.root_page, + }); + program.emit_insn(Insn::OpenReadAwait); + program.emit_insn(Insn::OpenReadAsync { + cursor_id: index_cursor_id, + root_page: index.root_page, + }); + program.emit_insn(Insn::OpenReadAwait); + + let cmp_reg = program.alloc_register(); + // TODO this only handles ascending indexes + match seek_cmp { + ast::Operator::Equals + | ast::Operator::Greater + | ast::Operator::GreaterEquals => { + translate_expr( + program, + Some(referenced_tables), + seek_expr, + cmp_reg, + None, + None, + )?; + } + ast::Operator::Less | ast::Operator::LessEquals => { + program.emit_insn(Insn::Null { + dest: cmp_reg, + dest_end: None, + }); + } + _ => unreachable!(), + } + program.emit_insn_with_label_dependency( + match seek_cmp { + ast::Operator::Equals | ast::Operator::GreaterEquals => { + Insn::SeekGE { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: *m.termination_label_stack.last().unwrap(), + } + } + ast::Operator::Greater + | ast::Operator::Less + | ast::Operator::LessEquals => Insn::SeekGT { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: *m.termination_label_stack.last().unwrap(), + }, + _ => unreachable!(), + }, + *m.termination_label_stack.last().unwrap(), + ); + if *seek_cmp == ast::Operator::Less + || *seek_cmp == ast::Operator::LessEquals + { + translate_expr( + program, + Some(referenced_tables), + seek_expr, + cmp_reg, + None, + None, + )?; + } + + program.defer_label_resolution(rewind_label, program.offset() as usize); + + // 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. + + let abort_jump_target = *m.termination_label_stack.last().unwrap(); + match seek_cmp { + ast::Operator::Equals | ast::Operator::LessEquals => { + program.emit_insn_with_label_dependency( + Insn::IdxGT { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } + ast::Operator::Less => { + program.emit_insn_with_label_dependency( + Insn::IdxGE { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } + _ => {} + } + + program.emit_insn(Insn::DeferredSeek { + index_cursor_id, + table_cursor_id, + }); + + Ok(OpStepResult::ReadyToEmit) + } + INDEX_SCAN_NEXT => { + let cursor_id = program.resolve_cursor_id(&index.name, None); + program + .resolve_label(*m.next_row_labels.get(id).unwrap(), program.offset()); + program.emit_insn(Insn::NextAsync { cursor_id }); + let jump_label = m.rewind_labels.pop().unwrap(); + program.emit_insn_with_label_dependency( + Insn::NextAwait { + cursor_id, + pc_if_next: jump_label, + }, + jump_label, + ); + Ok(OpStepResult::Done) + } + _ => Ok(OpStepResult::Done), + } + } Operator::SeekRowid { table, table_identifier, @@ -1196,6 +1357,23 @@ impl Emitter for Operator { Ok(start_reg) } + Operator::IndexScan { + table, + table_identifier, + .. + } => { + let start_reg = program.alloc_registers(col_count); + let table = cursor_override + .map(|c| c.pseudo_table.clone()) + .unwrap_or_else(|| Table::BTree(table.clone())); + let cursor_id = cursor_override + .map(|c| c.cursor_id) + .unwrap_or_else(|| program.resolve_cursor_id(table_identifier, None)); + let start_column_offset = cursor_override.map(|c| c.sort_key_len).unwrap_or(0); + translate_table_columns(program, cursor_id, &table, start_column_offset, start_reg); + + Ok(start_reg) + } Operator::Join { left, right, .. } => { let left_start_reg = left.result_columns(program, referenced_tables, m, cursor_override)?; @@ -1415,13 +1593,7 @@ impl Emitter for Operator { fn prologue( cache: ExpressionResultCache, -) -> Result<( - ProgramBuilder, - Metadata, - BranchOffset, - BranchOffset, - BranchOffset, -)> { +) -> Result<(ProgramBuilder, Metadata, BranchOffset, BranchOffset)> { let mut program = ProgramBuilder::new(); let init_label = program.allocate_label(); let halt_label = program.allocate_label(); @@ -1446,16 +1618,19 @@ fn prologue( sorts: HashMap::new(), }; - Ok((program, metadata, init_label, halt_label, start_offset)) + Ok((program, metadata, init_label, start_offset)) } fn epilogue( program: &mut ProgramBuilder, + metadata: &mut Metadata, init_label: BranchOffset, - halt_label: BranchOffset, start_offset: BranchOffset, ) -> Result<()> { - program.resolve_label(halt_label, program.offset()); + program.resolve_label( + metadata.termination_label_stack.pop().unwrap(), + program.offset(), + ); program.emit_insn(Insn::Halt { err_code: 0, description: String::new(), @@ -1479,7 +1654,7 @@ pub fn emit_program( mut plan: Plan, cache: ExpressionResultCache, ) -> Result { - let (mut program, mut metadata, init_label, halt_label, start_offset) = prologue(cache)?; + let (mut program, mut metadata, init_label, start_offset) = prologue(cache)?; loop { match plan .root_operator @@ -1495,7 +1670,7 @@ pub fn emit_program( )?; } OpStepResult::Done => { - epilogue(&mut program, init_label, halt_label, start_offset)?; + epilogue(&mut program, &mut metadata, init_label, start_offset)?; return Ok(program.build(database_header)); } } diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 14fe17d6f..7f764ae87 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -50,6 +50,7 @@ pub fn translate_insert( ); let root_page = match table.as_ref() { Table::BTree(btree) => btree.root_page, + Table::Index(index) => index.root_page, Table::Pseudo(_) => todo!(), }; diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 54164ac60..71ceb5765 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -2,7 +2,12 @@ use std::{collections::HashMap, rc::Rc}; use sqlite3_parser::ast; -use crate::{schema::BTreeTable, util::normalize_ident, Result}; +use crate::{ + schema::{BTreeTable, Index}, + types::OwnedValue, + util::normalize_ident, + Result, +}; use super::plan::{ get_table_ref_bitmask_for_ast_expr, get_table_ref_bitmask_for_operator, Operator, Plan, @@ -25,6 +30,7 @@ pub fn optimize_plan(mut select_plan: Plan) -> Result<(Plan, ExpressionResultCac Plan { root_operator: Operator::Nothing, referenced_tables: vec![], + available_indexes: vec![], }, expr_result_cache, )); @@ -32,6 +38,7 @@ pub fn optimize_plan(mut select_plan: Plan) -> Result<(Plan, ExpressionResultCac use_indexes( &mut select_plan.root_operator, &select_plan.referenced_tables, + &select_plan.available_indexes, )?; find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_operator_doesnt_recompute_them(&select_plan.root_operator, &mut expr_result_cache); Ok((select_plan, expr_result_cache)) @@ -43,8 +50,10 @@ pub fn optimize_plan(mut select_plan: Plan) -> Result<(Plan, ExpressionResultCac fn use_indexes( operator: &mut Operator, referenced_tables: &[(Rc, String)], + available_indexes: &[Rc], ) -> Result<()> { match operator { + Operator::IndexScan { .. } => Ok(()), Operator::Scan { table, predicates: filter, @@ -90,35 +99,72 @@ fn use_indexes( predicates: predicates_owned, id: *id, step: 0, + }; + return Ok(()); + } + + let mut maybe_index_predicate = None; + let mut maybe_index_idx = None; + let fs = filter.as_mut().unwrap(); + for i in 0..fs.len() { + let mut f = fs[i].take_ownership(); + let index_idx = f.check_index_scan(available_indexes)?; + if index_idx.is_some() { + maybe_index_predicate = Some(f); + maybe_index_idx = index_idx; + fs.remove(i); + break; + } + } + + if let Some(index_idx) = maybe_index_idx { + let index_predicate = maybe_index_predicate.unwrap(); + match index_predicate { + ast::Expr::Binary(lhs, op, rhs) => { + *operator = Operator::IndexScan { + table: table.clone(), + index: available_indexes[index_idx].clone(), + index_predicate: ast::Expr::Binary(lhs, op, rhs.clone()), + predicates: Some(std::mem::take(fs)), + seek_cmp: op, + seek_expr: *rhs, + table_identifier: table_identifier.clone(), + id: *id, + step: 0, + }; + } + _ => { + crate::bail_parse_error!("Unsupported index predicate"); + } } } Ok(()) } Operator::Aggregate { source, .. } => { - use_indexes(source, referenced_tables)?; + use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } Operator::Filter { source, .. } => { - use_indexes(source, referenced_tables)?; + use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } Operator::SeekRowid { .. } => Ok(()), Operator::Limit { source, .. } => { - use_indexes(source, referenced_tables)?; + use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } Operator::Join { left, right, .. } => { - use_indexes(left, referenced_tables)?; - use_indexes(right, referenced_tables)?; + use_indexes(left, referenced_tables, available_indexes)?; + use_indexes(right, referenced_tables, available_indexes)?; Ok(()) } Operator::Order { source, .. } => { - use_indexes(source, referenced_tables)?; + use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } Operator::Projection { source, .. } => { - use_indexes(source, referenced_tables)?; + use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } Operator::Nothing => Ok(()), @@ -279,6 +325,7 @@ fn eliminate_constants(operator: &mut Operator) -> Result Ok(ConstantConditionEliminationResult::Continue), Operator::Nothing => Ok(ConstantConditionEliminationResult::Continue), } } @@ -379,6 +426,7 @@ fn push_predicates( Ok(()) } Operator::Scan { .. } => Ok(()), + Operator::IndexScan { .. } => Ok(()), Operator::Nothing => Ok(()), } } @@ -424,6 +472,7 @@ fn push_predicate( Ok(None) } + Operator::IndexScan { .. } => Ok(Some(predicate)), Operator::Filter { source, predicates: ps, @@ -684,6 +733,7 @@ fn find_indexes_of_all_result_columns_in_operator_that_match_expr_either_fully_o mask } Operator::Scan { .. } => 0, + Operator::IndexScan { .. } => 0, Operator::Nothing => 0, }; @@ -883,6 +933,7 @@ fn find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_o find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_operator_doesnt_recompute_them(source, expr_result_cache) } Operator::Scan { .. } => {} + Operator::IndexScan { .. } => {} Operator::Nothing => {} } } @@ -915,6 +966,7 @@ pub trait Optimizable { &self, referenced_tables: &[(Rc, String)], ) -> Result>; + fn check_index_scan(&mut self, available_indexes: &[Rc]) -> Result>; } impl Optimizable for ast::Expr { @@ -967,6 +1019,54 @@ impl Optimizable for ast::Expr { _ => Ok(None), } } + fn check_index_scan(&mut self, available_indexes: &[Rc]) -> Result> { + match self { + ast::Expr::Id(ident) => { + let ident = normalize_ident(&ident.0); + let indexes = available_indexes + .iter() + .enumerate() + .filter(|(_, i)| i.columns.iter().any(|c| c.name == ident)) + .collect::>(); + if indexes.is_empty() { + return Ok(None); + } + if indexes.len() > 1 { + crate::bail_parse_error!("ambiguous column name {}", ident) + } + Ok(Some(indexes.first().unwrap().0)) + } + ast::Expr::Qualified(tbl, ident) => { + let tbl = normalize_ident(&tbl.0); + let ident = normalize_ident(&ident.0); + let index = available_indexes.iter().enumerate().find(|(_, i)| { + let normalized_tbl = normalize_ident(&i.table_name); + normalized_tbl == tbl + && i.columns.iter().any(|c| normalize_ident(&c.name) == ident) + }); + if index.is_none() { + return Ok(None); + } + Ok(Some(index.unwrap().0)) + } + ast::Expr::Binary(lhs, op, rhs) => { + let lhs_index = lhs.check_index_scan(available_indexes)?; + if lhs_index.is_some() { + return Ok(lhs_index); + } + let rhs_index = rhs.check_index_scan(available_indexes)?; + if rhs_index.is_some() { + // swap lhs and rhs + let lhs_new = rhs.take_ownership(); + let rhs_new = lhs.take_ownership(); + *self = ast::Expr::Binary(Box::new(lhs_new), *op, Box::new(rhs_new)); + return Ok(rhs_index); + } + Ok(None) + } + _ => Ok(None), + } + } fn check_constant(&self) -> Result> { match self { ast::Expr::Literal(lit) => match lit { diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 6a73e4b57..e7e02f23c 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -6,12 +6,18 @@ use std::{ use sqlite3_parser::ast; -use crate::{function::AggFunc, schema::BTreeTable, util::normalize_ident, Result}; +use crate::{ + function::AggFunc, + schema::{BTreeTable, Index}, + util::normalize_ident, + Result, +}; #[derive(Debug)] pub struct Plan { pub root_operator: Operator, pub referenced_tables: Vec<(Rc, String)>, + pub available_indexes: Vec>, } impl Display for Plan { @@ -123,6 +129,17 @@ pub enum Operator { predicates: Option>, step: usize, }, + IndexScan { + id: usize, + index: Rc, + seek_cmp: ast::Operator, + seek_expr: ast::Expr, + index_predicate: ast::Expr, + table: Rc, + table_identifier: String, + predicates: Option>, + step: usize, + }, // Nothing operator // This operator is used to represent an empty query. // e.g. SELECT * from foo WHERE 0 will eventually be optimized to Nothing. @@ -172,6 +189,7 @@ impl Operator { .map(|e| e.column_count(referenced_tables)) .sum(), Operator::Scan { table, .. } => table.columns.len(), + Operator::IndexScan { table, .. } => table.columns.len(), Operator::Nothing => 0, } } @@ -226,6 +244,9 @@ impl Operator { }) .collect(), Operator::Scan { table, .. } => table.columns.iter().map(|c| c.name.clone()).collect(), + Operator::IndexScan { table, .. } => { + table.columns.iter().map(|c| c.name.clone()).collect() + } Operator::Nothing => vec![], } } @@ -240,6 +261,7 @@ impl Operator { Operator::Order { id, .. } => *id, Operator::Projection { id, .. } => *id, Operator::Scan { id, .. } => *id, + Operator::IndexScan { id, .. } => *id, Operator::Nothing => unreachable!(), } } @@ -429,6 +451,10 @@ impl Display for Operator { }?; Ok(()) } + Operator::IndexScan { table, .. } => { + writeln!(f, "{}INDEX SCAN {}", indent, table.name)?; + Ok(()) + } Operator::Nothing => Ok(()), } } @@ -489,6 +515,13 @@ pub fn get_table_ref_bitmask_for_operator<'a>( .position(|(t, _)| Rc::ptr_eq(t, table)) .unwrap(); } + Operator::IndexScan { table, .. } => { + table_refs_mask |= 1 + << tables + .iter() + .position(|(t, _)| Rc::ptr_eq(t, table)) + .unwrap(); + } Operator::Nothing => {} } Ok(table_refs_mask) diff --git a/core/translate/planner.rs b/core/translate/planner.rs index cc5151c13..1c3dba4e6 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -277,10 +277,19 @@ pub fn prepare_select_plan<'a>(schema: &Schema, select: ast::Select) -> Result

todo!(), diff --git a/core/types.rs b/core/types.rs index 507ae04cb..48cbd2d11 100644 --- a/core/types.rs +++ b/core/types.rs @@ -416,6 +416,8 @@ pub trait Cursor { fn wait_for_completion(&mut self) -> Result<()>; fn rowid(&self) -> Result>; fn seek_rowid(&mut self, rowid: u64) -> Result>; + fn seek_ge(&mut self, key: &OwnedRecord) -> Result>; + fn seek_gt(&mut self, key: &OwnedRecord) -> Result>; fn seek_to_last(&mut self) -> Result>; fn record(&self) -> Result>>; fn insert( diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 4a0161e07..313967c5d 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -299,6 +299,22 @@ impl ProgramBuilder { assert!(*target_pc_eq < 0); *target_pc_eq = to_offset; } + Insn::SeekGE { target_pc, .. } => { + assert!(*target_pc < 0); + *target_pc = to_offset; + } + Insn::SeekGT { target_pc, .. } => { + assert!(*target_pc < 0); + *target_pc = to_offset; + } + Insn::IdxGE { target_pc, .. } => { + assert!(*target_pc < 0); + *target_pc = to_offset; + } + Insn::IdxGT { target_pc, .. } => { + assert!(*target_pc < 0); + *target_pc = to_offset; + } _ => { todo!("missing resolve_label for {:?}", insn); } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 340624ce5..25b6de9ca 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -521,6 +521,74 @@ pub fn insn_to_str( target_pc ), ), + Insn::DeferredSeek { + index_cursor_id, + table_cursor_id, + } => ( + "DeferredSeek", + *index_cursor_id as i32, + *table_cursor_id as i32, + 0, + OwnedValue::Text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::SeekGT { + cursor_id, + start_reg, + num_regs, + target_pc, + } => ( + "SeekGT", + *cursor_id as i32, + *target_pc as i32, + *start_reg as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::SeekGE { + cursor_id, + start_reg, + num_regs, + target_pc, + } => ( + "SeekGE", + *cursor_id as i32, + *target_pc as i32, + *start_reg as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::IdxGT { + cursor_id, + start_reg, + num_regs, + target_pc, + } => ( + "IdxGT", + *cursor_id as i32, + *target_pc as i32, + *start_reg as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::IdxGE { + cursor_id, + start_reg, + num_regs, + target_pc, + } => ( + "IdxGE", + *cursor_id as i32, + *target_pc as i32, + *start_reg as i32, + OwnedValue::Text(Rc::new("".to_string())), + 0, + "".to_string(), + ), Insn::DecrJumpZero { reg, target_pc } => ( "DecrJumpZero", *reg as i32, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index fcd1d2690..d761ed664 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -298,6 +298,47 @@ pub enum Insn { target_pc: BranchOffset, }, + // P1 is an open index cursor and P3 is a cursor on the corresponding table. This opcode does a deferred seek of the P3 table cursor to the row that corresponds to the current row of P1. + // This is a deferred seek. Nothing actually happens until the cursor is used to read a record. That way, if no reads occur, no unnecessary I/O happens. + DeferredSeek { + index_cursor_id: CursorID, + table_cursor_id: CursorID, + }, + + // Seek to the first index entry that is greater than or equal to the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. + SeekGE { + cursor_id: CursorID, + start_reg: usize, + num_regs: usize, + target_pc: BranchOffset, + }, + + // Seek to the first index entry that is greater than the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. + SeekGT { + cursor_id: CursorID, + start_reg: usize, + num_regs: usize, + target_pc: BranchOffset, + }, + + // The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. + // If the P1 index entry is greater or equal than the key value then jump to P2. Otherwise fall through to the next instruction. + IdxGE { + cursor_id: CursorID, + start_reg: usize, + num_regs: usize, + target_pc: BranchOffset, + }, + + // The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. + // If the P1 index entry is greater than the key value then jump to P2. Otherwise fall through to the next instruction. + IdxGT { + cursor_id: CursorID, + start_reg: usize, + num_regs: usize, + target_pc: BranchOffset, + }, + // Decrement the given register and jump to the given PC if the result is zero. DecrJumpZero { reg: usize, @@ -444,6 +485,7 @@ pub struct ProgramState { cursors: RefCell>>, registers: Vec, last_compare: Option, + deferred_seek: Option<(CursorID, CursorID)>, ended_coroutine: bool, // flag to notify yield coroutine finished regex_cache: RegexCache, } @@ -458,6 +500,7 @@ impl ProgramState { cursors, registers, last_compare: None, + deferred_seek: None, ended_coroutine: false, regex_cache: RegexCache::new(), } @@ -958,6 +1001,13 @@ impl Program { column, dest, } => { + if let Some((index_cursor_id, table_cursor_id)) = state.deferred_seek.take() { + let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); + let rowid = index_cursor.rowid()?; + let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); + table_cursor.seek_rowid(rowid.unwrap())?; + } + let cursor = cursors.get_mut(cursor_id).unwrap(); if let Some(ref record) = *cursor.record()? { let null_flag = cursor.get_null_flag(); @@ -1085,6 +1135,13 @@ impl Program { state.pc += 1; } Insn::RowId { cursor_id, dest } => { + if let Some((index_cursor_id, table_cursor_id)) = state.deferred_seek.take() { + let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); + let rowid = index_cursor.rowid()?; + let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); + table_cursor.seek_rowid(rowid.unwrap())?; + } + let cursor = cursors.get_mut(cursor_id).unwrap(); if let Some(ref rowid) = cursor.rowid()? { state.registers[*dest] = OwnedValue::Integer(*rowid as i64); @@ -1121,6 +1178,104 @@ impl Program { } } } + Insn::DeferredSeek { + index_cursor_id, + table_cursor_id, + } => { + state.deferred_seek = Some((*index_cursor_id, *table_cursor_id)); + state.pc += 1; + } + Insn::SeekGE { + cursor_id, + start_reg, + num_regs, + target_pc, + } => { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + match cursor.seek_ge(&record_from_regs)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } + } + } + Insn::SeekGT { + cursor_id, + start_reg, + num_regs, + target_pc, + } => { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + match cursor.seek_gt(&record_from_regs)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } + } + } + Insn::IdxGE { + cursor_id, + start_reg, + num_regs, + target_pc, + } => { + assert!(*target_pc >= 0); + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + if let Some(ref idx_record) = *cursor.record()? { + // omit the rowid from the idx_record, which is the last value + if idx_record.values[..idx_record.values.len() - 1] + >= *record_from_regs.values + { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } else { + state.pc = *target_pc; + } + } + Insn::IdxGT { + cursor_id, + start_reg, + num_regs, + target_pc, + } => { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + if let Some(ref idx_record) = *cursor.record()? { + // omit the rowid from the idx_record, which is the last value + if idx_record.values[..idx_record.values.len() - 1] + > *record_from_regs.values + { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } else { + state.pc = *target_pc; + } + } Insn::DecrJumpZero { reg, target_pc } => { assert!(*target_pc >= 0); match state.registers[*reg] { @@ -1863,7 +2018,10 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin fn get_indent_count(indent_count: usize, curr_insn: &Insn, prev_insn: Option<&Insn>) -> usize { let indent_count = if let Some(insn) = prev_insn { match insn { - Insn::RewindAwait { .. } | Insn::SorterSort { .. } => indent_count + 1, + Insn::RewindAwait { .. } + | Insn::SorterSort { .. } + | Insn::SeekGE { .. } + | Insn::SeekGT { .. } => indent_count + 1, _ => indent_count, } } else { @@ -2369,6 +2527,14 @@ mod tests { self.seek_rowid(rowid) } + fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { + unimplemented!(); + } + + fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { + unimplemented!(); + } + fn rewind(&mut self) -> Result> { unimplemented!() } diff --git a/core/vdbe/sorter.rs b/core/vdbe/sorter.rs index da214e531..75980f222 100644 --- a/core/vdbe/sorter.rs +++ b/core/vdbe/sorter.rs @@ -79,6 +79,14 @@ impl Cursor for Sorter { unimplemented!(); } + fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { + unimplemented!(); + } + + fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { + unimplemented!(); + } + fn seek_to_last(&mut self) -> Result> { unimplemented!(); } diff --git a/testing/testing.db b/testing/testing.db index c97eac5ff4e33e7ec3b5f9d666e7d38be5eab1d0..e508d589dfd51183fb8bf04a1e1f46a212be79b5 100644 GIT binary patch delta 94882 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 No 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 delta 125 zcmZo@@M>7#GC^99nSp^(kO2XhCh8ai1@)2{d4YmVd`S#^Px+EID=K{AZBF8APvT<) vVkRJF24WT Date: Sat, 5 Oct 2024 12:07:27 +0300 Subject: [PATCH 02/32] stuff --- core/pseudo.rs | 12 +- core/storage/btree.rs | 181 +++++++++++++++++++++-- core/translate/emitter.rs | 152 ++++++++++++++----- core/translate/optimizer.rs | 281 +++++++++++++++++++----------------- core/translate/plan.rs | 15 +- core/translate/planner.rs | 2 - core/types.rs | 6 +- core/vdbe/explain.rs | 2 + core/vdbe/mod.rs | 130 ++++++++++++++--- core/vdbe/sorter.rs | 12 +- testing/cmdlineshell.test | 3 +- 11 files changed, 574 insertions(+), 222 deletions(-) diff --git a/core/pseudo.rs b/core/pseudo.rs index 78a80afc6..3ef849c9e 100644 --- a/core/pseudo.rs +++ b/core/pseudo.rs @@ -52,11 +52,19 @@ impl Cursor for PseudoCursor { unimplemented!(); } - fn seek_ge(&mut self, _: &OwnedRecord) -> Result> { + fn seek_ge_rowid(&mut self, _: u64) -> Result> { unimplemented!(); } - fn seek_gt(&mut self, _: &OwnedRecord) -> Result> { + fn seek_gt_rowid(&mut self, _: u64) -> Result> { + unimplemented!(); + } + + fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { + unimplemented!(); + } + + fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { unimplemented!(); } diff --git a/core/storage/btree.rs b/core/storage/btree.rs index dfb9d313c..87261681a 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -24,7 +24,7 @@ const BTREE_HEADER_OFFSET_FRAGMENTED: usize = 7; /* number of fragmented bytes - const BTREE_HEADER_OFFSET_RIGHTMOST: usize = 8; /* if internalnode, pointer right most pointer (saved separately from cells) -> u32 */ #[derive(Clone)] -pub enum IndexSeekOp { +pub enum SeekOp { GT, GE, } @@ -152,6 +152,7 @@ impl BTreeCursor { return Ok(CursorResult::Ok((Some(*_rowid), Some(record)))); } BTreeCell::IndexInteriorCell(IndexInteriorCell { + payload, left_child_page, .. }) => { mem_page.advance(); @@ -176,7 +177,7 @@ impl BTreeCursor { fn btree_index_seek( &mut self, key: &OwnedRecord, - op: IndexSeekOp, + op: SeekOp, ) -> Result, Option)>> { self.move_to_index_leaf(key, op.clone())?; @@ -203,8 +204,8 @@ impl BTreeCursor { mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match op { - IndexSeekOp::GT => record > *key, - IndexSeekOp::GE => record >= *key, + SeekOp::GT => record > *key, + SeekOp::GE => record >= *key, }; if comparison { let rowid = match record.values.get(1) { @@ -222,11 +223,63 @@ impl BTreeCursor { Ok(CursorResult::Ok((None, None))) } + fn btree_table_seek( + &mut self, + rowid: u64, + op: SeekOp, + ) -> Result, Option)>> { + self.move_to_table_leaf(rowid, op.clone())?; + + let mem_page = self.get_mem_page(); + let page_idx = mem_page.page_idx; + let page = self.pager.read_page(page_idx)?; + let page = RefCell::borrow(&page); + if page.is_locked() { + return Ok(CursorResult::IO); + } + + let page = page.contents.read().unwrap(); + let page = page.as_ref().unwrap(); + + for cell_idx in 0..page.cell_count() { + match &page.cell_get( + cell_idx, + self.pager.clone(), + self.max_local(page.page_type()), + self.min_local(page.page_type()), + self.usable_space(), + )? { + BTreeCell::TableLeafCell(TableLeafCell { + _rowid: cell_rowid, + _payload: payload, + first_overflow_page: _, + }) => { + mem_page.advance(); + let comparison = match op { + SeekOp::GT => *cell_rowid > rowid, + SeekOp::GE => *cell_rowid >= rowid, + }; + if comparison { + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + return Ok(CursorResult::Ok((Some(*cell_rowid), Some(record)))); + } + } + cell_type => { + unreachable!("unexpected cell type: {:?}", cell_type); + } + } + } + Ok(CursorResult::Ok((None, None))) + } + fn btree_seek_rowid( &mut self, rowid: u64, ) -> Result, Option)>> { - self.move_to(rowid)?; + match self.move_to(rowid)? { + CursorResult::Ok(_) => {} + CursorResult::IO => return Ok(CursorResult::IO), + }; let mem_page = self.get_mem_page(); @@ -266,6 +319,80 @@ impl BTreeCursor { Ok(CursorResult::Ok((None, None))) } + fn move_to_table_leaf(&mut self, rowid: u64, cmp: SeekOp) -> Result> { + self.move_to_root(); + + loop { + let mem_page = self.get_mem_page(); + let page_idx = mem_page.page_idx; + let page = self.pager.read_page(page_idx)?; + let page = RefCell::borrow(&page); + if page.is_locked() { + return Ok(CursorResult::IO); + } + + let page = page.contents.read().unwrap(); + let page = page.as_ref().unwrap(); + if page.is_leaf() { + return Ok(CursorResult::Ok(())); + } + + let mut found_cell = false; + for cell_idx in 0..page.cell_count() { + match &page.cell_get( + cell_idx, + self.pager.clone(), + self.max_local(page.page_type()), + self.min_local(page.page_type()), + self.usable_space(), + )? { + BTreeCell::TableInteriorCell(TableInteriorCell { + _left_child_page, + _rowid, + }) => { + let comparison = match cmp { + SeekOp::GT => *_rowid > rowid, + SeekOp::GE => *_rowid >= rowid, + }; + if comparison { + mem_page.advance(); + let mem_page = + MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + found_cell = true; + break; + } + } + BTreeCell::TableLeafCell(_) => { + unreachable!("we don't iterate leaf cells while trying to move to a leaf cell"); + } + BTreeCell::IndexInteriorCell(_) => { + unimplemented!(); + } + BTreeCell::IndexLeafCell(_) => { + unimplemented!(); + } + } + } + + if !found_cell { + let parent = mem_page.clone(); + match page.rightmost_pointer() { + Some(right_most_pointer) => { + let mem_page = MemPage::new(Some(parent), right_most_pointer as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + continue; + } + None => { + unreachable!("we shall not go back up! The only way is down the slope"); + } + } + } + + return Ok(CursorResult::Ok(())); + } + } + fn move_to_root(&mut self) { self.page .replace(Some(Rc::new(MemPage::new(None, self.root_page, 0)))); @@ -407,7 +534,7 @@ impl BTreeCursor { fn move_to_index_leaf( &mut self, key: &OwnedRecord, - cmp: IndexSeekOp, + cmp: SeekOp, ) -> Result> { self.move_to_root(); loop { @@ -443,8 +570,8 @@ impl BTreeCursor { let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { - IndexSeekOp::GT => record > *key, - IndexSeekOp::GE => record >= *key, + SeekOp::GT => record > *key, + SeekOp::GE => record >= *key, }; if comparison { mem_page.advance(); @@ -1410,6 +1537,12 @@ impl Cursor for BTreeCursor { fn next(&mut self) -> Result> { match self.get_next_record()? { CursorResult::Ok((rowid, next)) => { + { + let curr_rowid = self.rowid.borrow(); + if curr_rowid.is_some() && curr_rowid.unwrap() >= 8000 && rowid.is_some() && rowid.unwrap() < 8000 { + println!("curr_rowid: {:?}, rowid: {:?}, next: {:?}", curr_rowid.unwrap(), rowid.unwrap(), next); + } + } self.rowid.replace(rowid); self.record.replace(next); Ok(CursorResult::Ok(())) @@ -1438,11 +1571,32 @@ impl Cursor for BTreeCursor { } } - fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { - match self.btree_index_seek(key, IndexSeekOp::GE)? { + fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result> { + match self.btree_index_seek(key, SeekOp::GE)? { + CursorResult::Ok((rowid, record)) => { + self.rowid.replace(rowid); + self.record.replace(record); + Ok(CursorResult::Ok(rowid.is_some())) + } + CursorResult::IO => Ok(CursorResult::IO), + } + } + + fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result> { + match self.btree_index_seek(key, SeekOp::GT)? { + CursorResult::Ok((rowid, record)) => { + self.rowid.replace(rowid); + self.record.replace(record); + Ok(CursorResult::Ok(rowid.is_some())) + } + CursorResult::IO => Ok(CursorResult::IO), + } + } + + fn seek_ge_rowid(&mut self, rowid: u64) -> Result> { + match self.btree_table_seek(rowid, SeekOp::GE)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); - println!("seek_ge: {:?}", record); self.record.replace(record); Ok(CursorResult::Ok(rowid.is_some())) } @@ -1450,11 +1604,10 @@ impl Cursor for BTreeCursor { } } - fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { - match self.btree_index_seek(key, IndexSeekOp::GT)? { + fn seek_gt_rowid(&mut self, rowid: u64) -> Result> { + match self.btree_table_seek(rowid, SeekOp::GT)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); - println!("seek_gt: {:?}", record); self.record.replace(record); Ok(CursorResult::Ok(rowid.is_some())) } diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 0a22d7047..5693f134d 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -253,7 +253,7 @@ impl Emitter for Operator { _ => Ok(OpStepResult::Done), } } - Operator::IndexScan { + Operator::Search { table, table_identifier, index, @@ -265,18 +265,25 @@ impl Emitter for Operator { .. } => { *step += 1; - const INDEX_SCAN_OPEN_AND_SEEK: usize = 1; - const INDEX_SCAN_NEXT: usize = 2; + const SEARCH_OPEN_READ: usize = 1; + const SEARCH_SEEK_AND_CONDITIONS: usize = 2; + const SEARCH_NEXT: usize = 3; match *step { - INDEX_SCAN_OPEN_AND_SEEK => { + SEARCH_OPEN_READ => { let table_cursor_id = program.alloc_cursor_id( Some(table_identifier.clone()), Some(Table::BTree(table.clone())), ); - let index_cursor_id = program.alloc_cursor_id( - Some(index.name.clone()), - Some(Table::Index(index.clone())), - ); + + let index_cursor_id = if let Some(index) = index { + program.alloc_cursor_id( + Some(index.name.clone()), + Some(Table::Index(index.clone())), + ) + } else { + table_cursor_id + }; + let next_row_label = program.allocate_label(); m.next_row_labels.insert(*id, next_row_label); let rewind_label = program.allocate_label(); @@ -286,12 +293,24 @@ impl Emitter for Operator { root_page: table.root_page, }); program.emit_insn(Insn::OpenReadAwait); - program.emit_insn(Insn::OpenReadAsync { - cursor_id: index_cursor_id, - root_page: index.root_page, - }); - program.emit_insn(Insn::OpenReadAwait); + if let Some(index) = index { + program.emit_insn(Insn::OpenReadAsync { + cursor_id: index_cursor_id, + root_page: index.root_page, + }); + program.emit_insn(Insn::OpenReadAwait); + } + Ok(OpStepResult::Continue) + } + SEARCH_SEEK_AND_CONDITIONS => { + let table_cursor_id = program.resolve_cursor_id(table_identifier, None); + let index_cursor_id = if let Some(index) = index { + program.resolve_cursor_id(&index.name, None) + } else { + table_cursor_id + }; + let rewind_label = *m.rewind_labels.last().unwrap(); let cmp_reg = program.alloc_register(); // TODO this only handles ascending indexes match seek_cmp { @@ -319,6 +338,7 @@ impl Emitter for Operator { match seek_cmp { ast::Operator::Equals | ast::Operator::GreaterEquals => { Insn::SeekGE { + is_index: index.is_some(), cursor_id: index_cursor_id, start_reg: cmp_reg, num_regs: 1, @@ -328,6 +348,7 @@ impl Emitter for Operator { ast::Operator::Greater | ast::Operator::Less | ast::Operator::LessEquals => Insn::SeekGT { + is_index: index.is_some(), cursor_id: index_cursor_id, start_reg: cmp_reg, num_regs: 1, @@ -352,50 +373,113 @@ impl Emitter for Operator { program.defer_label_resolution(rewind_label, program.offset() as usize); - // We are currently only handling ascending indexes. + // 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. - let abort_jump_target = *m.termination_label_stack.last().unwrap(); + let abort_jump_target = *m.next_row_labels.get(id).unwrap_or(m.termination_label_stack.last().unwrap()); match seek_cmp { ast::Operator::Equals | ast::Operator::LessEquals => { - program.emit_insn_with_label_dependency( - Insn::IdxGT { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: abort_jump_target, - }, - abort_jump_target, - ); + if index.is_some() { + program.emit_insn_with_label_dependency( + Insn::IdxGT { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn_with_label_dependency( + Insn::Gt { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } } ast::Operator::Less => { + if index.is_some() { program.emit_insn_with_label_dependency( Insn::IdxGE { cursor_id: index_cursor_id, start_reg: cmp_reg, num_regs: 1, target_pc: abort_jump_target, - }, - abort_jump_target, - ); + }, + abort_jump_target, + ); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn_with_label_dependency( + Insn::Ge { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } } _ => {} } - program.emit_insn(Insn::DeferredSeek { - index_cursor_id, - table_cursor_id, - }); + if index.is_some() { + program.emit_insn(Insn::DeferredSeek { + index_cursor_id, + table_cursor_id, + }); + } + + let jump_label = m + .next_row_labels + .get(id) + .unwrap_or(m.termination_label_stack.last().unwrap()); + 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: *jump_label, + }; + translate_condition_expr( + program, + referenced_tables, + predicate, + None, + condition_metadata, + )?; + program.resolve_label(jump_target_when_true, program.offset()); + } + } Ok(OpStepResult::ReadyToEmit) } - INDEX_SCAN_NEXT => { - let cursor_id = program.resolve_cursor_id(&index.name, None); + SEARCH_NEXT => { + let cursor_id = if let Some(index) = index { + program.resolve_cursor_id(&index.name, None) + } else { + program.resolve_cursor_id(table_identifier, None) + }; program .resolve_label(*m.next_row_labels.get(id).unwrap(), program.offset()); program.emit_insn(Insn::NextAsync { cursor_id }); @@ -1357,7 +1441,7 @@ impl Emitter for Operator { Ok(start_reg) } - Operator::IndexScan { + Operator::Search { table, table_identifier, .. diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 71ceb5765..54b7e083b 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -53,7 +53,7 @@ fn use_indexes( available_indexes: &[Rc], ) -> Result<()> { match operator { - Operator::IndexScan { .. } => Ok(()), + Operator::Search { .. } => Ok(()), Operator::Scan { table, predicates: filter, @@ -66,75 +66,59 @@ fn use_indexes( } let fs = filter.as_mut().unwrap(); - let mut i = 0; - let mut maybe_rowid_predicate = None; - while i < fs.len() { + for i in 0..fs.len() { let f = fs[i].take_ownership(); - let table_index = referenced_tables + let table = referenced_tables .iter() - .position(|(t, t_id)| Rc::ptr_eq(t, table) && t_id == table_identifier) + .find(|(t, t_id)| Rc::ptr_eq(t, table) && t_id == table_identifier) .unwrap(); - let (can_use, expr) = - try_extract_rowid_comparison_expression(f, table_index, referenced_tables)?; - if can_use { - maybe_rowid_predicate = Some(expr); - fs.remove(i); - break; - } else { - fs[i] = expr; - i += 1; - } - } - - if let Some(rowid_predicate) = maybe_rowid_predicate { - let predicates_owned = if fs.is_empty() { - None - } else { - Some(std::mem::take(fs)) - }; - *operator = Operator::SeekRowid { - table: table.clone(), - table_identifier: table_identifier.clone(), - rowid_predicate, - predicates: predicates_owned, - id: *id, - step: 0, - }; - return Ok(()); - } - - let mut maybe_index_predicate = None; - let mut maybe_index_idx = None; - let fs = filter.as_mut().unwrap(); - for i in 0..fs.len() { - let mut f = fs[i].take_ownership(); - let index_idx = f.check_index_scan(available_indexes)?; - if index_idx.is_some() { - maybe_index_predicate = Some(f); - maybe_index_idx = index_idx; - fs.remove(i); - break; - } - } - - if let Some(index_idx) = maybe_index_idx { - let index_predicate = maybe_index_predicate.unwrap(); - match index_predicate { - ast::Expr::Binary(lhs, op, rhs) => { - *operator = Operator::IndexScan { - table: table.clone(), - index: available_indexes[index_idx].clone(), - index_predicate: ast::Expr::Binary(lhs, op, rhs.clone()), - predicates: Some(std::mem::take(fs)), - seek_cmp: op, - seek_expr: *rhs, - table_identifier: table_identifier.clone(), - id: *id, - step: 0, - }; + match try_extract_expr_that_utilizes_index(f, table, available_indexes)? { + Either::Left(non_index_using_expr) => { + fs[i] = non_index_using_expr; } - _ => { - crate::bail_parse_error!("Unsupported index predicate"); + Either::Right(index_using_expr) => { + match index_using_expr { + SearchableExpr::IndexSearch { index, cmp_op, cmp_expr } => { + fs.remove(i); + *operator = Operator::Search { + table: table.0.clone(), + index: Some(index.clone()), + predicates: Some(std::mem::take(fs)), + seek_cmp: cmp_op, + seek_expr: cmp_expr, + table_identifier: table_identifier.clone(), + id: *id, + step: 0, + }; + return Ok(()); + } + SearchableExpr::PrimaryKeySearch { table, cmp_op, cmp_expr } => { + fs.remove(i); + *operator = Operator::Search { + table, + index: None, + predicates: Some(std::mem::take(fs)), + seek_cmp: cmp_op, + seek_expr: cmp_expr, + table_identifier: table_identifier.clone(), + id: *id, + step: 0, + }; + return Ok(()); + } + SearchableExpr::PrimaryKeyEq { cmp_expr, table } => { + fs.remove(i); + *operator = Operator::SeekRowid { + table, + table_identifier: table_identifier.clone(), + rowid_predicate: cmp_expr, + predicates: Some(std::mem::take(fs)), + id: *id, + step: 0, + }; + return Ok(()); + } + } } } } @@ -325,7 +309,7 @@ fn eliminate_constants(operator: &mut Operator) -> Result Ok(ConstantConditionEliminationResult::Continue), + Operator::Search { .. } => Ok(ConstantConditionEliminationResult::Continue), Operator::Nothing => Ok(ConstantConditionEliminationResult::Continue), } } @@ -426,7 +410,7 @@ fn push_predicates( Ok(()) } Operator::Scan { .. } => Ok(()), - Operator::IndexScan { .. } => Ok(()), + Operator::Search { .. } => Ok(()), Operator::Nothing => Ok(()), } } @@ -472,7 +456,7 @@ fn push_predicate( Ok(None) } - Operator::IndexScan { .. } => Ok(Some(predicate)), + Operator::Search { .. } => Ok(Some(predicate)), Operator::Filter { source, predicates: ps, @@ -733,7 +717,7 @@ fn find_indexes_of_all_result_columns_in_operator_that_match_expr_either_fully_o mask } Operator::Scan { .. } => 0, - Operator::IndexScan { .. } => 0, + Operator::Search { .. } => 0, Operator::Nothing => 0, }; @@ -933,7 +917,7 @@ fn find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_o find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_operator_doesnt_recompute_them(source, expr_result_cache) } Operator::Scan { .. } => {} - Operator::IndexScan { .. } => {} + Operator::Search { .. } => {} Operator::Nothing => {} } } @@ -962,71 +946,40 @@ pub trait Optimizable { .map_or(false, |c| c == ConstantPredicate::AlwaysFalse)) } // if the expression is the primary key of a table, returns the index of the table - fn check_primary_key( + fn is_primary_key_of( &self, - referenced_tables: &[(Rc, String)], - ) -> Result>; - fn check_index_scan(&mut self, available_indexes: &[Rc]) -> Result>; + table: &(Rc, String), + ) -> bool; + fn check_index_scan(&mut self, table: &(Rc, String), available_indexes: &[Rc]) -> Result>; } impl Optimizable for ast::Expr { - fn check_primary_key( + fn is_primary_key_of( &self, - referenced_tables: &[(Rc, String)], - ) -> Result> { + table: &(Rc, String), + ) -> bool { match self { ast::Expr::Id(ident) => { let ident = normalize_ident(&ident.0); - let tables = referenced_tables - .iter() - .enumerate() - .filter_map(|(i, (t, _))| { - if t.get_column(&ident).map_or(false, |(_, c)| c.primary_key) { - Some(i) - } else { - None - } - }); - - let mut matches = 0; - let mut matching_tbl = None; - - for tbl in tables { - matching_tbl = Some(tbl); - matches += 1; - if matches > 1 { - crate::bail_parse_error!("ambiguous column name {}", ident) - } - } - - Ok(matching_tbl) + table.0.get_column(&ident).map_or(false, |(_, c)| c.primary_key) } ast::Expr::Qualified(tbl, ident) => { let tbl = normalize_ident(&tbl.0); let ident = normalize_ident(&ident.0); - let table = referenced_tables.iter().enumerate().find(|(_, (t, t_id))| { - *t_id == tbl && t.get_column(&ident).map_or(false, |(_, c)| c.primary_key) - }); - - if table.is_none() { - return Ok(None); - } - - let table = table.unwrap(); - - Ok(Some(table.0)) + + tbl == table.1 && table.0.get_column(&ident).map_or(false, |(_, c)| c.primary_key) } - _ => Ok(None), + _ => false, } } - fn check_index_scan(&mut self, available_indexes: &[Rc]) -> Result> { + fn check_index_scan(&mut self, table: &(Rc, String), available_indexes: &[Rc]) -> Result> { match self { ast::Expr::Id(ident) => { let ident = normalize_ident(&ident.0); let indexes = available_indexes .iter() .enumerate() - .filter(|(_, i)| i.columns.iter().any(|c| c.name == ident)) + .filter(|(_, i)| i.table_name == table.1 && i.columns.iter().any(|c| c.name == ident)) .collect::>(); if indexes.is_empty() { return Ok(None); @@ -1040,9 +993,10 @@ impl Optimizable for ast::Expr { let tbl = normalize_ident(&tbl.0); let ident = normalize_ident(&ident.0); let index = available_indexes.iter().enumerate().find(|(_, i)| { - let normalized_tbl = normalize_ident(&i.table_name); - normalized_tbl == tbl - && i.columns.iter().any(|c| normalize_ident(&c.name) == ident) + if i.table_name != tbl { + return false; + } + i.columns.iter().any(|c| normalize_ident(&c.name) == ident) }); if index.is_none() { return Ok(None); @@ -1050,11 +1004,11 @@ impl Optimizable for ast::Expr { Ok(Some(index.unwrap().0)) } ast::Expr::Binary(lhs, op, rhs) => { - let lhs_index = lhs.check_index_scan(available_indexes)?; + let lhs_index = lhs.check_index_scan(table, available_indexes)?; if lhs_index.is_some() { return Ok(lhs_index); } - let rhs_index = rhs.check_index_scan(available_indexes)?; + let rhs_index = rhs.check_index_scan(table, available_indexes)?; if rhs_index.is_some() { // swap lhs and rhs let lhs_new = rhs.take_ownership(); @@ -1186,28 +1140,89 @@ impl Optimizable for ast::Expr { } } -pub fn try_extract_rowid_comparison_expression( +pub enum Either { + Left(T), + Right(U), +} + +/// An expression that can be used to search for a row in a table using an index +/// (i.e. a primary key or a secondary index) +/// +pub enum SearchableExpr { + /// A primary key equality search. This is a special case of the primary key search + /// that uses the SeekRowid operator and bytecode instruction. + PrimaryKeyEq { + table: Rc, + cmp_expr: ast::Expr, + }, + /// A primary key search. This uses the Search operator and uses bytecode instructions like SeekGT, SeekGE etc. + PrimaryKeySearch { + table: Rc, + cmp_op: ast::Operator, + cmp_expr: ast::Expr, + }, + /// A secondary index search. This uses the Search operator and uses bytecode instructions like SeekGE, SeekGT etc. + IndexSearch { + index: Rc, + cmp_op: ast::Operator, + cmp_expr: ast::Expr, + }, +} + +pub fn try_extract_expr_that_utilizes_index( expr: ast::Expr, - table_index: usize, - referenced_tables: &[(Rc, String)], -) -> Result<(bool, ast::Expr)> { + table: &(Rc, String), + available_indexes: &[Rc], +) -> Result> { match expr { - ast::Expr::Binary(lhs, ast::Operator::Equals, rhs) => { - if let Some(lhs_table_index) = lhs.check_primary_key(referenced_tables)? { - if lhs_table_index == table_index { - return Ok((true, *rhs)); + ast::Expr::Binary(mut lhs, operator, mut rhs) => { + if lhs.is_primary_key_of(table) { + match operator { + ast::Operator::Equals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { table: table.0.clone(), cmp_expr: *rhs })); + } + ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { table: table.0.clone(), cmp_op: operator, cmp_expr: *rhs })); + } + _ => {} } } - if let Some(rhs_table_index) = rhs.check_primary_key(referenced_tables)? { - if rhs_table_index == table_index { - return Ok((true, *lhs)); + if rhs.is_primary_key_of(table) { + match operator { + ast::Operator::Equals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { table: table.0.clone(), cmp_expr: *lhs })); + } + ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { table: table.0.clone(), cmp_op: operator, cmp_expr: *lhs })); + } + _ => {} + } + } + + if let Some(index_index) = lhs.check_index_scan(table, available_indexes)? { + match operator { + ast::Operator::Equals | + ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *rhs })); + } + _ => {} + } + } + + if let Some(index_index) = rhs.check_index_scan(table, available_indexes)? { + match operator { + ast::Operator::Equals | + ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *lhs })); + } + _ => {} } } - Ok((false, ast::Expr::Binary(lhs, ast::Operator::Equals, rhs))) + Ok(Either::Left(ast::Expr::Binary(lhs, operator, rhs))) } - _ => Ok((false, expr)), + _ => Ok(Either::Left(expr)), } } diff --git a/core/translate/plan.rs b/core/translate/plan.rs index e7e02f23c..24a3b6513 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -129,12 +129,11 @@ pub enum Operator { predicates: Option>, step: usize, }, - IndexScan { + Search { id: usize, - index: Rc, + index: Option>, seek_cmp: ast::Operator, seek_expr: ast::Expr, - index_predicate: ast::Expr, table: Rc, table_identifier: String, predicates: Option>, @@ -189,7 +188,7 @@ impl Operator { .map(|e| e.column_count(referenced_tables)) .sum(), Operator::Scan { table, .. } => table.columns.len(), - Operator::IndexScan { table, .. } => table.columns.len(), + Operator::Search { table, .. } => table.columns.len(), Operator::Nothing => 0, } } @@ -244,7 +243,7 @@ impl Operator { }) .collect(), Operator::Scan { table, .. } => table.columns.iter().map(|c| c.name.clone()).collect(), - Operator::IndexScan { table, .. } => { + Operator::Search { table, .. } => { table.columns.iter().map(|c| c.name.clone()).collect() } Operator::Nothing => vec![], @@ -261,7 +260,7 @@ impl Operator { Operator::Order { id, .. } => *id, Operator::Projection { id, .. } => *id, Operator::Scan { id, .. } => *id, - Operator::IndexScan { id, .. } => *id, + Operator::Search { id, .. } => *id, Operator::Nothing => unreachable!(), } } @@ -451,7 +450,7 @@ impl Display for Operator { }?; Ok(()) } - Operator::IndexScan { table, .. } => { + Operator::Search { table, .. } => { writeln!(f, "{}INDEX SCAN {}", indent, table.name)?; Ok(()) } @@ -515,7 +514,7 @@ pub fn get_table_ref_bitmask_for_operator<'a>( .position(|(t, _)| Rc::ptr_eq(t, table)) .unwrap(); } - Operator::IndexScan { table, .. } => { + Operator::Search { table, .. } => { table_refs_mask |= 1 << tables .iter() diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 1c3dba4e6..deca4497c 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -277,8 +277,6 @@ pub fn prepare_select_plan<'a>(schema: &Schema, select: ast::Select) -> Result

Result<()>; fn rowid(&self) -> Result>; fn seek_rowid(&mut self, rowid: u64) -> Result>; - fn seek_ge(&mut self, key: &OwnedRecord) -> Result>; - fn seek_gt(&mut self, key: &OwnedRecord) -> Result>; + fn seek_ge_rowid(&mut self, rowid: u64) -> Result>; + fn seek_gt_rowid(&mut self, rowid: u64) -> Result>; + fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result>; + fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result>; fn seek_to_last(&mut self) -> Result>; fn record(&self) -> Result>>; fn insert( diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 25b6de9ca..669c159b0 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -534,6 +534,7 @@ pub fn insn_to_str( "".to_string(), ), Insn::SeekGT { + is_index, cursor_id, start_reg, num_regs, @@ -548,6 +549,7 @@ pub fn insn_to_str( "".to_string(), ), Insn::SeekGE { + is_index, cursor_id, start_reg, num_regs, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index d761ed664..0179f9072 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -305,16 +305,22 @@ pub enum Insn { table_cursor_id: CursorID, }, + // If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. + // If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. // Seek to the first index entry that is greater than or equal to the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekGE { + is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, + // If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. + // If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. // Seek to the first index entry that is greater than the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekGT { + is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, @@ -1190,21 +1196,59 @@ impl Program { start_reg, num_regs, target_pc, + is_index, } => { - let cursor = cursors.get_mut(cursor_id).unwrap(); - let record_from_regs: OwnedRecord = - make_owned_record(&state.registers, start_reg, num_regs); - match cursor.seek_ge(&record_from_regs)? { - CursorResult::Ok(found) => { - if !found { - state.pc = *target_pc; - } else { - state.pc += 1; + if *is_index { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + match cursor.seek_ge_index(&record_from_regs)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); } } - CursorResult::IO => { - // If there is I/O, the instruction is restarted. - return Ok(StepResult::IO); + } else { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let rowid = match &state.registers[*start_reg] { + OwnedValue::Null => { + // All integer values are greater than null so we just rewind the cursor + match cursor.rewind()? { + CursorResult::Ok(()) => {} + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } + } + state.pc += 1; + continue; + } + OwnedValue::Integer(rowid) => *rowid as u64, + _ => { + return Err(LimboError::InternalError( + "SeekRowid: the value in the register is not an integer".into(), + )); + } + }; + match cursor.seek_ge_rowid(rowid)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } } } } @@ -1213,21 +1257,59 @@ impl Program { start_reg, num_regs, target_pc, + is_index, } => { - let cursor = cursors.get_mut(cursor_id).unwrap(); - let record_from_regs: OwnedRecord = - make_owned_record(&state.registers, start_reg, num_regs); - match cursor.seek_gt(&record_from_regs)? { - CursorResult::Ok(found) => { - if !found { - state.pc = *target_pc; - } else { - state.pc += 1; + if *is_index { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let record_from_regs: OwnedRecord = + make_owned_record(&state.registers, start_reg, num_regs); + match cursor.seek_gt_index(&record_from_regs)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); } } - CursorResult::IO => { - // If there is I/O, the instruction is restarted. - return Ok(StepResult::IO); + } else { + let cursor = cursors.get_mut(cursor_id).unwrap(); + let rowid = match &state.registers[*start_reg] { + OwnedValue::Null => { + // All integer values are greater than null so we just rewind the cursor + match cursor.rewind()? { + CursorResult::Ok(()) => {} + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } + } + state.pc += 1; + continue; + } + OwnedValue::Integer(rowid) => *rowid as u64, + _ => { + return Err(LimboError::InternalError( + "SeekRowid: the value in the register is not an integer".into(), + )); + } + }; + match cursor.seek_gt_rowid(rowid)? { + CursorResult::Ok(found) => { + if !found { + state.pc = *target_pc; + } else { + state.pc += 1; + } + } + CursorResult::IO => { + // If there is I/O, the instruction is restarted. + return Ok(StepResult::IO); + } } } } diff --git a/core/vdbe/sorter.rs b/core/vdbe/sorter.rs index 75980f222..e75650f26 100644 --- a/core/vdbe/sorter.rs +++ b/core/vdbe/sorter.rs @@ -79,11 +79,19 @@ impl Cursor for Sorter { unimplemented!(); } - fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { + fn seek_ge_rowid(&mut self, _: u64) -> Result> { unimplemented!(); } - fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { + fn seek_gt_rowid(&mut self, _: u64) -> Result> { + unimplemented!(); + } + + fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { + unimplemented!(); + } + + fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { unimplemented!(); } diff --git a/testing/cmdlineshell.test b/testing/cmdlineshell.test index 599dcfeb4..d68a341a0 100755 --- a/testing/cmdlineshell.test +++ b/testing/cmdlineshell.test @@ -21,7 +21,8 @@ CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, price REAL - );}} + );} +"CREATE INDEX age_idx on users (age);"} do_execsql_test schema-1 { .schema users From e118b7012743f2c364c9c62bf40ceea71b2645a0 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 12:08:40 +0300 Subject: [PATCH 03/32] fmt --- core/storage/btree.rs | 26 +++-- core/translate/emitter.rs | 23 +++-- core/translate/optimizer.rs | 189 +++++++++++++++++++++++------------- 3 files changed, 150 insertions(+), 88 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 87261681a..b326a956e 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -153,7 +153,8 @@ impl BTreeCursor { } BTreeCell::IndexInteriorCell(IndexInteriorCell { payload, - left_child_page, .. + left_child_page, + .. }) => { mem_page.advance(); let mem_page = @@ -364,7 +365,9 @@ impl BTreeCursor { } } BTreeCell::TableLeafCell(_) => { - unreachable!("we don't iterate leaf cells while trying to move to a leaf cell"); + unreachable!( + "we don't iterate leaf cells while trying to move to a leaf cell" + ); } BTreeCell::IndexInteriorCell(_) => { unimplemented!(); @@ -531,11 +534,7 @@ impl BTreeCursor { } } - fn move_to_index_leaf( - &mut self, - key: &OwnedRecord, - cmp: SeekOp, - ) -> Result> { + fn move_to_index_leaf(&mut self, key: &OwnedRecord, cmp: SeekOp) -> Result> { self.move_to_root(); loop { let mem_page = self.get_mem_page(); @@ -1539,8 +1538,17 @@ impl Cursor for BTreeCursor { CursorResult::Ok((rowid, next)) => { { let curr_rowid = self.rowid.borrow(); - if curr_rowid.is_some() && curr_rowid.unwrap() >= 8000 && rowid.is_some() && rowid.unwrap() < 8000 { - println!("curr_rowid: {:?}, rowid: {:?}, next: {:?}", curr_rowid.unwrap(), rowid.unwrap(), next); + if curr_rowid.is_some() + && curr_rowid.unwrap() >= 8000 + && rowid.is_some() + && rowid.unwrap() < 8000 + { + println!( + "curr_rowid: {:?}, rowid: {:?}, next: {:?}", + curr_rowid.unwrap(), + rowid.unwrap(), + next + ); } } self.rowid.replace(rowid); diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 5693f134d..90d35ec72 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -297,7 +297,7 @@ impl Emitter for Operator { if let Some(index) = index { program.emit_insn(Insn::OpenReadAsync { cursor_id: index_cursor_id, - root_page: index.root_page, + root_page: index.root_page, }); program.emit_insn(Insn::OpenReadAwait); } @@ -383,7 +383,10 @@ impl Emitter for Operator { // // For primary key searches we emit RowId and then compare it to the seek value. - let abort_jump_target = *m.next_row_labels.get(id).unwrap_or(m.termination_label_stack.last().unwrap()); + let abort_jump_target = *m + .next_row_labels + .get(id) + .unwrap_or(m.termination_label_stack.last().unwrap()); match seek_cmp { ast::Operator::Equals | ast::Operator::LessEquals => { if index.is_some() { @@ -394,7 +397,7 @@ impl Emitter for Operator { num_regs: 1, target_pc: abort_jump_target, }, - abort_jump_target, + abort_jump_target, ); } else { let rowid_reg = program.alloc_register(); @@ -414,12 +417,12 @@ impl Emitter for Operator { } ast::Operator::Less => { if index.is_some() { - program.emit_insn_with_label_dependency( - Insn::IdxGE { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: abort_jump_target, + program.emit_insn_with_label_dependency( + Insn::IdxGE { + cursor_id: index_cursor_id, + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, }, abort_jump_target, ); @@ -448,7 +451,7 @@ impl Emitter for Operator { table_cursor_id, }); } - + let jump_label = m .next_row_labels .get(id) diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 54b7e083b..6933116c8 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -76,50 +76,56 @@ fn use_indexes( Either::Left(non_index_using_expr) => { fs[i] = non_index_using_expr; } - Either::Right(index_using_expr) => { - match index_using_expr { - SearchableExpr::IndexSearch { index, cmp_op, cmp_expr } => { - fs.remove(i); - *operator = Operator::Search { - table: table.0.clone(), - index: Some(index.clone()), - predicates: Some(std::mem::take(fs)), - seek_cmp: cmp_op, - seek_expr: cmp_expr, - table_identifier: table_identifier.clone(), - id: *id, - step: 0, - }; - return Ok(()); - } - SearchableExpr::PrimaryKeySearch { table, cmp_op, cmp_expr } => { - fs.remove(i); - *operator = Operator::Search { - table, - index: None, - predicates: Some(std::mem::take(fs)), - seek_cmp: cmp_op, - seek_expr: cmp_expr, - table_identifier: table_identifier.clone(), - id: *id, - step: 0, - }; - return Ok(()); - } - SearchableExpr::PrimaryKeyEq { cmp_expr, table } => { - fs.remove(i); - *operator = Operator::SeekRowid { - table, - table_identifier: table_identifier.clone(), - rowid_predicate: cmp_expr, - predicates: Some(std::mem::take(fs)), - id: *id, - step: 0, - }; - return Ok(()); - } + Either::Right(index_using_expr) => match index_using_expr { + SearchableExpr::IndexSearch { + index, + cmp_op, + cmp_expr, + } => { + fs.remove(i); + *operator = Operator::Search { + table: table.0.clone(), + index: Some(index.clone()), + predicates: Some(std::mem::take(fs)), + seek_cmp: cmp_op, + seek_expr: cmp_expr, + table_identifier: table_identifier.clone(), + id: *id, + step: 0, + }; + return Ok(()); } - } + SearchableExpr::PrimaryKeySearch { + table, + cmp_op, + cmp_expr, + } => { + fs.remove(i); + *operator = Operator::Search { + table, + index: None, + predicates: Some(std::mem::take(fs)), + seek_cmp: cmp_op, + seek_expr: cmp_expr, + table_identifier: table_identifier.clone(), + id: *id, + step: 0, + }; + return Ok(()); + } + SearchableExpr::PrimaryKeyEq { cmp_expr, table } => { + fs.remove(i); + *operator = Operator::SeekRowid { + table, + table_identifier: table_identifier.clone(), + rowid_predicate: cmp_expr, + predicates: Some(std::mem::take(fs)), + id: *id, + step: 0, + }; + return Ok(()); + } + }, } } @@ -946,40 +952,51 @@ pub trait Optimizable { .map_or(false, |c| c == ConstantPredicate::AlwaysFalse)) } // if the expression is the primary key of a table, returns the index of the table - fn is_primary_key_of( - &self, + fn is_primary_key_of(&self, table: &(Rc, String)) -> bool; + fn check_index_scan( + &mut self, table: &(Rc, String), - ) -> bool; - fn check_index_scan(&mut self, table: &(Rc, String), available_indexes: &[Rc]) -> Result>; + available_indexes: &[Rc], + ) -> Result>; } impl Optimizable for ast::Expr { - fn is_primary_key_of( - &self, - table: &(Rc, String), - ) -> bool { + fn is_primary_key_of(&self, table: &(Rc, String)) -> bool { match self { ast::Expr::Id(ident) => { let ident = normalize_ident(&ident.0); - table.0.get_column(&ident).map_or(false, |(_, c)| c.primary_key) + table + .0 + .get_column(&ident) + .map_or(false, |(_, c)| c.primary_key) } ast::Expr::Qualified(tbl, ident) => { let tbl = normalize_ident(&tbl.0); let ident = normalize_ident(&ident.0); - - tbl == table.1 && table.0.get_column(&ident).map_or(false, |(_, c)| c.primary_key) + + tbl == table.1 + && table + .0 + .get_column(&ident) + .map_or(false, |(_, c)| c.primary_key) } _ => false, } } - fn check_index_scan(&mut self, table: &(Rc, String), available_indexes: &[Rc]) -> Result> { + fn check_index_scan( + &mut self, + table: &(Rc, String), + available_indexes: &[Rc], + ) -> Result> { match self { ast::Expr::Id(ident) => { let ident = normalize_ident(&ident.0); let indexes = available_indexes .iter() .enumerate() - .filter(|(_, i)| i.table_name == table.1 && i.columns.iter().any(|c| c.name == ident)) + .filter(|(_, i)| { + i.table_name == table.1 && i.columns.iter().any(|c| c.name == ident) + }) .collect::>(); if indexes.is_empty() { return Ok(None); @@ -1147,7 +1164,7 @@ pub enum Either { /// An expression that can be used to search for a row in a table using an index /// (i.e. a primary key or a secondary index) -/// +/// pub enum SearchableExpr { /// A primary key equality search. This is a special case of the primary key search /// that uses the SeekRowid operator and bytecode instruction. @@ -1179,10 +1196,20 @@ pub fn try_extract_expr_that_utilizes_index( if lhs.is_primary_key_of(table) { match operator { ast::Operator::Equals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { table: table.0.clone(), cmp_expr: *rhs })); + return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { + table: table.0.clone(), + cmp_expr: *rhs, + })); } - ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { table: table.0.clone(), cmp_op: operator, cmp_expr: *rhs })); + ast::Operator::Greater + | ast::Operator::GreaterEquals + | ast::Operator::Less + | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { + table: table.0.clone(), + cmp_op: operator, + cmp_expr: *rhs, + })); } _ => {} } @@ -1191,10 +1218,20 @@ pub fn try_extract_expr_that_utilizes_index( if rhs.is_primary_key_of(table) { match operator { ast::Operator::Equals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { table: table.0.clone(), cmp_expr: *lhs })); + return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { + table: table.0.clone(), + cmp_expr: *lhs, + })); } - ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { table: table.0.clone(), cmp_op: operator, cmp_expr: *lhs })); + ast::Operator::Greater + | ast::Operator::GreaterEquals + | ast::Operator::Less + | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { + table: table.0.clone(), + cmp_op: operator, + cmp_expr: *lhs, + })); } _ => {} } @@ -1202,9 +1239,16 @@ pub fn try_extract_expr_that_utilizes_index( if let Some(index_index) = lhs.check_index_scan(table, available_indexes)? { match operator { - ast::Operator::Equals | - ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *rhs })); + ast::Operator::Equals + | ast::Operator::Greater + | ast::Operator::GreaterEquals + | ast::Operator::Less + | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::IndexSearch { + index: available_indexes[index_index].clone(), + cmp_op: operator, + cmp_expr: *rhs, + })); } _ => {} } @@ -1212,9 +1256,16 @@ pub fn try_extract_expr_that_utilizes_index( if let Some(index_index) = rhs.check_index_scan(table, available_indexes)? { match operator { - ast::Operator::Equals | - ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *lhs })); + ast::Operator::Equals + | ast::Operator::Greater + | ast::Operator::GreaterEquals + | ast::Operator::Less + | ast::Operator::LessEquals => { + return Ok(Either::Right(SearchableExpr::IndexSearch { + index: available_indexes[index_index].clone(), + cmp_op: operator, + cmp_expr: *lhs, + })); } _ => {} } From 99871bbeea2ffd096de822f3600e316d37ec3895 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 12:23:26 +0300 Subject: [PATCH 04/32] yield on io --- core/storage/btree.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index b326a956e..ef9837418 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -180,7 +180,10 @@ impl BTreeCursor { key: &OwnedRecord, op: SeekOp, ) -> Result, Option)>> { - self.move_to_index_leaf(key, op.clone())?; + match self.move_to_index_leaf(key, op.clone())? { + CursorResult::Ok(_) => {} + CursorResult::IO => return Ok(CursorResult::IO), + }; let mem_page = self.get_mem_page(); let page_idx = mem_page.page_idx; @@ -229,7 +232,10 @@ impl BTreeCursor { rowid: u64, op: SeekOp, ) -> Result, Option)>> { - self.move_to_table_leaf(rowid, op.clone())?; + match self.move_to_table_leaf(rowid, op.clone())? { + CursorResult::Ok(_) => {} + CursorResult::IO => return Ok(CursorResult::IO), + }; let mem_page = self.get_mem_page(); let page_idx = mem_page.page_idx; @@ -1536,21 +1542,6 @@ impl Cursor for BTreeCursor { fn next(&mut self) -> Result> { match self.get_next_record()? { CursorResult::Ok((rowid, next)) => { - { - let curr_rowid = self.rowid.borrow(); - if curr_rowid.is_some() - && curr_rowid.unwrap() >= 8000 - && rowid.is_some() - && rowid.unwrap() < 8000 - { - println!( - "curr_rowid: {:?}, rowid: {:?}, next: {:?}", - curr_rowid.unwrap(), - rowid.unwrap(), - next - ); - } - } self.rowid.replace(rowid); self.record.replace(next); Ok(CursorResult::Ok(())) From ff236c7781e0f05804948eb8abb5fccb0aea4a17 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 14:10:06 +0300 Subject: [PATCH 05/32] Fix not advancing the cell index of pages --- core/storage/btree.rs | 19 ++++++++++--------- testing/where.test | 17 +++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index ef9837418..348839803 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -29,6 +29,7 @@ pub enum SeekOp { GE, } +#[derive(Debug)] pub struct MemPage { parent: Option>, page_idx: usize, @@ -249,17 +250,18 @@ impl BTreeCursor { let page = page.as_ref().unwrap(); for cell_idx in 0..page.cell_count() { - match &page.cell_get( + let cell = page.cell_get( cell_idx, self.pager.clone(), self.max_local(page.page_type()), self.min_local(page.page_type()), self.usable_space(), - )? { + )?; + match &cell { BTreeCell::TableLeafCell(TableLeafCell { _rowid: cell_rowid, _payload: payload, - first_overflow_page: _, + first_overflow_page: fop, }) => { mem_page.advance(); let comparison = match op { @@ -346,23 +348,24 @@ impl BTreeCursor { let mut found_cell = false; for cell_idx in 0..page.cell_count() { - match &page.cell_get( + let cell = page.cell_get( cell_idx, self.pager.clone(), self.max_local(page.page_type()), self.min_local(page.page_type()), self.usable_space(), - )? { + )?; + match &cell { BTreeCell::TableInteriorCell(TableInteriorCell { _left_child_page, _rowid, }) => { + mem_page.advance(); let comparison = match cmp { SeekOp::GT => *_rowid > rowid, SeekOp::GE => *_rowid >= rowid, }; if comparison { - mem_page.advance(); let mem_page = MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); @@ -571,15 +574,13 @@ impl BTreeCursor { payload, .. }) => { - // get the logic for this from btree_index_seek - + mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { SeekOp::GT => record > *key, SeekOp::GE => record >= *key, }; if comparison { - mem_page.advance(); let mem_page = MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); diff --git a/testing/where.test b/testing/where.test index 4cb5d1978..fb4e2c10f 100755 --- a/testing/where.test +++ b/testing/where.test @@ -92,24 +92,25 @@ do_execsql_test where-clause-no-table-constant-condition-false-7 { select 1 where 'hamburger'; } {} +# this test functions as an assertion that the index on users.age is being used, since the results are ordered by age without an order by. do_execsql_test select-where-and { select first_name, age from users where first_name = 'Jamie' and age > 80 -} {Jamie|94 +} {Jamie|87 Jamie|88 -Jamie|99 -Jamie|92 -Jamie|87 Jamie|88 +Jamie|92 +Jamie|94 +Jamie|99 } do_execsql_test select-where-or { select first_name, age from users where first_name = 'Jamie' and age > 80 -} {Jamie|94 +} {Jamie|87 Jamie|88 -Jamie|99 -Jamie|92 -Jamie|87 Jamie|88 +Jamie|92 +Jamie|94 +Jamie|99 } do_execsql_test select-where-and-or { From ed19f477621d5e7f64a984e62c8355f22d1bbf37 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 16:05:20 +0300 Subject: [PATCH 06/32] fix --- core/storage/btree.rs | 14 +++++++------- testing/where.test | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 348839803..0052f1c84 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -328,7 +328,7 @@ impl BTreeCursor { Ok(CursorResult::Ok((None, None))) } - fn move_to_table_leaf(&mut self, rowid: u64, cmp: SeekOp) -> Result> { + fn move_to_table_leaf(&mut self, key: u64, cmp: SeekOp) -> Result> { self.move_to_root(); loop { @@ -362,8 +362,8 @@ impl BTreeCursor { }) => { mem_page.advance(); let comparison = match cmp { - SeekOp::GT => *_rowid > rowid, - SeekOp::GE => *_rowid >= rowid, + SeekOp::GT => key <= *_rowid, + SeekOp::GE => key < *_rowid, }; if comparison { let mem_page = @@ -388,10 +388,10 @@ impl BTreeCursor { } if !found_cell { - let parent = mem_page.clone(); + let parent = mem_page.parent.clone(); match page.rightmost_pointer() { Some(right_most_pointer) => { - let mem_page = MemPage::new(Some(parent), right_most_pointer as usize, 0); + let mem_page = MemPage::new(parent, right_most_pointer as usize, 0); self.page.replace(Some(Rc::new(mem_page))); continue; } @@ -597,10 +597,10 @@ impl BTreeCursor { } if !found_cell { - let parent = mem_page.clone(); + let parent = mem_page.parent.clone(); match page.rightmost_pointer() { Some(right_most_pointer) => { - let mem_page = MemPage::new(Some(parent), right_most_pointer as usize, 0); + let mem_page = MemPage::new(parent, right_most_pointer as usize, 0); self.page.replace(Some(Rc::new(mem_page))); continue; } diff --git a/testing/where.test b/testing/where.test index fb4e2c10f..53ec71fdf 100755 --- a/testing/where.test +++ b/testing/where.test @@ -268,3 +268,21 @@ do_execsql_test where-complex-parentheses { select id, name from products where ((id = 5 and name = 'sweatshirt') or (id = 1 and name = 'hat')) and (name = 'sweatshirt' or name = 'hat') ORDER BY id; } {1|hat 5|sweatshirt} + +# regression test for primary key index behavior +do_execsql_test where_id_index_seek_test { + select id from users where id > 9995; +} {9996 +9997 +9998 +9999 +10000} + +# regression test for secondary index (users.age) behavior +do_execsql_test where_id_index_seek_test { + select id,age from users where age >= 100 limit 5; +} {186|100 +198|100 +301|100 +364|100 +460|100} From 3a118871228c94478b512cba13f9e4461d926190 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 16:35:29 +0300 Subject: [PATCH 07/32] fixerinos --- core/storage/btree.rs | 1 + core/translate/optimizer.rs | 5 ++--- core/vdbe/mod.rs | 24 ++++++++++++++++++------ testing/join.test | 6 ++++++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 0052f1c84..a8b24bf9d 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -314,6 +314,7 @@ impl BTreeCursor { _payload: p, first_overflow_page: _, }) => { + mem_page.advance(); if *cell_rowid == rowid { let record = crate::storage::sqlite3_ondisk::read_record(p)?; return Ok(CursorResult::Ok((Some(*cell_rowid), Some(record)))); diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 6933116c8..270481e3d 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -1006,11 +1006,10 @@ impl Optimizable for ast::Expr { } Ok(Some(indexes.first().unwrap().0)) } - ast::Expr::Qualified(tbl, ident) => { - let tbl = normalize_ident(&tbl.0); + ast::Expr::Qualified(_, ident) => { let ident = normalize_ident(&ident.0); let index = available_indexes.iter().enumerate().find(|(_, i)| { - if i.table_name != tbl { + if i.table_name != table.0.name { return false; } i.columns.iter().any(|c| normalize_ident(&c.name) == ident) diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 0179f9072..5954c6014 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1164,9 +1164,13 @@ impl Program { let cursor = cursors.get_mut(cursor_id).unwrap(); let rowid = match &state.registers[*src_reg] { OwnedValue::Integer(rowid) => *rowid as u64, - _ => { + OwnedValue::Null => { + state.pc = *target_pc; + continue; + } + other => { return Err(LimboError::InternalError( - "SeekRowid: the value in the register is not an integer".into(), + format!("SeekRowid: the value in the register is not an integer or NULL: {}", other) )); } }; @@ -1233,7 +1237,7 @@ impl Program { OwnedValue::Integer(rowid) => *rowid as u64, _ => { return Err(LimboError::InternalError( - "SeekRowid: the value in the register is not an integer".into(), + "SeekGE: the value in the register is not an integer".into(), )); } }; @@ -1294,7 +1298,7 @@ impl Program { OwnedValue::Integer(rowid) => *rowid as u64, _ => { return Err(LimboError::InternalError( - "SeekRowid: the value in the register is not an integer".into(), + "SeekGT: the value in the register is not an integer".into(), )); } }; @@ -2609,11 +2613,19 @@ mod tests { self.seek_rowid(rowid) } - fn seek_ge(&mut self, key: &OwnedRecord) -> Result> { + fn seek_ge_rowid(&mut self, _: u64) -> Result> { + unimplemented!(); + } + + fn seek_gt_rowid(&mut self, _: u64) -> Result> { + unimplemented!(); + } + + fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { unimplemented!(); } - fn seek_gt(&mut self, key: &OwnedRecord) -> Result> { + fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { unimplemented!(); } diff --git a/testing/join.test b/testing/join.test index 77cb040c4..887fc2af3 100755 --- a/testing/join.test +++ b/testing/join.test @@ -200,3 +200,9 @@ Jamie||Edward} do_execsql_test left-join-constant-condition-true-inner-join-constant-condition-false { select u.first_name, p.name, u2.first_name from users u left join products as p on 1 join users u2 on 0 limit 5; } {} + +do_execsql_test join-utilizing-both-seekrowid-and-secondary-index { + select u.first_name, p.name from users u join products p on u.id = p.id and u.age > 70; +} {Matthew|boots +Nicholas|shorts +Jamie|hat} \ No newline at end of file From 02d6fa31d3ba7e2df084d937a9d6006ed0cfe5c0 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 16:50:07 +0300 Subject: [PATCH 08/32] Fix .schema users not displaying indexes on the users table --- cli/main.rs | 2 +- testing/cmdlineshell.test | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/main.rs b/cli/main.rs index 81eae98e3..6f887902f 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -201,7 +201,7 @@ fn display_schema( ) -> anyhow::Result<()> { let sql = match table { Some(table_name) => format!( - "SELECT sql FROM sqlite_schema WHERE type='table' AND name = '{}' AND name NOT LIKE 'sqlite_%'", + "SELECT sql FROM sqlite_schema WHERE type IN ('table', 'index') AND tbl_name = '{}' AND name NOT LIKE 'sqlite_%'", table_name ), None => String::from( diff --git a/testing/cmdlineshell.test b/testing/cmdlineshell.test index d68a341a0..f1c6f0261 100755 --- a/testing/cmdlineshell.test +++ b/testing/cmdlineshell.test @@ -5,7 +5,7 @@ source $testdir/tester.tcl do_execsql_test schema { .schema -} {{CREATE TABLE users ( +} {"CREATE TABLE users ( id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, @@ -21,12 +21,12 @@ CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, price REAL - );} -"CREATE INDEX age_idx on users (age);"} + ); +CREATE INDEX age_idx on users (age);"} do_execsql_test schema-1 { .schema users -} {{CREATE TABLE users ( +} {"CREATE TABLE users ( id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT, @@ -37,7 +37,8 @@ do_execsql_test schema-1 { state TEXT, zipcode TEXT, age INTEGER - );}} + ); +CREATE INDEX age_idx on users (age);"} do_execsql_test schema-2 { .schema products From fe90aacd35a9ce0db5eb7ea08e157a7e6bc94cf0 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 16:56:22 +0300 Subject: [PATCH 09/32] Handle CursorResult in deferred seek --- core/vdbe/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 5954c6014..716380020 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1011,7 +1011,13 @@ impl Program { let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); let rowid = index_cursor.rowid()?; let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); - table_cursor.seek_rowid(rowid.unwrap())?; + match table_cursor.seek_rowid(rowid.unwrap())? { + CursorResult::Ok(_) => {} + CursorResult::IO => { + state.deferred_seek = Some((index_cursor_id, table_cursor_id)); + return Ok(StepResult::IO); + } + } } let cursor = cursors.get_mut(cursor_id).unwrap(); From 9169f6e39b9826bdae5acd37d800794de36efe87 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 16:57:06 +0300 Subject: [PATCH 10/32] same thing --- core/vdbe/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 716380020..c66edb0c9 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1151,7 +1151,13 @@ impl Program { let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); let rowid = index_cursor.rowid()?; let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); - table_cursor.seek_rowid(rowid.unwrap())?; + match table_cursor.seek_rowid(rowid.unwrap())? { + CursorResult::Ok(_) => {} + CursorResult::IO => { + state.deferred_seek = Some((index_cursor_id, table_cursor_id)); + return Ok(StepResult::IO); + } + } } let cursor = cursors.get_mut(cursor_id).unwrap(); From 43015f6949783d7639a7407a5f4d43a9cd05a1bf Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:05:35 +0300 Subject: [PATCH 11/32] Workaround for compat test --- testing/agg-functions.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/agg-functions.test b/testing/agg-functions.test index e7c15e1c7..fbc007b62 100755 --- a/testing/agg-functions.test +++ b/testing/agg-functions.test @@ -28,8 +28,8 @@ do_execsql_test select-total-text { } {0.0} do_execsql_test select-limit { - SELECT id FROM users LIMIT 1; -} {1} + SELECT typeof(id) FROM users LIMIT 1; +} {integer} do_execsql_test select-count { SELECT count(id) FROM users; From d8a695a991c36e58a868d26972ddaaa1976e8852 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:06:08 +0300 Subject: [PATCH 12/32] rename tests --- testing/where.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/where.test b/testing/where.test index 53ec71fdf..4f5915a49 100755 --- a/testing/where.test +++ b/testing/where.test @@ -270,7 +270,7 @@ do_execsql_test where-complex-parentheses { 5|sweatshirt} # regression test for primary key index behavior -do_execsql_test where_id_index_seek_test { +do_execsql_test where-id-index-seek-test { select id from users where id > 9995; } {9996 9997 @@ -279,7 +279,7 @@ do_execsql_test where_id_index_seek_test { 10000} # regression test for secondary index (users.age) behavior -do_execsql_test where_id_index_seek_test { +do_execsql_test where-id-index-seek-test-2 { select id,age from users where age >= 100 limit 5; } {186|100 198|100 From db0e2ea54fe3d5dae657d29a8dd542c31783a113 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:10:46 +0300 Subject: [PATCH 13/32] Change another compat test to work around sqlite's weird choice to use the age index --- testing/select.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/select.test b/testing/select.test index ced47d820..d69f3c07e 100755 --- a/testing/select.test +++ b/testing/select.test @@ -44,8 +44,8 @@ do_execsql_test table-star { } {1|hat|79.0|hat} do_execsql_test table-star-2 { - select p.*, u.age from users u join products p limit 1; -} {1|hat|79.0|94} + select p.*, u.first_name from users u join products p on u.id = p.id limit 1; +} {1|hat|79.0|Jamie} do_execsql_test seekrowid { select * from users u where u.id = 5; From d2233d69d3702da529c1f0e8a745e104834c2ea9 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:13:43 +0300 Subject: [PATCH 14/32] Dont assume the rowid is the second column - it's the last --- core/storage/btree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index a8b24bf9d..fd1535d69 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -166,8 +166,8 @@ impl BTreeCursor { BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; - let rowid = match record.values[1] { - OwnedValue::Integer(rowid) => rowid as u64, + let rowid = match record.values.last() { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, _ => unreachable!("index cells should have an integer rowid"), }; return Ok(CursorResult::Ok((Some(rowid), Some(record)))); From 3826d4e1ff6501375dae3df4e96c22ba328b70b4 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:17:33 +0300 Subject: [PATCH 15/32] Add comment about code duplication --- core/storage/btree.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index fd1535d69..b702cc802 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -544,6 +544,8 @@ impl BTreeCursor { } } + // TODO: there is a lot of code duplication here between move_to, move_to_index_leaf and move_to_table_leaf. + // I wanted to get this working first, but this should really be refactored. - Jussi fn move_to_index_leaf(&mut self, key: &OwnedRecord, cmp: SeekOp) -> Result> { self.move_to_root(); loop { From d22dbe9840cd16cc758e5f9666497459da8b7b1e Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 17:26:54 +0300 Subject: [PATCH 16/32] remove garbage comment --- core/translate/optimizer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 270481e3d..da08bab1d 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -951,7 +951,6 @@ pub trait Optimizable { .check_constant()? .map_or(false, |c| c == ConstantPredicate::AlwaysFalse)) } - // if the expression is the primary key of a table, returns the index of the table fn is_primary_key_of(&self, table: &(Rc, String)) -> bool; fn check_index_scan( &mut self, From d3e797f59edcefff860ecb410c6a6a44667e48bc Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sat, 5 Oct 2024 18:26:50 +0300 Subject: [PATCH 17/32] rewind_labels was renamed to scan_loop_body_labels --- core/translate/emitter.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 90d35ec72..99c56304e 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -286,8 +286,8 @@ impl Emitter for Operator { let next_row_label = program.allocate_label(); m.next_row_labels.insert(*id, next_row_label); - let rewind_label = program.allocate_label(); - m.rewind_labels.push(rewind_label); + let scan_loop_body_label = program.allocate_label(); + m.scan_loop_body_labels.push(scan_loop_body_label); program.emit_insn(Insn::OpenReadAsync { cursor_id: table_cursor_id, root_page: table.root_page, @@ -310,7 +310,7 @@ impl Emitter for Operator { } else { table_cursor_id }; - let rewind_label = *m.rewind_labels.last().unwrap(); + let scan_loop_body_label = *m.scan_loop_body_labels.last().unwrap(); let cmp_reg = program.alloc_register(); // TODO this only handles ascending indexes match seek_cmp { @@ -371,7 +371,10 @@ impl Emitter for Operator { )?; } - program.defer_label_resolution(rewind_label, program.offset() as usize); + program.defer_label_resolution( + scan_loop_body_label, + program.offset() as usize, + ); // 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. @@ -486,7 +489,7 @@ impl Emitter for Operator { program .resolve_label(*m.next_row_labels.get(id).unwrap(), program.offset()); program.emit_insn(Insn::NextAsync { cursor_id }); - let jump_label = m.rewind_labels.pop().unwrap(); + let jump_label = m.scan_loop_body_labels.pop().unwrap(); program.emit_insn_with_label_dependency( Insn::NextAwait { cursor_id, From 47534cb8df2e1ddd71692d0ab5ba3569d93dbe0c Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:11:38 +0300 Subject: [PATCH 18/32] Get rid of Seekrowid operator in favor of a unified Search operator --- core/translate/emitter.rs | 455 ++++++++++++++++-------------------- core/translate/optimizer.rs | 164 ++++--------- core/translate/plan.rs | 79 ++----- 3 files changed, 268 insertions(+), 430 deletions(-) diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 99c56304e..cb0ed95c4 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -7,6 +7,7 @@ use sqlite3_parser::ast; use crate::schema::{BTreeTable, Column, PseudoTable, Table}; use crate::storage::sqlite3_ondisk::DatabaseHeader; use crate::translate::expr::resolve_ident_pseudo_table; +use crate::translate::plan::Search; use crate::types::{OwnedRecord, OwnedValue}; use crate::vdbe::builder::ProgramBuilder; use crate::vdbe::{BranchOffset, Insn, Program}; @@ -176,7 +177,7 @@ impl Emitter for Operator { } => { *step += 1; const SCAN_OPEN_READ: usize = 1; - const SCAN_REWIND_AND_CONDITIONS: usize = 2; + const SCAN_BODY: usize = 2; const SCAN_NEXT: usize = 3; match *step { SCAN_OPEN_READ => { @@ -195,7 +196,7 @@ impl Emitter for Operator { Ok(OpStepResult::Continue) } - SCAN_REWIND_AND_CONDITIONS => { + SCAN_BODY => { let cursor_id = program.resolve_cursor_id(table_identifier, None); program.emit_insn(Insn::RewindAsync { cursor_id }); let scan_loop_body_label = program.allocate_label(); @@ -256,9 +257,7 @@ impl Emitter for Operator { Operator::Search { table, table_identifier, - index, - seek_cmp, - seek_expr, + search, predicates, step, id, @@ -266,7 +265,7 @@ impl Emitter for Operator { } => { *step += 1; const SEARCH_OPEN_READ: usize = 1; - const SEARCH_SEEK_AND_CONDITIONS: usize = 2; + const SEARCH_BODY: usize = 2; const SEARCH_NEXT: usize = 3; match *step { SEARCH_OPEN_READ => { @@ -275,17 +274,13 @@ impl Emitter for Operator { Some(Table::BTree(table.clone())), ); - let index_cursor_id = if let Some(index) = index { - program.alloc_cursor_id( - Some(index.name.clone()), - Some(Table::Index(index.clone())), - ) - } else { - table_cursor_id - }; - let next_row_label = program.allocate_label(); - m.next_row_labels.insert(*id, next_row_label); + + if !matches!(search, Search::PrimaryKeyEq { .. }) { + // Primary key equality search is handled with a SeekRowid instruction which does not loop, since it is a single row lookup. + m.next_row_labels.insert(*id, next_row_label); + } + let scan_loop_body_label = program.allocate_label(); m.scan_loop_body_labels.push(scan_loop_body_label); program.emit_insn(Insn::OpenReadAsync { @@ -294,7 +289,11 @@ impl Emitter for Operator { }); program.emit_insn(Insn::OpenReadAwait); - if let Some(index) = index { + if let Search::IndexSearch { index, .. } = search { + let index_cursor_id = program.alloc_cursor_id( + Some(index.name.clone()), + Some(Table::Index(index.clone())), + ); program.emit_insn(Insn::OpenReadAsync { cursor_id: index_cursor_id, root_page: index.root_page, @@ -303,162 +302,194 @@ impl Emitter for Operator { } Ok(OpStepResult::Continue) } - SEARCH_SEEK_AND_CONDITIONS => { + SEARCH_BODY => { let table_cursor_id = program.resolve_cursor_id(table_identifier, None); - let index_cursor_id = if let Some(index) = index { - program.resolve_cursor_id(&index.name, None) - } else { - table_cursor_id - }; - let scan_loop_body_label = *m.scan_loop_body_labels.last().unwrap(); - let cmp_reg = program.alloc_register(); - // TODO this only handles ascending indexes - match seek_cmp { - ast::Operator::Equals - | ast::Operator::Greater - | ast::Operator::GreaterEquals => { + + // Open the loop for the index search. + // Primary key equality search is handled with a SeekRowid instruction which does not loop, since it is a single row lookup. + if !matches!(search, Search::PrimaryKeyEq { .. }) { + let index_cursor_id = if let Search::IndexSearch { index, .. } = search + { + Some(program.resolve_cursor_id(&index.name, None)) + } else { + None + }; + let scan_loop_body_label = *m.scan_loop_body_labels.last().unwrap(); + let cmp_reg = program.alloc_register(); + let (cmp_expr, cmp_op) = match search { + Search::IndexSearch { + cmp_expr, cmp_op, .. + } => (cmp_expr, cmp_op), + Search::PrimaryKeySearch { cmp_expr, cmp_op } => (cmp_expr, cmp_op), + Search::PrimaryKeyEq { .. } => 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, + None, + None, + )?; + } + ast::Operator::Less | ast::Operator::LessEquals => { + program.emit_insn(Insn::Null { + dest: cmp_reg, + dest_end: None, + }); + } + _ => unreachable!(), + } + program.emit_insn_with_label_dependency( + 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: *m.termination_label_stack.last().unwrap(), + } + } + 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: *m.termination_label_stack.last().unwrap(), + }, + _ => unreachable!(), + }, + *m.termination_label_stack.last().unwrap(), + ); + if *cmp_op == ast::Operator::Less + || *cmp_op == ast::Operator::LessEquals + { translate_expr( program, Some(referenced_tables), - seek_expr, + cmp_expr, cmp_reg, None, None, )?; } - ast::Operator::Less | ast::Operator::LessEquals => { - program.emit_insn(Insn::Null { - dest: cmp_reg, - dest_end: None, - }); - } - _ => unreachable!(), - } - program.emit_insn_with_label_dependency( - match seek_cmp { - ast::Operator::Equals | ast::Operator::GreaterEquals => { - Insn::SeekGE { - is_index: index.is_some(), - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: *m.termination_label_stack.last().unwrap(), - } - } - ast::Operator::Greater - | ast::Operator::Less - | ast::Operator::LessEquals => Insn::SeekGT { - is_index: index.is_some(), - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: *m.termination_label_stack.last().unwrap(), - }, - _ => unreachable!(), - }, - *m.termination_label_stack.last().unwrap(), - ); - if *seek_cmp == ast::Operator::Less - || *seek_cmp == ast::Operator::LessEquals - { - translate_expr( - program, - Some(referenced_tables), - seek_expr, - cmp_reg, - None, - None, - )?; - } - program.defer_label_resolution( - scan_loop_body_label, - program.offset() as usize, - ); - - // 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. - - let abort_jump_target = *m - .next_row_labels - .get(id) - .unwrap_or(m.termination_label_stack.last().unwrap()); - match seek_cmp { - ast::Operator::Equals | ast::Operator::LessEquals => { - if index.is_some() { - program.emit_insn_with_label_dependency( - Insn::IdxGT { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: abort_jump_target, - }, - abort_jump_target, - ); - } else { - let rowid_reg = program.alloc_register(); - program.emit_insn(Insn::RowId { - cursor_id: table_cursor_id, - dest: rowid_reg, - }); - program.emit_insn_with_label_dependency( - Insn::Gt { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: abort_jump_target, - }, - abort_jump_target, - ); + program.defer_label_resolution( + scan_loop_body_label, + program.offset() as usize, + ); + // 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. + + let abort_jump_target = *m + .next_row_labels + .get(id) + .unwrap_or(m.termination_label_stack.last().unwrap()); + match cmp_op { + ast::Operator::Equals | ast::Operator::LessEquals => { + if index_cursor_id.is_some() { + program.emit_insn_with_label_dependency( + Insn::IdxGT { + cursor_id: index_cursor_id.unwrap(), + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn_with_label_dependency( + Insn::Gt { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } } - } - ast::Operator::Less => { - if index.is_some() { - program.emit_insn_with_label_dependency( - Insn::IdxGE { - cursor_id: index_cursor_id, - start_reg: cmp_reg, - num_regs: 1, - target_pc: abort_jump_target, - }, - abort_jump_target, - ); - } else { - let rowid_reg = program.alloc_register(); - program.emit_insn(Insn::RowId { - cursor_id: table_cursor_id, - dest: rowid_reg, - }); - program.emit_insn_with_label_dependency( - Insn::Ge { - lhs: rowid_reg, - rhs: cmp_reg, - target_pc: abort_jump_target, - }, - abort_jump_target, - ); + ast::Operator::Less => { + if index_cursor_id.is_some() { + program.emit_insn_with_label_dependency( + Insn::IdxGE { + cursor_id: index_cursor_id.unwrap(), + start_reg: cmp_reg, + num_regs: 1, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } else { + let rowid_reg = program.alloc_register(); + program.emit_insn(Insn::RowId { + cursor_id: table_cursor_id, + dest: rowid_reg, + }); + program.emit_insn_with_label_dependency( + Insn::Ge { + lhs: rowid_reg, + rhs: cmp_reg, + target_pc: abort_jump_target, + }, + abort_jump_target, + ); + } } + _ => {} } - _ => {} - } - if index.is_some() { - program.emit_insn(Insn::DeferredSeek { - index_cursor_id, - table_cursor_id, - }); + if index_cursor_id.is_some() { + program.emit_insn(Insn::DeferredSeek { + index_cursor_id: index_cursor_id.unwrap(), + table_cursor_id, + }); + } } let jump_label = m .next_row_labels .get(id) .unwrap_or(m.termination_label_stack.last().unwrap()); + + if let Search::PrimaryKeyEq { cmp_expr } = search { + let src_reg = program.alloc_register(); + translate_expr( + program, + Some(referenced_tables), + cmp_expr, + src_reg, + None, + None, + )?; + program.emit_insn_with_label_dependency( + Insn::SeekRowid { + cursor_id: table_cursor_id, + src_reg, + target_pc: *jump_label, + }, + *jump_label, + ); + } if let Some(predicates) = predicates { for predicate in predicates.iter() { let jump_target_when_true = program.allocate_label(); @@ -481,10 +512,18 @@ impl Emitter for Operator { Ok(OpStepResult::ReadyToEmit) } SEARCH_NEXT => { - let cursor_id = if let Some(index) = index { - program.resolve_cursor_id(&index.name, None) - } else { - program.resolve_cursor_id(table_identifier, None) + if matches!(search, Search::PrimaryKeyEq { .. }) { + // Primary key equality search is handled with a SeekRowid instruction which does not loop, so there is no need to emit a NextAsync instruction. + return Ok(OpStepResult::Done); + } + let cursor_id = match search { + Search::IndexSearch { index, .. } => { + program.resolve_cursor_id(&index.name, None) + } + Search::PrimaryKeySearch { .. } => { + program.resolve_cursor_id(table_identifier, None) + } + Search::PrimaryKeyEq { .. } => unreachable!(), }; program .resolve_label(*m.next_row_labels.get(id).unwrap(), program.offset()); @@ -502,80 +541,6 @@ impl Emitter for Operator { _ => Ok(OpStepResult::Done), } } - Operator::SeekRowid { - table, - table_identifier, - rowid_predicate, - predicates, - step, - id, - .. - } => { - *step += 1; - const SEEKROWID_OPEN_READ: usize = 1; - const SEEKROWID_SEEK_AND_CONDITIONS: usize = 2; - match *step { - SEEKROWID_OPEN_READ => { - let cursor_id = program.alloc_cursor_id( - Some(table_identifier.clone()), - Some(Table::BTree(table.clone())), - ); - let root_page = table.root_page; - program.emit_insn(Insn::OpenReadAsync { - cursor_id, - root_page, - }); - program.emit_insn(Insn::OpenReadAwait); - - Ok(OpStepResult::Continue) - } - SEEKROWID_SEEK_AND_CONDITIONS => { - let cursor_id = program.resolve_cursor_id(table_identifier, None); - let rowid_reg = program.alloc_register(); - translate_expr( - program, - Some(referenced_tables), - rowid_predicate, - rowid_reg, - None, - None, - )?; - let jump_label = m - .next_row_labels - .get(id) - .unwrap_or(m.termination_label_stack.last().unwrap()); - program.emit_insn_with_label_dependency( - Insn::SeekRowid { - cursor_id, - src_reg: rowid_reg, - target_pc: *jump_label, - }, - *jump_label, - ); - 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: *jump_label, - }; - translate_condition_expr( - program, - referenced_tables, - predicate, - None, - condition_metadata, - )?; - program.resolve_label(jump_target_when_true, program.offset()); - } - } - - Ok(OpStepResult::ReadyToEmit) - } - _ => Ok(OpStepResult::Done), - } - } Operator::Join { left, right, @@ -679,7 +644,7 @@ impl Emitter for Operator { Operator::Scan { table_identifier, .. } => program.resolve_cursor_id(table_identifier, None), - Operator::SeekRowid { + Operator::Search { table_identifier, .. } => program.resolve_cursor_id(table_identifier, None), _ => unreachable!(), @@ -694,9 +659,14 @@ impl Emitter for Operator { }, lj_meta.set_match_flag_true_label, ); - // This points to the NextAsync instruction of the left table - program.resolve_label(lj_meta.on_match_jump_to_label, program.offset()); } + let next_row_label = if *outer { + m.left_joins.get(id).unwrap().on_match_jump_to_label + } else { + *m.next_row_labels.get(&right.id()).unwrap() + }; + // This points to the NextAsync instruction of the left table + program.resolve_label(next_row_label, program.offset()); left.step(program, m, referenced_tables)?; Ok(OpStepResult::Done) @@ -1524,23 +1494,6 @@ impl Emitter for Operator { } } Operator::Filter { .. } => unreachable!("predicates have been pushed down"), - Operator::SeekRowid { - table_identifier, - table, - .. - } => { - let start_reg = program.alloc_registers(col_count); - let table = cursor_override - .map(|c| c.pseudo_table.clone()) - .unwrap_or_else(|| Table::BTree(table.clone())); - let cursor_id = cursor_override - .map(|c| c.cursor_id) - .unwrap_or_else(|| program.resolve_cursor_id(table_identifier, None)); - let start_column_offset = cursor_override.map(|c| c.sort_key_len).unwrap_or(0); - translate_table_columns(program, cursor_id, &table, start_column_offset, start_reg); - - Ok(start_reg) - } Operator::Limit { .. } => { unimplemented!() } diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index da08bab1d..07975b020 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -4,14 +4,13 @@ use sqlite3_parser::ast; use crate::{ schema::{BTreeTable, Index}, - types::OwnedValue, util::normalize_ident, Result, }; use super::plan::{ get_table_ref_bitmask_for_ast_expr, get_table_ref_bitmask_for_operator, Operator, Plan, - ProjectionColumn, + ProjectionColumn, Search, }; /** @@ -72,60 +71,23 @@ fn use_indexes( .iter() .find(|(t, t_id)| Rc::ptr_eq(t, table) && t_id == table_identifier) .unwrap(); - match try_extract_expr_that_utilizes_index(f, table, available_indexes)? { + match try_extract_index_search_expression(f, table, available_indexes)? { Either::Left(non_index_using_expr) => { fs[i] = non_index_using_expr; } - Either::Right(index_using_expr) => match index_using_expr { - SearchableExpr::IndexSearch { - index, - cmp_op, - cmp_expr, - } => { - fs.remove(i); - *operator = Operator::Search { - table: table.0.clone(), - index: Some(index.clone()), - predicates: Some(std::mem::take(fs)), - seek_cmp: cmp_op, - seek_expr: cmp_expr, - table_identifier: table_identifier.clone(), - id: *id, - step: 0, - }; - return Ok(()); - } - SearchableExpr::PrimaryKeySearch { - table, - cmp_op, - cmp_expr, - } => { - fs.remove(i); - *operator = Operator::Search { - table, - index: None, - predicates: Some(std::mem::take(fs)), - seek_cmp: cmp_op, - seek_expr: cmp_expr, - table_identifier: table_identifier.clone(), - id: *id, - step: 0, - }; - return Ok(()); - } - SearchableExpr::PrimaryKeyEq { cmp_expr, table } => { - fs.remove(i); - *operator = Operator::SeekRowid { - table, - table_identifier: table_identifier.clone(), - rowid_predicate: cmp_expr, - predicates: Some(std::mem::take(fs)), - id: *id, - step: 0, - }; - return Ok(()); - } - }, + Either::Right(index_search) => { + fs.remove(i); + *operator = Operator::Search { + id: *id, + table: table.0.clone(), + table_identifier: table.1.clone(), + predicates: Some(fs.clone()), + search: index_search, + step: 0, + }; + + return Ok(()); + } } } @@ -139,7 +101,6 @@ fn use_indexes( use_indexes(source, referenced_tables, available_indexes)?; Ok(()) } - Operator::SeekRowid { .. } => Ok(()), Operator::Limit { source, .. } => { use_indexes(source, referenced_tables, available_indexes)?; Ok(()) @@ -242,31 +203,6 @@ fn eliminate_constants(operator: &mut Operator) -> Result { - if let Some(predicates) = predicates { - let mut i = 0; - while i < predicates.len() { - let predicate = &predicates[i]; - if predicate.is_always_true()? { - predicates.remove(i); - } else if predicate.is_always_false()? { - return Ok(ConstantConditionEliminationResult::ImpossibleCondition); - } else { - i += 1; - } - } - } - - if rowid_predicate.is_always_false()? { - return Ok(ConstantConditionEliminationResult::ImpossibleCondition); - } - - Ok(ConstantConditionEliminationResult::Continue) - } Operator::Limit { source, .. } => { let constant_elimination_result = eliminate_constants(source)?; if constant_elimination_result @@ -315,7 +251,23 @@ fn eliminate_constants(operator: &mut Operator) -> Result Ok(ConstantConditionEliminationResult::Continue), + Operator::Search { predicates, .. } => { + if let Some(predicates) = predicates { + let mut i = 0; + while i < predicates.len() { + let predicate = &predicates[i]; + if predicate.is_always_true()? { + predicates.remove(i); + } else if predicate.is_always_false()? { + return Ok(ConstantConditionEliminationResult::ImpossibleCondition); + } else { + i += 1; + } + } + } + + Ok(ConstantConditionEliminationResult::Continue) + } Operator::Nothing => Ok(ConstantConditionEliminationResult::Continue), } } @@ -402,7 +354,6 @@ fn push_predicates( Ok(()) } - Operator::SeekRowid { .. } => Ok(()), Operator::Limit { source, .. } => { push_predicates(source, referenced_tables)?; Ok(()) @@ -525,7 +476,6 @@ fn push_predicate( Ok(Some(push_result.unwrap())) } - Operator::SeekRowid { .. } => Ok(Some(predicate)), Operator::Limit { source, .. } => { let push_result = push_predicate(source, predicate, referenced_tables)?; if push_result.is_none() { @@ -702,7 +652,6 @@ fn find_indexes_of_all_result_columns_in_operator_that_match_expr_either_fully_o mask } Operator::Filter { .. } => 0, - Operator::SeekRowid { .. } => 0, Operator::Limit { .. } => 0, Operator::Join { .. } => 0, Operator::Order { .. } => 0, @@ -887,7 +836,6 @@ fn find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_o ) } Operator::Filter { .. } => unreachable!(), - Operator::SeekRowid { .. } => {} Operator::Limit { source, .. } => { find_shared_expressions_in_child_operators_and_mark_them_so_that_the_parent_operator_doesnt_recompute_them(source, expr_result_cache) } @@ -1160,51 +1108,23 @@ pub enum Either { Right(U), } -/// An expression that can be used to search for a row in a table using an index -/// (i.e. a primary key or a secondary index) -/// -pub enum SearchableExpr { - /// A primary key equality search. This is a special case of the primary key search - /// that uses the SeekRowid operator and bytecode instruction. - PrimaryKeyEq { - table: Rc, - cmp_expr: ast::Expr, - }, - /// A primary key search. This uses the Search operator and uses bytecode instructions like SeekGT, SeekGE etc. - PrimaryKeySearch { - table: Rc, - cmp_op: ast::Operator, - cmp_expr: ast::Expr, - }, - /// A secondary index search. This uses the Search operator and uses bytecode instructions like SeekGE, SeekGT etc. - IndexSearch { - index: Rc, - cmp_op: ast::Operator, - cmp_expr: ast::Expr, - }, -} - -pub fn try_extract_expr_that_utilizes_index( +pub fn try_extract_index_search_expression( expr: ast::Expr, table: &(Rc, String), available_indexes: &[Rc], -) -> Result> { +) -> Result> { match expr { ast::Expr::Binary(mut lhs, operator, mut rhs) => { if lhs.is_primary_key_of(table) { match operator { ast::Operator::Equals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { - table: table.0.clone(), - cmp_expr: *rhs, - })); + return Ok(Either::Right(Search::PrimaryKeyEq { cmp_expr: *rhs })); } ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { - table: table.0.clone(), + return Ok(Either::Right(Search::PrimaryKeySearch { cmp_op: operator, cmp_expr: *rhs, })); @@ -1216,17 +1136,13 @@ pub fn try_extract_expr_that_utilizes_index( if rhs.is_primary_key_of(table) { match operator { ast::Operator::Equals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeyEq { - table: table.0.clone(), - cmp_expr: *lhs, - })); + return Ok(Either::Right(Search::PrimaryKeyEq { cmp_expr: *lhs })); } ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::PrimaryKeySearch { - table: table.0.clone(), + return Ok(Either::Right(Search::PrimaryKeySearch { cmp_op: operator, cmp_expr: *lhs, })); @@ -1242,7 +1158,7 @@ pub fn try_extract_expr_that_utilizes_index( | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::IndexSearch { + return Ok(Either::Right(Search::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *rhs, @@ -1259,7 +1175,7 @@ pub fn try_extract_expr_that_utilizes_index( | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals => { - return Ok(Either::Right(SearchableExpr::IndexSearch { + return Ok(Either::Right(Search::IndexSearch { index: available_indexes[index_index].clone(), cmp_op: operator, cmp_expr: *lhs, diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 24a3b6513..174ea2d3b 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -64,19 +64,6 @@ pub enum Operator { source: Box, predicates: Vec, }, - // SeekRowid operator - // This operator is used to retrieve a single row from a table by its rowid. - // rowid_predicate is an expression that produces the comparison value for the rowid. - // e.g. rowid = 5, or rowid = other_table.foo - // predicates is an optional list of additional predicates to evaluate. - SeekRowid { - id: usize, - table: Rc, - table_identifier: String, - rowid_predicate: ast::Expr, - predicates: Option>, - step: usize, - }, // Limit operator // This operator is used to limit the number of rows returned by the source operator. Limit { @@ -129,13 +116,14 @@ pub enum Operator { predicates: Option>, step: usize, }, + // Search operator + // This operator is used to search for a row in a table using an index + // (i.e. a primary key or a secondary index) Search { id: usize, - index: Option>, - seek_cmp: ast::Operator, - seek_expr: ast::Expr, table: Rc, table_identifier: String, + search: Search, predicates: Option>, step: usize, }, @@ -145,6 +133,26 @@ pub enum Operator { Nothing, } +/// An enum that represents a search operation that can be used to search for a row in a table using an index +/// (i.e. a primary key or a secondary index) +#[derive(Clone, Debug)] +pub enum Search { + /// A primary key equality search. This is a special case of the primary key search + /// that uses the SeekRowid bytecode instruction. + PrimaryKeyEq { cmp_expr: ast::Expr }, + /// A primary key search. Uses bytecode instructions like SeekGT, SeekGE etc. + PrimaryKeySearch { + cmp_op: ast::Operator, + cmp_expr: ast::Expr, + }, + /// A secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc. + IndexSearch { + index: Rc, + cmp_op: ast::Operator, + cmp_expr: ast::Expr, + }, +} + #[derive(Clone, Debug)] pub enum ProjectionColumn { Column(ast::Expr), @@ -177,7 +185,6 @@ impl Operator { .. } => aggregates.len() + group_by.as_ref().map_or(0, |g| g.len()), Operator::Filter { source, .. } => source.column_count(referenced_tables), - Operator::SeekRowid { table, .. } => table.columns.len(), Operator::Limit { source, .. } => source.column_count(referenced_tables), Operator::Join { left, right, .. } => { left.column_count(referenced_tables) + right.column_count(referenced_tables) @@ -220,9 +227,6 @@ impl Operator { names } Operator::Filter { source, .. } => source.column_names(), - Operator::SeekRowid { table, .. } => { - table.columns.iter().map(|c| c.name.clone()).collect() - } Operator::Limit { source, .. } => source.column_names(), Operator::Join { left, right, .. } => { let mut names = left.column_names(); @@ -254,7 +258,6 @@ impl Operator { match self { Operator::Aggregate { id, .. } => *id, Operator::Filter { id, .. } => *id, - Operator::SeekRowid { id, .. } => *id, Operator::Limit { id, .. } => *id, Operator::Join { id, .. } => *id, Operator::Order { id, .. } => *id, @@ -343,33 +346,6 @@ impl Display for Operator { writeln!(f, "{}FILTER {}", indent, predicates_string)?; fmt_operator(source, f, level + 1, true) } - Operator::SeekRowid { - table, - rowid_predicate, - predicates, - .. - } => { - match predicates { - Some(ps) => { - let predicates_string = ps - .iter() - .map(|p| p.to_string()) - .collect::>() - .join(" AND "); - writeln!( - f, - "{}SEEK {}.rowid ON rowid={} FILTER {}", - indent, &table.name, rowid_predicate, predicates_string - )?; - } - None => writeln!( - f, - "{}SEEK {}.rowid ON rowid={}", - indent, &table.name, rowid_predicate - )?, - } - Ok(()) - } Operator::Limit { source, limit, .. } => { writeln!(f, "{}TAKE {}", indent, limit)?; fmt_operator(source, f, level + 1, true) @@ -487,13 +463,6 @@ pub fn get_table_ref_bitmask_for_operator<'a>( table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, predicate)?; } } - Operator::SeekRowid { table, .. } => { - table_refs_mask |= 1 - << tables - .iter() - .position(|(t, _)| Rc::ptr_eq(t, table)) - .unwrap(); - } Operator::Limit { source, .. } => { table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?; } From dde10d2dd76a1859ba94df922648ed6bb9e10dba Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:19:56 +0300 Subject: [PATCH 19/32] Better EXPLAIN QUERY PLAN for Operator::Search --- core/translate/plan.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 174ea2d3b..ea181ef85 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -426,8 +426,15 @@ impl Display for Operator { }?; Ok(()) } - Operator::Search { table, .. } => { - writeln!(f, "{}INDEX SCAN {}", indent, table.name)?; + Operator::Search { table_identifier, search, .. } => { + match search { + Search::PrimaryKeyEq { .. } | Search::PrimaryKeySearch { .. } => { + writeln!(f, "{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)", indent, table_identifier)?; + } + Search::IndexSearch { index, .. } => { + writeln!(f, "{}SEARCH {} USING INDEX {}", indent, table_identifier, index.name)?; + } + } Ok(()) } Operator::Nothing => Ok(()), From bb1c8b65e8a2ac5a3b1ea9ee01d088bff81b3791 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:22:12 +0300 Subject: [PATCH 20/32] fmt --- core/translate/plan.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/core/translate/plan.rs b/core/translate/plan.rs index ea181ef85..80463af4d 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -426,13 +426,25 @@ impl Display for Operator { }?; Ok(()) } - Operator::Search { table_identifier, search, .. } => { + Operator::Search { + table_identifier, + search, + .. + } => { match search { Search::PrimaryKeyEq { .. } | Search::PrimaryKeySearch { .. } => { - writeln!(f, "{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)", indent, table_identifier)?; + writeln!( + f, + "{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)", + indent, table_identifier + )?; } Search::IndexSearch { index, .. } => { - writeln!(f, "{}SEARCH {} USING INDEX {}", indent, table_identifier, index.name)?; + writeln!( + f, + "{}SEARCH {} USING INDEX {}", + indent, table_identifier, index.name + )?; } } Ok(()) From 37f877109e1356effdc91ce4da2030e320df9315 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:39:50 +0300 Subject: [PATCH 21/32] Reduce duplication in btree.rs --- core/storage/btree.rs | 162 ++++++------------------------------------ 1 file changed, 22 insertions(+), 140 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index b702cc802..f2a6d5f20 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -25,6 +25,7 @@ const BTREE_HEADER_OFFSET_RIGHTMOST: usize = 8; /* if internalnode, pointer righ #[derive(Clone)] pub enum SeekOp { + EQ, GT, GE, } @@ -181,7 +182,7 @@ impl BTreeCursor { key: &OwnedRecord, op: SeekOp, ) -> Result, Option)>> { - match self.move_to_index_leaf(key, op.clone())? { + match self.index_move_to(key, op.clone())? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -211,6 +212,7 @@ impl BTreeCursor { let comparison = match op { SeekOp::GT => record > *key, SeekOp::GE => record >= *key, + SeekOp::EQ => record == *key, }; if comparison { let rowid = match record.values.get(1) { @@ -233,7 +235,7 @@ impl BTreeCursor { rowid: u64, op: SeekOp, ) -> Result, Option)>> { - match self.move_to_table_leaf(rowid, op.clone())? { + match self.table_move_to(rowid, op.clone())? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -267,6 +269,7 @@ impl BTreeCursor { let comparison = match op { SeekOp::GT => *cell_rowid > rowid, SeekOp::GE => *cell_rowid >= rowid, + SeekOp::EQ => *cell_rowid == rowid, }; if comparison { let record = crate::storage::sqlite3_ondisk::read_record(payload)?; @@ -281,131 +284,6 @@ impl BTreeCursor { Ok(CursorResult::Ok((None, None))) } - fn btree_seek_rowid( - &mut self, - rowid: u64, - ) -> Result, Option)>> { - match self.move_to(rowid)? { - CursorResult::Ok(_) => {} - CursorResult::IO => return Ok(CursorResult::IO), - }; - - let mem_page = self.get_mem_page(); - - let page_idx = mem_page.page_idx; - let page = self.pager.read_page(page_idx)?; - let page = RefCell::borrow(&page); - if page.is_locked() { - return Ok(CursorResult::IO); - } - let page = page.contents.read().unwrap(); - let page = page.as_ref().unwrap(); - - for cell_idx in 0..page.cell_count() { - match &page.cell_get( - cell_idx, - self.pager.clone(), - self.max_local(page.page_type()), - self.min_local(page.page_type()), - self.usable_space(), - )? { - BTreeCell::TableLeafCell(TableLeafCell { - _rowid: cell_rowid, - _payload: p, - first_overflow_page: _, - }) => { - mem_page.advance(); - if *cell_rowid == rowid { - let record = crate::storage::sqlite3_ondisk::read_record(p)?; - return Ok(CursorResult::Ok((Some(*cell_rowid), Some(record)))); - } - } - cell_type => { - unreachable!("unexpected cell type: {:?}", cell_type); - } - } - } - - Ok(CursorResult::Ok((None, None))) - } - - fn move_to_table_leaf(&mut self, key: u64, cmp: SeekOp) -> Result> { - self.move_to_root(); - - loop { - let mem_page = self.get_mem_page(); - let page_idx = mem_page.page_idx; - let page = self.pager.read_page(page_idx)?; - let page = RefCell::borrow(&page); - if page.is_locked() { - return Ok(CursorResult::IO); - } - - let page = page.contents.read().unwrap(); - let page = page.as_ref().unwrap(); - if page.is_leaf() { - return Ok(CursorResult::Ok(())); - } - - let mut found_cell = false; - for cell_idx in 0..page.cell_count() { - let cell = page.cell_get( - cell_idx, - self.pager.clone(), - self.max_local(page.page_type()), - self.min_local(page.page_type()), - self.usable_space(), - )?; - match &cell { - BTreeCell::TableInteriorCell(TableInteriorCell { - _left_child_page, - _rowid, - }) => { - mem_page.advance(); - let comparison = match cmp { - SeekOp::GT => key <= *_rowid, - SeekOp::GE => key < *_rowid, - }; - if comparison { - let mem_page = - MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); - self.page.replace(Some(Rc::new(mem_page))); - found_cell = true; - break; - } - } - BTreeCell::TableLeafCell(_) => { - unreachable!( - "we don't iterate leaf cells while trying to move to a leaf cell" - ); - } - BTreeCell::IndexInteriorCell(_) => { - unimplemented!(); - } - BTreeCell::IndexLeafCell(_) => { - unimplemented!(); - } - } - } - - if !found_cell { - let parent = mem_page.parent.clone(); - match page.rightmost_pointer() { - Some(right_most_pointer) => { - let mem_page = MemPage::new(parent, right_most_pointer as usize, 0); - self.page.replace(Some(Rc::new(mem_page))); - continue; - } - None => { - unreachable!("we shall not go back up! The only way is down the slope"); - } - } - } - - return Ok(CursorResult::Ok(())); - } - } - fn move_to_root(&mut self) { self.page .replace(Some(Rc::new(MemPage::new(None, self.root_page, 0)))); @@ -447,7 +325,7 @@ impl BTreeCursor { } } - pub fn move_to(&mut self, key: u64) -> Result> { + pub fn table_move_to(&mut self, key: u64, cmp: SeekOp) -> Result> { // For a table with N rows, we can find any row by row id in O(log(N)) time by starting at the root page and following the B-tree pointers. // B-trees consist of interior pages and leaf pages. Interior pages contain pointers to other pages, while leaf pages contain the actual row data. // @@ -501,8 +379,13 @@ impl BTreeCursor { _left_child_page, _rowid, }) => { - if key < *_rowid { - mem_page.advance(); + mem_page.advance(); + let comparison = match cmp { + SeekOp::GT => key <= *_rowid, + SeekOp::GE => key < *_rowid, + SeekOp::EQ => key < *_rowid, + }; + if comparison { let mem_page = MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); @@ -529,10 +412,10 @@ impl BTreeCursor { } if !found_cell { - let parent = mem_page.clone(); + let parent = mem_page.parent.clone(); match page.rightmost_pointer() { Some(right_most_pointer) => { - let mem_page = MemPage::new(Some(parent), right_most_pointer as usize, 0); + let mem_page = MemPage::new(parent, right_most_pointer as usize, 0); self.page.replace(Some(Rc::new(mem_page))); continue; } @@ -544,9 +427,7 @@ impl BTreeCursor { } } - // TODO: there is a lot of code duplication here between move_to, move_to_index_leaf and move_to_table_leaf. - // I wanted to get this working first, but this should really be refactored. - Jussi - fn move_to_index_leaf(&mut self, key: &OwnedRecord, cmp: SeekOp) -> Result> { + fn index_move_to(&mut self, key: &OwnedRecord, cmp: SeekOp) -> Result> { self.move_to_root(); loop { let mem_page = self.get_mem_page(); @@ -580,8 +461,9 @@ impl BTreeCursor { mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { - SeekOp::GT => record > *key, - SeekOp::GE => record >= *key, + SeekOp::GT => key <= &record, + SeekOp::GE => key < &record, + SeekOp::EQ => key < &record, }; if comparison { let mem_page = @@ -1564,7 +1446,7 @@ impl Cursor for BTreeCursor { } fn seek_rowid(&mut self, rowid: u64) -> Result> { - match self.btree_seek_rowid(rowid)? { + match self.btree_table_seek(rowid, SeekOp::EQ)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1633,7 +1515,7 @@ impl Cursor for BTreeCursor { _ => unreachable!("btree tables are indexed by integers!"), }; if !moved_before { - match self.move_to(*int_key as u64)? { + match self.table_move_to(*int_key as u64, SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -1658,7 +1540,7 @@ impl Cursor for BTreeCursor { OwnedValue::Integer(i) => i, _ => unreachable!("btree tables are indexed by integers!"), }; - match self.move_to(*int_key as u64)? { + match self.table_move_to(*int_key as u64, SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; From 1ae8d286692aa4ebcf137ac8f9420d83d1c501cc Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:51:14 +0300 Subject: [PATCH 22/32] Use same move_to() function for tables and indexes --- core/storage/btree.rs | 90 ++++++++++++------------------------------- 1 file changed, 24 insertions(+), 66 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index f2a6d5f20..ae75fa2da 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -30,6 +30,11 @@ pub enum SeekOp { GE, } +pub enum SeekKey<'a> { + TableRowId(u64), + IndexKey(&'a OwnedRecord), +} + #[derive(Debug)] pub struct MemPage { parent: Option>, @@ -182,7 +187,7 @@ impl BTreeCursor { key: &OwnedRecord, op: SeekOp, ) -> Result, Option)>> { - match self.index_move_to(key, op.clone())? { + match self.move_to(SeekKey::IndexKey(key), op.clone())? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -235,7 +240,7 @@ impl BTreeCursor { rowid: u64, op: SeekOp, ) -> Result, Option)>> { - match self.table_move_to(rowid, op.clone())? { + match self.move_to(SeekKey::TableRowId(rowid), op.clone())? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -325,7 +330,7 @@ impl BTreeCursor { } } - pub fn table_move_to(&mut self, key: u64, cmp: SeekOp) -> Result> { + pub fn move_to<'a>(&mut self, key: SeekKey<'a>, cmp: SeekOp) -> Result> { // For a table with N rows, we can find any row by row id in O(log(N)) time by starting at the root page and following the B-tree pointers. // B-trees consist of interior pages and leaf pages. Interior pages contain pointers to other pages, while leaf pages contain the actual row data. // @@ -379,11 +384,14 @@ impl BTreeCursor { _left_child_page, _rowid, }) => { + let SeekKey::TableRowId(rowid_key) = key else { + unreachable!("table seek key should be a rowid"); + }; mem_page.advance(); let comparison = match cmp { - SeekOp::GT => key <= *_rowid, - SeekOp::GE => key < *_rowid, - SeekOp::EQ => key < *_rowid, + SeekOp::GT => rowid_key <= *_rowid, + SeekOp::GE => rowid_key < *_rowid, + SeekOp::EQ => rowid_key < *_rowid, }; if comparison { let mem_page = @@ -402,68 +410,20 @@ impl BTreeCursor { "we don't iterate leaf cells while trying to move to a leaf cell" ); } - BTreeCell::IndexInteriorCell(_) => { - unimplemented!(); - } - BTreeCell::IndexLeafCell(_) => { - unimplemented!(); - } - } - } - - if !found_cell { - let parent = mem_page.parent.clone(); - match page.rightmost_pointer() { - Some(right_most_pointer) => { - let mem_page = MemPage::new(parent, right_most_pointer as usize, 0); - self.page.replace(Some(Rc::new(mem_page))); - continue; - } - None => { - unreachable!("we shall not go back up! The only way is down the slope"); - } - } - } - } - } - - fn index_move_to(&mut self, key: &OwnedRecord, cmp: SeekOp) -> Result> { - self.move_to_root(); - loop { - let mem_page = self.get_mem_page(); - let page_idx = mem_page.page_idx; - let page = self.pager.read_page(page_idx)?; - let page = RefCell::borrow(&page); - if page.is_locked() { - return Ok(CursorResult::IO); - } - - let page = page.contents.read().unwrap(); - let page = page.as_ref().unwrap(); - if page.is_leaf() { - return Ok(CursorResult::Ok(())); - } - - let mut found_cell = false; - for cell_idx in 0..page.cell_count() { - match &page.cell_get( - cell_idx, - self.pager.clone(), - self.max_local(page.page_type()), - self.min_local(page.page_type()), - self.usable_space(), - )? { BTreeCell::IndexInteriorCell(IndexInteriorCell { left_child_page, payload, .. }) => { + let SeekKey::IndexKey(index_key) = key else { + unreachable!("index seek key should be a record"); + }; mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { - SeekOp::GT => key <= &record, - SeekOp::GE => key < &record, - SeekOp::EQ => key < &record, + SeekOp::GT => index_key <= &record, + SeekOp::GE => index_key < &record, + SeekOp::EQ => index_key < &record, }; if comparison { let mem_page = @@ -473,10 +433,8 @@ impl BTreeCursor { break; } } - _ => { - unreachable!( - "we don't iterate leaf cells while trying to move to a leaf cell" - ); + BTreeCell::IndexLeafCell(_) => { + unreachable!("we don't iterate leaf cells while trying to move to a leaf cell"); } } } @@ -1515,7 +1473,7 @@ impl Cursor for BTreeCursor { _ => unreachable!("btree tables are indexed by integers!"), }; if !moved_before { - match self.table_move_to(*int_key as u64, SeekOp::EQ)? { + match self.move_to(SeekKey::TableRowId(*int_key as u64), SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -1540,7 +1498,7 @@ impl Cursor for BTreeCursor { OwnedValue::Integer(i) => i, _ => unreachable!("btree tables are indexed by integers!"), }; - match self.table_move_to(*int_key as u64, SeekOp::EQ)? { + match self.move_to(SeekKey::TableRowId(*int_key as u64), SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; From af9a751d369583138300daaa97086367074d0891 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:56:18 +0300 Subject: [PATCH 23/32] Single seek function --- core/storage/btree.rs | 104 +++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 66 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index ae75fa2da..d67588f5d 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -30,6 +30,7 @@ pub enum SeekOp { GE, } +#[derive(Clone)] pub enum SeekKey<'a> { TableRowId(u64), IndexKey(&'a OwnedRecord), @@ -182,65 +183,12 @@ impl BTreeCursor { } } - fn btree_index_seek( - &mut self, - key: &OwnedRecord, - op: SeekOp, - ) -> Result, Option)>> { - match self.move_to(SeekKey::IndexKey(key), op.clone())? { - CursorResult::Ok(_) => {} - CursorResult::IO => return Ok(CursorResult::IO), - }; - - let mem_page = self.get_mem_page(); - let page_idx = mem_page.page_idx; - let page = self.pager.read_page(page_idx)?; - let page = RefCell::borrow(&page); - if page.is_locked() { - return Ok(CursorResult::IO); - } - - let page = page.contents.read().unwrap(); - let page = page.as_ref().unwrap(); - - for cell_idx in 0..page.cell_count() { - match &page.cell_get( - cell_idx, - self.pager.clone(), - self.max_local(page.page_type()), - self.min_local(page.page_type()), - self.usable_space(), - )? { - BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { - mem_page.advance(); - let record = crate::storage::sqlite3_ondisk::read_record(payload)?; - let comparison = match op { - SeekOp::GT => record > *key, - SeekOp::GE => record >= *key, - SeekOp::EQ => record == *key, - }; - if comparison { - let rowid = match record.values.get(1) { - Some(OwnedValue::Integer(rowid)) => *rowid as u64, - _ => unreachable!("index cells should have an integer rowid"), - }; - return Ok(CursorResult::Ok((Some(rowid), Some(record)))); - } - } - cell_type => { - unreachable!("unexpected cell type: {:?}", cell_type); - } - } - } - Ok(CursorResult::Ok((None, None))) - } - - fn btree_table_seek( + fn seek<'a>( &mut self, - rowid: u64, + key: SeekKey<'a>, op: SeekOp, ) -> Result, Option)>> { - match self.move_to(SeekKey::TableRowId(rowid), op.clone())? { + match self.move_to(key.clone(), op.clone())? { CursorResult::Ok(_) => {} CursorResult::IO => return Ok(CursorResult::IO), }; @@ -268,19 +216,41 @@ impl BTreeCursor { BTreeCell::TableLeafCell(TableLeafCell { _rowid: cell_rowid, _payload: payload, - first_overflow_page: fop, + first_overflow_page: _, }) => { + let SeekKey::TableRowId(rowid_key) = key else { + unreachable!("table seek key should be a rowid"); + }; mem_page.advance(); let comparison = match op { - SeekOp::GT => *cell_rowid > rowid, - SeekOp::GE => *cell_rowid >= rowid, - SeekOp::EQ => *cell_rowid == rowid, + SeekOp::GT => *cell_rowid > rowid_key, + SeekOp::GE => *cell_rowid >= rowid_key, + SeekOp::EQ => *cell_rowid == rowid_key, }; if comparison { let record = crate::storage::sqlite3_ondisk::read_record(payload)?; return Ok(CursorResult::Ok((Some(*cell_rowid), Some(record)))); } } + BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { + let SeekKey::IndexKey(index_key) = key else { + unreachable!("index seek key should be a record"); + }; + mem_page.advance(); + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + let comparison = match op { + SeekOp::GT => record > *index_key, + SeekOp::GE => record >= *index_key, + SeekOp::EQ => record == *index_key, + }; + if comparison { + let rowid = match record.values.get(1) { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } + } cell_type => { unreachable!("unexpected cell type: {:?}", cell_type); } @@ -434,7 +404,9 @@ impl BTreeCursor { } } BTreeCell::IndexLeafCell(_) => { - unreachable!("we don't iterate leaf cells while trying to move to a leaf cell"); + unreachable!( + "we don't iterate leaf cells while trying to move to a leaf cell" + ); } } } @@ -1404,7 +1376,7 @@ impl Cursor for BTreeCursor { } fn seek_rowid(&mut self, rowid: u64) -> Result> { - match self.btree_table_seek(rowid, SeekOp::EQ)? { + match self.seek(SeekKey::TableRowId(rowid), SeekOp::EQ)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1415,7 +1387,7 @@ impl Cursor for BTreeCursor { } fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result> { - match self.btree_index_seek(key, SeekOp::GE)? { + match self.seek(SeekKey::IndexKey(key), SeekOp::GE)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1426,7 +1398,7 @@ impl Cursor for BTreeCursor { } fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result> { - match self.btree_index_seek(key, SeekOp::GT)? { + match self.seek(SeekKey::IndexKey(key), SeekOp::GT)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1437,7 +1409,7 @@ impl Cursor for BTreeCursor { } fn seek_ge_rowid(&mut self, rowid: u64) -> Result> { - match self.btree_table_seek(rowid, SeekOp::GE)? { + match self.seek(SeekKey::TableRowId(rowid), SeekOp::GE)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1448,7 +1420,7 @@ impl Cursor for BTreeCursor { } fn seek_gt_rowid(&mut self, rowid: u64) -> Result> { - match self.btree_table_seek(rowid, SeekOp::GT)? { + match self.seek(SeekKey::TableRowId(rowid), SeekOp::GT)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); From 6e7db361210149c05dee52fee58ae3ab8de08165 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 00:58:32 +0300 Subject: [PATCH 24/32] reorder --- core/storage/btree.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index d67588f5d..4b568d8b5 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -1386,8 +1386,8 @@ impl Cursor for BTreeCursor { } } - fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result> { - match self.seek(SeekKey::IndexKey(key), SeekOp::GE)? { + fn seek_ge_rowid(&mut self, rowid: u64) -> Result> { + match self.seek(SeekKey::TableRowId(rowid), SeekOp::GE)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1397,8 +1397,8 @@ impl Cursor for BTreeCursor { } } - fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result> { - match self.seek(SeekKey::IndexKey(key), SeekOp::GT)? { + fn seek_gt_rowid(&mut self, rowid: u64) -> Result> { + match self.seek(SeekKey::TableRowId(rowid), SeekOp::GT)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1408,8 +1408,8 @@ impl Cursor for BTreeCursor { } } - fn seek_ge_rowid(&mut self, rowid: u64) -> Result> { - match self.seek(SeekKey::TableRowId(rowid), SeekOp::GE)? { + fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result> { + match self.seek(SeekKey::IndexKey(key), SeekOp::GE)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); @@ -1419,8 +1419,8 @@ impl Cursor for BTreeCursor { } } - fn seek_gt_rowid(&mut self, rowid: u64) -> Result> { - match self.seek(SeekKey::TableRowId(rowid), SeekOp::GT)? { + fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result> { + match self.seek(SeekKey::IndexKey(key), SeekOp::GT)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); From 15a66ea6629ce46efcd6e8ac0c5be74d65e3c1b1 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 08:55:05 +0300 Subject: [PATCH 25/32] single seek function in cursor trait --- core/pseudo.rs | 23 ++++------------ core/storage/btree.rs | 63 +++---------------------------------------- core/types.rs | 19 +++++++++---- core/vdbe/mod.rs | 63 ++++++++++++++++++------------------------- core/vdbe/sorter.rs | 20 ++------------ 5 files changed, 50 insertions(+), 138 deletions(-) diff --git a/core/pseudo.rs b/core/pseudo.rs index 3ef849c9e..73aee406a 100644 --- a/core/pseudo.rs +++ b/core/pseudo.rs @@ -1,4 +1,7 @@ -use crate::Result; +use crate::{ + types::{SeekKey, SeekOp}, + Result, +}; use std::cell::{Ref, RefCell}; use crate::types::{Cursor, CursorResult, OwnedRecord, OwnedValue}; @@ -48,23 +51,7 @@ impl Cursor for PseudoCursor { Ok(x) } - fn seek_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_ge_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_gt_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { - unimplemented!(); - } - - fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { + fn seek(&mut self, _: SeekKey<'_>, _: SeekOp) -> Result> { unimplemented!(); } diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 4b568d8b5..a6c7fa312 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -5,7 +5,7 @@ use crate::storage::sqlite3_ondisk::{ read_btree_cell, read_varint, write_varint, BTreeCell, DatabaseHeader, PageContent, PageType, TableInteriorCell, TableLeafCell, }; -use crate::types::{Cursor, CursorResult, OwnedRecord, OwnedValue}; +use crate::types::{Cursor, CursorResult, OwnedRecord, OwnedValue, SeekKey, SeekOp}; use crate::Result; use std::cell::{Ref, RefCell}; @@ -23,19 +23,6 @@ const BTREE_HEADER_OFFSET_CELL_CONTENT: usize = 5; /* pointer to first byte of c const BTREE_HEADER_OFFSET_FRAGMENTED: usize = 7; /* number of fragmented bytes -> u8 */ const BTREE_HEADER_OFFSET_RIGHTMOST: usize = 8; /* if internalnode, pointer right most pointer (saved separately from cells) -> u32 */ -#[derive(Clone)] -pub enum SeekOp { - EQ, - GT, - GE, -} - -#[derive(Clone)] -pub enum SeekKey<'a> { - TableRowId(u64), - IndexKey(&'a OwnedRecord), -} - #[derive(Debug)] pub struct MemPage { parent: Option>, @@ -1375,52 +1362,8 @@ impl Cursor for BTreeCursor { Ok(*self.rowid.borrow()) } - fn seek_rowid(&mut self, rowid: u64) -> Result> { - match self.seek(SeekKey::TableRowId(rowid), SeekOp::EQ)? { - CursorResult::Ok((rowid, record)) => { - self.rowid.replace(rowid); - self.record.replace(record); - Ok(CursorResult::Ok(rowid.is_some())) - } - CursorResult::IO => Ok(CursorResult::IO), - } - } - - fn seek_ge_rowid(&mut self, rowid: u64) -> Result> { - match self.seek(SeekKey::TableRowId(rowid), SeekOp::GE)? { - CursorResult::Ok((rowid, record)) => { - self.rowid.replace(rowid); - self.record.replace(record); - Ok(CursorResult::Ok(rowid.is_some())) - } - CursorResult::IO => Ok(CursorResult::IO), - } - } - - fn seek_gt_rowid(&mut self, rowid: u64) -> Result> { - match self.seek(SeekKey::TableRowId(rowid), SeekOp::GT)? { - CursorResult::Ok((rowid, record)) => { - self.rowid.replace(rowid); - self.record.replace(record); - Ok(CursorResult::Ok(rowid.is_some())) - } - CursorResult::IO => Ok(CursorResult::IO), - } - } - - fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result> { - match self.seek(SeekKey::IndexKey(key), SeekOp::GE)? { - CursorResult::Ok((rowid, record)) => { - self.rowid.replace(rowid); - self.record.replace(record); - Ok(CursorResult::Ok(rowid.is_some())) - } - CursorResult::IO => Ok(CursorResult::IO), - } - } - - fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result> { - match self.seek(SeekKey::IndexKey(key), SeekOp::GT)? { + fn seek(&mut self, key: SeekKey<'_>, op: SeekOp) -> Result> { + match self.seek(key, op)? { CursorResult::Ok((rowid, record)) => { self.rowid.replace(rowid); self.record.replace(record); diff --git a/core/types.rs b/core/types.rs index e236d4843..9b24b2fbd 100644 --- a/core/types.rs +++ b/core/types.rs @@ -409,17 +409,26 @@ pub enum CursorResult { IO, } +#[derive(Clone, PartialEq, Debug)] +pub enum SeekOp { + EQ, + GE, + GT, +} + +#[derive(Clone, PartialEq, Debug)] +pub enum SeekKey<'a> { + TableRowId(u64), + IndexKey(&'a OwnedRecord), +} + pub trait Cursor { fn is_empty(&self) -> bool; fn rewind(&mut self) -> Result>; fn next(&mut self) -> Result>; fn wait_for_completion(&mut self) -> Result<()>; fn rowid(&self) -> Result>; - fn seek_rowid(&mut self, rowid: u64) -> Result>; - fn seek_ge_rowid(&mut self, rowid: u64) -> Result>; - fn seek_gt_rowid(&mut self, rowid: u64) -> Result>; - fn seek_ge_index(&mut self, key: &OwnedRecord) -> Result>; - fn seek_gt_index(&mut self, key: &OwnedRecord) -> Result>; + fn seek(&mut self, key: SeekKey, op: SeekOp) -> Result>; fn seek_to_last(&mut self) -> Result>; fn record(&self) -> Result>>; fn insert( diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index c66edb0c9..4ee979014 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -30,7 +30,9 @@ use crate::pseudo::PseudoCursor; use crate::schema::Table; use crate::storage::sqlite3_ondisk::DatabaseHeader; use crate::storage::{btree::BTreeCursor, pager::Pager}; -use crate::types::{AggContext, Cursor, CursorResult, OwnedRecord, OwnedValue, Record}; +use crate::types::{ + AggContext, Cursor, CursorResult, OwnedRecord, OwnedValue, Record, SeekKey, SeekOp, +}; use crate::{Result, DATABASE_VERSION}; use datetime::{exec_date, exec_time, exec_unixepoch}; @@ -1011,7 +1013,7 @@ impl Program { let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); let rowid = index_cursor.rowid()?; let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); - match table_cursor.seek_rowid(rowid.unwrap())? { + match table_cursor.seek(SeekKey::TableRowId(rowid.unwrap()), SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => { state.deferred_seek = Some((index_cursor_id, table_cursor_id)); @@ -1151,7 +1153,7 @@ impl Program { let index_cursor = cursors.get_mut(&index_cursor_id).unwrap(); let rowid = index_cursor.rowid()?; let table_cursor = cursors.get_mut(&table_cursor_id).unwrap(); - match table_cursor.seek_rowid(rowid.unwrap())? { + match table_cursor.seek(SeekKey::TableRowId(rowid.unwrap()), SeekOp::EQ)? { CursorResult::Ok(_) => {} CursorResult::IO => { state.deferred_seek = Some((index_cursor_id, table_cursor_id)); @@ -1186,7 +1188,7 @@ impl Program { )); } }; - match cursor.seek_rowid(rowid)? { + match cursor.seek(SeekKey::TableRowId(rowid), SeekOp::EQ)? { CursorResult::Ok(found) => { if !found { state.pc = *target_pc; @@ -1218,7 +1220,7 @@ impl Program { let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = make_owned_record(&state.registers, start_reg, num_regs); - match cursor.seek_ge_index(&record_from_regs)? { + match cursor.seek(SeekKey::IndexKey(&record_from_regs), SeekOp::GE)? { CursorResult::Ok(found) => { if !found { state.pc = *target_pc; @@ -1253,7 +1255,7 @@ impl Program { )); } }; - match cursor.seek_ge_rowid(rowid)? { + match cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GE)? { CursorResult::Ok(found) => { if !found { state.pc = *target_pc; @@ -1279,7 +1281,7 @@ impl Program { let cursor = cursors.get_mut(cursor_id).unwrap(); let record_from_regs: OwnedRecord = make_owned_record(&state.registers, start_reg, num_regs); - match cursor.seek_gt_index(&record_from_regs)? { + match cursor.seek(SeekKey::IndexKey(&record_from_regs), SeekOp::GT)? { CursorResult::Ok(found) => { if !found { state.pc = *target_pc; @@ -1314,7 +1316,7 @@ impl Program { )); } }; - match cursor.seek_gt_rowid(rowid)? { + match cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GT)? { CursorResult::Ok(found) => { if !found { state.pc = *target_pc; @@ -2052,7 +2054,7 @@ fn get_new_rowid(cursor: &mut Box, mut rng: R) -> Result break, // Found a non-existing rowid CursorResult::Ok(true) => { if count == max_attempts - 1 { @@ -2593,6 +2595,8 @@ fn execute_sqlite_version(version_integer: i64) -> String { #[cfg(test)] mod tests { + use crate::types::{SeekKey, SeekOp}; + use super::{ exec_abs, exec_char, exec_hex, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_max, exec_min, exec_nullif, exec_quote, exec_random, exec_randomblob, exec_round, @@ -2607,6 +2611,7 @@ mod tests { mock! { Cursor { fn seek_to_last(&mut self) -> Result>; + fn seek<'a>(&mut self, key: SeekKey<'a>, op: SeekOp) -> Result>; fn rowid(&self) -> Result>; fn seek_rowid(&mut self, rowid: u64) -> Result>; } @@ -2621,24 +2626,8 @@ mod tests { self.rowid() } - fn seek_rowid(&mut self, rowid: u64) -> Result> { - self.seek_rowid(rowid) - } - - fn seek_ge_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_gt_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { - unimplemented!(); - } - - fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { - unimplemented!(); + fn seek(&mut self, key: SeekKey<'_>, op: SeekOp) -> Result> { + self.seek(key, op) } fn rewind(&mut self) -> Result> { @@ -2713,10 +2702,10 @@ mod tests { .return_once(|| Ok(CursorResult::Ok(()))); mock.expect_rowid() .return_once(|| Ok(Some(std::i64::MAX as u64))); - mock.expect_seek_rowid() - .with(predicate::always()) - .returning(|rowid| { - if rowid == 50 { + mock.expect_seek() + .with(predicate::always(), predicate::always()) + .returning(|rowid, _| { + if rowid == SeekKey::TableRowId(50) { Ok(CursorResult::Ok(false)) } else { Ok(CursorResult::Ok(true)) @@ -2734,9 +2723,9 @@ mod tests { .return_once(|| Ok(CursorResult::Ok(()))); mock.expect_rowid() .return_once(|| Ok(Some(std::i64::MAX as u64))); - mock.expect_seek_rowid() - .with(predicate::always()) - .return_once(|_| Ok(CursorResult::IO)); + mock.expect_seek() + .with(predicate::always(), predicate::always()) + .return_once(|_, _| Ok(CursorResult::IO)); let result = get_new_rowid(&mut (Box::new(mock) as Box), thread_rng()); assert!(matches!(result, Ok(CursorResult::IO))); @@ -2747,9 +2736,9 @@ mod tests { .return_once(|| Ok(CursorResult::Ok(()))); mock.expect_rowid() .return_once(|| Ok(Some(std::i64::MAX as u64))); - mock.expect_seek_rowid() - .with(predicate::always()) - .returning(|_| Ok(CursorResult::Ok(true))); + mock.expect_seek() + .with(predicate::always(), predicate::always()) + .returning(|_, _| Ok(CursorResult::Ok(true))); // Mock the random number generation let result = get_new_rowid(&mut (Box::new(mock) as Box), StepRng::new(1, 1)); diff --git a/core/vdbe/sorter.rs b/core/vdbe/sorter.rs index e75650f26..c9a87e79a 100644 --- a/core/vdbe/sorter.rs +++ b/core/vdbe/sorter.rs @@ -1,5 +1,5 @@ use crate::{ - types::{Cursor, CursorResult, OwnedRecord, OwnedValue}, + types::{Cursor, CursorResult, OwnedRecord, OwnedValue, SeekKey, SeekOp}, Result, }; use std::{ @@ -75,23 +75,7 @@ impl Cursor for Sorter { todo!(); } - fn seek_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_ge_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_gt_rowid(&mut self, _: u64) -> Result> { - unimplemented!(); - } - - fn seek_ge_index(&mut self, _: &OwnedRecord) -> Result> { - unimplemented!(); - } - - fn seek_gt_index(&mut self, _: &OwnedRecord) -> Result> { + fn seek(&mut self, _: SeekKey<'_>, _: SeekOp) -> Result> { unimplemented!(); } From e5cf052f072c251fd4f249288a9959eb10277bd9 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 6 Oct 2024 23:48:59 +0300 Subject: [PATCH 26/32] Why do sqlite btree child keys have <= keys and not < keys --- core/storage/btree.rs | 12 ++++++------ testing/where.test | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index a6c7fa312..e09346936 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -346,9 +346,9 @@ impl BTreeCursor { }; mem_page.advance(); let comparison = match cmp { - SeekOp::GT => rowid_key <= *_rowid, - SeekOp::GE => rowid_key < *_rowid, - SeekOp::EQ => rowid_key < *_rowid, + SeekOp::GT => rowid_key < *_rowid, + SeekOp::GE => rowid_key <= *_rowid, + SeekOp::EQ => rowid_key <= *_rowid, }; if comparison { let mem_page = @@ -378,9 +378,9 @@ impl BTreeCursor { mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { - SeekOp::GT => index_key <= &record, - SeekOp::GE => index_key < &record, - SeekOp::EQ => index_key < &record, + SeekOp::GT => index_key < &record, + SeekOp::GE => index_key <= &record, + SeekOp::EQ => index_key <= &record, }; if comparison { let mem_page = diff --git a/testing/where.test b/testing/where.test index 4f5915a49..c98ea58bb 100755 --- a/testing/where.test +++ b/testing/where.test @@ -270,7 +270,7 @@ do_execsql_test where-complex-parentheses { 5|sweatshirt} # regression test for primary key index behavior -do_execsql_test where-id-index-seek-test { +do_execsql_test where-id-index-seek-regression-test { select id from users where id > 9995; } {9996 9997 @@ -278,11 +278,34 @@ do_execsql_test where-id-index-seek-test { 9999 10000} +do_execsql_test where-id-index-seek-regression-test-2 { + select count(1) from users where id > 0; +} {10000} + # regression test for secondary index (users.age) behavior -do_execsql_test where-id-index-seek-test-2 { - select id,age from users where age >= 100 limit 5; -} {186|100 -198|100 -301|100 -364|100 -460|100} +do_execsql_test where-age-index-seek-regression-test { + select age from users where age >= 100 limit 20; +} {100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100 +100} + +do_execsql_test where-age-index-seek-regression-test-2 { + select count(1) from users where age > 0; +} {10000} From fc71f2b32f4f8494eb49b658cb6f350691495039 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Mon, 7 Oct 2024 12:25:10 +0300 Subject: [PATCH 27/32] traverse index properly interior index cells have values that are not in the leaves, e.g. (interior: 3) / \ (leaf: 2) (leaf: 4) so their values need to be emitted after the left subtree is emitted. --- core/storage/btree.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index e09346936..87fb20d1c 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -57,6 +57,7 @@ pub struct BTreeCursor { record: RefCell>, null_flag: bool, database_header: Rc>, + going_upwards: bool, } impl BTreeCursor { @@ -73,6 +74,7 @@ impl BTreeCursor { record: RefCell::new(None), null_flag: false, database_header, + going_upwards: false, } } @@ -110,6 +112,7 @@ impl BTreeCursor { } None => match parent { Some(ref parent) => { + self.going_upwards = true; self.page.replace(Some(parent.clone())); continue; } @@ -151,11 +154,21 @@ impl BTreeCursor { left_child_page, .. }) => { - mem_page.advance(); - let mem_page = - MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); - self.page.replace(Some(Rc::new(mem_page))); - continue; + if self.going_upwards { + self.going_upwards = false; + mem_page.advance(); + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + let rowid = match record.values.last() { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } else { + let mem_page = + MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + continue; + } } BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { mem_page.advance(); @@ -164,6 +177,7 @@ impl BTreeCursor { Some(OwnedValue::Integer(rowid)) => *rowid as u64, _ => unreachable!("index cells should have an integer rowid"), }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); } } @@ -375,7 +389,6 @@ impl BTreeCursor { let SeekKey::IndexKey(index_key) = key else { unreachable!("index seek key should be a record"); }; - mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; let comparison = match cmp { SeekOp::GT => index_key < &record, @@ -388,6 +401,8 @@ impl BTreeCursor { self.page.replace(Some(Rc::new(mem_page))); found_cell = true; break; + } else { + mem_page.advance(); } } BTreeCell::IndexLeafCell(_) => { From 8563d620afaffc74f2ab59e008a4bae5a4e32299 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Mon, 7 Oct 2024 17:03:50 +0300 Subject: [PATCH 28/32] renaming --- core/storage/btree.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 87fb20d1c..78a898796 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -223,12 +223,12 @@ impl BTreeCursor { unreachable!("table seek key should be a rowid"); }; mem_page.advance(); - let comparison = match op { + let found = match op { SeekOp::GT => *cell_rowid > rowid_key, SeekOp::GE => *cell_rowid >= rowid_key, SeekOp::EQ => *cell_rowid == rowid_key, }; - if comparison { + if found { let record = crate::storage::sqlite3_ondisk::read_record(payload)?; return Ok(CursorResult::Ok((Some(*cell_rowid), Some(record)))); } @@ -239,12 +239,12 @@ impl BTreeCursor { }; mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; - let comparison = match op { + let found = match op { SeekOp::GT => record > *index_key, SeekOp::GE => record >= *index_key, SeekOp::EQ => record == *index_key, }; - if comparison { + if found { let rowid = match record.values.get(1) { Some(OwnedValue::Integer(rowid)) => *rowid as u64, _ => unreachable!("index cells should have an integer rowid"), @@ -359,12 +359,12 @@ impl BTreeCursor { unreachable!("table seek key should be a rowid"); }; mem_page.advance(); - let comparison = match cmp { + let target_leaf_page_is_in_left_subtree = match cmp { SeekOp::GT => rowid_key < *_rowid, SeekOp::GE => rowid_key <= *_rowid, SeekOp::EQ => rowid_key <= *_rowid, }; - if comparison { + if target_leaf_page_is_in_left_subtree { let mem_page = MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); @@ -390,12 +390,12 @@ impl BTreeCursor { unreachable!("index seek key should be a record"); }; let record = crate::storage::sqlite3_ondisk::read_record(payload)?; - let comparison = match cmp { + let target_leaf_page_is_in_the_left_subtree = match cmp { SeekOp::GT => index_key < &record, SeekOp::GE => index_key <= &record, SeekOp::EQ => index_key <= &record, }; - if comparison { + if target_leaf_page_is_in_the_left_subtree { let mem_page = MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); From 93a8110773e764c8507a2b4cd64187567c7b78f6 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Mon, 7 Oct 2024 17:05:38 +0300 Subject: [PATCH 29/32] dont assume index key has rowid in the second column: its the last --- core/storage/btree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 78a898796..4644836a8 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -245,7 +245,7 @@ impl BTreeCursor { SeekOp::EQ => record == *index_key, }; if found { - let rowid = match record.values.get(1) { + let rowid = match record.values.last() { Some(OwnedValue::Integer(rowid)) => *rowid as u64, _ => unreachable!("index cells should have an integer rowid"), }; From 572db69b5e42078d47df22f9e6cd313af7cd4144 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Mon, 7 Oct 2024 17:12:19 +0300 Subject: [PATCH 30/32] Add TODO comment about index corner case --- core/storage/btree.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 4644836a8..8ef8bc12b 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -396,6 +396,7 @@ impl BTreeCursor { SeekOp::EQ => index_key <= &record, }; if target_leaf_page_is_in_the_left_subtree { + // TODO: indexes store payloads in interior cells as well. What if cmp is SeekOp::EQ and the only exactly equal key is in the interior cell? let mem_page = MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); From 43038cb6aa1c902cce252c3e9526b8df4836117f Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Tue, 8 Oct 2024 07:45:21 +0300 Subject: [PATCH 31/32] Handle seek() edge case with index seek --- core/storage/btree.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 8ef8bc12b..3180eb004 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -257,6 +257,28 @@ impl BTreeCursor { } } } + + // We have now iterated over all cells in the leaf page and found no match. + let is_index = matches!(key, SeekKey::IndexKey(_)); + if is_index { + // Unlike tables, indexes store payloads in interior cells as well. self.move_to() always moves to a leaf page, so there are cases where we need to + // move back up to the parent interior cell and get the next record from there to perform a correct seek. + // an example of how this can occur: + // + // we do an index seek for key K with cmp = SeekOp::GT, meaning we want to seek to the first key that is greater than K. + // in self.move_to(), we encounter an interior cell with key K' = K+2, and move the left child page, which is a leaf page. + // the reason we move to the left child page is that we know that in an index, all keys in the left child page are less than K' i.e. less than K+2, + // meaning that the left subtree may contain a key greater than K, e.g. K+1. however, it is possible that it doesn't, in which case the correct + // next key is K+2, which is in the parent interior cell. + // + // In the seek() method, once we have landed in the leaf page and find that there is no cell with a key greater than K, + // if we were to return Ok(CursorResult::Ok((None, None))), self.record would be None, which is incorrect, because we already know + // that there is a record with a key greater than K (K' = K+2) in the parent interior cell. Hence, we need to move back up to the parent + // interior cell and get the next record from there. self.get_next_record() does that automatically by setting self.going_upwards to true + // and then obtaining the next record from the parent interior cell. + return self.get_next_record(); + } + Ok(CursorResult::Ok((None, None))) } @@ -396,7 +418,6 @@ impl BTreeCursor { SeekOp::EQ => index_key <= &record, }; if target_leaf_page_is_in_the_left_subtree { - // TODO: indexes store payloads in interior cells as well. What if cmp is SeekOp::EQ and the only exactly equal key is in the interior cell? let mem_page = MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); self.page.replace(Some(Rc::new(mem_page))); From 556f4b73c90ea1812413cc73d0483259b791ce87 Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Tue, 8 Oct 2024 08:23:30 +0300 Subject: [PATCH 32/32] Refine edge case handling: add optional predicate to get_next_record() --- core/storage/btree.rs | 85 +++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 3180eb004..3368e9d7e 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -90,7 +90,10 @@ impl BTreeCursor { Ok(CursorResult::Ok(page.cell_count() == 0)) } - fn get_next_record(&mut self) -> Result, Option)>> { + fn get_next_record<'a>( + &mut self, + predicate: Option<(SeekKey<'a>, SeekOp)>, + ) -> Result, Option)>> { loop { let mem_page = self.get_mem_page(); let page_idx = mem_page.page_idx; @@ -134,6 +137,7 @@ impl BTreeCursor { _left_child_page, _rowid, }) => { + assert!(predicate.is_none()); mem_page.advance(); let mem_page = MemPage::new(Some(mem_page.clone()), *_left_child_page as usize, 0); @@ -145,6 +149,7 @@ impl BTreeCursor { _payload, first_overflow_page: _, }) => { + assert!(predicate.is_none()); mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(_payload)?; return Ok(CursorResult::Ok((Some(*_rowid), Some(record)))); @@ -154,31 +159,72 @@ impl BTreeCursor { left_child_page, .. }) => { - if self.going_upwards { - self.going_upwards = false; - mem_page.advance(); - let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + if !self.going_upwards { + let mem_page = + MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); + self.page.replace(Some(Rc::new(mem_page))); + continue; + } + + self.going_upwards = false; + mem_page.advance(); + + let record = crate::storage::sqlite3_ondisk::read_record(payload)?; + if predicate.is_none() { + let rowid = match record.values.last() { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } + + let (key, op) = predicate.as_ref().unwrap(); + let SeekKey::IndexKey(index_key) = key else { + unreachable!("index seek key should be a record"); + }; + let found = match op { + SeekOp::GT => &record > *index_key, + SeekOp::GE => &record >= *index_key, + SeekOp::EQ => &record == *index_key, + }; + if found { let rowid = match record.values.last() { Some(OwnedValue::Integer(rowid)) => *rowid as u64, _ => unreachable!("index cells should have an integer rowid"), }; return Ok(CursorResult::Ok((Some(rowid), Some(record)))); } else { - let mem_page = - MemPage::new(Some(mem_page.clone()), *left_child_page as usize, 0); - self.page.replace(Some(Rc::new(mem_page))); continue; } } BTreeCell::IndexLeafCell(IndexLeafCell { payload, .. }) => { mem_page.advance(); let record = crate::storage::sqlite3_ondisk::read_record(payload)?; - let rowid = match record.values.last() { - Some(OwnedValue::Integer(rowid)) => *rowid as u64, - _ => unreachable!("index cells should have an integer rowid"), + if predicate.is_none() { + let rowid = match record.values.last() { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } + let (key, op) = predicate.as_ref().unwrap(); + let SeekKey::IndexKey(index_key) = key else { + unreachable!("index seek key should be a record"); }; - - return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + let found = match op { + SeekOp::GT => &record > *index_key, + SeekOp::GE => &record >= *index_key, + SeekOp::EQ => &record == *index_key, + }; + if found { + let rowid = match record.values.last() { + Some(OwnedValue::Integer(rowid)) => *rowid as u64, + _ => unreachable!("index cells should have an integer rowid"), + }; + return Ok(CursorResult::Ok((Some(rowid), Some(record)))); + } else { + continue; + } } } } @@ -273,10 +319,9 @@ impl BTreeCursor { // // In the seek() method, once we have landed in the leaf page and find that there is no cell with a key greater than K, // if we were to return Ok(CursorResult::Ok((None, None))), self.record would be None, which is incorrect, because we already know - // that there is a record with a key greater than K (K' = K+2) in the parent interior cell. Hence, we need to move back up to the parent - // interior cell and get the next record from there. self.get_next_record() does that automatically by setting self.going_upwards to true - // and then obtaining the next record from the parent interior cell. - return self.get_next_record(); + // that there is a record with a key greater than K (K' = K+2) in the parent interior cell. Hence, we need to move back up the tree + // and get the next matching record from there. + return self.get_next_record(Some((key, op))); } Ok(CursorResult::Ok((None, None))) @@ -1344,7 +1389,7 @@ fn find_free_cell(page_ref: &PageContent, db_header: Ref, amount impl Cursor for BTreeCursor { fn seek_to_last(&mut self) -> Result> { self.move_to_rightmost()?; - match self.get_next_record()? { + match self.get_next_record(None)? { CursorResult::Ok((rowid, next)) => { if rowid.is_none() { match self.is_empty_table()? { @@ -1369,7 +1414,7 @@ impl Cursor for BTreeCursor { fn rewind(&mut self) -> Result> { let mem_page = MemPage::new(None, self.root_page, 0); self.page.replace(Some(Rc::new(mem_page))); - match self.get_next_record()? { + match self.get_next_record(None)? { CursorResult::Ok((rowid, next)) => { self.rowid.replace(rowid); self.record.replace(next); @@ -1380,7 +1425,7 @@ impl Cursor for BTreeCursor { } fn next(&mut self) -> Result> { - match self.get_next_record()? { + match self.get_next_record(None)? { CursorResult::Ok((rowid, next)) => { self.rowid.replace(rowid); self.record.replace(next);