Propagate column renames to FK and unique constraints in the table dialog (#9060) (#10046)

* Propagate column renames to FK and unique constraints. #9060

In the new-table dialog, the primary key already updated its column
references when a column was renamed, but foreign key and unique
constraint definitions did not, leaving them pointing at the old name.
Mirror the PK rename-propagation in the foreign_key and unique_constraint
depChange handlers (and add 'columns' to the unique constraint deps so it
fires on column changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review feedback for column rename propagation (#9060)

- Remap unique constraint INCLUDE columns on rename. The INCLUDE list
  holds bare name strings (not {column} objects), so renaming an
  included column previously emitted stale DDL. Now mirrors the
  primary key INCLUDE handling.
- Add regression tests covering rename propagation in depChange for
  foreign_key and unique_constraint, including the unique constraint
  INCLUDE case.
- Correct the release note wording from "Create/Edit Table" to
  "Create Table"; the propagation only runs on the new-table path
  (state.oid === undefined).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashesh Vashi <ashesh.vashi@enterprisedb.com>
This commit is contained in:
Dave Page
2026-06-12 23:01:28 +05:30
committed by GitHub
co-authored by Claude Opus 4.8 Ashesh Vashi
parent 6de0f371b2
commit 0b4bb3ea3f
3 changed files with 118 additions and 3 deletions
+1
View File
@@ -44,6 +44,7 @@ Bug fixes
| `Issue #6308 <https://github.com/pgadmin-org/pgadmin4/issues/6308>`_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog.
| `Issue #7596 <https://github.com/pgadmin-org/pgadmin4/issues/7596>`_ - Fix the Query Tool turning into a blank white screen when the runtime has a malformed default locale, by guarding the Query History date/time formatting against the resulting RangeError.
| `Issue #8318 <https://github.com/pgadmin-org/pgadmin4/issues/8318>`_ - Fixed an error ("i.default.find(...) is undefined") that prevented deleting a table or relationship link in the ERD tool when a foreign key referenced a column that had been renamed.
| `Issue #9060 <https://github.com/pgadmin-org/pgadmin4/issues/9060>`_ - Fixed an issue in the Create Table dialog where renaming a column did not update the column references in foreign key and unique constraint definitions for the new table.
| `Issue #9091 <https://github.com/pgadmin-org/pgadmin4/issues/9091>`_ - Fix the Query Tool re-prompting for an unsaved password in a loop and rejecting the re-entered password, by caching the entered password on the server manager when the primary connection is already established.
| `Issue #9128 <https://github.com/pgadmin-org/pgadmin4/issues/9128>`_ - Fixed an issue where the object breadcrumbs popup blocked clicks on the object explorer items beneath it.
| `Issue #9595 <https://github.com/pgadmin-org/pgadmin4/issues/9595>`_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script.
@@ -200,10 +200,25 @@ export class ConstraintsSchema extends BaseUISchema {
disabled: this.inCatalog,
canAddRow: obj.anyColumnAdded,
expandEditOnAdd: true,
depChange: (state)=>{
depChange: (state, source, topState, actionObj)=>{
if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) {
return {foreign_key: []};
}
/* If a column is renamed, sync the foreign key local column references. #9060 */
if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE && actionObj.path[0] == 'columns' &&
actionObj.path[actionObj.path.length-1] == 'name' && state.oid === undefined &&
state.foreign_key?.length) {
let oldName = actionObj.oldState.columns[actionObj.path[1]]?.name,
newName = _.get(state, _.slice(actionObj.path, 0, -1))?.name;
if(oldName && newName && oldName !== newName) {
return {foreign_key: state.foreign_key.map((fk)=>({
...fk,
columns: fk.columns?.map((c)=>(
c.local_column === oldName ? {...c, local_column: newName} : c
)),
}))};
}
}
}
},{
id: 'check_group', type: 'group', label: gettext('Check'), visible: !this.inErd,
@@ -223,7 +238,7 @@ export class ConstraintsSchema extends BaseUISchema {
schema: this.uniqueConsObj,
editable: false, type: 'collection',
group: 'unique_group', mode: ['edit', 'create'],
canEdit: true, canDelete: true, deps:['is_partitioned', 'typname'],
canEdit: true, canDelete: true, deps:['is_partitioned', 'typname', 'columns'],
columns : ['name', 'columns'],
disabled: this.inCatalog,
canAdd: function(state) {
@@ -231,10 +246,26 @@ export class ConstraintsSchema extends BaseUISchema {
},
canAddRow: obj.anyColumnAdded,
expandEditOnAdd: true,
depChange: (state)=>{
depChange: (state, source, topState, actionObj)=>{
if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) {
return {unique_constraint: []};
}
/* If a column is renamed, sync the unique constraint column references. #9060 */
if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE && actionObj.path[0] == 'columns' &&
actionObj.path[actionObj.path.length-1] == 'name' && state.oid === undefined &&
state.unique_constraint?.length) {
let oldName = actionObj.oldState.columns[actionObj.path[1]]?.name,
newName = _.get(state, _.slice(actionObj.path, 0, -1))?.name;
if(oldName && newName && oldName !== newName) {
return {unique_constraint: state.unique_constraint.map((uc)=>({
...uc,
columns: uc.columns?.map((c)=>(
c.column === oldName ? {...c, column: newName} : c
)),
include: uc.include?.map((c)=>(c === oldName ? newName : c)),
}))};
}
}
}
},{
id: 'exclude_group', type: 'group', label: gettext('Exclude'), visible: !this.inErd,
@@ -298,6 +298,89 @@ describe('TableSchema', () => {
});
});
it('depChange column rename propagation', () => {
jest.spyOn(schemaObj, 'getServerVersion').mockReturnValue(110000);
schemaObj.constraintsObj.top = schemaObj;
let actionObj = {
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: ['columns', 0, 'name'],
oldState: {
columns: [{ name: 'old_col' }],
},
};
// Foreign key local_column should be renamed (Create path only).
let fkState = {
oid: undefined,
is_partitioned: false,
columns: [{ name: 'new_col' }],
foreign_key: [{
name: 'fk1',
columns: [
{ local_column: 'old_col', referenced: 'r1' },
{ local_column: 'other', referenced: 'r2' },
],
}],
};
expect(getFieldDepChange(schemaObj.constraintsObj, 'foreign_key')(
fkState, ['columns'], null, actionObj)).toEqual({
foreign_key: [{
name: 'fk1',
columns: [
{ local_column: 'new_col', referenced: 'r1' },
{ local_column: 'other', referenced: 'r2' },
],
}],
});
// Unique constraint columns should be renamed.
let ucState = {
oid: undefined,
is_partitioned: false,
columns: [{ name: 'new_col' }],
unique_constraint: [{
name: 'uc1',
columns: [
{ column: 'old_col' },
{ column: 'other' },
],
include: ['inc1', 'inc2'],
}],
};
expect(getFieldDepChange(schemaObj.constraintsObj, 'unique_constraint')(
ucState, ['columns'], null, actionObj)).toEqual({
unique_constraint: [{
name: 'uc1',
columns: [
{ column: 'new_col' },
{ column: 'other' },
],
include: ['inc1', 'inc2'],
}],
});
// Unique constraint INCLUDE list should be renamed too (Fix #9060).
let ucIncludeState = {
oid: undefined,
is_partitioned: false,
columns: [{ name: 'new_col' }],
unique_constraint: [{
name: 'uc2',
columns: [{ column: 'keycol' }],
include: ['old_col', 'inc2'],
}],
};
expect(getFieldDepChange(schemaObj.constraintsObj, 'unique_constraint')(
ucIncludeState, ['columns'], null, actionObj)).toEqual({
unique_constraint: [{
name: 'uc2',
columns: [{ column: 'keycol' }],
include: ['new_col', 'inc2'],
}],
});
});
it('validate', () => {
let state = {is_partitioned: true};
let setError = jest.fn();