diff --git a/core/schema.rs b/core/schema.rs index dda37d15b..21bed120d 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -161,6 +161,7 @@ pub struct BTreeTable { pub primary_key_column_names: Vec, pub columns: Vec, pub has_rowid: bool, + pub is_strict: bool, } impl BTreeTable { @@ -262,12 +263,14 @@ fn create_table( let mut has_rowid = true; let mut primary_key_column_names = vec![]; let mut cols = vec![]; + let is_strict: bool; match body { CreateTableBody::ColumnsAndConstraints { columns, constraints, options, } => { + is_strict = options.contains(TableOptions::STRICT); if let Some(constraints) = constraints { for c in constraints { if let limbo_sqlite3_parser::ast::TableConstraint::PrimaryKey { @@ -390,6 +393,7 @@ fn create_table( has_rowid, primary_key_column_names, columns: cols, + is_strict, }) } @@ -456,7 +460,7 @@ pub fn affinity(datatype: &str) -> Affinity { } // Rule 3: BLOB or empty -> BLOB affinity (historically called NONE) - if datatype.contains("BLOB") || datatype.is_empty() { + if datatype.contains("BLOB") || datatype.is_empty() || datatype.contains("ANY") { return Affinity::Blob; } @@ -508,11 +512,11 @@ pub enum Affinity { Numeric, } -pub const SQLITE_AFF_TEXT: char = 'a'; -pub const SQLITE_AFF_NONE: char = 'b'; // Historically called NONE, but it's the same as BLOB -pub const SQLITE_AFF_NUMERIC: char = 'c'; -pub const SQLITE_AFF_INTEGER: char = 'd'; -pub const SQLITE_AFF_REAL: char = 'e'; +pub const SQLITE_AFF_NONE: char = 'A'; // Historically called NONE, but it's the same as BLOB +pub const SQLITE_AFF_TEXT: char = 'B'; +pub const SQLITE_AFF_NUMERIC: char = 'C'; +pub const SQLITE_AFF_INTEGER: char = 'D'; +pub const SQLITE_AFF_REAL: char = 'E'; impl Affinity { /// This is meant to be used in opcodes like Eq, which state: @@ -552,6 +556,7 @@ pub fn sqlite_schema_table() -> BTreeTable { root_page: 1, name: "sqlite_schema".to_string(), has_rowid: true, + is_strict: false, primary_key_column_names: vec![], columns: vec![ Column { @@ -1046,6 +1051,7 @@ mod tests { root_page: 0, name: "t1".to_string(), has_rowid: true, + is_strict: false, primary_key_column_names: vec!["nonexistent".to_string()], columns: vec![Column { name: Some("a".to_string()), diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 7713b9355..9ee253da5 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -251,6 +251,17 @@ pub fn translate_insert( program.resolve_label(make_record_label, program.offset()); } + match table.btree() { + Some(t) if t.is_strict => { + program.emit_insn(Insn::TypeCheck { + start_reg: column_registers_start, + count: num_cols, + check_generated: true, + table_reference: Rc::clone(&t), + }); + } + _ => (), + } // Create and insert the record program.emit_insn(Insn::MakeRecord { start_reg: column_registers_start, diff --git a/core/types.rs b/core/types.rs index 1556ee100..631bc7492 100644 --- a/core/types.rs +++ b/core/types.rs @@ -22,6 +22,20 @@ pub enum OwnedValueType { Error, } +impl Display for OwnedValueType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::Null => "NULL", + Self::Integer => "INT", + Self::Float => "REAL", + Self::Blob => "BLOB", + Self::Text => "TEXT", + Self::Error => "ERROR", + }; + write!(f, "{}", value) + } +} + #[derive(Debug, Clone, PartialEq)] pub enum TextSubtype { Text, @@ -69,6 +83,15 @@ impl Text { } } +impl From for Text { + fn from(value: String) -> Self { + Text { + value: value.into_bytes(), + subtype: TextSubtype::Text, + } + } +} + impl TextRef { pub fn as_str(&self) -> &str { unsafe { std::str::from_utf8_unchecked(self.value.to_slice()) } diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 09c283ecd..cf1b9e03e 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -1,5 +1,5 @@ #![allow(unused_variables)] -use crate::error::{LimboError, SQLITE_CONSTRAINT_PRIMARYKEY}; +use crate::error::{LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_PRIMARYKEY}; use crate::ext::ExtValue; use crate::function::{AggFunc, ExtFunc, MathFunc, MathFuncArity, ScalarFunc, VectorFunc}; use crate::functions::datetime::{ @@ -10,11 +10,13 @@ use std::{borrow::BorrowMut, rc::Rc}; use crate::pseudo::PseudoCursor; use crate::result::LimboResult; + use crate::schema::{affinity, Affinity}; use crate::storage::btree::{BTreeCursor, BTreeKey}; + use crate::storage::wal::CheckpointResult; use crate::types::{ - AggContext, Cursor, CursorResult, ExternalAggState, OwnedValue, SeekKey, SeekOp, + AggContext, Cursor, CursorResult, ExternalAggState, OwnedValue, OwnedValueType, SeekKey, SeekOp, }; use crate::util::{ cast_real_to_integer, cast_text_to_integer, cast_text_to_numeric, cast_text_to_real, @@ -1341,6 +1343,68 @@ pub fn op_column( Ok(InsnFunctionStepResult::Step) } +pub fn op_type_check( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Rc, + mv_store: Option<&Rc>, +) -> Result { + let Insn::TypeCheck { + start_reg, + count, + check_generated, + table_reference, + } = insn + else { + unreachable!("unexpected Insn {:?}", insn) + }; + assert_eq!(table_reference.is_strict, true); + state.registers[*start_reg..*start_reg + *count] + .iter_mut() + .zip(table_reference.columns.iter()) + .try_for_each(|(reg, col)| { + // INT PRIMARY KEY is not row_id_alias so we throw error if this col is NULL + if !col.is_rowid_alias + && col.primary_key + && matches!(reg.get_owned_value(), OwnedValue::Null) + { + bail_constraint_error!( + "NOT NULL constraint failed: {}.{} ({})", + &table_reference.name, + col.name.as_ref().map(|s| s.as_str()).unwrap_or(""), + SQLITE_CONSTRAINT + ) + } else if col.is_rowid_alias { + // If it is INTEGER PRIMARY KEY we let sqlite assign row_id + return Ok(()); + }; + let col_affinity = col.affinity(); + let ty_str = col.ty_str.as_str(); + let applied = apply_affinity_char(reg, col_affinity); + let value_type = reg.get_owned_value().value_type(); + match (ty_str, value_type) { + ("INTEGER" | "INT", OwnedValueType::Integer) => {} + ("REAL", OwnedValueType::Float) => {} + ("BLOB", OwnedValueType::Blob) => {} + ("TEXT", OwnedValueType::Text) => {} + ("ANY", _) => {} + (t, v) => bail_constraint_error!( + "cannot store {} value in {} column {}.{} ({})", + v, + t, + &table_reference.name, + col.name.as_ref().map(|s| s.as_str()).unwrap_or(""), + SQLITE_CONSTRAINT + ), + }; + Ok(()) + })?; + + state.pc += 1; + Ok(InsnFunctionStepResult::Step) +} + pub fn op_make_record( program: &Program, state: &mut ProgramState, @@ -5012,6 +5076,77 @@ fn exec_if(reg: &OwnedValue, jump_if_null: bool, not: bool) -> bool { } } +fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { + if let Register::OwnedValue(value) = target { + if matches!(value, OwnedValue::Blob(_)) { + return true; + } + match affinity { + Affinity::Blob => return true, + Affinity::Text => { + if matches!(value, OwnedValue::Text(_) | OwnedValue::Null) { + return true; + } + let text = value.to_string(); + *value = OwnedValue::Text(text.into()); + return true; + } + Affinity::Integer | Affinity::Numeric => { + if matches!(value, OwnedValue::Integer(_)) { + return true; + } + if !matches!(value, OwnedValue::Text(_) | OwnedValue::Float(_)) { + return true; + } + + if let OwnedValue::Float(fl) = *value { + if let Ok(int) = cast_real_to_integer(fl).map(OwnedValue::Integer) { + *value = int; + return true; + } + return false; + } + + let text = value.to_text().unwrap(); + let Ok(num) = checked_cast_text_to_numeric(&text) else { + return false; + }; + + *value = match &num { + OwnedValue::Float(fl) => { + cast_real_to_integer(*fl) + .map(OwnedValue::Integer) + .unwrap_or(num); + return true; + } + OwnedValue::Integer(_) if text.starts_with("0x") => { + return false; + } + _ => num, + }; + } + + Affinity::Real => { + if let OwnedValue::Integer(i) = value { + *value = OwnedValue::Float(*i as f64); + return true; + } else if let OwnedValue::Text(t) = value { + if t.as_str().starts_with("0x") { + return false; + } + if let Ok(num) = checked_cast_text_to_numeric(t.as_str()) { + *value = num; + return true; + } else { + return false; + } + } + } + }; + } + return true; +} + fn exec_cast(value: &OwnedValue, datatype: &str) -> OwnedValue { if matches!(value, OwnedValue::Null) { return OwnedValue::Null; diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 66c68d9c0..7b9b02d2a 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -528,6 +528,20 @@ pub fn insn_to_str( ), ) } + Insn::TypeCheck { + start_reg, + count, + check_generated, + .. + } => ( + "TypeCheck", + *start_reg as i32, + *count as i32, + *check_generated as i32, + OwnedValue::build_text(""), + 0, + String::from(""), + ), Insn::MakeRecord { start_reg, count, diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 8d3a9afca..d7fc39609 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -1,8 +1,10 @@ use std::num::NonZero; +use std::rc::Rc; use super::{ cast_text_to_numeric, execute, AggFunc, BranchOffset, CursorID, FuncCtx, InsnFunction, PageIdx, }; +use crate::schema::BTreeTable; use crate::storage::wal::CheckpointMode; use crate::types::{OwnedValue, Record}; use limbo_macros::Description; @@ -344,7 +346,16 @@ pub enum Insn { dest: usize, }, - /// Make a record and write it to destination register. + TypeCheck { + start_reg: usize, // P1 + count: usize, // P2 + /// GENERATED ALWAYS AS ... STATIC columns are only checked if P3 is zero. + /// When P3 is non-zero, no type checking occurs for static generated columns. + check_generated: bool, // P3 + table_reference: Rc, // P4 + }, + + // Make a record and write it to destination register. MakeRecord { start_reg: usize, // P1 count: usize, // P2 @@ -427,7 +438,7 @@ pub enum Insn { register: usize, }, - /// Write a string value into a register. + // Write a string value into a register. String8 { value: String, dest: usize, @@ -1271,6 +1282,7 @@ impl Insn { Insn::LastAwait { .. } => execute::op_last_await, Insn::Column { .. } => execute::op_column, + Insn::TypeCheck { .. } => execute::op_type_check, Insn::MakeRecord { .. } => execute::op_make_record, Insn::ResultRow { .. } => execute::op_result_row,