implemented strict table

This commit is contained in:
Ihor Andrianov 2025-04-02 16:13:15 +03:00
parent 3a1b87eb21
commit 4a08b98bab
No known key found for this signature in database
6 changed files with 211 additions and 10 deletions

View file

@ -161,6 +161,7 @@ pub struct BTreeTable {
pub primary_key_column_names: Vec<String>,
pub columns: Vec<Column>,
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()),

View file

@ -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,

View file

@ -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<String> 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()) }

View file

@ -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<Pager>,
mv_store: Option<&Rc<MvStore>>,
) -> Result<InsnFunctionStepResult> {
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;

View file

@ -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,

View file

@ -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<BTreeTable>, // 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,