🐛 fix(sqlserver): 修复对象 SQL 定义获取失败

- SQL Server 对象定义改为通过 sys.all_sql_modules 按库、schema、对象名精确查询
- 增加 sp_helptext 兼容候选,支持拼接多行 Text 返回完整定义
- 统一修复视图、函数/存储过程、触发器定义查看与对象修改入口
- 补充 SQL Server 对象定义查询和组件回归测试
This commit is contained in:
Syngnat
2026-06-16 12:54:39 +08:00
parent f41a15c7b8
commit 54195e0591
7 changed files with 304 additions and 20 deletions

View File

@@ -178,6 +178,68 @@ describe('DefinitionViewer object edit entry', () => {
}));
});
it('uses SQL Server catalog metadata when loading routine definitions', async () => {
storeState.connections[0].config.type = 'sqlserver';
backendApp.DBQuery.mockResolvedValue({
success: true,
data: [{ routine_definition: 'CREATE PROCEDURE [reporting].[refresh_stats]\nAS\nSELECT 1;' }],
});
let renderer: any;
await act(async () => {
renderer = create(<DefinitionViewer tab={createTab({
id: 'routine-def-conn-1-main-reporting.refresh_stats',
title: '存储过程: reporting.refresh_stats',
type: 'routine-def',
routineName: 'reporting.refresh_stats',
routineType: 'PROCEDURE',
viewName: undefined,
viewKind: undefined,
})} />);
await flushPromises();
});
const sql = String(backendApp.DBQuery.mock.calls[0][2] || '');
expect(sql).toContain('FROM [main].sys.all_sql_modules AS m');
expect(sql).toContain("WHERE o.name = N'refresh_stats'");
expect(sql).toContain("AND s.name = N'reporting'");
expect(sql).not.toContain('OBJECT_DEFINITION');
expect(String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''))).toContain('CREATE PROCEDURE [reporting].[refresh_stats]');
});
it('joins SQL Server sp_helptext rows when catalog metadata is empty', async () => {
storeState.connections[0].config.type = 'sqlserver';
backendApp.DBQuery
.mockResolvedValueOnce({ success: true, data: [] })
.mockResolvedValueOnce({
success: true,
data: [
{ Text: 'CREATE PROCEDURE [reporting].[refresh_stats]\n' },
{ Text: 'AS\n' },
{ Text: 'BEGIN\n SELECT 1;\nEND' },
],
});
let renderer: any;
await act(async () => {
renderer = create(<DefinitionViewer tab={createTab({
id: 'routine-def-conn-1-main-reporting.refresh_stats',
title: '存储过程: reporting.refresh_stats',
type: 'routine-def',
routineName: 'reporting.refresh_stats',
routineType: 'PROCEDURE',
viewName: undefined,
viewKind: undefined,
})} />);
await flushPromises();
});
expect(backendApp.DBQuery.mock.calls[1][2]).toBe("EXEC [main].sys.sp_helptext @objname = N'[reporting].[refresh_stats]'");
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('CREATE PROCEDURE [reporting].[refresh_stats]');
expect(editorText).toContain('BEGIN\n SELECT 1;\nEND');
});
it('adds CREATE OR REPLACE for routine source snippets returned without ddl prefix', async () => {
storeState.connections[0].config.type = 'oracle';
backendApp.DBQuery.mockResolvedValue({

View File

@@ -8,6 +8,7 @@ import { DBQuery } from '../../wailsjs/go/app/App';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
import { splitQualifiedNameLast } from '../utils/qualifiedName';
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
interface DefinitionViewerProps {
tab: TabData;
@@ -204,7 +205,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
return [`SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`];
}
case 'sqlserver':
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(viewName)}')) AS view_definition`];
return buildSqlServerObjectDefinitionQueries('view', viewName, dbName, 'view_definition');
case 'oracle':
case 'dm':
if (schema) {
@@ -253,7 +254,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
return [`SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${safeName}' LIMIT 1`];
}
case 'sqlserver':
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(routineName)}')) AS routine_definition`];
return buildSqlServerObjectDefinitionQueries('routine', routineName, dbName, 'routine_definition');
case 'oracle':
case 'dm': {
const owner = schema ? escapeSQLLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : '');
@@ -381,6 +382,19 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
case 'oracle':
case 'dm':
return row.view_definition || row.VIEW_DEFINITION || row.text || row.TEXT || Object.values(row)[0] || '';
case 'sqlserver': {
const directDefinition = getCaseInsensitiveRawValue(row, ['view_definition', 'definition']);
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
return String(directDefinition);
}
const helpTextDefinition = data
.map((item) => getCaseInsensitiveRawValue(item, ['Text', 'text']))
.filter((value) => value !== undefined && value !== null)
.map((value) => String(value))
.join('');
if (helpTextDefinition.trim()) return helpTextDefinition;
return String(Object.values(row)[0] || '');
}
default:
return row.view_definition || row.VIEW_DEFINITION || row.sql || row.SQL || Object.values(row)[0] || '';
}
@@ -432,6 +446,19 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
}
return JSON.stringify(row, null, 2);
}
case 'sqlserver': {
const directDefinition = getCaseInsensitiveRawValue(data[0], ['routine_definition', 'definition']);
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
return String(directDefinition);
}
const helpTextDefinition = data
.map((row) => getCaseInsensitiveRawValue(row, ['Text', 'text']))
.filter((value) => value !== undefined && value !== null)
.map((value) => String(value))
.join('');
if (helpTextDefinition.trim()) return helpTextDefinition;
return String(Object.values(data[0])[0] || '');
}
default: {
const row = data[0];
return row.routine_definition || row.ROUTINE_DEFINITION || Object.values(row)[0] || '';

View File

@@ -91,6 +91,7 @@ import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
import JVMModeBadge from './jvm/JVMModeBadge';
import MessagePublishModal from './MessagePublishModal';
import {
@@ -1259,6 +1260,19 @@ const Sidebar: React.FC<{
return '';
};
const extractSqlServerDefinitionRows = (rows: any[], definitionKeys: string[]): string => {
if (!Array.isArray(rows) || rows.length === 0) return '';
const directDefinition = getCaseInsensitiveRawValue(rows[0] as Record<string, any>, definitionKeys);
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
return String(directDefinition);
}
return rows
.map((row) => getCaseInsensitiveRawValue(row as Record<string, any>, ['Text', 'text']))
.filter((value) => value !== undefined && value !== null)
.map((value) => String(value))
.join('');
};
const getMySQLShowTablesName = (row: Record<string, any>): string => {
for (const key of Object.keys(row || {})) {
if (!key.toLowerCase().startsWith('tables_in_')) continue;
@@ -4481,44 +4495,51 @@ const Sidebar: React.FC<{
try {
const config = buildRuntimeConfig(conn, dbName);
let query = '';
let queries: string[] = [];
switch (dialect) {
case 'mysql':
case 'starrocks':
query = `SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``;
queries = [`SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``];
break;
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': case 'gaussdb': {
const parts = splitQualifiedName(viewName);
const schema = parts.schemaName || 'public';
const name = parts.objectName || viewName;
query = `SELECT pg_get_viewdef('${escapeSQLLiteral(schema)}.${escapeSQLLiteral(name)}'::regclass, true) AS view_definition`;
queries = [`SELECT pg_get_viewdef('${escapeSQLLiteral(schema)}.${escapeSQLLiteral(name)}'::regclass, true) AS view_definition`];
break;
}
case 'sqlserver':
query = `SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(viewName)}')) AS view_definition`;
queries = buildSqlServerObjectDefinitionQueries('view', viewName, dbName, 'view_definition');
break;
case 'sqlite':
query = `SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${escapeSQLLiteral(viewName)}'`;
queries = [`SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${escapeSQLLiteral(viewName)}'`];
break;
case 'duckdb': {
const parts = splitQualifiedName(viewName);
const viewSchema = escapeSQLLiteral(parts.schemaName || 'main');
const viewObject = escapeSQLLiteral(parts.objectName || viewName);
query = `SELECT view_definition FROM information_schema.views WHERE table_schema='${viewSchema}' AND table_name='${viewObject}' LIMIT 1`;
queries = [`SELECT view_definition FROM information_schema.views WHERE table_schema='${viewSchema}' AND table_name='${viewObject}' LIMIT 1`];
break;
}
}
if (query) {
for (const query of queries) {
const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, query);
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
const row = result.data[0] as Record<string, any>;
const def = row.view_definition || row.VIEW_DEFINITION || Object.values(row).find(v => typeof v === 'string' && String(v).length > 10) || '';
const def = dialect === 'sqlserver'
? extractSqlServerDefinitionRows(result.data, ['view_definition', 'definition'])
: row.view_definition || row.VIEW_DEFINITION || Object.values(row).find(v => typeof v === 'string' && String(v).length > 10) || '';
if (def) {
if (dialect === 'mysql') {
template = `-- 编辑视图 ${viewName}\n${normalizeMySQLViewDDLForEditing(viewName, def)}`;
} else if (dialect === 'sqlserver') {
template = /^\s*create\s+view\b/i.test(String(def))
? `-- 编辑视图 ${viewName}\n${def}`
: `-- 编辑视图 ${viewName}\nCREATE VIEW ${viewName} AS\n${def}`;
} else {
template = `-- 编辑视图 ${viewName}\nCREATE OR REPLACE VIEW ${viewName} AS\n${def}`;
}
break;
}
}
}
@@ -4812,7 +4833,7 @@ const Sidebar: React.FC<{
break;
}
case 'sqlserver':
query = `SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(routineName)}')) AS routine_definition`;
query = '';
break;
case 'oracle': case 'dm': {
const owner = schema ? escapeSQLLiteral(schema).toUpperCase() : '';
@@ -4829,12 +4850,18 @@ const Sidebar: React.FC<{
break;
}
}
if (query) {
const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, query);
const queries = dialect === 'sqlserver'
? buildSqlServerObjectDefinitionQueries('routine', routineName, dbName, 'routine_definition')
: [query].filter(Boolean);
for (const queryText of queries) {
const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, queryText);
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
if (dialect === 'oracle' || dialect === 'dm') {
const lines = result.data.map((row: any) => row.text || row.TEXT || Object.values(row)[0] || '').join('');
if (lines) template = `-- 编辑${typeLabel} ${routineName}\nCREATE OR REPLACE ${lines}`;
if (lines) {
template = `-- 编辑${typeLabel} ${routineName}\nCREATE OR REPLACE ${lines}`;
break;
}
} else if (dialect === 'duckdb') {
const row = result.data[0] as Record<string, any>;
const ddl = buildDuckDBMacroDDL(
@@ -4843,11 +4870,19 @@ const Sidebar: React.FC<{
getCaseInsensitiveRawValue(row, ['parameters']),
getCaseInsensitiveRawValue(row, ['macro_definition'])
);
if (ddl) template = `-- 编辑${typeLabel} ${routineName}\n${ddl}`;
if (ddl) {
template = `-- 编辑${typeLabel} ${routineName}\n${ddl}`;
break;
}
} else {
const row = result.data[0] as Record<string, any>;
const def = row.routine_definition || row.ROUTINE_DEFINITION || Object.values(row).find(v => typeof v === 'string' && String(v).length > 10) || '';
if (def) template = `-- 编辑${typeLabel} ${routineName}\n${def}`;
const def = dialect === 'sqlserver'
? extractSqlServerDefinitionRows(result.data, ['routine_definition', 'definition'])
: row.routine_definition || row.ROUTINE_DEFINITION || Object.values(row).find(v => typeof v === 'string' && String(v).length > 10) || '';
if (def) {
template = `-- 编辑${typeLabel} ${routineName}\n${def}`;
break;
}
}
}
}

View File

@@ -117,6 +117,28 @@ describe('TriggerViewer object edit entry', () => {
}));
});
it('uses SQL Server catalog metadata when loading trigger definitions', async () => {
storeState.connections[0].config.type = 'sqlserver';
backendApp.DBQuery.mockResolvedValue({
success: true,
data: [{ trigger_definition: 'CREATE TRIGGER [audit].[users_bi] ON [audit].[users] AFTER INSERT AS SELECT 1;' }],
});
let renderer: any;
await act(async () => {
renderer = create(<TriggerViewer tab={tab} />);
await flushPromises();
});
const sql = String(backendApp.DBQuery.mock.calls[0][2] || '');
expect(sql).toContain('FROM [main].sys.all_sql_modules AS m');
expect(sql).toContain("WHERE o.name = N'users_bi'");
expect(sql).toContain("AND s.name = N'audit'");
expect(sql).toContain("o.type IN ('TR', 'TA')");
expect(sql).not.toContain('OBJECT_DEFINITION');
expect(String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''))).toContain('CREATE TRIGGER [audit].[users_bi]');
});
it('adds CREATE OR REPLACE for trigger source snippets returned without ddl prefix', async () => {
storeState.connections[0].config.type = 'oracle';
backendApp.DBQuery.mockResolvedValue({

View File

@@ -9,6 +9,7 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
import { splitQualifiedNameLast } from '../utils/qualifiedName';
import { buildEditableTriggerSql } from '../utils/triggerEditSql';
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
interface TriggerViewerProps {
tab: TabData;
@@ -125,7 +126,6 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
const parseSchemaAndName = (fullName: string): { schema: string; name: string } => {
const parsed = splitQualifiedNameLast(fullName);
return { schema: parsed.parentPath, name: parsed.objectName };
@@ -185,7 +185,7 @@ WHERE t.tgname = '${safeTriggerName}'
AND NOT t.tgisinternal
LIMIT 1`];
case 'sqlserver': {
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(triggerName)}')) AS trigger_definition`];
return buildSqlServerObjectDefinitionQueries('trigger', triggerName, dbName, 'trigger_definition');
}
case 'oracle':
case 'dm':
@@ -318,7 +318,17 @@ LIMIT 1`];
return row.trigger_definition || row.TRIGGER_DEFINITION || Object.values(row)[0] || '';
}
case 'sqlserver': {
return row.trigger_definition || row.TRIGGER_DEFINITION || Object.values(row)[0] || '';
const directDefinition = getCaseInsensitiveRawValue(row, ['trigger_definition', 'definition']);
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
return String(directDefinition);
}
const helpTextDefinition = data
.map((item) => getCaseInsensitiveRawValue(item, ['Text', 'text']))
.filter((value) => value !== undefined && value !== null)
.map((value) => String(value))
.join('');
if (helpTextDefinition.trim()) return helpTextDefinition;
return Object.values(row)[0] || '';
}
case 'oracle':
case 'dm': {

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import { buildSqlServerObjectDefinitionQueries } from './sqlServerObjectDefinition';
describe('buildSqlServerObjectDefinitionQueries', () => {
it('builds schema-aware SQL Server routine definition queries', () => {
const queries = buildSqlServerObjectDefinitionQueries('routine', 'dbo.p_get_select', 'BizDB', 'routine_definition');
expect(queries).toHaveLength(2);
expect(queries[0]).toContain('FROM [BizDB].sys.all_sql_modules AS m');
expect(queries[0]).toContain('JOIN [BizDB].sys.all_objects AS o ON o.object_id = m.object_id');
expect(queries[0]).toContain("WHERE o.name = N'p_get_select'");
expect(queries[0]).toContain("AND s.name = N'dbo'");
expect(queries[0]).toContain("o.type IN ('P', 'PC', 'RF', 'FN', 'FS', 'FT', 'IF', 'TF')");
expect(queries[0]).not.toContain('OBJECT_DEFINITION');
expect(queries[1]).toBe("EXEC [BizDB].sys.sp_helptext @objname = N'[dbo].[p_get_select]'");
});
it('uses the database segment from a three-part SQL Server object name', () => {
const queries = buildSqlServerObjectDefinitionQueries('view', 'Archive.reporting.active_users', 'BizDB', 'view_definition');
expect(queries[0]).toContain('FROM [Archive].sys.all_sql_modules AS m');
expect(queries[0]).toContain("WHERE o.name = N'active_users'");
expect(queries[0]).toContain("AND s.name = N'reporting'");
expect(queries[0]).toContain("o.type IN ('V')");
expect(queries[1]).toBe("EXEC [Archive].sys.sp_helptext @objname = N'[reporting].[active_users]'");
});
it('falls back to all schemas when SQL Server object name is unqualified', () => {
const queries = buildSqlServerObjectDefinitionQueries('routine', 'sp_helptext', 'master', 'routine_definition');
expect(queries[0]).toContain('FROM [master].sys.all_sql_modules AS m');
expect(queries[0]).toContain("WHERE o.name = N'sp_helptext'");
expect(queries[0]).not.toContain('AND s.name = N');
expect(queries[0]).toContain("CASE WHEN s.name = N'dbo' THEN 0 WHEN s.name = N'sys' THEN 1 ELSE 2 END");
expect(queries[1]).toBe("EXEC [master].sys.sp_helptext @objname = N'sp_helptext'");
});
it('escapes SQL Server literals and bracket identifiers', () => {
const queries = buildSqlServerObjectDefinitionQueries('trigger', "audit]x.o'clock", 'Biz]DB', 'trigger_definition');
expect(queries[0]).toContain('FROM [Biz]]DB].sys.all_sql_modules AS m');
expect(queries[0]).toContain("WHERE o.name = N'o''clock'");
expect(queries[0]).toContain("AND s.name = N'audit]x'");
expect(queries[1]).toBe("EXEC [Biz]]DB].sys.sp_helptext @objname = N'[audit]]x].[o''clock]'");
});
});

View File

@@ -0,0 +1,81 @@
import { splitQualifiedNameSegments } from './qualifiedName';
export type SqlServerObjectDefinitionKind = 'view' | 'routine' | 'trigger';
const SQL_SERVER_OBJECT_TYPES: Record<SqlServerObjectDefinitionKind, string[]> = {
view: ['V'],
routine: ['P', 'PC', 'RF', 'FN', 'FS', 'FT', 'IF', 'TF'],
trigger: ['TR', 'TA'],
};
const escapeSqlServerLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
const buildSqlServerObjectRef = (schemaName: string, objectName: string): string => {
const object = String(objectName || '').trim();
const schema = String(schemaName || '').trim();
if (!object) return '';
if (!schema) return object;
return `${quoteSqlServerIdentifier(schema)}.${quoteSqlServerIdentifier(object)}`;
};
const resolveSqlServerObjectTarget = (rawObjectName: string, fallbackDbName: string) => {
const segments = splitQualifiedNameSegments(rawObjectName).filter(Boolean);
const objectName = String(segments[segments.length - 1] || '').trim();
let schemaName = '';
let databaseName = String(fallbackDbName || '').trim();
if (segments.length >= 3) {
databaseName = String(segments[segments.length - 3] || databaseName).trim();
schemaName = String(segments[segments.length - 2] || '').trim();
} else if (segments.length === 2) {
schemaName = String(segments[0] || '').trim();
}
return { databaseName, schemaName, objectName };
};
export const buildSqlServerObjectDefinitionQueries = (
kind: SqlServerObjectDefinitionKind,
objectName: string,
dbName: string,
resultAlias: string,
): string[] => {
const target = resolveSqlServerObjectTarget(objectName, dbName);
if (!target.objectName) return [];
const catalogPrefix = target.databaseName ? `${quoteSqlServerIdentifier(target.databaseName)}.` : '';
const safeObjectName = escapeSqlServerLiteral(target.objectName);
const safeSchemaName = escapeSqlServerLiteral(target.schemaName);
const objectTypes = SQL_SERVER_OBJECT_TYPES[kind] || [];
const typeFilter = objectTypes.length > 0
? ` AND o.type IN (${objectTypes.map((type) => `'${type}'`).join(', ')})\n`
: '';
const schemaFilter = target.schemaName
? ` AND s.name = N'${safeSchemaName}'\n`
: '';
const schemaOrder = target.schemaName
? 's.name'
: "CASE WHEN s.name = N'dbo' THEN 0 WHEN s.name = N'sys' THEN 1 ELSE 2 END, s.name";
const moduleQuery = [
`SELECT TOP (1)`,
` m.definition AS ${resultAlias}`,
`FROM ${catalogPrefix}sys.all_sql_modules AS m`,
`JOIN ${catalogPrefix}sys.all_objects AS o ON o.object_id = m.object_id`,
`JOIN ${catalogPrefix}sys.schemas AS s ON s.schema_id = o.schema_id`,
`WHERE o.name = N'${safeObjectName}'`,
typeFilter.trimEnd(),
schemaFilter.trimEnd(),
` AND m.definition IS NOT NULL`,
`ORDER BY ${schemaOrder}, o.name`,
].filter(Boolean).join('\n');
const objectRef = buildSqlServerObjectRef(target.schemaName, target.objectName);
const helpTextProcedure = target.databaseName
? `EXEC ${quoteSqlServerIdentifier(target.databaseName)}.sys.sp_helptext @objname = N'${escapeSqlServerLiteral(objectRef)}'`
: `EXEC sys.sp_helptext @objname = N'${escapeSqlServerLiteral(objectRef)}'`;
return [moduleQuery, helpTextProcedure];
};