From fb86476525bb9ec39f526f5c86fc5f23a3a45313 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 5 Jun 2025 19:02:31 +0300 Subject: [PATCH] Implement basic not null constraint checks --- core/error.rs | 1 + core/translate/insert.rs | 21 ++++++++++- core/vdbe/execute.rs | 75 +++++++++++++++++++++++++++++++++++++++- core/vdbe/explain.rs | 13 +++++++ core/vdbe/insn.rs | 8 +++++ 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/core/error.rs b/core/error.rs index a56dfbb10..283d82dc7 100644 --- a/core/error.rs +++ b/core/error.rs @@ -88,3 +88,4 @@ impl From for LimboError { pub const SQLITE_CONSTRAINT: usize = 19; pub const SQLITE_CONSTRAINT_PRIMARYKEY: usize = SQLITE_CONSTRAINT | (6 << 8); +pub const SQLITE_CONSTRAINT_NOTNULL: usize = SQLITE_CONSTRAINT | (5 << 8); diff --git a/core/translate/insert.rs b/core/translate/insert.rs index d664aca6c..17b3b7ef1 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -4,7 +4,7 @@ use limbo_sqlite3_parser::ast::{ DistinctNames, Expr, InsertBody, OneSelect, QualifiedName, ResolveType, ResultColumn, With, }; -use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY; +use crate::error::{SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY}; use crate::schema::{IndexColumn, Table}; use crate::util::normalize_ident; use crate::vdbe::builder::{ProgramBuilderOpts, QueryMode}; @@ -508,6 +508,25 @@ pub fn translate_insert( }); } + for (i, col) in column_mappings + .iter() + .enumerate() + .filter(|(_, col)| col.column.notnull) + { + let target_reg = i + column_registers_start; + program.emit_insn(Insn::HaltIfNull { + target_reg, + err_code: SQLITE_CONSTRAINT_NOTNULL, + description: format!( + "{}.{}", + table_name, + col.column + .name + .as_ref() + .expect("Column name must be present") + ), + }); + } // Create and insert the record program.emit_insn(Insn::MakeRecord { start_reg: column_registers_start, diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 0db530c5e..b64d000f5 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -7,7 +7,9 @@ use crate::storage::pager::CreateBTreeFlags; use crate::storage::wal::DummyWAL; use crate::types::ImmutableRecord; use crate::{ - error::{LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_PRIMARYKEY}, + error::{ + LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, + }, ext::ExtValue, function::{AggFunc, ExtFunc, MathFunc, MathFuncArity, ScalarFunc, VectorFunc}, functions::{ @@ -1595,6 +1597,48 @@ pub fn op_prev( Ok(InsnFunctionStepResult::Step) } +pub fn halt( + program: &Program, + state: &mut ProgramState, + pager: &Rc, + mv_store: Option<&Rc>, + err_code: usize, + description: &str, +) -> Result { + if err_code > 0 { + // invalidate page cache in case of error + pager.clear_page_cache(); + } + match err_code { + 0 => {} + SQLITE_CONSTRAINT_PRIMARYKEY => { + return Err(LimboError::Constraint(format!( + "UNIQUE constraint failed: {} (19)", + description + ))); + } + SQLITE_CONSTRAINT_NOTNULL => { + return Err(LimboError::Constraint(format!( + "NOTNULL constraint failed: {} (19)", + description + ))); + } + _ => { + return Err(LimboError::Constraint(format!( + "undocumented halt error code {}", + description + ))); + } + } + match program.commit_txn(pager.clone(), state, mv_store)? { + StepResult::Done => Ok(InsnFunctionStepResult::Done), + StepResult::IO => Ok(InsnFunctionStepResult::IO), + StepResult::Row => Ok(InsnFunctionStepResult::Row), + StepResult::Interrupt => Ok(InsnFunctionStepResult::Interrupt), + StepResult::Busy => Ok(InsnFunctionStepResult::Busy), + } +} + pub fn op_halt( program: &Program, state: &mut ProgramState, @@ -1621,6 +1665,12 @@ pub fn op_halt( description ))); } + SQLITE_CONSTRAINT_NOTNULL => { + return Err(LimboError::Constraint(format!( + "NOTNULL constraint failed: {} (19)", + description + ))); + } _ => { return Err(LimboError::Constraint(format!( "undocumented halt error code {}", @@ -1637,6 +1687,29 @@ pub fn op_halt( } } +pub fn op_halt_if_null( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Rc, + mv_store: Option<&Rc>, +) -> Result { + let Insn::HaltIfNull { + target_reg, + err_code, + description, + } = insn + else { + unreachable!("unexpected Insn {:?}", insn) + }; + if state.registers[*target_reg].get_owned_value() == &Value::Null { + halt(program, state, pager, mv_store, *err_code, &description) + } else { + state.pc += 1; + Ok(InsnFunctionStepResult::Step) + } +} + pub fn op_transaction( program: &Program, state: &mut ProgramState, diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 69bb886c1..359275862 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -634,6 +634,19 @@ pub fn insn_to_str( 0, "".to_string(), ), + Insn::HaltIfNull { + err_code, + target_reg, + description, + } => ( + "HalfIfNull", + *err_code as i32, + 0, + *target_reg as i32, + Value::build_text(&description), + 0, + "".to_string(), + ), Insn::Transaction { write } => ( "Transaction", 0, diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 976f6aba4..01ed1e394 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -406,6 +406,13 @@ pub enum Insn { description: String, }, + /// Halt the program if P3 is null. + HaltIfNull { + target_reg: usize, // P3 + description: String, // p4 + err_code: usize, // p1 + }, + /// Start a transaction. Transaction { write: bool, @@ -957,6 +964,7 @@ impl Insn { Insn::Next { .. } => execute::op_next, Insn::Prev { .. } => execute::op_prev, Insn::Halt { .. } => execute::op_halt, + Insn::HaltIfNull { .. } => execute::op_halt_if_null, Insn::Transaction { .. } => execute::op_transaction, Insn::AutoCommit { .. } => execute::op_auto_commit, Insn::Goto { .. } => execute::op_goto,