Added the ability to generate ERDs for tables. #4756

This commit is contained in:
Aditya Toshniwal 2022-11-09 11:36:04 +05:30 committed by GitHub
parent beb43c69dc
commit af32e3c296
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 136 additions and 12 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -196,6 +196,10 @@ Use the fields on the *Options* panel to manage ERD preferences.
generated by the ERD Tool will add DROP table DDL before each CREATE
table DDL.
* *Table Relation Depth* is useful when generating an ERD for a table.
It allows to set the limit on the depth level pgAdmin should traverse
to find the relations. Use -1 to set no limit.
The Graphs Node
***************

View File

@ -121,7 +121,11 @@ define('pgadmin.node.table', [
applies: ['object', 'context'], callback: 'count_table_rows',
category: 'Count', priority: 2, label: gettext('Count Rows'),
enable: true,
},
},{
name: 'generate_erd', node: 'table', module: this,
applies: ['object', 'context'], callback: 'generate_erd',
category: 'erd', priority: 5, label: gettext('ERD For Table'),
}
]);
pgBrowser.Events.on(
'pgadmin:browser:node:table:updated', this.onTableUpdated, this
@ -289,6 +293,14 @@ define('pgadmin.node.table', [
t.unload(i);
});
},
/* Generate the ERD */
generate_erd: function(args) {
let input = args || {},
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i ? t.itemData(i) : undefined;
pgAdmin.Tools.ERD.showErdTool(d, i, true);
},
},
getSchema: function(treeNodeInfo, itemNodeData) {
return getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser);

View File

@ -13,6 +13,7 @@ SELECT ct.oid,
confrelid,
nl.nspname as fknsp,
cl.relname as fktab,
nr.oid as refnspoid,
nr.nspname as refnsp,
cr.relname as reftab,
description as comment,

View File

@ -0,0 +1,10 @@
SELECT
co.conrelid as confrelid, co.conname, nl.oid as refnspoid
FROM pg_catalog.pg_depend dep
JOIN pg_catalog.pg_constraint co ON dep.objid=co.oid
JOIN pg_catalog.pg_class cl ON cl.oid=co.conrelid
JOIN pg_catalog.pg_namespace nl ON nl.oid=cl.relnamespace
WHERE dep.refobjid={{oid}}::OID
AND deptype = 'n'

View File

@ -395,6 +395,22 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
status=200
)
def get_fk_ref_tables(self, tid):
"""
This function get the depending tables of the current table.
The tables depending on tid table using FK relation.
Args:
tid: Table ID
"""
sql = render_template("/".join([self.table_template_path,
'fk_ref_tables.sql']), oid=tid)
status, res = self.conn.execute_dict(sql)
if not status:
return status, res
return status, res['rows']
def get_table_statistics(self, scid, tid):
"""
Statistics

View File

@ -98,7 +98,7 @@ define('pgadmin.node.database', [
},{
name: 'generate_erd', node: 'database', module: this,
applies: ['object', 'context'], callback: 'generate_erd',
category: 'erd', priority: 5, label: gettext('Generate ERD'),
category: 'erd', priority: 5, label: gettext('ERD For Database'),
enable: (node) => {
return node.allowConn;
}

View File

@ -7,6 +7,7 @@
//
//////////////////////////////////////////////////////////////
import { io } from 'socketio';
import gettext from 'sources/gettext';
export function openSocket(namespace, options) {
return new Promise((resolve, reject)=>{
@ -41,5 +42,8 @@ export function socketApiGet(socket, endpoint, params) {
socket.on(`${endpoint}_failed`, (data)=>{
reject(data);
});
socket.on('disconnect', ()=>{
reject(gettext('Connection to pgAdmin server has been lost'));
});
});
}

View File

@ -386,6 +386,20 @@ class ERDModule(PgAdminModule):
)
)
self.preference.register(
'options',
'table_relation_depth',
gettext('Table Relation Depth'),
'integer',
-1,
category_label=PREF_LABEL_OPTIONS,
help_str=gettext(
'The maximum depth pgAdmin should traverse to find '
'related tables when generating an ERD for a table. '
'Use -1 for no limit.'
)
)
blueprint = ERDModule(MODULE_NAME, __name__, static_url_path='/static')
@ -621,7 +635,9 @@ def tables(params):
try:
helper = ERDHelper(params['trans_id'], params['sid'], params['did'])
_get_connection(params['sid'], params['did'], params['trans_id'])
status, tables = helper.get_all_tables()
status, tables = helper.get_all_tables(params.get('scid', None),
params.get('tid', None))
if not status:
socketio.emit('tables_failed', tables,

View File

@ -193,6 +193,11 @@ export default class ERDModule {
+`&did=${parentData.database._id}`
+`&gen=${gen}`;
if(parentData.table) {
openUrl += `&scid=${parentData.schema._id}`
+`&tid=${parentData.table._id}`;
}
return openUrl;
}

View File

@ -355,7 +355,6 @@ class ERDTool extends React.Component {
} else {
window.removeEventListener('beforeunload', this.onBeforeUnload);
}
}
componentWillUnmount() {
@ -660,7 +659,7 @@ class ERDTool extends React.Component {
}
onSQLClick(sqlWithDrop=false) {
let scriptHeader = gettext('-- This script was generated by a beta version of the ERD tool in pgAdmin 4.\n');
let scriptHeader = gettext('-- This script was generated by the ERD tool in pgAdmin 4.\n');
scriptHeader += gettext('-- Please log an issue at https://redmine.postgresql.org/projects/pgadmin4/issues/new if you find any bugs, including reproduction steps.\n');
let url = url_for('erd.sql', {
@ -926,12 +925,18 @@ class ERDTool extends React.Component {
sgid: parseInt(this.props.params.sgid),
sid: parseInt(this.props.params.sid),
did: parseInt(this.props.params.did),
scid: this.props.params.scid ? parseInt(this.props.params.scid) : undefined,
tid: this.props.params.tid ? parseInt(this.props.params.tid) : undefined,
});
} catch (error) {
this.handleAxiosCatch(error);
}
socket?.disconnect();
this.diagram.deserializeData(resData);
try {
this.diagram.deserializeData(resData);
} catch (error) {
this.handleAxiosCatch(error);
}
this.setLoading(null);
}
@ -964,6 +969,8 @@ ERDTool.propTypes = {
sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
scid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
tid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
server_type: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
bgcolor: PropTypes.string,

View File

@ -13,6 +13,7 @@ from pgadmin.browser.server_groups.servers.databases.schemas.utils \
import get_schemas
from pgadmin.browser.server_groups.servers.databases.schemas.utils \
import DataTypeReader
from pgadmin.utils.preferences import Preferences
class ERDTableView(BaseTableView, DataTypeReader):
@ -31,7 +32,7 @@ class ERDTableView(BaseTableView, DataTypeReader):
return DataTypeReader.get_types(self, self.conn, condition, True)
@BaseTableView.check_precondition
def fetch_all_tables(self, conn_id=None, did=None, sid=None):
def fetch_all_tables(self, did=None, sid=None):
status, schemas = get_schemas(self.conn, show_system_objects=False)
if not status:
return status, schemas
@ -47,6 +48,44 @@ class ERDTableView(BaseTableView, DataTypeReader):
return True, all_tables
@BaseTableView.check_precondition
def traverse_related_tables(self, did=None, sid=None, scid=None,
tid=None, related={}, maxdepth=0, currdepth=0):
status, res = \
BaseTableView.fetch_tables(self, sid, did, scid, tid=tid)
if not status:
return status, res
related[tid] = res
# Max depth limit reached
if currdepth == maxdepth:
new_fks = []
for fk in related[tid].pop('foreign_key', []):
if fk['confrelid'] in related:
new_fks.append(fk)
related[tid]['foreign_key'] = new_fks
return True, None
status, depending_res = BaseTableView.get_fk_ref_tables(
self, tid)
if not status:
return status, depending_res
for fk in [*res.get('foreign_key', []), *depending_res]:
if fk['confrelid'] in related:
continue
status, res = self.traverse_related_tables(
did=did, sid=sid, scid=fk['refnspoid'], tid=fk['confrelid'],
related=related, maxdepth=maxdepth, currdepth=currdepth + 1)
if not status:
return status, res
return True, None
class ERDHelper:
def __init__(self, conn_id, sid, did):
@ -66,8 +105,18 @@ class ERDHelper:
data=data, with_drop=with_drop)
return SQL
def get_all_tables(self):
status, res = self.table_view.fetch_all_tables(
conn_id=self.conn_id, did=self.did, sid=self.sid)
def get_all_tables(self, scid, tid):
if tid is None and scid is None:
status, res = self.table_view.fetch_all_tables(
did=self.did, sid=self.sid)
else:
prefs = Preferences.module('erd')
table_relation_depth = prefs.preference('table_relation_depth')
related = {}
status, res = self.table_view.traverse_related_tables(
did=self.did, sid=self.sid, scid=scid, tid=tid,
related=related, maxdepth=table_relation_depth.get()
)
if status:
res = list(related.values())
return status, res

View File

@ -393,7 +393,7 @@ describe('ERDTool', ()=>{
bodyInstance.onSQLClick();
setTimeout(()=>{
let sql = '-- This script was generated by a beta version of the ERD tool in pgAdmin 4.\n'
let sql = '-- This script was generated by the ERD tool in pgAdmin 4.\n'
+ '-- Please log an issue at https://redmine.postgresql.org/projects/pgadmin4/issues/new if you find any bugs, including reproduction steps.\n'
+ 'BEGIN;\nSELECT 1;\nEND;';