From a91f672b0e2910a58b06ed44925e42963ec80c7b Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 1 Apr 2026 12:53:46 +0300 Subject: [PATCH 1/5] HCK-15535: add support for procedures (RE from instance) --- .../databaseService/databaseService.js | 40 ++++++++++ .../helpers/parsers/parseProcedure.js | 75 +++++++++++++++++++ .../reverseEngineeringService.js | 6 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 reverse_engineering/databaseService/helpers/parsers/parseProcedure.js diff --git a/reverse_engineering/databaseService/databaseService.js b/reverse_engineering/databaseService/databaseService.js index 6d8ba03..e585f22 100644 --- a/reverse_engineering/databaseService/databaseService.js +++ b/reverse_engineering/databaseService/databaseService.js @@ -11,6 +11,7 @@ const { const { getDatabaseIndexesSubQueryForRetrievingTheTablesSelectedByTheUser, } = require('../queries/selectedTablesSubQuery/databaseIndexesSubQueryForRetrievingTheTablesSelectedByTheUser'); +const { parseProcedure } = require('./helpers/parsers/parseProcedure'); const QUERY_REQUEST_TIMEOUT = 60000; @@ -675,6 +676,44 @@ async function getTableRowCount(tableSchema, tableName, currentDbConnectionClien return rowCount; } +const getDatabaseProcedures = async ({ connectionClient, dbName, logger }) => { + try { + const currentDbConnectionClient = await getNewConnectionClientByDb(connectionClient, dbName); + + logger.log('info', { message: `Get '${dbName}' database procedures.` }, 'Reverse Engineering'); + + const response = await currentDbConnectionClient.query(` + SELECT + s.name AS schema_name, + p.name AS procedure_name, + sm.definition AS procedure_body + FROM sys.procedures p + JOIN sys.schemas s + ON p.schema_id = s.schema_id + LEFT JOIN sys.sql_modules sm + ON p.object_id = sm.object_id + ORDER BY s.name, p.name; + `); + + const rawProcedures = await mapResponse(response); + + return rawProcedures.map(parseProcedure(logger)); + } catch (error) { + logger.log( + 'error', + { message: error.message, stack: error.stack, error }, + `Get '${dbName}' database procedures.`, + ); + logger.progress({ + message: `Warning: failed to reverse-engineer procedures.`, + containerName: '', + entityName: '', + }); + + return []; + } +}; + module.exports = { getConnectionClient, getObjectsFromDatabase, @@ -694,4 +733,5 @@ module.exports = { getViewDistributedColumns, queryDistribution, getPartitions, + getDatabaseProcedures, }; diff --git a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js new file mode 100644 index 0000000..621f2a1 --- /dev/null +++ b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js @@ -0,0 +1,75 @@ +const { trim } = require('lodash'); +/** + * @typedef {object} RawProcedure + * @property {string} schema_name + * @property {string} procedure_name + * @property {string|null} procedure_body + * + * @typedef {object} Procedure + * @property {string} name + * @property {string} schemaName + * @property {boolean} orReplace + * @property {string} [inputArgs] + * @property {string} [body] + */ + +const createProcedureRegexp = + /CREATE(?\s*OR\s*ALTER)?\s*(?:PROC|PROCEDURE)\s*(?:[^\s(]+)\s*(?\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:\s*@\w+[^@]*?)*?)?\s*AS\s*(?[\s\S]+)/i; + +/** + * + * @param {string} [args] + * @returns {string|undefined} + */ +const formatArgs = args => { + if (typeof args === 'string') { + return args.split(',').map(trim).join(',\n'); + } +}; + +/** + * + * @param {string} [body] + * @returns {string|undefined} + */ +const formatBody = body => { + if (typeof body === 'string') { + return body.replace(/;$/, ''); + } +}; + +/** + * + * @param {{ log: (logType: string, error: Error, message: string) => void }} logger + * @returns {(rawProcedure: RawProcedure) => Procedure} + */ +const parseProcedure = logger => rawProcedure => { + const { schema_name, procedure_name, procedure_body } = rawProcedure; + + try { + const result = createProcedureRegexp.exec(procedure_body); + const { inputArgs, body } = result.groups; + + return { + name: procedure_name, + schemaName: schema_name, + inputArgs: formatArgs(inputArgs), + body: formatBody(body), + }; + } catch (error) { + logger.log( + 'error', + { message: error.message, stack: error.stack, error }, + `Error parsing procedure ${schema_name}.${procedure_name}`, + ); + + return { + name: procedure_name, + schemaName: schema_name, + }; + } +}; + +module.exports = { + parseProcedure, +}; diff --git a/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js b/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js index ecb6e41..e4d8d16 100644 --- a/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js +++ b/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js @@ -16,6 +16,7 @@ const { queryDistribution, getPartitions, getTableMaskedColumns, + getDatabaseProcedures, } = require('../databaseService/databaseService'); const { transformDatabaseTableInfoToJSON, @@ -369,10 +370,11 @@ const getPersistence = tableName => { const reverseCollectionsToJSON = logger => async (dbConnectionClient, tablesInfo, reverseEngineeringOptions) => { const dbName = dbConnectionClient.config.database; progress(logger, `RE data from database "${dbName}"`, dbName); - const [databaseIndexes, databaseUDT, dataBasePartitions] = await Promise.all([ + const [databaseIndexes, databaseUDT, dataBasePartitions, databaseProcedures] = await Promise.all([ getDatabaseIndexes({ connectionClient: dbConnectionClient, tablesInfo, dbName, logger }), getDatabaseUserDefinedTypes({ connectionClient: dbConnectionClient, dbName, logger }), getPartitions({ connectionClient: dbConnectionClient, tablesInfo, dbName, logger }), + getDatabaseProcedures({ connectionClient: dbConnectionClient, dbName, logger }), ]); return Object.entries(tablesInfo).reduce(async (jsonSchemas, [schemaName, tableNames]) => { @@ -387,6 +389,7 @@ const reverseCollectionsToJSON = logger => async (dbConnectionClient, tablesInfo const tablePartitions = dataBasePartitions.filter( partition => partition.tableName === tableName && partition.schemaName === schemaName, ); + const schemaProcedures = databaseProcedures.filter(procedure => procedure.schemaName === schemaName); const tableInfo = await getTableInfo({ connectionClient: dbConnectionClient, @@ -514,6 +517,7 @@ const reverseCollectionsToJSON = logger => async (dbConnectionClient, tablesInfo documents: cleanDocuments(reorderedTableRows), bucketInfo: { databaseName: dbName, + Procedures: schemaProcedures, }, modelDefinitions: { definitions: getUserDefinedTypes(tableInfo, databaseUDT), From 713ddd0277562d9dd8f45ee0379fef5c073c7d75 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 1 Apr 2026 16:38:01 +0300 Subject: [PATCH 2/5] HCK-15535: fix sonar issue --- .../helpers/parsers/parseProcedure.js | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js index 621f2a1..3527611 100644 --- a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js +++ b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js @@ -13,29 +13,20 @@ const { trim } = require('lodash'); * @property {string} [body] */ -const createProcedureRegexp = - /CREATE(?\s*OR\s*ALTER)?\s*(?:PROC|PROCEDURE)\s*(?:[^\s(]+)\s*(?\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:\s*@\w+[^@]*?)*?)?\s*AS\s*(?[\s\S]+)/i; +const parseProcedureProperties = statement => { + const createProcedureRegexp = /CREATE(?:\s+OR\s+ALTER)?\s+(?:\bPROC|\bPROCEDURE)\s*(?:[^\s(]+)\s+/gi; + const procedurePropertiesRegexp = + /(?\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:\s*@\w+[^@]*?)*?)?\s*AS\s*(?[\s\S]+)/gi; -/** - * - * @param {string} [args] - * @returns {string|undefined} - */ -const formatArgs = args => { - if (typeof args === 'string') { - return args.split(',').map(trim).join(',\n'); - } -}; + const procedureStatement = statement.replace(createProcedureRegexp, ''); + const { groups } = procedurePropertiesRegexp.exec(procedureStatement); + const inputArgs = groups.inputArgs?.split(',').map(trim).join(',\n'); + const body = groups.body?.replace(/;$/, ''); -/** - * - * @param {string} [body] - * @returns {string|undefined} - */ -const formatBody = body => { - if (typeof body === 'string') { - return body.replace(/;$/, ''); - } + return { + inputArgs, + body, + }; }; /** @@ -47,14 +38,13 @@ const parseProcedure = logger => rawProcedure => { const { schema_name, procedure_name, procedure_body } = rawProcedure; try { - const result = createProcedureRegexp.exec(procedure_body); - const { inputArgs, body } = result.groups; + const { inputArgs, body } = parseProcedureProperties(procedure_body); return { name: procedure_name, schemaName: schema_name, - inputArgs: formatArgs(inputArgs), - body: formatBody(body), + inputArgs, + body, }; } catch (error) { logger.log( From 6a995b2f122d0522b0d1bc8972c212a165989dfd Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 1 Apr 2026 16:57:29 +0300 Subject: [PATCH 3/5] HCK-15535: fix sonar issue --- .../helpers/parsers/parseProcedure.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js index 3527611..95f2e10 100644 --- a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js +++ b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js @@ -13,15 +13,22 @@ const { trim } = require('lodash'); * @property {string} [body] */ +/** + * + * @param {string} statement + * @returns {Partial} + */ const parseProcedureProperties = statement => { - const createProcedureRegexp = /CREATE(?:\s+OR\s+ALTER)?\s+(?:\bPROC|\bPROCEDURE)\s*(?:[^\s(]+)\s+/gi; - const procedurePropertiesRegexp = - /(?\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:\s*@\w+[^@]*?)*?)?\s*AS\s*(?[\s\S]+)/gi; + const createProcedureRegexp = /CREATE(?:\s+OR\s+ALTER)?\s+(?:\bPROC\b|\bPROCEDURE\b)\s*(?:[^\s(]+)\s+/i; + const inputArgsRegexp = /^\s*(\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:(?!AS\b)[^()]+?))(?=\s*\bAS\b)/i; + const bodyRegexp = /\bAS\b\s*([\s\S]+)$/i; + + const procedureString = statement.replace(createProcedureRegexp, ''); + const inputArgsString = procedureString.match(inputArgsRegexp)?.[1]; + const bodyString = procedureString.match(bodyRegexp)?.[1]; - const procedureStatement = statement.replace(createProcedureRegexp, ''); - const { groups } = procedurePropertiesRegexp.exec(procedureStatement); - const inputArgs = groups.inputArgs?.split(',').map(trim).join(',\n'); - const body = groups.body?.replace(/;$/, ''); + const inputArgs = inputArgsString?.split(',').map(trim).join(',\n'); + const body = bodyString?.replace(/;$/, ''); return { inputArgs, From c144f659053ac3a17f8fb8b1a83eb76f413c1acc Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 1 Apr 2026 17:18:33 +0300 Subject: [PATCH 4/5] HCK-15535: fix sonar issue --- .../databaseService/helpers/parsers/parseProcedure.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js index 95f2e10..9c30210 100644 --- a/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js +++ b/reverse_engineering/databaseService/helpers/parsers/parseProcedure.js @@ -20,12 +20,12 @@ const { trim } = require('lodash'); */ const parseProcedureProperties = statement => { const createProcedureRegexp = /CREATE(?:\s+OR\s+ALTER)?\s+(?:\bPROC\b|\bPROCEDURE\b)\s*(?:[^\s(]+)\s+/i; - const inputArgsRegexp = /^\s*(\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:(?!AS\b)[^()]+?))(?=\s*\bAS\b)/i; + const inputArgsRegexp = /^([\s\S]+)(?=\s+\bAS\b)/i; const bodyRegexp = /\bAS\b\s*([\s\S]+)$/i; const procedureString = statement.replace(createProcedureRegexp, ''); - const inputArgsString = procedureString.match(inputArgsRegexp)?.[1]; - const bodyString = procedureString.match(bodyRegexp)?.[1]; + const inputArgsString = inputArgsRegexp.exec(procedureString)?.[1]; + const bodyString = bodyRegexp.exec(procedureString)?.[1]; const inputArgs = inputArgsString?.split(',').map(trim).join(',\n'); const body = bodyString?.replace(/;$/, ''); From cb43750cb3dcae1b6fd72e12fdd28650028f3d77 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 1 Apr 2026 17:21:38 +0300 Subject: [PATCH 5/5] HCK-15535: fix sonar issue --- .../reverseEngineeringService/reverseEngineeringService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js b/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js index e4d8d16..853bca2 100644 --- a/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js +++ b/reverse_engineering/reverseEngineeringService/reverseEngineeringService.js @@ -380,6 +380,7 @@ const reverseCollectionsToJSON = logger => async (dbConnectionClient, tablesInfo return Object.entries(tablesInfo).reduce(async (jsonSchemas, [schemaName, tableNames]) => { progress(logger, 'Fetching database information', dbName); const isSystemIndex = index => /^ClusteredIndex_[a-f0-9]{32}$/m.test(index.name || ''); + const schemaProcedures = databaseProcedures.filter(procedure => procedure.schemaName === schemaName); async function processTable(untrimmedTableName) { const tableName = untrimmedTableName.replace(/ \(v\)$/, ''); @@ -389,7 +390,6 @@ const reverseCollectionsToJSON = logger => async (dbConnectionClient, tablesInfo const tablePartitions = dataBasePartitions.filter( partition => partition.tableName === tableName && partition.schemaName === schemaName, ); - const schemaProcedures = databaseProcedures.filter(procedure => procedure.schemaName === schemaName); const tableInfo = await getTableInfo({ connectionClient: dbConnectionClient,