From 7c746e476ce2e6d9e677a1ba1ebb07ae29db4e50 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 21 Oct 2025 09:43:23 -0400 Subject: [PATCH 1/5] Fix to_sql method on BTreeTable to not double write primary keys --- core/schema.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/core/schema.rs b/core/schema.rs index ce30d3950..9ee195d94 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -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 } From 06e3b9611bc65e05b55c236ba6f5340d5380faf0 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 21 Oct 2025 10:40:31 -0400 Subject: [PATCH 2/5] Add helpers to rewrite REFERENCES from foriegn keys in ColumnDefinition --- core/util.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/util.rs b/core/util.rs index 1093c61ba..26696eabe 100644 --- a/core/util.rs +++ b/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 (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::*; From 2fbd4b7cec084b57e0c78802d1ccd8842cb9611b Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 21 Oct 2025 10:46:52 -0400 Subject: [PATCH 3/5] Ensure op_alter_column and Func::AlterColumn are fixing table references to columns with fk's --- core/vdbe/execute.rs | 271 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 224 insertions(+), 47 deletions(-) diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 0d7daeacd..292657274 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -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(); + } + } + } + } } }); From c48d7a09632537f8d073a3a21e7bd16bb56ad32d Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 21 Oct 2025 10:47:08 -0400 Subject: [PATCH 4/5] Add tcl tests for alter column fixes --- testing/alter_column.test | 216 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/testing/alter_column.test b/testing/alter_column.test index 3672497ab..1b4da6dd0 100755 --- a/testing/alter_column.test +++ b/testing/alter_column.test @@ -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))" +} From 08197e345ab53180fcf765423c5d43d9a1c67dad Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Tue, 21 Oct 2025 11:22:29 -0400 Subject: [PATCH 5/5] Fix cdc test to assert for correct schema output --- tests/integration/functions/test_cdc.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/integration/functions/test_cdc.rs b/tests/integration/functions/test_cdc.rs index d631c1f93..28606eca3 100644 --- a/tests/integration/functions/test_cdc.rs +++ b/tests/integration/functions/test_cdc.rs @@ -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([