Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/en_US/erd_tool.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,22 @@ The table node shows table details in a graphical representation:
* you can click on the node and drag to move on the canvas.
* 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.

The One to One Link Dialog
***************************

.. image:: images/erd_11_dialog.png
:alt: ERD tool 1-1 dialog
:align: center

The one to one link dialog allows you to:

* Add a one to one relationship between two tables.
* *Local Table* is the table that references a table and has the *one* end point.
* *Local Column* the column that references.
* *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.
* *Referenced Table* is the table that is being referred and has the *one* end point.
* *Referenced Column* the column that is being referred.

The One to Many Link Dialog
***************************

Expand Down
Binary file added docs/en_US/images/erd_11_dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions web/pgadmin/tools/erd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,24 @@ def register_preferences(self):
fields=shortcut_fields
)

self.preference.register(
'keyboard_shortcuts',
'one_to_one',
gettext('One to one link'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'key': {
'key_code': 66,
'char': 'b'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)

self.preference.register(
'keyboard_shortcuts',
'one_to_many',
Expand Down
1 change: 1 addition & 0 deletions web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const ERD_EVENTS = {
CLONE_NODE: 'CLONE_NODE',
DELETE_NODE: 'DELETE_NODE',
SHOW_NOTE: 'SHOW_NOTE',
ONE_TO_ONE: 'ONE_TO_ONE',
ONE_TO_MANY: 'ONE_TO_MANY',
MANY_TO_MANY: 'MANY_TO_MANY',
AUTO_DISTRIBUTE: 'AUTO_DISTRIBUTE',
Expand Down
48 changes: 38 additions & 10 deletions web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import ForeignKeySchema from '../../../../../browser/server_groups/servers/datab
import diffArray from 'diff-arrays-of-objects';
import TableSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
import ColumnSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui';
import UniqueConstraintSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui';
import PrimaryKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui';
import { boundingBoxFromPolygons } from '@projectstorm/geometry';

export default class ERDCore {
Expand Down Expand Up @@ -337,18 +339,19 @@ export default class ERDCore {
let tableData = tableNode.getData();
/* Remove the links if column dropped or primary key removed */
_.differenceWith(oldTableData.columns, tableData.columns, function(existing, incoming) {
if(existing.attnum == incoming.attnum && existing.is_primary_key && !incoming.is_primary_key) {
return false;
}
return existing.attnum == incoming.attnum;
}).forEach((col)=>{
let existPort = tableNode.getPort(tableNode.getPortName(col.attnum));
if(existPort) {
Object.values(existPort.getLinks()).forEach((link)=>{
self.removeOneToManyLink(link);
});
tableNode.removePort(existPort);
}
this.getLeftRightPorts(tableNode, col.attnum).forEach(port => {
if (port) {
Object.values(port.getLinks()).forEach(link => {
self.removeOneToManyLink(link);
});
tableNode.removePort(port);
}
});
});
Object.values(tableNode.getLinks()).forEach(link=>{
link.fireEvent({},'updateLink');
});
}

Expand Down Expand Up @@ -482,6 +485,31 @@ export default class ERDCore {
columns: [col],
})
);
// Below logic is to add one to one relationship
if(onetomanyData.constraint_type === 'primary_key') {
let newPk = new PrimaryKeySchema({},{});
let pkCol = {};
let column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum);
column.is_primary_key = true;
pkCol.column =column.name;
tableData.primary_key = tableData.primary_key || [];
tableData.primary_key.push(
newPk.getNewData({
columns: [pkCol]
})
);

} else if (onetomanyData.constraint_type === 'unique') {
let newUk = new UniqueConstraintSchema({},{});
let ukCol = {};
ukCol.column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum).name;
tableData.unique_constraint = tableData.unique_constraint || [];
tableData.unique_constraint.push(
newUk.getNewData({
columns: [ukCol]
})
);
}
targetNode.setData(tableData);
let newLink = this.addLink(onetomanyData, 'onetomany');
this.clearSelection();
Expand Down
27 changes: 25 additions & 2 deletions web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export default class ERDTool extends React.Component {

_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
'onNoteClose', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel'
]);

Expand Down Expand Up @@ -220,6 +220,7 @@ export default class ERDTool extends React.Component {
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
this.eventBus.registerListener(ERD_EVENTS.DELETE_NODE, this.onDeleteNode);
this.eventBus.registerListener(ERD_EVENTS.SHOW_NOTE, this.onNoteClick);
this.eventBus.registerListener(ERD_EVENTS.ONE_TO_ONE, this.onOneToOneClick);
this.eventBus.registerListener(ERD_EVENTS.ONE_TO_MANY, this.onOneToManyClick);
this.eventBus.registerListener(ERD_EVENTS.MANY_TO_MANY, this.onManyToManyClick);
this.eventBus.registerListener(ERD_EVENTS.AUTO_DISTRIBUTE, this.onAutoDistribute);
Expand Down Expand Up @@ -265,6 +266,9 @@ export default class ERDTool extends React.Component {
[this.state.preferences.add_edit_note, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.SHOW_NOTE);
}],
[this.state.preferences.one_to_one, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE);
}],
[this.state.preferences.one_to_many, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
}],
Expand Down Expand Up @@ -397,7 +401,7 @@ export default class ERDTool extends React.Component {
serverInfo, callback
});
};
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog' || dialogName === 'onetoone_dialog') {
return (title, attributes, callback)=>{
this.erdDialogs.showRelationDialog(dialogName, {
title, attributes, tableNodes: this.diagram.getModel().getNodesDict(),
Expand Down Expand Up @@ -429,6 +433,17 @@ export default class ERDTool extends React.Component {
if(this.diagram.anyDuplicateNodeName(newData, oldData)) {
return gettext('Table name already exists');
}
// If a column that is part of a foreign key is removed, the foreign key constraint should also be removed.
_.differenceWith(oldData.columns, newData.columns, function(existing, incoming) {
return existing.attnum == incoming.attnum;
}).forEach(colm=>{
newData.foreign_key?.forEach((theFkRow, index)=>{
let fkCols = theFkRow.columns[0];
if (fkCols.local_column === colm.name) {
newData.foreign_key.splice(index,1);
}
});
});
node.setData(newData);
this.diagram.syncTableLinks(node, oldData);
this.diagram.repaint();
Expand Down Expand Up @@ -774,6 +789,14 @@ export default class ERDTool extends React.Component {
}, 1000);
}

onOneToOneClick() {
let dialog = this.getDialog('onetoone_dialog');
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};
dialog(gettext('One to one relation'), initData, (newData)=>{
this.diagram.addOneToManyLink(newData);
});
}

onOneToManyClick() {
let dialog = this.getDialog('onetomany_dialog');
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
'save': true,
'edit-table': true,
'clone-table': true,
'one-to-one': true,
'one-to-many': true,
'many-to-many': true,
'show-note': true,
Expand Down Expand Up @@ -121,6 +122,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
[ERD_EVENTS.SINGLE_NODE_SELECTED, (selected)=>{
setDisableButton('edit-table', !selected);
setDisableButton('clone-table', !selected);
setDisableButton('one-to-one', !selected);
setDisableButton('one-to-many', !selected);
setDisableButton('many-to-many', !selected);
setDisableButton('show-note', !selected);
Expand Down Expand Up @@ -210,12 +212,17 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
}} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('One-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>1M</span>}
<PgIconButton title={gettext('One-to-One Relation')} icon={<span style={{letterSpacing: '-1px'}}>1 - 1</span>}
shortcut={preferences.one_to_one} disabled={buttonsDisabled['one-to-one']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE);
}} />
<PgIconButton title={gettext('One-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>1 - M</span>}
shortcut={preferences.one_to_many} disabled={buttonsDisabled['one-to-many']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
}} />
<PgIconButton title={gettext('Many-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>MM</span>}
<PgIconButton title={gettext('Many-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>M - M</span>}
shortcut={preferences.many_to_many} disabled={buttonsDisabled['many-to-many']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.MANY_TO_MANY);
Expand Down
106 changes: 106 additions & 0 deletions web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToOneDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import gettext from 'sources/gettext';
import { isEmptyString } from 'sources/validators';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import _ from 'lodash';

class OneToOneSchema extends BaseUISchema {
constructor(fieldOptions={}, initValues={}, localTableData={}) {
super({
local_table_uid: undefined,
local_column_attnum: undefined,
referenced_table_uid: undefined,
referenced_column_attnum: undefined,
constraint_type: undefined,
...initValues,
});
this.fieldOptions = fieldOptions;
this.localTableData = localTableData;
}

isVisible (state) {
let colName = _.find(this.localTableData.getData().columns, col => col.attnum === state.local_column_attnum)?.name;
let {pkCols, ukCols} = this.localTableData.getConstraintCols();
return !((pkCols.includes(colName) || ukCols.includes(colName)) || isEmptyString(state.local_column_attnum));
}
get baseFields() {
return [{
id: 'local_table_uid', label: gettext('Local Table'),
type: 'select', readonly: true, controlProps: {allowClear: false},
options: this.fieldOptions.local_table_uid,
},{
id: 'local_column_attnum', label: gettext('Local Column'),
type: 'select', options: this.fieldOptions.local_column_attnum,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'constraint_type', label: gettext('Select constraint'),
type: 'toggle', deps: ['local_column_attnum'],
options: [
{label: 'Primary Key', value: 'primary_key'},
{label: 'Unique', value: 'unique'},
],
visible: this.isVisible,
depChange: (state, source)=>{
if (source[0] === 'local_column_attnum' && this.isVisible(state)) {
return {constraint_type: 'unique'};
} else if (source[0] === 'local_column_attnum') {
return {constraint_type: ''};
}
}, helpMessage: gettext('A constraint is required to implement One to One relationship.')
}, {
id: 'referenced_table_uid', label: gettext('Referenced Table'),
type: 'select', options: this.fieldOptions.referenced_table_uid,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'referenced_column_attnum', label: gettext('Referenced Column'),
controlProps: {allowClear: false}, deps: ['referenced_table_uid'], noEmpty: true,
type: (state)=>({
type: 'select',
options: state.referenced_table_uid ? ()=>this.fieldOptions.getRefColumns(state.referenced_table_uid) : [],
optionsReloadBasis: state.referenced_table_uid,
}),
}];
}

validate(state, setError) {
let tableData = this.localTableData.getData();
if (tableData.primary_key.length && state.constraint_type === 'primary_key') {
setError('constraint_type', gettext('Primary key already exists, please select different constraint.'));
return true;
}
return false;
}
}

export function getOneToOneDialogSchema(attributes, tableNodesDict) {
let tablesData = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
});

return new OneToOneSchema({
local_table_uid: tablesData,
local_column_attnum: tableNodesDict[attributes.local_table_uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
}),
referenced_table_uid: tablesData,
getRefColumns: (uid)=>{
return tableNodesDict[uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
});
},
}, attributes, tableNodesDict[attributes.local_table_uid]);
}
3 changes: 3 additions & 0 deletions web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import {getTableDialogSchema} from './TableDialog';
import {getOneToManyDialogSchema} from './OneToManyDialog';
import {getManyToManyDialogSchema} from './ManyToManyDialog';
import {getOneToOneDialogSchema} from './OneToOneDialog';

import pgAdmin from 'sources/pgadmin';
import SchemaView from '../../../../../../static/js/SchemaView';
Expand Down Expand Up @@ -67,6 +68,8 @@ export default class ERDDialogs {
schema = getOneToManyDialogSchema(params.attributes, params.tableNodes);
} else if(dialogName === 'manytomany_dialog') {
schema = getManyToManyDialogSchema(params.attributes, params.tableNodes);
} else if(dialogName === 'onetoone_dialog') {
schema = getOneToOneDialogSchema(params.attributes, params.tableNodes);
}

this.modal.showModal(params.title, (closeModal)=>{
Expand Down
Loading