mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-03 23:34:24 +01:00
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:
@@ -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> {
|
||||
|
||||
137
core/schema.rs
137
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<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
380
core/translate/alter.rs
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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[{}]={}.{}",
|
||||
|
||||
@@ -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
57
fuzz/Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
365
fuzz/fuzz_targets/schema.rs
Normal 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) });
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user