diff --git a/integration-tests/__tests__/object-analyse.spec.js b/integration-tests/__tests__/object-analyse.spec.js index e49e098df..f94223b69 100644 --- a/integration-tests/__tests__/object-analyse.spec.js +++ b/integration-tests/__tests__/object-analyse.spec.js @@ -10,6 +10,14 @@ function flatSource() { ); } +function flatSourceParameters() { + return _.flatten( + engines.map(engine => + (engine.parameters || []).map(parameter => [engine.label, parameter.testName, parameter, engine]) + ) + ); +} + const obj1Match = expect.objectContaining({ pureName: 'obj1', }); @@ -78,7 +86,7 @@ describe('Object analyse', () => { const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2[type].length).toEqual(0); - await driver.query(conn, structure1[type][0].createSql, { discardResult: true }); + await driver.script(conn, structure1[type][0].createSql); const structure3 = await driver.analyseIncremental(conn, structure2); @@ -86,4 +94,45 @@ describe('Object analyse', () => { expect(structure3[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match); }) ); + + test.each(flatSourceParameters())( + 'Test parameters simple analyse - %s - %s', + testWrapper(async (conn, driver, testName, parameter, engine) => { + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); + for (const sql of engine.parametersOtherSql) await driver.query(conn, sql, { discardResult: true }); + + await driver.query(conn, parameter.create, { discardResult: true }); + const structure = await driver.analyseFull(conn); + + const parameters = structure[parameter.objectTypeField].find(x => x.pureName == 'obj1').parameters; + + expect(parameters.length).toEqual(parameter.list.length); + for (let i = 0; i < parameters.length; i += 1) { + expect(parameters[i]).toEqual(expect.objectContaining(parameter.list[i])); + } + }) + ); + + test.each(flatSourceParameters())( + 'Test parameters create SQL - %s - %s', + testWrapper(async (conn, driver, testName, parameter, engine) => { + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); + for (const sql of engine.parametersOtherSql) await driver.query(conn, sql, { discardResult: true }); + + await driver.query(conn, parameter.create, { discardResult: true }); + const structure1 = await driver.analyseFull(conn); + await driver.query(conn, parameter.drop, { discardResult: true }); + + const obj = structure1[parameter.objectTypeField].find(x => x.pureName == 'obj1'); + await driver.script(conn, obj.createSql); + + const structure2 = await driver.analyseFull(conn); + const parameters = structure2[parameter.objectTypeField].find(x => x.pureName == 'obj1').parameters; + + expect(parameters.length).toEqual(parameter.list.length); + for (let i = 0; i < parameters.length; i += 1) { + expect(parameters[i]).toEqual(expect.objectContaining(parameter.list[i])); + } + }) + ); }); diff --git a/integration-tests/engines.js b/integration-tests/engines.js index 1bd0ab110..6ea74f7ab 100644 --- a/integration-tests/engines.js +++ b/integration-tests/engines.js @@ -28,7 +28,16 @@ const engines = [ port: 15001, }, // skipOnCI: true, - objects: [views], + objects: [ + views, + { + type: 'procedures', + create1: 'CREATE PROCEDURE obj1() BEGIN SELECT * FROM t1; END', + create2: 'CREATE PROCEDURE obj2() BEGIN SELECT * FROM t2; END', + drop1: 'DROP PROCEDURE obj1', + drop2: 'DROP PROCEDURE obj2', + }, + ], dbSnapshotBySeconds: true, dumpFile: 'data/chinook-mysql.sql', dumpChecks: [ @@ -37,6 +46,68 @@ const engines = [ res: '25', }, ], + parametersOtherSql: ['CREATE PROCEDURE obj2(a int, b int) BEGIN SELECT * FROM t1; END'], + parameters: [ + { + testName: 'simple', + create: 'CREATE PROCEDURE obj1(a int) BEGIN SELECT * FROM t1; END', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'int', + }, + ], + }, + { + testName: 'paramTypes', + create: 'CREATE PROCEDURE obj1(a int, b varchar(50), c numeric(10,2)) BEGIN SELECT * FROM t1; END', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'int', + }, + { + parameterName: 'b', + parameterMode: 'IN', + dataType: 'varchar(50)', + }, + { + parameterName: 'c', + parameterMode: 'IN', + dataType: 'decimal(10,2)', + }, + ], + }, + { + testName: 'paramModes', + create: 'CREATE PROCEDURE obj1(IN a int, OUT b int, INOUT c int) BEGIN SELECT * FROM t1; END', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'int', + }, + { + parameterName: 'b', + parameterMode: 'OUT', + dataType: 'int', + }, + { + parameterName: 'c', + parameterMode: 'INOUT', + dataType: 'int', + }, + ], + }, + ], }, { label: 'MariaDB', @@ -105,6 +176,94 @@ const engines = [ res: '25', }, ], + + parametersOtherSql: ['CREATE PROCEDURE obj2(a integer, b integer) LANGUAGE SQL AS $$ select * from t1 $$'], + parameters: [ + { + testName: 'simple', + create: 'CREATE PROCEDURE obj1(a integer) LANGUAGE SQL AS $$ select * from t1 $$', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'integer', + }, + ], + }, + { + testName: 'dataTypes', + create: + 'CREATE PROCEDURE obj1(a integer, b varchar(20), c numeric(18,2)) LANGUAGE SQL AS $$ select * from t1 $$', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'integer', + }, + { + parameterName: 'b', + parameterMode: 'IN', + dataType: 'varchar', + }, + { + parameterName: 'c', + parameterMode: 'IN', + dataType: 'numeric', + }, + ], + }, + { + testName: 'paramModes', + create: 'CREATE PROCEDURE obj1(IN a integer, INOUT b integer) LANGUAGE SQL AS $$ select * from t1 $$', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: 'a', + parameterMode: 'IN', + dataType: 'integer', + }, + { + parameterName: 'b', + parameterMode: 'INOUT', + dataType: 'integer', + }, + ], + }, + { + testName: 'paramModesFunction', + objectTypeField: 'functions', + create: ` +create or replace function obj1( + out min_len int, + out max_len int) +language plpgsql +as $$ +begin + select min(id), + max(id) + into min_len, max_len + from t1; +end;$$`, + drop: 'DROP FUNCTION obj1', + list: [ + { + parameterName: 'min_len', + parameterMode: 'OUT', + dataType: 'integer', + }, + { + parameterName: 'max_len', + parameterMode: 'OUT', + dataType: 'integer', + }, + ], + }, + ], }, { label: 'SQL Server', @@ -129,6 +288,63 @@ const engines = [ drop2: 'DROP PROCEDURE obj2', }, ], + parametersOtherSql: ['CREATE PROCEDURE obj2 (@p1 int, @p2 int) AS SELECT id from t1'], + parameters: [ + { + testName: 'simple', + create: 'CREATE PROCEDURE obj1 (@param1 int) AS SELECT id from t1', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: '@param1', + parameterMode: 'IN', + dataType: 'int', + }, + ], + }, + { + testName: 'dataTypes', + create: 'CREATE PROCEDURE obj1 (@p1 bit, @p2 nvarchar(20), @p3 decimal(18,2), @p4 float) AS SELECT id from t1', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: '@p1', + parameterMode: 'IN', + dataType: 'bit', + }, + { + parameterName: '@p2', + parameterMode: 'IN', + dataType: 'nvarchar(20)', + }, + { + parameterName: '@p3', + parameterMode: 'IN', + dataType: 'decimal(18,2)', + }, + { + parameterName: '@p4', + parameterMode: 'IN', + dataType: 'float', + }, + ], + }, + { + testName: 'outputParam', + create: 'CREATE PROCEDURE obj1 (@p1 int OUTPUT) AS SELECT id from t1', + drop: 'DROP PROCEDURE obj1', + objectTypeField: 'procedures', + list: [ + { + parameterName: '@p1', + parameterMode: 'OUT', + dataType: 'int', + }, + ], + }, + ], supportSchemas: true, supportRenameSqlObject: true, defaultSchemaName: 'dbo', @@ -188,10 +404,10 @@ const engines = [ const filterLocal = [ // filter local testing - '-MySQL', + 'MySQL', '-MariaDB', '-PostgreSQL', - 'SQL Server', + '-SQL Server', '-SQLite', '-CockroachDB', '-ClickHouse', diff --git a/packages/types/dbinfo.d.ts b/packages/types/dbinfo.d.ts index b5b59d1ec..24d379c61 100644 --- a/packages/types/dbinfo.d.ts +++ b/packages/types/dbinfo.d.ts @@ -35,7 +35,7 @@ export interface IndexInfo extends ColumnsConstraintInfo { isUnique: boolean; // indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext'; indexType?: string; - // condition for filtered index (SQL Server) + // condition for filtered index (SQL Server) filterDefinition?: string; } @@ -118,9 +118,21 @@ export interface ViewInfo extends SqlObjectInfo { columns: ColumnInfo[]; } -export interface ProcedureInfo extends SqlObjectInfo {} +export type ParameterMode = 'IN' | 'OUT' | 'INOUT' | 'RETURN'; + +export interface ParameterInfo { + schemaName: string; + parameterName?: string; + pureName: string; + dataType: string; + parameterMode?: ParameterMode; +} +export interface ProcedureInfo extends SqlObjectInfo { + parameters?: ParameterInfo[]; +} export interface FunctionInfo extends SqlObjectInfo { + parameters?: ParameterInfo[]; // returnDataType?: string; } diff --git a/packages/web/src/appobj/AppObjectListItem.svelte b/packages/web/src/appobj/AppObjectListItem.svelte index 2b61b232b..395127e63 100644 --- a/packages/web/src/appobj/AppObjectListItem.svelte +++ b/packages/web/src/appobj/AppObjectListItem.svelte @@ -62,7 +62,7 @@ {#if (isExpanded || isExpandedBySearch) && subItemsComponent}
+ export const extractKey = ({ columnName }) => columnName; + + + + + diff --git a/packages/web/src/appobj/SubProcedureParamList.svelte b/packages/web/src/appobj/SubProcedureParamList.svelte new file mode 100644 index 000000000..852775e26 --- /dev/null +++ b/packages/web/src/appobj/SubProcedureParamList.svelte @@ -0,0 +1,14 @@ + + + ({ + ...data, + ...parameter, + }))} + module={parameterAppObject} +/> diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 90cdae967..1f8d6930f 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -63,6 +63,7 @@ 'icon open-in-new': 'mdi mdi-open-in-new', 'icon add-folder': 'mdi mdi-folder-plus-outline', 'icon add-column': 'mdi mdi-table-column-plus-after', + 'icon parameter': 'mdi mdi-at', 'icon window-restore': 'mdi mdi-window-restore', 'icon window-maximize': 'mdi mdi-window-maximize', diff --git a/packages/web/src/widgets/ConnectionList.svelte b/packages/web/src/widgets/ConnectionList.svelte index 58b161487..d73169be8 100644 --- a/packages/web/src/widgets/ConnectionList.svelte +++ b/packages/web/src/widgets/ConnectionList.svelte @@ -274,7 +274,7 @@ SubDatabaseList} expandOnClick isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase} {filter} @@ -298,7 +298,7 @@ SubDatabaseList} expandOnClick isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase} {filter} diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 8f38a7efe..aef4486bc 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -52,6 +52,7 @@ import AppObjectListHandler from './AppObjectListHandler.svelte'; import { matchDatabaseObjectAppObject } from '../appobj/appObjectTools'; import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte'; + import SubProcedureParamList from '../appobj/SubProcedureParamList.svelte'; export let conid; export let database; @@ -238,9 +239,16 @@ .map(x => ({ ...x, conid, database }))} module={databaseObjectAppObject} groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)} - subItemsComponent={SubColumnParamList} + subItemsComponent={data => + data.objectTypeField == 'procedures' || data.objectTypeField == 'functions' + ? SubProcedureParamList + : SubColumnParamList} isExpandable={data => - data.objectTypeField == 'tables' || data.objectTypeField == 'views' || data.objectTypeField == 'matviews'} + data.objectTypeField == 'tables' || + data.objectTypeField == 'views' || + data.objectTypeField == 'matviews' || + ((data.objectTypeField == 'procedures' || data.objectTypeField == 'functions') && + !!data.parameters?.length)} expandIconFunc={chevronExpandIcon} {filter} passProps={{ diff --git a/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js b/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js index 1eac0ea32..79b040f76 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js +++ b/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js @@ -31,6 +31,18 @@ function simplifyComutedExpression(expr) { return expr; } +function getFullDataTypeName({ dataType, charMaxLength, numericScale, numericPrecision }) { + let fullDataType = dataType; + if (charMaxLength && isTypeString(dataType)) { + fullDataType = `${dataType}(${charMaxLength < 0 ? 'MAX' : charMaxLength})`; + } + if (numericPrecision && numericScale && isTypeNumeric(dataType)) { + fullDataType = `${dataType}(${numericPrecision},${numericScale})`; + } + + return fullDataType; +} + function getColumnInfo({ isNullable, isIdentity, @@ -43,13 +55,12 @@ function getColumnInfo({ defaultConstraint, computedExpression, }) { - let fullDataType = dataType; - if (charMaxLength && isTypeString(dataType)) { - fullDataType = `${dataType}(${charMaxLength < 0 ? 'MAX' : charMaxLength})`; - } - if (numericPrecision && numericScale && isTypeNumeric(dataType)) { - fullDataType = `${dataType}(${numericPrecision},${numericScale})`; - } + const fullDataType = getFullDataTypeName({ + dataType, + charMaxLength, + numericPrecision, + numericScale, + }); if (defaultValue) { defaultValue = defaultValue.trim(); @@ -116,7 +127,11 @@ class MsSqlAnalyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'Loading views' }); const viewsRows = await this.analyserQuery('views', ['views']); this.feedback({ analysingMessage: 'Loading procedures & functions' }); + const programmableRows = await this.analyserQuery('programmables', ['procedures', 'functions']); + const procedureParameterRows = await this.analyserQuery('proceduresParameters'); + const functionParameterRows = await this.analyserQuery('functionParameters'); + this.feedback({ analysingMessage: 'Loading view columns' }); const viewColumnRows = await this.analyserQuery('viewColumns', ['views']); @@ -157,20 +172,46 @@ class MsSqlAnalyser extends DatabaseAnalyser { columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo), })); + const procedureParameter = procedureParameterRows.rows.map(row => ({ + ...row, + dataType: getFullDataTypeName(row), + })); + + const prodceureToParameters = procedureParameter.reduce((acc, parameter) => { + if (!acc[parameter.parentObjectId]) acc[parameter.parentObjectId] = []; + acc[parameter.parentObjectId].push(parameter); + + return acc; + }, {}); + const procedures = programmableRows.rows .filter(x => x.sqlObjectType.trim() == 'P') .map(row => ({ ...row, contentHash: row.modifyDate && row.modifyDate.toISOString(), createSql: getCreateSql(row), + parameters: prodceureToParameters[row.objectId], })); + const functionParameters = functionParameterRows.rows.map(row => ({ + ...row, + dataType: getFullDataTypeName(row), + })); + + const functionToParameters = functionParameters.reduce((acc, parameter) => { + if (!acc[parameter.parentObjectId]) acc[parameter.parentObjectId] = []; + + acc[parameter.parentObjectId].push(parameter); + return acc; + }, {}); + const functions = programmableRows.rows .filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim())) .map(row => ({ ...row, contentHash: row.modifyDate && row.modifyDate.toISOString(), createSql: getCreateSql(row), + parameters: functionToParameters[row.objectId], })); this.feedback({ analysingMessage: null }); diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/functionParameters.js b/plugins/dbgate-plugin-mssql/src/backend/sql/functionParameters.js new file mode 100644 index 000000000..2a255d939 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/functionParameters.js @@ -0,0 +1,47 @@ +module.exports = ` +SELECT + o.object_id as parentObjectId, + p.object_id AS parameterObjectId, + o.name as pureName, + CASE + WHEN p.name IS NULL OR LTRIM(RTRIM(p.name)) = '' THEN + '@Output' + ELSE + p.name + END AS parameterName, + TYPE_NAME(p.user_type_id) AS dataType, + CASE + WHEN TYPE_NAME(p.user_type_id) = 'nvarchar' THEN p.max_length / 2 + ELSE p.max_length + END AS charMaxLength, + CASE + WHEN p.is_output = 1 THEN 'OUT' + ELSE 'IN' + END AS parameterMode, + CASE + WHEN TYPE_NAME(p.user_type_id) IN ('numeric', 'decimal') THEN p.precision + ELSE NULL + END AS numericPrecision, + CASE + WHEN TYPE_NAME(p.user_type_id) IN ('numeric', 'decimal') THEN p.scale + ELSE NULL + END AS numericScale, + CASE + WHEN p.is_output = 1 THEN 'OUT' + ELSE 'IN' + END AS parameterMode, + p.parameter_id AS parameterIndex, + s.name as schemaName +FROM + sys.objects o +JOIN + sys.parameters p ON o.object_id = p.object_id +INNER JOIN + sys.schemas s ON s.schema_id=o.schema_id +WHERE + o.type IN ('FN', 'IF', 'TF') + and o.object_id =OBJECT_ID_CONDITION and s.name =SCHEMA_NAME_CONDITION +ORDER BY + p.object_id, + p.parameter_id; +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js index 563b8d080..6352b6a9b 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js @@ -7,6 +7,8 @@ const modifications = require('./modifications'); const loadSqlCode = require('./loadSqlCode'); const views = require('./views'); const programmables = require('./programmables'); +const proceduresParameters = require('./proceduresParameters'); +const functionParameters = require('./functionParameters'); const viewColumns = require('./viewColumns'); const indexes = require('./indexes'); const indexcols = require('./indexcols'); @@ -20,6 +22,8 @@ module.exports = { loadSqlCode, views, programmables, + proceduresParameters, + functionParameters, viewColumns, indexes, indexcols, diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/proceduresParameters.js b/plugins/dbgate-plugin-mssql/src/backend/sql/proceduresParameters.js new file mode 100644 index 000000000..d02b5fcb1 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/proceduresParameters.js @@ -0,0 +1,38 @@ +module.exports = ` +SELECT + o.object_id as parentObjectId, + p.object_id as objectId, + o.name as pureName, + p.name AS parameterName, + TYPE_NAME(p.user_type_id) AS dataType, + CASE + WHEN TYPE_NAME(p.user_type_id) = 'nvarchar' THEN p.max_length / 2 + ELSE p.max_length + END AS charMaxLength, + CASE + WHEN p.is_output = 1 THEN 'OUT' + ELSE 'IN' + END AS parameterMode, + CASE + WHEN TYPE_NAME(p.user_type_id) IN ('numeric', 'decimal') THEN p.precision + ELSE NULL + END AS numericPrecision, + CASE + WHEN TYPE_NAME(p.user_type_id) IN ('numeric', 'decimal') THEN p.scale + ELSE NULL + END AS numericScale, + p.parameter_id AS parameterIndex, + s.name as schemaName +FROM + sys.objects o +JOIN + sys.parameters p ON o.object_id = p.object_id +INNER JOIN + sys.schemas s ON s.schema_id=o.schema_id +WHERE + o.type = 'P' + and o.object_id =OBJECT_ID_CONDITION and s.name =SCHEMA_NAME_CONDITION +ORDER BY + o.object_id, + p.parameter_id; +`; diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index ae68e0ce2..0e4565568 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -15,6 +15,11 @@ function quoteDefaultValue(value) { return value; } +function normalizeTypeName(typeName) { + if (/int\(\d+\)/.test(typeName)) return 'int'; + return typeName; +} + function getColumnInfo( { isNullable, @@ -60,6 +65,18 @@ function getColumnInfo( }; } +function getParametersSqlString(parameters = []) { + if (!parameters?.length) return ''; + + return parameters + .map(i => { + const mode = i.parameterMode ? `${i.parameterMode} ` : ''; + const dataType = i.dataType ? ` ${i.dataType.toUpperCase()}` : ''; + return mode + i.parameterName + dataType; + }) + .join(', '); +} + class Analyser extends DatabaseAnalyser { constructor(dbhan, driver, version) { super(dbhan, driver, version); @@ -114,6 +131,30 @@ class Analyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'Loading programmables' }); const programmables = await this.analyserQuery('programmables', ['procedures', 'functions']); + const parameters = await this.analyserQuery('parameters', ['procedures', 'functions']); + + const functionParameters = parameters.rows.filter(x => x.routineType == 'FUNCTION'); + const functionNameToParameters = functionParameters.reduce((acc, row) => { + if (!acc[`${row.schemaName}.${row.pureName}`]) acc[`${row.schemaName}.${row.pureName}`] = []; + + acc[`${row.schemaName}.${row.pureName}`].push({ + ...row, + dataType: normalizeTypeName(row.dataType), + }); + return acc; + }, {}); + + const procedureParameters = parameters.rows.filter(x => x.routineType == 'PROCEDURE'); + const procedureNameToParameters = procedureParameters.reduce((acc, row) => { + if (!acc[`${row.schemaName}.${row.pureName}`]) acc[`${row.schemaName}.${row.pureName}`] = []; + + acc[`${row.schemaName}.${row.pureName}`].push({ + ...row, + dataType: normalizeTypeName(row.dataType), + }); + return acc; + }, {}); + this.feedback({ analysingMessage: 'Loading view texts' }); const viewTexts = await this.getViewTexts(views.rows.map(x => x.pureName)); this.feedback({ analysingMessage: 'Loading indexes' }); @@ -174,20 +215,26 @@ class Analyser extends DatabaseAnalyser { .map(x => _.omit(x, ['objectType'])) .map(x => ({ ...x, - createSql: `DELIMITER //\n\nCREATE PROCEDURE \`${x.pureName}\`()\n${x.routineDefinition}\n\nDELIMITER ;\n`, + createSql: `DELIMITER //\n\nCREATE PROCEDURE \`${x.pureName}\`(${getParametersSqlString( + procedureNameToParameters[`${x.schemaName}.${x.pureName}`] + )})\n${x.routineDefinition}\n\nDELIMITER ;\n`, objectId: x.pureName, contentHash: _.isDate(x.modifyDate) ? x.modifyDate.toISOString() : x.modifyDate, + parameters: procedureNameToParameters[`${x.schemaName}.${x.pureName}`], })), functions: programmables.rows .filter(x => x.objectType == 'FUNCTION') .map(x => _.omit(x, ['objectType'])) .map(x => ({ ...x, - createSql: `CREATE FUNCTION \`${x.pureName}\`()\nRETURNS ${x.returnDataType} ${ - x.isDeterministic == 'YES' ? 'DETERMINISTIC' : 'NOT DETERMINISTIC' - }\n${x.routineDefinition}`, + createSql: `CREATE FUNCTION \`${x.pureName}\`(${getParametersSqlString( + functionNameToParameters[`${x.schemaName}.${x.pureName}`]?.filter(i => i.parameterMode !== 'RETURN') + )})\nRETURNS ${x.returnDataType} ${x.isDeterministic == 'YES' ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}\n${ + x.routineDefinition + }`, objectId: x.pureName, contentHash: _.isDate(x.modifyDate) ? x.modifyDate.toISOString() : x.modifyDate, + parameters: functionNameToParameters[`${x.schemaName}.${x.pureName}`], })), }; this.feedback({ analysingMessage: null }); diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js index 4839071e7..8c3e095fa 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js @@ -10,6 +10,7 @@ const procedureModifications = require('./procedureModifications'); const functionModifications = require('./functionModifications'); const uniqueNames = require('./uniqueNames'); const viewTexts = require('./viewTexts'); +const parameters = require('./parameters'); module.exports = { columns, @@ -19,6 +20,7 @@ module.exports = { tableModifications, views, programmables, + parameters, procedureModifications, functionModifications, indexes, diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/parameters.js b/plugins/dbgate-plugin-mysql/src/backend/sql/parameters.js new file mode 100644 index 000000000..e3091da1d --- /dev/null +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/parameters.js @@ -0,0 +1,26 @@ +module.exports = ` +SELECT + r.ROUTINE_SCHEMA AS schemaName, + r.SPECIFIC_NAME AS pureName, + CASE + WHEN COALESCE(NULLIF(PARAMETER_MODE, ''), 'RETURN') = 'RETURN' THEN 'Return' + ELSE PARAMETER_NAME + END AS parameterName, + p.CHARACTER_MAXIMUM_LENGTH AS charMaxLength, + p.NUMERIC_PRECISION AS numericPrecision, + p.NUMERIC_SCALE AS numericScale, + p.DTD_IDENTIFIER AS dataType, + COALESCE(NULLIF(PARAMETER_MODE, ''), 'RETURN') AS parameterMode, + r.ROUTINE_TYPE AS routineType, -- Function or Procedure + p.ORDINAL_POSITION AS ordinalPosition +FROM + information_schema.PARAMETERS p +JOIN + information_schema.ROUTINES r +ON + p.SPECIFIC_NAME = r.SPECIFIC_NAME AND r.ROUTINE_SCHEMA = p.SPECIFIC_SCHEMA +WHERE + r.ROUTINE_SCHEMA = '#DATABASE#' AND r.ROUTINE_NAME =OBJECT_ID_CONDITION +ORDER BY + r.ROUTINE_SCHEMA, r.SPECIFIC_NAME, p.ORDINAL_POSITION +`; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/programmables.js b/plugins/dbgate-plugin-mysql/src/backend/sql/programmables.js index f6442fcc1..588c04012 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/programmables.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/programmables.js @@ -1,5 +1,6 @@ module.exports = ` select + ROUTINE_SCHEMA AS schemaName, ROUTINE_NAME as pureName, ROUTINE_TYPE as objectType, COALESCE(LAST_ALTERED, CREATED) as modifyDate, diff --git a/plugins/dbgate-plugin-postgres/src/backend/Analyser.js b/plugins/dbgate-plugin-postgres/src/backend/Analyser.js index 84b67a858..90bb94ab8 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-postgres/src/backend/Analyser.js @@ -49,6 +49,19 @@ function getColumnInfo( }; } +function getParametersSqlString(parameters = []) { + if (!parameters?.length) return ''; + + return parameters + .map(i => { + const mode = i.parameterMode ? `${i.parameterMode} ` : ''; + const dataType = i.dataType ? ` ${i.dataType.toUpperCase()}` : ''; + const parameterName = i.parameterName ?? ''; + return `${mode}${parameterName}${dataType}`; + }) + .join(', '); +} + class Analyser extends DatabaseAnalyser { constructor(dbhan, driver, version) { super(dbhan, driver, version); @@ -144,6 +157,9 @@ class Analyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'Loading routines' }); const routines = await this.analyserQuery('routines', ['procedures', 'functions']); + this.feedback({ analysingMessage: 'Loading routine parameters' }); + const routineParametersRows = await this.analyserQuery('proceduresParameters'); + this.feedback({ analysingMessage: 'Loading indexes' }); const indexes = this.driver.__analyserInternals.skipIndexes ? { rows: [] } @@ -191,6 +207,40 @@ class Analyser extends DatabaseAnalyser { columnName: x.column_name, })); + const procedureParameters = routineParametersRows.rows + .filter(i => i.routine_type == 'PROCEDURE') + .map(i => ({ + pureName: i.pure_name, + parameterName: i.parameter_name, + dataType: normalizeTypeName(i.data_type), + parameterMode: i.parameter_mode, + schemaName: i.schema_name, + })); + + const procedureNameToParameters = procedureParameters.reduce((acc, row) => { + if (!acc[`${row.schemaName}.${row.pureName}`]) acc[`${row.schemaName}.${row.pureName}`] = []; + acc[`${row.schemaName}.${row.pureName}`].push(row); + + return acc; + }, {}); + + const functionParameters = routineParametersRows.rows + .filter(i => i.routine_type == 'FUNCTION') + .map(i => ({ + pureName: i.pure_name, + parameterName: i.parameter_name, + dataType: normalizeTypeName(i.data_type), + parameterMode: i.parameter_mode, + schemaName: i.schema_name, + })); + + const functionNameToParameters = functionParameters.reduce((acc, row) => { + if (!acc[`${row.schemaName}.${row.pureName}`]) acc[`${row.schemaName}.${row.pureName}`] = []; + acc[`${row.schemaName}.${row.pureName}`].push(row); + + return acc; + }, {}); + const res = { tables: tables.rows.map(table => { const newTable = { @@ -279,17 +329,23 @@ class Analyser extends DatabaseAnalyser { objectId: `procedures:${proc.schema_name}.${proc.pure_name}`, pureName: proc.pure_name, schemaName: proc.schema_name, - createSql: `CREATE PROCEDURE "${proc.schema_name}"."${proc.pure_name}"() LANGUAGE ${proc.language}\nAS\n$$\n${proc.definition}\n$$`, + createSql: `CREATE PROCEDURE "${proc.schema_name}"."${proc.pure_name}"(${getParametersSqlString( + procedureNameToParameters[`${proc.schema_name}.${proc.pure_name}`] + )}) LANGUAGE ${proc.language}\nAS\n$$\n${proc.definition}\n$$`, contentHash: proc.hash_code, + parameters: procedureNameToParameters[`${proc.schema_name}.${proc.pure_name}`], })), functions: routines.rows .filter(x => x.object_type == 'FUNCTION') .map(func => ({ objectId: `functions:${func.schema_name}.${func.pure_name}`, - createSql: `CREATE FUNCTION "${func.schema_name}"."${func.pure_name}"() RETURNS ${func.data_type} LANGUAGE ${func.language}\nAS\n$$\n${func.definition}\n$$`, + createSql: `CREATE FUNCTION "${func.schema_name}"."${func.pure_name}"(${getParametersSqlString( + functionNameToParameters[`${func.schema_name}.${func.pure_name}`] + )}) RETURNS ${func.data_type.toUpperCase()} LANGUAGE ${func.language}\nAS\n$$\n${func.definition}\n$$`, pureName: func.pure_name, schemaName: func.schema_name, contentHash: func.hash_code, + parameters: functionNameToParameters[`${func.schema_name}.${func.pure_name}`], })), }; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js index 53a858ab5..b3f338646 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js @@ -14,6 +14,7 @@ const indexcols = require('./indexcols'); const uniqueNames = require('./uniqueNames'); const geometryColumns = require('./geometryColumns'); const geographyColumns = require('./geographyColumns'); +const proceduresParameters = require('./proceduresParameters'); const fk_keyColumnUsage = require('./fk_key_column_usage'); const fk_referentialConstraints = require('./fk_referential_constraints'); @@ -39,4 +40,5 @@ module.exports = { uniqueNames, geometryColumns, geographyColumns, + proceduresParameters, }; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/proceduresParameters.js b/plugins/dbgate-plugin-postgres/src/backend/sql/proceduresParameters.js new file mode 100644 index 000000000..667cfed09 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/proceduresParameters.js @@ -0,0 +1,31 @@ +module.exports = ` +SELECT + proc.specific_schema AS schema_name, + proc.routine_name AS pure_name, + proc.routine_type as routine_type, + args.parameter_name AS parameter_name, + args.parameter_mode, + args.data_type AS data_type, + args.ordinal_position AS parameter_index, + args.parameter_mode AS parameter_mode +FROM + information_schema.routines proc +LEFT JOIN + information_schema.parameters args + ON proc.specific_schema = args.specific_schema + AND proc.specific_name = args.specific_name +WHERE + proc.specific_schema NOT IN ('pg_catalog', 'information_schema') -- Exclude system schemas + AND args.parameter_name IS NOT NULL + AND proc.routine_type IN ('PROCEDURE', 'FUNCTION') -- Filter for procedures + AND proc.specific_schema !~ '^_timescaledb_' + AND proc.specific_schema =SCHEMA_NAME_CONDITION + AND ( + (routine_type = 'PROCEDURE' AND ('procedures:' || proc.specific_schema || '.' || routine_name) =OBJECT_ID_CONDITION) + OR + (routine_type = 'FUNCTION' AND ('functions:' || proc.specific_schema || '.' || routine_name) =OBJECT_ID_CONDITION) + ) +ORDER BY + schema_name, + args.ordinal_position; +`;