mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-31 05:44:25 +01:00
Merge 'core/vdbe: fix ALTER COLUMN to propagate constraints to other table references' from Preston Thorpe
with fix to ensure `PRIMARY KEY` isn't written twice Closes #3798
This commit is contained in:
@@ -1358,6 +1358,8 @@ impl BTreeTable {
|
||||
/// `CREATE TABLE t (x)`, whereas sqlite stores it with the original extra whitespace.
|
||||
pub fn to_sql(&self) -> String {
|
||||
let mut sql = format!("CREATE TABLE {} (", self.name);
|
||||
let needs_pk_inline = self.primary_key_columns.len() == 1;
|
||||
// Add columns
|
||||
for (i, column) in self.columns.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
@@ -1384,8 +1386,7 @@ impl BTreeTable {
|
||||
if column.unique {
|
||||
sql.push_str(" UNIQUE");
|
||||
}
|
||||
|
||||
if column.primary_key {
|
||||
if needs_pk_inline && column.primary_key {
|
||||
sql.push_str(" PRIMARY KEY");
|
||||
}
|
||||
|
||||
@@ -1394,6 +1395,64 @@ impl BTreeTable {
|
||||
sql.push_str(&default.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let has_table_pk = !self.primary_key_columns.is_empty();
|
||||
// Add table-level PRIMARY KEY constraint if exists
|
||||
if !needs_pk_inline && has_table_pk {
|
||||
sql.push_str(", PRIMARY KEY (");
|
||||
for (i, col) in self.primary_key_columns.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(&col.0);
|
||||
}
|
||||
sql.push(')');
|
||||
}
|
||||
|
||||
for fk in &self.foreign_keys {
|
||||
sql.push_str(", FOREIGN KEY (");
|
||||
for (i, col) in fk.child_columns.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(col);
|
||||
}
|
||||
sql.push_str(") REFERENCES ");
|
||||
sql.push_str(&fk.parent_table);
|
||||
sql.push('(');
|
||||
for (i, col) in fk.parent_columns.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(col);
|
||||
}
|
||||
sql.push(')');
|
||||
|
||||
// Add ON DELETE/UPDATE actions, NoAction is default so just make empty in that case
|
||||
if fk.on_delete != RefAct::NoAction {
|
||||
sql.push_str(" ON DELETE ");
|
||||
sql.push_str(match fk.on_delete {
|
||||
RefAct::SetNull => "SET NULL",
|
||||
RefAct::SetDefault => "SET DEFAULT",
|
||||
RefAct::Cascade => "CASCADE",
|
||||
RefAct::Restrict => "RESTRICT",
|
||||
_ => "",
|
||||
});
|
||||
}
|
||||
if fk.on_update != RefAct::NoAction {
|
||||
sql.push_str(" ON UPDATE ");
|
||||
sql.push_str(match fk.on_update {
|
||||
RefAct::SetNull => "SET NULL",
|
||||
RefAct::SetDefault => "SET DEFAULT",
|
||||
RefAct::Cascade => "CASCADE",
|
||||
RefAct::Restrict => "RESTRICT",
|
||||
_ => "",
|
||||
});
|
||||
}
|
||||
if fk.deferred {
|
||||
sql.push_str(" DEFERRABLE INITIALLY DEFERRED");
|
||||
}
|
||||
}
|
||||
sql.push(')');
|
||||
sql
|
||||
}
|
||||
|
||||
33
core/util.rs
33
core/util.rs
@@ -1331,6 +1331,39 @@ pub fn extract_view_columns(
|
||||
Ok(ViewColumnSchema { tables, columns })
|
||||
}
|
||||
|
||||
pub fn rewrite_fk_parent_cols_if_self_ref(
|
||||
clause: &mut ast::ForeignKeyClause,
|
||||
table: &str,
|
||||
from: &str,
|
||||
to: &str,
|
||||
) {
|
||||
if normalize_ident(clause.tbl_name.as_str()) == normalize_ident(table) {
|
||||
for c in &mut clause.columns {
|
||||
if normalize_ident(c.col_name.as_str()) == normalize_ident(from) {
|
||||
c.col_name = ast::Name::exact(to.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a column-level REFERENCES <tbl>(col,...) constraint
|
||||
pub fn rewrite_column_references_if_needed(
|
||||
col: &mut ast::ColumnDefinition,
|
||||
table: &str,
|
||||
from: &str,
|
||||
to: &str,
|
||||
) {
|
||||
for cc in &mut col.constraints {
|
||||
if let ast::NamedColumnConstraint {
|
||||
constraint: ast::ColumnConstraint::ForeignKey { clause, .. },
|
||||
..
|
||||
} = cc
|
||||
{
|
||||
rewrite_fk_parent_cols_if_self_ref(clause, table, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -17,7 +17,9 @@ use crate::types::{
|
||||
compare_immutable, compare_records_generic, Extendable, IOCompletions, ImmutableRecord,
|
||||
SeekResult, Text,
|
||||
};
|
||||
use crate::util::normalize_ident;
|
||||
use crate::util::{
|
||||
normalize_ident, rewrite_column_references_if_needed, rewrite_fk_parent_cols_if_self_ref,
|
||||
};
|
||||
use crate::vdbe::insn::InsertFlags;
|
||||
use crate::vdbe::{registers_to_ref_values, TxnCleanup};
|
||||
use crate::vector::{vector32_sparse, vector_concat, vector_distance_jaccard, vector_slice};
|
||||
@@ -73,7 +75,7 @@ use super::{
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use rand::{thread_rng, Rng, RngCore};
|
||||
use turso_parser::ast::{self, Name, SortOrder};
|
||||
use turso_parser::ast::{self, ForeignKeyClause, Name, SortOrder};
|
||||
use turso_parser::parser::Parser;
|
||||
|
||||
use super::{
|
||||
@@ -5463,11 +5465,9 @@ pub fn op_function(
|
||||
.parse_column_definition(true)
|
||||
.unwrap();
|
||||
|
||||
let new_sql = 'sql: {
|
||||
if table != tbl_name {
|
||||
break 'sql None;
|
||||
}
|
||||
let rename_to = normalize_ident(column_def.col_name.as_str());
|
||||
|
||||
let new_sql = 'sql: {
|
||||
let Value::Text(sql) = sql else {
|
||||
break 'sql None;
|
||||
};
|
||||
@@ -5521,34 +5521,160 @@ pub fn op_function(
|
||||
temporary,
|
||||
if_not_exists,
|
||||
} => {
|
||||
if table != normalize_ident(tbl_name.name.as_str()) {
|
||||
break 'sql None;
|
||||
}
|
||||
|
||||
let ast::CreateTableBody::ColumnsAndConstraints {
|
||||
mut columns,
|
||||
constraints,
|
||||
mut constraints,
|
||||
options,
|
||||
} = body
|
||||
else {
|
||||
todo!()
|
||||
};
|
||||
|
||||
let column = columns
|
||||
.iter_mut()
|
||||
.find(|column| {
|
||||
column.col_name.as_str() == original_rename_from.as_str()
|
||||
})
|
||||
.expect("column being renamed should be present");
|
||||
let normalized_tbl_name = normalize_ident(tbl_name.name.as_str());
|
||||
|
||||
match alter_func {
|
||||
AlterTableFunc::AlterColumn => *column = column_def,
|
||||
AlterTableFunc::RenameColumn => {
|
||||
column.col_name = column_def.col_name
|
||||
if normalized_tbl_name == table {
|
||||
// This is the table being altered - update its column
|
||||
let column = columns
|
||||
.iter_mut()
|
||||
.find(|column| {
|
||||
column.col_name.as_str()
|
||||
== original_rename_from.as_str()
|
||||
})
|
||||
.expect("column being renamed should be present");
|
||||
|
||||
match alter_func {
|
||||
AlterTableFunc::AlterColumn => *column = column_def.clone(),
|
||||
AlterTableFunc::RenameColumn => {
|
||||
column.col_name = column_def.col_name.clone()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Update table-level constraints (PRIMARY KEY, UNIQUE, FOREIGN KEY)
|
||||
for constraint in &mut constraints {
|
||||
match &mut constraint.constraint {
|
||||
ast::TableConstraint::PrimaryKey {
|
||||
columns: pk_cols,
|
||||
..
|
||||
} => {
|
||||
for col in pk_cols {
|
||||
let (ast::Expr::Name(ref name)
|
||||
| ast::Expr::Id(ref name)) = *col.expr
|
||||
else {
|
||||
return Err(LimboError::ParseError("Unexpected expression in PRIMARY KEY constraint".to_string()));
|
||||
};
|
||||
if normalize_ident(name.as_str()) == rename_from
|
||||
{
|
||||
*col.expr = ast::Expr::Name(Name::exact(
|
||||
column_def.col_name.as_str().to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::TableConstraint::Unique {
|
||||
columns: uniq_cols,
|
||||
..
|
||||
} => {
|
||||
for col in uniq_cols {
|
||||
let (ast::Expr::Name(ref name)
|
||||
| ast::Expr::Id(ref name)) = *col.expr
|
||||
else {
|
||||
return Err(LimboError::ParseError("Unexpected expression in UNIQUE constraint".to_string()));
|
||||
};
|
||||
if normalize_ident(name.as_str()) == rename_from
|
||||
{
|
||||
*col.expr = ast::Expr::Name(Name::exact(
|
||||
column_def.col_name.as_str().to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::TableConstraint::ForeignKey {
|
||||
columns: child_cols,
|
||||
clause,
|
||||
..
|
||||
} => {
|
||||
// Update child columns in this table's FK definitions
|
||||
for child_col in child_cols {
|
||||
if normalize_ident(child_col.col_name.as_str())
|
||||
== rename_from
|
||||
{
|
||||
child_col.col_name = Name::exact(
|
||||
column_def.col_name.as_str().to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
rewrite_fk_parent_cols_if_self_ref(
|
||||
clause,
|
||||
&normalized_tbl_name,
|
||||
&rename_from,
|
||||
column_def.col_name.as_str(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for col in &mut columns {
|
||||
rewrite_column_references_if_needed(
|
||||
col,
|
||||
&normalized_tbl_name,
|
||||
&rename_from,
|
||||
column_def.col_name.as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a different table, check if it has FKs referencing the renamed column
|
||||
let mut fk_updated = false;
|
||||
|
||||
for constraint in &mut constraints {
|
||||
if let ast::TableConstraint::ForeignKey {
|
||||
columns: _,
|
||||
clause:
|
||||
ForeignKeyClause {
|
||||
tbl_name,
|
||||
columns: parent_cols,
|
||||
..
|
||||
},
|
||||
..
|
||||
} = &mut constraint.constraint
|
||||
{
|
||||
// Check if this FK references the table being altered
|
||||
if normalize_ident(tbl_name.as_str()) == table {
|
||||
// Update parent column references if they match the renamed column
|
||||
for parent_col in parent_cols {
|
||||
if normalize_ident(parent_col.col_name.as_str())
|
||||
== rename_from
|
||||
{
|
||||
parent_col.col_name = Name::exact(
|
||||
column_def.col_name.as_str().to_owned(),
|
||||
);
|
||||
fk_updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for col in &mut columns {
|
||||
let before = fk_updated;
|
||||
let mut local_col = col.clone();
|
||||
rewrite_column_references_if_needed(
|
||||
&mut local_col,
|
||||
&table,
|
||||
&rename_from,
|
||||
column_def.col_name.as_str(),
|
||||
);
|
||||
if local_col != *col {
|
||||
*col = local_col;
|
||||
fk_updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only return updated SQL if we actually changed something
|
||||
if !fk_updated {
|
||||
break 'sql None;
|
||||
}
|
||||
}
|
||||
Some(
|
||||
ast::Stmt::CreateTable {
|
||||
tbl_name,
|
||||
@@ -5563,7 +5689,7 @@ pub fn op_function(
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
_ => todo!(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8238,43 +8364,94 @@ pub fn op_alter_column(
|
||||
.clone()
|
||||
};
|
||||
let new_column = crate::schema::Column::from(definition);
|
||||
let new_name = definition.col_name.as_str().to_owned();
|
||||
|
||||
conn.with_schema_mut(|schema| {
|
||||
let table = schema
|
||||
let table_arc = schema
|
||||
.tables
|
||||
.get_mut(&normalized_table_name)
|
||||
.expect("table being renamed should be in schema");
|
||||
.expect("table being ALTERed should be in schema");
|
||||
let table = Arc::make_mut(table_arc);
|
||||
|
||||
let table = Arc::make_mut(table);
|
||||
|
||||
let Table::BTree(btree) = table else {
|
||||
panic!("only btree tables can be renamed");
|
||||
let Table::BTree(ref mut btree_arc) = table else {
|
||||
panic!("only btree tables can be altered");
|
||||
};
|
||||
|
||||
let btree = Arc::make_mut(btree);
|
||||
|
||||
let column = btree
|
||||
let btree = Arc::make_mut(btree_arc);
|
||||
let col = btree
|
||||
.columns
|
||||
.get_mut(*column_index)
|
||||
.expect("renamed column should be in schema");
|
||||
.expect("column being ALTERed should be in schema");
|
||||
|
||||
if let Some(indexes) = schema.indexes.get_mut(&normalized_table_name) {
|
||||
for index in indexes {
|
||||
let index = Arc::make_mut(index);
|
||||
for index_column in &mut index.columns {
|
||||
if index_column.name
|
||||
== *column.name.as_ref().expect("btree column should be named")
|
||||
{
|
||||
index_column.name = definition.col_name.as_str().to_owned();
|
||||
// Update indexes on THIS table that name the old column (you already had this)
|
||||
if let Some(idxs) = schema.indexes.get_mut(&normalized_table_name) {
|
||||
for idx in idxs {
|
||||
let idx = Arc::make_mut(idx);
|
||||
for ic in &mut idx.columns {
|
||||
if ic.name.eq_ignore_ascii_case(
|
||||
col.name.as_ref().expect("btree column should be named"),
|
||||
) {
|
||||
ic.name = new_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if *rename {
|
||||
col.name = Some(new_name.clone());
|
||||
} else {
|
||||
*col = new_column.clone();
|
||||
}
|
||||
|
||||
// Keep primary_key_columns consistent (names may change on rename)
|
||||
for (pk_name, _ord) in &mut btree.primary_key_columns {
|
||||
if pk_name.eq_ignore_ascii_case(&old_column_name) {
|
||||
*pk_name = new_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain rowid-alias bit after change/rename (INTEGER PRIMARY KEY)
|
||||
if !*rename {
|
||||
// recompute alias from `new_column`
|
||||
btree.columns[*column_index].is_rowid_alias = new_column.is_rowid_alias;
|
||||
}
|
||||
|
||||
// Update this table’s OWN foreign keys
|
||||
for fk_arc in &mut btree.foreign_keys {
|
||||
let fk = Arc::make_mut(fk_arc);
|
||||
// child side: rename child column if it matches
|
||||
for cc in &mut fk.child_columns {
|
||||
if cc.eq_ignore_ascii_case(&old_column_name) {
|
||||
*cc = new_name.clone();
|
||||
}
|
||||
}
|
||||
// parent side: if self-referencing, rename parent column too
|
||||
if normalize_ident(&fk.parent_table) == normalized_table_name {
|
||||
for pc in &mut fk.parent_columns {
|
||||
if pc.eq_ignore_ascii_case(&old_column_name) {
|
||||
*pc = new_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *rename {
|
||||
column.name = new_column.name;
|
||||
} else {
|
||||
*column = new_column;
|
||||
// fix OTHER tables that reference this table as parent
|
||||
for (tname, t_arc) in schema.tables.iter_mut() {
|
||||
if normalize_ident(tname) == normalized_table_name {
|
||||
continue;
|
||||
}
|
||||
if let Table::BTree(ref mut child_btree_arc) = Arc::make_mut(t_arc) {
|
||||
let child_btree = Arc::make_mut(child_btree_arc);
|
||||
for fk_arc in &mut child_btree.foreign_keys {
|
||||
if normalize_ident(&fk_arc.parent_table) != normalized_table_name {
|
||||
continue;
|
||||
}
|
||||
let fk = Arc::make_mut(fk_arc);
|
||||
for pc in &mut fk.parent_columns {
|
||||
if pc.eq_ignore_ascii_case(&old_column_name) {
|
||||
*pc = new_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -22,3 +22,219 @@ do_execsql_test_in_memory_any_error fail-alter-column-unique {
|
||||
CREATE TABLE t (a);
|
||||
ALTER TABLE t ALTER COLUMN a TO a UNIQUE;
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-pk-column {
|
||||
CREATE TABLE customers (cust_id INTEGER PRIMARY KEY, cust_name TEXT);
|
||||
INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob');
|
||||
|
||||
ALTER TABLE customers RENAME COLUMN cust_id TO customer_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'customers';
|
||||
SELECT customer_id, cust_name FROM customers ORDER BY customer_id;
|
||||
} {
|
||||
"CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, cust_name TEXT)"
|
||||
"1|Alice"
|
||||
"2|Bob"
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-composite-pk {
|
||||
CREATE TABLE products (category TEXT, prod_code TEXT, name TEXT, PRIMARY KEY (category, prod_code));
|
||||
INSERT INTO products VALUES ('Electronics', 'E001', 'Laptop');
|
||||
|
||||
ALTER TABLE products RENAME COLUMN prod_code TO product_code;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'products';
|
||||
SELECT category, product_code, name FROM products;
|
||||
} {
|
||||
"CREATE TABLE products (category TEXT, product_code TEXT, name TEXT, PRIMARY KEY (category, product_code))"
|
||||
"Electronics|E001|Laptop"
|
||||
}
|
||||
|
||||
# Foreign key child column rename
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-child {
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (cid INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES parent(id));
|
||||
INSERT INTO parent VALUES (1);
|
||||
INSERT INTO child VALUES (1, 1);
|
||||
|
||||
ALTER TABLE child RENAME COLUMN pid TO parent_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'child';
|
||||
} {
|
||||
"CREATE TABLE child (cid INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent (id))"
|
||||
}
|
||||
|
||||
# Foreign key parent column rename - critical test
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-parent {
|
||||
CREATE TABLE orders (order_id INTEGER PRIMARY KEY, date TEXT);
|
||||
CREATE TABLE items (item_id INTEGER PRIMARY KEY, oid INTEGER, FOREIGN KEY (oid) REFERENCES orders(order_id));
|
||||
|
||||
ALTER TABLE orders RENAME COLUMN order_id TO ord_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'orders';
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'items';
|
||||
} {
|
||||
"CREATE TABLE orders (ord_id INTEGER PRIMARY KEY, date TEXT)"
|
||||
"CREATE TABLE items (item_id INTEGER PRIMARY KEY, oid INTEGER, FOREIGN KEY (oid) REFERENCES orders (ord_id))"
|
||||
}
|
||||
|
||||
# Composite foreign key parent rename
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-composite-fk-parent {
|
||||
CREATE TABLE products (cat TEXT, code TEXT, PRIMARY KEY (cat, code));
|
||||
CREATE TABLE inventory (id INTEGER PRIMARY KEY, cat TEXT, code TEXT, FOREIGN KEY (cat, code) REFERENCES products(cat, code));
|
||||
|
||||
ALTER TABLE products RENAME COLUMN code TO sku;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'products';
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'inventory';
|
||||
} {
|
||||
"CREATE TABLE products (cat TEXT, sku TEXT, PRIMARY KEY (cat, sku))"
|
||||
"CREATE TABLE inventory (id INTEGER PRIMARY KEY, cat TEXT, code TEXT, FOREIGN KEY (cat, code) REFERENCES products (cat, sku))"
|
||||
}
|
||||
|
||||
# Multiple foreign keys to same parent
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-multiple-fks {
|
||||
CREATE TABLE users (uid INTEGER PRIMARY KEY);
|
||||
CREATE TABLE messages (mid INTEGER PRIMARY KEY, sender INTEGER, receiver INTEGER,
|
||||
FOREIGN KEY (sender) REFERENCES users(uid),
|
||||
FOREIGN KEY (receiver) REFERENCES users(uid));
|
||||
|
||||
ALTER TABLE users RENAME COLUMN uid TO user_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'messages';
|
||||
} {
|
||||
"CREATE TABLE messages (mid INTEGER PRIMARY KEY, sender INTEGER, receiver INTEGER, FOREIGN KEY (sender) REFERENCES users (user_id), FOREIGN KEY (receiver) REFERENCES users (user_id))"
|
||||
}
|
||||
|
||||
# Self-referencing foreign key
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-self-ref-fk {
|
||||
CREATE TABLE employees (emp_id INTEGER PRIMARY KEY, manager_id INTEGER,
|
||||
FOREIGN KEY (manager_id) REFERENCES employees(emp_id));
|
||||
|
||||
ALTER TABLE employees RENAME COLUMN emp_id TO employee_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'employees';
|
||||
} {
|
||||
"CREATE TABLE employees (employee_id INTEGER PRIMARY KEY, manager_id INTEGER, FOREIGN KEY (manager_id) REFERENCES employees (employee_id))"
|
||||
}
|
||||
|
||||
# Chain of FK renames - parent is both PK and referenced
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-chain {
|
||||
CREATE TABLE t1 (a INTEGER PRIMARY KEY);
|
||||
CREATE TABLE t2 (b INTEGER PRIMARY KEY, a_ref INTEGER, FOREIGN KEY (a_ref) REFERENCES t1(a));
|
||||
CREATE TABLE t3 (c INTEGER PRIMARY KEY, b_ref INTEGER, FOREIGN KEY (b_ref) REFERENCES t2(b));
|
||||
|
||||
ALTER TABLE t1 RENAME COLUMN a TO a_new;
|
||||
ALTER TABLE t2 RENAME COLUMN b TO b_new;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 't2';
|
||||
SELECT sql FROM sqlite_schema WHERE name = 't3';
|
||||
} {
|
||||
"CREATE TABLE t2 (b_new INTEGER PRIMARY KEY, a_ref INTEGER, FOREIGN KEY (a_ref) REFERENCES t1 (a_new))"
|
||||
"CREATE TABLE t3 (c INTEGER PRIMARY KEY, b_ref INTEGER, FOREIGN KEY (b_ref) REFERENCES t2 (b_new))"
|
||||
}
|
||||
|
||||
# FK with ON DELETE/UPDATE actions
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-actions {
|
||||
CREATE TABLE parent (pid INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (cid INTEGER PRIMARY KEY, pid INTEGER,
|
||||
FOREIGN KEY (pid) REFERENCES parent(pid) ON DELETE CASCADE ON UPDATE RESTRICT);
|
||||
|
||||
ALTER TABLE parent RENAME COLUMN pid TO parent_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'child';
|
||||
} {
|
||||
"CREATE TABLE child (cid INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES parent (parent_id) ON DELETE CASCADE ON UPDATE RESTRICT)"
|
||||
}
|
||||
|
||||
# FK with DEFERRABLE
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-deferrable {
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER,
|
||||
FOREIGN KEY (pid) REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
|
||||
ALTER TABLE parent RENAME COLUMN id TO parent_id;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'child';
|
||||
} {
|
||||
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES parent (parent_id) DEFERRABLE INITIALLY DEFERRED)"
|
||||
}
|
||||
|
||||
# Rename with quoted identifiers in FK
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-fk-quoted {
|
||||
CREATE TABLE "parent table" ("parent id" INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER,
|
||||
FOREIGN KEY (pid) REFERENCES "parent table"("parent id"));
|
||||
|
||||
ALTER TABLE "parent table" RENAME COLUMN "parent id" TO "new id";
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'child';
|
||||
} {
|
||||
"CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES \"parent table\" (\"new id\"))"
|
||||
}
|
||||
|
||||
# Verify FK constraint still works after rename
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-fk-constraint-after-rename {
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES parent(id));
|
||||
INSERT INTO parent VALUES (1);
|
||||
INSERT INTO child VALUES (1, 1);
|
||||
|
||||
ALTER TABLE parent RENAME COLUMN id TO parent_id;
|
||||
|
||||
-- This should work
|
||||
INSERT INTO child VALUES (2, 1);
|
||||
SELECT COUNT(*) FROM child;
|
||||
} {
|
||||
"2"
|
||||
}
|
||||
|
||||
# FK constraint violation after rename should still fail
|
||||
do_execsql_test_in_memory_any_error alter-table-fk-violation-after-rename {
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER, FOREIGN KEY (pid) REFERENCES parent(id));
|
||||
INSERT INTO parent VALUES (1);
|
||||
|
||||
ALTER TABLE parent RENAME COLUMN id TO parent_id;
|
||||
|
||||
-- This should fail with FK violation
|
||||
INSERT INTO child VALUES (1, 999);
|
||||
}
|
||||
|
||||
# Complex scenario with multiple table constraints
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-complex-constraints {
|
||||
CREATE TABLE t (
|
||||
a INTEGER,
|
||||
b TEXT,
|
||||
c REAL,
|
||||
PRIMARY KEY (a, b),
|
||||
UNIQUE (b, c),
|
||||
FOREIGN KEY (a) REFERENCES t(a)
|
||||
);
|
||||
|
||||
ALTER TABLE t RENAME COLUMN a TO x;
|
||||
ALTER TABLE t RENAME COLUMN b TO y;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 't';
|
||||
} {
|
||||
"CREATE TABLE t (x INTEGER, y TEXT, c REAL, PRIMARY KEY (x, y), UNIQUE (y, c), FOREIGN KEY (x) REFERENCES t (x))"
|
||||
}
|
||||
|
||||
# Rename column that appears in both PK and FK
|
||||
do_execsql_test_on_specific_db {:memory:} alter-table-rename-pk-and-fk {
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child (
|
||||
id INTEGER PRIMARY KEY,
|
||||
parent_ref INTEGER,
|
||||
FOREIGN KEY (id) REFERENCES parent(id),
|
||||
FOREIGN KEY (parent_ref) REFERENCES parent(id)
|
||||
);
|
||||
|
||||
ALTER TABLE parent RENAME COLUMN id TO pid;
|
||||
|
||||
SELECT sql FROM sqlite_schema WHERE name = 'child';
|
||||
} {
|
||||
"CREATE TABLE child (id INTEGER PRIMARY KEY, parent_ref INTEGER, FOREIGN KEY (id) REFERENCES parent (pid), FOREIGN KEY (parent_ref) REFERENCES parent (pid))"
|
||||
}
|
||||
|
||||
@@ -1107,9 +1107,7 @@ fn test_cdc_schema_changes_alter_table() {
|
||||
Value::Text("t".to_string()),
|
||||
Value::Text("t".to_string()),
|
||||
Value::Integer(4),
|
||||
Value::Text(
|
||||
"CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE)".to_string()
|
||||
)
|
||||
Value::Text("CREATE TABLE t (x, y, z UNIQUE, PRIMARY KEY (x, y))".to_string())
|
||||
])),
|
||||
Value::Blob(record([
|
||||
Value::Integer(0),
|
||||
@@ -1135,9 +1133,7 @@ fn test_cdc_schema_changes_alter_table() {
|
||||
Value::Text("t".to_string()),
|
||||
Value::Text("t".to_string()),
|
||||
Value::Integer(4),
|
||||
Value::Text(
|
||||
"CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE)".to_string()
|
||||
)
|
||||
Value::Text("CREATE TABLE t (x, y, z UNIQUE, PRIMARY KEY (x, y))".to_string())
|
||||
])),
|
||||
Value::Blob(record([
|
||||
Value::Text("table".to_string()),
|
||||
@@ -1145,7 +1141,7 @@ fn test_cdc_schema_changes_alter_table() {
|
||||
Value::Text("t".to_string()),
|
||||
Value::Integer(4),
|
||||
Value::Text(
|
||||
"CREATE TABLE t (x PRIMARY KEY, y PRIMARY KEY, z UNIQUE, t)".to_string()
|
||||
"CREATE TABLE t (x, y, z UNIQUE, t, PRIMARY KEY (x, y))".to_string()
|
||||
)
|
||||
])),
|
||||
Value::Blob(record([
|
||||
|
||||
Reference in New Issue
Block a user