mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-12-23 08:21:09 +00:00
2532 lines
90 KiB
Rust
2532 lines
90 KiB
Rust
use std::sync::Arc;
|
|
use turso_parser::{
|
|
ast::{self, TableInternalId},
|
|
parser::Parser,
|
|
};
|
|
|
|
use crate::{
|
|
function::{AlterTableFunc, Func},
|
|
schema::{Column, ForeignKey, Table, RESERVED_TABLE_PREFIXES},
|
|
translate::{
|
|
emitter::Resolver,
|
|
expr::{walk_expr, WalkControl},
|
|
plan::{ColumnUsedMask, OuterQueryReference, TableReferences},
|
|
},
|
|
util::normalize_ident,
|
|
vdbe::{
|
|
builder::{CursorType, ProgramBuilder},
|
|
insn::{Cookie, Insn, RegisterOrLiteral},
|
|
},
|
|
vtab::VirtualTable,
|
|
LimboError, Result,
|
|
};
|
|
|
|
use super::{schema::SQLITE_TABLEID, update::translate_update_for_schema_change};
|
|
|
|
pub fn translate_alter_table(
|
|
alter: ast::AlterTable,
|
|
resolver: &Resolver,
|
|
mut program: ProgramBuilder,
|
|
connection: &Arc<crate::Connection>,
|
|
input: &str,
|
|
) -> Result<ProgramBuilder> {
|
|
program.begin_write_operation();
|
|
let ast::AlterTable {
|
|
name: table_name,
|
|
body: alter_table,
|
|
} = alter;
|
|
let table_name = table_name.name.as_str();
|
|
|
|
// Check if someone is trying to ALTER a system table
|
|
if crate::schema::is_system_table(table_name) {
|
|
crate::bail_parse_error!("table {} may not be modified", table_name);
|
|
}
|
|
|
|
if let ast::AlterTableBody::RenameTo(new_table_name) = &alter_table {
|
|
let normalized_new_name = normalize_ident(new_table_name.as_str());
|
|
|
|
if RESERVED_TABLE_PREFIXES
|
|
.iter()
|
|
.any(|prefix| normalized_new_name.starts_with(prefix))
|
|
{
|
|
crate::bail_parse_error!("Object name reserved for internal use: {}", new_table_name);
|
|
}
|
|
}
|
|
|
|
let table_indexes = resolver.schema.get_indices(table_name).collect::<Vec<_>>();
|
|
|
|
let Some(table) = resolver.schema.get_table(table_name) else {
|
|
return Err(LimboError::ParseError(format!(
|
|
"no such table: {table_name}"
|
|
)));
|
|
};
|
|
if let Some(tbl) = table.virtual_table() {
|
|
if let ast::AlterTableBody::RenameTo(new_name) = &alter_table {
|
|
let new_name_norm = normalize_ident(new_name.as_str());
|
|
return translate_rename_virtual_table(
|
|
program,
|
|
tbl,
|
|
table_name,
|
|
new_name_norm,
|
|
resolver,
|
|
);
|
|
}
|
|
}
|
|
let Some(original_btree) = table.btree() else {
|
|
crate::bail_parse_error!("ALTER TABLE is only supported for BTree tables");
|
|
};
|
|
|
|
// Check if this table has dependent materialized views
|
|
let dependent_views = resolver.schema.get_dependent_materialized_views(table_name);
|
|
if !dependent_views.is_empty() {
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot alter table \"{table_name}\": it has dependent materialized view(s): {}",
|
|
dependent_views.join(", ")
|
|
)));
|
|
}
|
|
|
|
let mut btree = (*original_btree).clone();
|
|
|
|
Ok(match alter_table {
|
|
ast::AlterTableBody::DropColumn(column_name) => {
|
|
let column_name = column_name.as_str();
|
|
|
|
// 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}\""))
|
|
})?;
|
|
|
|
// Column cannot be dropped if:
|
|
// The column is a PRIMARY KEY or part of one.
|
|
// The column has a UNIQUE constraint.
|
|
// The column is indexed.
|
|
// The column is named in the WHERE clause of a partial index.
|
|
// The column is named in a table or column CHECK constraint not associated with the column being dropped.
|
|
// The column is used in a foreign key constraint.
|
|
// The column is used in the expression of a generated column.
|
|
// The column appears in a trigger or view.
|
|
|
|
if column.primary_key() {
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot drop column \"{column_name}\": PRIMARY KEY"
|
|
)));
|
|
}
|
|
|
|
if column.unique()
|
|
|| btree.unique_sets.iter().any(|set| {
|
|
set.columns
|
|
.iter()
|
|
.any(|(name, _)| name == &normalize_ident(column_name))
|
|
})
|
|
{
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot drop column \"{column_name}\": UNIQUE"
|
|
)));
|
|
}
|
|
|
|
for index in table_indexes.iter() {
|
|
// Referenced in regular index
|
|
let maybe_indexed_col = index
|
|
.columns
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, col)| col.pos_in_table == dropped_index);
|
|
if let Some((pos_in_index, indexed_col)) = maybe_indexed_col {
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot drop column \"{column_name}\": it is referenced in the index {}; position in index is {pos_in_index}, position in table is {}",
|
|
index.name, indexed_col.pos_in_table
|
|
)));
|
|
}
|
|
// Referenced in partial index
|
|
if index.where_clause.is_some() {
|
|
let mut table_references = TableReferences::new(
|
|
vec![],
|
|
vec![OuterQueryReference {
|
|
identifier: table_name.to_string(),
|
|
internal_id: TableInternalId::from(0),
|
|
table: Table::BTree(Arc::new(btree.clone())),
|
|
col_used_mask: ColumnUsedMask::default(),
|
|
}],
|
|
);
|
|
let where_copy = index
|
|
.bind_where_expr(Some(&mut table_references), connection)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(
|
|
"index where clause unexpectedly missing".to_string(),
|
|
)
|
|
})?;
|
|
let mut column_referenced = false;
|
|
walk_expr(
|
|
&where_copy,
|
|
&mut |e: &ast::Expr| -> crate::Result<WalkControl> {
|
|
if let ast::Expr::Column {
|
|
table,
|
|
column: column_index,
|
|
..
|
|
} = e
|
|
{
|
|
if *table == TableInternalId::from(0)
|
|
&& *column_index == dropped_index
|
|
{
|
|
column_referenced = true;
|
|
return Ok(WalkControl::SkipChildren);
|
|
}
|
|
}
|
|
Ok(WalkControl::Continue)
|
|
},
|
|
)?;
|
|
if column_referenced {
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot drop column \"{column_name}\": indexed"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: check usage in CHECK constraint when implemented
|
|
// TODO: check usage in foreign key constraint when implemented
|
|
// TODO: check usage in generated column when implemented
|
|
|
|
// References in VIEWs are checked in the VDBE layer op_drop_column instruction.
|
|
|
|
// Check if any trigger owned by this table references the column being dropped
|
|
for trigger in resolver.schema.get_triggers_for_table(table_name) {
|
|
if trigger_references_column(trigger, &btree, column_name)? {
|
|
return Err(LimboError::ParseError(format!(
|
|
"cannot drop column \"{column_name}\": it is referenced in trigger {}",
|
|
trigger.name
|
|
)));
|
|
}
|
|
}
|
|
|
|
btree.columns.remove(dropped_index);
|
|
|
|
let sql = btree.to_sql().replace('\'', "''");
|
|
|
|
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 cmd = parser.next_cmd().map_err(|e| {
|
|
LimboError::ParseError(format!("failed to parse generated UPDATE statement: {e}"))
|
|
})?;
|
|
let Some(ast::Cmd::Stmt(ast::Stmt::Update(update))) = cmd else {
|
|
return Err(LimboError::ParseError(
|
|
"generated UPDATE statement did not parse as expected".to_string(),
|
|
));
|
|
};
|
|
|
|
translate_update_for_schema_change(
|
|
update,
|
|
resolver,
|
|
program,
|
|
connection,
|
|
input,
|
|
|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(CursorType::BTreeTable(original_btree));
|
|
|
|
program.emit_insn(Insn::OpenWrite {
|
|
cursor_id,
|
|
root_page: RegisterOrLiteral::Literal(root_page),
|
|
db: 0,
|
|
});
|
|
|
|
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_or_rowid(cursor_id, i, iter);
|
|
|
|
iter += 1;
|
|
}
|
|
|
|
let record = program.alloc_register();
|
|
|
|
let affinity_str = btree
|
|
.columns
|
|
.iter()
|
|
.map(|col| col.affinity().aff_mask())
|
|
.collect::<String>();
|
|
|
|
program.emit_insn(Insn::MakeRecord {
|
|
start_reg: first_column,
|
|
count: column_count,
|
|
dest_reg: record,
|
|
index_name: None,
|
|
affinity_str: Some(affinity_str),
|
|
});
|
|
|
|
program.emit_insn(Insn::Insert {
|
|
cursor: cursor_id,
|
|
key_reg: rowid,
|
|
record_reg: record,
|
|
flag: crate::vdbe::insn::InsertFlags(0),
|
|
table_name: table_name.clone(),
|
|
});
|
|
});
|
|
|
|
program.emit_insn(Insn::SetCookie {
|
|
db: 0,
|
|
cookie: Cookie::SchemaVersion,
|
|
value: resolver.schema.schema_version as i32 + 1,
|
|
p5: 0,
|
|
});
|
|
|
|
program.emit_insn(Insn::DropColumn {
|
|
table: table_name,
|
|
column_index: dropped_index,
|
|
})
|
|
},
|
|
)?
|
|
}
|
|
ast::AlterTableBody::AddColumn(col_def) => {
|
|
if col_def
|
|
.constraints
|
|
.iter()
|
|
.any(|c| matches!(c.constraint, ast::ColumnConstraint::Generated { .. }))
|
|
{
|
|
return Err(LimboError::ParseError(
|
|
"Alter table does not support adding generated columns".to_string(),
|
|
));
|
|
}
|
|
let constraints = col_def.constraints.clone();
|
|
let column = Column::from(&col_def);
|
|
|
|
if let Some(default) = &column.default {
|
|
if !matches!(
|
|
default.as_ref(),
|
|
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(),
|
|
));
|
|
}
|
|
}
|
|
|
|
let new_column_name = column.name.as_ref().ok_or_else(|| {
|
|
LimboError::ParseError(
|
|
"column name is missing in ALTER TABLE ADD COLUMN".to_string(),
|
|
)
|
|
})?;
|
|
if btree.get_column(new_column_name).is_some() {
|
|
return Err(LimboError::ParseError(
|
|
"duplicate column name: ".to_string() + new_column_name,
|
|
));
|
|
}
|
|
|
|
// TODO: All quoted ids will be quoted with `[]`, we should store some info from the parsed AST
|
|
btree.columns.push(column.clone());
|
|
|
|
// Add foreign key constraints to the btree table
|
|
for constraint in constraints {
|
|
if let ast::ColumnConstraint::ForeignKey {
|
|
clause,
|
|
defer_clause,
|
|
} = constraint.constraint
|
|
{
|
|
let fk = ForeignKey {
|
|
parent_table: normalize_ident(clause.tbl_name.as_str()),
|
|
parent_columns: clause
|
|
.columns
|
|
.iter()
|
|
.map(|c| normalize_ident(c.col_name.as_str()))
|
|
.collect(),
|
|
on_delete: clause
|
|
.args
|
|
.iter()
|
|
.find_map(|arg| {
|
|
if let ast::RefArg::OnDelete(act) = arg {
|
|
Some(*act)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or(ast::RefAct::NoAction),
|
|
on_update: clause
|
|
.args
|
|
.iter()
|
|
.find_map(|arg| {
|
|
if let ast::RefArg::OnUpdate(act) = arg {
|
|
Some(*act)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or(ast::RefAct::NoAction),
|
|
child_columns: vec![new_column_name.to_string()],
|
|
deferred: match defer_clause {
|
|
Some(d) => {
|
|
d.deferrable
|
|
&& matches!(
|
|
d.init_deferred,
|
|
Some(ast::InitDeferredPred::InitiallyDeferred)
|
|
)
|
|
}
|
|
None => false,
|
|
},
|
|
};
|
|
btree.foreign_keys.push(Arc::new(fk));
|
|
}
|
|
}
|
|
|
|
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 cmd = parser.next_cmd().map_err(|e| {
|
|
LimboError::ParseError(format!("failed to parse generated UPDATE statement: {e}"))
|
|
})?;
|
|
let Some(ast::Cmd::Stmt(ast::Stmt::Update(update))) = cmd else {
|
|
return Err(LimboError::ParseError(
|
|
"generated UPDATE statement did not parse as expected".to_string(),
|
|
));
|
|
};
|
|
|
|
translate_update_for_schema_change(
|
|
update,
|
|
resolver,
|
|
program,
|
|
connection,
|
|
input,
|
|
|program| {
|
|
program.emit_insn(Insn::SetCookie {
|
|
db: 0,
|
|
cookie: Cookie::SchemaVersion,
|
|
value: resolver.schema.schema_version as i32 + 1,
|
|
p5: 0,
|
|
});
|
|
program.emit_insn(Insn::AddColumn {
|
|
table: table_name.to_owned(),
|
|
column,
|
|
});
|
|
},
|
|
)?
|
|
}
|
|
ast::AlterTableBody::RenameTo(new_name) => {
|
|
let new_name = new_name.as_str();
|
|
|
|
if resolver.schema.get_table(new_name).is_some()
|
|
|| resolver
|
|
.schema
|
|
.indexes
|
|
.values()
|
|
.flatten()
|
|
.any(|index| index.name == normalize_ident(new_name))
|
|
{
|
|
return Err(LimboError::ParseError(format!(
|
|
"there is already another table or index with this name: {new_name}"
|
|
)));
|
|
};
|
|
|
|
let sqlite_schema =
|
|
resolver
|
|
.schema
|
|
.get_btree_table(SQLITE_TABLEID)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(
|
|
"sqlite_schema table not found in schema".to_string(),
|
|
)
|
|
})?;
|
|
|
|
let cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(sqlite_schema.clone()));
|
|
|
|
program.emit_insn(Insn::OpenWrite {
|
|
cursor_id,
|
|
root_page: RegisterOrLiteral::Literal(sqlite_schema.root_page),
|
|
db: 0,
|
|
});
|
|
|
|
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_or_rowid(cursor_id, i, first_column + i);
|
|
}
|
|
|
|
program.emit_string8_new_reg(table_name.to_string());
|
|
program.mark_last_insn_constant();
|
|
|
|
program.emit_string8_new_reg(new_name.to_string());
|
|
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,
|
|
affinity_str: None,
|
|
});
|
|
|
|
program.emit_insn(Insn::Insert {
|
|
cursor: cursor_id,
|
|
key_reg: rowid,
|
|
record_reg: record,
|
|
flag: crate::vdbe::insn::InsertFlags(0),
|
|
table_name: table_name.to_string(),
|
|
});
|
|
});
|
|
|
|
program.emit_insn(Insn::SetCookie {
|
|
db: 0,
|
|
cookie: Cookie::SchemaVersion,
|
|
value: resolver.schema.schema_version as i32 + 1,
|
|
p5: 0,
|
|
});
|
|
|
|
program.emit_insn(Insn::RenameTable {
|
|
from: table_name.to_owned(),
|
|
to: new_name.to_owned(),
|
|
});
|
|
|
|
program
|
|
}
|
|
body @ (ast::AlterTableBody::AlterColumn { .. }
|
|
| ast::AlterTableBody::RenameColumn { .. }) => {
|
|
let from;
|
|
let definition;
|
|
let col_name;
|
|
let rename;
|
|
|
|
match body {
|
|
ast::AlterTableBody::AlterColumn { old, new } => {
|
|
from = old;
|
|
definition = new;
|
|
col_name = definition.col_name.clone();
|
|
rename = false;
|
|
}
|
|
ast::AlterTableBody::RenameColumn { old, new } => {
|
|
from = old;
|
|
definition = ast::ColumnDefinition {
|
|
col_name: new.clone(),
|
|
col_type: None,
|
|
constraints: vec![],
|
|
};
|
|
col_name = new;
|
|
rename = true;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
let from = from.as_str();
|
|
let col_name = col_name.as_str();
|
|
|
|
let Some((column_index, _)) = btree.get_column(from) else {
|
|
return Err(LimboError::ParseError(format!(
|
|
"no such column: \"{from}\""
|
|
)));
|
|
};
|
|
|
|
if btree.get_column(col_name).is_some() {
|
|
return Err(LimboError::ParseError(format!(
|
|
"duplicate column name: \"{col_name}\""
|
|
)));
|
|
};
|
|
|
|
if definition
|
|
.constraints
|
|
.iter()
|
|
.any(|c| matches!(c.constraint, ast::ColumnConstraint::PrimaryKey { .. }))
|
|
{
|
|
return Err(LimboError::ParseError(
|
|
"PRIMARY KEY constraint cannot be altered".to_string(),
|
|
));
|
|
}
|
|
|
|
if definition
|
|
.constraints
|
|
.iter()
|
|
.any(|c| matches!(c.constraint, ast::ColumnConstraint::Unique { .. }))
|
|
{
|
|
return Err(LimboError::ParseError(
|
|
"UNIQUE constraint cannot be altered".to_string(),
|
|
));
|
|
}
|
|
|
|
let is_making_column_generated = definition
|
|
.constraints
|
|
.iter()
|
|
.any(|c| matches!(c.constraint, ast::ColumnConstraint::Generated { .. }));
|
|
|
|
if is_making_column_generated {
|
|
let non_generated_count = btree
|
|
.columns
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(idx, col)| *idx != column_index && col.generated.is_none())
|
|
.count();
|
|
|
|
if non_generated_count == 0 {
|
|
return Err(LimboError::ParseError(
|
|
"must have at least one non-generated column".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
// If renaming, rewrite trigger SQL for all triggers that reference this column
|
|
// We'll collect the triggers to rewrite and update them in sqlite_schema
|
|
let mut triggers_to_rewrite: Vec<(String, String)> = Vec::new();
|
|
if rename {
|
|
// Find all triggers that might reference this column
|
|
let target_table_name_norm = normalize_ident(table_name);
|
|
for trigger in resolver.schema.triggers.values().flatten() {
|
|
let trigger_table_name_norm = normalize_ident(&trigger.table_name);
|
|
|
|
// SQLite fails RENAME COLUMN if a trigger's WHEN clause references the column
|
|
// or if trigger commands contain qualified references to the trigger table (e.g., t.x)
|
|
if trigger_table_name_norm == target_table_name_norm {
|
|
let column_name_norm = normalize_ident(from);
|
|
let trigger_table = resolver
|
|
.schema
|
|
.get_btree_table(&trigger_table_name_norm)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(format!(
|
|
"trigger table not found: {trigger_table_name_norm}"
|
|
))
|
|
})?;
|
|
|
|
// Check WHEN clause
|
|
if let Some(ref when_expr) = trigger.when_clause {
|
|
if expr_references_trigger_column(
|
|
when_expr,
|
|
&trigger_table,
|
|
&trigger_table_name_norm,
|
|
&column_name_norm,
|
|
)? {
|
|
return Err(LimboError::ParseError(format!(
|
|
"error in trigger {}: no such column: {}",
|
|
trigger.name, from
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Check trigger commands for qualified references to trigger table (e.g., t.x)
|
|
// SQLite fails RENAME COLUMN if triggers contain qualified references like t.x
|
|
if check_trigger_has_qualified_ref_to_column(
|
|
trigger,
|
|
&trigger_table,
|
|
&trigger_table_name_norm,
|
|
&column_name_norm,
|
|
)? {
|
|
return Err(LimboError::ParseError(format!(
|
|
"error in trigger {}: no such column: {}",
|
|
trigger.name, from
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Check if trigger references the column being renamed
|
|
// This includes:
|
|
// 1. References from the trigger's owning table (NEW.x, OLD.x, unqualified x)
|
|
// 2. References in INSERT column lists targeting the table being renamed
|
|
// 3. References in UPDATE SET column lists targeting the table being renamed
|
|
let mut needs_rewrite = false;
|
|
|
|
if trigger_table_name_norm == target_table_name_norm {
|
|
// Trigger is on the table being renamed - check for references
|
|
let trigger_table = resolver
|
|
.schema
|
|
.get_btree_table(&trigger_table_name_norm)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(format!(
|
|
"trigger table not found: {trigger_table_name_norm}"
|
|
))
|
|
})?;
|
|
|
|
needs_rewrite = trigger_references_column(trigger, &trigger_table, from)?;
|
|
}
|
|
|
|
// Also check if trigger references the column in INSERT/UPDATE targeting other tables
|
|
// Parse the trigger to check INSERT column lists and UPDATE SET column lists
|
|
if !needs_rewrite {
|
|
needs_rewrite = trigger_references_column_in_other_tables(
|
|
trigger,
|
|
&target_table_name_norm,
|
|
from,
|
|
)?;
|
|
}
|
|
|
|
if needs_rewrite {
|
|
match rewrite_trigger_sql_for_column_rename(
|
|
&trigger.sql,
|
|
table_name,
|
|
from,
|
|
col_name,
|
|
resolver,
|
|
) {
|
|
Ok(new_sql) => {
|
|
triggers_to_rewrite.push((trigger.name.clone(), new_sql));
|
|
}
|
|
Err(e) => {
|
|
// If we can't rewrite the trigger, fail the ALTER TABLE operation
|
|
return Err(LimboError::ParseError(format!(
|
|
"error in trigger {} after rename column: {}",
|
|
trigger.name, e
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let sqlite_schema =
|
|
resolver
|
|
.schema
|
|
.get_btree_table(SQLITE_TABLEID)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(
|
|
"sqlite_schema table not found in schema".to_string(),
|
|
)
|
|
})?;
|
|
|
|
let cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(sqlite_schema.clone()));
|
|
|
|
program.emit_insn(Insn::OpenWrite {
|
|
cursor_id,
|
|
root_page: RegisterOrLiteral::Literal(sqlite_schema.root_page),
|
|
db: 0,
|
|
});
|
|
|
|
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_or_rowid(cursor_id, i, first_column + i);
|
|
}
|
|
|
|
program.emit_string8_new_reg(table_name.to_string());
|
|
program.mark_last_insn_constant();
|
|
|
|
program.emit_string8_new_reg(from.to_string());
|
|
program.mark_last_insn_constant();
|
|
|
|
program.emit_string8_new_reg(definition.to_string());
|
|
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(if rename {
|
|
AlterTableFunc::RenameColumn
|
|
} else {
|
|
AlterTableFunc::AlterColumn
|
|
}),
|
|
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,
|
|
affinity_str: None,
|
|
});
|
|
|
|
program.emit_insn(Insn::Insert {
|
|
cursor: cursor_id,
|
|
key_reg: rowid,
|
|
record_reg: record,
|
|
flag: crate::vdbe::insn::InsertFlags(0),
|
|
table_name: table_name.to_string(),
|
|
});
|
|
});
|
|
|
|
// Update trigger SQL for renamed columns
|
|
for (trigger_name, new_sql) in triggers_to_rewrite {
|
|
let escaped_sql = new_sql.replace('\'', "''");
|
|
let update_stmt = format!(
|
|
r#"
|
|
UPDATE {SQLITE_TABLEID}
|
|
SET sql = '{escaped_sql}'
|
|
WHERE name = '{trigger_name}' COLLATE NOCASE AND type = 'trigger'
|
|
"#,
|
|
);
|
|
|
|
let mut parser = Parser::new(update_stmt.as_bytes());
|
|
let cmd = parser.next_cmd().map_err(|e| {
|
|
LimboError::ParseError(format!(
|
|
"failed to parse trigger update SQL for {trigger_name}: {e}"
|
|
))
|
|
})?;
|
|
let Some(ast::Cmd::Stmt(ast::Stmt::Update(update))) = cmd else {
|
|
return Err(LimboError::ParseError(format!(
|
|
"failed to parse trigger update SQL for {trigger_name}",
|
|
)));
|
|
};
|
|
|
|
program = translate_update_for_schema_change(
|
|
update,
|
|
resolver,
|
|
program,
|
|
connection,
|
|
input,
|
|
|_program| {},
|
|
)?;
|
|
}
|
|
|
|
program.emit_insn(Insn::SetCookie {
|
|
db: 0,
|
|
cookie: Cookie::SchemaVersion,
|
|
value: resolver.schema.schema_version as i32 + 1,
|
|
p5: 0,
|
|
});
|
|
program.emit_insn(Insn::AlterColumn {
|
|
table: table_name.to_owned(),
|
|
column_index,
|
|
definition,
|
|
rename,
|
|
});
|
|
|
|
program
|
|
}
|
|
})
|
|
}
|
|
|
|
fn translate_rename_virtual_table(
|
|
mut program: ProgramBuilder,
|
|
vtab: Arc<VirtualTable>,
|
|
old_name: &str,
|
|
new_name_norm: String,
|
|
resolver: &Resolver,
|
|
) -> Result<ProgramBuilder> {
|
|
program.begin_write_operation();
|
|
let vtab_cur = program.alloc_cursor_id(CursorType::VirtualTable(vtab.clone()));
|
|
program.emit_insn(Insn::VOpen {
|
|
cursor_id: vtab_cur,
|
|
});
|
|
|
|
let new_name_reg = program.emit_string8_new_reg(new_name_norm.clone());
|
|
program.emit_insn(Insn::VRename {
|
|
cursor_id: vtab_cur,
|
|
new_name_reg,
|
|
});
|
|
// Rewrite sqlite_schema entry
|
|
let sqlite_schema = resolver
|
|
.schema
|
|
.get_btree_table(SQLITE_TABLEID)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError("sqlite_schema table not found in schema".to_string())
|
|
})?;
|
|
|
|
let schema_cur = program.alloc_cursor_id(CursorType::BTreeTable(sqlite_schema.clone()));
|
|
program.emit_insn(Insn::OpenWrite {
|
|
cursor_id: schema_cur,
|
|
root_page: RegisterOrLiteral::Literal(sqlite_schema.root_page),
|
|
db: 0,
|
|
});
|
|
|
|
program.cursor_loop(schema_cur, |program, rowid| {
|
|
let ncols = sqlite_schema.columns.len();
|
|
assert_eq!(ncols, 5);
|
|
|
|
let first_col = program.alloc_registers(ncols);
|
|
for i in 0..ncols {
|
|
program.emit_column_or_rowid(schema_cur, i, first_col + i);
|
|
}
|
|
|
|
program.emit_string8_new_reg(old_name.to_string());
|
|
program.mark_last_insn_constant();
|
|
|
|
program.emit_string8_new_reg(new_name_norm.clone());
|
|
program.mark_last_insn_constant();
|
|
|
|
let out = program.alloc_registers(ncols);
|
|
|
|
program.emit_insn(Insn::Function {
|
|
constant_mask: 0,
|
|
start_reg: first_col,
|
|
dest: out,
|
|
func: crate::function::FuncCtx {
|
|
func: Func::AlterTable(AlterTableFunc::RenameTable),
|
|
arg_count: 7,
|
|
},
|
|
});
|
|
|
|
let rec = program.alloc_register();
|
|
program.emit_insn(Insn::MakeRecord {
|
|
start_reg: out,
|
|
count: ncols,
|
|
dest_reg: rec,
|
|
index_name: None,
|
|
affinity_str: None,
|
|
});
|
|
|
|
program.emit_insn(Insn::Insert {
|
|
cursor: schema_cur,
|
|
key_reg: rowid,
|
|
record_reg: rec,
|
|
flag: crate::vdbe::insn::InsertFlags(0),
|
|
table_name: old_name.to_string(),
|
|
});
|
|
});
|
|
|
|
// Bump schema cookie
|
|
program.emit_insn(Insn::SetCookie {
|
|
db: 0,
|
|
cookie: Cookie::SchemaVersion,
|
|
value: resolver.schema.schema_version as i32 + 1,
|
|
p5: 0,
|
|
});
|
|
|
|
program.emit_insn(Insn::RenameTable {
|
|
from: old_name.to_owned(),
|
|
to: new_name_norm,
|
|
});
|
|
|
|
program.emit_insn(Insn::Close {
|
|
cursor_id: schema_cur,
|
|
});
|
|
program.emit_insn(Insn::Close {
|
|
cursor_id: vtab_cur,
|
|
});
|
|
|
|
Ok(program)
|
|
}
|
|
|
|
/* Triggers must be rewritten when a column is renamed, and DROP COLUMN on table T must be forbidden if any trigger on T references the column.
|
|
Here are some helpers related to that: */
|
|
|
|
/// Check if a trigger contains qualified references to a specific column in its owning table.
|
|
/// This is used to detect cases like `t.x` in a trigger on table `t`, which SQLite fails on RENAME COLUMN.
|
|
/// Only checks for qualified table references (e.g., t.x), not NEW.x, OLD.x, or unqualified x.
|
|
fn check_trigger_has_qualified_ref_to_column(
|
|
trigger: &crate::schema::Trigger,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name_norm: &str,
|
|
column_name_norm: &str,
|
|
) -> Result<bool> {
|
|
use crate::translate::expr::walk_expr;
|
|
use std::cell::Cell;
|
|
|
|
let found = Cell::new(false);
|
|
|
|
// Helper callback to check for qualified references to trigger table
|
|
let mut check_qualified_ref = |e: &ast::Expr| -> Result<crate::translate::expr::WalkControl> {
|
|
if let ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) = e {
|
|
let ns_norm = normalize_ident(ns.as_str());
|
|
let col_norm = normalize_ident(col.as_str());
|
|
// Only check for qualified refs to trigger table (not NEW/OLD)
|
|
if !ns_norm.eq_ignore_ascii_case("new")
|
|
&& !ns_norm.eq_ignore_ascii_case("old")
|
|
&& ns_norm == trigger_table_name_norm
|
|
&& col_norm == column_name_norm
|
|
&& trigger_table.get_column(&col_norm).is_some()
|
|
{
|
|
found.set(true);
|
|
return Ok(crate::translate::expr::WalkControl::SkipChildren);
|
|
}
|
|
}
|
|
Ok(crate::translate::expr::WalkControl::Continue)
|
|
};
|
|
|
|
for cmd in &trigger.commands {
|
|
match cmd {
|
|
ast::TriggerCmd::Update {
|
|
sets, where_clause, ..
|
|
} => {
|
|
for set in sets {
|
|
walk_expr(&set.expr, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
if let Some(ref where_expr) = where_clause {
|
|
walk_expr(where_expr, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
ast::TriggerCmd::Insert { select, .. } | ast::TriggerCmd::Select(select) => {
|
|
// Walk through all expressions in SELECT checking for qualified refs
|
|
if let Some(ref with_clause) = select.with {
|
|
for cte in &with_clause.ctes {
|
|
if walk_one_select_expressions_for_qualified_ref(
|
|
&cte.select.body.select,
|
|
&mut check_qualified_ref,
|
|
)? {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
if walk_one_select_expressions_for_qualified_ref(
|
|
&select.body.select,
|
|
&mut check_qualified_ref,
|
|
)? {
|
|
return Ok(true);
|
|
}
|
|
for compound in &select.body.compounds {
|
|
if walk_one_select_expressions_for_qualified_ref(
|
|
&compound.select,
|
|
&mut check_qualified_ref,
|
|
)? {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
for sorted_col in &select.order_by {
|
|
walk_expr(&sorted_col.expr, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
if let Some(ref limit) = select.limit {
|
|
walk_expr(&limit.expr, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
if let Some(ref offset) = limit.offset {
|
|
walk_expr(offset, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ast::TriggerCmd::Delete { where_clause, .. } => {
|
|
if let Some(ref where_expr) = where_clause {
|
|
walk_expr(where_expr, &mut check_qualified_ref)?;
|
|
if found.get() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Helper to walk OneSelect expressions checking for qualified references
|
|
/// Uses a callback that can mutate the found flag through a closure
|
|
fn walk_one_select_expressions_for_qualified_ref<F>(
|
|
one_select: &ast::OneSelect,
|
|
check_fn: &mut F,
|
|
) -> Result<bool>
|
|
where
|
|
F: FnMut(&ast::Expr) -> Result<crate::translate::expr::WalkControl>,
|
|
{
|
|
use crate::translate::expr::walk_expr;
|
|
|
|
match one_select {
|
|
ast::OneSelect::Select {
|
|
columns,
|
|
from,
|
|
where_clause,
|
|
group_by,
|
|
window_clause,
|
|
..
|
|
} => {
|
|
for col in columns {
|
|
if let ast::ResultColumn::Expr(expr, _) = col {
|
|
if matches!(
|
|
walk_expr(expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
if let Some(ref from_clause) = from {
|
|
// Walk FROM clause expressions manually using walk_expr
|
|
match from_clause.select.as_ref() {
|
|
ast::SelectTable::Select(select, _) => {
|
|
// Recursively check the subquery
|
|
if walk_one_select_expressions_for_qualified_ref(
|
|
&select.body.select,
|
|
check_fn,
|
|
)? {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
ast::SelectTable::TableCall(_, args, _) => {
|
|
for arg in args {
|
|
if matches!(
|
|
walk_expr(arg, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
for join in &from_clause.joins {
|
|
if let Some(ast::JoinConstraint::On(ref expr)) = join.constraint {
|
|
let skip_children = matches!(
|
|
walk_expr(expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
);
|
|
if skip_children {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(ref where_expr) = where_clause {
|
|
if matches!(
|
|
walk_expr(where_expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
if let Some(ref group_by) = group_by {
|
|
for expr in &group_by.exprs {
|
|
if matches!(
|
|
walk_expr(expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
if let Some(ref having_expr) = group_by.having {
|
|
if matches!(
|
|
walk_expr(having_expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
for window_def in window_clause {
|
|
for expr in &window_def.window.partition_by {
|
|
if matches!(
|
|
walk_expr(expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
for sorted_col in &window_def.window.order_by {
|
|
if matches!(
|
|
walk_expr(&sorted_col.expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ast::OneSelect::Values(values) => {
|
|
for row in values {
|
|
for expr in row {
|
|
if matches!(
|
|
walk_expr(expr, check_fn)?,
|
|
crate::translate::expr::WalkControl::SkipChildren
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
/// Check if an expression references a specific column from a trigger's owning table.
|
|
/// Checks for NEW.column, OLD.column, unqualified column references, and qualified
|
|
/// references to the trigger table (e.g., t.x in a trigger on table t).
|
|
/// Returns true if the column is found, false otherwise.
|
|
fn expr_references_trigger_column(
|
|
expr: &ast::Expr,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
) -> Result<bool> {
|
|
use crate::translate::expr::walk_expr;
|
|
let trigger_table_name_norm = normalize_ident(trigger_table_name);
|
|
let mut found = false;
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<
|
|
crate::translate::expr::WalkControl,
|
|
> {
|
|
match e {
|
|
ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) => {
|
|
let ns_norm = normalize_ident(ns.as_str());
|
|
let col_norm = normalize_ident(col.as_str());
|
|
|
|
// Check NEW.column or OLD.column
|
|
if (ns_norm.eq_ignore_ascii_case("new") || ns_norm.eq_ignore_ascii_case("old"))
|
|
&& col_norm == *column_name_norm
|
|
{
|
|
found = true;
|
|
return Ok(crate::translate::expr::WalkControl::SkipChildren);
|
|
}
|
|
// Check qualified reference to trigger table (e.g., t.x)
|
|
if ns_norm == trigger_table_name_norm
|
|
&& col_norm == *column_name_norm
|
|
&& table.get_column(&col_norm).is_some()
|
|
{
|
|
found = true;
|
|
return Ok(crate::translate::expr::WalkControl::SkipChildren);
|
|
}
|
|
}
|
|
ast::Expr::Id(col) => {
|
|
// Unqualified column reference - check if it matches
|
|
let col_norm = normalize_ident(col.as_str());
|
|
if col_norm == *column_name_norm {
|
|
// Verify this column exists in the trigger's owning table
|
|
if table.get_column(&col_norm).is_some() {
|
|
found = true;
|
|
return Ok(crate::translate::expr::WalkControl::SkipChildren);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(crate::translate::expr::WalkControl::Continue)
|
|
})?;
|
|
Ok(found)
|
|
}
|
|
|
|
/// Check if a trigger references a specific column from its owning table.
|
|
/// Returns true if the column is referenced as old.x, new.x, or unqualified x.
|
|
fn trigger_references_column(
|
|
trigger: &crate::schema::Trigger,
|
|
table: &crate::schema::BTreeTable,
|
|
column_name: &str,
|
|
) -> Result<bool> {
|
|
let column_name_norm = normalize_ident(column_name);
|
|
let mut found = false;
|
|
|
|
// Check when_clause
|
|
if let Some(ref when_expr) = trigger.when_clause {
|
|
// Get trigger table name for checking qualified references
|
|
let trigger_table_name = normalize_ident(&trigger.table_name);
|
|
found = expr_references_trigger_column(
|
|
when_expr,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
)?;
|
|
}
|
|
|
|
if found {
|
|
return Ok(true);
|
|
}
|
|
|
|
// Check all trigger commands
|
|
// Note: We only check NEW.x, OLD.x, and unqualified x references in expressions.
|
|
// INSERT column lists and UPDATE SET column lists are NOT checked here because
|
|
// SQLite allows DROP COLUMN even when triggers reference columns in INSERT/UPDATE
|
|
// column lists targeting the owning table. The error only occurs when the trigger
|
|
// is actually executed.
|
|
for cmd in &trigger.commands {
|
|
match cmd {
|
|
ast::TriggerCmd::Update {
|
|
sets, where_clause, ..
|
|
} => {
|
|
// Check SET expressions (not column names in SET clause)
|
|
let trigger_table_name = normalize_ident(&trigger.table_name);
|
|
for set in sets {
|
|
if expr_references_trigger_column(
|
|
&set.expr,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
)? {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
// Check WHERE clause
|
|
if !found {
|
|
if let Some(ref where_expr) = where_clause {
|
|
found = expr_references_trigger_column(
|
|
where_expr,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
ast::TriggerCmd::Insert { select, .. } => {
|
|
// Check SELECT/VALUES expressions (not column names in INSERT clause)
|
|
let trigger_table_name = normalize_ident(&trigger.table_name);
|
|
walk_select_expressions(
|
|
select,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
&mut found,
|
|
)?;
|
|
}
|
|
ast::TriggerCmd::Delete { where_clause, .. } => {
|
|
if let Some(ref where_expr) = where_clause {
|
|
let trigger_table_name = normalize_ident(&trigger.table_name);
|
|
found = expr_references_trigger_column(
|
|
where_expr,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
)?;
|
|
}
|
|
}
|
|
ast::TriggerCmd::Select(select) => {
|
|
let trigger_table_name = normalize_ident(&trigger.table_name);
|
|
walk_select_expressions(
|
|
select,
|
|
table,
|
|
&trigger_table_name,
|
|
&column_name_norm,
|
|
&mut found,
|
|
)?;
|
|
}
|
|
}
|
|
if found {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(found)
|
|
}
|
|
|
|
/// Helper to check if an expression references a column.
|
|
/// Used as a callback within walk_expr, so it mutates a found flag.
|
|
/// Also checks for qualified references to the trigger table (e.g., t.x).
|
|
fn check_column_ref(
|
|
e: &ast::Expr,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
let trigger_table_name_norm = normalize_ident(trigger_table_name);
|
|
match e {
|
|
ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) => {
|
|
let ns_norm = normalize_ident(ns.as_str());
|
|
let col_norm = normalize_ident(col.as_str());
|
|
|
|
let new_or_old = (ns_norm.eq_ignore_ascii_case("new")
|
|
|| ns_norm.eq_ignore_ascii_case("old"))
|
|
&& col_norm == *column_name_norm;
|
|
let qualified_ref = ns_norm == trigger_table_name_norm
|
|
&& col_norm == *column_name_norm
|
|
&& table.get_column(&col_norm).is_some();
|
|
if new_or_old || qualified_ref {
|
|
*found = true;
|
|
}
|
|
}
|
|
ast::Expr::Id(col) => {
|
|
// Unqualified column reference - check if it matches
|
|
let col_norm = normalize_ident(col.as_str());
|
|
if col_norm == *column_name_norm {
|
|
// Verify this column exists in the trigger's owning table
|
|
if table.get_column(&col_norm).is_some() {
|
|
*found = true;
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if a trigger references a column from a table other than its owning table.
|
|
/// This checks INSERT column lists and UPDATE SET column lists that target the table being renamed.
|
|
fn trigger_references_column_in_other_tables(
|
|
trigger: &crate::schema::Trigger,
|
|
target_table_name: &str,
|
|
column_name: &str,
|
|
) -> Result<bool> {
|
|
let column_name_norm = normalize_ident(column_name);
|
|
let target_table_name_norm = normalize_ident(target_table_name);
|
|
|
|
// Check all trigger commands for INSERT/UPDATE targeting the table being renamed
|
|
for cmd in &trigger.commands {
|
|
match cmd {
|
|
ast::TriggerCmd::Insert {
|
|
tbl_name,
|
|
col_names,
|
|
..
|
|
} => {
|
|
// Check if INSERT targets the table being renamed
|
|
let insert_table_name_norm = normalize_ident(tbl_name.as_str());
|
|
if insert_table_name_norm == target_table_name_norm {
|
|
// Check if column name appears in INSERT column list
|
|
for col_name in col_names {
|
|
let col_norm = normalize_ident(col_name.as_str());
|
|
if col_norm == column_name_norm {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ast::TriggerCmd::Update { tbl_name, sets, .. } => {
|
|
// Check if UPDATE targets the table being renamed
|
|
let update_table_name_norm = normalize_ident(tbl_name.as_str());
|
|
if update_table_name_norm == target_table_name_norm {
|
|
// Check if column name appears in UPDATE SET column list
|
|
for set in sets {
|
|
for col_name in &set.col_names {
|
|
let col_norm = normalize_ident(col_name.as_str());
|
|
if col_norm == column_name_norm {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Walk through all expressions in a SELECT statement
|
|
fn walk_select_expressions(
|
|
select: &ast::Select,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
// Check WITH clause (CTEs)
|
|
if let Some(ref with_clause) = select.with {
|
|
for cte in &with_clause.ctes {
|
|
walk_select_expressions(
|
|
&cte.select,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check main SELECT body
|
|
walk_one_select_expressions(
|
|
&select.body.select,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
|
|
// Check compound SELECTs (UNION, EXCEPT, INTERSECT)
|
|
for compound in &select.body.compounds {
|
|
walk_one_select_expressions(
|
|
&compound.select,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check ORDER BY
|
|
for sorted_col in &select.order_by {
|
|
walk_expr(
|
|
&sorted_col.expr,
|
|
&mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
},
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check LIMIT
|
|
if let Some(ref limit) = select.limit {
|
|
walk_expr(&limit.expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
if let Some(ref offset) = limit.offset {
|
|
walk_expr(offset, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Walk through all expressions in a OneSelect
|
|
fn walk_one_select_expressions(
|
|
one_select: &ast::OneSelect,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
match one_select {
|
|
ast::OneSelect::Select {
|
|
columns,
|
|
from,
|
|
where_clause,
|
|
group_by,
|
|
window_clause,
|
|
..
|
|
} => {
|
|
// Check columns
|
|
for col in columns {
|
|
if let ast::ResultColumn::Expr(expr, _) = col {
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check FROM clause and JOIN conditions
|
|
if let Some(ref from_clause) = from {
|
|
walk_from_clause_expressions(
|
|
from_clause,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check WHERE clause
|
|
if let Some(ref where_expr) = where_clause {
|
|
walk_expr(where_expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check GROUP BY and HAVING
|
|
if let Some(ref group_by) = group_by {
|
|
for expr in &group_by.exprs {
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
if let Some(ref having_expr) = group_by.having {
|
|
walk_expr(having_expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check WINDOW clause
|
|
for window_def in window_clause {
|
|
walk_window_expressions(
|
|
&window_def.window,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
ast::OneSelect::Values(values) => {
|
|
for row in values {
|
|
for expr in row {
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Walk through expressions in a FROM clause (including JOIN conditions)
|
|
fn walk_from_clause_expressions(
|
|
from_clause: &ast::FromClause,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
// Check main table (could be a subquery)
|
|
walk_select_table_expressions(
|
|
&from_clause.select,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
|
|
// Check JOIN conditions
|
|
for join in &from_clause.joins {
|
|
walk_select_table_expressions(
|
|
&join.table,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
if let Some(ref constraint) = join.constraint {
|
|
match constraint {
|
|
ast::JoinConstraint::On(expr) => {
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
ast::JoinConstraint::Using(_) => {
|
|
// USING clause contains column names, not expressions
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Walk through expressions in a SelectTable (table, subquery, or table function)
|
|
fn walk_select_table_expressions(
|
|
select_table: &ast::SelectTable,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
match select_table {
|
|
ast::SelectTable::Select(select, _) => {
|
|
walk_select_expressions(select, table, trigger_table_name, column_name_norm, found)?;
|
|
}
|
|
ast::SelectTable::Sub(from_clause, _) => {
|
|
walk_from_clause_expressions(
|
|
from_clause,
|
|
table,
|
|
trigger_table_name,
|
|
column_name_norm,
|
|
found,
|
|
)?;
|
|
}
|
|
ast::SelectTable::TableCall(_, args, _) => {
|
|
for arg in args {
|
|
walk_expr(arg, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
ast::SelectTable::Table(_, _, _) => {
|
|
// Table reference, no expressions
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Walk through expressions in a Window definition
|
|
fn walk_window_expressions(
|
|
window: &ast::Window,
|
|
table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
column_name_norm: &str,
|
|
found: &mut bool,
|
|
) -> Result<()> {
|
|
// Check PARTITION BY expressions
|
|
for expr in &window.partition_by {
|
|
walk_expr(expr, &mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check ORDER BY expressions
|
|
for sorted_col in &window.order_by {
|
|
walk_expr(
|
|
&sorted_col.expr,
|
|
&mut |e: &ast::Expr| -> Result<WalkControl> {
|
|
check_column_ref(e, table, trigger_table_name, column_name_norm, found)?;
|
|
Ok(WalkControl::Continue)
|
|
},
|
|
)?;
|
|
if *found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// TODO: FrameClause can also contain expressions, but they're more complex
|
|
// For now, we'll skip them as they're less common in triggers
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite trigger SQL to replace old column name with new column name.
|
|
/// This handles old.x, new.x, and unqualified x references.
|
|
fn rewrite_trigger_sql_for_column_rename(
|
|
trigger_sql: &str,
|
|
table_name: &str,
|
|
old_column_name: &str,
|
|
new_column_name: &str,
|
|
resolver: &Resolver,
|
|
) -> Result<String> {
|
|
use turso_parser::parser::Parser;
|
|
|
|
// Parse the trigger SQL
|
|
let mut parser = Parser::new(trigger_sql.as_bytes());
|
|
let cmd = parser
|
|
.next_cmd()
|
|
.map_err(|e| LimboError::ParseError(format!("failed to parse trigger SQL: {e}")))?;
|
|
let Some(ast::Cmd::Stmt(ast::Stmt::CreateTrigger {
|
|
temporary,
|
|
if_not_exists,
|
|
trigger_name,
|
|
time,
|
|
event,
|
|
tbl_name,
|
|
for_each_row,
|
|
when_clause,
|
|
commands,
|
|
})) = cmd
|
|
else {
|
|
return Err(LimboError::ParseError(format!(
|
|
"failed to parse trigger SQL: {trigger_sql}"
|
|
)));
|
|
};
|
|
|
|
let old_col_norm = normalize_ident(old_column_name);
|
|
let new_col_norm = normalize_ident(new_column_name);
|
|
|
|
// Get the trigger's owning table to check unqualified column references
|
|
let trigger_table_name_raw = tbl_name.name.as_str();
|
|
let trigger_table_name = normalize_ident(trigger_table_name_raw);
|
|
let trigger_table = resolver
|
|
.schema
|
|
.get_btree_table(&trigger_table_name)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(format!("trigger table not found: {trigger_table_name}"))
|
|
})?;
|
|
|
|
// Check if this trigger references the column being renamed
|
|
// We need to check if the column exists in the table being renamed
|
|
let target_table_name = normalize_ident(table_name);
|
|
let target_table = resolver
|
|
.schema
|
|
.get_btree_table(&target_table_name)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(format!("target table not found: {target_table_name}"))
|
|
})?;
|
|
|
|
// Rewrite UPDATE OF column list if renaming a column in the trigger's owning table
|
|
let is_renaming_trigger_table = trigger_table_name == target_table_name;
|
|
let new_event = if is_renaming_trigger_table {
|
|
match event {
|
|
ast::TriggerEvent::UpdateOf(mut cols) => {
|
|
// Rewrite column names in UPDATE OF list
|
|
for col in &mut cols {
|
|
let col_norm = normalize_ident(col.as_str());
|
|
if col_norm == old_col_norm {
|
|
*col = ast::Name::from_string(new_col_norm.clone());
|
|
}
|
|
}
|
|
ast::TriggerEvent::UpdateOf(cols)
|
|
}
|
|
other => other,
|
|
}
|
|
} else {
|
|
event
|
|
};
|
|
|
|
// Note: SQLite fails RENAME COLUMN if a trigger's WHEN clause references the column.
|
|
// We check for this earlier and fail the operation immediately, matching SQLite.
|
|
// If we reach here, the WHEN clause doesn't reference the column, so we keep it unchanged.
|
|
let new_when_clause = when_clause.clone();
|
|
|
|
let mut new_commands = Vec::new();
|
|
for cmd in commands {
|
|
let new_cmd = rewrite_trigger_cmd_for_column_rename(
|
|
cmd,
|
|
&trigger_table,
|
|
&target_table,
|
|
trigger_table_name_raw,
|
|
&target_table_name,
|
|
&old_col_norm,
|
|
&new_col_norm,
|
|
resolver,
|
|
)?;
|
|
new_commands.push(new_cmd);
|
|
}
|
|
|
|
// Reconstruct the SQL
|
|
use crate::translate::trigger::create_trigger_to_sql;
|
|
let new_sql = create_trigger_to_sql(
|
|
temporary,
|
|
if_not_exists,
|
|
&trigger_name,
|
|
time,
|
|
&new_event,
|
|
&tbl_name,
|
|
for_each_row,
|
|
new_when_clause.as_deref(),
|
|
&new_commands,
|
|
);
|
|
|
|
Ok(new_sql)
|
|
}
|
|
|
|
/// Rewrite an expression to replace column references
|
|
///
|
|
/// `context_table_name` is used for UPDATE/DELETE WHERE clauses where unqualified column
|
|
/// references refer to the UPDATE/DELETE target table, not the trigger's owning table.
|
|
/// If `None`, unqualified references refer to the trigger's owning table.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn rewrite_expr_for_column_rename(
|
|
expr: &ast::Expr,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
context_table_name: Option<&str>,
|
|
resolver: &Resolver,
|
|
) -> Result<ast::Expr> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let trigger_table_name_norm = normalize_ident(trigger_table_name);
|
|
let target_table_name_norm = normalize_ident(target_table_name);
|
|
let is_renaming_trigger_table = trigger_table_name_norm == target_table_name_norm;
|
|
|
|
// Get context table if provided (for UPDATE/DELETE WHERE clauses)
|
|
let context_table_info: Option<(std::sync::Arc<crate::schema::BTreeTable>, String, bool)> =
|
|
if let Some(ctx_name) = context_table_name {
|
|
let ctx_name_norm = normalize_ident(ctx_name);
|
|
let is_renaming = ctx_name_norm == target_table_name_norm;
|
|
let table = resolver
|
|
.schema
|
|
.get_btree_table(&ctx_name_norm)
|
|
.ok_or_else(|| {
|
|
LimboError::ParseError(format!("context table not found: {ctx_name_norm}"))
|
|
})?;
|
|
Some((table, ctx_name_norm, is_renaming))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut expr = expr.clone();
|
|
walk_expr_mut(&mut expr, &mut |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref_with_context(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
is_renaming_trigger_table,
|
|
context_table_info
|
|
.as_ref()
|
|
.map(|(t, n, r)| (t.as_ref(), n, *r)),
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
})?;
|
|
|
|
Ok(expr)
|
|
}
|
|
|
|
/// Rewrite a trigger command to replace column references
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn rewrite_trigger_cmd_for_column_rename(
|
|
cmd: ast::TriggerCmd,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
target_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
resolver: &Resolver,
|
|
) -> Result<ast::TriggerCmd> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
match cmd {
|
|
ast::TriggerCmd::Update {
|
|
or_conflict,
|
|
tbl_name,
|
|
mut sets,
|
|
from,
|
|
where_clause,
|
|
} => {
|
|
// Get the UPDATE target table to check if we're renaming a column in it
|
|
let update_table_name_norm = normalize_ident(tbl_name.as_str());
|
|
let is_renaming_update_table = update_table_name_norm == *target_table_name;
|
|
|
|
// Rewrite SET column names if renaming a column in the UPDATE target table
|
|
if is_renaming_update_table {
|
|
for set in &mut sets {
|
|
for col_name in &mut set.col_names {
|
|
let col_norm = normalize_ident(col_name.as_str());
|
|
if col_norm == *old_col_norm {
|
|
*col_name = ast::Name::from_string(new_col_norm);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rewrite SET expressions
|
|
for set in &mut sets {
|
|
walk_expr_mut(
|
|
&mut set.expr,
|
|
&mut |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
},
|
|
)?;
|
|
}
|
|
|
|
// Rewrite WHERE clause - unqualified column references refer to UPDATE target table
|
|
let new_where = where_clause
|
|
.map(|e| {
|
|
rewrite_expr_for_column_rename(
|
|
&e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
Some(&update_table_name_norm), // UPDATE WHERE: unqualified refs refer to UPDATE target
|
|
resolver,
|
|
)
|
|
.map(Box::new)
|
|
})
|
|
.transpose()?;
|
|
Ok(ast::TriggerCmd::Update {
|
|
or_conflict,
|
|
tbl_name,
|
|
sets,
|
|
from,
|
|
where_clause: new_where,
|
|
})
|
|
}
|
|
ast::TriggerCmd::Insert {
|
|
or_conflict,
|
|
tbl_name,
|
|
mut col_names,
|
|
select,
|
|
upsert,
|
|
returning,
|
|
} => {
|
|
// Rewrite column names in INSERT column list
|
|
// Check if the INSERT is targeting the table being renamed
|
|
let insert_table_name_norm = normalize_ident(tbl_name.as_str());
|
|
if insert_table_name_norm == *target_table_name {
|
|
// This INSERT targets the table being renamed, so rewrite column names
|
|
for col_name in &mut col_names {
|
|
let col_norm = normalize_ident(col_name.as_str());
|
|
if col_norm == *old_col_norm {
|
|
*col_name = ast::Name::from_string(new_col_norm);
|
|
}
|
|
}
|
|
}
|
|
// Rewrite SELECT expressions
|
|
let new_select = rewrite_select_for_column_rename(
|
|
select,
|
|
trigger_table,
|
|
target_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(ast::TriggerCmd::Insert {
|
|
or_conflict,
|
|
tbl_name,
|
|
col_names,
|
|
select: new_select,
|
|
upsert,
|
|
returning,
|
|
})
|
|
}
|
|
ast::TriggerCmd::Delete {
|
|
tbl_name,
|
|
where_clause,
|
|
} => {
|
|
// Get the DELETE target table to check if we're renaming a column in it
|
|
let delete_table_name_norm = normalize_ident(tbl_name.as_str());
|
|
|
|
// Rewrite WHERE clause - unqualified column references refer to DELETE target table
|
|
let new_where = where_clause
|
|
.map(|e| {
|
|
rewrite_expr_for_column_rename(
|
|
&e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
Some(&delete_table_name_norm), // DELETE WHERE: unqualified refs refer to DELETE target
|
|
resolver,
|
|
)
|
|
.map(Box::new)
|
|
})
|
|
.transpose()?;
|
|
Ok(ast::TriggerCmd::Delete {
|
|
tbl_name,
|
|
where_clause: new_where,
|
|
})
|
|
}
|
|
ast::TriggerCmd::Select(select) => {
|
|
let new_select = rewrite_select_for_column_rename(
|
|
select,
|
|
trigger_table,
|
|
target_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(ast::TriggerCmd::Select(new_select))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rewrite a SELECT statement to replace column references
|
|
fn rewrite_select_for_column_rename(
|
|
select: ast::Select,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
_target_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<ast::Select> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let mut rewrite_cb = |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
};
|
|
|
|
let mut select = select;
|
|
|
|
// Rewrite WITH clause (CTEs)
|
|
// Note: We clone here because Select doesn't implement Default, so we can't use mem::take.
|
|
// The clone is necessary to move ownership to rewrite_select_for_column_rename.
|
|
if let Some(ref mut with_clause) = select.with {
|
|
for cte in &mut with_clause.ctes {
|
|
cte.select = rewrite_select_for_column_rename(
|
|
cte.select.clone(),
|
|
trigger_table,
|
|
_target_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
// Rewrite main SELECT body
|
|
rewrite_one_select_for_column_rename(
|
|
&mut select.body.select,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
|
|
// Rewrite compound SELECTs (UNION, EXCEPT, INTERSECT)
|
|
for compound in &mut select.body.compounds {
|
|
rewrite_one_select_for_column_rename(
|
|
&mut compound.select,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
|
|
// Rewrite ORDER BY
|
|
for sorted_col in &mut select.order_by {
|
|
walk_expr_mut(&mut sorted_col.expr, &mut rewrite_cb)?;
|
|
}
|
|
|
|
// Rewrite LIMIT
|
|
if let Some(ref mut limit) = select.limit {
|
|
walk_expr_mut(&mut limit.expr, &mut rewrite_cb)?;
|
|
if let Some(ref mut offset) = limit.offset {
|
|
walk_expr_mut(offset, &mut rewrite_cb)?;
|
|
}
|
|
}
|
|
|
|
Ok(select)
|
|
}
|
|
|
|
/// Rewrite a OneSelect to replace column references
|
|
fn rewrite_one_select_for_column_rename(
|
|
one_select: &mut ast::OneSelect,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<()> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let mut rewrite_cb = |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
};
|
|
|
|
match one_select {
|
|
ast::OneSelect::Select {
|
|
columns,
|
|
from,
|
|
where_clause,
|
|
group_by,
|
|
window_clause,
|
|
..
|
|
} => {
|
|
// Rewrite columns
|
|
for col in columns {
|
|
if let ast::ResultColumn::Expr(expr, _) = col {
|
|
walk_expr_mut(expr, &mut rewrite_cb)?;
|
|
}
|
|
}
|
|
|
|
// Rewrite FROM clause and JOIN conditions
|
|
if let Some(ref mut from_clause) = from {
|
|
rewrite_from_clause_for_column_rename(
|
|
from_clause,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
|
|
// Rewrite WHERE clause
|
|
if let Some(ref mut where_expr) = where_clause {
|
|
walk_expr_mut(where_expr, &mut rewrite_cb)?;
|
|
}
|
|
|
|
// Rewrite GROUP BY and HAVING
|
|
if let Some(ref mut group_by) = group_by {
|
|
for expr in &mut group_by.exprs {
|
|
walk_expr_mut(expr, &mut rewrite_cb)?;
|
|
}
|
|
if let Some(ref mut having_expr) = group_by.having {
|
|
walk_expr_mut(having_expr, &mut rewrite_cb)?;
|
|
}
|
|
}
|
|
|
|
// Rewrite WINDOW clause
|
|
for window_def in window_clause {
|
|
rewrite_window_for_column_rename(
|
|
&mut window_def.window,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
}
|
|
ast::OneSelect::Values(values) => {
|
|
for row in values {
|
|
for expr in row {
|
|
walk_expr_mut(expr, &mut rewrite_cb)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite expressions in a FROM clause (including JOIN conditions)
|
|
fn rewrite_from_clause_for_column_rename(
|
|
from_clause: &mut ast::FromClause,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<()> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let mut rewrite_cb = |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
};
|
|
|
|
// Rewrite main table (could be a subquery)
|
|
rewrite_select_table_for_column_rename(
|
|
&mut from_clause.select,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
|
|
// Rewrite JOIN conditions
|
|
for join in &mut from_clause.joins {
|
|
rewrite_select_table_for_column_rename(
|
|
&mut join.table,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
if let Some(ref mut constraint) = join.constraint {
|
|
match constraint {
|
|
ast::JoinConstraint::On(expr) => {
|
|
walk_expr_mut(expr, &mut rewrite_cb)?;
|
|
}
|
|
ast::JoinConstraint::Using(_) => {
|
|
// USING clause contains column names, not expressions
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite expressions in a SelectTable (table, subquery, or table function)
|
|
fn rewrite_select_table_for_column_rename(
|
|
select_table: &mut ast::SelectTable,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<()> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let mut rewrite_cb = |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
};
|
|
|
|
match select_table {
|
|
ast::SelectTable::Select(select, _) => {
|
|
// Note: We clone here because Select doesn't implement Default, so we can't use mem::take.
|
|
// The clone is necessary to move ownership to rewrite_select_for_column_rename.
|
|
*select = rewrite_select_for_column_rename(
|
|
select.clone(),
|
|
trigger_table,
|
|
trigger_table, // target_table not needed for subqueries
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
ast::SelectTable::Sub(from_clause, _) => {
|
|
rewrite_from_clause_for_column_rename(
|
|
from_clause,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
}
|
|
ast::SelectTable::TableCall(_, args, _) => {
|
|
for arg in args {
|
|
walk_expr_mut(arg, &mut rewrite_cb)?;
|
|
}
|
|
}
|
|
ast::SelectTable::Table(_, _, _) => {
|
|
// Table reference, no expressions
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite expressions in a Window definition
|
|
fn rewrite_window_for_column_rename(
|
|
window: &mut ast::Window,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<()> {
|
|
use crate::translate::expr::walk_expr_mut;
|
|
|
|
let mut rewrite_cb = |e: &mut ast::Expr| -> Result<WalkControl> {
|
|
rewrite_expr_column_ref(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
target_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
)?;
|
|
Ok(WalkControl::Continue)
|
|
};
|
|
|
|
// Rewrite PARTITION BY expressions
|
|
for expr in &mut window.partition_by {
|
|
walk_expr_mut(expr, &mut rewrite_cb)?;
|
|
}
|
|
|
|
// Rewrite ORDER BY expressions
|
|
for sorted_col in &mut window.order_by {
|
|
walk_expr_mut(&mut sorted_col.expr, &mut rewrite_cb)?;
|
|
}
|
|
|
|
// TODO: FrameClause can also contain expressions, but they're more complex
|
|
// For now, we'll skip them as they're less common in triggers
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite a single expression's column reference
|
|
///
|
|
/// Handles column references in trigger expressions:
|
|
/// - NEW.column and OLD.column: Always refer to the trigger's owning table
|
|
/// - Qualified references (e.g., u.x): Refer to the specified table
|
|
/// - Unqualified references (e.g., x): Resolution order:
|
|
/// 1. If `context_table` is provided (UPDATE/DELETE WHERE clauses), check the context table first
|
|
/// 2. Otherwise, check the trigger's owning table
|
|
///
|
|
/// This matches SQLite's column resolution order where unqualified columns in UPDATE/DELETE
|
|
/// WHERE clauses refer to the target table, not the trigger's owning table.
|
|
///
|
|
/// `context_table`: Optional tuple of (table, normalized_name, is_renaming) for UPDATE/DELETE
|
|
/// target tables. If `None`, unqualified references refer to the trigger's owning table.
|
|
fn rewrite_expr_column_ref_with_context(
|
|
e: &mut ast::Expr,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
is_renaming_trigger_table: bool,
|
|
context_table: Option<(&crate::schema::BTreeTable, &String, bool)>,
|
|
) -> Result<()> {
|
|
match e {
|
|
ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) => {
|
|
let ns_norm = normalize_ident(ns.as_str());
|
|
let col_norm = normalize_ident(col.as_str());
|
|
|
|
// Check if this is NEW.column or OLD.column
|
|
if (ns_norm.eq_ignore_ascii_case("new") || ns_norm.eq_ignore_ascii_case("old"))
|
|
&& col_norm == *old_col_norm
|
|
{
|
|
// NEW.x and OLD.x always refer to the trigger's owning table
|
|
if is_renaming_trigger_table && trigger_table.get_column(&col_norm).is_some() {
|
|
*col = ast::Name::from_string(new_col_norm);
|
|
}
|
|
} else if col_norm == *old_col_norm {
|
|
// This is a qualified column reference like u.x or t.x
|
|
// Check if it refers to the context table (UPDATE/DELETE target) or trigger table
|
|
if let Some((_, ctx_name_norm, is_renaming_ctx)) = context_table {
|
|
if ns_norm == *ctx_name_norm && is_renaming_ctx {
|
|
// Qualified reference to context table (e.g., u.x where u is UPDATE target)
|
|
*col = ast::Name::from_string(new_col_norm);
|
|
}
|
|
}
|
|
// Also check if it's a qualified reference to the trigger's owning table
|
|
// (e.g., t.x in a trigger on table t)
|
|
if is_renaming_trigger_table {
|
|
let trigger_table_name_norm = normalize_ident(trigger_table_name);
|
|
if ns_norm == trigger_table_name_norm
|
|
&& trigger_table.get_column(&col_norm).is_some()
|
|
{
|
|
*col = ast::Name::from_string(new_col_norm);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ast::Expr::Id(col) => {
|
|
// Unqualified column reference
|
|
let col_norm = normalize_ident(col.as_str());
|
|
if col_norm == *old_col_norm {
|
|
// Check context table first (for UPDATE/DELETE WHERE clauses)
|
|
if let Some((ctx_table, _, is_renaming_ctx)) = context_table {
|
|
if ctx_table.get_column(&col_norm).is_some() {
|
|
// This refers to the context table (UPDATE/DELETE target)
|
|
if is_renaming_ctx {
|
|
*e = ast::Expr::Id(ast::Name::from_string(new_col_norm));
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
// Otherwise, check trigger's owning table
|
|
if is_renaming_trigger_table && trigger_table.get_column(&col_norm).is_some() {
|
|
*e = ast::Expr::Id(ast::Name::from_string(new_col_norm));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrite a single expression's column reference (convenience wrapper for non-context cases)
|
|
fn rewrite_expr_column_ref(
|
|
e: &mut ast::Expr,
|
|
trigger_table: &crate::schema::BTreeTable,
|
|
trigger_table_name: &str,
|
|
target_table_name: &str,
|
|
old_col_norm: &str,
|
|
new_col_norm: &str,
|
|
) -> Result<()> {
|
|
let trigger_table_name_norm = normalize_ident(trigger_table_name);
|
|
let target_table_name_norm = normalize_ident(target_table_name);
|
|
let is_renaming_trigger_table = trigger_table_name_norm == target_table_name_norm;
|
|
|
|
rewrite_expr_column_ref_with_context(
|
|
e,
|
|
trigger_table,
|
|
trigger_table_name,
|
|
old_col_norm,
|
|
new_col_norm,
|
|
is_renaming_trigger_table,
|
|
None,
|
|
)
|
|
}
|