From b76c12c7d7f1d7df57ec8639a6c6c98cd484ff3a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 2 Feb 2020 16:43:41 +0100 Subject: [PATCH] pk, fk analyse, show in structure tab --- api/src/engines/mssql/MsSqlAnalyser.js | 27 ++++++++++++ api/src/engines/mssql/foreign_keys.sql | 38 +++++++++++++++++ api/src/engines/mssql/primary_keys.sql | 12 ++++++ api/tsconfig.json | 4 -- types/dbinfo.d.ts | 58 +++++++++++++++++++------- types/engines.d.ts | 35 ++++++++++------ types/index.d.ts | 9 ++-- web/src/appobj/constraintAppObject.js | 24 +++++++++++ web/src/tabs/TableStructureTab.js | 33 ++++++++++++++- web/src/utility/ObjectListControl.js | 4 +- 10 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 api/src/engines/mssql/foreign_keys.sql create mode 100644 api/src/engines/mssql/primary_keys.sql create mode 100644 web/src/appobj/constraintAppObject.js diff --git a/api/src/engines/mssql/MsSqlAnalyser.js b/api/src/engines/mssql/MsSqlAnalyser.js index e1fc7e39..f1453a0d 100644 --- a/api/src/engines/mssql/MsSqlAnalyser.js +++ b/api/src/engines/mssql/MsSqlAnalyser.js @@ -1,4 +1,5 @@ const fs = require('fs-extra'); +const fp = require('lodash/fp'); const path = require('path'); const _ = require('lodash'); @@ -9,6 +10,28 @@ async function loadQuery(name) { return await fs.readFile(path.join(__dirname, name), 'utf-8'); } +const byTableFilter = table => x => x.pureName == table.pureName && x.schemaName == x.schemaName; + +function extractPrimaryKeys(table, pkColumns) { + const filtered = pkColumns.filter(byTableFilter(table)); + if (filtered.length == 0) return undefined; + return { + constraintName: filtered[0].constraintName, + constraintType: 'primaryKey', + columns: filtered.map(fp.pick('columnName')), + }; +} + +function extractForeignKeys(table, fkColumns) { + const grouped = _.groupBy(fkColumns.filter(byTableFilter(table)), 'constraintName'); + return _.keys(grouped).map(constraintName => ({ + constraintName, + constraintType: 'foreignKey', + ..._.pick(fkColumns[0], ['refSchemaName', 'refTableName', 'updateAction', 'deleteAction']), + columns: grouped[constraintName].map(fp.pick(['columnName', 'refColumnName'])), + })); +} + class MsSqlAnalyser extends DatabaseAnalayser { constructor(pool, driver) { super(pool, driver); @@ -29,6 +52,8 @@ class MsSqlAnalyser extends DatabaseAnalayser { async runAnalysis() { const tables = await this.driver.query(this.pool, await this.createQuery('tables.sql')); const columns = await this.driver.query(this.pool, await this.createQuery('columns.sql')); + const pkColumns = await this.driver.query(this.pool, await this.createQuery('primary_keys.sql')); + const fkColumns = await this.driver.query(this.pool, await this.createQuery('foreign_keys.sql')); this.result.tables = tables.rows.map(table => ({ ...table, @@ -39,6 +64,8 @@ class MsSqlAnalyser extends DatabaseAnalayser { notNull: !isNullable, autoIncrement: !!isIdentity, })), + primaryKey: extractPrimaryKeys(table, pkColumns.rows), + foreignKeys: extractForeignKeys(table, fkColumns.rows), })); } } diff --git a/api/src/engines/mssql/foreign_keys.sql b/api/src/engines/mssql/foreign_keys.sql new file mode 100644 index 00000000..8df9c5f1 --- /dev/null +++ b/api/src/engines/mssql/foreign_keys.sql @@ -0,0 +1,38 @@ +SELECT + schemaName = FK.TABLE_SCHEMA, + pureName = FK.TABLE_NAME, + columnName = CU.COLUMN_NAME, + + refSchemaName = ISNULL(IXS.name, PK.TABLE_SCHEMA), + refTableName = ISNULL(IXT.name, PK.TABLE_NAME), + refColumnName = IXCC.name, + + constraintName = C.CONSTRAINT_NAME, + updateAction = rc.UPDATE_RULE, + deleteAction = rc.DELETE_RULE, + + objectId = o.object_id + +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C +INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME + +LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME +LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME +--LEFT JOIN ( +--SELECT i1.TABLE_NAME, i2.COLUMN_NAME +--FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1 +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2 ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME +--WHERE i1.CONSTRAINT_TYPE = 'PRIMARY KEY' +--) PT ON PT.TABLE_NAME = PK.TABLE_NAME +INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON FK.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + +LEFT JOIN sys.indexes IX ON IX.name = C.UNIQUE_CONSTRAINT_NAME +LEFT JOIN sys.objects IXT ON IXT.object_id = IX.object_id +LEFT JOIN sys.index_columns IXC ON IX.index_id = IXC.index_id and IX.object_id = IXC.object_id +LEFT JOIN sys.columns IXCC ON IXCC.column_id = IXC.column_id AND IXCC.object_id = IXC.object_id +LEFT JOIN sys.schemas IXS ON IXT.schema_id = IXS.schema_id + +inner join sys.objects o on FK.TABLE_NAME = o.name +inner join sys.schemas s on o.schema_id = s.schema_id and FK.TABLE_SCHEMA = s.name + +where o.object_id =[OBJECT_ID_CONDITION] diff --git a/api/src/engines/mssql/primary_keys.sql b/api/src/engines/mssql/primary_keys.sql new file mode 100644 index 00000000..c7a21eaf --- /dev/null +++ b/api/src/engines/mssql/primary_keys.sql @@ -0,0 +1,12 @@ +select o.object_id, pureName = t.Table_Name, schemaName = t.Table_Schema, columnName = c.Column_Name, constraintName=t.constraint_name from + INFORMATION_SCHEMA.TABLE_CONSTRAINTS t, + sys.objects o, + sys.schemas s, + INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE c +where + c.Constraint_Name = t.Constraint_Name + and t.table_name = o.name + and o.schema_id = s.schema_id and t.Table_Schema = s.name + and c.Table_Name = t.Table_Name + and Constraint_Type = 'PRIMARY KEY' + and o.object_id =[OBJECT_ID_CONDITION] diff --git a/api/tsconfig.json b/api/tsconfig.json index 12f5cf76..e5459c4c 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -5,10 +5,6 @@ "checkJs": true, "noEmit": true, "moduleResolution": "node", - // doesn't work - "types": [ - "dbgate" - ] }, "include": [ "src" diff --git a/types/dbinfo.d.ts b/types/dbinfo.d.ts index b751051b..f147fc7a 100644 --- a/types/dbinfo.d.ts +++ b/types/dbinfo.d.ts @@ -1,24 +1,50 @@ export interface NamedObjectInfo { - pureName: string; - schemaName: string; + pureName: string; + schemaName: string; } + +export interface ColumnReference { + columnName: string; + refColumnName?: string; +} + +export interface ConstraintInfo { + constraintName: string; + constraintType: string; +} + +export interface ColumnsConstraintInfo extends ConstraintInfo { + columns: ConstraintInfo[]; +} + +export interface PrimaryKeyInfo extends ColumnsConstraintInfo {} + +export interface ForeignKeyInfo extends ColumnsConstraintInfo { + refSchemaName: string; + refTableName: string; + updateAction: string; + deleteAction: string; +} + export interface ColumnInfo { - columnName: string; - notNull: boolean; - autoIncrement: boolean; - dataType: string; - precision: number; - scale: number; - length: number; - computedExpression: string; - isPersisted: boolean; - isSparse: boolean; - defaultValue: string; - defaultConstraint: string; + columnName: string; + notNull: boolean; + autoIncrement: boolean; + dataType: string; + precision: number; + scale: number; + length: number; + computedExpression: string; + isPersisted: boolean; + isSparse: boolean; + defaultValue: string; + defaultConstraint: string; } export interface TableInfo extends NamedObjectInfo { - columns: ColumnInfo[]; + columns: ColumnInfo[]; + primaryKey?: PrimaryKeyInfo; + foreignKeys: ForeignKeyInfo[]; } export interface DatabaseInfo { - tables: TableInfo[]; + tables: TableInfo[]; } diff --git a/types/engines.d.ts b/types/engines.d.ts index cbe0161b..a05365a3 100644 --- a/types/engines.d.ts +++ b/types/engines.d.ts @@ -1,16 +1,25 @@ import { QueryResult } from "./query"; export interface EngineDriver { - connect({ server, port, user, password }: { - server: any; - port: any; - user: any; - password: any; - }): any; - query(pool: any, sql: string): Promise; - getVersion(pool: any): Promise; - listDatabases(pool: any): Promise<{ - name: string; - }[]>; - analyseFull(pool: any): Promise; - analyseIncremental(pool: any): Promise; + connect({ + server, + port, + user, + password + }: { + server: any; + port: any; + user: any; + password: any; + }): any; + query(pool: any, sql: string): Promise; + getVersion(pool: any): Promise; + listDatabases( + pool: any + ): Promise< + { + name: string; + }[] + >; + analyseFull(pool: any): Promise; + analyseIncremental(pool: any): Promise; } diff --git a/types/index.d.ts b/types/index.d.ts index d6d61ab9..17d18d0b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,11 +1,10 @@ -/// import { ChildProcess } from "child_process"; import { DatabaseInfo } from "./dbinfo"; export interface OpenedDatabaseConnection { - conid: string; - database: string; - structure: DatabaseInfo; - subprocess: ChildProcess; + conid: string; + database: string; + structure: DatabaseInfo; + subprocess: ChildProcess; } export * from "./engines"; export * from "./dbinfo"; diff --git a/web/src/appobj/constraintAppObject.js b/web/src/appobj/constraintAppObject.js new file mode 100644 index 00000000..abb6d9a7 --- /dev/null +++ b/web/src/appobj/constraintAppObject.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { PrimaryKeyIcon, ForeignKeyIcon } from '../icons'; +import { DropDownMenuItem } from '../modals/DropDownMenu'; +import showModal from '../modals/showModal'; +import ConnectionModal from '../modals/ConnectionModal'; +import axios from '../utility/axios'; +import { openNewTab } from '../utility/common'; +import { useSetOpenedTabs } from '../utility/globalState'; + +/** @param props {import('dbgate').ConstraintInfo} */ +function getConstraintIcon(props) { + if (props.constraintType == 'primaryKey') return PrimaryKeyIcon; + if (props.constraintType == 'foreignKey') return ForeignKeyIcon; + return null; +} + +/** @param props {import('dbgate').ConstraintInfo} */ +export default function constraintAppObject(props, { setOpenedTabs }) { + const title = props.constraintName; + const key = title; + const Icon = getConstraintIcon(props); + + return { title, key, Icon }; +} diff --git a/web/src/tabs/TableStructureTab.js b/web/src/tabs/TableStructureTab.js index f8840029..5d9a6adb 100644 --- a/web/src/tabs/TableStructureTab.js +++ b/web/src/tabs/TableStructureTab.js @@ -1,9 +1,12 @@ import React from 'react'; -import useFetch from '../utility/useFetch'; import styled from 'styled-components'; +import _ from 'lodash'; import theme from '../theme'; +import useFetch from '../utility/useFetch'; import ObjectListControl from '../utility/ObjectListControl'; import { TableColumn } from '../utility/TableControl'; +import columnAppObject from '../appobj/columnAppObject'; +import constraintAppObject from '../appobj/constraintAppObject'; const WhitePage = styled.div` position: absolute; @@ -21,10 +24,12 @@ export default function TableStructureTab({ conid, database, schemaName, pureNam params: { conid, database, schemaName, pureName }, }); if (!tableInfo) return null; + const { columns, primaryKey, foreignKeys } = tableInfo; return ( ({ ...x, ordinal: index + 1 }))} + collection={columns.map((x, index) => ({ ...x, ordinal: index + 1 }))} + makeAppObj={columnAppObject} title="Columns" > */} + + + row.columns.map(x => x.columnName).join(', ')} + /> + + + + row.columns.map(x => x.columnName).join(', ')} + /> + row.refTableName} /> + row.columns.map(x => x.refColumnName).join(', ')} + /> + + + ); } diff --git a/web/src/utility/ObjectListControl.js b/web/src/utility/ObjectListControl.js index d4a486b7..a4180b43 100644 --- a/web/src/utility/ObjectListControl.js +++ b/web/src/utility/ObjectListControl.js @@ -27,7 +27,7 @@ const ObjectListBody = styled.div` // margin-top: 3px; `; -export default function ObjectListControl({ collection = [], title, showIfEmpty = false, children }) { +export default function ObjectListControl({ collection = [], title, showIfEmpty = false, makeAppObj, children }) { if (collection.length == 0 && !showIfEmpty) return null; return ( @@ -40,7 +40,7 @@ export default function ObjectListControl({ collection = [], title, showIfEmpty } + formatter={col => } /> {children}