This commit is contained in:
Levy A.
2025-06-05 17:01:01 -03:00
parent dd0551b6f9
commit 3b36c3e771
3 changed files with 378 additions and 369 deletions

368
core/translate/alter.rs Normal file
View File

@@ -0,0 +1,368 @@
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: Box<(ast::QualifiedName, ast::AlterTableBody)>,
syms: &SymbolTable,
schema: &Schema,
mut program: ProgramBuilder,
) -> Result<ProgramBuilder> {
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}\""
)));
};
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 first_column = program.alloc_registers(5);
for i in 0..5 {
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(5);
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: 5,
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 first_column = program.alloc_registers(5);
for i in 0..5 {
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: 5,
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
}
})
}

View File

@@ -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;
@@ -31,31 +32,24 @@ pub(crate) mod update;
mod values;
use crate::fast_lock::SpinLock;
use crate::function::{AlterTableFunc, Func};
use crate::schema::{Column, Schema};
use crate::schema::Schema;
use crate::storage::pager::Pager;
use crate::storage::sqlite3_ondisk::DatabaseHeader;
use crate::translate::delete::translate_delete;
use crate::util::normalize_ident;
use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
use crate::vdbe::insn::{Insn, RegisterOrLiteral};
use crate::vdbe::Program;
use crate::{bail_parse_error, Connection, LimboError, Result, SymbolTable};
use emitter::TransactionMode;
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, 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;
use tracing::{instrument, Level};
use transaction::{translate_tx_begin, translate_tx_commit};
use update::{translate_update, translate_update_with_after};
use update::translate_update;
#[instrument(skip_all, level = Level::TRACE)]
pub fn translate(
@@ -111,359 +105,10 @@ pub fn translate_inner(
stmt: ast::Stmt,
syms: &SymbolTable,
query_mode: QueryMode,
mut program: ProgramBuilder,
program: ProgramBuilder,
) -> Result<ProgramBuilder> {
let program = match stmt {
ast::Stmt::AlterTable(a) => {
let (table_name, alter_table) = *a;
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();
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}\""
)));
};
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 first_column = program.alloc_registers(5);
for i in 0..5 {
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(5);
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: 5,
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 first_column = program.alloc_registers(5);
for i in 0..5 {
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: 5,
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::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)?,

View File

@@ -1,11 +1,9 @@
#![no_main]
use core::fmt;
use std::{error::Error, num::NonZero, sync::Arc};
use std::{error::Error, sync::Arc};
use arbitrary::Arbitrary;
use libfuzzer_sys::{fuzz_target, Corpus};
use limbo_core::{Value, IO as _};
use rusqlite::ffi::SQLITE_STATIC;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Id(String);
@@ -71,7 +69,6 @@ impl fmt::Display for ColumnDef {
let ColumnDef {
name,
r#type,
unique,
} = self;
write!(f, "{name} {type}",)?;
@@ -92,7 +89,6 @@ impl<'a> Arbitrary<'a> for Columns {
out.push(ColumnDef {
name: Id(format!("c{i}")),
r#type: u.arbitrary()?,
unique: u.arbitrary()?,
});
}
@@ -145,7 +141,7 @@ impl fmt::Display for IndexDef {
write!(f, "CREATE INDEX {name} ON {table}(")?;
for (i, column) in self.columns.iter().enumerate() {
for (i, column) in columns.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}