From 07a4fe1fc9455657da9bfb10cb3a782bc0416d9d Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 18 Jul 2026 10:32:51 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20MySQL=20=E5=85=B3=E9=94=AE=E5=AD=97=E9=9C=80?= =?UTF-8?q?=E7=BE=8E=E5=8C=96=E5=90=8E=E6=89=8D=E7=9D=80=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根据当前连接方言为 MySQL 系查询启用 Monaco MySQL 语法 - 将补全、悬浮、斜杠命令和代码片段同步注册到 MySQL 语言 - 增加方言映射和首屏着色回归测试 Fixes #612 --- .../QueryEditor.external-sql-save.test.tsx | 49 ++++++++++++++++--- frontend/src/components/QueryEditor.tsx | 38 ++++++++++---- .../queryEditor/QueryEditorHelpers.test.ts | 14 ++++++ .../queryEditor/QueryEditorHelpers.ts | 13 ++++- 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 1f2371d2..efcac9de 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -170,6 +170,7 @@ const autoFetchState = vi.hoisted(() => ({ const monacoEditorMockState = vi.hoisted(() => ({ deferOnMount: false, + latestProps: null as any, })); const defaultEditorContributionResolver = (state: { @@ -199,7 +200,9 @@ const editorState = vi.hoisted(() => { position: { lineNumber: 1, column: 1 }, selection: null as any, providers: [] as any[], + providerLanguages: [] as string[], hoverProviders: [] as any[], + hoverProviderLanguages: [] as string[], contentChangeListeners: [] as Array<() => void>, cursorPositionListeners: [] as Array<(event: any) => void>, modelContentListeners: [] as Array<(event: any) => void>, @@ -365,7 +368,9 @@ vi.mock('../utils/autoFetchVisibility', () => ({ })); vi.mock('@monaco-editor/react', () => ({ - default: ({ defaultValue, onChange, onMount }: any) => { + default: (props: any) => { + const { defaultValue, onChange, onMount } = props; + monacoEditorMockState.latestProps = props; React.useEffect(() => { editorState.value = String(defaultValue || ''); editorState.latestOnChange = onChange; @@ -376,11 +381,13 @@ vi.mock('@monaco-editor/react', () => ({ languages: { CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 }, CompletionItemInsertTextRule: { InsertAsSnippet: 1 }, - registerCompletionItemProvider: vi.fn((_language: string, provider: any) => { + registerCompletionItemProvider: vi.fn((language: string, provider: any) => { + editorState.providerLanguages.push(language); editorState.providers.push(provider); return { dispose: vi.fn() }; }), - registerHoverProvider: vi.fn((_language: string, provider: any) => { + registerHoverProvider: vi.fn((language: string, provider: any) => { + editorState.hoverProviderLanguages.push(language); editorState.hoverProviders.push(provider); return { dispose: vi.fn() }; }), @@ -865,9 +872,12 @@ describe('QueryEditor external SQL save', () => { editorState.value = ''; editorState.position = { lineNumber: 1, column: 1 }; editorState.selection = null; + monacoEditorMockState.latestProps = null; editorState.domNode.style.cursor = ''; editorState.providers = []; + editorState.providerLanguages = []; editorState.hoverProviders = []; + editorState.hoverProviderLanguages = []; editorState.contentChangeListeners = []; editorState.cursorPositionListeners = []; editorState.modelContentListeners = []; @@ -2369,9 +2379,34 @@ describe('QueryEditor external SQL save', () => { const completionState = (globalThis as any).__gonaviSqlCompletionState; - expect(editorState.hoverProviders).toHaveLength(1); - expect(editorState.providers).toHaveLength(3); - expect(completionState.disposables).toHaveLength(4); + expect(editorState.hoverProviderLanguages).toEqual(['sql', 'mysql']); + expect(editorState.providerLanguages).toEqual(['sql', 'mysql', 'sql', 'mysql', 'sql', 'mysql']); + expect(editorState.hoverProviders).toHaveLength(2); + expect(editorState.providers).toHaveLength(6); + expect(completionState.disposables).toHaveLength(8); + + await act(async () => { + renderer.unmount(); + }); + }); + + it.each([ + ['mysql', 'mysql'], + ['mariadb', 'mysql'], + ['postgres', 'sql'], + ])('uses the %s connection grammar before formatting SQL', async (dbType, expectedLanguage) => { + storeState.connections[0].config.type = dbType; + const initialSql = "update finan_ set openid = 'ol_' where pay_status = '0' limit 50"; + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + expect(monacoEditorMockState.latestProps).toMatchObject({ + defaultValue: initialSql, + language: expectedLanguage, + }); await act(async () => { renderer.unmount(); @@ -6122,7 +6157,7 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); - expect(editorState.hoverProviders).toHaveLength(1); + expect(editorState.hoverProviders).toHaveLength(2); const hover = editorState.hoverProviders[0].provideHover( editorState.editor.getModel(), { lineNumber: 1, column: 18 }, diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 96a9acf5..4ae07b3b 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -160,6 +160,7 @@ import { resolveOracleLikeExecutionSchemaName, resolveOracleLikeLookupSchemaCandidates, resolveQueryEditorFormatterLanguage, + resolveQueryEditorMonacoLanguage, resolveQueryEditorHoverTarget, resolveQueryEditorNavigationDecorations, resolveQueryEditorNavigationTarget, @@ -886,7 +887,8 @@ const resolveQueryEditorAiConnectionHost = (connection: any): string => { // HMR 重载时释放旧注册避免补全和 hover 内容重复 const _g = globalThis as any; -const SQL_COMPLETION_PROVIDER_VERSION = '20260715-oracle-view-synonym-v1'; +const SQL_COMPLETION_PROVIDER_VERSION = '20260718-mysql-language-v1'; +const QUERY_EDITOR_MONACO_LANGUAGE_IDS = ['sql', 'mysql'] as const; if (!_g.__gonaviSqlCompletionState) { _g.__gonaviSqlCompletionState = { registered: false, version: '', disposables: [] as any[] }; } @@ -1379,6 +1381,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc ), [connections, currentConnectionId], ); + const queryEditorMonacoLanguage = useMemo( + () => resolveQueryEditorMonacoLanguage( + connections.find(connection => connection.id === currentConnectionId), + ), + [connections, currentConnectionId], + ); const addSqlLog = useStore(state => state.addSqlLog); const sqlLogCount = useStore(state => state.sqlLogs.length); @@ -4860,7 +4868,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc _g.__gonaviSqlCompletionState.version = SQL_COMPLETION_PROVIDER_VERSION; sqlCompletionDisposables.forEach((d: any) => d?.dispose?.()); sqlCompletionDisposables.length = 0; - sqlCompletionDisposables.push(monaco.languages.registerHoverProvider('sql', { + const registerQueryEditorHoverProvider = (provider: any) => { + QUERY_EDITOR_MONACO_LANGUAGE_IDS.forEach((languageId) => { + sqlCompletionDisposables.push(monaco.languages.registerHoverProvider(languageId, provider)); + }); + }; + const registerQueryEditorCompletionProvider = (provider: any) => { + QUERY_EDITOR_MONACO_LANGUAGE_IDS.forEach((languageId) => { + sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider(languageId, provider)); + }); + }; + registerQueryEditorHoverProvider({ provideHover: (model: any, position: any) => { const normalizedPosition = normalizeEditorPosition(position); if (!normalizedPosition) { @@ -4896,8 +4914,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc contents: [{ value: buildQueryEditorHoverMarkdown(hoverTarget) }], }; }, - })); - sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', { + }); + registerQueryEditorCompletionProvider({ triggerCharacters: ['.'], provideCompletionItems: async (model: any, position: any, _context?: any, token?: { isCancellationRequested?: boolean }) => { if (isSqlCompletionRequestCancelled(token)) { @@ -5792,8 +5810,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc ]; return { suggestions }; } - })); - sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', { + }); + registerQueryEditorCompletionProvider({ triggerCharacters: ['/'], provideCompletionItems: (model: any, position: any) => { const lineContent = model.getLineContent(position.lineNumber); @@ -5820,11 +5838,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc })), }; }, - })); + }); // SQL snippet completion provider - sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', { + registerQueryEditorCompletionProvider({ provideCompletionItems: (model: any, position: any) => { const word = model.getWordUntilPosition(position); const prefix = word.word.toLowerCase(); @@ -5856,7 +5874,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc })), }; }, - })); + }); } // end sqlCompletionRegistered guard @@ -8710,7 +8728,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc { diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts index 234fac21..983cae1b 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.test.ts @@ -7,11 +7,25 @@ import { resolveOracleLikeDefaultSchemaName, resolveOracleLikeExecutionSchemaName, resolveOracleLikeLookupSchemaCandidates, + resolveQueryEditorMonacoLanguage, resolveQueryEditorNavigationTarget, selectUnqualifiedCompletionSynonyms, shouldHandleQueryEditorRunShortcutFallback, } from './QueryEditorHelpers'; +describe('QueryEditor Monaco SQL grammar', () => { + it.each([ + [{ config: { type: 'mysql' } }, 'mysql'], + [{ config: { type: 'mariadb' } }, 'mysql'], + [{ config: { type: 'custom', driver: 'greatdb' } }, 'mysql'], + [{ config: { type: 'oceanbase', oceanBaseProtocol: 'mysql' } }, 'mysql'], + [{ config: { type: 'oceanbase', oceanBaseProtocol: 'oracle' } }, 'sql'], + [{ config: { type: 'postgres' } }, 'sql'], + ])('maps connection row %# to the expected Monaco grammar', (connection, expectedLanguage) => { + expect(resolveQueryEditorMonacoLanguage(connection)).toBe(expectedLanguage); + }); +}); + describe('QueryEditor run shortcut routing', () => { it('reserves editor-originated shortcuts for Monaco and keeps document targets as a fallback', () => { const editorTarget = {} as Node; diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index 7a2635a6..6a14503f 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -2,7 +2,7 @@ import type { SqlLanguage } from 'sql-formatter'; import type { TabData, ColumnDefinition, IndexDefinition } from '../../types'; import { DBGetColumns, DBGetIndexes, DBQuery } from '../../../wailsjs/go/app/App'; import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; -import { isOracleLikeDialect, resolveSqlDialect } from '../../utils/sqlDialect'; +import { isMysqlFamilyDialect, isOracleLikeDialect, resolveSqlDialect } from '../../utils/sqlDialect'; import { extractQueryResultTableRef, type QueryResultTableRef } from '../../utils/queryResultTable'; import { quoteIdentPart } from '../../utils/sql'; import { splitSidebarQualifiedName } from '../../utils/sidebarLocate'; @@ -572,6 +572,17 @@ export const normalizeMetadataDialect = (conn: any): string => { return String(dialect || '').toLowerCase(); }; +export type QueryEditorMonacoLanguage = 'sql' | 'mysql'; + +export const resolveQueryEditorMonacoLanguage = (conn: any): QueryEditorMonacoLanguage => { + const dialect = resolveSqlDialect( + String(conn?.config?.type || ''), + String(conn?.config?.driver || ''), + { oceanBaseProtocol: conn?.config?.oceanBaseProtocol }, + ); + return isMysqlFamilyDialect(dialect) ? 'mysql' : 'sql'; +}; + export const resolveQueryEditorFormatterLanguage = (conn: any): SqlLanguage => { const dialect = normalizeMetadataDialect(conn); switch (dialect) {