diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 4ca7e6fca..5e8365383 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -283,19 +283,7 @@ pub fn translate_insert( } _ => (), } - // Create and insert the record - program.emit_insn(Insn::MakeRecord { - start_reg: column_registers_start, - count: num_cols, - dest_reg: record_register, - }); - program.emit_insn(Insn::Insert { - cursor: cursor_id, - key_reg: rowid_reg, - record_reg: record_register, - flag: 0, - }); for index_col_mapping in index_col_mappings.iter() { // find which cursor we opened earlier for this index let idx_cursor_id = idx_cursors @@ -332,6 +320,43 @@ pub fn translate_insert( dest_reg: record_reg, }); + let make_record_label = program.allocate_label(); + program.emit_insn(Insn::NoConflict { + cursor_id: idx_cursor_id, + target_pc: make_record_label, + record_reg: idx_start_reg, + num_regs: num_cols, + }); + let mut column_names = Vec::new(); + for (index, ..) in index_col_mapping.columns.iter() { + let name = btree_table + .columns + .get(*index) + .unwrap() + .name + .as_ref() + .expect("column name is None"); + column_names.push(format!("{}.{name}", btree_table.name)); + } + let column_names = + column_names + .into_iter() + .enumerate() + .fold(String::new(), |mut accum, (idx, name)| { + if idx % 2 == 1 { + accum.push(','); + } + accum.push_str(&name); + accum + }); + + program.emit_insn(Insn::Halt { + err_code: SQLITE_CONSTRAINT_PRIMARYKEY, + description: format!("{}.{}", table_name.0, column_names), + }); + + program.resolve_label(make_record_label, program.offset()); + // now do the actual index insertion using the unpacked registers program.emit_insn(Insn::IdxInsert { cursor_id: idx_cursor_id, @@ -342,6 +367,21 @@ pub fn translate_insert( flags: IdxInsertFlags::new(), }); } + + // Create and insert the record + program.emit_insn(Insn::MakeRecord { + start_reg: column_registers_start, + count: num_cols, + dest_reg: record_register, + }); + + program.emit_insn(Insn::Insert { + cursor: cursor_id, + key_reg: rowid_reg, + record_reg: record_register, + flag: 0, + }); + if inserting_multiple_rows { // For multiple rows, loop back program.emit_insn(Insn::Goto { @@ -472,7 +512,7 @@ fn resolve_columns_for_insert<'a>( /// Represents how a column in an index should be populated during an INSERT. /// Similar to ColumnMapping above but includes the index name, as well as multiple /// possible value indices for each. -#[derive(Default)] +#[derive(Debug, Default)] struct IndexColMapping { idx_name: String, columns: Vec<(usize, IndexColumn)>, diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 05fdc4938..66b2143bb 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -440,6 +440,9 @@ impl ProgramBuilder { Insn::VFilter { pc_if_empty, .. } => { resolve(pc_if_empty, "VFilter"); } + Insn::NoConflict { target_pc, .. } => { + resolve(target_pc, "NoConflict"); + } _ => {} } } diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 1185a77b0..31a81e9b9 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -3,6 +3,7 @@ use crate::numeric::{NullableInteger, Numeric}; use crate::storage::database::FileMemoryStorage; use crate::storage::page_cache::DumbLruPageCache; use crate::storage::pager::CreateBTreeFlags; +use crate::types::ImmutableRecord; use crate::{ error::{LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_PRIMARYKEY}, ext::ExtValue, @@ -3895,6 +3896,67 @@ pub fn op_soft_null( Ok(InsnFunctionStepResult::Step) } +pub fn op_no_conflict( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Rc, + mv_store: Option<&Rc>, +) -> Result { + let Insn::NoConflict { + cursor_id, + target_pc, + record_reg, + num_regs, + } = insn + else { + unreachable!("unexpected Insn {:?}", insn) + }; + let found = { + let mut cursor = state.get_cursor(*cursor_id); + let cursor = cursor.as_btree_mut(); + + let any_fn = |record: &ImmutableRecord| { + for val in record.values.iter() { + if matches!(val, RefValue::Null) { + return false; + } + } + true + }; + + let record = if *num_regs == 0 { + let record = match &state.registers[*record_reg] { + Register::Record(r) => r, + _ => { + return Err(LimboError::InternalError( + "NoConflict: exepected a record in the register".into(), + )); + } + }; + record + } else { + &make_record(&state.registers, record_reg, num_regs) + }; + + // Should early return and jump if any of the values in the record is NULL + let found = any_fn(record); + if found { + return_if_io!(cursor.seek(SeekKey::IndexKey(record), SeekOp::EQ)) + } else { + found + } + }; + + if found { + state.pc += 1; + } else { + state.pc = target_pc.to_offset_int(); + } + + Ok(InsnFunctionStepResult::Step) +} + pub fn op_not_exists( program: &Program, state: &mut ProgramState, diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 96afc5d17..eadb5a0d9 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -569,13 +569,13 @@ pub fn insn_to_str( ), Insn::Halt { err_code, - description: _, + description, } => ( "Halt", *err_code as i32, 0, 0, - OwnedValue::build_text(""), + OwnedValue::build_text(&description), 0, "".to_string(), ), @@ -1068,6 +1068,20 @@ pub fn insn_to_str( 0, "".to_string(), ), + Insn::NoConflict { + cursor_id, + target_pc, + record_reg, + num_regs, + } => ( + "NoConflict", + *cursor_id as i32, + target_pc.to_debug_int(), + *record_reg as i32, + OwnedValue::build_text(&format!("{num_regs}")), + 0, + format!("key=r[{}]", record_reg), + ), Insn::NotExists { cursor, rowid_reg, diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 56f44bd2b..633647c36 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -664,6 +664,13 @@ pub enum Insn { reg: usize, }, + NoConflict { + cursor_id: CursorID, // P1 index cursor + target_pc: BranchOffset, // P2 jump target + record_reg: usize, + num_regs: usize, + }, + NotExists { cursor: CursorID, rowid_reg: usize, @@ -922,6 +929,7 @@ impl Insn { Insn::NewRowid { .. } => execute::op_new_rowid, Insn::MustBeInt { .. } => execute::op_must_be_int, Insn::SoftNull { .. } => execute::op_soft_null, + Insn::NoConflict { .. } => execute::op_no_conflict, Insn::NotExists { .. } => execute::op_not_exists, Insn::OffsetLimit { .. } => execute::op_offset_limit, Insn::OpenWrite { .. } => execute::op_open_write,