Merge 'Ephemeral Table in Update' from Pedro Muniz

Closes #1713. Adds ephemeral table when a rowid_alias is being updated.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #1726
This commit is contained in:
Jussi Saurio 2025-06-21 19:07:32 +03:00
commit a549f2971d
14 changed files with 396 additions and 153 deletions

View file

@ -6610,7 +6610,8 @@ mod tests {
let page = page.get();
let page = page.get_contents();
let header_size = 8;
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(1))]);
let regs = &[Register::Value(Value::Integer(1))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(1, 0, page, record, &conn);
assert_eq!(page.cell_count(), 1);
let free = compute_free_space(page, 4096);
@ -6639,8 +6640,8 @@ mod tests {
let mut cells = Vec::new();
let usable_space = 4096;
for i in 0..3 {
let record =
ImmutableRecord::from_registers(&[Register::Value(Value::Integer(i as i64))]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(i, i, page, record, &conn);
assert_eq!(page.cell_count(), i + 1);
let free = compute_free_space(page, usable_space);
@ -6928,10 +6929,8 @@ mod tests {
pager.deref(),
)
.unwrap();
let value = ImmutableRecord::from_registers(&[Register::Value(Value::Blob(vec![
0;
*size
]))]);
let regs = &[Register::Value(Value::Blob(vec![0; *size]))];
let value = ImmutableRecord::from_registers(regs, regs.len());
tracing::info!("insert key:{}", key);
run_until_done(
|| cursor.insert(&BTreeKey::new_table_rowid(*key, Some(&value)), true),
@ -7022,8 +7021,8 @@ mod tests {
pager.deref(),
)
.unwrap();
let value =
ImmutableRecord::from_registers(&[Register::Value(Value::Blob(vec![0; size]))]);
let regs = &[Register::Value(Value::Blob(vec![0; size]))];
let value = ImmutableRecord::from_registers(regs, regs.len());
let btree_before = if do_validate {
format_btree(pager.clone(), root_page, 0)
} else {
@ -7143,11 +7142,11 @@ mod tests {
};
tracing::info!("insert {}/{}: {:?}", i + 1, inserts, key);
keys.push(key.clone());
let value = ImmutableRecord::from_registers(
&key.iter()
.map(|col| Register::Value(Value::Integer(*col)))
.collect::<Vec<_>>(),
);
let regs = key
.iter()
.map(|col| Register::Value(Value::Integer(*col)))
.collect::<Vec<_>>();
let value = ImmutableRecord::from_registers(&regs, regs.len());
run_until_done(
|| {
cursor.insert(
@ -7176,12 +7175,12 @@ mod tests {
tracing::info!("seeking key {}/{}: {:?}", i + 1, keys.len(), key);
let exists = run_until_done(
|| {
let regs = key
.iter()
.map(|col| Register::Value(Value::Integer(*col)))
.collect::<Vec<_>>();
cursor.seek(
SeekKey::IndexKey(&ImmutableRecord::from_registers(
&key.iter()
.map(|col| Register::Value(Value::Integer(*col)))
.collect::<Vec<_>>(),
)),
SeekKey::IndexKey(&ImmutableRecord::from_registers(&regs, regs.len())),
SeekOp::GE { eq_only: true },
)
},
@ -7244,8 +7243,8 @@ mod tests {
let usable_space = 4096;
let total_cells = 10;
for i in 0..total_cells {
let record =
ImmutableRecord::from_registers(&[Register::Value(Value::Integer(i as i64))]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(i, i, page, record, &conn);
assert_eq!(page.cell_count(), i + 1);
let free = compute_free_space(page, usable_space);
@ -7650,8 +7649,8 @@ mod tests {
let mut cells = Vec::new();
let usable_space = 4096;
for i in 0..3 {
let record =
ImmutableRecord::from_registers(&[Register::Value(Value::Integer(i as i64))]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(i, i, page, record, &conn);
assert_eq!(page.cell_count(), i + 1);
let free = compute_free_space(page, usable_space);
@ -7692,8 +7691,8 @@ mod tests {
let usable_space = 4096;
let total_cells = 10;
for i in 0..total_cells {
let record =
ImmutableRecord::from_registers(&[Register::Value(Value::Integer(i as i64))]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(i, i, page, record, &conn);
assert_eq!(page.cell_count(), i + 1);
let free = compute_free_space(page, usable_space);
@ -7748,9 +7747,8 @@ mod tests {
// allow appends with extra place to insert
let cell_idx = rng.next_u64() as usize % (page.cell_count() + 1);
let free = compute_free_space(page, usable_space);
let record = ImmutableRecord::from_registers(&[Register::Value(
Value::Integer(i as i64),
)]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let mut payload: Vec<u8> = Vec::new();
fill_cell_payload(
page.page_type(),
@ -7827,9 +7825,8 @@ mod tests {
// allow appends with extra place to insert
let cell_idx = rng.next_u64() as usize % (page.cell_count() + 1);
let free = compute_free_space(page, usable_space);
let record = ImmutableRecord::from_registers(&[Register::Value(
Value::Integer(i as i64),
)]);
let regs = &[Register::Value(Value::Integer(i as i64))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let mut payload: Vec<u8> = Vec::new();
fill_cell_payload(
page.page_type(),
@ -7987,7 +7984,8 @@ mod tests {
let header_size = 8;
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(0, 0, page, record, &conn);
let free = compute_free_space(page, usable_space);
assert_eq!(free, 4096 - payload.len() as u16 - 2 - header_size);
@ -8003,7 +8001,8 @@ mod tests {
let page = page.get_contents();
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(0, 0, page, record, &conn);
assert_eq!(page.cell_count(), 1);
@ -8029,17 +8028,19 @@ mod tests {
let page = page.get_contents();
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[
let regs = &[
Register::Value(Value::Integer(0)),
Register::Value(Value::Text(Text::new("aaaaaaaa"))),
]);
];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 0, page, record, &conn);
assert_eq!(page.cell_count(), 1);
drop_cell(page, 0, usable_space).unwrap();
assert_eq!(page.cell_count(), 0);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(0, 0, page, record, &conn);
assert_eq!(page.cell_count(), 1);
@ -8063,10 +8064,11 @@ mod tests {
let page = page.get_contents();
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[
let regs = &[
Register::Value(Value::Integer(0)),
Register::Value(Value::Text(Text::new("aaaaaaaa"))),
]);
];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 0, page, record, &conn);
for _ in 0..100 {
@ -8074,7 +8076,8 @@ mod tests {
drop_cell(page, 0, usable_space).unwrap();
assert_eq!(page.cell_count(), 0);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(0, 0, page, record, &conn);
assert_eq!(page.cell_count(), 1);
@ -8099,11 +8102,14 @@ mod tests {
let page = page.get_contents();
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let payload = add_record(0, 0, page, record, &conn);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(1))]);
let regs = &[Register::Value(Value::Integer(1))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(1, 1, page, record, &conn);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(2))]);
let regs = &[Register::Value(Value::Integer(2))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(2, 2, page, record, &conn);
drop_cell(page, 1, usable_space).unwrap();
@ -8122,21 +8128,25 @@ mod tests {
let page = page.get_contents();
let usable_space = 4096;
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 0, page, record, &conn);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 0, page, record, &conn);
drop_cell(page, 0, usable_space).unwrap();
defragment_page(page, usable_space);
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 1, page, record, &conn);
drop_cell(page, 0, usable_space).unwrap();
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, 1, page, record, &conn);
}
@ -8148,7 +8158,8 @@ mod tests {
let page = get_page(2);
let usable_space = 4096;
let insert = |pos, page| {
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, pos, page, record, &conn);
};
let drop = |pos, page| {
@ -8188,7 +8199,8 @@ mod tests {
let page = get_page(2);
let usable_space = 4096;
let insert = |pos, page| {
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let _ = add_record(0, pos, page, record, &conn);
};
let drop = |pos, page| {
@ -8197,7 +8209,8 @@ mod tests {
let defragment = |page| {
defragment_page(page, usable_space);
};
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(0))]);
let regs = &[Register::Value(Value::Integer(0))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let mut payload: Vec<u8> = Vec::new();
fill_cell_payload(
page.get().get_contents().page_type(),
@ -8231,7 +8244,8 @@ mod tests {
for i in 0..10000 {
let mut cursor = BTreeCursor::new_table(None, pager.clone(), root_page);
tracing::info!("INSERT INTO t VALUES ({});", i,);
let value = ImmutableRecord::from_registers(&[Register::Value(Value::Integer(i))]);
let regs = &[Register::Value(Value::Integer(i))];
let value = ImmutableRecord::from_registers(regs, regs.len());
tracing::trace!("before insert {}", i);
run_until_done(
|| {
@ -8270,8 +8284,8 @@ mod tests {
let page = get_page(2);
let usable_space = 4096;
let record =
ImmutableRecord::from_registers(&[Register::Value(Value::Blob(vec![0; 3600]))]);
let regs = &[Register::Value(Value::Blob(vec![0; 3600]))];
let record = ImmutableRecord::from_registers(regs, regs.len());
let mut payload: Vec<u8> = Vec::new();
fill_cell_payload(
page.get().get_contents().page_type(),
@ -8306,9 +8320,8 @@ mod tests {
// Insert 10,000 records in to the BTree.
for i in 1..=10000 {
let mut cursor = BTreeCursor::new_table(None, pager.clone(), root_page);
let value = ImmutableRecord::from_registers(&[Register::Value(Value::Text(
Text::new("hello world"),
))]);
let regs = &[Register::Value(Value::Text(Text::new("hello world")))];
let value = ImmutableRecord::from_registers(regs, regs.len());
run_until_done(
|| {
@ -8385,10 +8398,11 @@ mod tests {
for i in 0..iterations {
let mut cursor = BTreeCursor::new_table(None, pager.clone(), root_page);
tracing::info!("INSERT INTO t VALUES ({});", i,);
let value = ImmutableRecord::from_registers(&[Register::Value(Value::Text(Text {
let regs = &[Register::Value(Value::Text(Text {
value: huge_texts[i].as_bytes().to_vec(),
subtype: crate::types::TextSubtype::Text,
}))]);
}))];
let value = ImmutableRecord::from_registers(regs, regs.len());
tracing::trace!("before insert {}", i);
tracing::debug!(
"=========== btree before ===========\n{}\n\n",
@ -8433,8 +8447,8 @@ mod tests {
let offset = 2; // blobs data starts at offset 2
let initial_text = "hello world";
let initial_blob = initial_text.as_bytes().to_vec();
let value =
ImmutableRecord::from_registers(&[Register::Value(Value::Blob(initial_blob.clone()))]);
let regs = &[Register::Value(Value::Blob(initial_blob.clone()))];
let value = ImmutableRecord::from_registers(regs, regs.len());
run_until_done(
|| {
@ -8509,8 +8523,8 @@ mod tests {
let mut large_blob = vec![b'A'; 40960 - 11]; // insert large blob. 40960 = 10 page long.
let hello_world = b"hello world";
large_blob.extend_from_slice(hello_world);
let value =
ImmutableRecord::from_registers(&[Register::Value(Value::Blob(large_blob.clone()))]);
let regs = &[Register::Value(Value::Blob(large_blob.clone()))];
let value = ImmutableRecord::from_registers(regs, regs.len());
run_until_done(
|| {
@ -8700,10 +8714,8 @@ mod tests {
page_type: PageType,
) {
let mut payload = Vec::new();
let record = ImmutableRecord::from_registers(&[Register::Value(Value::Blob(vec![
0;
size as usize
]))]);
let regs = &[Register::Value(Value::Blob(vec![0; size as usize]))];
let record = ImmutableRecord::from_registers(regs, regs.len());
fill_cell_payload(
page_type,
Some(i as i64),

View file

@ -24,11 +24,12 @@ use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY;
use crate::function::Func;
use crate::schema::Schema;
use crate::translate::compound_select::emit_program_for_compound_select;
use crate::translate::plan::{DeletePlan, Plan, Search};
use crate::translate::plan::{DeletePlan, Plan, QueryDestination, Search};
use crate::translate::values::emit_values;
use crate::util::exprs_are_equivalent;
use crate::vdbe::builder::{CursorKey, CursorType, ProgramBuilder};
use crate::vdbe::insn::{CmpInsFlags, IdxInsertFlags, InsertFlags, RegisterOrLiteral};
use crate::vdbe::CursorID;
use crate::vdbe::{insn::Insn, BranchOffset};
use crate::{Result, SymbolTable};
@ -256,9 +257,9 @@ fn emit_program_for_select(
#[instrument(skip_all, level = Level::TRACE)]
pub fn emit_query<'a>(
program: &'a mut ProgramBuilder,
program: &mut ProgramBuilder,
plan: &'a mut SelectPlan,
t_ctx: &'a mut TranslateCtx<'a>,
t_ctx: &mut TranslateCtx<'a>,
) -> Result<usize> {
if !plan.values.is_empty() {
let reg_result_cols_start = emit_values(program, &plan, &t_ctx.resolver)?;
@ -348,13 +349,20 @@ pub fn emit_query<'a>(
&plan.table_references,
&plan.join_order,
&mut plan.where_clause,
None,
)?;
// Process result columns and expressions in the inner loop
emit_loop(program, t_ctx, plan)?;
// Clean up and close the main execution loop
close_loop(program, t_ctx, &plan.table_references, &plan.join_order)?;
close_loop(
program,
t_ctx,
&plan.table_references,
&plan.join_order,
None,
)?;
program.preassign_label_to_next_insn(after_main_loop_label);
@ -439,6 +447,7 @@ fn emit_program_for_delete(
&plan.table_references,
&[JoinOrderMember::default()],
&mut plan.where_clause,
None,
)?;
emit_delete_insns(program, &mut t_ctx, &plan.table_references)?;
@ -449,6 +458,7 @@ fn emit_program_for_delete(
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
None,
)?;
program.preassign_label_to_next_insn(after_main_loop_label);
@ -603,6 +613,24 @@ fn emit_program_for_update(
});
}
let ephemeral_plan = plan.ephemeral_plan.take();
let temp_cursor_id = ephemeral_plan.as_ref().map(|plan| {
let QueryDestination::EphemeralTable { cursor_id, .. } = &plan.query_destination else {
unreachable!()
};
*cursor_id
});
if let Some(ephemeral_plan) = ephemeral_plan {
program.emit_insn(Insn::OpenEphemeral {
cursor_id: temp_cursor_id.unwrap(),
is_table: true,
});
program.incr_nesting();
emit_program_for_select(program, ephemeral_plan, schema, syms)?;
program.decr_nesting();
}
// Initialize the main loop
init_loop(
program,
&mut t_ctx,
@ -612,10 +640,11 @@ fn emit_program_for_update(
OperationMode::UPDATE,
&plan.where_clause,
)?;
// Open indexes for update.
// Prepare index cursors
let mut index_cursors = Vec::with_capacity(plan.indexes_to_update.len());
for index in &plan.indexes_to_update {
if let Some(index_cursor) = program.resolve_cursor_id_safe(&CursorKey::index(
let index_cursor = if let Some(cursor) = program.resolve_cursor_id_safe(&CursorKey::index(
plan.table_references
.joined_tables()
.first()
@ -623,34 +652,40 @@ fn emit_program_for_update(
.internal_id,
index.clone(),
)) {
// Don't reopen index if it was already opened as the iteration cursor for this update plan.
let record_reg = program.alloc_register();
index_cursors.push((index_cursor, record_reg));
continue;
}
let index_cursor = program.alloc_cursor_id(CursorType::BTreeIndex(index.clone()));
program.emit_insn(Insn::OpenWrite {
cursor_id: index_cursor,
root_page: RegisterOrLiteral::Literal(index.root_page),
name: index.name.clone(),
});
cursor
} else {
let cursor = program.alloc_cursor_id(CursorType::BTreeIndex(index.clone()));
program.emit_insn(Insn::OpenWrite {
cursor_id: cursor,
root_page: RegisterOrLiteral::Literal(index.root_page),
name: index.name.clone(),
});
cursor
};
let record_reg = program.alloc_register();
index_cursors.push((index_cursor, record_reg));
}
// Open the main loop
open_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
&mut plan.where_clause,
temp_cursor_id,
)?;
emit_update_insns(&plan, &t_ctx, program, index_cursors)?;
// Emit update instructions
emit_update_insns(&plan, &t_ctx, program, index_cursors, temp_cursor_id)?;
// Close the main loop
close_loop(
program,
&mut t_ctx,
&plan.table_references,
&[JoinOrderMember::default()],
temp_cursor_id,
)?;
program.preassign_label_to_next_insn(after_main_loop_label);
@ -670,12 +705,13 @@ fn emit_update_insns(
t_ctx: &TranslateCtx,
program: &mut ProgramBuilder,
index_cursors: Vec<(usize, usize)>,
temp_cursor_id: Option<CursorID>,
) -> crate::Result<()> {
let table_ref = plan.table_references.joined_tables().first().unwrap();
let loop_labels = t_ctx.labels_main_loop.first().unwrap();
let (cursor_id, index, is_virtual) = match &table_ref.op {
let cursor_id = program.resolve_cursor_id(&CursorKey::table(table_ref.internal_id));
let (index, is_virtual) = match &table_ref.op {
Operation::Scan { index, .. } => (
program.resolve_cursor_id(&CursorKey::table(table_ref.internal_id)),
index.as_ref().map(|index| {
(
index.clone(),
@ -686,15 +722,10 @@ fn emit_update_insns(
table_ref.virtual_table().is_some(),
),
Operation::Search(search) => match search {
&Search::RowidEq { .. } | Search::Seek { index: None, .. } => (
program.resolve_cursor_id(&CursorKey::table(table_ref.internal_id)),
None,
false,
),
&Search::RowidEq { .. } | Search::Seek { index: None, .. } => (None, false),
Search::Seek {
index: Some(index), ..
} => (
program.resolve_cursor_id(&CursorKey::table(table_ref.internal_id)),
Some((
index.clone(),
program
@ -714,26 +745,26 @@ fn emit_update_insns(
},
);
program.emit_insn(Insn::RowId {
cursor_id,
cursor_id: temp_cursor_id.unwrap_or(cursor_id),
dest: beg,
});
// Check if rowid was provided (through INTEGER PRIMARY KEY as a rowid alias)
let rowid_alias_index = {
let rowid_alias_index = table_ref.columns().iter().position(|c| c.is_rowid_alias);
if let Some(index) = rowid_alias_index {
plan.set_clauses.iter().position(|(idx, _)| *idx == index)
} else {
None
}
};
let rowid_set_clause_reg = if rowid_alias_index.is_some() {
let rowid_alias_index = table_ref.columns().iter().position(|c| c.is_rowid_alias);
let has_user_provided_rowid = if let Some(index) = rowid_alias_index {
plan.set_clauses.iter().position(|(idx, _)| *idx == index)
} else {
None
}
.is_some();
let rowid_set_clause_reg = if has_user_provided_rowid {
Some(program.alloc_register())
} else {
None
};
let has_user_provided_rowid = rowid_alias_index.is_some();
let check_rowid_not_exists_label = if has_user_provided_rowid {
Some(program.allocate_label())

View file

@ -514,8 +514,8 @@ impl<'a> GroupByAggArgumentSource<'a> {
/// Emits bytecode for processing a single GROUP BY group.
pub fn group_by_process_single_group<'a>(
program: &mut ProgramBuilder,
group_by: &'a GroupBy,
plan: &'a SelectPlan,
group_by: &GroupBy,
plan: &SelectPlan,
t_ctx: &mut TranslateCtx<'a>,
) -> Result<()> {
let GroupByMetadata {
@ -718,7 +718,7 @@ pub fn group_by_process_single_group<'a>(
pub fn group_by_agg_phase<'a>(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx<'a>,
plan: &'a SelectPlan,
plan: &SelectPlan,
) -> Result<()> {
let GroupByMetadata {
labels, row_source, ..

View file

@ -261,12 +261,16 @@ pub fn init_loop(
}
}
OperationMode::DELETE | OperationMode::UPDATE => {
let table_cursor_id = table_cursor_id.expect("table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE");
let table_cursor_id = table_cursor_id.expect(
"table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE",
);
program.emit_insn(Insn::OpenWrite {
cursor_id: table_cursor_id,
root_page: table.table.get_root_page().into(),
name: table.table.get_name().to_string(),
});
// For DELETE, we need to open all the indexes for writing
// UPDATE opens these in emit_program_for_update() separately
if mode == OperationMode::DELETE {
@ -357,6 +361,7 @@ pub fn open_loop(
table_references: &TableReferences,
join_order: &[JoinOrderMember],
predicates: &[WhereTerm],
temp_cursor_id: Option<CursorID>,
) -> Result<()> {
for (join_index, join) in join_order.iter().enumerate() {
let joined_table_index = join.original_idx;
@ -389,8 +394,12 @@ pub fn open_loop(
Operation::Scan { iter_dir, .. } => {
match &table.table {
Table::BTree(_) => {
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect(
"Either ephemeral or index or table cursor must be opened",
)
})
});
if *iter_dir == IterationDirection::Backwards {
program.emit_insn(Insn::Last {
@ -637,8 +646,11 @@ pub fn open_loop(
};
let is_index = index_cursor_id.is_some();
let seek_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
let seek_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id
.expect("Either ephemeral or index or table cursor must be opened")
})
});
let Search::Seek { seek_def, .. } = search else {
unreachable!("Rowid equality point lookup should have been handled above");
@ -738,7 +750,7 @@ enum LoopEmitTarget {
pub fn emit_loop<'a>(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx<'a>,
plan: &'a SelectPlan,
plan: &SelectPlan,
) -> Result<()> {
// if we have a group by, we emit a record into the group by sorter,
// or if the rows are already sorted, we do the group by aggregation phase directly.
@ -764,7 +776,7 @@ pub fn emit_loop<'a>(
fn emit_loop_source<'a>(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx<'a>,
plan: &'a SelectPlan,
plan: &SelectPlan,
emit_target: LoopEmitTarget,
) -> Result<()> {
match emit_target {
@ -971,6 +983,7 @@ pub fn close_loop(
t_ctx: &mut TranslateCtx,
tables: &TableReferences,
join_order: &[JoinOrderMember],
temp_cursor_id: Option<CursorID>,
) -> Result<()> {
// We close the loops for all tables in reverse order, i.e. innermost first.
// OPEN t1
@ -995,8 +1008,12 @@ pub fn close_loop(
program.resolve_label(loop_labels.next, program.offset());
match &table.table {
Table::BTree(_) => {
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect(
"Either ephemeral or index or table cursor must be opened",
)
})
});
if *iter_dir == IterationDirection::Backwards {
program.emit_insn(Insn::Prev {
@ -1035,8 +1052,11 @@ pub fn close_loop(
"Subqueries do not support index seeks"
);
program.resolve_label(loop_labels.next, program.offset());
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
table_cursor_id.expect("Either index or table cursor must be opened")
let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| {
index_cursor_id.unwrap_or_else(|| {
table_cursor_id
.expect("Either ephemeral or index or table cursor must be opened")
})
});
// Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a Next instruction.
if !matches!(search, Search::RowidEq { .. }) {

View file

@ -107,7 +107,7 @@ fn optimize_delete_plan(plan: &mut DeletePlan, _schema: &Schema) -> Result<()> {
Ok(())
}
fn optimize_update_plan(plan: &mut UpdatePlan, _schema: &Schema) -> Result<()> {
fn optimize_update_plan(plan: &mut UpdatePlan, schema: &Schema) -> Result<()> {
rewrite_exprs_update(plan)?;
if let ConstantConditionEliminationResult::ImpossibleCondition =
eliminate_constant_conditions(&mut plan.where_clause)?
@ -115,18 +115,16 @@ fn optimize_update_plan(plan: &mut UpdatePlan, _schema: &Schema) -> Result<()> {
plan.contains_constant_false_condition = true;
return Ok(());
}
// FIXME: don't use indexes for update right now because it's not safe to traverse an index
// while also updating the same table, things go wrong.
// e.g. in 'explain update t set x=x+5 where x > 10;' where x is an indexed column,
// sqlite first creates an ephemeral index to store the current values so the tree traversal
// doesn't get messed up while updating.
// let _ = optimize_table_access(
// &mut plan.table_references,
// &schema.indexes,
// &mut plan.where_clause,
// &mut plan.order_by,
// &mut None,
// )?;
if let Some(ephemeral_plan) = &mut plan.ephemeral_plan {
optimize_select_plan(ephemeral_plan, schema)?;
}
let _ = optimize_table_access(
&mut plan.table_references,
&schema.indexes,
&mut plan.where_clause,
&mut plan.order_by,
&mut None,
)?;
Ok(())
}
@ -236,6 +234,7 @@ fn optimize_table_access(
.map_or(false, |join_info| join_info.outer),
})
.collect();
// Mutate the Operations in `joined_tables` to use the selected access methods.
for (i, join_order_member) in best_join_order.iter().enumerate() {
let table_idx = join_order_member.original_idx;

View file

@ -325,6 +325,14 @@ pub enum QueryDestination {
/// The index that will be used to store the results.
index: Arc<Index>,
},
/// The results of the query are stored in an ephemeral table,
/// later used by the parent query.
EphemeralTable {
/// The cursor ID of the ephemeral table that will be used to store the results.
cursor_id: CursorID,
/// The table that will be used to store the results.
table: Rc<BTreeTable>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -534,6 +542,8 @@ pub struct UpdatePlan {
// whether the WHERE clause is always false
pub contains_constant_false_condition: bool,
pub indexes_to_update: Vec<Arc<Index>>,
// If the table's rowid alias is used, gather all the target rowids into an ephemeral table, and then use that table as the single JoinedTable for the actual UPDATE loop.
pub ephemeral_plan: Option<SelectPlan>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View file

@ -1,7 +1,7 @@
use crate::{
vdbe::{
builder::ProgramBuilder,
insn::{IdxInsertFlags, Insn},
insn::{IdxInsertFlags, InsertFlags, Insn},
BranchOffset,
},
Result,
@ -100,6 +100,27 @@ pub fn emit_result_row_and_limit(
flags: IdxInsertFlags::new(),
});
}
QueryDestination::EphemeralTable {
cursor_id: table_cursor_id,
table,
} => {
let record_reg = program.alloc_register();
if plan.result_columns.len() > 1 {
program.emit_insn(Insn::MakeRecord {
start_reg: result_columns_start_reg,
count: plan.result_columns.len() - 1,
dest_reg: record_reg,
index_name: Some(table.name.clone()),
});
}
program.emit_insn(Insn::Insert {
cursor: *table_cursor_id,
key_reg: result_columns_start_reg + (plan.result_columns.len() - 1), // Rowid reg is the last register
record_reg,
flag: InsertFlags(0),
table_name: table.name.clone(),
});
}
QueryDestination::CoroutineYield { yield_reg, .. } => {
program.emit_insn(Insn::Yield {
yield_reg: *yield_reg,

View file

@ -715,7 +715,7 @@ fn estimate_num_labels(select: &SelectPlan) -> usize {
pub fn emit_simple_count<'a>(
program: &mut ProgramBuilder,
_t_ctx: &mut TranslateCtx<'a>,
plan: &'a SelectPlan,
plan: &SelectPlan,
) -> Result<()> {
let cursors = plan
.joined_tables()

View file

@ -1,5 +1,8 @@
use crate::translate::plan::Operation;
use crate::vdbe::builder::TableRefIdCounter;
use std::rc::Rc;
use crate::schema::{BTreeTable, Column, Type};
use crate::translate::plan::{Operation, QueryDestination, SelectPlan};
use crate::vdbe::builder::CursorType;
use crate::{
bail_parse_error,
schema::{Schema, Table},
@ -53,7 +56,7 @@ pub fn translate_update(
syms: &SymbolTable,
mut program: ProgramBuilder,
) -> crate::Result<ProgramBuilder> {
let mut plan = prepare_update_plan(schema, body, &mut program.table_reference_counter)?;
let mut plan = prepare_update_plan(&mut program, schema, body)?;
optimize_plan(&mut plan, schema)?;
// TODO: freestyling these numbers
let opts = ProgramBuilderOpts {
@ -75,7 +78,7 @@ pub fn translate_update_with_after(
mut program: ProgramBuilder,
after: impl FnOnce(&mut ProgramBuilder),
) -> crate::Result<ProgramBuilder> {
let mut plan = prepare_update_plan(schema, body, &mut program.table_reference_counter)?;
let mut plan = prepare_update_plan(&mut program, schema, body)?;
optimize_plan(&mut plan, schema)?;
// TODO: freestyling these numbers
let opts = ProgramBuilderOpts {
@ -90,9 +93,9 @@ pub fn translate_update_with_after(
}
pub fn prepare_update_plan(
program: &mut ProgramBuilder,
schema: &Schema,
body: &mut Update,
table_ref_counter: &mut TableRefIdCounter,
) -> crate::Result<Plan> {
if body.with.is_some() {
bail_parse_error!("WITH clause is not supported");
@ -127,6 +130,7 @@ pub fn prepare_update_plan(
})
})
.unwrap_or(IterationDirection::Forwards);
let joined_tables = vec![JoinedTable {
table: match table.as_ref() {
Table::Virtual(vtab) => Table::Virtual(vtab.clone()),
@ -134,7 +138,7 @@ pub fn prepare_update_plan(
_ => unreachable!(),
},
identifier: table_name.0.clone(),
internal_id: table_ref_counter.next(),
internal_id: program.table_reference_counter.next(),
op: Operation::Scan {
iter_dir,
index: None,
@ -198,13 +202,103 @@ pub fn prepare_update_plan(
.map(|o| (o.expr.clone(), o.order.unwrap_or(SortOrder::Asc)))
.collect()
});
// Parse the WHERE clause
parse_where(
body.where_clause.as_ref().map(|w| *w.clone()),
&mut table_references,
Some(&result_columns),
&mut where_clause,
)?;
// Sqlite determines we should create an ephemeral table if we do not have a FROM clause
// Difficult to say what items from the plan can be checked for this so currently just checking if a RowId Alias is referenced
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L395
// https://github.com/sqlite/sqlite/blob/master/src/update.c#L670
let columns = table.columns();
let rowid_alias_used = set_clauses.iter().fold(false, |accum, (idx, _)| {
accum || columns[*idx].is_rowid_alias
});
let (ephemeral_plan, where_clause) = if rowid_alias_used {
let internal_id = program.table_reference_counter.next();
let joined_tables = vec![JoinedTable {
table: match table.as_ref() {
Table::Virtual(vtab) => Table::Virtual(vtab.clone()),
Table::BTree(btree_table) => Table::BTree(btree_table.clone()),
_ => unreachable!(),
},
identifier: table_name.0.clone(),
internal_id,
op: Operation::Scan {
iter_dir,
index: None,
},
join_info: None,
col_used_mask: ColumnUsedMask::new(),
}];
let mut table_references = TableReferences::new(joined_tables, vec![]);
// Parse the WHERE clause
parse_where(
body.where_clause.as_ref().map(|w| *w.clone()),
&mut table_references,
Some(&result_columns),
&mut where_clause,
)?;
let table = Rc::new(BTreeTable {
root_page: 0, // Not relevant for ephemeral table definition
name: "ephemeral_scratch".to_string(),
has_rowid: true,
primary_key_columns: vec![],
columns: vec![Column {
name: Some("rowid".to_string()),
ty: Type::Integer,
ty_str: "INTEGER".to_string(),
primary_key: true,
is_rowid_alias: false,
notnull: true,
default: None,
unique: false,
collation: None,
}],
is_strict: false,
unique_sets: None,
});
let temp_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table.clone()));
let ephemeral_plan = SelectPlan {
table_references,
result_columns: vec![ResultSetColumn {
expr: Expr::RowId {
database: None,
table: internal_id,
},
alias: None,
contains_aggregates: false,
}],
where_clause, // original WHERE terms from the UPDATE clause
group_by: None, // N/A
order_by: None, // N/A
aggregates: vec![], // N/A
limit: None, // N/A
query_destination: QueryDestination::EphemeralTable {
cursor_id: temp_cursor_id,
table,
},
join_order: vec![],
offset: None,
contains_constant_false_condition: false,
distinctness: super::plan::Distinctness::NonDistinct,
values: vec![],
};
(Some(ephemeral_plan), vec![])
} else {
// Parse the WHERE clause
parse_where(
body.where_clause.as_ref().map(|w| *w.clone()),
&mut table_references,
Some(&result_columns),
&mut where_clause,
)?;
(None, where_clause)
};
// Parse the LIMIT/OFFSET clause
let (limit, offset) = body
@ -238,5 +332,6 @@ pub fn prepare_update_plan(
offset,
contains_constant_false_condition: false,
indexes_to_update,
ephemeral_plan,
}))
}

View file

@ -22,6 +22,7 @@ pub fn emit_values(
emit_values_in_subquery(program, plan, resolver, yield_reg)?
}
QueryDestination::EphemeralIndex { .. } => unreachable!(),
QueryDestination::EphemeralTable { .. } => unreachable!(),
};
Ok(reg_result_cols_start)
}
@ -58,6 +59,7 @@ fn emit_values_when_single_row(
});
}
QueryDestination::EphemeralIndex { .. } => unreachable!(),
QueryDestination::EphemeralTable { .. } => unreachable!(),
}
Ok(start_reg)
}

View file

@ -859,9 +859,12 @@ impl ImmutableRecord {
self.values.len()
}
pub fn from_registers(registers: &[Register]) -> Self {
let mut values = Vec::with_capacity(registers.len());
let mut serials = Vec::with_capacity(registers.len());
pub fn from_registers<'a>(
registers: impl IntoIterator<Item = &'a Register> + Copy,
len: usize,
) -> Self {
let mut values = Vec::with_capacity(len);
let mut serials = Vec::with_capacity(len);
let mut size_header = 0;
let mut size_values = 0;

View file

@ -4236,15 +4236,25 @@ pub fn op_insert(
{
let mut cursor = state.get_cursor(*cursor);
let cursor = cursor.as_btree_mut();
let record = match &state.registers[*record_reg] {
Register::Record(r) => r,
_ => unreachable!("Not a record! Cannot insert a non record value."),
};
let key = match &state.registers[*key_reg].get_owned_value() {
Value::Integer(i) => *i,
_ => unreachable!("expected integer key"),
};
return_if_io!(cursor.insert(&BTreeKey::new_table_rowid(key, Some(record)), true));
let record = match &state.registers[*record_reg] {
Register::Record(r) => std::borrow::Cow::Borrowed(r),
Register::Value(value) => {
let x = 1;
let regs = &state.registers[*record_reg..*record_reg + 1];
let new_regs = [&state.registers[*record_reg]];
let record = ImmutableRecord::from_registers(new_regs, new_regs.len());
std::borrow::Cow::Owned(record)
}
Register::Aggregate(..) => unreachable!("Cannot insert an aggregate value."),
};
return_if_io!(cursor.insert(&BTreeKey::new_table_rowid(key, Some(record.as_ref())), true));
// Only update last_insert_rowid for regular table inserts, not schema modifications
if cursor.root_page() != 1 {
if let Some(rowid) = return_if_io!(cursor.rowid()) {

View file

@ -522,7 +522,8 @@ fn get_new_rowid<R: Rng>(cursor: &mut BTreeCursor, mut rng: R) -> Result<CursorR
}
fn make_record(registers: &[Register], start_reg: &usize, count: &usize) -> ImmutableRecord {
ImmutableRecord::from_registers(&registers[*start_reg..*start_reg + *count])
let regs = &registers[*start_reg..*start_reg + *count];
ImmutableRecord::from_registers(regs, regs.len())
}
#[instrument(skip(program), level = Level::TRACE)]

View file

@ -214,3 +214,42 @@ do_execsql_test_on_specific_db {:memory:} update_where_or_regression_test {
SELECT * from t;
} {lovely_revolt
lovely_revolt}
do_execsql_test_in_memory_any_error update_primary_key_constraint_error {
CREATE TABLE eye (study REAL, spring BLOB, save TEXT, thank REAL, election INTEGER, PRIMARY KEY (election));
INSERT INTO eye VALUES (183559032.521585, x'6625d092', 'Trial six should.', 2606132742.43174, 2817);
INSERT INTO eye VALUES (78255586.9204539, x'651061e8', 'World perhaps.', -5815764.49018679, 1917);
UPDATE eye SET election = 6150;
}
do_execsql_test_in_memory_any_error update_primary_key_constraint_error_2 {
CREATE TABLE eye (study REAL, spring BLOB, save TEXT, thank REAL, election INTEGER, PRIMARY KEY (election));
INSERT INTO eye VALUES (183559032.521585, x'6625d092', 'Trial six should.', 2606132742.43174, 2817);
INSERT INTO eye VALUES (78255586.9204539, x'651061e8', 'World perhaps.', -5815764.49018679, 1917);
INSERT INTO eye VALUES (53.3274327094467, x'f574c507', 'Senior wish degree.', -423.432750526747, 2650);
INSERT INTO eye VALUES (-908148213048.983, x'6d812051', 'Possible able.', 101.171781837336, 4100);
INSERT INTO eye VALUES (-572332773760.924, x'd7a4d9fb', 'Money catch expect.', -271065488.756746, 4667);
UPDATE eye SET election = 6150 WHERE election != 1917;
}
do_execsql_test_in_memory_any_error update_primary_key_constraint_error_3 {
CREATE TABLE eye (study REAL, spring BLOB, save TEXT, thank REAL, election INTEGER, PRIMARY KEY (election));
INSERT INTO eye VALUES (183559032.521585, x'6625d092', 'Trial six should.', 2606132742.43174, 2817);
INSERT INTO eye VALUES (78255586.9204539, x'651061e8', 'World perhaps.', -5815764.49018679, 1917);
INSERT INTO eye VALUES (53.3274327094467, x'f574c507', 'Senior wish degree.', -423.432750526747, 2650);
INSERT INTO eye VALUES (-908148213048.983, x'6d812051', 'Possible able.', 101.171781837336, 4100);
INSERT INTO eye VALUES (-572332773760.924, x'd7a4d9fb', 'Money catch expect.', -271065488.756746, 4667);
UPDATE eye SET election = 6150 WHERE election > 1000 AND study > 1;
}
do_execsql_test_in_memory_any_error update_primary_key_constraint_error_4 {
CREATE TABLE t(a PRIMARY KEY INTEGER, b UNIQUE);
INSERT INTO t(b) VALUES (100), (200), (300);
UPDATE t SET a = 1;
}
do_execsql_test_in_memory_any_error update_primary_key_unique_constraint_error {
CREATE TABLE t(a PRIMARY KEY INTEGER, b UNIQUE);
INSERT INTO t(b) VALUES (100), (200), (300);
UPDATE t SET b = 2;
}