Skip to content

Commit 0b4bb3e

Browse files
dpageclaudeasheshv
authored
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>
1 parent 6de0f37 commit 0b4bb3e

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

docs/en_US/release_notes_9_16.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Bug fixes
4444
| `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.
4545
| `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.
4646
| `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.
47+
| `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.
4748
| `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.
4849
| `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.
4950
| `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.

web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,25 @@ export class ConstraintsSchema extends BaseUISchema {
200200
disabled: this.inCatalog,
201201
canAddRow: obj.anyColumnAdded,
202202
expandEditOnAdd: true,
203-
depChange: (state)=>{
203+
depChange: (state, source, topState, actionObj)=>{
204204
if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) {
205205
return {foreign_key: []};
206206
}
207+
/* If a column is renamed, sync the foreign key local column references. #9060 */
208+
if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE && actionObj.path[0] == 'columns' &&
209+
actionObj.path[actionObj.path.length-1] == 'name' && state.oid === undefined &&
210+
state.foreign_key?.length) {
211+
let oldName = actionObj.oldState.columns[actionObj.path[1]]?.name,
212+
newName = _.get(state, _.slice(actionObj.path, 0, -1))?.name;
213+
if(oldName && newName && oldName !== newName) {
214+
return {foreign_key: state.foreign_key.map((fk)=>({
215+
...fk,
216+
columns: fk.columns?.map((c)=>(
217+
c.local_column === oldName ? {...c, local_column: newName} : c
218+
)),
219+
}))};
220+
}
221+
}
207222
}
208223
},{
209224
id: 'check_group', type: 'group', label: gettext('Check'), visible: !this.inErd,
@@ -223,18 +238,34 @@ export class ConstraintsSchema extends BaseUISchema {
223238
schema: this.uniqueConsObj,
224239
editable: false, type: 'collection',
225240
group: 'unique_group', mode: ['edit', 'create'],
226-
canEdit: true, canDelete: true, deps:['is_partitioned', 'typname'],
241+
canEdit: true, canDelete: true, deps:['is_partitioned', 'typname', 'columns'],
227242
columns : ['name', 'columns'],
228243
disabled: this.inCatalog,
229244
canAdd: function(state) {
230245
return obj.canAdd(state);
231246
},
232247
canAddRow: obj.anyColumnAdded,
233248
expandEditOnAdd: true,
234-
depChange: (state)=>{
249+
depChange: (state, source, topState, actionObj)=>{
235250
if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) {
236251
return {unique_constraint: []};
237252
}
253+
/* If a column is renamed, sync the unique constraint column references. #9060 */
254+
if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE && actionObj.path[0] == 'columns' &&
255+
actionObj.path[actionObj.path.length-1] == 'name' && state.oid === undefined &&
256+
state.unique_constraint?.length) {
257+
let oldName = actionObj.oldState.columns[actionObj.path[1]]?.name,
258+
newName = _.get(state, _.slice(actionObj.path, 0, -1))?.name;
259+
if(oldName && newName && oldName !== newName) {
260+
return {unique_constraint: state.unique_constraint.map((uc)=>({
261+
...uc,
262+
columns: uc.columns?.map((c)=>(
263+
c.column === oldName ? {...c, column: newName} : c
264+
)),
265+
include: uc.include?.map((c)=>(c === oldName ? newName : c)),
266+
}))};
267+
}
268+
}
238269
}
239270
},{
240271
id: 'exclude_group', type: 'group', label: gettext('Exclude'), visible: !this.inErd,

web/regression/javascript/schema_ui_files/table.ui.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,89 @@ describe('TableSchema', () => {
298298
});
299299
});
300300

301+
it('depChange column rename propagation', () => {
302+
jest.spyOn(schemaObj, 'getServerVersion').mockReturnValue(110000);
303+
schemaObj.constraintsObj.top = schemaObj;
304+
305+
let actionObj = {
306+
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
307+
path: ['columns', 0, 'name'],
308+
oldState: {
309+
columns: [{ name: 'old_col' }],
310+
},
311+
};
312+
313+
// Foreign key local_column should be renamed (Create path only).
314+
let fkState = {
315+
oid: undefined,
316+
is_partitioned: false,
317+
columns: [{ name: 'new_col' }],
318+
foreign_key: [{
319+
name: 'fk1',
320+
columns: [
321+
{ local_column: 'old_col', referenced: 'r1' },
322+
{ local_column: 'other', referenced: 'r2' },
323+
],
324+
}],
325+
};
326+
expect(getFieldDepChange(schemaObj.constraintsObj, 'foreign_key')(
327+
fkState, ['columns'], null, actionObj)).toEqual({
328+
foreign_key: [{
329+
name: 'fk1',
330+
columns: [
331+
{ local_column: 'new_col', referenced: 'r1' },
332+
{ local_column: 'other', referenced: 'r2' },
333+
],
334+
}],
335+
});
336+
337+
// Unique constraint columns should be renamed.
338+
let ucState = {
339+
oid: undefined,
340+
is_partitioned: false,
341+
columns: [{ name: 'new_col' }],
342+
unique_constraint: [{
343+
name: 'uc1',
344+
columns: [
345+
{ column: 'old_col' },
346+
{ column: 'other' },
347+
],
348+
include: ['inc1', 'inc2'],
349+
}],
350+
};
351+
expect(getFieldDepChange(schemaObj.constraintsObj, 'unique_constraint')(
352+
ucState, ['columns'], null, actionObj)).toEqual({
353+
unique_constraint: [{
354+
name: 'uc1',
355+
columns: [
356+
{ column: 'new_col' },
357+
{ column: 'other' },
358+
],
359+
include: ['inc1', 'inc2'],
360+
}],
361+
});
362+
363+
// Unique constraint INCLUDE list should be renamed too (Fix #9060).
364+
let ucIncludeState = {
365+
oid: undefined,
366+
is_partitioned: false,
367+
columns: [{ name: 'new_col' }],
368+
unique_constraint: [{
369+
name: 'uc2',
370+
columns: [{ column: 'keycol' }],
371+
include: ['old_col', 'inc2'],
372+
}],
373+
};
374+
expect(getFieldDepChange(schemaObj.constraintsObj, 'unique_constraint')(
375+
ucIncludeState, ['columns'], null, actionObj)).toEqual({
376+
unique_constraint: [{
377+
name: 'uc2',
378+
columns: [{ column: 'keycol' }],
379+
include: ['new_col', 'inc2'],
380+
}],
381+
});
382+
});
383+
301384
it('validate', () => {
302385
let state = {is_partitioned: true};
303386
let setError = jest.fn();

0 commit comments

Comments
 (0)