diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index 287c7ff7..a896453c 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -295,6 +295,130 @@ describe('DefinitionViewer object edit entry', () => { expect(query).toContain('SELECT COUNT(*) INTO v_count FROM dual;'); }); + it('opens an editable query tab for Oracle sequence definitions', async () => { + storeState.connections[0].config.type = 'oracle'; + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [{ + SEQUENCE_OWNER: 'H2', + SEQUENCE_NAME: 'HWMS_PACK_SEQNO', + MIN_VALUE: 1, + MAX_VALUE: 999999999999, + INCREMENT_BY: 1, + CYCLE_FLAG: 'N', + ORDER_FLAG: 'N', + CACHE_SIZE: 20, + }], + }); + + let renderer: any; + await act(async () => { + renderer = create(renderWithI18n(createTab({ + id: 'sequence-def-conn-1-H2-H2.HWMS_PACK_SEQNO', + title: 'Sequence: H2.HWMS_PACK_SEQNO', + type: 'sequence-def', + sequenceName: 'H2.HWMS_PACK_SEQNO', + viewName: undefined, + viewKind: undefined, + }))); + await flushPromises(); + }); + + expect(String(backendApp.DBQuery.mock.calls[0][2] || '')).toContain('FROM ALL_SEQUENCES'); + const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); + expect(editorText).toContain('CREATE SEQUENCE H2.HWMS_PACK_SEQNO'); + expect(editorText).toContain('CACHE 20'); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; + await act(async () => { + await button.props.onClick(); + await flushPromises(); + }); + + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'query', + queryMode: 'object-edit', + query: expect.stringContaining('CREATE SEQUENCE H2.HWMS_PACK_SEQNO'), + })); + }); + + it('loads Oracle package specification and body definitions without dropping lines', async () => { + storeState.connections[0].config.type = 'oracle'; + backendApp.DBQuery + .mockResolvedValueOnce({ + success: true, + data: [ + { TEXT: 'PACKAGE pkg_order AS\n' }, + { TEXT: ' PROCEDURE sync_order(p_id IN NUMBER);\n' }, + { TEXT: 'END pkg_order;\n' }, + ], + }) + .mockResolvedValueOnce({ + success: true, + data: [ + { TEXT: 'PACKAGE BODY pkg_order AS\n' }, + { TEXT: ' PROCEDURE sync_order(p_id IN NUMBER) IS\n' }, + { TEXT: ' BEGIN\n' }, + { TEXT: ' NULL;\n' }, + { TEXT: ' END;\n' }, + { TEXT: 'END pkg_order;\n' }, + ], + }); + + let renderer: any; + await act(async () => { + renderer = create(renderWithI18n(createTab({ + id: 'package-def-conn-1-H2-H2.PKG_ORDER', + title: 'Package: H2.PKG_ORDER', + type: 'package-def', + packageName: 'H2.PKG_ORDER', + viewName: undefined, + viewKind: undefined, + }))); + await flushPromises(); + }); + + expect(String(backendApp.DBQuery.mock.calls[0][2] || '')).toContain("TYPE = 'PACKAGE'"); + expect(String(backendApp.DBQuery.mock.calls[1][2] || '')).toContain("TYPE = 'PACKAGE BODY'"); + const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); + expect(editorText).toContain('PACKAGE pkg_order AS'); + expect(editorText).toContain('PROCEDURE sync_order(p_id IN NUMBER);'); + expect(editorText).toContain('PACKAGE BODY pkg_order AS'); + expect(editorText).toContain('NULL;'); + + backendApp.DBQuery + .mockResolvedValueOnce({ + success: true, + data: [ + { TEXT: 'PACKAGE pkg_order AS\n' }, + { TEXT: ' PROCEDURE sync_order(p_id IN NUMBER);\n' }, + { TEXT: 'END pkg_order;\n' }, + ], + }) + .mockResolvedValueOnce({ + success: true, + data: [ + { TEXT: 'PACKAGE BODY pkg_order AS\n' }, + { TEXT: ' PROCEDURE sync_order(p_id IN NUMBER) IS\n' }, + { TEXT: ' BEGIN\n' }, + { TEXT: ' NULL;\n' }, + { TEXT: ' END sync_order;\n' }, + { TEXT: 'END pkg_order;\n' }, + ], + }); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; + await act(async () => { + await button.props.onClick(); + await flushPromises(); + }); + + const editQuery = String(storeState.addTab.mock.calls[0][0].query || ''); + expect(editQuery).toContain('CREATE OR REPLACE PACKAGE pkg_order AS'); + expect(editQuery).toContain('/\nCREATE OR REPLACE PACKAGE BODY pkg_order AS'); + expect(editQuery).toContain('END sync_order;'); + }); + it('reloads the latest object definition before opening object edit', async () => { backendApp.DBQuery .mockResolvedValueOnce({ diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 27dfb8de..3724307e 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -48,6 +48,17 @@ const isCommentOnlyDefinition = (definition: string): boolean => { return lines.length > 0 && lines.every((line) => line.startsWith('--')); }; +const withCreateOrReplacePackageHeaders = (definition: string): string => { + const normalized = String(definition || '').replace(/\r\n/g, '\n').trim(); + if (!normalized) return ''; + return normalized + .split(/(?=^\s*PACKAGE(?:\s+BODY)?\b)/gim) + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => (/^\s*CREATE\b/i.test(part) ? part : `CREATE OR REPLACE ${part}`)) + .join('\n/\n'); +}; + const buildEditableDefinitionSql = ( tab: TabData, definition: string, @@ -75,6 +86,18 @@ const buildEditableDefinitionSql = ( return `${header}CREATE OR REPLACE VIEW ${objectName} AS\n${ensureSqlStatementTerminator(normalizedDefinition)}`; } + if (tab.type === 'sequence-def' && !/^\s*create\b/i.test(normalizedDefinition)) { + return `${header}${ensureSqlStatementTerminator(`CREATE SEQUENCE ${objectName}\n${normalizedDefinition}`)}`; + } + + if ( + tab.type === 'package-def' + && !/^\s*create\b/i.test(normalizedDefinition) + && /^\s*package\b/i.test(normalizedDefinition) + ) { + return `${header}${withCreateOrReplacePackageHeaders(normalizedDefinition)}`; + } + if ( tab.type === 'routine-def' && !/^\s*create\b/i.test(normalizedDefinition) @@ -109,6 +132,8 @@ const DefinitionViewer: React.FC = ({ tab }) => { tab.eventName, tab.routineName, tab.routineType, + tab.sequenceName, + tab.packageName, ].map((item) => String(item || '')).join('||'); const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''"); @@ -320,6 +345,58 @@ const DefinitionViewer: React.FC = ({ tab }) => { } }; + const buildShowSequenceQueries = (dialect: string, sequenceName: string, dbName: string): string[] => { + const { schema, name } = parseSchemaAndName(sequenceName); + const safeName = escapeSQLLiteral(name); + const safeDbName = escapeSQLLiteral(dbName); + const owner = schema ? escapeSQLLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : ''); + + switch (dialect) { + case 'oracle': + case 'dm': + if (owner) { + return [`SELECT SEQUENCE_OWNER, SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, ORDER_FLAG, CACHE_SIZE, LAST_NUMBER FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${owner}' AND SEQUENCE_NAME = '${safeName.toUpperCase()}'`]; + } + return [`SELECT SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, ORDER_FLAG, CACHE_SIZE, LAST_NUMBER FROM USER_SEQUENCES WHERE SEQUENCE_NAME = '${safeName.toUpperCase()}'`]; + case 'postgres': + case 'kingbase': + case 'highgo': + case 'vastbase': + case 'opengauss': + case 'gaussdb': { + const schemaRef = schema || 'public'; + return [`SELECT sequence_schema, sequence_name, data_type, start_value, minimum_value, maximum_value, increment FROM information_schema.sequences WHERE sequence_schema = '${escapeSQLLiteral(schemaRef)}' AND sequence_name = '${safeName}' LIMIT 1`]; + } + default: + return [`-- ${t('definition_viewer.editor.unsupported_sequence_definition')}`]; + } + }; + + const buildShowPackageQueries = (dialect: string, packageName: string, dbName: string): string[] => { + const { schema, name } = parseSchemaAndName(packageName); + const safeName = escapeSQLLiteral(name); + const safeDbName = escapeSQLLiteral(dbName); + + switch (dialect) { + case 'oracle': + case 'dm': { + const owner = schema ? escapeSQLLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : ''); + if (owner) { + return [ + `SELECT TEXT FROM ALL_SOURCE WHERE OWNER = '${owner}' AND NAME = '${safeName.toUpperCase()}' AND TYPE = 'PACKAGE' ORDER BY LINE`, + `SELECT TEXT FROM ALL_SOURCE WHERE OWNER = '${owner}' AND NAME = '${safeName.toUpperCase()}' AND TYPE = 'PACKAGE BODY' ORDER BY LINE`, + ]; + } + return [ + `SELECT TEXT FROM USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = 'PACKAGE' ORDER BY LINE`, + `SELECT TEXT FROM USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = 'PACKAGE BODY' ORDER BY LINE`, + ]; + } + default: + return [`-- ${t('definition_viewer.editor.unsupported_package_definition')}`]; + } + }; + const runQueryCandidates = async ( config: Record, dbName: string, @@ -350,6 +427,35 @@ const DefinitionViewer: React.FC = ({ tab }) => { return { success: false, data: [], message: lastMessage }; }; + const runQueryCandidatesCollectAll = async ( + config: Record, + dbName: string, + queries: string[] + ): Promise<{ success: boolean; data: any[]; message?: string }> => { + let lastMessage = ''; + let hasSuccessfulQuery = false; + const data: any[] = []; + for (const query of queries) { + const sql = String(query || '').trim(); + if (!sql) continue; + try { + const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, sql); + if (!result.success || !Array.isArray(result.data)) { + lastMessage = result.message || lastMessage; + continue; + } + hasSuccessfulQuery = true; + data.push(...result.data); + } catch (error: any) { + lastMessage = error?.message || String(error); + } + } + if (hasSuccessfulQuery) { + return { success: true, data }; + } + return { success: false, data: [], message: lastMessage }; + }; + const getVersionHint = async (config: Record, dbName: string): Promise => { const candidates = [ `SELECT VERSION() AS version`, @@ -511,6 +617,66 @@ const DefinitionViewer: React.FC = ({ tab }) => { } }; + const buildSequenceDefinitionFromRow = (row: Record): string => { + const sequenceName = String(getCaseInsensitiveRawValue(row, ['sequence_name']) || '').trim(); + const owner = String(getCaseInsensitiveRawValue(row, ['sequence_owner', 'owner', 'sequence_schema']) || '').trim(); + const name = owner && sequenceName ? `${owner}.${sequenceName}` : sequenceName; + if (!name) return JSON.stringify(row, null, 2); + + const clauses: string[] = []; + const increment = getCaseInsensitiveRawValue(row, ['increment_by', 'increment']); + const minValue = getCaseInsensitiveRawValue(row, ['min_value', 'minimum_value']); + const maxValue = getCaseInsensitiveRawValue(row, ['max_value', 'maximum_value']); + const cacheSize = Number(getCaseInsensitiveRawValue(row, ['cache_size'])); + const cycleFlag = String(getCaseInsensitiveRawValue(row, ['cycle_flag']) || '').trim().toUpperCase(); + const orderFlag = String(getCaseInsensitiveRawValue(row, ['order_flag']) || '').trim().toUpperCase(); + + if (increment !== undefined && increment !== null && String(increment).trim() !== '') { + clauses.push(`INCREMENT BY ${increment}`); + } + if (minValue !== undefined && minValue !== null && String(minValue).trim() !== '') { + clauses.push(`MINVALUE ${minValue}`); + } + if (maxValue !== undefined && maxValue !== null && String(maxValue).trim() !== '') { + clauses.push(`MAXVALUE ${maxValue}`); + } + if (Number.isFinite(cacheSize)) { + clauses.push(cacheSize > 0 ? `CACHE ${cacheSize}` : 'NOCACHE'); + } + if (cycleFlag) clauses.push(cycleFlag === 'Y' ? 'CYCLE' : 'NOCYCLE'); + if (orderFlag) clauses.push(orderFlag === 'Y' ? 'ORDER' : 'NOORDER'); + + return [`CREATE SEQUENCE ${name}`, ...clauses.map((clause) => ` ${clause}`)].join('\n') + ';'; + }; + + const extractSequenceDefinition = (dialect: string, data: any[]): string => { + if (!data || data.length === 0) return `-- ${t('definition_viewer.editor.sequence_definition_not_found')}`; + switch (dialect) { + case 'oracle': + case 'dm': + case 'postgres': + case 'kingbase': + case 'highgo': + case 'vastbase': + case 'opengauss': + case 'gaussdb': + return buildSequenceDefinitionFromRow(data[0] as Record); + default: + return JSON.stringify(data[0], null, 2); + } + }; + + const extractPackageDefinition = (dialect: string, data: any[]): string => { + if (!data || data.length === 0) return `-- ${t('definition_viewer.editor.package_definition_not_found')}`; + switch (dialect) { + case 'oracle': + case 'dm': + return data.map(row => row.text || row.TEXT || Object.values(row)[0] || '').join(''); + default: + return JSON.stringify(data[0], null, 2); + } + }; + const loadDefinition = async (): Promise<{ success: boolean; definition?: string; error?: string }> => { const conn = connections.find(c => c.id === tab.connectionId); if (!conn) { @@ -543,6 +709,22 @@ const DefinitionViewer: React.FC = ({ tab }) => { queries = buildShowEventQueries(dialect, eventName, dbName); extractFn = extractEventDefinition; resolvedObjectLabel = t('definition_viewer.object.event'); + } else if (tab.type === 'sequence-def') { + const sequenceName = tab.sequenceName || ''; + if (!sequenceName) { + return { success: false, error: t('definition_viewer.error.sequence_name_empty') }; + } + queries = buildShowSequenceQueries(dialect, sequenceName, dbName); + extractFn = extractSequenceDefinition; + resolvedObjectLabel = t('definition_viewer.object.sequence'); + } else if (tab.type === 'package-def') { + const packageName = tab.packageName || ''; + if (!packageName) { + return { success: false, error: t('definition_viewer.error.package_name_empty') }; + } + queries = buildShowPackageQueries(dialect, packageName, dbName); + extractFn = extractPackageDefinition; + resolvedObjectLabel = t('definition_viewer.object.package'); } else { const routineName = tab.routineName || ''; const routineType = tab.routineType || 'FUNCTION'; @@ -573,7 +755,9 @@ const DefinitionViewer: React.FC = ({ tab }) => { ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' } }; - const result = await runQueryCandidates(config, dbName, queries); + const result = tab.type === 'package-def' + ? await runQueryCandidatesCollectAll(config, dbName, queries) + : await runQueryCandidates(config, dbName, queries); if (result.success && Array.isArray(result.data) && result.data.length > 0) { return { success: true, definition: extractFn(dialect, result.data) }; @@ -655,7 +839,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { return () => { cancelled = true; }; - }, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.eventName, tab.routineName, tab.routineType, tab.type, connections, objectIdentityKey, t]); + }, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.eventName, tab.routineName, tab.routineType, tab.sequenceName, tab.packageName, tab.type, connections, objectIdentityKey, t]); useEffect(() => () => { isMountedRef.current = false; @@ -663,13 +847,29 @@ const DefinitionViewer: React.FC = ({ tab }) => { const objectLabel = tab.type === 'view-def' ? (tab.viewKind === 'materialized' ? t('definition_viewer.object.materialized_view') : t('definition_viewer.object.view')) - : (tab.type === 'event-def' ? t('definition_viewer.object.event') : t('definition_viewer.object.routine')); + : (tab.type === 'event-def' + ? t('definition_viewer.object.event') + : (tab.type === 'sequence-def' + ? t('definition_viewer.object.sequence') + : (tab.type === 'package-def' + ? t('definition_viewer.object.package') + : t('definition_viewer.object.routine')))); const objectName = tab.type === 'view-def' ? tab.viewName - : (tab.type === 'event-def' ? tab.eventName : tab.routineName); + : (tab.type === 'event-def' + ? tab.eventName + : (tab.type === 'sequence-def' + ? tab.sequenceName + : (tab.type === 'package-def' ? tab.packageName : tab.routineName))); const loadingTip = tab.type === 'view-def' ? t('definition_viewer.loading.view_definition') - : (tab.type === 'event-def' ? t('definition_viewer.loading.event_definition') : t('definition_viewer.loading.routine_definition')); + : (tab.type === 'event-def' + ? t('definition_viewer.loading.event_definition') + : (tab.type === 'sequence-def' + ? t('definition_viewer.loading.sequence_definition') + : (tab.type === 'package-def' + ? t('definition_viewer.loading.package_definition') + : t('definition_viewer.loading.routine_definition')))); const normalizedObjectName = String(objectName || '').trim(); const displayedDefinition = loadedDefinitionKeyRef.current === objectIdentityKey ? definition : ''; const hasDefinition = String(displayedDefinition || '').trim() !== ''; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 002b7b9f..412111ff 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1636,7 +1636,11 @@ const Sidebar: React.FC<{ routineType }); return; - } else if (node.type === 'sequence' || node.type === 'package') { + } else if (node.type === 'sequence') { + openSequenceDefinition(node); + return; + } else if (node.type === 'package') { + openPackageDefinition(node); return; } else if (node.type === 'jvm-mode') { const { providerMode, id } = node.dataRef; @@ -1844,6 +1848,8 @@ const Sidebar: React.FC<{ handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, + openSequenceDefinition, + openPackageDefinition, openEditRoutine, openCreateRoutine, handleDropRoutine, @@ -2188,6 +2194,8 @@ const Sidebar: React.FC<{ openEditRoutine, handleDropRoutine, openEventDefinition, + openSequenceDefinition, + openPackageDefinition, resolveMessagePublishTarget, openMessagePublishModal, openDesign, diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx index 4563ef9a..2c2b566d 100644 --- a/frontend/src/components/TabManager.tsx +++ b/frontend/src/components/TabManager.tsx @@ -61,6 +61,8 @@ const getTabKindLabel = (tab: TabData): string => { } if (tab.type === 'event-def') return t('tab_manager.kind_badge.event'); if (tab.type === 'routine-def') return t('tab_manager.kind_badge.routine'); + if (tab.type === 'sequence-def') return t('tab_manager.kind_badge.sequence'); + if (tab.type === 'package-def') return t('tab_manager.kind_badge.package'); return t('tab_manager.kind_badge.fallback'); }; @@ -89,6 +91,8 @@ const getTabKindTooltipLabel = (tab: TabData): string => { } if (tab.type === 'event-def') return t('tab_manager.hover.kind.event'); if (tab.type === 'routine-def') return t('tab_manager.hover.kind.routine'); + if (tab.type === 'sequence-def') return t('tab_manager.hover.kind.sequence'); + if (tab.type === 'package-def') return t('tab_manager.hover.kind.package'); return t('tab_manager.hover.kind.fallback'); }; @@ -97,6 +101,8 @@ const getTabObjectLabel = (tab: TabData): string => { if (tab.viewName) return tab.viewName; if (tab.eventName) return tab.eventName; if (tab.routineName) return tab.routineName; + if (tab.sequenceName) return tab.sequenceName; + if (tab.packageName) return tab.packageName; if (tab.triggerName) return tab.triggerName; if (tab.resourcePath) return tab.resourcePath; if (tab.filePath) return tab.filePath; @@ -409,7 +415,7 @@ const TabContent: React.FC<{ tab: TabData; isActive: boolean }> = React.memo(({ if (tab.type === 'trigger') { return ; } - if (tab.type === 'view-def' || tab.type === 'event-def' || tab.type === 'routine-def') { + if (tab.type === 'view-def' || tab.type === 'event-def' || tab.type === 'routine-def' || tab.type === 'sequence-def' || tab.type === 'package-def') { return ; } if (tab.type === 'table-overview') { diff --git a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx index a2bbe3d6..6129962d 100644 --- a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx +++ b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx @@ -165,6 +165,8 @@ export const buildSidebarLegacyNodeMenuItems = ( openEditRoutine, handleDropRoutine, openEventDefinition, + openSequenceDefinition, + openPackageDefinition, resolveMessagePublishTarget, openMessagePublishModal, openDesign, @@ -834,6 +836,36 @@ export const buildSidebarLegacyNodeMenuItems = ( ] }, ]; + } else if (node.type === 'sequence') { + return [ + { + key: 'view-sequence-def', + label: t('sidebar.menu.view_object_definition'), + icon: , + onClick: () => openSequenceDefinition(node) + }, + { + key: 'copy-sequence-name', + label: t('sidebar.menu.copy_object_name'), + icon: , + onClick: () => handleCopyTableName(node) + }, + ]; + } else if (node.type === 'package') { + return [ + { + key: 'view-package-def', + label: t('sidebar.menu.view_object_definition'), + icon: , + onClick: () => openPackageDefinition(node) + }, + { + key: 'copy-package-name', + label: t('sidebar.menu.copy_object_name'), + icon: , + onClick: () => handleCopyTableName(node) + }, + ]; } else if (node.type === 'db-event') { return [ { diff --git a/frontend/src/components/sidebar/useSidebarObjectActions.tsx b/frontend/src/components/sidebar/useSidebarObjectActions.tsx index a8b39288..42c16b53 100644 --- a/frontend/src/components/sidebar/useSidebarObjectActions.tsx +++ b/frontend/src/components/sidebar/useSidebarObjectActions.tsx @@ -986,6 +986,30 @@ export const useSidebarObjectActions = ({ }); }; + const openSequenceDefinition = (node: any) => { + const { sequenceName, dbName, id } = node.dataRef; + addTab({ + id: `sequence-def-${id}-${dbName}-${sequenceName}`, + title: t('sidebar.tab.sequence_definition', { name: sequenceName }), + type: 'sequence-def', + connectionId: id, + dbName, + sequenceName, + }); + }; + + const openPackageDefinition = (node: any) => { + const { packageName, dbName, id } = node.dataRef; + addTab({ + id: `package-def-${id}-${dbName}-${packageName}`, + title: t('sidebar.tab.package_definition', { name: packageName }), + type: 'package-def', + connectionId: id, + dbName, + packageName, + }); + }; + const openEditRoutine = async (node: any) => { const conn = node.dataRef; const { routineName, routineType, dbName, id } = conn; @@ -1222,6 +1246,8 @@ export const useSidebarObjectActions = ({ handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, + openSequenceDefinition, + openPackageDefinition, openEditRoutine, openCreateRoutine, handleDropRoutine, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a88574b4..660d5aa3 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -453,6 +453,8 @@ export interface TabData { | "view-def" | "event-def" | "routine-def" + | "sequence-def" + | "package-def" | "table-overview" | "table-export" | "jvm-overview" @@ -480,6 +482,8 @@ export interface TabData { eventName?: string; // Event name for MySQL event definition tabs routineName?: string; // Routine name for function/procedure definition tabs routineType?: string; // 'FUNCTION' or 'PROCEDURE' + sequenceName?: string; // Sequence name for sequence definition tabs + packageName?: string; // Package name for package definition tabs schemaName?: string; // Schema / owner name for schema-grouped objects sidebarLocateKey?: string; // Precise sidebar tree key for locating an object node savedQueryId?: string; // Saved query identity for quick-save behavior diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index c7661dd1..c24fc492 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -1,6 +1,6 @@ import { splitQualifiedNameLast } from './qualifiedName'; -export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews' | 'triggers' | 'routines' | 'externalSqlFiles'; +export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews' | 'triggers' | 'routines' | 'sequences' | 'packages' | 'externalSqlFiles'; export type SidebarLocateDatabaseObjectGroup = Exclude; export interface SidebarLocateDatabaseObjectRequest { @@ -57,6 +57,8 @@ export interface SidebarLocateTabLike { triggerName?: string; triggerTableName?: string; routineName?: string; + sequenceName?: string; + packageName?: string; schemaName?: string; sidebarLocateKey?: string; filePath?: string; @@ -84,12 +86,16 @@ const inferObjectGroup = (detail: Record, connectionId: string, if (explicitGroup === 'materializedViews' || explicitGroup === 'materialized-view') return 'materializedViews'; if (explicitGroup === 'triggers' || explicitGroup === 'trigger') return 'triggers'; if (explicitGroup === 'routines' || explicitGroup === 'routine') return 'routines'; + if (explicitGroup === 'sequences' || explicitGroup === 'sequence') return 'sequences'; + if (explicitGroup === 'packages' || explicitGroup === 'package') return 'packages'; const explicitType = toTrimmedString(detail.objectType); if (explicitType === 'view' || explicitType === 'views') return 'views'; if (explicitType === 'materialized' || explicitType === 'materialized-view') return 'materializedViews'; if (explicitType === 'trigger' || explicitType === 'triggers') return 'triggers'; if (explicitType === 'routine' || explicitType === 'routines') return 'routines'; + if (explicitType === 'sequence' || explicitType === 'sequences') return 'sequences'; + if (explicitType === 'package' || explicitType === 'packages') return 'packages'; const tabId = toTrimmedString(detail.tabId); const dbNodeKey = `${connectionId}-${dbName}`; @@ -97,6 +103,8 @@ const inferObjectGroup = (detail: Record, connectionId: string, if (tabId.startsWith(`${dbNodeKey}-view-`)) return 'views'; if (tabId.startsWith(`${dbNodeKey}-trigger-`)) return 'triggers'; if (tabId.startsWith(`${dbNodeKey}-routine-`) || tabId.startsWith(`routine-def-${connectionId}-${dbName}-`)) return 'routines'; + if (tabId.startsWith(`${dbNodeKey}-sequence-`) || tabId.startsWith(`sequence-def-${connectionId}-${dbName}-`)) return 'sequences'; + if (tabId.startsWith(`${dbNodeKey}-package-`) || tabId.startsWith(`package-def-${connectionId}-${dbName}-`)) return 'packages'; return 'tables'; }; @@ -117,7 +125,7 @@ export const normalizeSidebarLocateObjectRequest = (detail: unknown): SidebarLoc const connectionId = toTrimmedString(raw.connectionId); const dbName = toTrimmedString(raw.dbName); - const tableName = toTrimmedString(raw.tableName || raw.objectName || raw.viewName || raw.triggerName || raw.routineName); + const tableName = toTrimmedString(raw.tableName || raw.objectName || raw.viewName || raw.triggerName || raw.routineName || raw.sequenceName || raw.packageName); if (!connectionId || !dbName || !tableName) { return null; @@ -155,8 +163,12 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab ? toTrimmedString(tab.triggerName || tab.tableName) : tab.type === 'routine-def' ? toTrimmedString(tab.routineName || tab.tableName) - : toTrimmedString(tab.tableName || tab.viewName); - if (tab.type !== 'table' && tab.type !== 'view-def' && tab.type !== 'trigger' && tab.type !== 'routine-def') { + : tab.type === 'sequence-def' + ? toTrimmedString(tab.sequenceName || tab.tableName) + : tab.type === 'package-def' + ? toTrimmedString(tab.packageName || tab.tableName) + : toTrimmedString(tab.tableName || tab.viewName); + if (tab.type !== 'table' && tab.type !== 'view-def' && tab.type !== 'trigger' && tab.type !== 'routine-def' && tab.type !== 'sequence-def' && tab.type !== 'package-def') { return null; } @@ -172,7 +184,11 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab ? 'triggers' : (tab.type === 'routine-def' ? 'routines' - : (tab.objectType === 'materialized-view' ? 'materializedViews' : (tab.objectType === 'view' ? 'views' : undefined)))), + : (tab.type === 'sequence-def' + ? 'sequences' + : (tab.type === 'package-def' + ? 'packages' + : (tab.objectType === 'materialized-view' ? 'materializedViews' : (tab.objectType === 'view' ? 'views' : undefined)))))), }); }; diff --git a/frontend/src/utils/sqlStatementSelection.test.ts b/frontend/src/utils/sqlStatementSelection.test.ts index 4f5579a2..c0764aea 100644 --- a/frontend/src/utils/sqlStatementSelection.test.ts +++ b/frontend/src/utils/sqlStatementSelection.test.ts @@ -175,6 +175,103 @@ describe('sqlStatementSelection', () => { }); }); + it('skips SQL*Plus slash delimiter comments after named Oracle procedure endings', () => { + const sql = [ + '-- 修改函数/存储过程:H2.cproc_tzhssr_order2sale_A1', + '-- 请确认语法兼容当前数据库后执行', + 'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(', + ' p_sourceid IN VARCHAR2,', + ' p_msg_out OUT NVARCHAR2', + ') AS', + ' v_saleno VARCHAR2(40);', + ' v_ecnt NUMBER;', + 'BEGIN', + ' SELECT COUNT(*) INTO v_ecnt FROM dual;', + " p_msg_out := 'OK';", + 'EXCEPTION', + ' WHEN OTHERS THEN', + ' p_msg_out := SQLERRM;', + 'END cproc_tzhssr_order2sale_A1;', + '/ -- SQLPlus delimiter from PL/SQL tools', + 'SELECT 1 FROM dual;', + ].join('\n'); + + const ranges = findSqlStatementRanges(sql).map((range) => range.text); + + expect(ranges).toEqual([ + [ + '-- 修改函数/存储过程:H2.cproc_tzhssr_order2sale_A1', + '-- 请确认语法兼容当前数据库后执行', + 'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(', + ' p_sourceid IN VARCHAR2,', + ' p_msg_out OUT NVARCHAR2', + ') AS', + ' v_saleno VARCHAR2(40);', + ' v_ecnt NUMBER;', + 'BEGIN', + ' SELECT COUNT(*) INTO v_ecnt FROM dual;', + " p_msg_out := 'OK';", + 'EXCEPTION', + ' WHEN OTHERS THEN', + ' p_msg_out := SQLERRM;', + 'END cproc_tzhssr_order2sale_A1;', + ].join('\n'), + 'SELECT 1 FROM dual', + ]); + expect(resolveExecutableSql(sql, sql.indexOf('CREATE OR REPLACE'))).toEqual({ + sql: ranges[0], + source: 'statement', + }); + expect(resolveExecutableSql(sql, sql.indexOf('p_msg_out := SQLERRM'))).toEqual({ + sql: ranges[0], + source: 'statement', + }); + }); + + it('keeps Oracle PACKAGE specification and body definitions as complete executable statements', () => { + const sql = [ + 'CREATE OR REPLACE PACKAGE pkg_order AS', + ' PROCEDURE sync_order(p_id IN NUMBER);', + 'END pkg_order;', + '/', + 'CREATE OR REPLACE PACKAGE BODY pkg_order AS', + ' PROCEDURE sync_order(p_id IN NUMBER) IS', + ' BEGIN', + ' NULL;', + ' END sync_order;', + 'END pkg_order;', + '/ -- SQLPlus delimiter from PL/SQL tools', + 'SELECT 1 FROM dual;', + ].join('\n'); + + const ranges = findSqlStatementRanges(sql).map((range) => range.text); + + expect(ranges).toEqual([ + [ + 'CREATE OR REPLACE PACKAGE pkg_order AS', + ' PROCEDURE sync_order(p_id IN NUMBER);', + 'END pkg_order;', + ].join('\n'), + [ + 'CREATE OR REPLACE PACKAGE BODY pkg_order AS', + ' PROCEDURE sync_order(p_id IN NUMBER) IS', + ' BEGIN', + ' NULL;', + ' END sync_order;', + 'END pkg_order;', + ].join('\n'), + 'SELECT 1 FROM dual', + ]); + expect(resolveExecutableSql(sql, sql.indexOf('PROCEDURE sync_order'))).toEqual({ + sql: ranges[0], + source: 'statement', + }); + expect(resolveExecutableSql(sql, sql.indexOf('NULL'))).toEqual({ + sql: ranges[1], + source: 'statement', + }); + }); + it('does not treat a slash operator line as a SQL*Plus delimiter', () => { const sql = 'SELECT 10\n/\n2 FROM dual;'; diff --git a/frontend/src/utils/sqlStatementSelection.ts b/frontend/src/utils/sqlStatementSelection.ts index 60467b35..2788ce0e 100644 --- a/frontend/src/utils/sqlStatementSelection.ts +++ b/frontend/src/utils/sqlStatementSelection.ts @@ -75,6 +75,12 @@ const resolveStandaloneSqlSlashLineEnd = (text: string, index: number): number | let lineEnd = index + 1; while (lineEnd < text.length && text[lineEnd] !== '\n') { + if (text[lineEnd] === '-' && text[lineEnd + 1] === '-') { + while (lineEnd < text.length && text[lineEnd] !== '\n') { + lineEnd += 1; + } + return lineEnd; + } if (!isHorizontalWhitespace(text[lineEnd])) { return null; } @@ -116,7 +122,32 @@ const isCreateRoutineHeaderPrefix = (text: string): boolean => { current = nextSqlSignificantTokenSpan(text, current.end); } - return current.token === 'procedure' || current.token === 'function'; + if (current.token === 'procedure' || current.token === 'function') { + return true; + } + if (current.token !== 'package') { + return false; + } + current = nextSqlSignificantTokenSpan(text, current.end); + return current.token === '' || current.token === 'body' || isSqlIdentifierStart(current.token[0] || ''); +}; + +const isCreatePackageHeaderPrefix = (text: string): boolean => { + let current = nextSqlSignificantTokenSpan(text, 0); + if (current.token !== 'create') return false; + + current = nextSqlSignificantTokenSpan(text, current.end); + if (current.token === 'or') { + current = nextSqlSignificantTokenSpan(text, current.end); + if (current.token !== 'replace') return false; + current = nextSqlSignificantTokenSpan(text, current.end); + } + + while (['editionable', 'noneditionable'].includes(current.token)) { + current = nextSqlSignificantTokenSpan(text, current.end); + } + + return current.token === 'package'; }; const shouldEnterPlsqlCreateRoutineBlock = ( @@ -290,7 +321,9 @@ export const findSqlStatementRanges = (sql: string): SqlStatementRange[] => { justClosedPLSQLBlock = false; } else if (plsqlDepth === 0 && shouldEnterPlsqlCreateRoutineBlock(text, statementStart, token, tokenEnd)) { plsqlDepth++; - plsqlDeclareBeginSkips++; + if (!isCreatePackageHeaderPrefix(text.slice(statementStart, tokenEnd - token.length))) { + plsqlDeclareBeginSkips++; + } justClosedPLSQLBlock = false; } else if (token === 'end' && plsqlDepth > 0 && !isPlsqlControlEnd(text, tokenEnd)) { plsqlDepth--; diff --git a/frontend/src/utils/tabDisplay.ts b/frontend/src/utils/tabDisplay.ts index 5c14aae5..05f445f8 100644 --- a/frontend/src/utils/tabDisplay.ts +++ b/frontend/src/utils/tabDisplay.ts @@ -463,6 +463,12 @@ const buildCompactObjectTabTitle = (tab: TabData, translate: TabDisplayTranslate if (tab.type === 'routine-def') { return replaceTitleObjectLabel(tab.title, tab.routineName); } + if (tab.type === 'sequence-def') { + return replaceTitleObjectLabel(tab.title, tab.sequenceName); + } + if (tab.type === 'package-def') { + return replaceTitleObjectLabel(tab.title, tab.packageName); + } return tab.title; }; @@ -479,6 +485,8 @@ export const getTabDisplayKindLabel = (tab: TabData): string => { if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? 'MV' : 'VIEW'; if (tab.type === 'event-def') return 'EVT'; if (tab.type === 'routine-def') return 'FUNC'; + if (tab.type === 'sequence-def') return 'SEQ'; + if (tab.type === 'package-def') return 'PKG'; return 'TAB'; }; @@ -488,6 +496,8 @@ const getTabRawObjectLabel = (tab: TabData, translate: TabDisplayTranslate = def if (tab.viewName) return tab.viewName; if (tab.eventName) return tab.eventName; if (tab.routineName) return tab.routineName; + if (tab.sequenceName) return tab.sequenceName; + if (tab.packageName) return tab.packageName; if (tab.triggerName) return tab.triggerName; if (tab.resourcePath) return tab.resourcePath; if (tab.filePath) return getFileNameFromPath(tab.filePath); diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index ee256e44..3b52dddc 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "reflect" "strings" "testing" @@ -481,6 +482,59 @@ END;` } } +func TestDBQueryMultiKeepsOraclePackageSpecAndBodyTogether(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + fakeDB := &fakeBatchWriteDB{} + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + config := connection.ConnectionConfig{ + Type: "oracle", + Host: "127.0.0.1", + Port: 1521, + User: "app", + } + query := `CREATE OR REPLACE PACKAGE pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER); +END pkg_order; +/ +CREATE OR REPLACE PACKAGE BODY pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER) IS + BEGIN + NULL; + END sync_order; +END pkg_order; +/ -- SQLPlus delimiter from PL/SQL tools` + wantExecuted := []string{ + `CREATE OR REPLACE PACKAGE pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER); +END pkg_order;`, + `CREATE OR REPLACE PACKAGE BODY pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER) IS + BEGIN + NULL; + END sync_order; +END pkg_order;`, + } + + result := app.DBQueryMulti(config, "ORCLPDB1", query, "oracle-package-test") + if !result.Success { + t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) + } + if fakeDB.execCalls != 2 || len(fakeDB.execQueries) != 2 { + t.Fatalf("expected two sequential exec calls, got execCalls=%d queries=%#v", fakeDB.execCalls, fakeDB.execQueries) + } + if !reflect.DeepEqual(fakeDB.execQueries, wantExecuted) { + t.Fatalf("expected package spec/body to stay intact, got %#v", fakeDB.execQueries) + } +} + var _ db.BatchWriteExecer = (*fakeBatchWriteDB)(nil) var _ db.SessionExecerProvider = (*fakeBatchWriteDB)(nil) var _ db.QueryMessageExecer = (*fakeBatchWriteDB)(nil) diff --git a/internal/app/methods_file_sql_execution_test.go b/internal/app/methods_file_sql_execution_test.go index f68e2ef2..5a127895 100644 --- a/internal/app/methods_file_sql_execution_test.go +++ b/internal/app/methods_file_sql_execution_test.go @@ -475,3 +475,52 @@ func TestStreamSQLFileSkipsOracleSqlPlusSlashDelimiter(t *testing.T) { t.Fatalf("unexpected second statement: %q", statements[1]) } } + +func TestStreamSQLFileKeepsOraclePackageSpecAndBodyTogether(t *testing.T) { + input := strings.Join([]string{ + "CREATE OR REPLACE PACKAGE pkg_order AS", + " PROCEDURE sync_order(p_id IN NUMBER);", + "END pkg_order;", + "/", + "CREATE OR REPLACE PACKAGE BODY pkg_order AS", + " PROCEDURE sync_order(p_id IN NUMBER) IS", + " BEGIN", + " NULL;", + " END sync_order;", + "END pkg_order;", + "/ -- SQLPlus delimiter from PL/SQL tools", + "SELECT 1 FROM dual;", + }, "\n") + var statements []string + + count, err := streamSQLFile(&chunkedReader{data: []byte(input), step: 3}, func(index int, stmt string) error { + statements = append(statements, stmt) + return nil + }) + if err != nil { + t.Fatalf("streamSQLFile returned error: %v", err) + } + if count != 3 || len(statements) != 3 { + t.Fatalf("expected 3 statements, got count=%d statements=%#v", count, statements) + } + if statements[0] != strings.Join([]string{ + "CREATE OR REPLACE PACKAGE pkg_order AS", + " PROCEDURE sync_order(p_id IN NUMBER);", + "END pkg_order;", + }, "\n") { + t.Fatalf("unexpected package spec statement: %q", statements[0]) + } + if statements[1] != strings.Join([]string{ + "CREATE OR REPLACE PACKAGE BODY pkg_order AS", + " PROCEDURE sync_order(p_id IN NUMBER) IS", + " BEGIN", + " NULL;", + " END sync_order;", + "END pkg_order;", + }, "\n") { + t.Fatalf("unexpected package body statement: %q", statements[1]) + } + if statements[2] != "SELECT 1 FROM dual" { + t.Fatalf("unexpected third statement: %q", statements[2]) + } +} diff --git a/internal/app/sql_split.go b/internal/app/sql_split.go index 35c988a1..c08002dd 100644 --- a/internal/app/sql_split.go +++ b/internal/app/sql_split.go @@ -131,7 +131,9 @@ func splitSQLStatements(sql string) []string { justClosedPLSQLBlock = false } else if plsqlDepth == 0 && shouldEnterPLSQLCreateRoutineBlock(text, cur.String(), token, tokenEnd) { plsqlDepth++ - plsqlDeclareBeginSkips++ + if !isCreatePackageHeaderPrefix(cur.String()) { + plsqlDeclareBeginSkips++ + } justClosedPLSQLBlock = false } else if token == "end" && plsqlDepth > 0 && !isPLSQLControlEnd(text, tokenEnd) { plsqlDepth-- @@ -262,6 +264,18 @@ func scanSQLStandaloneSlashLineSuffix(text string, pos int) (lineEnd int, standa if text[i] == '\n' { return i, true, true } + if text[i] == '-' { + if i+1 >= len(text) { + return len(text), true, false + } + if text[i+1] == '-' { + lineEnd := scanSQLLineCommentEnd(text, i+2) + if lineEnd >= len(text) { + return lineEnd, true, false + } + return lineEnd, true, true + } + } if !isSQLHorizontalWhitespace(text[i]) { return 0, false, true } @@ -269,6 +283,15 @@ func scanSQLStandaloneSlashLineSuffix(text string, pos int) (lineEnd int, standa return len(text), true, false } +func scanSQLLineCommentEnd(text string, pos int) int { + for i := pos; i < len(text); i++ { + if text[i] == '\n' { + return i + } + } + return len(text) +} + func skipSQLWhitespaceAndComments(text string, pos int) int { i := pos for i < len(text) { @@ -384,7 +407,36 @@ func isCreateRoutineHeaderPrefix(text string) bool { currentToken, currentEnd = nextSQLSignificantTokenSpan(text, currentEnd) } - return currentToken == "procedure" || currentToken == "function" + if currentToken == "procedure" || currentToken == "function" { + return true + } + if currentToken != "package" { + return false + } + currentToken, _ = nextSQLSignificantTokenSpan(text, currentEnd) + return currentToken == "" || currentToken == "body" || isSQLIdentifierStart(currentToken[0]) +} + +func isCreatePackageHeaderPrefix(text string) bool { + currentToken, currentEnd := nextSQLSignificantTokenSpan(text, 0) + if currentToken != "create" { + return false + } + + currentToken, currentEnd = nextSQLSignificantTokenSpan(text, currentEnd) + if currentToken == "or" { + currentToken, currentEnd = nextSQLSignificantTokenSpan(text, currentEnd) + if currentToken != "replace" { + return false + } + currentToken, currentEnd = nextSQLSignificantTokenSpan(text, currentEnd) + } + + for currentToken == "editionable" || currentToken == "noneditionable" { + currentToken, currentEnd = nextSQLSignificantTokenSpan(text, currentEnd) + } + + return currentToken == "package" } func nextSQLSignificantTokenSpan(text string, pos int) (string, int) { diff --git a/internal/app/sql_split_stream.go b/internal/app/sql_split_stream.go index 0ec22919..d11f5ff9 100644 --- a/internal/app/sql_split_stream.go +++ b/internal/app/sql_split_stream.go @@ -148,7 +148,9 @@ func (s *sqlStreamSplitter) Feed(chunk []byte) []string { s.closedPLSQL = false } else if s.plsqlDepth == 0 && shouldEnterPLSQLCreateRoutineBlock(text, s.cur.String(), token, tokenEnd) { s.plsqlDepth++ - s.declareSkips++ + if !isCreatePackageHeaderPrefix(s.cur.String()) { + s.declareSkips++ + } s.closedPLSQL = false } else if token == "end" && s.plsqlDepth > 0 && !isPLSQLControlEnd(text, tokenEnd) { s.plsqlDepth-- @@ -336,7 +338,7 @@ func isIncompleteSQLDollarTag(s string) bool { func shouldDeferPLSQLKeywordInStream(text string, tokenStart int, tokenEnd int, token string) bool { switch token { - case "begin", "declare", "end", "create", "or", "replace", "editionable", "noneditionable", "procedure", "function", "is", "as": + case "begin", "declare", "end", "create", "or", "replace", "editionable", "noneditionable", "procedure", "function", "package", "body", "is", "as": default: return false } @@ -361,7 +363,7 @@ func shouldDeferPLSQLKeywordPrefixInStream(text string, tokenStart int, tokenEnd if tokenEnd < len(text) { return false } - for _, keyword := range []string{"begin", "declare", "end", "create", "or", "replace", "editionable", "noneditionable", "procedure", "function", "is", "as"} { + for _, keyword := range []string{"begin", "declare", "end", "create", "or", "replace", "editionable", "noneditionable", "procedure", "function", "package", "body", "is", "as"} { if strings.HasPrefix(keyword, token) && token != keyword { if tokenStart > 0 && isSQLIdentifierPart(text[tokenStart-1]) { return false diff --git a/internal/app/sql_split_test.go b/internal/app/sql_split_test.go index 3a0de413..d812cac8 100644 --- a/internal/app/sql_split_test.go +++ b/internal/app/sql_split_test.go @@ -251,6 +251,83 @@ END;`, } } +func TestSplitSQLStatements_OracleCreateProcedureSkipsCommentedSqlPlusSlashDelimiter(t *testing.T) { + input := `-- 修改函数/存储过程:H2.cproc_tzhssr_order2sale_A1 +-- 请确认语法兼容当前数据库后执行 +CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1( + p_sourceid IN VARCHAR2, + p_saleno_out OUT VARCHAR2, + p_msg_out OUT NVARCHAR2 +) AS + v_saleno VARCHAR2(40); + v_ecnt NUMBER; +BEGIN + SELECT COUNT(*) INTO v_ecnt FROM dual; + p_saleno_out := v_saleno; + p_msg_out := 'OK'; +EXCEPTION + WHEN OTHERS THEN + p_msg_out := SQLERRM; +END cproc_tzhssr_order2sale_A1; +/ -- SQLPlus delimiter from PL/SQL tools +SELECT 1 FROM dual;` + got := splitSQLStatements(input) + want := []string{ + `-- 修改函数/存储过程:H2.cproc_tzhssr_order2sale_A1 +-- 请确认语法兼容当前数据库后执行 +CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1( + p_sourceid IN VARCHAR2, + p_saleno_out OUT VARCHAR2, + p_msg_out OUT NVARCHAR2 +) AS + v_saleno VARCHAR2(40); + v_ecnt NUMBER; +BEGIN + SELECT COUNT(*) INTO v_ecnt FROM dual; + p_saleno_out := v_saleno; + p_msg_out := 'OK'; +EXCEPTION + WHEN OTHERS THEN + p_msg_out := SQLERRM; +END cproc_tzhssr_order2sale_A1;`, + "SELECT 1 FROM dual", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("splitSQLStatements(%q) = %#v, want %#v", input, got, want) + } +} + +func TestSplitSQLStatements_OraclePackageSpecAndBodyStayWhole(t *testing.T) { + input := `CREATE OR REPLACE PACKAGE pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER); +END pkg_order; +/ +CREATE OR REPLACE PACKAGE BODY pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER) IS + BEGIN + NULL; + END sync_order; +END pkg_order; +/ -- SQLPlus delimiter from PL/SQL tools +SELECT 1 FROM dual;` + got := splitSQLStatements(input) + want := []string{ + `CREATE OR REPLACE PACKAGE pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER); +END pkg_order;`, + `CREATE OR REPLACE PACKAGE BODY pkg_order AS + PROCEDURE sync_order(p_id IN NUMBER) IS + BEGIN + NULL; + END sync_order; +END pkg_order;`, + "SELECT 1 FROM dual", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("splitSQLStatements(%q) = %#v, want %#v", input, got, want) + } +} + func TestSplitSQLStatements_DoesNotTreatSlashOperatorLineAsDelimiter(t *testing.T) { input := "SELECT 10\n/\n2 FROM dual;" got := splitSQLStatements(input) diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 079f0c06..49071679 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "Name", "definition_viewer.editor.metadata_fallback.type_label": "Typ", "definition_viewer.editor.object_definition_not_found": "{{object}}definition nicht gefunden", + "definition_viewer.editor.package_definition_not_found": "Package definition not found", "definition_viewer.editor.routine_definition_not_found": "Funktions-/Prozedurdefinition nicht gefunden", + "definition_viewer.editor.sequence_definition_not_found": "Sequence definition not found", "definition_viewer.editor.sphinx.compat_queries_hint": "Mehrere Kompatibilitätsabfragen wurden ausgeführt. Ursache können Versionsbeschränkungen oder ein nicht unterstützter Objekttyp sein.", "definition_viewer.editor.sphinx.empty_result": "Die aktuelle Sphinx-Instanz{{version}} hat keine {{object}}definition zurückgegeben.", "definition_viewer.editor.sphinx.failed_message_label": "Zurückgegebene Fehlermeldung", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": " (Version: {{version}})", "definition_viewer.editor.unsupported_event_definition": "Dieser Datenbanktyp unterstützt das Anzeigen von Ereignisdefinitionen nicht", "definition_viewer.editor.unsupported_object_definition": "Diese Objektdefinition wird nicht unterstützt", + "definition_viewer.editor.unsupported_package_definition": "This database type does not support viewing package definitions", "definition_viewer.editor.unsupported_routine_definition": "Dieser Datenbanktyp unterstützt das Anzeigen von Funktions-/Prozedurdefinitionen nicht", + "definition_viewer.editor.unsupported_sequence_definition": "This database type does not support viewing sequence definitions", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite unterstützt keine Verwaltung von Funktions-/Prozedurdefinitionen", "definition_viewer.editor.unsupported_view_definition": "Dieser Datenbanktyp unterstützt das Anzeigen von Ansichtsdefinitionen nicht", "definition_viewer.editor.view_definition_not_found": "Ansichtsdefinition nicht gefunden", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "Laden fehlgeschlagen", "definition_viewer.error.query_failed": "Definitionsabfrage fehlgeschlagen", "definition_viewer.error.query_failed_detail": "Definitionsabfrage fehlgeschlagen: {{detail}}", + "definition_viewer.error.package_name_empty": "Package name is empty", "definition_viewer.error.routine_name_empty": "Funktions-/Prozedurname ist leer", + "definition_viewer.error.sequence_name_empty": "Sequence name is empty", "definition_viewer.error.view_name_empty": "Ansichtsname ist leer", "definition_viewer.field.database": "Datenbank", "definition_viewer.field.type": "Typ", "definition_viewer.loading.event_definition": "Ereignisdefinition wird geladen...", + "definition_viewer.loading.package_definition": "Loading package definition...", "definition_viewer.loading.routine_definition": "Funktions-/Prozedurdefinition wird geladen...", + "definition_viewer.loading.sequence_definition": "Loading sequence definition...", "definition_viewer.loading.view_definition": "Ansichtsdefinition wird geladen...", "definition_viewer.object.event": "Ereignis", "definition_viewer.object.materialized_view": "Materialisierte Ansicht", + "definition_viewer.object.package": "Package", "definition_viewer.object.routine": "Funktion/Prozedur", + "definition_viewer.object.sequence": "Sequence", "definition_viewer.object.view": "Ansicht", "definition_viewer.warning.refresh_latest_failed": "Neueste Definition konnte nicht aktualisiert werden", "dev.perf_data_grid.columns": "Spalten", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "Neue Abfrage", "sidebar.tab.new_query_database": "Neue Abfrage ({{database}})", "sidebar.tab.new_table": "Neue Tabelle - {{database}}", + "sidebar.tab.package_definition": "Package: {{name}}", "sidebar.tab.recent_query": "Letzte Abfrage", "sidebar.tab.redis_command": "Befehl - {{database}}", "sidebar.tab.redis_monitor": "Monitoring - {{database}}", "sidebar.tab.routine_definition": "{{type}}: {{name}}", + "sidebar.tab.sequence_definition": "Sequence: {{name}}", "sidebar.tab.table_overview": "Tabellenübersicht - {{database}}{{schema}}", "sidebar.tab.table_structure": "Tabellenstruktur ({{table}})", "sidebar.tab.trigger": "Trigger: {{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "JVM-Übersicht", "tab_manager.hover.kind.jvm_resource": "JVM-Ressource", "tab_manager.hover.kind.materialized_view": "Materialisierte Ansicht", + "tab_manager.hover.kind.package": "Package", "tab_manager.hover.kind.query": "SQL-Abfrage", "tab_manager.hover.kind.redis_command": "Redis-Befehl", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Redis-Monitor", "tab_manager.hover.kind.routine": "Funktion / Prozedur", + "tab_manager.hover.kind.sequence": "Sequence", "tab_manager.hover.kind.sql_analysis": "SQL-Analyse-Workbench", "tab_manager.hover.kind.table": "Tabellendaten", "tab_manager.hover.kind.table_export": "Export-Workbench", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "Tab", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "MV", + "tab_manager.kind_badge.package": "Pkg", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "Funktion", + "tab_manager.kind_badge.sequence": "Seq", "tab_manager.kind_badge.sql_analysis": "Analyse", "tab_manager.kind_badge.table": "Tabelle", "tab_manager.kind_badge.table_export": "Export", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index f487f941..a5348726 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "Name", "definition_viewer.editor.metadata_fallback.type_label": "Type", "definition_viewer.editor.object_definition_not_found": "{{object}} definition not found", + "definition_viewer.editor.package_definition_not_found": "Package definition not found", "definition_viewer.editor.routine_definition_not_found": "Function/procedure definition not found", + "definition_viewer.editor.sequence_definition_not_found": "Sequence definition not found", "definition_viewer.editor.sphinx.compat_queries_hint": "Multiple compatibility queries were executed. This may be due to version capability limits or an unsupported object type.", "definition_viewer.editor.sphinx.empty_result": "Current Sphinx instance{{version}} returned no {{object}} definition.", "definition_viewer.editor.sphinx.failed_message_label": "Returned failure message", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": " (version: {{version}})", "definition_viewer.editor.unsupported_event_definition": "This database type does not support viewing event definitions", "definition_viewer.editor.unsupported_object_definition": "This object definition is not supported", + "definition_viewer.editor.unsupported_package_definition": "This database type does not support viewing package definitions", "definition_viewer.editor.unsupported_routine_definition": "This database type does not support viewing function/procedure definitions", + "definition_viewer.editor.unsupported_sequence_definition": "This database type does not support viewing sequence definitions", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite does not support function/procedure definition management", "definition_viewer.editor.unsupported_view_definition": "This database type does not support viewing view definitions", "definition_viewer.editor.view_definition_not_found": "View definition not found", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "Load failed", "definition_viewer.error.query_failed": "Failed to query definition", "definition_viewer.error.query_failed_detail": "Failed to query definition: {{detail}}", + "definition_viewer.error.package_name_empty": "Package name is empty", "definition_viewer.error.routine_name_empty": "Function/procedure name is empty", + "definition_viewer.error.sequence_name_empty": "Sequence name is empty", "definition_viewer.error.view_name_empty": "View name is empty", "definition_viewer.field.database": "Database", "definition_viewer.field.type": "Type", "definition_viewer.loading.event_definition": "Loading event definition...", + "definition_viewer.loading.package_definition": "Loading package definition...", "definition_viewer.loading.routine_definition": "Loading function/procedure definition...", + "definition_viewer.loading.sequence_definition": "Loading sequence definition...", "definition_viewer.loading.view_definition": "Loading view definition...", "definition_viewer.object.event": "Event", "definition_viewer.object.materialized_view": "Materialized view", + "definition_viewer.object.package": "Package", "definition_viewer.object.routine": "Function/procedure", + "definition_viewer.object.sequence": "Sequence", "definition_viewer.object.view": "View", "definition_viewer.warning.refresh_latest_failed": "Failed to refresh the latest definition", "dev.perf_data_grid.columns": "Columns", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "New query", "sidebar.tab.new_query_database": "New query ({{database}})", "sidebar.tab.new_table": "New table - {{database}}", + "sidebar.tab.package_definition": "Package: {{name}}", "sidebar.tab.recent_query": "Recent query", "sidebar.tab.redis_command": "Command - {{database}}", "sidebar.tab.redis_monitor": "Monitor - {{database}}", "sidebar.tab.routine_definition": "{{type}}: {{name}}", + "sidebar.tab.sequence_definition": "Sequence: {{name}}", "sidebar.tab.table_overview": "Table overview - {{database}}{{schema}}", "sidebar.tab.table_structure": "Table structure ({{table}})", "sidebar.tab.trigger": "Trigger: {{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "JVM overview", "tab_manager.hover.kind.jvm_resource": "JVM resource", "tab_manager.hover.kind.materialized_view": "Materialized view", + "tab_manager.hover.kind.package": "Package", "tab_manager.hover.kind.query": "SQL query", "tab_manager.hover.kind.redis_command": "Redis command", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Redis monitor", "tab_manager.hover.kind.routine": "Function / procedure", + "tab_manager.hover.kind.sequence": "Sequence", "tab_manager.hover.kind.sql_analysis": "SQL analysis workbench", "tab_manager.hover.kind.table": "Table data", "tab_manager.hover.kind.table_export": "Export workbench", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "Tab", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "MV", + "tab_manager.kind_badge.package": "Pkg", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "Func", + "tab_manager.kind_badge.sequence": "Seq", "tab_manager.kind_badge.sql_analysis": "Analyze", "tab_manager.kind_badge.table": "Table", "tab_manager.kind_badge.table_export": "Export", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index a9ef62db..7efc07fb 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "名前", "definition_viewer.editor.metadata_fallback.type_label": "種類", "definition_viewer.editor.object_definition_not_found": "{{object}}定義が見つかりません", + "definition_viewer.editor.package_definition_not_found": "Package definition not found", "definition_viewer.editor.routine_definition_not_found": "関数/ストアドプロシージャ定義が見つかりません", + "definition_viewer.editor.sequence_definition_not_found": "Sequence definition not found", "definition_viewer.editor.sphinx.compat_queries_hint": "複数の互換クエリを実行しました。バージョン機能の制限、または未対応のオブジェクト種類が原因の可能性があります。", "definition_viewer.editor.sphinx.empty_result": "現在の Sphinx インスタンス{{version}}は{{object}}定義を返しませんでした。", "definition_viewer.editor.sphinx.failed_message_label": "返された失敗メッセージ", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": "(バージョン: {{version}})", "definition_viewer.editor.unsupported_event_definition": "このデータベース種類ではイベント定義の表示はサポートされていません", "definition_viewer.editor.unsupported_object_definition": "このオブジェクト定義の表示はサポートされていません", + "definition_viewer.editor.unsupported_package_definition": "This database type does not support viewing package definitions", "definition_viewer.editor.unsupported_routine_definition": "このデータベース種類では関数/ストアドプロシージャ定義の表示はサポートされていません", + "definition_viewer.editor.unsupported_sequence_definition": "This database type does not support viewing sequence definitions", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite は関数/ストアドプロシージャ定義の管理をサポートしていません", "definition_viewer.editor.unsupported_view_definition": "このデータベース種類ではビュー定義の表示はサポートされていません", "definition_viewer.editor.view_definition_not_found": "ビュー定義が見つかりません", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "読み込みに失敗しました", "definition_viewer.error.query_failed": "定義の取得に失敗しました", "definition_viewer.error.query_failed_detail": "定義の取得に失敗しました: {{detail}}", + "definition_viewer.error.package_name_empty": "Package name is empty", "definition_viewer.error.routine_name_empty": "関数/ストアドプロシージャ名が空です", + "definition_viewer.error.sequence_name_empty": "Sequence name is empty", "definition_viewer.error.view_name_empty": "ビュー名が空です", "definition_viewer.field.database": "データベース", "definition_viewer.field.type": "種類", "definition_viewer.loading.event_definition": "イベント定義を読み込み中...", + "definition_viewer.loading.package_definition": "Loading package definition...", "definition_viewer.loading.routine_definition": "関数/ストアドプロシージャ定義を読み込み中...", + "definition_viewer.loading.sequence_definition": "Loading sequence definition...", "definition_viewer.loading.view_definition": "ビュー定義を読み込み中...", "definition_viewer.object.event": "イベント", "definition_viewer.object.materialized_view": "マテリアライズドビュー", + "definition_viewer.object.package": "Package", "definition_viewer.object.routine": "関数/ストアドプロシージャ", + "definition_viewer.object.sequence": "Sequence", "definition_viewer.object.view": "ビュー", "definition_viewer.warning.refresh_latest_failed": "最新の定義を更新できませんでした", "dev.perf_data_grid.columns": "列数", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "新規クエリ", "sidebar.tab.new_query_database": "新規クエリ({{database}})", "sidebar.tab.new_table": "新規テーブル - {{database}}", + "sidebar.tab.package_definition": "Package: {{name}}", "sidebar.tab.recent_query": "最近のクエリ", "sidebar.tab.redis_command": "コマンド - {{database}}", "sidebar.tab.redis_monitor": "監視 - {{database}}", "sidebar.tab.routine_definition": "{{type}}: {{name}}", + "sidebar.tab.sequence_definition": "Sequence: {{name}}", "sidebar.tab.table_overview": "テーブル概要 - {{database}}{{schema}}", "sidebar.tab.table_structure": "テーブル構造({{table}})", "sidebar.tab.trigger": "トリガー: {{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "JVM 概要", "tab_manager.hover.kind.jvm_resource": "JVM リソース", "tab_manager.hover.kind.materialized_view": "マテリアライズドビュー", + "tab_manager.hover.kind.package": "Package", "tab_manager.hover.kind.query": "SQL クエリ", "tab_manager.hover.kind.redis_command": "Redis コマンド", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Redis 監視", "tab_manager.hover.kind.routine": "関数 / プロシージャ", + "tab_manager.hover.kind.sequence": "Sequence", "tab_manager.hover.kind.sql_analysis": "SQL 分析ワークベンチ", "tab_manager.hover.kind.table": "テーブルデータ", "tab_manager.hover.kind.table_export": "エクスポートワークベンチ", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "タブ", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "MV", + "tab_manager.kind_badge.package": "Pkg", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "関数", + "tab_manager.kind_badge.sequence": "Seq", "tab_manager.kind_badge.sql_analysis": "分析", "tab_manager.kind_badge.table": "表", "tab_manager.kind_badge.table_export": "エクスポート", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index c190d6ff..4e1756d8 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "Имя", "definition_viewer.editor.metadata_fallback.type_label": "Тип", "definition_viewer.editor.object_definition_not_found": "Определение {{object}} не найдено", + "definition_viewer.editor.package_definition_not_found": "Package definition not found", "definition_viewer.editor.routine_definition_not_found": "Определение функции/процедуры не найдено", + "definition_viewer.editor.sequence_definition_not_found": "Sequence definition not found", "definition_viewer.editor.sphinx.compat_queries_hint": "Было выполнено несколько совместимых запросов. Причиной могут быть ограничения версии или неподдерживаемый тип объекта.", "definition_viewer.editor.sphinx.empty_result": "Текущий экземпляр Sphinx{{version}} не вернул определение {{object}}.", "definition_viewer.editor.sphinx.failed_message_label": "Возвращенное сообщение об ошибке", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": " (версия: {{version}})", "definition_viewer.editor.unsupported_event_definition": "Этот тип базы данных не поддерживает просмотр определений событий", "definition_viewer.editor.unsupported_object_definition": "Просмотр определения этого объекта не поддерживается", + "definition_viewer.editor.unsupported_package_definition": "This database type does not support viewing package definitions", "definition_viewer.editor.unsupported_routine_definition": "Этот тип базы данных не поддерживает просмотр определений функций/процедур", + "definition_viewer.editor.unsupported_sequence_definition": "This database type does not support viewing sequence definitions", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite не поддерживает управление определениями функций/процедур", "definition_viewer.editor.unsupported_view_definition": "Этот тип базы данных не поддерживает просмотр определений представлений", "definition_viewer.editor.view_definition_not_found": "Определение представления не найдено", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "Не удалось загрузить", "definition_viewer.error.query_failed": "Не удалось запросить определение", "definition_viewer.error.query_failed_detail": "Не удалось запросить определение: {{detail}}", + "definition_viewer.error.package_name_empty": "Package name is empty", "definition_viewer.error.routine_name_empty": "Имя функции/процедуры пустое", + "definition_viewer.error.sequence_name_empty": "Sequence name is empty", "definition_viewer.error.view_name_empty": "Имя представления пустое", "definition_viewer.field.database": "База данных", "definition_viewer.field.type": "Тип", "definition_viewer.loading.event_definition": "Загрузка определения события...", + "definition_viewer.loading.package_definition": "Loading package definition...", "definition_viewer.loading.routine_definition": "Загрузка определения функции/процедуры...", + "definition_viewer.loading.sequence_definition": "Loading sequence definition...", "definition_viewer.loading.view_definition": "Загрузка определения представления...", "definition_viewer.object.event": "Событие", "definition_viewer.object.materialized_view": "Материализованное представление", + "definition_viewer.object.package": "Package", "definition_viewer.object.routine": "Функция/процедура", + "definition_viewer.object.sequence": "Sequence", "definition_viewer.object.view": "Представление", "definition_viewer.warning.refresh_latest_failed": "Не удалось обновить последнее определение", "dev.perf_data_grid.columns": "Столбцы", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "Новый запрос", "sidebar.tab.new_query_database": "Новый запрос ({{database}})", "sidebar.tab.new_table": "Новая таблица - {{database}}", + "sidebar.tab.package_definition": "Package: {{name}}", "sidebar.tab.recent_query": "Недавний запрос", "sidebar.tab.redis_command": "Команда - {{database}}", "sidebar.tab.redis_monitor": "Мониторинг - {{database}}", "sidebar.tab.routine_definition": "{{type}}: {{name}}", + "sidebar.tab.sequence_definition": "Sequence: {{name}}", "sidebar.tab.table_overview": "Обзор таблицы - {{database}}{{schema}}", "sidebar.tab.table_structure": "Структура таблицы ({{table}})", "sidebar.tab.trigger": "Триггер: {{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "Обзор JVM", "tab_manager.hover.kind.jvm_resource": "Ресурс JVM", "tab_manager.hover.kind.materialized_view": "Материализованное представление", + "tab_manager.hover.kind.package": "Package", "tab_manager.hover.kind.query": "SQL-запрос", "tab_manager.hover.kind.redis_command": "Команда Redis", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Монитор Redis", "tab_manager.hover.kind.routine": "Функция / процедура", + "tab_manager.hover.kind.sequence": "Sequence", "tab_manager.hover.kind.sql_analysis": "Рабочая область анализа SQL", "tab_manager.hover.kind.table": "Данные таблицы", "tab_manager.hover.kind.table_export": "Рабочая область экспорта", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "Вкладка", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "MV", + "tab_manager.kind_badge.package": "Pkg", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "Функция", + "tab_manager.kind_badge.sequence": "Seq", "tab_manager.kind_badge.sql_analysis": "Анализ", "tab_manager.kind_badge.table": "Таблица", "tab_manager.kind_badge.table_export": "Экспорт", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 94dd5a91..9c2371cb 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "名称", "definition_viewer.editor.metadata_fallback.type_label": "类型", "definition_viewer.editor.object_definition_not_found": "未找到{{object}}定义", + "definition_viewer.editor.package_definition_not_found": "未找到存储包定义", "definition_viewer.editor.routine_definition_not_found": "未找到函数/存储过程定义", + "definition_viewer.editor.sequence_definition_not_found": "未找到序列定义", "definition_viewer.editor.sphinx.compat_queries_hint": "已执行多套兼容查询,可能是版本能力限制或对象类型不支持。", "definition_viewer.editor.sphinx.empty_result": "当前 Sphinx 实例{{version}}未返回{{object}}定义。", "definition_viewer.editor.sphinx.failed_message_label": "返回失败信息", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": "(版本: {{version}})", "definition_viewer.editor.unsupported_event_definition": "暂不支持该数据库类型的事件定义查看", "definition_viewer.editor.unsupported_object_definition": "暂不支持该对象定义查看", + "definition_viewer.editor.unsupported_package_definition": "暂不支持该数据库类型的存储包定义查看", "definition_viewer.editor.unsupported_routine_definition": "暂不支持该数据库类型的函数/存储过程定义查看", + "definition_viewer.editor.unsupported_sequence_definition": "暂不支持该数据库类型的序列定义查看", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite 不支持函数/存储过程定义管理", "definition_viewer.editor.unsupported_view_definition": "暂不支持该数据库类型的视图定义查看", "definition_viewer.editor.view_definition_not_found": "未找到视图定义", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "加载失败", "definition_viewer.error.query_failed": "查询定义失败", "definition_viewer.error.query_failed_detail": "查询定义失败:{{detail}}", + "definition_viewer.error.package_name_empty": "存储包名称为空", "definition_viewer.error.routine_name_empty": "函数/存储过程名称为空", + "definition_viewer.error.sequence_name_empty": "序列名称为空", "definition_viewer.error.view_name_empty": "视图名称为空", "definition_viewer.field.database": "数据库", "definition_viewer.field.type": "类型", "definition_viewer.loading.event_definition": "加载事件定义...", + "definition_viewer.loading.package_definition": "加载存储包定义...", "definition_viewer.loading.routine_definition": "加载函数/存储过程定义...", + "definition_viewer.loading.sequence_definition": "加载序列定义...", "definition_viewer.loading.view_definition": "加载视图定义...", "definition_viewer.object.event": "事件", "definition_viewer.object.materialized_view": "物化视图", + "definition_viewer.object.package": "存储包", "definition_viewer.object.routine": "函数/存储过程", + "definition_viewer.object.sequence": "序列", "definition_viewer.object.view": "视图", "definition_viewer.warning.refresh_latest_failed": "刷新最新定义失败", "dev.perf_data_grid.columns": "列数", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "新建查询", "sidebar.tab.new_query_database": "新建查询({{database}})", "sidebar.tab.new_table": "新建表 - {{database}}", + "sidebar.tab.package_definition": "存储包:{{name}}", "sidebar.tab.recent_query": "最近查询", "sidebar.tab.redis_command": "命令 - {{database}}", "sidebar.tab.redis_monitor": "监控 - {{database}}", "sidebar.tab.routine_definition": "{{type}}:{{name}}", + "sidebar.tab.sequence_definition": "序列:{{name}}", "sidebar.tab.table_overview": "表概览 - {{database}}{{schema}}", "sidebar.tab.table_structure": "表结构({{table}})", "sidebar.tab.trigger": "触发器:{{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "JVM 概览", "tab_manager.hover.kind.jvm_resource": "JVM 资源", "tab_manager.hover.kind.materialized_view": "物化视图", + "tab_manager.hover.kind.package": "存储包", "tab_manager.hover.kind.query": "SQL 查询", "tab_manager.hover.kind.redis_command": "Redis 命令", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Redis 监控", "tab_manager.hover.kind.routine": "函数 / 存储过程", + "tab_manager.hover.kind.sequence": "序列", "tab_manager.hover.kind.sql_analysis": "SQL 分析工作台", "tab_manager.hover.kind.table": "表数据", "tab_manager.hover.kind.table_export": "导出工作台", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "标签", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "物化视图", + "tab_manager.kind_badge.package": "包", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "函数", + "tab_manager.kind_badge.sequence": "序列", "tab_manager.kind_badge.sql_analysis": "分析", "tab_manager.kind_badge.table": "表", "tab_manager.kind_badge.table_export": "导出", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index f459a483..643a5a5d 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -4677,7 +4677,9 @@ "definition_viewer.editor.metadata_fallback.name_label": "名稱", "definition_viewer.editor.metadata_fallback.type_label": "類型", "definition_viewer.editor.object_definition_not_found": "找不到{{object}}定義", + "definition_viewer.editor.package_definition_not_found": "Package definition not found", "definition_viewer.editor.routine_definition_not_found": "找不到函式/預存程序定義", + "definition_viewer.editor.sequence_definition_not_found": "Sequence definition not found", "definition_viewer.editor.sphinx.compat_queries_hint": "已執行多組相容查詢,可能是版本能力限制或物件類型不支援。", "definition_viewer.editor.sphinx.empty_result": "目前 Sphinx 實例{{version}}未返回{{object}}定義。", "definition_viewer.editor.sphinx.failed_message_label": "返回失敗訊息", @@ -4686,7 +4688,9 @@ "definition_viewer.editor.sphinx.version_suffix": "(版本: {{version}})", "definition_viewer.editor.unsupported_event_definition": "暫不支援此資料庫類型的事件定義查看", "definition_viewer.editor.unsupported_object_definition": "暫不支援此物件定義查看", + "definition_viewer.editor.unsupported_package_definition": "This database type does not support viewing package definitions", "definition_viewer.editor.unsupported_routine_definition": "暫不支援此資料庫類型的函式/預存程序定義查看", + "definition_viewer.editor.unsupported_sequence_definition": "This database type does not support viewing sequence definitions", "definition_viewer.editor.unsupported_sqlite_routine_definition": "SQLite 不支援函式/預存程序定義管理", "definition_viewer.editor.unsupported_view_definition": "暫不支援此資料庫類型的檢視定義查看", "definition_viewer.editor.view_definition_not_found": "找不到檢視定義", @@ -4695,16 +4699,22 @@ "definition_viewer.error.load_failed": "載入失敗", "definition_viewer.error.query_failed": "查詢定義失敗", "definition_viewer.error.query_failed_detail": "查詢定義失敗:{{detail}}", + "definition_viewer.error.package_name_empty": "Package name is empty", "definition_viewer.error.routine_name_empty": "函式/預存程序名稱為空", + "definition_viewer.error.sequence_name_empty": "Sequence name is empty", "definition_viewer.error.view_name_empty": "檢視名稱為空", "definition_viewer.field.database": "資料庫", "definition_viewer.field.type": "類型", "definition_viewer.loading.event_definition": "載入事件定義...", + "definition_viewer.loading.package_definition": "Loading package definition...", "definition_viewer.loading.routine_definition": "載入函式/預存程序定義...", + "definition_viewer.loading.sequence_definition": "Loading sequence definition...", "definition_viewer.loading.view_definition": "載入檢視定義...", "definition_viewer.object.event": "事件", "definition_viewer.object.materialized_view": "物化檢視", + "definition_viewer.object.package": "Package", "definition_viewer.object.routine": "函式/預存程序", + "definition_viewer.object.sequence": "Sequence", "definition_viewer.object.view": "檢視", "definition_viewer.warning.refresh_latest_failed": "重新整理最新定義失敗", "dev.perf_data_grid.columns": "欄數", @@ -7019,10 +7029,12 @@ "sidebar.tab.new_query": "新增查詢", "sidebar.tab.new_query_database": "新增查詢({{database}})", "sidebar.tab.new_table": "新增資料表 - {{database}}", + "sidebar.tab.package_definition": "Package: {{name}}", "sidebar.tab.recent_query": "最近查詢", "sidebar.tab.redis_command": "命令 - {{database}}", "sidebar.tab.redis_monitor": "監控 - {{database}}", "sidebar.tab.routine_definition": "{{type}}:{{name}}", + "sidebar.tab.sequence_definition": "Sequence: {{name}}", "sidebar.tab.table_overview": "資料表概覽 - {{database}}{{schema}}", "sidebar.tab.table_structure": "資料表結構({{table}})", "sidebar.tab.trigger": "觸發器:{{name}}", @@ -7280,11 +7292,13 @@ "tab_manager.hover.kind.jvm_overview": "JVM 概覽", "tab_manager.hover.kind.jvm_resource": "JVM 資源", "tab_manager.hover.kind.materialized_view": "物化視圖", + "tab_manager.hover.kind.package": "Package", "tab_manager.hover.kind.query": "SQL 查詢", "tab_manager.hover.kind.redis_command": "Redis 命令", "tab_manager.hover.kind.redis_keys": "Redis Key", "tab_manager.hover.kind.redis_monitor": "Redis 監控", "tab_manager.hover.kind.routine": "函數 / 程序", + "tab_manager.hover.kind.sequence": "Sequence", "tab_manager.hover.kind.sql_analysis": "SQL 分析工作台", "tab_manager.hover.kind.table": "表資料", "tab_manager.hover.kind.table_export": "匯出工作台", @@ -7300,9 +7314,11 @@ "tab_manager.kind_badge.fallback": "標籤", "tab_manager.kind_badge.jvm": "JVM", "tab_manager.kind_badge.materialized_view": "物化視圖", + "tab_manager.kind_badge.package": "Pkg", "tab_manager.kind_badge.query": "SQL", "tab_manager.kind_badge.redis": "Redis", "tab_manager.kind_badge.routine": "函式", + "tab_manager.kind_badge.sequence": "Seq", "tab_manager.kind_badge.sql_analysis": "分析", "tab_manager.kind_badge.table": "表", "tab_manager.kind_badge.table_export": "匯出",