diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 79d334b8..f40c16b8 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -1,17 +1,22 @@ const _ = require('lodash'); +const fp = require('lodash/fp'); const uuidv1 = require('uuid/v1'); const connections = require('./connections'); const socket = require('../utility/socket'); const { fork } = require('child_process'); const DatabaseAnalyser = require('@dbgate/engines/default/DatabaseAnalyser'); +function pickObjectNames(array) { + return _.sortBy(array, (x) => `${x.schemaName}.${x.pureName}`).map(fp.pick(['pureName', 'schemaName'])); +} + module.exports = { /** @type {import('@dbgate/types').OpenedDatabaseConnection[]} */ opened: [], requests: {}, handle_structure(conid, database, { structure }) { - const existing = this.opened.find(x => x.conid == conid && x.database == database); + const existing = this.opened.find((x) => x.conid == conid && x.database == database); if (!existing) return; existing.structure = structure; socket.emit(`database-structure-changed-${conid}-${database}`); @@ -27,7 +32,7 @@ module.exports = { }, async ensureOpened(conid, database) { - const existing = this.opened.find(x => x.conid == conid && x.database == database); + const existing = this.opened.find((x) => x.conid == conid && x.database == database); if (existing) return existing; const connection = await connections.get({ conid }); const subprocess = fork(process.argv[1], ['databaseConnectionProcess']); @@ -60,10 +65,14 @@ module.exports = { listObjects_meta: 'get', async listObjects({ conid, database }) { const opened = await this.ensureOpened(conid, database); - const { tables } = opened.structure; - return { - tables: _.sortBy(tables, x => `${x.schemaName}.${x.pureName}`), - }; // .map(fp.pick(['tableName', 'schemaName'])); + const types = ['tables', 'views', 'procedures', 'functions', 'triggers']; + return types.reduce( + (res, type) => ({ + ...res, + [type]: pickObjectNames(opened.structure[type]), + }), + {} + ); }, queryData_meta: 'post', diff --git a/packages/engines/mssql/MsSqlAnalyser.js b/packages/engines/mssql/MsSqlAnalyser.js index 2827ed0f..8bed1262 100644 --- a/packages/engines/mssql/MsSqlAnalyser.js +++ b/packages/engines/mssql/MsSqlAnalyser.js @@ -208,25 +208,64 @@ class MsSqlAnalyser extends DatabaseAnalayser { return res; } async _runAnalysis() { - const tables = await this.driver.query(this.pool, this.createQuery('tables', ['tables'])); - const columns = await this.driver.query(this.pool, this.createQuery('columns', ['tables'])); - const pkColumns = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables'])); - const fkColumns = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables'])); + const tablesRows = await this.driver.query(this.pool, this.createQuery('tables', ['tables'])); + const columnsRows = await this.driver.query(this.pool, this.createQuery('columns', ['tables'])); + const pkColumnsRows = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables'])); + const fkColumnsRows = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables'])); + + const sqlCodeRows = await this.driver.query( + this.pool, + this.createQuery('loadSqlCode', ['views', 'procedures', 'functions', 'triggers']) + ); + const getCreateSql = (row) => + sqlCodeRows.rows + .filter((x) => x.pureName == row.pureName && x.schemaName == row.schemaName) + .map((x) => x.codeText) + .join(''); + const viewsRows = await this.driver.query(this.pool, this.createQuery('views', ['views'])); + const programmableRows = await this.driver.query( + this.pool, + this.createQuery('programmables', ['procedures', 'functions']) + ); + + const tables = tablesRows.rows.map((row) => ({ + ...row, + columns: columnsRows.rows + .filter((col) => col.objectId == row.objectId) + .map(({ isNullable, isIdentity, ...col }) => ({ + ...col, + notNull: !isNullable, + autoIncrement: !!isIdentity, + commonType: detectType(col), + })), + primaryKey: extractPrimaryKeys(row, pkColumnsRows.rows), + foreignKeys: extractForeignKeys(row, fkColumnsRows.rows), + })); + + const views = viewsRows.rows.map((row) => ({ + ...row, + createSql: getCreateSql(row), + })); + + const procedures = programmableRows.rows + .filter((x) => x.sqlObjectType.trim() == 'P') + .map((row) => ({ + ...row, + createSql: getCreateSql(row), + })); + + const functions = programmableRows.rows + .filter((x) => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim())) + .map((row) => ({ + ...row, + createSql: getCreateSql(row), + })); return this.mergeAnalyseResult({ - tables: tables.rows.map((table) => ({ - ...table, - columns: columns.rows - .filter((col) => col.objectId == table.objectId) - .map(({ isNullable, isIdentity, ...col }) => ({ - ...col, - notNull: !isNullable, - autoIncrement: !!isIdentity, - commonType: detectType(col), - })), - primaryKey: extractPrimaryKeys(table, pkColumns.rows), - foreignKeys: extractForeignKeys(table, fkColumns.rows), - })), + tables, + views, + procedures, + functions, }); } diff --git a/packages/engines/mssql/sql/index.js b/packages/engines/mssql/sql/index.js index 3213ee5a..cd99b326 100644 --- a/packages/engines/mssql/sql/index.js +++ b/packages/engines/mssql/sql/index.js @@ -3,6 +3,9 @@ const foreignKeys = require('./foreignKeys'); const primaryKeys = require('./primaryKeys'); const tables = require('./tables'); const modifications = require('./modifications'); +const loadSqlCode = require('./loadSqlCode'); +const views = require('./views'); +const programmables = require('./programmables'); module.exports = { columns, @@ -10,4 +13,7 @@ module.exports = { foreignKeys, primaryKeys, modifications, + loadSqlCode, + views, + programmables, }; diff --git a/packages/engines/mssql/sql/loadSqlCode.js b/packages/engines/mssql/sql/loadSqlCode.js new file mode 100644 index 00000000..c5f8ee87 --- /dev/null +++ b/packages/engines/mssql/sql/loadSqlCode.js @@ -0,0 +1,8 @@ +module.exports = ` +select s.name as pureName, u.name as schemaName, c.text AS codeText + from sys.objects s + inner join sys.syscomments c on s.object_id = c.id + inner join sys.schemas u on u.schema_id = s.schema_id +where (s.object_id =[OBJECT_ID_CONDITION]) +order by u.name, s.name, c.colid +`; diff --git a/packages/engines/mssql/sql/programmables.js b/packages/engines/mssql/sql/programmables.js new file mode 100644 index 00000000..8e651d3e --- /dev/null +++ b/packages/engines/mssql/sql/programmables.js @@ -0,0 +1,6 @@ +module.exports = ` +select o.name as pureName, s.name as schemaName, o.object_id as objectId, o.create_date as createDate, o.modify_date as modifyDate, o.type as sqlObjectType +from sys.objects o +inner join sys.schemas s on o.schema_id = s.schema_id +where o.type in ('P', 'IF', 'FN', 'TF') and o.object_id =[OBJECT_ID_CONDITION] +`; diff --git a/packages/engines/mssql/sql/views.js b/packages/engines/mssql/sql/views.js new file mode 100644 index 00000000..f0edb498 --- /dev/null +++ b/packages/engines/mssql/sql/views.js @@ -0,0 +1,10 @@ +module.exports = ` +SELECT + o.name as pureName, + u.name as schemaName, + o.object_id as objectId, + o.create_date as createDate, + o.modify_date as modifyDate +FROM sys.objects o INNER JOIN sys.schemas u ON u.schema_id=o.schema_id +WHERE type in ('V') and o.object_id =[OBJECT_ID_CONDITION] +`; diff --git a/packages/web/src/appobj/databaseObjectAppObject.js b/packages/web/src/appobj/databaseObjectAppObject.js new file mode 100644 index 00000000..69087f7d --- /dev/null +++ b/packages/web/src/appobj/databaseObjectAppObject.js @@ -0,0 +1,16 @@ +import tableAppObject from './tableAppObject'; +import viewAppObject from './viewAppObject'; + +const databaseObjectAppObject = () => ({ objectTypeField, ...other }, props) => { + switch (objectTypeField) { + case 'tables': + // @ts-ignore + return tableAppObject()(other, props); + case 'views': + // @ts-ignore + return viewAppObject()(other, props); + } + return null; +}; + +export default databaseObjectAppObject; diff --git a/packages/web/src/appobj/tableAppObject.js b/packages/web/src/appobj/tableAppObject.js index af159191..a1e69edd 100644 --- a/packages/web/src/appobj/tableAppObject.js +++ b/packages/web/src/appobj/tableAppObject.js @@ -58,9 +58,10 @@ const tableAppObject = () => ({ conid, database, pureName, schemaName }, { setOp database, }); }; - const matcher = filter => filterName(filter, pureName); + const matcher = (filter) => filterName(filter, pureName); + const groupTitle = 'Tables'; - return { title, key, Icon, Menu, onClick, matcher }; + return { title, key, Icon, Menu, onClick, matcher, groupTitle }; }; export default tableAppObject; diff --git a/packages/web/src/appobj/viewAppObject.js b/packages/web/src/appobj/viewAppObject.js new file mode 100644 index 00000000..70604317 --- /dev/null +++ b/packages/web/src/appobj/viewAppObject.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { ViewIcon } from '../icons'; +import { DropDownMenuItem } from '../modals/DropDownMenu'; +import { openNewTab } from '../utility/common'; +import getConnectionInfo from '../utility/getConnectionInfo'; +import fullDisplayName from '../utility/fullDisplayName'; +import { filterName } from '@dbgate/datalib'; + +// async function openTableDetail(setOpenedTabs, tabComponent, { schemaName, pureName, conid, database }) { +// const connection = await getConnectionInfo(conid); +// const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({ +// schemaName, +// pureName, +// })}`; + +// openNewTab(setOpenedTabs, { +// title: pureName, +// tooltip, +// icon: 'table2.svg', +// tabComponent, +// props: { +// schemaName, +// pureName, +// conid, +// database, +// }, +// }); +// } + +// function Menu({ data, makeAppObj, setOpenedTabs }) { +// const handleOpenData = () => { +// openTableDetail(setOpenedTabs, 'TableDataTab', data); +// }; +// const handleOpenStructure = () => { +// openTableDetail(setOpenedTabs, 'TableStructureTab', data); +// }; +// const handleOpenCreateScript = () => { +// openTableDetail(setOpenedTabs, 'TableCreateScriptTab', data); +// }; +// return ( +// <> +// Open data +// Open structure +// Create SQL +// +// ); +// } + +const viewAppObject = () => ({ conid, database, pureName, schemaName }, { setOpenedTabs }) => { + const title = schemaName ? `${schemaName}.${pureName}` : pureName; + const key = title; + const Icon = ViewIcon; + // const onClick = ({ schemaName, pureName }) => { + // openTableDetail(setOpenedTabs, 'TableDataTab', { + // schemaName, + // pureName, + // conid, + // database, + // }); + // }; + const matcher = (filter) => filterName(filter, pureName); + const groupTitle = 'Views'; + + return { + title, + key, + Icon, + // Menu, onClick, + matcher, + groupTitle, + }; +}; + +export default viewAppObject; diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index c4c4fb7c..6ccdf813 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -57,6 +57,8 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo const handleExecute = async () => { setExecuteNumber((num) => num + 1); + const selectedText = editorRef.current.editor.getSelectedText(); + let sesid = sessionId; if (!sesid) { const resp = await axios.post('sessions/create', { @@ -68,7 +70,7 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo } await axios.post('sessions/execute-query', { sesid, - sql: queryText, + sql: selectedText || queryText, }); }; @@ -99,7 +101,11 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo /> - + diff --git a/packages/web/src/widgets/DatabaseWidget.js b/packages/web/src/widgets/DatabaseWidget.js index 63651a16..3dd02f2f 100644 --- a/packages/web/src/widgets/DatabaseWidget.js +++ b/packages/web/src/widgets/DatabaseWidget.js @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components'; +import _ from 'lodash'; import useFetch from '../utility/useFetch'; import { AppObjectList } from '../appobj/AppObjectList'; @@ -9,6 +10,7 @@ import { useSetCurrentDatabase, useCurrentDatabase } from '../utility/globalStat import tableAppObject from '../appobj/tableAppObject'; import theme from '../theme'; import InlineButton from './InlineButton'; +import databaseObjectAppObject from '../appobj/databaseObjectAppObject'; const SearchBoxWrapper = styled.div` display: flex; @@ -46,7 +48,7 @@ const Input = styled.input` function SubDatabaseList({ data }) { const setDb = useSetCurrentDatabase(); - const handleDatabaseClick = database => { + const handleDatabaseClick = (database) => { setDb({ ...database, connection: data, @@ -59,7 +61,7 @@ function SubDatabaseList({ data }) { }); return ( ({ ...db, connection: data }))} + list={(databases || []).map((db) => ({ ...db, connection: data }))} makeAppObj={databaseAppObject({ boldCurrentDatabase: true })} onObjectClick={handleDatabaseClick} /> @@ -75,7 +77,7 @@ function ConnectionList() { return ( <> - setFilter(e.target.value)} /> + setFilter(e.target.value)} /> Refresh @@ -96,8 +98,13 @@ function SqlObjectList({ conid, database }) { url: `database-connections/list-objects?conid=${conid}&database=${database}`, reloadTrigger: `database-structure-changed-${conid}-${database}`, }); - const { tables } = objects || {}; + const [filter, setFilter] = React.useState(''); + const objectList = _.flatten( + ['tables', 'views'].map((objectTypeField) => + ((objects || {})[objectTypeField] || []).map((obj) => ({ ...obj, objectTypeField })) + ) + ); return ( <> @@ -105,15 +112,15 @@ function SqlObjectList({ conid, database }) { type="text" placeholder="Search tables or objects" value={filter} - onChange={e => setFilter(e.target.value)} + onChange={(e) => setFilter(e.target.value)} /> Refresh ({ ...x, conid, database }))} - makeAppObj={tableAppObject()} - groupFunc={appobj => 'Tables'} + list={objectList.map((x) => ({ ...x, conid, database }))} + makeAppObj={databaseObjectAppObject()} + groupFunc={(appobj) => appobj.groupTitle} filter={filter} />