From 54195e059133981a1db23224358596a855f411da Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 16 Jun 2026 12:54:39 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sqlserver):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AF=B9=E8=B1=A1=20SQL=20=E5=AE=9A=E4=B9=89=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SQL Server 对象定义改为通过 sys.all_sql_modules 按库、schema、对象名精确查询 - 增加 sp_helptext 兼容候选,支持拼接多行 Text 返回完整定义 - 统一修复视图、函数/存储过程、触发器定义查看与对象修改入口 - 补充 SQL Server 对象定义查询和组件回归测试 --- .../DefinitionViewer.object-edit.test.tsx | 62 ++++++++++++++ frontend/src/components/DefinitionViewer.tsx | 31 ++++++- frontend/src/components/Sidebar.tsx | 65 +++++++++++---- .../TriggerViewer.object-edit.test.tsx | 22 +++++ frontend/src/components/TriggerViewer.tsx | 16 +++- .../utils/sqlServerObjectDefinition.test.ts | 47 +++++++++++ .../src/utils/sqlServerObjectDefinition.ts | 81 +++++++++++++++++++ 7 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 frontend/src/utils/sqlServerObjectDefinition.test.ts create mode 100644 frontend/src/utils/sqlServerObjectDefinition.ts diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index 9735390..ac1b7b9 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -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(); + 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(); + 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({ diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 85d97e4..b184621 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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] || ''; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 170dbe2..8bef562 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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, definitionKeys); + if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') { + return String(directDefinition); + } + return rows + .map((row) => getCaseInsensitiveRawValue(row as Record, ['Text', 'text'])) + .filter((value) => value !== undefined && value !== null) + .map((value) => String(value)) + .join(''); + }; + const getMySQLShowTablesName = (row: Record): 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; - 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; 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; - 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; + } } } } diff --git a/frontend/src/components/TriggerViewer.object-edit.test.tsx b/frontend/src/components/TriggerViewer.object-edit.test.tsx index a2312a5..cbecf87 100644 --- a/frontend/src/components/TriggerViewer.object-edit.test.tsx +++ b/frontend/src/components/TriggerViewer.object-edit.test.tsx @@ -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(); + 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({ diff --git a/frontend/src/components/TriggerViewer.tsx b/frontend/src/components/TriggerViewer.tsx index e28dcf5..8264bde 100644 --- a/frontend/src/components/TriggerViewer.tsx +++ b/frontend/src/components/TriggerViewer.tsx @@ -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 = ({ 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': { diff --git a/frontend/src/utils/sqlServerObjectDefinition.test.ts b/frontend/src/utils/sqlServerObjectDefinition.test.ts new file mode 100644 index 0000000..0c0c861 --- /dev/null +++ b/frontend/src/utils/sqlServerObjectDefinition.test.ts @@ -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]'"); + }); +}); diff --git a/frontend/src/utils/sqlServerObjectDefinition.ts b/frontend/src/utils/sqlServerObjectDefinition.ts new file mode 100644 index 0000000..6fb8103 --- /dev/null +++ b/frontend/src/utils/sqlServerObjectDefinition.ts @@ -0,0 +1,81 @@ +import { splitQualifiedNameSegments } from './qualifiedName'; + +export type SqlServerObjectDefinitionKind = 'view' | 'routine' | 'trigger'; + +const SQL_SERVER_OBJECT_TYPES: Record = { + 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]; +};