Skip to content

Commit 2fc6558

Browse files
Add support for one to one relationship in the ERD tool. #5128
1 parent bf7f8cd commit 2fc6558

File tree

12 files changed

+334
-19
lines changed

12 files changed

+334
-19
lines changed

docs/en_US/erd_tool.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,22 @@ The table node shows table details in a graphical representation:
207207
* you can click on the node and drag to move on the canvas.
208208
* Upon double click on the table node or by clicking the edit button from the toolbar, the table dialog opens where you can change the table details. Refer :ref:`table dialog <table_dialog>` for information on different fields.
209209

210+
The One to One Link Dialog
211+
***************************
212+
213+
.. image:: images/erd_11_dialog.png
214+
:alt: ERD tool 1-1 dialog
215+
:align: center
216+
217+
The one to one link dialog allows you to:
218+
219+
* Add a one to one relationship between two tables.
220+
* *Local Table* is the table that references a table and has the *one* end point.
221+
* *Local Column* the column that references.
222+
* *Select Constraint* To implement one to one relationship, the *Local Column* must have primaty key or unique constraint. The default is a unique constraint. Please note that this field is visible only when the selected *Local Column* does not have either of the mentioned constraints.
223+
* *Referenced Table* is the table that is being referred and has the *one* end point.
224+
* *Referenced Column* the column that is being referred.
225+
210226
The One to Many Link Dialog
211227
***************************
212228

87.1 KB
Loading

web/pgadmin/tools/erd/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,24 @@ def register_preferences(self):
246246
fields=shortcut_fields
247247
)
248248

249+
self.preference.register(
250+
'keyboard_shortcuts',
251+
'one_to_one',
252+
gettext('One to one link'),
253+
'keyboardshortcut',
254+
{
255+
'alt': True,
256+
'shift': False,
257+
'control': True,
258+
'key': {
259+
'key_code': 66,
260+
'char': 'b'
261+
}
262+
},
263+
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
264+
fields=shortcut_fields
265+
)
266+
249267
self.preference.register(
250268
'keyboard_shortcuts',
251269
'one_to_many',

web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const ERD_EVENTS = {
99
CLONE_NODE: 'CLONE_NODE',
1010
DELETE_NODE: 'DELETE_NODE',
1111
SHOW_NOTE: 'SHOW_NOTE',
12+
ONE_TO_ONE: 'ONE_TO_ONE',
1213
ONE_TO_MANY: 'ONE_TO_MANY',
1314
MANY_TO_MANY: 'MANY_TO_MANY',
1415
AUTO_DISTRIBUTE: 'AUTO_DISTRIBUTE',

web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import ForeignKeySchema from '../../../../../browser/server_groups/servers/datab
2222
import diffArray from 'diff-arrays-of-objects';
2323
import TableSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
2424
import ColumnSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui';
25+
import UniqueConstraintSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui';
26+
import PrimaryKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui';
2527
import { boundingBoxFromPolygons } from '@projectstorm/geometry';
2628

2729
export default class ERDCore {
@@ -337,18 +339,19 @@ export default class ERDCore {
337339
let tableData = tableNode.getData();
338340
/* Remove the links if column dropped or primary key removed */
339341
_.differenceWith(oldTableData.columns, tableData.columns, function(existing, incoming) {
340-
if(existing.attnum == incoming.attnum && existing.is_primary_key && !incoming.is_primary_key) {
341-
return false;
342-
}
343342
return existing.attnum == incoming.attnum;
344343
}).forEach((col)=>{
345-
let existPort = tableNode.getPort(tableNode.getPortName(col.attnum));
346-
if(existPort) {
347-
Object.values(existPort.getLinks()).forEach((link)=>{
348-
self.removeOneToManyLink(link);
349-
});
350-
tableNode.removePort(existPort);
351-
}
344+
this.getLeftRightPorts(tableNode, col.attnum).forEach(port => {
345+
if (port) {
346+
Object.values(port.getLinks()).forEach(link => {
347+
self.removeOneToManyLink(link);
348+
});
349+
tableNode.removePort(port);
350+
}
351+
});
352+
});
353+
Object.values(tableNode.getLinks()).forEach(link=>{
354+
link.fireEvent({},'updateLink');
352355
});
353356
}
354357

@@ -482,6 +485,31 @@ export default class ERDCore {
482485
columns: [col],
483486
})
484487
);
488+
// Below logic is to add one to one relationship
489+
if(onetomanyData.constraint_type === 'primary_key') {
490+
let newPk = new PrimaryKeySchema({},{});
491+
let pkCol = {};
492+
let column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum);
493+
column.is_primary_key = true;
494+
pkCol.column =column.name;
495+
tableData.primary_key = tableData.primary_key || [];
496+
tableData.primary_key.push(
497+
newPk.getNewData({
498+
columns: [pkCol]
499+
})
500+
);
501+
502+
} else if (onetomanyData.constraint_type === 'unique') {
503+
let newUk = new UniqueConstraintSchema({},{});
504+
let ukCol = {};
505+
ukCol.column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum).name;
506+
tableData.unique_constraint = tableData.unique_constraint || [];
507+
tableData.unique_constraint.push(
508+
newUk.getNewData({
509+
columns: [ukCol]
510+
})
511+
);
512+
}
485513
targetNode.setData(tableData);
486514
let newLink = this.addLink(onetomanyData, 'onetomany');
487515
this.clearSelection();

web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export default class ERDTool extends React.Component {
145145

146146
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
147147
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
148-
'onNoteClose', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
148+
'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
149149
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel'
150150
]);
151151

@@ -220,6 +220,7 @@ export default class ERDTool extends React.Component {
220220
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
221221
this.eventBus.registerListener(ERD_EVENTS.DELETE_NODE, this.onDeleteNode);
222222
this.eventBus.registerListener(ERD_EVENTS.SHOW_NOTE, this.onNoteClick);
223+
this.eventBus.registerListener(ERD_EVENTS.ONE_TO_ONE, this.onOneToOneClick);
223224
this.eventBus.registerListener(ERD_EVENTS.ONE_TO_MANY, this.onOneToManyClick);
224225
this.eventBus.registerListener(ERD_EVENTS.MANY_TO_MANY, this.onManyToManyClick);
225226
this.eventBus.registerListener(ERD_EVENTS.AUTO_DISTRIBUTE, this.onAutoDistribute);
@@ -265,6 +266,9 @@ export default class ERDTool extends React.Component {
265266
[this.state.preferences.add_edit_note, ()=>{
266267
this.eventBus.fireEvent(ERD_EVENTS.SHOW_NOTE);
267268
}],
269+
[this.state.preferences.one_to_one, ()=>{
270+
this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE);
271+
}],
268272
[this.state.preferences.one_to_many, ()=>{
269273
this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
270274
}],
@@ -397,7 +401,7 @@ export default class ERDTool extends React.Component {
397401
serverInfo, callback
398402
});
399403
};
400-
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
404+
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog' || dialogName === 'onetoone_dialog') {
401405
return (title, attributes, callback)=>{
402406
this.erdDialogs.showRelationDialog(dialogName, {
403407
title, attributes, tableNodes: this.diagram.getModel().getNodesDict(),
@@ -429,6 +433,17 @@ export default class ERDTool extends React.Component {
429433
if(this.diagram.anyDuplicateNodeName(newData, oldData)) {
430434
return gettext('Table name already exists');
431435
}
436+
// If a column that is part of a foreign key is removed, the foreign key constraint should also be removed.
437+
_.differenceWith(oldData.columns, newData.columns, function(existing, incoming) {
438+
return existing.attnum == incoming.attnum;
439+
}).forEach(colm=>{
440+
newData.foreign_key?.forEach((theFkRow, index)=>{
441+
let fkCols = theFkRow.columns[0];
442+
if (fkCols.local_column === colm.name) {
443+
newData.foreign_key.splice(index,1);
444+
}
445+
});
446+
});
432447
node.setData(newData);
433448
this.diagram.syncTableLinks(node, oldData);
434449
this.diagram.repaint();
@@ -774,6 +789,14 @@ export default class ERDTool extends React.Component {
774789
}, 1000);
775790
}
776791

792+
onOneToOneClick() {
793+
let dialog = this.getDialog('onetoone_dialog');
794+
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};
795+
dialog(gettext('One to one relation'), initData, (newData)=>{
796+
this.diagram.addOneToManyLink(newData);
797+
});
798+
}
799+
777800
onOneToManyClick() {
778801
let dialog = this.getDialog('onetomany_dialog');
779802
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};

web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
5454
'save': true,
5555
'edit-table': true,
5656
'clone-table': true,
57+
'one-to-one': true,
5758
'one-to-many': true,
5859
'many-to-many': true,
5960
'show-note': true,
@@ -121,6 +122,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
121122
[ERD_EVENTS.SINGLE_NODE_SELECTED, (selected)=>{
122123
setDisableButton('edit-table', !selected);
123124
setDisableButton('clone-table', !selected);
125+
setDisableButton('one-to-one', !selected);
124126
setDisableButton('one-to-many', !selected);
125127
setDisableButton('many-to-many', !selected);
126128
setDisableButton('show-note', !selected);
@@ -210,12 +212,17 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
210212
}} />
211213
</PgButtonGroup>
212214
<PgButtonGroup size="small">
213-
<PgIconButton title={gettext('One-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>1M</span>}
215+
<PgIconButton title={gettext('One-to-One Relation')} icon={<span style={{letterSpacing: '-1px'}}>1 - 1</span>}
216+
shortcut={preferences.one_to_one} disabled={buttonsDisabled['one-to-one']}
217+
onClick={()=>{
218+
eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE);
219+
}} />
220+
<PgIconButton title={gettext('One-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>1 - M</span>}
214221
shortcut={preferences.one_to_many} disabled={buttonsDisabled['one-to-many']}
215222
onClick={()=>{
216223
eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
217224
}} />
218-
<PgIconButton title={gettext('Many-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>MM</span>}
225+
<PgIconButton title={gettext('Many-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>M - M</span>}
219226
shortcut={preferences.many_to_many} disabled={buttonsDisabled['many-to-many']}
220227
onClick={()=>{
221228
eventBus.fireEvent(ERD_EVENTS.MANY_TO_MANY);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/////////////////////////////////////////////////////////////
2+
//
3+
// pgAdmin 4 - PostgreSQL Tools
4+
//
5+
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
6+
// This software is released under the PostgreSQL Licence
7+
//
8+
//////////////////////////////////////////////////////////////
9+
10+
import gettext from 'sources/gettext';
11+
import { isEmptyString } from 'sources/validators';
12+
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
13+
import _ from 'lodash';
14+
15+
class OneToOneSchema extends BaseUISchema {
16+
constructor(fieldOptions={}, initValues={}, localTableData={}) {
17+
super({
18+
local_table_uid: undefined,
19+
local_column_attnum: undefined,
20+
referenced_table_uid: undefined,
21+
referenced_column_attnum: undefined,
22+
constraint_type: undefined,
23+
...initValues,
24+
});
25+
this.fieldOptions = fieldOptions;
26+
this.localTableData = localTableData;
27+
}
28+
29+
isVisible (state) {
30+
let colName = _.find(this.localTableData.getData().columns, col => col.attnum === state.local_column_attnum)?.name;
31+
let {pkCols, ukCols} = this.localTableData.getConstraintCols();
32+
return !((pkCols.includes(colName) || ukCols.includes(colName)) || isEmptyString(state.local_column_attnum));
33+
}
34+
get baseFields() {
35+
return [{
36+
id: 'local_table_uid', label: gettext('Local Table'),
37+
type: 'select', readonly: true, controlProps: {allowClear: false},
38+
options: this.fieldOptions.local_table_uid,
39+
},{
40+
id: 'local_column_attnum', label: gettext('Local Column'),
41+
type: 'select', options: this.fieldOptions.local_column_attnum,
42+
controlProps: {allowClear: false}, noEmpty: true,
43+
},{
44+
id: 'constraint_type', label: gettext('Select constraint'),
45+
type: 'toggle', deps: ['local_column_attnum'],
46+
options: [
47+
{label: 'Primary Key', value: 'primary_key'},
48+
{label: 'Unique', value: 'unique'},
49+
],
50+
visible: this.isVisible,
51+
depChange: (state, source)=>{
52+
if (source[0] === 'local_column_attnum' && this.isVisible(state)) {
53+
return {constraint_type: 'unique'};
54+
} else if (source[0] === 'local_column_attnum') {
55+
return {constraint_type: ''};
56+
}
57+
}, helpMessage: gettext('A constraint is required to implement One to One relationship.')
58+
}, {
59+
id: 'referenced_table_uid', label: gettext('Referenced Table'),
60+
type: 'select', options: this.fieldOptions.referenced_table_uid,
61+
controlProps: {allowClear: false}, noEmpty: true,
62+
},{
63+
id: 'referenced_column_attnum', label: gettext('Referenced Column'),
64+
controlProps: {allowClear: false}, deps: ['referenced_table_uid'], noEmpty: true,
65+
type: (state)=>({
66+
type: 'select',
67+
options: state.referenced_table_uid ? ()=>this.fieldOptions.getRefColumns(state.referenced_table_uid) : [],
68+
optionsReloadBasis: state.referenced_table_uid,
69+
}),
70+
}];
71+
}
72+
73+
validate(state, setError) {
74+
let tableData = this.localTableData.getData();
75+
if (tableData.primary_key.length && state.constraint_type === 'primary_key') {
76+
setError('constraint_type', gettext('Primary key already exists, please select different constraint.'));
77+
return true;
78+
}
79+
return false;
80+
}
81+
}
82+
83+
export function getOneToOneDialogSchema(attributes, tableNodesDict) {
84+
let tablesData = [];
85+
_.forEach(tableNodesDict, (node, uid)=>{
86+
let [schema, name] = node.getSchemaTableName();
87+
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
88+
});
89+
90+
return new OneToOneSchema({
91+
local_table_uid: tablesData,
92+
local_column_attnum: tableNodesDict[attributes.local_table_uid].getColumns().map((col)=>{
93+
return {
94+
value: col.attnum, label: col.name, 'image': 'icon-column',
95+
};
96+
}),
97+
referenced_table_uid: tablesData,
98+
getRefColumns: (uid)=>{
99+
return tableNodesDict[uid].getColumns().map((col)=>{
100+
return {
101+
value: col.attnum, label: col.name, 'image': 'icon-column',
102+
};
103+
});
104+
},
105+
}, attributes, tableNodesDict[attributes.local_table_uid]);
106+
}

web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import {getTableDialogSchema} from './TableDialog';
1111
import {getOneToManyDialogSchema} from './OneToManyDialog';
1212
import {getManyToManyDialogSchema} from './ManyToManyDialog';
13+
import {getOneToOneDialogSchema} from './OneToOneDialog';
1314

1415
import pgAdmin from 'sources/pgadmin';
1516
import SchemaView from '../../../../../../static/js/SchemaView';
@@ -67,6 +68,8 @@ export default class ERDDialogs {
6768
schema = getOneToManyDialogSchema(params.attributes, params.tableNodes);
6869
} else if(dialogName === 'manytomany_dialog') {
6970
schema = getManyToManyDialogSchema(params.attributes, params.tableNodes);
71+
} else if(dialogName === 'onetoone_dialog') {
72+
schema = getOneToOneDialogSchema(params.attributes, params.tableNodes);
7073
}
7174

7275
this.modal.showModal(params.title, (closeModal)=>{

0 commit comments

Comments
 (0)