diff --git a/core/function.rs b/core/function.rs index 5a436465e..d3207ad0b 100644 --- a/core/function.rs +++ b/core/function.rs @@ -554,6 +554,21 @@ impl Display for MathFunc { } } +#[derive(Debug)] +pub enum AlterTableFunc { + RenameTable, + RenameColumn, +} + +impl Display for AlterTableFunc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AlterTableFunc::RenameTable => write!(f, "limbo_rename_table"), + AlterTableFunc::RenameColumn => write!(f, "limbo_rename_column"), + } + } +} + #[derive(Debug)] pub enum Func { Agg(AggFunc), @@ -562,6 +577,7 @@ pub enum Func { Vector(VectorFunc), #[cfg(feature = "json")] Json(JsonFunc), + AlterTable(AlterTableFunc), External(Rc), } @@ -575,6 +591,7 @@ impl Display for Func { #[cfg(feature = "json")] Self::Json(json_func) => write!(f, "{}", json_func), Self::External(generic_func) => write!(f, "{}", generic_func), + Self::AlterTable(alter_func) => write!(f, "{}", alter_func), } } } @@ -595,6 +612,7 @@ impl Func { #[cfg(feature = "json")] Self::Json(json_func) => json_func.is_deterministic(), Self::External(external_func) => external_func.is_deterministic(), + Self::AlterTable(_) => true, } } pub fn resolve_function(name: &str, arg_count: usize) -> Result { diff --git a/core/schema.rs b/core/schema.rs index eceabbcfc..3535c9237 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -4,7 +4,7 @@ use crate::{util::normalize_ident, Result}; use crate::{LimboError, VirtualTable}; use core::fmt; use fallible_iterator::FallibleIterator; -use limbo_sqlite3_parser::ast::{Expr, Literal, SortOrder, TableOptions}; +use limbo_sqlite3_parser::ast::{self, ColumnDefinition, Expr, Literal, SortOrder, TableOptions}; use limbo_sqlite3_parser::{ ast::{Cmd, CreateTableBody, QualifiedName, ResultColumn, Stmt}, lexer::sql::Parser, @@ -220,12 +220,11 @@ impl BTreeTable { /// then get_column("b") returns (1, &Column { .. }) pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> { let name = normalize_ident(name); - for (i, column) in self.columns.iter().enumerate() { - if column.name.as_ref().map_or(false, |n| *n == name) { - return Some((i, column)); - } - } - None + + self.columns + .iter() + .enumerate() + .find(|(_, column)| column.name.as_ref() == Some(&name)) } pub fn from_sql(sql: &str, root_page: usize) -> Result { @@ -240,17 +239,30 @@ impl BTreeTable { } pub fn to_sql(&self) -> String { - let mut sql = format!("CREATE TABLE {} (\n", self.name); + let mut sql = format!("CREATE TABLE {} (", self.name); for (i, column) in self.columns.iter().enumerate() { if i > 0 { - sql.push_str(",\n"); + sql.push(','); } - sql.push_str(" "); + sql.push(' '); sql.push_str(column.name.as_ref().expect("column name is None")); sql.push(' '); sql.push_str(&column.ty.to_string()); + + if column.unique { + sql.push_str(" UNIQUE"); + } + + if column.primary_key { + sql.push_str(" PRIMARY KEY"); + } + + if let Some(default) = &column.default { + sql.push_str(" DEFAULT "); + sql.push_str(&default.to_string()); + } } - sql.push_str(");\n"); + sql.push_str(" )"); sql } @@ -586,6 +598,80 @@ impl Column { } } +// TODO: This might replace some of util::columns_from_create_table_body +impl From for Column { + fn from(value: ColumnDefinition) -> Self { + let ast::Name(name) = value.col_name; + + let mut default = None; + let mut notnull = false; + let mut primary_key = false; + let mut unique = false; + let mut collation = None; + + for ast::NamedColumnConstraint { constraint, .. } in value.constraints { + match constraint { + ast::ColumnConstraint::PrimaryKey { .. } => primary_key = true, + ast::ColumnConstraint::NotNull { .. } => notnull = true, + ast::ColumnConstraint::Unique(..) => unique = true, + ast::ColumnConstraint::Default(expr) => { + default.replace(expr); + } + ast::ColumnConstraint::Collate { collation_name } => { + collation.replace( + CollationSeq::new(&collation_name.0) + .expect("collation should have been set correctly in create table"), + ); + } + _ => {} + }; + } + + let ty = match value.col_type { + Some(ref data_type) => { + // https://www.sqlite.org/datatype3.html + let type_name = data_type.name.clone().to_uppercase(); + + if type_name.contains("INT") { + Type::Integer + } else if type_name.contains("CHAR") + || type_name.contains("CLOB") + || type_name.contains("TEXT") + { + Type::Text + } else if type_name.contains("BLOB") || type_name.is_empty() { + Type::Blob + } else if type_name.contains("REAL") + || type_name.contains("FLOA") + || type_name.contains("DOUB") + { + Type::Real + } else { + Type::Numeric + } + } + None => Type::Null, + }; + + let ty_str = value + .col_type + .map(|t| t.name.to_string()) + .unwrap_or_default(); + + Column { + name: Some(name), + ty, + default, + notnull, + ty_str, + primary_key, + is_rowid_alias: primary_key && matches!(ty, Type::Integer), + unique, + collation, + } + } +} + /// 3.1. Determination Of Column Affinity /// For tables not declared as STRICT, the affinity of a column is determined by the declared type of the column, according to the following rules in the order shown: /// @@ -877,6 +963,7 @@ pub struct IndexColumn { /// b.pos_in_table == 1 pub pos_in_table: usize, pub collation: Option, + pub default: Option, } impl Index { @@ -901,12 +988,13 @@ impl Index { name, index_name, table.name ))); }; - let collation = table.get_column(&name).unwrap().1.collation; + let (_, column) = table.get_column(&name).unwrap(); index_columns.push(IndexColumn { name, order: col.order.unwrap_or(SortOrder::Asc), pos_in_table, - collation, + collation: column.collation, + default: column.default.clone(), }); } Ok(Index { @@ -963,11 +1051,14 @@ impl Index { ); }; + let (_, column) = table.get_column(col_name).unwrap(); + IndexColumn { name: normalize_ident(col_name), - order: order.clone(), + order: *order, pos_in_table, - collation: table.get_column(col_name).unwrap().1.collation, + collation: column.collation, + default: column.default.clone(), } }) .collect::>(); @@ -999,6 +1090,7 @@ impl Index { return None; } let (index_name, root_page) = auto_indices.next().expect("number of auto_indices in schema should be same number of indices calculated"); + let (_, column) = table.get_column(col_name).unwrap(); Some(Index { name: normalize_ident(index_name.as_str()), table_name: table.name.clone(), @@ -1007,7 +1099,8 @@ impl Index { name: normalize_ident(col_name), order: SortOrder::Asc, // Default Sort Order pos_in_table, - collation: table.get_column(col_name).unwrap().1.collation, + collation: column.collation, + default: column.default.clone(), }], unique: true, ephemeral: false, @@ -1069,11 +1162,13 @@ impl Index { col_name, index_name, table.name ); }; + let (_, column) = table.get_column(col_name).unwrap(); IndexColumn { name: normalize_ident(col_name), order: *order, pos_in_table, - collation: table.get_column(col_name).unwrap().1.collation, + collation: column.collation, + default: column.default.clone(), } }); Index { @@ -1404,13 +1499,7 @@ mod tests { #[test] pub fn test_sqlite_schema() { - let expected = r#"CREATE TABLE sqlite_schema ( - type TEXT, - name TEXT, - tbl_name TEXT, - rootpage INTEGER, - sql TEXT); -"#; + let expected = r#"CREATE TABLE sqlite_schema ( type TEXT, name TEXT, tbl_name TEXT, rootpage INTEGER, sql TEXT )"#; let actual = sqlite_schema_table().to_sql(); assert_eq!(expected, actual); } diff --git a/core/translate/alter.rs b/core/translate/alter.rs new file mode 100644 index 000000000..5940b3d95 --- /dev/null +++ b/core/translate/alter.rs @@ -0,0 +1,380 @@ +use fallible_iterator::FallibleIterator as _; +use limbo_sqlite3_parser::{ast, lexer::sql::Parser}; + +use crate::{ + function::{AlterTableFunc, Func}, + schema::{Column, Schema}, + util::normalize_ident, + vdbe::{ + builder::{ProgramBuilder, QueryMode}, + insn::{Insn, RegisterOrLiteral}, + }, + LimboError, Result, SymbolTable, +}; + +use super::{ + emitter::TransactionMode, schema::SQLITE_TABLEID, update::translate_update_with_after, +}; + +pub fn translate_alter_table( + alter: (ast::QualifiedName, ast::AlterTableBody), + syms: &SymbolTable, + schema: &Schema, + mut program: ProgramBuilder, +) -> Result { + let (table_name, alter_table) = alter; + let ast::Name(table_name) = table_name.name; + + let Some(original_btree) = schema + .get_table(&table_name) + .and_then(|table| table.btree()) + else { + return Err(LimboError::ParseError(format!( + "no such table: {table_name}" + ))); + }; + + let mut btree = (*original_btree).clone(); + + Ok(match alter_table { + ast::AlterTableBody::DropColumn(column_name) => { + let ast::Name(column_name) = column_name; + + // Tables always have at least one column. + assert_ne!(btree.columns.len(), 0); + + if btree.columns.len() == 1 { + return Err(LimboError::ParseError(format!( + "cannot drop column \"{column_name}\": no other columns exist" + ))); + } + + let (dropped_index, column) = btree.get_column(&column_name).ok_or_else(|| { + LimboError::ParseError(format!("no such column: \"{column_name}\"")) + })?; + + if column.primary_key { + return Err(LimboError::ParseError(format!( + "cannot drop column \"{column_name}\": PRIMARY KEY" + ))); + } + + if column.unique + || btree.unique_sets.as_ref().is_some_and(|set| { + set.iter().any(|set| { + set.iter() + .any(|(name, _)| name == &normalize_ident(&column_name)) + }) + }) + { + return Err(LimboError::ParseError(format!( + "cannot drop column \"{column_name}\": UNIQUE" + ))); + } + + btree.columns.remove(dropped_index); + + let sql = btree.to_sql(); + + let stmt = format!( + r#" + UPDATE {SQLITE_TABLEID} + SET sql = '{sql}' + WHERE name = '{table_name}' COLLATE NOCASE AND type = 'table' + "#, + ); + + let mut parser = Parser::new(stmt.as_bytes()); + let Some(ast::Cmd::Stmt(ast::Stmt::Update(mut update))) = parser.next().unwrap() else { + unreachable!(); + }; + + translate_update_with_after( + QueryMode::Normal, + schema, + &mut update, + syms, + program, + |program| { + let column_count = btree.columns.len(); + let root_page = btree.root_page; + let table_name = btree.name.clone(); + + let cursor_id = program.alloc_cursor_id( + crate::vdbe::builder::CursorType::BTreeTable(original_btree), + ); + + program.emit_insn(Insn::OpenWrite { + cursor_id, + root_page: RegisterOrLiteral::Literal(root_page), + name: table_name.clone(), + }); + + program.cursor_loop(cursor_id, |program, rowid| { + let first_column = program.alloc_registers(column_count); + + let mut iter = first_column; + + for i in 0..(column_count + 1) { + if i == dropped_index { + continue; + } + + program.emit_column(cursor_id, i, iter); + + iter += 1; + } + + let record = program.alloc_register(); + + program.emit_insn(Insn::MakeRecord { + start_reg: first_column, + count: column_count, + dest_reg: record, + index_name: None, + }); + + program.emit_insn(Insn::Insert { + cursor: cursor_id, + key_reg: rowid, + record_reg: record, + flag: 0, + table_name: table_name.clone(), + }); + }); + + program.emit_insn(Insn::ParseSchema { + db: usize::MAX, // TODO: This value is unused, change when we do something with it + where_clause: None, + }) + }, + )? + } + ast::AlterTableBody::AddColumn(col_def) => { + let column = Column::from(col_def); + + if let Some(default) = &column.default { + if !matches!( + default, + ast::Expr::Literal( + ast::Literal::Null + | ast::Literal::Blob(_) + | ast::Literal::Numeric(_) + | ast::Literal::String(_) + ) + ) { + // TODO: This is slightly inaccurate since sqlite returns a `Runtime + // error`. + return Err(LimboError::ParseError( + "Cannot add a column with non-constant default".to_string(), + )); + } + } + + btree.columns.push(column); + + let sql = btree.to_sql(); + let mut escaped = String::with_capacity(sql.len()); + + for ch in sql.chars() { + match ch { + '\'' => escaped.push_str("''"), + ch => escaped.push(ch), + } + } + + let stmt = format!( + r#" + UPDATE {SQLITE_TABLEID} + SET sql = '{escaped}' + WHERE name = '{table_name}' COLLATE NOCASE AND type = 'table' + "#, + ); + + let mut parser = Parser::new(stmt.as_bytes()); + let Some(ast::Cmd::Stmt(ast::Stmt::Update(mut update))) = parser.next().unwrap() else { + unreachable!(); + }; + + translate_update_with_after( + QueryMode::Normal, + schema, + &mut update, + syms, + program, + |program| { + program.emit_insn(Insn::ParseSchema { + db: usize::MAX, // TODO: This value is unused, change when we do something with it + where_clause: None, + }); + }, + )? + } + ast::AlterTableBody::RenameColumn { old, new } => { + let ast::Name(rename_from) = old; + let ast::Name(rename_to) = new; + + if btree.get_column(&rename_from).is_none() { + return Err(LimboError::ParseError(format!( + "no such column: \"{rename_from}\"" + ))); + }; + + if btree.get_column(&rename_to).is_some() { + return Err(LimboError::ParseError(format!( + "duplicate column name: \"{rename_from}\"" + ))); + }; + + let sqlite_schema = schema + .get_btree_table(SQLITE_TABLEID) + .expect("sqlite_schema should be on schema"); + + let cursor_id = program.alloc_cursor_id(crate::vdbe::builder::CursorType::BTreeTable( + sqlite_schema.clone(), + )); + + program.emit_insn(Insn::OpenWrite { + cursor_id, + root_page: RegisterOrLiteral::Literal(sqlite_schema.root_page), + name: sqlite_schema.name.clone(), + }); + + program.cursor_loop(cursor_id, |program, rowid| { + let sqlite_schema_column_len = sqlite_schema.columns.len(); + assert_eq!(sqlite_schema_column_len, 5); + + let first_column = program.alloc_registers(sqlite_schema_column_len); + + for i in 0..sqlite_schema_column_len { + program.emit_column(cursor_id, i, first_column + i); + } + + program.emit_string8_new_reg(table_name.clone()); + program.mark_last_insn_constant(); + + program.emit_string8_new_reg(rename_from.clone()); + program.mark_last_insn_constant(); + + program.emit_string8_new_reg(rename_to.clone()); + program.mark_last_insn_constant(); + + let out = program.alloc_registers(sqlite_schema_column_len); + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: first_column, + dest: out, + func: crate::function::FuncCtx { + func: Func::AlterTable(AlterTableFunc::RenameColumn), + arg_count: 8, + }, + }); + + let record = program.alloc_register(); + + program.emit_insn(Insn::MakeRecord { + start_reg: out, + count: sqlite_schema_column_len, + dest_reg: record, + index_name: None, + }); + + program.emit_insn(Insn::Insert { + cursor: cursor_id, + key_reg: rowid, + record_reg: record, + flag: 0, + table_name: table_name.clone(), + }); + }); + + program.emit_insn(Insn::ParseSchema { + db: usize::MAX, // TODO: This value is unused, change when we do something with it + where_clause: None, + }); + + program.epilogue(TransactionMode::Write); + + program + } + ast::AlterTableBody::RenameTo(new_name) => { + let ast::Name(new_name) = new_name; + + if schema.get_table(&new_name).is_some() { + return Err(LimboError::ParseError(format!( + "there is already another table or index with this name: {new_name}" + ))); + }; + + let sqlite_schema = schema + .get_btree_table(SQLITE_TABLEID) + .expect("sqlite_schema should be on schema"); + + let cursor_id = program.alloc_cursor_id(crate::vdbe::builder::CursorType::BTreeTable( + sqlite_schema.clone(), + )); + + program.emit_insn(Insn::OpenWrite { + cursor_id, + root_page: RegisterOrLiteral::Literal(sqlite_schema.root_page), + name: sqlite_schema.name.clone(), + }); + + program.cursor_loop(cursor_id, |program, rowid| { + let sqlite_schema_column_len = sqlite_schema.columns.len(); + assert_eq!(sqlite_schema_column_len, 5); + + let first_column = program.alloc_registers(sqlite_schema_column_len); + + for i in 0..sqlite_schema_column_len { + program.emit_column(cursor_id, i, first_column + i); + } + + program.emit_string8_new_reg(table_name.clone()); + program.mark_last_insn_constant(); + + program.emit_string8_new_reg(new_name.clone()); + program.mark_last_insn_constant(); + + let out = program.alloc_registers(5); + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: first_column, + dest: out, + func: crate::function::FuncCtx { + func: Func::AlterTable(AlterTableFunc::RenameTable), + arg_count: 7, + }, + }); + + let record = program.alloc_register(); + + program.emit_insn(Insn::MakeRecord { + start_reg: out, + count: sqlite_schema_column_len, + dest_reg: record, + index_name: None, + }); + + program.emit_insn(Insn::Insert { + cursor: cursor_id, + key_reg: rowid, + record_reg: record, + flag: 0, + table_name: table_name.clone(), + }); + }); + + program.emit_insn(Insn::ParseSchema { + db: usize::MAX, // TODO: This value is unused, change when we do something with it + where_clause: None, + }); + + program.epilogue(TransactionMode::Write); + + program + } + }) +} diff --git a/core/translate/delete.rs b/core/translate/delete.rs index 3786a0e00..301b1586d 100644 --- a/core/translate/delete.rs +++ b/core/translate/delete.rs @@ -36,7 +36,7 @@ pub fn translate_delete( approx_num_labels: 0, }; program.extend(&opts); - emit_program(&mut program, delete_plan, schema, syms)?; + emit_program(&mut program, delete_plan, schema, syms, |_| {})?; Ok(program) } diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 501c0d7b9..d5cddd7bd 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -19,7 +19,6 @@ use super::order_by::{emit_order_by, init_order_by, SortMetadata}; use super::plan::{ JoinOrderMember, Operation, QueryDestination, SelectPlan, TableReferences, UpdatePlan, }; -use super::schema::ParseSchema; use super::select::emit_simple_count; use super::subquery::emit_subqueries; use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY; @@ -182,11 +181,12 @@ pub fn emit_program( plan: Plan, schema: &Schema, syms: &SymbolTable, + after: impl FnOnce(&mut ProgramBuilder), ) -> Result<()> { match plan { Plan::Select(plan) => emit_program_for_select(program, plan, schema, syms), Plan::Delete(plan) => emit_program_for_delete(program, plan, schema, syms), - Plan::Update(plan) => emit_program_for_update(program, plan, schema, syms), + Plan::Update(plan) => emit_program_for_update(program, plan, schema, syms, after), Plan::CompoundSelect { .. } => { emit_program_for_compound_select(program, plan, schema, syms) } @@ -396,6 +396,7 @@ fn get_union_dedupe_index( order: SortOrder::Asc, pos_in_table: 0, collation: None, // FIXME: this should be inferred + default: None, }) .collect(), name: "union_dedupe".to_string(), @@ -437,11 +438,7 @@ fn read_deduplicated_union_rows( } else { dedupe_cols_start_reg }; - program.emit_insn(Insn::Column { - cursor_id: dedupe_cursor_id, - column: col_idx, - dest: start_reg + col_idx, - }); + program.emit_column(dedupe_cursor_id, col_idx, start_reg + col_idx); } if let Some(yield_reg) = yield_reg { program.emit_insn(Insn::Yield { @@ -796,11 +793,11 @@ fn emit_delete_insns( .iter() .enumerate() .for_each(|(reg_offset, column_index)| { - program.emit_insn(Insn::Column { - cursor_id: main_table_cursor_id, - column: column_index.pos_in_table, - dest: start_reg + reg_offset, - }); + program.emit_column( + main_table_cursor_id, + column_index.pos_in_table, + start_reg + reg_offset, + ); }); program.emit_insn(Insn::RowId { cursor_id: main_table_cursor_id, @@ -834,6 +831,7 @@ fn emit_program_for_update( mut plan: UpdatePlan, schema: &Schema, syms: &SymbolTable, + after: impl FnOnce(&mut ProgramBuilder), ) -> Result<()> { let mut t_ctx = TranslateCtx::new( program, @@ -902,16 +900,6 @@ fn emit_program_for_update( )?; emit_update_insns(&plan, &t_ctx, program, index_cursors)?; - match plan.parse_schema { - ParseSchema::None => {} - ParseSchema::Reload => { - program.emit_insn(crate::vdbe::insn::Insn::ParseSchema { - db: usize::MAX, // TODO: This value is unused, change when we do something with it - where_clause: None, - }); - } - } - close_loop( program, &mut t_ctx, @@ -921,6 +909,8 @@ fn emit_program_for_update( program.preassign_label_to_next_insn(after_main_loop_label); + after(program); + // Finalize program program.epilogue(TransactionMode::Write); program.result_columns = plan.returning.unwrap_or_default(); @@ -1127,20 +1117,17 @@ fn emit_update_insns( dest: target_reg, }); } else { - program.emit_insn(Insn::Column { - cursor_id: *index - .as_ref() - .and_then(|(_, id)| { - if column_idx_in_index.is_some() { - Some(id) - } else { - None - } - }) - .unwrap_or(&cursor_id), - column: column_idx_in_index.unwrap_or(idx), - dest: target_reg, - }); + let cursor_id = *index + .as_ref() + .and_then(|(_, id)| { + if column_idx_in_index.is_some() { + Some(id) + } else { + None + } + }) + .unwrap_or(&cursor_id); + program.emit_column(cursor_id, column_idx_in_index.unwrap_or(idx), target_reg); } } } @@ -1301,11 +1288,11 @@ fn emit_update_insns( .iter() .enumerate() .for_each(|(reg_offset, column_index)| { - program.emit_insn(Insn::Column { + program.emit_column( cursor_id, - column: column_index.pos_in_table, - dest: start_reg + reg_offset, - }); + column_index.pos_in_table, + start_reg + reg_offset, + ); }); program.emit_insn(Insn::RowId { diff --git a/core/translate/expr.rs b/core/translate/expr.rs index c1beb32d2..dbcdb47ff 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1799,6 +1799,7 @@ pub fn translate_expr( Ok(target_register) } }, + Func::AlterTable(_) => unreachable!(), } } ast::Expr::FunctionCallStar { .. } => todo!(), @@ -1884,11 +1885,8 @@ pub fn translate_expr( } else { *column }; - program.emit_insn(Insn::Column { - cursor_id: read_cursor, - column, - dest: target_register, - }); + + program.emit_column(read_cursor, column, target_register); } let Some(column) = table.get_column_at(*column) else { crate::bail_parse_error!("column index out of bounds"); diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index d47f116b1..b3875b264 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -442,11 +442,7 @@ impl<'a> GroupByAggArgumentSource<'a> { dest_reg_start, .. } => { - program.emit_insn(Insn::Column { - cursor_id: *cursor_id, - column: *col_start, - dest: dest_reg_start + arg_idx, - }); + program.emit_column(*cursor_id, *col_start, dest_reg_start + arg_idx); Ok(dest_reg_start + arg_idx) } GroupByAggArgumentSource::Register { @@ -493,11 +489,7 @@ pub fn group_by_process_single_group( for i in 0..group_by.exprs.len() { let sorter_column_index = i; let group_reg = groups_start_reg + i; - program.emit_insn(Insn::Column { - cursor_id: *pseudo_cursor, - column: sorter_column_index, - dest: group_reg, - }); + program.emit_column(*pseudo_cursor, sorter_column_index, group_reg); } groups_start_reg } @@ -617,11 +609,7 @@ pub fn group_by_process_single_group( } => { for (sorter_column_index, dest_reg) in column_register_mapping.iter().enumerate() { if let Some(dest_reg) = dest_reg { - program.emit_insn(Insn::Column { - cursor_id: *pseudo_cursor, - column: sorter_column_index, - dest: *dest_reg, - }); + program.emit_column(*pseudo_cursor, sorter_column_index, *dest_reg); } } } diff --git a/core/translate/index.rs b/core/translate/index.rs index d8edfa591..a645582f2 100644 --- a/core/translate/index.rs +++ b/core/translate/index.rs @@ -57,6 +57,7 @@ pub fn translate_create_index( order: *order, pos_in_table: *pos_in_table, collation: col.collation, + default: col.default.clone(), }) .collect(), unique: unique_if_not_exists.0, @@ -142,11 +143,7 @@ pub fn translate_create_index( // Then insert the record into the sorter let start_reg = program.alloc_registers(columns.len() + 1); for (i, (col, _)) in columns.iter().enumerate() { - program.emit_insn(Insn::Column { - cursor_id: table_cursor_id, - column: col.0, - dest: start_reg + i, - }); + program.emit_column(table_cursor_id, col.0, start_reg + i); } let rowid_reg = start_reg + columns.len(); program.emit_insn(Insn::RowId { @@ -371,11 +368,7 @@ pub fn translate_drop_index( // Read sqlite_schema.name into dest_reg let dest_reg = program.alloc_register(); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id, - column: 1, // sqlite_schema.name - dest: dest_reg, - }); + program.emit_column(sqlite_schema_cursor_id, 1, dest_reg); // if current column is not index_name then jump to Next // skip if sqlite_schema.name != index_name_reg @@ -390,11 +383,7 @@ pub fn translate_drop_index( // read type of table // skip if sqlite_schema.type != 'index' (index_str_reg) - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id, - column: 0, - dest: dest_reg, - }); + program.emit_column(sqlite_schema_cursor_id, 0, dest_reg); // if current column is not index then jump to Next program.emit_insn(Insn::Ne { lhs: index_str_reg, diff --git a/core/translate/insert.rs b/core/translate/insert.rs index d664aca6c..14431ab04 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -721,11 +721,11 @@ fn populate_columns_multiple_rows( // Decrement as we have now seen a value index instead other_values_seen -= 1; if let Some(temp_table_ctx) = temp_table_ctx { - program.emit_insn(Insn::Column { - cursor_id: temp_table_ctx.cursor_id, - column: value_index_seen, - dest: column_registers_start + i, - }); + program.emit_column( + temp_table_ctx.cursor_id, + value_index_seen, + column_registers_start + i, + ); } else { program.emit_insn(Insn::Copy { src_reg: yield_reg + value_index_seen, diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index 17868d9a7..fd8ec644f 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -87,6 +87,7 @@ pub fn init_distinct(program: &mut ProgramBuilder, plan: &mut SelectPlan) { order: SortOrder::Asc, pos_in_table: i, collation: None, // FIXME: this should be determined based on the result column expression! + default: None, // FIXME: this should be determined based on the result column expression! }) .collect(), unique: false, @@ -140,6 +141,7 @@ pub fn init_loop( order: SortOrder::Asc, pos_in_table: 0, collation: None, // FIXME: this should be inferred from the expression + default: None, // FIXME: this should be inferred from the expression }], has_rowid: false, unique: false, @@ -1405,11 +1407,7 @@ fn emit_autoindex( let ephemeral_cols_start_reg = program.alloc_registers(num_regs_to_reserve); for (i, col) in index.columns.iter().enumerate() { let reg = ephemeral_cols_start_reg + i; - program.emit_insn(Insn::Column { - cursor_id: table_cursor_id, - column: col.pos_in_table, - dest: reg, - }); + program.emit_column(table_cursor_id, col.pos_in_table, reg); } if table_has_rowid { program.emit_insn(Insn::RowId { diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 716479c6e..a103f9aaf 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -8,6 +8,7 @@ //! will read rows from the database and filter them according to a WHERE clause. pub(crate) mod aggregation; +pub(crate) mod alter; pub(crate) mod collate; pub(crate) mod delete; pub(crate) mod display; @@ -37,16 +38,12 @@ use crate::storage::sqlite3_ondisk::DatabaseHeader; use crate::translate::delete::translate_delete; use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode}; use crate::vdbe::Program; -use crate::{bail_parse_error, Connection, LimboError, Result, SymbolTable}; -use fallible_iterator::FallibleIterator as _; +use crate::{bail_parse_error, Connection, Result, SymbolTable}; +use alter::translate_alter_table; use index::{translate_create_index, translate_drop_index}; use insert::translate_insert; use limbo_sqlite3_parser::ast::{self, Delete, Insert}; -use limbo_sqlite3_parser::lexer::sql::Parser; -use schema::{ - translate_create_table, translate_create_virtual_table, translate_drop_table, ParseSchema, - SQLITE_TABLEID, -}; +use schema::{translate_create_table, translate_create_virtual_table, translate_drop_table}; use select::translate_select; use std::rc::{Rc, Weak}; use std::sync::Arc; @@ -111,58 +108,7 @@ pub fn translate_inner( program: ProgramBuilder, ) -> Result { let program = match stmt { - ast::Stmt::AlterTable(a) => { - let (table_name, alter_table) = a.as_ref(); - - match alter_table { - ast::AlterTableBody::RenameTo(name) => { - let rename = &name.0; - let name = &table_name.name.0; - - let Some(table) = schema.tables.get(name) else { - return Err(LimboError::ParseError(format!("no such table: {name}"))); - }; - - if schema.tables.contains_key(rename) { - return Err(LimboError::ParseError(format!( - "there is already another table or index with this name: {rename}" - ))); - }; - - let Some(btree) = table.btree() else { todo!() }; - - let mut btree = (*btree).clone(); - btree.name = rename.clone(); - - let sql = btree.to_sql(); - - let stmt = format!( - r#" - UPDATE {SQLITE_TABLEID} - SET name = '{rename}' - , tbl_name = '{rename}' - , sql = '{sql}' - WHERE tbl_name = '{name}' - "#, - ); - - let mut parser = Parser::new(stmt.as_bytes()); - let Some(ast::Cmd::Stmt(ast::Stmt::Update(mut update))) = parser.next()? else { - unreachable!(); - }; - - translate_update( - QueryMode::Normal, - schema, - &mut update, - syms, - ParseSchema::Reload, - program, - )? - } - _ => todo!(), - } - } + ast::Stmt::AlterTable(alter) => translate_alter_table(*alter, syms, schema, program)?, ast::Stmt::Analyze(_) => bail_parse_error!("ANALYZE not supported yet"), ast::Stmt::Attach { .. } => bail_parse_error!("ATTACH not supported yet"), ast::Stmt::Begin(tx_type, tx_name) => translate_tx_begin(tx_type, tx_name, program)?, @@ -248,14 +194,9 @@ pub fn translate_inner( )? .program } - ast::Stmt::Update(mut update) => translate_update( - query_mode, - schema, - &mut update, - syms, - ParseSchema::None, - program, - )?, + ast::Stmt::Update(mut update) => { + translate_update(query_mode, schema, &mut update, syms, program)? + } ast::Stmt::Vacuum(_, _) => bail_parse_error!("VACUUM not supported yet"), ast::Stmt::Insert(insert) => { let Insert { diff --git a/core/translate/optimizer/join.rs b/core/translate/optimizer/join.rs index 2378b101e..11ebbd109 100644 --- a/core/translate/optimizer/join.rs +++ b/core/translate/optimizer/join.rs @@ -657,6 +657,7 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }], unique: true, ephemeral: false, @@ -724,6 +725,7 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }], unique: true, ephemeral: false, @@ -839,6 +841,7 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }], unique: true, ephemeral: false, @@ -855,6 +858,7 @@ mod tests { order: SortOrder::Asc, pos_in_table: 1, collation: None, + default: None, }], unique: false, ephemeral: false, @@ -869,6 +873,7 @@ mod tests { order: SortOrder::Asc, pos_in_table: 1, collation: None, + default: None, }], unique: false, ephemeral: false, @@ -1278,12 +1283,14 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }, IndexColumn { name: "y".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, + default: None, }, ], unique: false, @@ -1362,18 +1369,21 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }, IndexColumn { name: "c2".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, + default: None, }, IndexColumn { name: "c3".to_string(), order: SortOrder::Asc, pos_in_table: 2, collation: None, + default: None, }, ], unique: false, @@ -1475,18 +1485,21 @@ mod tests { order: SortOrder::Asc, pos_in_table: 0, collation: None, + default: None, }, IndexColumn { name: "c2".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, + default: None, }, IndexColumn { name: "c3".to_string(), order: SortOrder::Asc, pos_in_table: 2, collation: None, + default: None, }, ], root_page: 2, diff --git a/core/translate/optimizer/mod.rs b/core/translate/optimizer/mod.rs index 71c156687..e99bc8895 100644 --- a/core/translate/optimizer/mod.rs +++ b/core/translate/optimizer/mod.rs @@ -776,6 +776,7 @@ fn ephemeral_index_build( order: SortOrder::Asc, pos_in_table: i, collation: c.collation, + default: c.default.clone(), }) // only include columns that are used in the query .filter(|c| table_reference.column_is_used(c.pos_in_table)) diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index eed98cd3e..d710996d8 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -166,11 +166,11 @@ pub fn emit_order_by( let start_reg = t_ctx.reg_result_cols_start.unwrap(); for i in 0..result_columns.len() { let reg = start_reg + i; - program.emit_insn(Insn::Column { + program.emit_column( cursor_id, - column: t_ctx.result_column_indexes_in_orderby_sorter[i], - dest: reg, - }); + t_ctx.result_column_indexes_in_orderby_sorter[i], + reg, + ); } emit_result_row_and_limit( diff --git a/core/translate/plan.rs b/core/translate/plan.rs index de0a5b00f..fca387cf5 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -17,7 +17,7 @@ use crate::{schema::Type, types::SeekOp, util::can_pushdown_predicate}; use limbo_sqlite3_parser::ast::TableInternalId; -use super::{emitter::OperationMode, planner::determine_where_to_eval_term, schema::ParseSchema}; +use super::{emitter::OperationMode, planner::determine_where_to_eval_term}; #[derive(Debug, Clone)] pub struct ResultSetColumn { @@ -564,7 +564,6 @@ pub struct UpdatePlan { // whether the WHERE clause is always false pub contains_constant_false_condition: bool, pub indexes_to_update: Vec>, - pub parse_schema: ParseSchema, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/core/translate/schema.rs b/core/translate/schema.rs index 1b1f6c040..fd8ee0ef3 100644 --- a/core/translate/schema.rs +++ b/core/translate/schema.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::fmt::Display; use std::ops::Range; use std::rc::Rc; @@ -24,12 +23,6 @@ use crate::{bail_parse_error, Result}; use limbo_ext::VTabKind; use limbo_sqlite3_parser::ast::{fmt::ToTokens, CreateVirtualTable}; -#[derive(Debug, Clone, Copy)] -pub enum ParseSchema { - None, - Reload, -} - pub fn translate_create_table( query_mode: QueryMode, tbl_name: ast::QualifiedName, @@ -447,20 +440,16 @@ enum PrimaryKeyDefinitionType<'a> { }, } -struct TableFormatter<'a> { - body: &'a ast::CreateTableBody, -} - -impl Display for TableFormatter<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.body.to_fmt(f) - } -} - fn create_table_body_to_str(tbl_name: &ast::QualifiedName, body: &ast::CreateTableBody) -> String { let mut sql = String::new(); - let formatter = TableFormatter { body }; - sql.push_str(format!("CREATE TABLE {} {}", tbl_name.name.0, formatter).as_str()); + sql.push_str( + format!( + "CREATE TABLE {} {}", + tbl_name.name.0, + body.format().unwrap() + ) + .as_str(), + ); match body { ast::CreateTableBody::ColumnsAndConstraints { columns: _, @@ -671,11 +660,11 @@ pub fn translate_drop_table( program.preassign_label_to_next_insn(metadata_loop); // start loop on schema table - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_0, - column: 2, - dest: table_name_and_root_page_register, - }); + program.emit_column( + sqlite_schema_cursor_id_0, + 2, + table_name_and_root_page_register, + ); let next_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: table_name_and_root_page_register, @@ -684,11 +673,11 @@ pub fn translate_drop_table( flags: CmpInsFlags::default(), collation: program.curr_collation(), }); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_0, - column: 0, - dest: table_name_and_root_page_register, - }); + program.emit_column( + sqlite_schema_cursor_id_0, + 0, + table_name_and_root_page_register, + ); program.emit_insn(Insn::Eq { lhs: table_name_and_root_page_register, rhs: table_type, @@ -821,11 +810,7 @@ pub fn translate_drop_table( }); program.preassign_label_to_next_insn(copy_schema_to_temp_table_loop); // start loop on schema table - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_1, - column: 3, - dest: prev_root_page_register, - }); + program.emit_column(sqlite_schema_cursor_id_1, 3, prev_root_page_register); // The label and Insn::Ne are used to skip over any rows in the schema table that don't have the root page that was moved let next_label = program.allocate_label(); program.emit_insn(Insn::Ne { @@ -884,21 +869,9 @@ pub fn translate_drop_table( rowid_reg: schema_row_id_register, target_pc: next_label, }); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_1, - column: 0, - dest: schema_column_0_register, - }); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_1, - column: 1, - dest: schema_column_1_register, - }); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_1, - column: 2, - dest: schema_column_2_register, - }); + program.emit_column(sqlite_schema_cursor_id_1, 0, schema_column_0_register); + program.emit_column(sqlite_schema_cursor_id_1, 1, schema_column_1_register); + program.emit_column(sqlite_schema_cursor_id_1, 2, schema_column_2_register); let root_page = table .get_root_page() .try_into() @@ -907,11 +880,7 @@ pub fn translate_drop_table( value: root_page, dest: moved_to_root_page_register, }); - program.emit_insn(Insn::Column { - cursor_id: sqlite_schema_cursor_id_1, - column: 4, - dest: schema_column_4_register, - }); + program.emit_column(sqlite_schema_cursor_id_1, 4, schema_column_4_register); program.emit_insn(Insn::MakeRecord { start_reg: schema_column_0_register, count: 5, diff --git a/core/translate/select.rs b/core/translate/select.rs index 392bfcf53..4aff03a63 100644 --- a/core/translate/select.rs +++ b/core/translate/select.rs @@ -79,7 +79,7 @@ pub fn translate_select( }; program.extend(&opts); - emit_program(&mut program, select_plan, schema, syms)?; + emit_program(&mut program, select_plan, schema, syms, |_| {})?; Ok(TranslateSelectResult { program, num_result_cols, diff --git a/core/translate/update.rs b/core/translate/update.rs index 9d3d72f9e..d4af63b0e 100644 --- a/core/translate/update.rs +++ b/core/translate/update.rs @@ -17,8 +17,6 @@ use super::plan::{ }; use super::planner::bind_column_references; use super::planner::{parse_limit, parse_where}; -use super::schema::ParseSchema; - /* * Update is simple. By default we scan the table, and for each row, we check the WHERE * clause. If it evaluates to true, we build the new record with the updated value and insert. @@ -53,15 +51,9 @@ pub fn translate_update( schema: &Schema, body: &mut Update, syms: &SymbolTable, - parse_schema: ParseSchema, mut program: ProgramBuilder, ) -> crate::Result { - let mut plan = prepare_update_plan( - schema, - body, - parse_schema, - &mut program.table_reference_counter, - )?; + let mut plan = prepare_update_plan(schema, body, &mut program.table_reference_counter)?; optimize_plan(&mut plan, schema)?; // TODO: freestyling these numbers let opts = ProgramBuilderOpts { @@ -71,14 +63,35 @@ pub fn translate_update( approx_num_labels: 4, }; program.extend(&opts); - emit_program(&mut program, plan, schema, syms)?; + emit_program(&mut program, plan, schema, syms, |_| {})?; + Ok(program) +} + +pub fn translate_update_with_after( + query_mode: QueryMode, + schema: &Schema, + body: &mut Update, + syms: &SymbolTable, + mut program: ProgramBuilder, + after: impl FnOnce(&mut ProgramBuilder), +) -> crate::Result { + let mut plan = prepare_update_plan(schema, body, &mut program.table_reference_counter)?; + optimize_plan(&mut plan, schema)?; + // TODO: freestyling these numbers + let opts = ProgramBuilderOpts { + query_mode, + num_cursors: 1, + approx_num_insns: 20, + approx_num_labels: 4, + }; + program.extend(&opts); + emit_program(&mut program, plan, schema, syms, after)?; Ok(program) } pub fn prepare_update_plan( schema: &Schema, body: &mut Update, - parse_schema: ParseSchema, table_ref_counter: &mut TableRefIdCounter, ) -> crate::Result { if body.with.is_some() { @@ -215,6 +228,5 @@ pub fn prepare_update_plan( offset, contains_constant_false_condition: false, indexes_to_update, - parse_schema, })) } diff --git a/core/types.rs b/core/types.rs index dcd5ed86e..010b01202 100644 --- a/core/types.rs +++ b/core/types.rs @@ -93,6 +93,12 @@ impl Text { } } +impl AsRef for Text { + fn as_ref(&self) -> &str { + self.as_str() + } +} + impl From for Text { fn from(value: String) -> Self { Text { diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 0da96dade..8254695d8 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -10,6 +10,7 @@ use tracing::{instrument, Level}; use crate::{ fast_lock::SpinLock, + numeric::Numeric, parameters::Parameters, schema::{BTreeTable, Index, PseudoTable, Table}, storage::sqlite3_ondisk::DatabaseHeader, @@ -18,7 +19,8 @@ use crate::{ emitter::TransactionMode, plan::{ResultSetColumn, TableReferences}, }, - Connection, VirtualTable, + types::Text, + Connection, Value, VirtualTable, }; pub struct TableRefIdCounter { next_free: TableInternalId, @@ -771,6 +773,86 @@ impl ProgramBuilder { self.table_references.contains_table(table) } + #[inline] + pub fn cursor_loop(&mut self, cursor_id: CursorID, f: impl Fn(&mut ProgramBuilder, usize)) { + let loop_start = self.allocate_label(); + let loop_end = self.allocate_label(); + + self.emit_insn(Insn::Rewind { + cursor_id, + pc_if_empty: loop_end, + }); + self.preassign_label_to_next_insn(loop_start); + + let rowid = self.alloc_register(); + + self.emit_insn(Insn::RowId { + cursor_id, + dest: rowid, + }); + + self.emit_insn(Insn::IsNull { + reg: rowid, + target_pc: loop_end, + }); + + f(self, rowid); + + self.emit_insn(Insn::Next { + cursor_id, + pc_if_next: loop_start, + }); + self.preassign_label_to_next_insn(loop_end); + } + + pub fn emit_column(&mut self, cursor_id: CursorID, column: usize, out: usize) { + let (_, cursor_type) = self.cursor_ref.get(cursor_id).unwrap(); + + use crate::translate::expr::sanitize_string; + + let default = 'value: { + let default = match cursor_type { + CursorType::BTreeTable(btree) => &btree.columns[column].default, + CursorType::BTreeIndex(index) => &index.columns[column].default, + _ => break 'value None, + }; + + let Some(ast::Expr::Literal(ref literal)) = default else { + break 'value None; + }; + + Some(match literal { + ast::Literal::Numeric(s) => match Numeric::from(s) { + Numeric::Null => Value::Null, + Numeric::Integer(v) => Value::Integer(v), + Numeric::Float(v) => Value::Float(v.into()), + }, + ast::Literal::Null => Value::Null, + ast::Literal::String(s) => Value::Text(Text::from_str(sanitize_string(s))), + ast::Literal::Blob(s) => Value::Blob( + // Taken from `translate_expr` + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + // We assume that sqlite3-parser has already validated that + // the input is valid hex string, thus unwrap is safe. + let hex_byte = std::str::from_utf8(pair).unwrap(); + u8::from_str_radix(hex_byte, 16).unwrap() + }) + .collect(), + ), + _ => break 'value None, + }) + }; + + self.emit_insn(Insn::Column { + cursor_id, + column, + dest: out, + default, + }); + } + pub fn build( mut self, database_header: Arc>, diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index fc8da4cc8..6618c9056 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -1,4 +1,5 @@ #![allow(unused_variables)] +use crate::function::AlterTableFunc; use crate::numeric::{NullableInteger, Numeric}; use crate::schema::Schema; use crate::storage::database::FileMemoryStorage; @@ -6,7 +7,8 @@ use crate::storage::page_cache::DumbLruPageCache; use crate::storage::pager::CreateBTreeFlags; use crate::storage::wal::DummyWAL; use crate::translate::collate::CollationSeq; -use crate::types::ImmutableRecord; +use crate::types::{ImmutableRecord, Text}; +use crate::util::normalize_ident; use crate::{ error::{LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_PRIMARYKEY}, ext::ExtValue, @@ -53,6 +55,10 @@ use super::{ insn::{Cookie, RegisterOrLiteral}, CommitState, }; +use fallible_iterator::FallibleIterator; +use limbo_sqlite3_parser::ast; +use limbo_sqlite3_parser::ast::fmt::ToTokens; +use limbo_sqlite3_parser::lexer::sql::Parser; use parking_lot::RwLock; use rand::thread_rng; @@ -1288,6 +1294,7 @@ pub fn op_column( cursor_id, column, dest, + default, } = insn else { unreachable!("unexpected Insn {:?}", insn) @@ -1322,38 +1329,48 @@ pub fn op_column( let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); match cursor_type { CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) => { - let value = { + let value = 'value: { let mut cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Column"); let cursor = cursor.as_btree_mut(); let record = return_if_io!(cursor.record()); - let value = if let Some(record) = record.as_ref() { - if cursor.get_null_flag() { - RefValue::Null - } else { - match record.get_value_opt(*column) { - Some(val) => val.clone(), - None => RefValue::Null, - } - } - } else { - RefValue::Null + + let Some(record) = record.as_ref() else { + break 'value Value::Null; }; - value + + let value = if cursor.get_null_flag() { + Value::Null + } else { + match record.get_value_opt(*column) { + Some(val) => val.to_owned(), + None => Value::Null, + } + }; + + if cursor.get_null_flag() { + break 'value Value::Null; + } + + if let Some(value) = record.get_value_opt(*column) { + break 'value value.to_owned(); + } + + default.clone().unwrap_or(Value::Null) }; // If we are copying a text/blob, let's try to simply update size of text if we need to allocate more and reuse. match (&value, &mut state.registers[*dest]) { - (RefValue::Text(text_ref), Register::Value(Value::Text(text_reg))) => { + (Value::Text(text_ref), Register::Value(Value::Text(text_reg))) => { text_reg.value.clear(); - text_reg.value.extend_from_slice(text_ref.value.to_slice()); + text_reg.value.extend_from_slice(text_ref.value.as_slice()); } - (RefValue::Blob(raw_slice), Register::Value(Value::Blob(blob_reg))) => { + (Value::Blob(raw_slice), Register::Value(Value::Blob(blob_reg))) => { blob_reg.clear(); - blob_reg.extend_from_slice(raw_slice.to_slice()); + blob_reg.extend_from_slice(raw_slice.as_slice()); } _ => { let reg = &mut state.registers[*dest]; - *reg = Register::Value(value.to_owned()); + *reg = Register::Value(value); } } } @@ -1366,7 +1383,7 @@ pub fn op_column( if let Some(record) = record { state.registers[*dest] = Register::Value(match record.get_value_opt(*column) { Some(val) => val.to_owned(), - None => Value::Null, + None => default.clone().unwrap_or(Value::Null), }); } else { state.registers[*dest] = Register::Value(Value::Null); @@ -3759,6 +3776,275 @@ pub fn op_function( ), }, }, + crate::function::Func::AlterTable(alter_func) => { + let r#type = &state.registers[*start_reg + 0].get_owned_value().clone(); + + let Value::Text(name) = &state.registers[*start_reg + 1].get_owned_value() else { + panic!("sqlite_schema.name should be TEXT") + }; + let name = name.to_string(); + + let Value::Text(tbl_name) = &state.registers[*start_reg + 2].get_owned_value() else { + panic!("sqlite_schema.tbl_name should be TEXT") + }; + let tbl_name = tbl_name.to_string(); + + let Value::Integer(root_page) = + &state.registers[*start_reg + 3].get_owned_value().clone() + else { + panic!("sqlite_schema.root_page should be INTEGER") + }; + + let sql = &state.registers[*start_reg + 4].get_owned_value().clone(); + + let (new_name, new_tbl_name, new_sql) = match alter_func { + AlterTableFunc::RenameTable => { + let rename_from = { + match &state.registers[*start_reg + 5].get_owned_value() { + Value::Text(rename_from) => normalize_ident(rename_from.as_str()), + _ => panic!("rename_from parameter should be TEXT"), + } + }; + + let rename_to = { + match &state.registers[*start_reg + 6].get_owned_value() { + Value::Text(rename_to) => normalize_ident(rename_to.as_str()), + _ => panic!("rename_to parameter should be TEXT"), + } + }; + + let new_name = if let Some(column) = + &name.strip_prefix(&format!("sqlite_autoindex_{rename_from}_")) + { + format!("sqlite_autoindex_{rename_to}_{column}") + } else if name == rename_from { + rename_to.clone() + } else { + name + }; + + let new_tbl_name = if tbl_name == rename_from { + rename_to.clone() + } else { + tbl_name + }; + + let new_sql = 'sql: { + let Value::Text(sql) = sql else { + break 'sql None; + }; + + let mut parser = Parser::new(sql.as_str().as_bytes()); + let ast::Cmd::Stmt(stmt) = parser.next().unwrap().unwrap() else { + todo!() + }; + + match stmt { + ast::Stmt::CreateIndex { + unique, + if_not_exists, + idx_name, + tbl_name, + columns, + where_clause, + } => { + let table_name = normalize_ident(&tbl_name.0); + + if rename_from != table_name { + break 'sql None; + } + + Some( + ast::Stmt::CreateIndex { + unique, + if_not_exists, + idx_name, + tbl_name: ast::Name(rename_to), + columns, + where_clause, + } + .format() + .unwrap(), + ) + } + ast::Stmt::CreateTable { + temporary, + if_not_exists, + tbl_name, + body, + } => { + let table_name = normalize_ident(&tbl_name.name.0); + + if rename_from != table_name { + break 'sql None; + } + + Some( + ast::Stmt::CreateTable { + temporary, + if_not_exists, + tbl_name: ast::QualifiedName { + db_name: None, + name: ast::Name(rename_to), + alias: None, + }, + body, + } + .format() + .unwrap(), + ) + } + _ => todo!(), + } + }; + + (new_name, new_tbl_name, new_sql) + } + AlterTableFunc::RenameColumn => { + let table = { + match &state.registers[*start_reg + 5].get_owned_value() { + Value::Text(rename_to) => normalize_ident(rename_to.as_str()), + _ => panic!("table parameter should be TEXT"), + } + }; + + let rename_from = { + match &state.registers[*start_reg + 6].get_owned_value() { + Value::Text(rename_from) => normalize_ident(rename_from.as_str()), + _ => panic!("rename_from parameter should be TEXT"), + } + }; + + let rename_to = { + match &state.registers[*start_reg + 7].get_owned_value() { + Value::Text(rename_to) => normalize_ident(rename_to.as_str()), + _ => panic!("rename_to parameter should be TEXT"), + } + }; + + let new_sql = 'sql: { + if table != tbl_name { + break 'sql None; + } + + let Value::Text(sql) = sql else { + break 'sql None; + }; + + let mut parser = Parser::new(sql.as_str().as_bytes()); + let ast::Cmd::Stmt(stmt) = parser.next().unwrap().unwrap() else { + todo!() + }; + + match stmt { + ast::Stmt::CreateIndex { + unique, + if_not_exists, + idx_name, + tbl_name, + mut columns, + where_clause, + } => { + if table != normalize_ident(&tbl_name.0) { + break 'sql None; + } + + for column in &mut columns { + match &mut column.expr { + ast::Expr::Id(ast::Id(id)) + if normalize_ident(&id) == rename_from => + { + *id = rename_to.clone(); + } + _ => {} + } + } + + Some( + ast::Stmt::CreateIndex { + unique, + if_not_exists, + idx_name, + tbl_name, + columns, + where_clause, + } + .format() + .unwrap(), + ) + } + ast::Stmt::CreateTable { + temporary, + if_not_exists, + tbl_name, + body, + } => { + if table != normalize_ident(&tbl_name.name.0) { + break 'sql None; + } + + let ast::CreateTableBody::ColumnsAndConstraints { + mut columns, + constraints, + options, + } = *body + else { + todo!() + }; + + let column_index = columns + .get_index_of(&ast::Name(rename_from)) + .expect("column being renamed should be present"); + + let mut column_definition = + columns.get_index(column_index).unwrap().1.clone(); + + column_definition.col_name = ast::Name(rename_to.clone()); + + assert!(columns + .insert(ast::Name(rename_to), column_definition.clone()) + .is_none()); + + // Swaps indexes with the last one and pops the end, effectively + // replacing the entry. + columns.swap_remove_index(column_index).unwrap(); + + Some( + ast::Stmt::CreateTable { + temporary, + if_not_exists, + tbl_name, + body: Box::new( + ast::CreateTableBody::ColumnsAndConstraints { + columns, + constraints, + options, + }, + ), + } + .format() + .unwrap(), + ) + } + _ => todo!(), + } + }; + + (name, tbl_name, new_sql) + } + }; + + state.registers[*dest + 0] = Register::Value(r#type.clone()); + state.registers[*dest + 1] = Register::Value(Value::Text(Text::from(new_name))); + state.registers[*dest + 2] = Register::Value(Value::Text(Text::from(new_tbl_name))); + state.registers[*dest + 3] = Register::Value(Value::Integer(*root_page)); + + if let Some(new_sql) = new_sql { + state.registers[*dest + 4] = Register::Value(Value::Text(Text::from(new_sql))); + } else { + state.registers[*dest + 4] = Register::Value(sql.clone()); + } + } crate::function::Func::Agg(_) => { unreachable!("Aggregate functions should not be handled here") } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index fa6a2a70e..ccf5096d4 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -527,11 +527,12 @@ pub fn insn_to_str( cursor_id, column, dest, + default, } => { let cursor_type = &program.cursor_ref[*cursor_id].1; let column_name: Option<&String> = match cursor_type { CursorType::BTreeTable(table) => { - let name = table.columns.get(*column).unwrap().name.as_ref(); + let name = table.columns.get(*column).and_then(|v| v.name.as_ref()); name } CursorType::BTreeIndex(index) => { @@ -550,7 +551,7 @@ pub fn insn_to_str( *cursor_id as i32, *column as i32, *dest as i32, - Value::build_text(""), + default.clone().unwrap_or_else(|| Value::build_text("")), 0, format!( "r[{}]={}.{}", diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index db319f936..8e8fcef18 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -9,6 +9,7 @@ use crate::{ schema::{Affinity, BTreeTable, Index}, storage::{pager::CreateBTreeFlags, wal::CheckpointMode}, translate::collate::CollationSeq, + Value, }; use limbo_macros::Description; use limbo_sqlite3_parser::ast::SortOrder; @@ -376,6 +377,7 @@ pub enum Insn { cursor_id: CursorID, column: usize, dest: usize, + default: Option, }, TypeCheck { diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 091feceb7..507975013 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -523,9 +523,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" -version = "0.1.39" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44" +checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" dependencies = [ "cc", "libc", @@ -566,8 +566,9 @@ dependencies = [ [[package]] name = "limbo_core" -version = "0.0.19" +version = "0.0.21" dependencies = [ + "bitflags", "built", "cfg_block", "chrono", @@ -590,16 +591,18 @@ dependencies = [ "rand", "regex", "regex-syntax", - "rustix", + "rustix 1.0.7", "ryu", "strum", + "strum_macros", "thiserror 1.0.69", "tracing", + "uncased", ] [[package]] name = "limbo_ext" -version = "0.0.19" +version = "0.0.21" dependencies = [ "chrono", "getrandom 0.3.1", @@ -608,7 +611,7 @@ dependencies = [ [[package]] name = "limbo_macros" -version = "0.0.19" +version = "0.0.21" dependencies = [ "proc-macro2", "quote", @@ -617,7 +620,7 @@ dependencies = [ [[package]] name = "limbo_sqlite3_parser" -version = "0.0.19" +version = "0.0.21" dependencies = [ "bitflags", "cc", @@ -636,7 +639,7 @@ dependencies = [ [[package]] name = "limbo_time" -version = "0.0.19" +version = "0.0.21" dependencies = [ "chrono", "limbo_ext", @@ -648,7 +651,7 @@ dependencies = [ [[package]] name = "limbo_uuid" -version = "0.0.19" +version = "0.0.21" dependencies = [ "limbo_ext", "mimalloc", @@ -661,6 +664,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.5" @@ -691,21 +700,20 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miette" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", "miette-derive", - "thiserror 1.0.69", "unicode-width", ] [[package]] name = "miette-derive" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", @@ -714,9 +722,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.43" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633" +checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" dependencies = [ "libmimalloc-sys", ] @@ -826,7 +834,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys", ] @@ -949,7 +957,20 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", "windows-sys", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 69d6f438f..7c157b8eb 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -18,6 +18,10 @@ rusqlite = { version = "0.34.0", features = ["bundled"] } [workspace] members = ["."] +[[bin]] +name = "schema" +path = "fuzz_targets/schema.rs" + [[bin]] name = "expression" path = "fuzz_targets/expression.rs" diff --git a/fuzz/fuzz_targets/expression.rs b/fuzz/fuzz_targets/expression.rs index bd838093b..4715e9673 100644 --- a/fuzz/fuzz_targets/expression.rs +++ b/fuzz/fuzz_targets/expression.rs @@ -184,7 +184,7 @@ fn do_fuzz(expr: Expr) -> Result> { let found = 'value: { let io = Arc::new(limbo_core::MemoryIO::new()); - let db = limbo_core::Database::open_file(io.clone(), ":memory:", true)?; + let db = limbo_core::Database::open_file(io.clone(), ":memory:", false)?; let conn = db.connect()?; let mut stmt = conn.prepare(sql)?; diff --git a/fuzz/fuzz_targets/schema.rs b/fuzz/fuzz_targets/schema.rs new file mode 100644 index 000000000..c5ff3c731 --- /dev/null +++ b/fuzz/fuzz_targets/schema.rs @@ -0,0 +1,365 @@ +#![no_main] +use core::fmt; +use std::{error::Error, sync::Arc}; + +use arbitrary::Arbitrary; +use libfuzzer_sys::{fuzz_target, Corpus}; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Id(String); + +impl<'a> Arbitrary<'a> for Id { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let len: usize = u.int_in_range(1..=10)?; + let is_quoted = bool::arbitrary(u)?; + + let mut out = String::with_capacity(len + if is_quoted { 2 } else { 0 }); + + if is_quoted { + out.push('"'); + } + + for _ in 0..len { + out.push(u.choose(b"abcdefghijklnmopqrstuvwxyz")?.clone() as char); + } + + if is_quoted { + out.push('"'); + } + + Ok(Id(out)) + } +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Arbitrary, Clone)] +enum Type { + None, + Integer, + Text, + Real, + Blob, +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Type::None => Ok(()), + Type::Integer => write!(f, "INTEGER"), + Type::Text => write!(f, "TEXT"), + Type::Real => write!(f, "REAL"), + Type::Blob => write!(f, "BLOB"), + } + } +} + +#[derive(Debug, Arbitrary, Clone)] +struct ColumnDef { + name: Id, + r#type: Type, +} + +impl fmt::Display for ColumnDef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ColumnDef { + name, + r#type, + } = self; + write!(f, "{name} {type}",)?; + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Columns(Vec); + +impl<'a> Arbitrary<'a> for Columns { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let len: usize = u.int_in_range(1..=4)?; + + let mut out: Vec = Vec::with_capacity(len); + + for i in 0..len { + out.push(ColumnDef { + name: Id(format!("c{i}")), + r#type: u.arbitrary()?, + }); + } + + Ok(Self(out)) + } +} + +impl fmt::Display for Columns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, column) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{column}")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct TableDef { + name: Id, + columns: Columns, +} + +impl fmt::Display for TableDef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let TableDef { name, columns } = self; + + write!(f, "CREATE TABLE {name} ( {columns} )") + } +} + +#[derive(Debug, Clone)] +struct IndexDef { + name: Id, + table: Id, + columns: Vec, +} + +impl fmt::Display for IndexDef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let IndexDef { + name, + table, + columns, + } = self; + + write!(f, "CREATE INDEX {name} ON {table}(")?; + + for (i, column) in columns.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{column}")?; + } + + write!(f, ")")?; + + Ok(()) + } +} + +#[derive(Debug)] +enum Op { + CreateTable(TableDef), + CreateIndex(IndexDef), + DropTable { + table: Id, + }, + DropColumn { + table: Id, + column: Id, + }, + RenameTable { + rename_from: Id, + rename_to: Id, + }, + RenameColumn { + table: Id, + rename_from: Id, + rename_to: Id, + }, +} + +impl fmt::Display for Op { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Op::CreateTable(table_def) => write!(f, "{table_def}"), + Op::CreateIndex(index_def) => write!(f, "{index_def}"), + Op::DropColumn { table, column } => { + write!(f, "ALTER TABLE {table} DROP COLUMN {column}") + } + Op::DropTable { table } => write!(f, "DROP TABLE {table}"), + Op::RenameTable { + rename_from, + rename_to, + } => write!(f, "ALTER TABLE {rename_from} RENAME TO {rename_to}"), + Op::RenameColumn { + table, + rename_from, + rename_to, + } => { + write!( + f, + "ALTER TABLE {table} RENAME COLUMN {rename_from} TO {rename_to}" + ) + } + } + } +} + +#[derive(Debug)] +struct Ops(Vec); + +impl<'a> Arbitrary<'a> for Ops { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let mut ops = Vec::new(); + let mut tables = Vec::new(); + + let mut drop_list = Vec::new(); + + let mut table_index: usize = 0; + + let num_ops = u.int_in_range(1..=10)?; + + for _ in 0..num_ops { + let op_type = if tables.is_empty() { + 0 + } else { + u.int_in_range(0..=2)? + }; + + match op_type { + 0 => { + let table_def = TableDef { + name: { + let out = format!("t{table_index}"); + table_index += 1; + + Id(out) + }, + columns: u.arbitrary()?, + }; + + ops.push(Op::CreateTable(table_def.clone())); + + tables.push(table_def); + } + 1 => { + let table = u.choose(&tables)?; + let index_def = IndexDef { + name: { + let out = format!("i{table_index}"); + table_index += 1; + + Id(out) + }, + table: table.name.clone(), + columns: vec![u.choose(&table.columns.0)?.name.clone()], + }; + + ops.push(Op::CreateIndex(index_def.clone())); + } + 2 => { + let index = u.choose_index(tables.len())?; + + let table = &tables[index]; + + let rename_to = Id(format!("t{table_index}")); + table_index += 1; + + ops.push(Op::RenameTable { + rename_from: table.name.clone(), + rename_to: rename_to.clone(), + }); + + tables.push(TableDef { + name: rename_to, + columns: table.columns.clone(), + }); + + tables.remove(index); + } + 3 => { + let index = u.choose_index(tables.len())?; + + let table = &tables[index]; + + if table.columns.0.len() == 1 { + let table = tables.remove(index); + + ops.push(Op::DropTable { + table: table.name.clone(), + }); + + drop_list.push(table.name); + } else { + let table = &mut tables[index]; + + let index = u.choose_index(table.columns.0.len())?; + + ops.push(Op::DropColumn { + table: table.name.clone(), + column: table.columns.0.remove(index).name, + }); + } + } + 4 => { + let index = u.choose_index(tables.len())?; + + let table = &mut tables[index]; + + let index = u.choose_index(table.columns.0.len())?; + + let rename_to = Id(format!("cr{table_index}")); + table_index += 1; + + let column = table.columns.0[index].clone(); + + table.columns.0.insert( + index, + ColumnDef { + name: rename_to.clone(), + ..column + }, + ); + + ops.push(Op::RenameColumn { + table: table.name.clone(), + rename_from: column.name, + rename_to, + }); + } + _ => panic!(), + } + } + + Ok(Self(ops)) + } +} + +fn do_fuzz(Ops(ops): Ops) -> Result> { + let rusqlite_conn = rusqlite::Connection::open_in_memory()?; + + let io = Arc::new(limbo_core::MemoryIO::new()); + let db = limbo_core::Database::open_file(io.clone(), ":memory:", false)?; + let limbo_conn = db.connect()?; + + for op in ops { + let sql = op.to_string(); + + rusqlite_conn + .execute(&sql, ()) + .inspect_err(|_| { + dbg!(&sql); + }) + .unwrap(); + + limbo_conn + .execute(&sql) + .inspect_err(|_| { + dbg!(&sql); + }) + .unwrap() + } + + Ok(Corpus::Keep) +} + +fuzz_target!(|ops: Ops| -> Corpus { do_fuzz(ops).unwrap_or(Corpus::Keep) }); diff --git a/testing/alter_table.test b/testing/alter_table.test index 65a397c9b..3d3b56053 100755 --- a/testing/alter_table.test +++ b/testing/alter_table.test @@ -3,9 +3,114 @@ set testdir [file dirname $argv0] source $testdir/tester.tcl -# ALTER TABLE _ RENAME TO _ do_execsql_test_on_specific_db {:memory:} alter-table-rename-table { - CREATE TABLE t1(x INTEGER PRIMARY KEY); + CREATE TABLE t1(x INTEGER PRIMARY KEY, u UNIQUE); ALTER TABLE t1 RENAME TO t2; - SELECT tbl_name FROM sqlite_schema; + SELECT name FROM sqlite_schema WHERE type = 'table'; } { "t2" } + +do_execsql_test_on_specific_db {:memory:} alter-table-rename-column { + CREATE TABLE t(a); + CREATE INDEX i ON t(a); + ALTER TABLE t RENAME a TO b; + SELECT sql FROM sqlite_schema; +} { + "CREATE TABLE t(b)" + "CREATE INDEX i ON t(b)" +} + +do_execsql_test_on_specific_db {:memory:} alter-table-add-column { + CREATE TABLE t(a); + INSERT INTO t VALUES (1); + SELECT * FROM t; + + ALTER TABLE t ADD b; + SELECT sql FROM sqlite_schema; + SELECT * FROM t; +} { + "1" + "CREATE TABLE t(a, b)" + "1|" +} + +do_execsql_test_on_specific_db {:memory:} alter-table-add-column-typed { + CREATE TABLE t(a); + ALTER TABLE t ADD b DEFAULT 0; + + SELECT sql FROM sqlite_schema; + + INSERT INTO t (a) VALUES (1); + SELECT * FROM t; +} { + "CREATE TABLE t(a, b DEFAULT 0)" + "1|0" +} + +do_execsql_test_on_specific_db {:memory:} alter-table-add-column-default { + CREATE TABLE test(a); + INSERT INTO test VALUES (1), (2), (3); + + ALTER TABLE test ADD b DEFAULT 0.1; + ALTER TABLE test ADD c DEFAULT 'hello'; + SELECT * FROM test; + + CREATE INDEX idx ON test (b); + SELECT b, c FROM test WHERE b = 0.1; + + ALTER TABLE test DROP a; + SELECT * FROM test; + +} { + "1|0.1|hello" + "2|0.1|hello" + "3|0.1|hello" + + "0.1|hello" + "0.1|hello" + "0.1|hello" + + "0.1|hello" + "0.1|hello" + "0.1|hello" +} + +do_execsql_test_on_specific_db {:memory:} alter-table-drop-column { + CREATE TABLE t(a, b); + INSERT INTO t VALUES (1, 1), (2, 2), (3, 3); + SELECT * FROM t; + + ALTER TABLE t DROP b; + SELECT sql FROM sqlite_schema; + + SELECT * FROM t; +} { + "1|1" + "2|2" + "3|3" + + "CREATE TABLE t(a)" + + "1" + "2" + "3" +} + +do_execsql_test_in_memory_any_error fail-alter-table-drop-unique-column { + CREATE TABLE t(a, b UNIQUE); + ALTER TABLE t DROP b; +} + +do_execsql_test_in_memory_any_error fail-alter-table-drop-unique-column-constraint { + CREATE TABLE t(a, b, UNIQUE (b)); + ALTER TABLE t DROP b; +} + +do_execsql_test_in_memory_any_error fail-alter-table-drop-primary-key-column { + CREATE TABLE t(a PRIMARY KEY, b); + ALTER TABLE t DROP a; +} + +do_execsql_test_in_memory_any_error fail-alter-table-drop-primary-key-column-constrait { + CREATE TABLE t(a, b, PRIMARY KEY (a)); + ALTER TABLE t DROP a; +} diff --git a/vendored/sqlite3-parser/src/lexer/sql/test.rs b/vendored/sqlite3-parser/src/lexer/sql/test.rs index c0923403b..7fb315c52 100644 --- a/vendored/sqlite3-parser/src/lexer/sql/test.rs +++ b/vendored/sqlite3-parser/src/lexer/sql/test.rs @@ -280,14 +280,6 @@ fn alter_add_column_unique() { ); } -#[test] -fn alter_rename_same() { - expect_parser_err_msg( - b"ALTER TABLE t RENAME TO t", - "there is already another table or index with this name: t", - ); -} - #[test] fn natural_join_on() { expect_parser_err_msg( diff --git a/vendored/sqlite3-parser/src/parser/ast/check.rs b/vendored/sqlite3-parser/src/parser/ast/check.rs index 3df2c1c97..cbe2cacc7 100644 --- a/vendored/sqlite3-parser/src/parser/ast/check.rs +++ b/vendored/sqlite3-parser/src/parser/ast/check.rs @@ -104,16 +104,8 @@ impl Stmt { pub fn check(&self) -> Result<(), ParserError> { match self { Self::AlterTable(alter_table) => { - let (old_name, body) = &**alter_table; + let (_, body) = &**alter_table; match body { - AlterTableBody::RenameTo(new_name) => { - if *new_name == old_name.name { - return Err(custom_err!( - "there is already another table or index with this name: {}", - new_name - )); - } - } AlterTableBody::AddColumn(cd) => { for c in cd { if let ColumnConstraint::PrimaryKey { .. } = c { diff --git a/vendored/sqlite3-parser/src/parser/ast/fmt.rs b/vendored/sqlite3-parser/src/parser/ast/fmt.rs index 3c264d607..6598e71d7 100644 --- a/vendored/sqlite3-parser/src/parser/ast/fmt.rs +++ b/vendored/sqlite3-parser/src/parser/ast/fmt.rs @@ -46,6 +46,45 @@ impl TokenStream for FmtTokenStream<'_, '_> { } } +struct WriteTokenStream<'a, T: fmt::Write> { + write: &'a mut T, + spaced: bool, +} + +impl TokenStream for WriteTokenStream<'_, T> { + type Error = fmt::Error; + + fn append(&mut self, ty: TokenType, value: Option<&str>) -> fmt::Result { + if !self.spaced { + match ty { + TK_COMMA | TK_SEMI | TK_RP | TK_DOT => {} + _ => { + self.write.write_char(' ')?; + self.spaced = true; + } + }; + } + if ty == TK_BLOB { + self.write.write_char('X')?; + self.write.write_char('\'')?; + if let Some(str) = value { + self.write.write_str(str)?; + } + return self.write.write_char('\''); + } else if let Some(str) = ty.as_str() { + self.write.write_str(str)?; + self.spaced = ty == TK_LP || ty == TK_DOT; // str should not be whitespace + } + if let Some(str) = value { + // trick for pretty-print + self.spaced = str.bytes().all(|b| b.is_ascii_whitespace()); + self.write.write_str(str) + } else { + Ok(()) + } + } +} + /// Stream of token pub trait TokenStream { /// Potential error raised @@ -63,6 +102,19 @@ pub trait ToTokens { let mut s = FmtTokenStream { f, spaced: true }; self.to_tokens(&mut s) } + /// Format AST node to string + fn format(&self) -> Result { + let mut s = String::new(); + + let mut w = WriteTokenStream { + write: &mut s, + spaced: true, + }; + + self.to_tokens(&mut w)?; + + Ok(s) + } } impl ToTokens for &T { @@ -77,18 +129,6 @@ impl ToTokens for String { } } -/* FIXME: does not work, find why -impl Display for dyn ToTokens { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut s = FmtTokenStream { f, spaced: true }; - match self.to_tokens(&mut s) { - Err(_) => Err(fmt::Error), - Ok(()) => Ok(()), - } - } -} -*/ - impl ToTokens for Cmd { fn to_tokens(&self, s: &mut S) -> Result<(), S::Error> { match self {