Files
turso/core/translate/index.rs
Pekka Enberg 14ef25ebb8 Merge 'Add drop index' from Anton Harniakou
This commit adds suport for DROP INDEX.
Bytecode produced by this commit differs from SQLITE's bytecode, main
reason we don't do autovacuum or repacking of pages like SQLITE does.
Closes #1280

Closes #1444
2025-05-10 08:04:39 +03:00

474 lines
15 KiB
Rust

use std::sync::Arc;
use crate::vdbe::insn::CmpInsFlags;
use crate::{
schema::{BTreeTable, Column, Index, IndexColumn, PseudoTable, Schema},
storage::pager::CreateBTreeFlags,
util::normalize_ident,
vdbe::{
builder::{CursorType, ProgramBuilder, QueryMode},
insn::{IdxInsertFlags, Insn, RegisterOrLiteral},
},
};
use limbo_sqlite3_parser::ast::{self, Expr, Id, SortOrder, SortedColumn};
use super::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
pub fn translate_create_index(
mode: QueryMode,
unique_if_not_exists: (bool, bool),
idx_name: &str,
tbl_name: &str,
columns: &[SortedColumn],
schema: &Schema,
) -> crate::Result<ProgramBuilder> {
let idx_name = normalize_ident(idx_name);
let tbl_name = normalize_ident(tbl_name);
let mut program = ProgramBuilder::new(crate::vdbe::builder::ProgramBuilderOpts {
query_mode: mode,
num_cursors: 5,
approx_num_insns: 40,
approx_num_labels: 5,
});
// Check if the index is being created on a valid btree table and
// the name is globally unique in the schema.
if !schema.is_unique_idx_name(&idx_name) {
crate::bail_parse_error!("Error: index with name '{idx_name}' already exists.");
}
let Some(tbl) = schema.tables.get(&tbl_name) else {
crate::bail_parse_error!("Error: table '{tbl_name}' does not exist.");
};
let Some(tbl) = tbl.btree() else {
crate::bail_parse_error!("Error: table '{tbl_name}' is not a b-tree table.");
};
let columns = resolve_sorted_columns(&tbl, columns)?;
// Prologue:
let init_label = program.emit_init();
let start_offset = program.offset();
let idx = Arc::new(Index {
name: idx_name.clone(),
table_name: tbl.name.clone(),
root_page: 0, // we dont have access till its created, after we parse the schema table
columns: columns
.iter()
.map(|((pos_in_table, col), order)| IndexColumn {
name: col.name.as_ref().unwrap().clone(),
order: *order,
pos_in_table: *pos_in_table,
})
.collect(),
unique: unique_if_not_exists.0,
ephemeral: false,
});
// Allocate the necessary cursors:
//
// 1. sqlite_schema_cursor_id - sqlite_schema table
// 2. btree_cursor_id - new index btree
// 3. table_cursor_id - table we are creating the index on
// 4. sorter_cursor_id - sorter
// 5. pseudo_cursor_id - pseudo table to store the sorted index values
let sqlite_table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(
Some(SQLITE_TABLEID.to_owned()),
CursorType::BTreeTable(sqlite_table.clone()),
);
let btree_cursor_id = program.alloc_cursor_id(
Some(idx_name.to_owned()),
CursorType::BTreeIndex(idx.clone()),
);
let table_cursor_id = program.alloc_cursor_id(
Some(tbl_name.to_owned()),
CursorType::BTreeTable(tbl.clone()),
);
let sorter_cursor_id = program.alloc_cursor_id(None, CursorType::Sorter);
let pseudo_table = PseudoTable::new_with_columns(tbl.columns.clone());
let pseudo_cursor_id = program.alloc_cursor_id(None, CursorType::Pseudo(pseudo_table.into()));
// Create a new B-Tree and store the root page index in a register
let root_page_reg = program.alloc_register();
program.emit_insn(Insn::CreateBtree {
db: 0,
root: root_page_reg,
flags: CreateBTreeFlags::new_index(),
});
// open the sqlite schema table for writing and create a new entry for the index
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: RegisterOrLiteral::Literal(sqlite_table.root_page),
});
let sql = create_idx_stmt_to_sql(&tbl_name, &idx_name, unique_if_not_exists, &columns);
emit_schema_entry(
&mut program,
sqlite_schema_cursor_id,
SchemaEntryType::Index,
&idx_name,
&tbl_name,
root_page_reg,
Some(sql),
);
// determine the order of the columns in the index for the sorter
let order = idx.columns.iter().map(|c| c.order.clone()).collect();
// open the sorter and the pseudo table
program.emit_insn(Insn::SorterOpen {
cursor_id: sorter_cursor_id,
columns: columns.len(),
order,
});
let content_reg = program.alloc_register();
program.emit_insn(Insn::OpenPseudo {
cursor_id: pseudo_cursor_id,
content_reg,
num_fields: columns.len() + 1,
});
// open the table we are creating the index on for reading
program.emit_insn(Insn::OpenRead {
cursor_id: table_cursor_id,
root_page: tbl.root_page,
});
let loop_start_label = program.allocate_label();
let loop_end_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: table_cursor_id,
pc_if_empty: loop_end_label,
});
program.preassign_label_to_next_insn(loop_start_label);
// Loop start:
// Collect index values into start_reg..rowid_reg
// emit MakeRecord (index key + rowid) into record_reg.
//
// Then insert the record into the sorter
let start_reg = program.alloc_registers(columns.len() + 1);
for (i, (col, _)) in columns.iter().enumerate() {
program.emit_insn(Insn::Column {
cursor_id: table_cursor_id,
column: col.0,
dest: start_reg + i,
});
}
let rowid_reg = start_reg + columns.len();
program.emit_insn(Insn::RowId {
cursor_id: table_cursor_id,
dest: rowid_reg,
});
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg,
count: columns.len() + 1,
dest_reg: record_reg,
});
program.emit_insn(Insn::SorterInsert {
cursor_id: sorter_cursor_id,
record_reg,
});
program.emit_insn(Insn::Next {
cursor_id: table_cursor_id,
pc_if_next: loop_start_label,
});
program.preassign_label_to_next_insn(loop_end_label);
// Open the index btree we created for writing to insert the
// newly sorted index records.
program.emit_insn(Insn::OpenWrite {
cursor_id: btree_cursor_id,
root_page: RegisterOrLiteral::Register(root_page_reg),
});
let sorted_loop_start = program.allocate_label();
let sorted_loop_end = program.allocate_label();
// Sort the index records in the sorter
program.emit_insn(Insn::SorterSort {
cursor_id: sorter_cursor_id,
pc_if_empty: sorted_loop_end,
});
program.preassign_label_to_next_insn(sorted_loop_start);
let sorted_record_reg = program.alloc_register();
program.emit_insn(Insn::SorterData {
pseudo_cursor: pseudo_cursor_id,
cursor_id: sorter_cursor_id,
dest_reg: sorted_record_reg,
});
// seek to the end of the index btree to position the cursor for appending
program.emit_insn(Insn::SeekEnd {
cursor_id: btree_cursor_id,
});
// insert new index record
program.emit_insn(Insn::IdxInsert {
cursor_id: btree_cursor_id,
record_reg: sorted_record_reg,
unpacked_start: None, // TODO: optimize with these to avoid decoding record twice
unpacked_count: None,
flags: IdxInsertFlags::new().use_seek(false),
});
program.emit_insn(Insn::SorterNext {
cursor_id: sorter_cursor_id,
pc_if_next: sorted_loop_start,
});
program.preassign_label_to_next_insn(sorted_loop_end);
// End of the outer loop
//
// Keep schema table open to emit ParseSchema, close the other cursors.
program.close_cursors(&[sorter_cursor_id, table_cursor_id, btree_cursor_id]);
// TODO: SetCookie for schema change
//
// Parse the schema table to get the index root page and add new index to Schema
let parse_schema_where_clause = format!("name = '{}' AND type = 'index'", idx_name);
program.emit_insn(Insn::ParseSchema {
db: sqlite_schema_cursor_id,
where_clause: Some(parse_schema_where_clause),
});
// Close the final sqlite_schema cursor
program.emit_insn(Insn::Close {
cursor_id: sqlite_schema_cursor_id,
});
// Epilogue:
program.emit_halt();
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
Ok(program)
}
fn resolve_sorted_columns<'a>(
table: &'a BTreeTable,
cols: &[SortedColumn],
) -> crate::Result<Vec<((usize, &'a Column), SortOrder)>> {
let mut resolved = Vec::with_capacity(cols.len());
for sc in cols {
let ident = normalize_ident(match &sc.expr {
Expr::Id(Id(col_name)) | Expr::Name(ast::Name(col_name)) => col_name,
_ => crate::bail_parse_error!("Error: cannot use expressions in CREATE INDEX"),
});
let Some(col) = table.get_column(&ident) else {
crate::bail_parse_error!(
"Error: column '{ident}' does not exist in table '{}'",
table.name
);
};
resolved.push((col, sc.order.unwrap_or(SortOrder::Asc)));
}
Ok(resolved)
}
fn create_idx_stmt_to_sql(
tbl_name: &str,
idx_name: &str,
unique_if_not_exists: (bool, bool),
cols: &[((usize, &Column), SortOrder)],
) -> String {
let mut sql = String::with_capacity(128);
sql.push_str("CREATE ");
if unique_if_not_exists.0 {
sql.push_str("UNIQUE ");
}
sql.push_str("INDEX ");
if unique_if_not_exists.1 {
sql.push_str("IF NOT EXISTS ");
}
sql.push_str(idx_name);
sql.push_str(" ON ");
sql.push_str(tbl_name);
sql.push_str(" (");
for (i, (col, order)) in cols.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
sql.push_str(col.1.name.as_ref().unwrap());
if *order == SortOrder::Desc {
sql.push_str(" DESC");
}
}
sql.push(')');
sql
}
pub fn translate_drop_index(
mode: QueryMode,
idx_name: &str,
if_exists: bool,
schema: &Schema,
) -> crate::Result<ProgramBuilder> {
let idx_name = normalize_ident(idx_name);
let mut program = ProgramBuilder::new(crate::vdbe::builder::ProgramBuilderOpts {
query_mode: mode,
num_cursors: 5,
approx_num_insns: 40,
approx_num_labels: 5,
});
// Find the index in Schema
let mut maybe_index = None;
for val in schema.indexes.values() {
if maybe_index.is_some() {
break;
}
for idx in val {
if idx.name == idx_name {
maybe_index = Some(idx);
break;
}
}
}
// If there's no index if_exist is true,
// then return normaly, otherwise show an error.
if maybe_index.is_none() {
if if_exists {
let init_label = program.emit_init();
let start_offset = program.offset();
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
return Ok(program);
} else {
return Err(crate::error::LimboError::InvalidArgument(format!(
"No such index: {}",
&idx_name
)));
}
}
// 1. Init
// 2. Goto
let init_label = program.emit_init();
let start_offset = program.offset();
// According to sqlite should emit Null instruction
// but why?
let null_reg = program.alloc_register();
program.emit_null(null_reg, None);
// String8; r[3] = 'some idx name'
let index_name_reg = program.emit_string8_new_reg(idx_name.to_string());
// String8; r[4] = 'index'
let index_str_reg = program.emit_string8_new_reg("index".to_string());
// for r[5]=rowid
let row_id_reg = program.alloc_register();
// We're going to use this cursor to search through sqlite_schema
let sqlite_table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(
Some(SQLITE_TABLEID.to_owned()),
CursorType::BTreeTable(sqlite_table.clone()),
);
// Open root=1 iDb=0; sqlite_schema for writing
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: RegisterOrLiteral::Literal(sqlite_table.root_page),
});
let loop_start_label = program.allocate_label();
let loop_end_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: sqlite_schema_cursor_id,
pc_if_empty: loop_end_label,
});
program.resolve_label(loop_start_label, program.offset());
// Read sqlite_schema.name into dest_reg
let dest_reg = program.alloc_register();
program.emit_insn(Insn::Column {
cursor_id: sqlite_schema_cursor_id,
column: 1, // sqlite_schema.name
dest: dest_reg,
});
// if current column is not index_name then jump to Next
// skip if sqlite_schema.name != index_name_reg
let next_label = program.allocate_label();
program.emit_insn(Insn::Ne {
lhs: index_name_reg,
rhs: dest_reg,
target_pc: next_label,
flags: CmpInsFlags::default(),
});
// read type of table
// skip if sqlite_schema.type != 'index' (index_str_reg)
program.emit_insn(Insn::Column {
cursor_id: sqlite_schema_cursor_id,
column: 0,
dest: dest_reg,
});
// if current column is not index then jump to Next
program.emit_insn(Insn::Ne {
lhs: index_str_reg,
rhs: dest_reg,
target_pc: next_label,
flags: CmpInsFlags::default(),
});
program.emit_insn(Insn::RowId {
cursor_id: sqlite_schema_cursor_id,
dest: row_id_reg,
});
let label_once_end = program.allocate_label();
program.emit_insn(Insn::Once {
target_pc_when_reentered: label_once_end,
});
program.resolve_label(label_once_end, program.offset());
program.emit_insn(Insn::Delete {
cursor_id: sqlite_schema_cursor_id,
});
program.resolve_label(next_label, program.offset());
program.emit_insn(Insn::Next {
cursor_id: sqlite_schema_cursor_id,
pc_if_next: loop_start_label,
});
program.resolve_label(loop_end_label, program.offset());
/* TODO do set cookie for real
program.emit_insn(Insn::SetCookie {
...
});
*/
// Destroy index btree
program.emit_insn(Insn::Destroy {
root: maybe_index.unwrap().root_page,
former_root_reg: 0,
is_temp: 0,
});
// Remove from the Schema any mention of the index
if let Some(idx) = maybe_index {
program.emit_insn(Insn::DropIndex {
index: idx.clone(),
db: 0,
});
}
// Epilogue:
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
Ok(program)
}