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) {