From f02da18acdd85f28652ee66a829461f6c09e4c6e Mon Sep 17 00:00:00 2001 From: jussisaurio Date: Sun, 29 Sep 2024 19:13:50 +0300 Subject: [PATCH] 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,16 +151,77 @@ 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(_) => { - unimplemented!(); + 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)))); + } + } + cell_type => { + unreachable!("unexpected cell type: {:?}", cell_type); + } + } + } + Ok(CursorResult::Ok((None, None))) + } + fn btree_seek_rowid( &mut self, rowid: u64, @@ -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 zcmce<1-R8l+qS*e$|RYIE_b)RH+6T1x*MfRg?gz_FKy~jDwHa1DRp=1?k?25P*+Ob zsQt-x_4xnqJHGdU?|q(^pW`6sy6%}IGxyvxlRYcRI{ME)#8>x&Ka{eSed6upb#84ornxJxs2TZkKEBx=qjwo~=#IOM+F_4f4n1hh zr~}5tQfq8FY_-jYHP+d1=&-FC|0Ff`9o;~3V<5B)9=DS>ZYM7m{*QQKJO0iX75_>a&M{^O!m(3 zPVr9kj`j}s4)jKQyN52?*4xtC*jv{d>aFZ8>n-jr==HpYSMoBR=SeT_{^9=Ye(Qee ze(cV0-*jJgpL3sZA9C+;Z*y;SuXZnWr?}_1r@Ir}~ik?z*+=I(~> zTJEaua_-{p0&d5xy1BshUFpW0InEExEawwvhVzE=qVu%#h;y%Vn{$J6rE`%p*_q@_ zaE^1vItMwUo!y*~&Io5yXFX@Avy!v4vxqa`G@X)@b{t2y|FVCvzqP-xKeXSmU$vjN zpRgaa@3L>PueC3;r`TuPr`hA|qwT}&1MI!*UF_}bU`u-=du@9)dwF{adqKNv*X+EV zu$3LRez$(KzP3KKW?FArFImr6k6QOxw_7(_S6LTZ=UHc1CtJr`M_LD4`&zqOJ6c;? zn_25yYg(&V%UX+BL#(z{v9gwDnO2|rO?|JvQXi}L)az=xdP+U4?oqd@>(v!%YM{m3HvFZqQpxQ_6s&-IYsZG?nY7MobT1qXf`c*>}RZ7`P$Uo)J@*DZN{6M}fUy;wr z$K?a^PI;nV*=!4D${1Me}L%5%XU2HuDDaO7mj#Jo60mWb=6QNb_KGUvqbJM{{d)Gjn}& zn7OjKjJc>eXtvCxG(IxkHC{7bFrG9XGVV5}8P^$?8y6bq7!!^0 z#xcg>#u#I7W0bMIG2GbLSjSl1SixA*SjgxFM%^eFNy9QU{SW;oeYXCY{=WW}{<8k8 z{+NEheusXOezksye!hODeu{pAew2QQzMsB_zLUO-PrNRsi>JiH;vR9UxL#Z-E*9sBGsMZ_cyXjSSnMl;-NlY#Yq6PF zUkno~i)F;3Vo&aRT)cjpL!} z2NL@OL49ek_9M{-swkVNE(%qdPFzH!2puT>MLYu(R|N_u5r+{xWB%Y6yT-JIwE&Q%1f?XcK31H!%#Y4-NSTEv=wbta&;LHr5D_LV=uDj789KVS*X z<&ma8OVCd`Vq1vMVH6XIBY^m?#NGtDoru%hakgtb^hFScUhG@siP1x`4XDaMAJh8Y zpgJRpe^HBs){icw^`3|7;p|#(OR5&oMPqB-r>Rgft@9#O=aLA4)OTCPOG(Ur}0_><=(r6|v zjj+;E?;|ZalemaDfk2RG$t9o?bBGfGA3ap_&!jqvz;L1YYja2MNLW1=aK9ujAXsg8 zQz!=`y=MOgWyb;QcVZTx%7A1kH`kswKRm(*p-bH_jcP*$6;wavk8mb^3?~H}Dg%ib_aC5wg zW5%2CZ@jrXvNT>L;Iw#S7}Zih{W742UK_7nM0FmqAyFdoKy|$cLA=WSE9kHB3W79V z=3&aHZoG^!I$lOR#LGSGUwV$<4y7fjbfAd%jTc$p0y@!rG1DW-K^xwpr#OcIJ1iDW=H3BL*2}rz7oJO!r{yR{9a2(09`50N^9*gMl z*xqu;;#C0m58`}c6tOn30^nc_iaT_-jrfh*h_SfM>e?)Y^-h32EIQF*9>r6rJCLT> z_VPCk*SN8B6^qcMZP^byzJNBqZij8kzzcMJMb3?i#Y z0osQIU7}$sk87i8WDPY2Qv5&|S)x~Ag&fr!xiA=-AQV|ysJ zYed`5UD)o9?TXmi*!D%+_OIA}f^9f6gqH9$;r762+ZvB;Bic4E#&!y}$k`l=&&41@ zeKNLNV7o51t3}(|&)9y0?XB3J6>TdhR%ID{_M&Y$ygUoH>thRh>CfnM=__o%Z>5=s)8>7OoQ9OM<9|-2PKro-z2ZDJm5X|TGfnZ(>1oL@)Aeh$z z!F*mH2;#c4C%XE-SRg=}g`q>A590XcU@{>jSv{5Z!g7YW|lC1oL@)AX@(r zjd3{rf4M*~pVtSXzds9@oWy^jPo@Yja&7*gt9>EweST3GhF- zJ`lvUxi1jlg!6BGAkgNqKoHe_KCTZ0xDuVq0>OM4gwGAUT$7}qHwf4U*5X|TG zfhhjtb6X&o&+7wW{Kx0IKro-z2g3EAxi1i)VdmfZKrpWbf~bAwQt#KH$8JwSq^nJqFu#qOJW0w$EZa9P1zU z+7MV`us)7HTd!anE*FKLu?nJoi$1HBqph5cZ5msw1exvVvl)-J23CuV4WrLG_6onW zj6Uy)?NZq0qOAy*eZut*?Sn{YFJOzsmiR-_=lGV`B5MpwQ?W}TOKeAM{b<{FN3=fh zgmyun|Goc}|FQpGxHd4|f69N@zbCwd;ClZGf2x13f4YBCxGr#nf1tmQzpKAPc-O!t z{<{7e{)&FERCv=szu)kSe#*DQTL%90e)hicKKDKdZy0#Rd(L~@d%(Lhyj|cL?^5pq z?=0`s@LqwyJJj3X+tb@Qyi;HcZ$ob_Z&h!(@IHYBypC7(a-JXFB@lDxxIehF+)u)L z1m198bf0z~aqkW95V*m;(!I!?>`n^r4>-;p8@LC#qut%Y+XF_po4V_{L*13!rQJo` z0k`Rv+_dYsy7QOwi}S7Xh4Z2Fj`OPXyz_+fpmUdVi*v1WnKQ*X+d0h{=N#=E<{aSc z_V4zO_Sg2O_DuUt`z8As`%yc%&%WKh z(Z0&Q*gnrb!#>46(GKiG?fvaN?Vasy?JevL?X~Px?d9yn?FHt7(<2wB=ZOp#D<7sBhI5>O=L8dR0BIo=^{}yVNb}T6LM4qRv*Qsd4IPb(lIp z?WJ~6+o>(pMrv)fnp$2hp%zqKRa1GDP$~?|-{p_;Yx$|1Dc_VY$!FxF@;-UHyir~y zFZq{u2LyA!J>dWR>;EEivN_3|U>;|VH4idJo4c7K%@O9N=6dE(b0u?Wa}jgEY?>uA zZ91lI{AK)Nd~1ARd}zF5ylOmeJYhU&+-2NiTx(orOfk+jP7922#?i)M#sS7 No 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 delta 125 zcmZo@@M>7#GC^99nSp^(kO2XhCh8ai1@)2{d4YmVd`S#^Px+EID=K{AZBF8APvT<) vVkRJF24WT