Merge 'Complete ALTER TABLE implementation' from Levy A.

Resolves #895
- [x] `ALTER TABLE _ ADD _`
- [x] `ALTER TABLE _ DROP _`
- [x] `ALTER TABLE _ RENAME _ TO _`

Reviewed-by: Preston Thorpe (@PThorpe92)
Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #1527
This commit is contained in:
Jussi Saurio
2025-06-13 10:23:43 +03:00
31 changed files with 1602 additions and 324 deletions

View File

@@ -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<ExternalFunc>),
}
@@ -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<Self, LimboError> {

View File

@@ -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<BTreeTable> {
@@ -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<ColumnDefinition> 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<CollationSeq>,
pub default: Option<Expr>,
}
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::<Vec<_>>();
@@ -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);
}

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

@@ -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<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}\""
)));
};
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
}
})
}

View File

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

View File

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

View File

@@ -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");

View File

@@ -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);
}
}
}

View File

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

View File

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

View File

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

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;
@@ -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<ProgramBuilder> {
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 {

View File

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

View File

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

View File

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

View File

@@ -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<Arc<Index>>,
pub parse_schema: ParseSchema,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View File

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

View File

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

View File

@@ -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<ProgramBuilder> {
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<ProgramBuilder> {
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<Plan> {
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,
}))
}

View File

@@ -93,6 +93,12 @@ impl Text {
}
}
impl AsRef<str> for Text {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Text {

View File

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

View File

@@ -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")
}

View File

@@ -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[{}]={}.{}",

View File

@@ -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<Value>,
},
TypeCheck {

57
fuzz/Cargo.lock generated
View File

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

View File

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

View File

@@ -184,7 +184,7 @@ fn do_fuzz(expr: Expr) -> Result<Corpus, Box<dyn Error>> {
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)?;

365
fuzz/fuzz_targets/schema.rs Normal file
View File

@@ -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<Self> {
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<ColumnDef>);
impl<'a> Arbitrary<'a> for Columns {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let len: usize = u.int_in_range(1..=4)?;
let mut out: Vec<ColumnDef> = 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<Id>,
}
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<Op>);
impl<'a> Arbitrary<'a> for Ops {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
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<Corpus, Box<dyn Error>> {
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) });

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -46,6 +46,45 @@ impl TokenStream for FmtTokenStream<'_, '_> {
}
}
struct WriteTokenStream<'a, T: fmt::Write> {
write: &'a mut T,
spaced: bool,
}
impl<T: fmt::Write> 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<String, fmt::Error> {
let mut s = String::new();
let mut w = WriteTokenStream {
write: &mut s,
spaced: true,
};
self.to_tokens(&mut w)?;
Ok(s)
}
}
impl<T: ?Sized + ToTokens> 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<S: TokenStream>(&self, s: &mut S) -> Result<(), S::Error> {
match self {