diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx
index dce3a549..1b64b7b6 100644
--- a/frontend/src/components/DataGrid.ddl.test.tsx
+++ b/frontend/src/components/DataGrid.ddl.test.tsx
@@ -1890,6 +1890,40 @@ describe('DataGrid DDL interactions', () => {
expect(content).toContain('name');
});
+ it('opens the embedded object designer from an initial v2 table view request', async () => {
+ storeState.appearance.uiVersion = 'v2';
+ backendApp.DBGetColumns.mockResolvedValueOnce({
+ success: true,
+ data: [
+ { name: 'id', type: 'bigint', key: 'PRI', nullable: 'NO', default: '', comment: '' },
+ { name: 'name', type: 'varchar(255)', key: '', nullable: 'YES', default: '', comment: '' },
+ ],
+ });
+
+ let renderer: ReactTestRenderer;
+ await act(async () => {
+ renderer = create(
+ ,
+ );
+ });
+ await waitForEffects();
+
+ const content = textContent(renderer!.root);
+ expect(content).toContain('SCHEMA DESIGNER');
+ expect(content).toContain('字段');
+ expect(content).toContain('id');
+ expect(content).toContain('name');
+ });
+
it('returns to the legacy table view when v2-only footer views are active during UI switch', async () => {
storeState.appearance.uiVersion = 'v2';
diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx
index 298c303e..b9a93033 100644
--- a/frontend/src/components/DataGrid.tsx
+++ b/frontend/src/components/DataGrid.tsx
@@ -286,7 +286,9 @@ const DataGrid: React.FC = ({
resultExportAllSql,
onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition,
onApplyQuickWhereCondition,
- scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions, showRowNumberColumn = false, isActive = true, enableSqlLogEvent = false
+ scrollSnapshot, onScrollSnapshotChange, toolbarExtraActions, showRowNumberColumn = false, isActive = true, enableSqlLogEvent = false,
+ initialViewMode,
+ initialViewModeRequestId
}) => {
const connections = useStore(state => state.connections);
const addTab = useStore(state => state.addTab);
@@ -1343,6 +1345,8 @@ const DataGrid: React.FC = ({
},
},
translate: translateDataGrid,
+ initialViewMode,
+ initialViewModeRequestId,
});
useEffect(() => {
diff --git a/frontend/src/components/DataGridCore.tsx b/frontend/src/components/DataGridCore.tsx
index fbb999e4..c8741e59 100644
--- a/frontend/src/components/DataGridCore.tsx
+++ b/frontend/src/components/DataGridCore.tsx
@@ -1336,6 +1336,8 @@ interface DataGridProps {
toolbarExtraActions?: React.ReactNode;
isActive?: boolean;
enableSqlLogEvent?: boolean;
+ initialViewMode?: GridViewMode;
+ initialViewModeRequestId?: string;
}
type GridFilterCondition = FilterCondition & {
diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx
index dee7fc50..d8cd05bc 100644
--- a/frontend/src/components/DataViewer.tsx
+++ b/frontend/src/components/DataViewer.tsx
@@ -1231,6 +1231,8 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
onScrollSnapshotChange={handleTableScrollSnapshotChange}
isActive={isActive}
enableSqlLogEvent
+ initialViewMode={tab.initialViewMode}
+ initialViewModeRequestId={tab.initialViewModeRequestId}
/>
);
diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index d11c3809..f8d13367 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -1302,6 +1302,83 @@ describe('QueryEditor external SQL save', () => {
});
});
+ it('suggests MySQL CALL keyword and stored routine names in SQL completion', async () => {
+ let renderer!: ReactTestRenderer;
+ autoFetchState.visible = true;
+ storeState.connections[0].config.type = 'mysql';
+ storeState.connections[0].config.database = 'main';
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+ backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
+ const text = String(sql || '');
+ if (text.includes('information_schema.routines')) {
+ return {
+ success: true,
+ data: [
+ { routine_name: 'codex_tmp_proc_link_test', routine_type: 'PROCEDURE', schema_name: 'main' },
+ { routine_name: 'codex_tmp_score_user', routine_type: 'FUNCTION', schema_name: 'main' },
+ ],
+ };
+ }
+ return { success: true, data: [] };
+ });
+
+ await act(async () => {
+ renderer = create();
+ });
+ await act(async () => {
+ for (let i = 0; i < 12; i += 1) {
+ await Promise.resolve();
+ }
+ });
+
+ const sqlProvider = findSqlCompletionProvider();
+ expect(sqlProvider).toBeTruthy();
+
+ editorState.value = 'CA';
+ editorState.latestOnChange?.(editorState.value);
+ const keywordItems = await sqlProvider.provideCompletionItems(
+ editorState.editor.getModel(),
+ { lineNumber: 1, column: editorState.value.length + 1 },
+ );
+ expect(keywordItems.suggestions.some((item: any) => item.label === 'CALL')).toBe(true);
+
+ editorState.value = 'CALL codex_tmp';
+ editorState.latestOnChange?.(editorState.value);
+ const routineItems = await sqlProvider.provideCompletionItems(
+ editorState.editor.getModel(),
+ { lineNumber: 1, column: editorState.value.length + 1 },
+ );
+ const procedureSuggestion = routineItems.suggestions.find((item: any) => item.label === 'codex_tmp_proc_link_test');
+ const functionSuggestion = routineItems.suggestions.find((item: any) => item.label === 'codex_tmp_score_user');
+
+ expect(procedureSuggestion).toMatchObject({
+ kind: 2,
+ insertText: 'codex_tmp_proc_link_test($0)',
+ detail: '存储过程 (main)',
+ });
+ expect(String(procedureSuggestion?.sortText || '')).toMatch(/^00/);
+ expect(functionSuggestion).toBeUndefined();
+
+ editorState.value = 'SELECT codex_tmp';
+ editorState.latestOnChange?.(editorState.value);
+ const expressionRoutineItems = await sqlProvider.provideCompletionItems(
+ editorState.editor.getModel(),
+ { lineNumber: 1, column: editorState.value.length + 1 },
+ );
+ const expressionFunctionSuggestion = expressionRoutineItems.suggestions.find((item: any) => item.label === 'codex_tmp_score_user');
+ expect(expressionFunctionSuggestion).toMatchObject({
+ kind: 2,
+ insertText: 'codex_tmp_score_user($0)',
+ detail: '函数 (main)',
+ });
+
+ await act(async () => {
+ renderer.unmount();
+ });
+ });
+
it('quotes uppercase postgres table names in FROM completion insert text', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
@@ -1622,7 +1699,7 @@ describe('QueryEditor external SQL save', () => {
});
});
- it('opens a table tab on ctrl left click inside the editor', async () => {
+ it('opens a table data tab with the embedded object designer on ctrl left click inside the editor', async () => {
editorState.value = 'select * from analytics.events where id = 1';
autoFetchState.visible = true;
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
@@ -1664,6 +1741,8 @@ describe('QueryEditor external SQL save', () => {
connectionId: 'conn-1',
dbName: 'analytics',
tableName: 'events',
+ initialViewMode: 'fields',
+ initialViewModeRequestId: expect.any(String),
objectType: 'table',
});
expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({
@@ -1673,7 +1752,7 @@ describe('QueryEditor external SQL save', () => {
expect(stopPropagation).toHaveBeenCalled();
});
- it('opens a table tab on macOS cmd click when Monaco omits leftButton', async () => {
+ it('opens a table data tab with the embedded object designer on macOS cmd click when Monaco omits leftButton', async () => {
editorState.value = 'select * from fs_mkefu_regist_record;';
autoFetchState.visible = true;
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
@@ -1709,6 +1788,8 @@ describe('QueryEditor external SQL save', () => {
connectionId: 'conn-1',
dbName: 'mkefu_location_dev_local',
tableName: 'fs_mkefu_regist_record',
+ initialViewMode: 'fields',
+ initialViewModeRequestId: expect.any(String),
objectType: 'table',
}));
expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({
@@ -1788,6 +1869,74 @@ describe('QueryEditor external SQL save', () => {
expect(stopPropagation).toHaveBeenCalled();
});
+ it('opens a MySQL procedure object-edit tab from a CALL routine link', async () => {
+ storeState.connections[0].config.type = 'mysql';
+ storeState.connections[0].config.database = 'main';
+ editorState.value = 'CALL codex_tmp_proc_link_test();';
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+ backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
+ const text = String(sql || '');
+ if (text.includes('information_schema.routines') || text.includes('SHOW FUNCTION STATUS') || text.includes('SHOW PROCEDURE STATUS')) {
+ return {
+ success: true,
+ data: [{ routine_name: 'codex_tmp_proc_link_test', routine_type: 'PROCEDURE', schema_name: 'main' }],
+ };
+ }
+ if (text.includes('SHOW CREATE PROCEDURE')) {
+ return {
+ success: true,
+ data: [{
+ 'Create Procedure': 'CREATE PROCEDURE codex_tmp_proc_link_test() BEGIN SELECT 1 AS codex_tmp_result; END',
+ }],
+ };
+ }
+ return { success: true, data: [] };
+ });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ for (let i = 0; i < 12; i += 1) {
+ await Promise.resolve();
+ }
+ });
+
+ const preventDefault = vi.fn();
+ const stopPropagation = vi.fn();
+ await act(async () => {
+ editorState.mouseDownListeners[0]?.({
+ target: { position: { lineNumber: 1, column: 12 } },
+ event: {
+ browserEvent: { button: 0, buttons: 1 },
+ leftButton: true,
+ ctrlKey: true,
+ metaKey: false,
+ preventDefault,
+ stopPropagation,
+ },
+ });
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
+ });
+
+ expect(backendApp.DBQuery).toHaveBeenCalledWith(expect.any(Object), 'main', 'SHOW CREATE PROCEDURE `codex_tmp_proc_link_test`');
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ title: expect.stringContaining('codex_tmp_proc_link_test'),
+ type: 'query',
+ connectionId: 'conn-1',
+ dbName: 'main',
+ queryMode: 'object-edit',
+ query: expect.stringContaining('CREATE PROCEDURE codex_tmp_proc_link_test()'),
+ }));
+ expect(preventDefault).toHaveBeenCalled();
+ expect(stopPropagation).toHaveBeenCalled();
+ });
+
it('does not read the full editor model when ctrl/cmd clicking objects in large SQL', async () => {
editorState.value = [
...Array.from({ length: 4000 }, (_, index) => `-- filler ${index + 1}`),
@@ -1836,6 +1985,7 @@ describe('QueryEditor external SQL save', () => {
connectionId: 'conn-1',
dbName: 'analytics',
tableName: 'events',
+ initialViewMode: 'fields',
}));
expect(preventDefault).toHaveBeenCalled();
expect(stopPropagation).toHaveBeenCalled();
@@ -1891,6 +2041,261 @@ describe('QueryEditor external SQL save', () => {
expect(editorState.editor.updateOptions).toHaveBeenLastCalledWith({ mouseStyle: 'text' });
});
+ it('keeps link-style feedback when modifier state is tracked but mousemove omits ctrl/meta flags', async () => {
+ const windowListeners: Record void)[]> = {};
+ vi.stubGlobal('window', {
+ addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
+ windowListeners[type] ||= [];
+ windowListeners[type].push(listener);
+ }),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ });
+
+ editorState.value = 'SELECT * FROM uk_user';
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'uk_user' }] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ editorState.editor.deltaDecorations.mockClear();
+ await act(async () => {
+ windowListeners.keydown?.forEach((listener) => listener({
+ type: 'keydown',
+ ctrlKey: true,
+ metaKey: false,
+ key: 'Control',
+ code: 'ControlLeft',
+ repeat: false,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ target: null,
+ }));
+ editorState.mouseMoveListeners[0]?.({
+ target: { position: { lineNumber: 1, column: 18 } },
+ event: {
+ ctrlKey: false,
+ metaKey: false,
+ },
+ });
+ });
+
+ expect(editorState.domNode.style.cursor).toBe('pointer');
+ const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
+ expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
+ });
+
+ it('opens an object tab when modifier state is tracked but mousedown omits ctrl/meta flags', async () => {
+ const windowListeners: Record void)[]> = {};
+ vi.stubGlobal('window', {
+ addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
+ windowListeners[type] ||= [];
+ windowListeners[type].push(listener);
+ }),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ });
+
+ editorState.value = 'SELECT * FROM uk_user';
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'uk_user' }] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const preventDefault = vi.fn();
+ const stopPropagation = vi.fn();
+ await act(async () => {
+ windowListeners.keydown?.forEach((listener) => listener({
+ type: 'keydown',
+ ctrlKey: true,
+ metaKey: false,
+ key: 'Control',
+ code: 'ControlLeft',
+ repeat: false,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ target: null,
+ }));
+ editorState.mouseDownListeners[0]?.({
+ target: { position: { lineNumber: 1, column: 18 } },
+ event: {
+ browserEvent: { button: 0, buttons: 1 },
+ ctrlKey: false,
+ metaKey: false,
+ preventDefault,
+ stopPropagation,
+ },
+ });
+ });
+
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ type: 'table',
+ connectionId: 'conn-1',
+ dbName: 'mkefu_location_dev_local',
+ tableName: 'uk_user',
+ initialViewMode: 'fields',
+ }));
+ expect(preventDefault).toHaveBeenCalled();
+ expect(stopPropagation).toHaveBeenCalled();
+ });
+
+ it('opens an object tab when mousedown stores ctrl/meta flags on the native browser event', async () => {
+ editorState.value = 'SELECT * FROM uk_user';
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'uk_user' }] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const preventDefault = vi.fn();
+ const stopPropagation = vi.fn();
+ await act(async () => {
+ editorState.mouseDownListeners[0]?.({
+ target: { position: { lineNumber: 1, column: 18 } },
+ event: {
+ leftButton: true,
+ ctrlKey: false,
+ metaKey: false,
+ browserEvent: {
+ button: 0,
+ buttons: 1,
+ ctrlKey: false,
+ metaKey: true,
+ preventDefault,
+ stopPropagation,
+ },
+ preventDefault,
+ stopPropagation,
+ },
+ });
+ });
+
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ type: 'table',
+ connectionId: 'conn-1',
+ dbName: 'mkefu_location_dev_local',
+ tableName: 'uk_user',
+ initialViewMode: 'fields',
+ }));
+ expect(preventDefault).toHaveBeenCalled();
+ expect(stopPropagation).toHaveBeenCalled();
+ });
+
+ it('shows link-style feedback from the current cursor when ctrl/cmd is pressed without moving the mouse', async () => {
+ const windowListeners: Record void)[]> = {};
+ vi.stubGlobal('window', {
+ addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
+ windowListeners[type] ||= [];
+ windowListeners[type].push(listener);
+ }),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ });
+
+ editorState.value = 'SELECT * FROM uk_user';
+ editorState.position = { lineNumber: 1, column: 'SELECT * FROM uk_user'.length + 1 };
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'uk_user' }] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ editorState.editor.deltaDecorations.mockClear();
+ await act(async () => {
+ windowListeners.keydown?.forEach((listener) => listener({
+ ctrlKey: true,
+ metaKey: false,
+ key: 'Control',
+ code: 'ControlLeft',
+ repeat: false,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ target: null,
+ }));
+ });
+
+ expect(editorState.editor.deltaDecorations).toHaveBeenCalled();
+ expect(editorState.domNode.style.cursor).toBe('pointer');
+ const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
+ expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
+ });
+
+ it('treats modifier keydown itself as pressed when desktop WebView omits ctrl/meta flags', async () => {
+ const windowListeners: Record void)[]> = {};
+ vi.stubGlobal('window', {
+ addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
+ windowListeners[type] ||= [];
+ windowListeners[type].push(listener);
+ }),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ });
+
+ editorState.value = 'SELECT * FROM uk_user';
+ editorState.position = { lineNumber: 1, column: 'SELECT * FROM uk_user'.length + 1 };
+ autoFetchState.visible = true;
+ backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
+ backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'uk_user' }] });
+ backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
+
+ await act(async () => {
+ create();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ editorState.editor.deltaDecorations.mockClear();
+ await act(async () => {
+ windowListeners.keydown?.forEach((listener) => listener({
+ type: 'keydown',
+ ctrlKey: false,
+ metaKey: false,
+ key: 'Meta',
+ code: 'MetaLeft',
+ repeat: false,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ target: null,
+ }));
+ });
+
+ expect(editorState.domNode.style.cursor).toBe('pointer');
+ const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
+ expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
+ });
+
it('shows hover shortcut hints in English for every navigable object kind', () => {
setCurrentLanguage('en-US');
@@ -1920,7 +2325,7 @@ describe('QueryEditor external SQL save', () => {
const cases = [
{ lineContent: 'use analytics', column: 6, expected: 'Ctrl + click to switch to this database' },
- { lineContent: 'select * from analytics.events', column: 27, expected: 'Ctrl + click to open this table' },
+ { lineContent: 'select * from analytics.events', column: 27, expected: 'Ctrl + click to open this table object design' },
{ lineContent: 'select * from reporting.active_users', column: 31, expected: 'Ctrl + click to open this view' },
{ lineContent: 'select * from analytics.mv_daily_stats', column: 37, expected: 'Ctrl + click to open this materialized view' },
{ lineContent: 'call audit.users_bi()', column: 18, expected: 'Ctrl + click to open this trigger' },
@@ -3729,7 +4134,7 @@ describe('QueryEditor external SQL save', () => {
expect(css).toMatch(/body\[data-theme='dark'\]\s+\.gonavi-query-editor-link-hint\s*\{[^}]*color:\s*#69b1ff\s*!important;/s);
});
- it('opens a view tab on ctrl left click inside the editor', async () => {
+ it('opens a view object-edit tab on ctrl left click inside the editor', async () => {
editorState.value = 'select * from reporting.active_users';
autoFetchState.visible = true;
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
@@ -3737,7 +4142,7 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (sql.includes('information_schema.views') || sql.includes('pg_catalog.pg_views') || sql.includes('USER_VIEWS') || sql.includes('ALL_VIEWS')) {
- return { success: true, data: [{ view_name: 'active_users', schema_name: 'reporting' }] };
+ return { success: true, data: [{ view_name: 'active_users', schema_name: 'reporting', view_definition: 'select id from users' }] };
}
return { success: true, data: [] };
});
@@ -3762,32 +4167,36 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
expect(storeState.setActiveContext).not.toHaveBeenCalled();
- expect(storeState.addTab).toHaveBeenCalledWith({
- id: 'view-def-conn-1-main-active_users',
- title: '视图:active_users',
- type: 'view-def',
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ id: expect.stringMatching(/^query-edit-object-conn-1-main-reporting\.active_users-\d+$/),
+ title: '修改视图: reporting.active_users',
+ type: 'query',
connectionId: 'conn-1',
dbName: 'main',
- viewName: 'active_users',
- viewKind: 'view',
- schemaName: 'reporting',
- sidebarLocateKey: 'conn-1-main-view-active_users',
- });
+ queryMode: 'object-edit',
+ query: expect.stringContaining('CREATE OR REPLACE VIEW reporting.active_users AS'),
+ }));
expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({
type: 'gonavi:locate-sidebar-object',
}));
});
- it('opens trigger and routine tabs on ctrl left click inside the editor', async () => {
+ it('opens trigger and routine object-edit tabs on ctrl left click inside the editor', async () => {
editorState.value = 'call audit.users_bi(); call reporting.refresh_stats();';
autoFetchState.visible = true;
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
+ if (sql.includes('SHOW CREATE TRIGGER')) {
+ return { success: true, data: [{ 'SQL Original Statement': 'CREATE TRIGGER audit.users_bi BEFORE INSERT ON audit.users FOR EACH ROW SET @a = 1' }] };
+ }
if (sql.includes('information_schema.triggers') || sql.includes('SHOW TRIGGERS') || sql.includes('USER_TRIGGERS') || sql.includes('ALL_TRIGGERS')) {
return { success: true, data: [{ trigger_name: 'users_bi', table_name: 'users', schema_name: 'audit' }] };
}
@@ -3817,6 +4226,9 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
await act(async () => {
@@ -3830,19 +4242,20 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
- expect(storeState.addTab).toHaveBeenCalledWith({
- id: 'trigger-conn-1-main-audit.users_bi',
- title: '触发器:audit.users_bi',
- type: 'trigger',
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ id: expect.stringMatching(/^query-edit-trigger-conn-1-main-audit\.users_bi-\d+$/),
+ title: '修改触发器: audit.users_bi',
+ type: 'query',
connectionId: 'conn-1',
dbName: 'main',
- triggerName: 'audit.users_bi',
- triggerTableName: 'audit.users',
- schemaName: 'audit',
- sidebarLocateKey: 'conn-1-main-trigger-audit.users_bi-audit.users',
- });
+ queryMode: 'object-edit',
+ query: expect.stringContaining('CREATE TRIGGER audit.users_bi'),
+ }));
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
id: expect.stringMatching(/^query-edit-routine-conn-1-main-reporting\.refresh_stats-\d+$/),
title: '编辑 存储过程:reporting.refresh_stats',
@@ -3857,7 +4270,7 @@ describe('QueryEditor external SQL save', () => {
}));
});
- it('opens sequence and package tabs on ctrl left click inside the editor', async () => {
+ it('opens sequence and package object-edit tabs on ctrl left click inside the editor', async () => {
editorState.value = 'select billing.order_seq.nextval from dual; begin billing.pkg_order.sync_order(1); end;';
autoFetchState.visible = true;
storeState.connections[0].config.type = 'oracle';
@@ -3867,7 +4280,25 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (sql.includes('ALL_SEQUENCES') || sql.includes('USER_SEQUENCES')) {
- return { success: true, data: [{ sequence_name: 'order_seq', schema_name: 'billing' }] };
+ return {
+ success: true,
+ data: [{
+ sequence_owner: 'BILLING',
+ sequence_name: 'ORDER_SEQ',
+ min_value: 1,
+ max_value: 999999,
+ increment_by: 1,
+ cache_size: 20,
+ cycle_flag: 'N',
+ order_flag: 'N',
+ }],
+ };
+ }
+ if (sql.includes('ALL_SOURCE') || sql.includes('USER_SOURCE')) {
+ if (sql.includes("TYPE = 'PACKAGE BODY'")) {
+ return { success: true, data: [{ TEXT: 'PACKAGE BODY pkg_order AS\nPROCEDURE sync_order(p_id NUMBER) IS BEGIN NULL; END;\nEND pkg_order;\n' }] };
+ }
+ return { success: true, data: [{ TEXT: 'PACKAGE pkg_order AS\nPROCEDURE sync_order(p_id NUMBER);\nEND pkg_order;\n' }] };
}
if (sql.includes('ALL_OBJECTS') && sql.includes("OBJECT_TYPE = 'PACKAGE'")) {
return { success: true, data: [{ package_name: 'pkg_order', schema_name: 'billing' }] };
@@ -3895,6 +4326,9 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
await act(async () => {
@@ -3908,35 +4342,36 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
- expect(storeState.addTab).toHaveBeenCalledWith({
- id: 'sequence-def-conn-1-main-billing.order_seq',
- title: '序列:billing.order_seq',
- type: 'sequence-def',
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ id: expect.stringMatching(/^query-edit-object-conn-1-main-BILLING\.ORDER_SEQ-\d+$/),
+ title: '修改序列: BILLING.ORDER_SEQ',
+ type: 'query',
connectionId: 'conn-1',
dbName: 'main',
- sequenceName: 'billing.order_seq',
- schemaName: 'billing',
- sidebarLocateKey: 'sequence-def-conn-1-main-billing.order_seq',
- });
- expect(storeState.addTab).toHaveBeenCalledWith({
- id: 'package-def-conn-1-main-billing.pkg_order',
- title: '存储包:billing.pkg_order',
- type: 'package-def',
+ queryMode: 'object-edit',
+ query: expect.stringContaining('CREATE SEQUENCE BILLING.ORDER_SEQ'),
+ }));
+ expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
+ id: expect.stringMatching(/^query-edit-object-conn-1-main-billing\.pkg_order-\d+$/),
+ title: '修改存储包: billing.pkg_order',
+ type: 'query',
connectionId: 'conn-1',
dbName: 'main',
- packageName: 'billing.pkg_order',
- schemaName: 'billing',
- sidebarLocateKey: 'package-def-conn-1-main-billing.pkg_order',
- });
+ queryMode: 'object-edit',
+ query: expect.stringContaining('CREATE OR REPLACE PACKAGE pkg_order'),
+ }));
expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({
type: 'gonavi:locate-sidebar-object',
}));
});
describe('object navigation tab title localization', () => {
- it('uses the English catalog title for view definition tabs', async () => {
+ it('uses the English catalog title for view object-edit tabs', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
editorState.value = 'select * from reporting.active_users';
@@ -3946,7 +4381,7 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (sql.includes('information_schema.views') || sql.includes('pg_catalog.pg_views') || sql.includes('USER_VIEWS') || sql.includes('ALL_VIEWS')) {
- return { success: true, data: [{ view_name: 'active_users', schema_name: 'reporting' }] };
+ return { success: true, data: [{ view_name: 'active_users', schema_name: 'reporting', view_definition: 'select id from users' }] };
}
return { success: true, data: [] };
});
@@ -3971,16 +4406,20 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
- id: 'view-def-conn-1-main-active_users',
- title: 'View: active_users',
- type: 'view-def',
+ id: expect.stringMatching(/^query-edit-object-conn-1-main-reporting\.active_users-\d+$/),
+ title: 'Edit View: reporting.active_users',
+ type: 'query',
+ queryMode: 'object-edit',
}));
});
- it('uses the English catalog titles for trigger and procedure tabs', async () => {
+ it('uses the English catalog titles for trigger and procedure object-edit tabs', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
editorState.value = 'call audit.users_bi(); call reporting.refresh_stats();';
@@ -3989,6 +4428,9 @@ describe('QueryEditor external SQL save', () => {
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
+ if (sql.includes('SHOW CREATE TRIGGER')) {
+ return { success: true, data: [{ 'SQL Original Statement': 'CREATE TRIGGER audit.users_bi BEFORE INSERT ON audit.users FOR EACH ROW SET @a = 1' }] };
+ }
if (sql.includes('information_schema.triggers') || sql.includes('SHOW TRIGGERS') || sql.includes('USER_TRIGGERS') || sql.includes('ALL_TRIGGERS')) {
return { success: true, data: [{ trigger_name: 'users_bi', table_name: 'users', schema_name: 'audit' }] };
}
@@ -4018,6 +4460,9 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
await act(async () => {
@@ -4031,12 +4476,16 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
- id: 'trigger-conn-1-main-audit.users_bi',
- title: 'Trigger: audit.users_bi',
- type: 'trigger',
+ id: expect.stringMatching(/^query-edit-trigger-conn-1-main-audit\.users_bi-\d+$/),
+ title: 'Edit trigger: audit.users_bi',
+ type: 'query',
+ queryMode: 'object-edit',
}));
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
id: expect.stringMatching(/^query-edit-routine-conn-1-main-reporting\.refresh_stats-\d+$/),
@@ -4046,7 +4495,7 @@ describe('QueryEditor external SQL save', () => {
}));
});
- it('uses the English catalog title for materialized view definition tabs', async () => {
+ it('uses the English catalog title for materialized view object-edit tabs', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');
storeState.connections[0].config.type = 'starrocks';
@@ -4059,6 +4508,9 @@ describe('QueryEditor external SQL save', () => {
if (sql.includes("UPPER(TABLE_TYPE) LIKE '%MATERIALIZED%'") || sql.includes('SHOW MATERIALIZED VIEWS')) {
return { success: true, data: [{ object_name: 'mv_daily_stats', schema_name: 'analytics' }] };
}
+ if (sql.includes('SHOW CREATE MATERIALIZED VIEW') || sql.includes('SHOW CREATE TABLE')) {
+ return { success: true, data: [{ 'Create Table': 'CREATE MATERIALIZED VIEW analytics.mv_daily_stats AS SELECT 1 AS id' }] };
+ }
return { success: true, data: [] };
});
@@ -4082,13 +4534,16 @@ describe('QueryEditor external SQL save', () => {
stopPropagation: vi.fn(),
},
});
+ for (let i = 0; i < 8; i += 1) {
+ await Promise.resolve();
+ }
});
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
- id: 'view-def-conn-1-analytics-analytics.mv_daily_stats',
- title: 'Materialized view: analytics.mv_daily_stats',
- type: 'view-def',
- viewKind: 'materialized',
+ id: expect.stringMatching(/^query-edit-object-conn-1-analytics-analytics\.mv_daily_stats-\d+$/),
+ title: 'Edit Materialized view: analytics.mv_daily_stats',
+ type: 'query',
+ queryMode: 'object-edit',
}));
});
});
@@ -7786,6 +8241,8 @@ describe('QueryEditor external SQL save', () => {
connectionId: 'conn-1',
dbName: 'front_end_sys',
tableName: 'fs_mkefu_regist_record',
+ initialViewMode: 'fields',
+ initialViewModeRequestId: expect.any(String),
objectType: 'table',
}));
});
@@ -7868,6 +8325,8 @@ describe('QueryEditor external SQL save', () => {
connectionId: 'conn-1',
dbName: 'front_end_sys',
tableName: 'fs_mkefu_regist_record',
+ initialViewMode: 'fields',
+ initialViewModeRequestId: expect.any(String),
objectType: 'table',
}));
});
diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx
index c8ded0c7..aa793210 100644
--- a/frontend/src/components/QueryEditor.tsx
+++ b/frontend/src/components/QueryEditor.tsx
@@ -42,6 +42,7 @@ import {
type EditRowLocator,
} from '../utils/rowLocator';
import { getQueryTabDraft, hasQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
+import { buildEditableTriggerSql } from '../utils/triggerEditSql';
import {
getColumnDefinitionComment,
getColumnDefinitionKey,
@@ -93,6 +94,7 @@ import {
getCaseInsensitiveValue,
getFirstRowValue,
getNormalizedPositionAtOffset,
+ hasQueryEditorCtrlMetaModifier,
getInitialEditorQuery,
getMySQLShowTablesName,
getNormalizedOffsetAtPosition,
@@ -182,6 +184,440 @@ const buildQueryEditorRoutineEditFallbackSql = (
return `CREATE OR REPLACE FUNCTION ${routineName}()\nRETURNS INTEGER\nBEGIN\n -- TODO: edit function body\n RETURN 0;\nEND;`;
};
+const normalizeQueryEditorMySQLViewDDL = (rawDefinition: unknown): string => {
+ const text = String(rawDefinition || '').trim();
+ if (!text) return '';
+
+ const normalized = text.replace(/\r\n/g, '\n').trim().replace(/;+\s*$/, '');
+ const createViewPrefixPattern = /^\s*create\s+(?:algorithm\s*=\s*\w+\s+)?(?:definer\s*=\s*(?:`[^`]+`|\S+)\s*@\s*(?:`[^`]+`|\S+)\s+)?(?:sql\s+security\s+(?:definer|invoker)\s+)?view\s+/i;
+ if (createViewPrefixPattern.test(normalized)) {
+ return `${normalized.replace(createViewPrefixPattern, 'CREATE OR REPLACE VIEW ')};`;
+ }
+
+ if (/^\s*(select|with)\b/i.test(normalized)) {
+ return normalized;
+ }
+
+ return `${normalized};`;
+};
+
+const normalizeQueryEditorSqlPlusSlashTerminator = (sql: string): string => (
+ String(sql || '').trim().replace(/(^|\n)([ \t]*\/[ \t]*);+([ \t]*(?:--[^\n]*)?)\s*$/i, '$1$2$3')
+);
+
+const hasQueryEditorSqlPlusSlashTerminator = (sql: string): boolean => (
+ /(?:^|\n)[ \t]*\/[ \t]*(?:--[^\n]*)?\s*$/i.test(String(sql || '').trim())
+);
+
+const ensureQueryEditorObjectEditSqlTerminator = (sql: string): string => {
+ const normalized = normalizeQueryEditorSqlPlusSlashTerminator(sql);
+ if (!normalized) return '';
+ if (hasQueryEditorSqlPlusSlashTerminator(normalized)) return normalized;
+ return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
+};
+
+const isQueryEditorCommentOnlyDefinition = (definition: string): boolean => {
+ const normalized = String(definition || '').replace(/\r\n/g, '\n').trim();
+ if (!normalized) return false;
+ const lines = normalized
+ .split('\n')
+ .map((line) => line.trim())
+ .filter(Boolean);
+ return lines.length > 0 && lines.every((line) => line.startsWith('--'));
+};
+
+const withQueryEditorCreateOrReplacePackageHeaders = (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 buildQueryEditorQualifiedObjectName = (objectName: string, schemaName?: string): string => {
+ const normalizedObjectName = String(objectName || '').trim();
+ const normalizedSchemaName = String(schemaName || '').trim();
+ if (!normalizedObjectName || !normalizedSchemaName || normalizedObjectName.includes('.')) {
+ return normalizedObjectName;
+ }
+ return `${normalizedSchemaName}.${normalizedObjectName}`;
+};
+
+const buildQueryEditorEditableDefinitionSql = (
+ objectType: 'view-def' | 'sequence-def' | 'package-def',
+ definition: string,
+ objectName: string,
+ objectLabel: string,
+): string => {
+ const normalizedDefinition = String(definition || '').trim();
+ const header = [
+ `-- ${translate('definition_viewer.edit.comment_title', { object: objectLabel, name: objectName })}`,
+ `-- ${translate('definition_viewer.edit.comment_compatibility')}`,
+ ].join('\n') + '\n';
+ if (!normalizedDefinition) {
+ return `${header}-- ${translate('definition_viewer.edit.comment_empty_definition', { name: objectName })}\n`;
+ }
+
+ if (isQueryEditorCommentOnlyDefinition(normalizedDefinition)) {
+ return `${header}${ensureQueryEditorObjectEditSqlTerminator(normalizedDefinition)}`;
+ }
+
+ if (objectType === 'view-def' && !/^\s*create\b/i.test(normalizedDefinition)) {
+ if (/^\s*view\b/i.test(normalizedDefinition)) {
+ return `${header}${ensureQueryEditorObjectEditSqlTerminator(normalizedDefinition.replace(/^\s*view\b/i, 'CREATE OR REPLACE VIEW'))}`;
+ }
+ return `${header}CREATE OR REPLACE VIEW ${objectName} AS\n${ensureQueryEditorObjectEditSqlTerminator(normalizedDefinition)}`;
+ }
+
+ if (objectType === 'sequence-def' && !/^\s*create\b/i.test(normalizedDefinition)) {
+ return `${header}${ensureQueryEditorObjectEditSqlTerminator(`CREATE SEQUENCE ${objectName}\n${normalizedDefinition}`)}`;
+ }
+
+ if (
+ objectType === 'package-def'
+ && !/^\s*create\b/i.test(normalizedDefinition)
+ && /^\s*package\b/i.test(normalizedDefinition)
+ ) {
+ return `${header}${withQueryEditorCreateOrReplacePackageHeaders(normalizedDefinition)}`;
+ }
+
+ return `${header}${ensureQueryEditorObjectEditSqlTerminator(normalizedDefinition)}`;
+};
+
+const buildQueryEditorObjectDefinitionConnectionConfig = (conn: any): Record => ({
+ ...conn.config,
+ port: Number(conn.config?.port),
+ password: conn.config?.password || '',
+ database: conn.config?.database || '',
+ useSSH: conn.config?.useSSH || false,
+ ssh: conn.config?.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' },
+});
+
+const runQueryEditorObjectDefinitionCandidates = async (
+ config: Record,
+ dbName: string,
+ queries: string[],
+ collectAll = false,
+): Promise => {
+ const collectedRows: any[] = [];
+ for (const query of queries) {
+ const sql = String(query || '').trim();
+ if (!sql || sql.startsWith('--')) continue;
+ try {
+ const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, sql);
+ if (!result.success || !Array.isArray(result.data) || result.data.length === 0) {
+ continue;
+ }
+ if (!collectAll) {
+ return result.data;
+ }
+ collectedRows.push(...result.data);
+ } catch {
+ // 元数据定义读取失败时保留编辑模板。
+ }
+ }
+ return collectedRows;
+};
+
+const buildQueryEditorViewDefinitionQueries = (
+ dialect: string,
+ viewName: string,
+ dbName: string,
+ schemaName?: string,
+ viewKind?: 'view' | 'materialized',
+): string[] => {
+ const parsed = splitSidebarQualifiedName(viewName);
+ const objectName = parsed.objectName || viewName;
+ const schema = String(schemaName || parsed.schemaName || '').trim();
+ const safeName = escapeQueryEditorObjectEditSqlLiteral(objectName);
+ const safeDbName = escapeQueryEditorObjectEditSqlLiteral(dbName);
+
+ switch (dialect) {
+ case 'mysql':
+ case 'starrocks': {
+ const viewRef = schema
+ ? `\`${schema.replace(/`/g, '``')}\`.\`${objectName.replace(/`/g, '``')}\``
+ : `\`${objectName.replace(/`/g, '``')}\``;
+ if (dialect === 'starrocks' && viewKind === 'materialized') {
+ return [
+ `SHOW CREATE MATERIALIZED VIEW ${viewRef}`,
+ `SHOW CREATE TABLE ${viewRef}`,
+ ];
+ }
+ return [
+ `SHOW CREATE VIEW ${viewRef}`,
+ safeDbName
+ ? `SELECT VIEW_DEFINITION AS view_definition FROM information_schema.views WHERE table_schema = '${safeDbName}' AND table_name = '${safeName}' LIMIT 1`
+ : '',
+ `SHOW CREATE TABLE ${viewRef}`,
+ ].filter(Boolean);
+ }
+ case 'postgres':
+ case 'kingbase':
+ case 'highgo':
+ case 'vastbase':
+ case 'opengauss':
+ case 'gaussdb': {
+ const schemaRef = schema || 'public';
+ return [`SELECT pg_get_viewdef('${escapeQueryEditorObjectEditSqlLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`];
+ }
+ case 'sqlserver':
+ return buildSqlServerObjectDefinitionQueries('view', viewName, dbName, 'view_definition');
+ case 'oracle': {
+ const owner = schema ? escapeQueryEditorObjectEditSqlLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : '');
+ if (owner) {
+ return [`SELECT TEXT AS view_definition FROM ALL_VIEWS WHERE OWNER = '${owner}' AND VIEW_NAME = '${safeName.toUpperCase()}'`];
+ }
+ return [`SELECT TEXT AS view_definition FROM USER_VIEWS WHERE VIEW_NAME = '${safeName.toUpperCase()}'`];
+ }
+ case 'sqlite':
+ return [`SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${safeName}'`];
+ case 'duckdb': {
+ const schemaRef = schema || 'main';
+ return [`SELECT view_definition FROM information_schema.views WHERE table_schema = '${escapeQueryEditorObjectEditSqlLiteral(schemaRef)}' AND table_name = '${safeName}' LIMIT 1`];
+ }
+ default:
+ return [];
+ }
+};
+
+const extractQueryEditorViewDefinition = (dialect: string, data: any[]): string => {
+ if (!Array.isArray(data) || data.length === 0) return '';
+ const row = data[0] as Record;
+ if (dialect === 'mysql' || dialect === 'starrocks') {
+ const direct = getQueryEditorObjectEditRawValue(row, ['view_definition', 'VIEW_DEFINITION']);
+ if (direct !== undefined && direct !== null && String(direct).trim()) {
+ return normalizeQueryEditorMySQLViewDDL(direct);
+ }
+ const sqlKey = Object.keys(row).find((key) => {
+ const lowerKey = key.toLowerCase();
+ return lowerKey.includes('create view') || lowerKey === 'create view' || lowerKey.includes('create table');
+ });
+ if (sqlKey) {
+ return normalizeQueryEditorMySQLViewDDL(row[sqlKey]);
+ }
+ const createValue = Object.values(row).find((value) => {
+ const text = String(value || '').toUpperCase();
+ return text.includes('CREATE') && (text.includes('VIEW') || text.includes('TABLE'));
+ });
+ return createValue ? normalizeQueryEditorMySQLViewDDL(createValue) : '';
+ }
+ if (dialect === 'sqlserver') {
+ const direct = getQueryEditorObjectEditRawValue(row, ['view_definition', 'definition']);
+ if (direct !== undefined && direct !== null && String(direct).trim()) {
+ return String(direct);
+ }
+ return data
+ .map((item) => getQueryEditorObjectEditRawValue(item, ['Text', 'text']))
+ .filter((value) => value !== undefined && value !== null)
+ .map((value) => String(value))
+ .join('');
+ }
+ const direct = getQueryEditorObjectEditRawValue(row, ['view_definition', 'definition', 'sql', 'text', 'TEXT', 'SQL']);
+ return direct !== undefined && direct !== null ? String(direct) : String(Object.values(row)[0] || '');
+};
+
+const buildQueryEditorSequenceDefinitionQueries = (
+ dialect: string,
+ sequenceName: string,
+ dbName: string,
+ schemaName?: string,
+): string[] => {
+ const parsed = splitSidebarQualifiedName(sequenceName);
+ const objectName = parsed.objectName || sequenceName;
+ const schema = String(schemaName || parsed.schemaName || '').trim();
+ const safeName = escapeQueryEditorObjectEditSqlLiteral(objectName);
+ const safeDbName = escapeQueryEditorObjectEditSqlLiteral(dbName);
+ const owner = schema ? escapeQueryEditorObjectEditSqlLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : '');
+
+ switch (dialect) {
+ case 'oracle':
+ 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 = '${escapeQueryEditorObjectEditSqlLiteral(schemaRef)}' AND sequence_name = '${safeName}' LIMIT 1`];
+ }
+ default:
+ return [];
+ }
+};
+
+const buildQueryEditorSequenceDefinitionFromRow = (
+ row: Record,
+ fallbackSequenceName: string,
+ fallbackSchemaName?: string,
+): string => {
+ const sequenceName = String(getQueryEditorObjectEditRawValue(row, ['sequence_name']) || splitSidebarQualifiedName(fallbackSequenceName).objectName || fallbackSequenceName).trim();
+ const owner = String(getQueryEditorObjectEditRawValue(row, ['sequence_owner', 'owner', 'sequence_schema']) || fallbackSchemaName || splitSidebarQualifiedName(fallbackSequenceName).schemaName || '').trim();
+ const name = buildQueryEditorQualifiedObjectName(sequenceName, owner);
+ if (!name) return '';
+
+ const clauses: string[] = [];
+ const increment = getQueryEditorObjectEditRawValue(row, ['increment_by', 'increment']);
+ const minValue = getQueryEditorObjectEditRawValue(row, ['min_value', 'minimum_value']);
+ const maxValue = getQueryEditorObjectEditRawValue(row, ['max_value', 'maximum_value']);
+ const cacheSize = Number(getQueryEditorObjectEditRawValue(row, ['cache_size']));
+ const cycleFlag = String(getQueryEditorObjectEditRawValue(row, ['cycle_flag']) || '').trim().toUpperCase();
+ const orderFlag = String(getQueryEditorObjectEditRawValue(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 extractQueryEditorSequenceDefinition = (
+ data: any[],
+ sequenceName: string,
+ schemaName?: string,
+): string => {
+ if (!Array.isArray(data) || data.length === 0) return '';
+ return buildQueryEditorSequenceDefinitionFromRow(data[0] as Record, sequenceName, schemaName);
+};
+
+const buildQueryEditorPackageDefinitionQueries = (
+ dialect: string,
+ packageName: string,
+ dbName: string,
+ schemaName?: string,
+): string[] => {
+ const parsed = splitSidebarQualifiedName(packageName);
+ const objectName = parsed.objectName || packageName;
+ const schema = String(schemaName || parsed.schemaName || '').trim();
+ const safeName = escapeQueryEditorObjectEditSqlLiteral(objectName);
+ const safeDbName = escapeQueryEditorObjectEditSqlLiteral(dbName);
+
+ if (dialect !== 'oracle') {
+ return [];
+ }
+
+ const owner = schema ? escapeQueryEditorObjectEditSqlLiteral(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`,
+ ];
+};
+
+const extractQueryEditorPackageDefinition = (data: any[]): string => {
+ if (!Array.isArray(data) || data.length === 0) return '';
+ return data
+ .map((row: any) => getQueryEditorObjectEditRawValue(row, ['text', 'TEXT']) ?? Object.values(row || {})[0] ?? '')
+ .map((value) => String(value))
+ .join('');
+};
+
+const buildQueryEditorTriggerDefinitionQueries = (
+ dialect: string,
+ triggerName: string,
+ dbName: string,
+ schemaName?: string,
+): string[] => {
+ const parsed = splitSidebarQualifiedName(triggerName);
+ const objectName = parsed.objectName || triggerName;
+ const schema = String(schemaName || parsed.schemaName || '').trim();
+ const safeName = escapeQueryEditorObjectEditSqlLiteral(objectName);
+ const safeDbName = escapeQueryEditorObjectEditSqlLiteral(dbName);
+
+ switch (dialect) {
+ case 'mysql':
+ case 'starrocks': {
+ const triggerRef = schema
+ ? `\`${schema.replace(/`/g, '``')}\`.\`${objectName.replace(/`/g, '``')}\``
+ : `\`${objectName.replace(/`/g, '``')}\``;
+ return [
+ `SHOW CREATE TRIGGER ${triggerRef}`,
+ safeDbName
+ ? `SELECT TRIGGER_NAME, TRIGGER_SCHEMA, EVENT_OBJECT_SCHEMA, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORIENTATION, ACTION_STATEMENT FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' AND trigger_name = '${safeName}' LIMIT 1`
+ : '',
+ ].filter(Boolean);
+ }
+ case 'postgres':
+ case 'kingbase':
+ case 'highgo':
+ case 'vastbase':
+ case 'opengauss':
+ case 'gaussdb':
+ return [`SELECT pg_get_triggerdef(t.oid, true) AS trigger_definition
+FROM pg_trigger t
+JOIN pg_class c ON t.tgrelid = c.oid
+WHERE t.tgname = '${safeName}'
+ AND NOT t.tgisinternal
+LIMIT 1`];
+ case 'sqlserver':
+ return buildSqlServerObjectDefinitionQueries('trigger', triggerName, dbName, 'trigger_definition');
+ case 'oracle': {
+ if (schema) {
+ return [
+ `SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeName.toUpperCase()}', '${escapeQueryEditorObjectEditSqlLiteral(schema).toUpperCase()}') AS trigger_definition FROM DUAL`,
+ `SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${escapeQueryEditorObjectEditSqlLiteral(schema).toUpperCase()}' AND TRIGGER_NAME = '${safeName.toUpperCase()}'`,
+ ];
+ }
+ if (safeDbName) {
+ return [
+ `SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeName.toUpperCase()}', '${safeDbName.toUpperCase()}') AS trigger_definition FROM DUAL`,
+ `SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' AND TRIGGER_NAME = '${safeName.toUpperCase()}'`,
+ ];
+ }
+ return [
+ `SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeName.toUpperCase()}') AS trigger_definition FROM DUAL`,
+ `SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeName.toUpperCase()}'`,
+ ];
+ }
+ case 'sqlite':
+ return [`SELECT sql AS trigger_definition FROM sqlite_master WHERE type = 'trigger' AND name = '${safeName}'`];
+ default:
+ return [];
+ }
+};
+
+const extractQueryEditorTriggerDefinition = (dialect: string, data: any[]): string => {
+ if (!Array.isArray(data) || data.length === 0) return '';
+ const row = data[0] as Record;
+ const direct = getQueryEditorObjectEditRawValue(row, ['trigger_definition', 'definition', 'sql', 'SQL']);
+ if (direct !== undefined && direct !== null && String(direct).trim()) {
+ return String(direct);
+ }
+ if (dialect === 'mysql' || dialect === 'starrocks') {
+ const statementKey = Object.keys(row).find((key) => {
+ const lowerKey = key.toLowerCase();
+ return lowerKey.includes('statement') || lowerKey.includes('create trigger');
+ });
+ if (statementKey) return String(row[statementKey] || '');
+ const createValue = Object.values(row).find((value) => String(value || '').toUpperCase().includes('CREATE TRIGGER'));
+ return createValue ? String(createValue) : String(getQueryEditorObjectEditRawValue(row, ['ACTION_STATEMENT', 'action_statement']) || '');
+ }
+ return String(getQueryEditorObjectEditRawValue(row, ['TRIGGER_BODY', 'trigger_body', 'TEXT', 'text']) || Object.values(row)[0] || '');
+};
+
const buildQueryEditorAiContextPrompt = (connection: any, database: string): string => {
if (!connection) {
return '';
@@ -236,6 +672,7 @@ const clearRecord = (record: Record) => {
});
};
const resetSharedQueryEditorMetadata = () => {
+ sharedCurrentDb = '';
sharedTablesData = [];
sharedAllColumnsData = [];
sharedVisibleDbs = [];
@@ -1063,6 +1500,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const metadataDbName = String(currentDbRef.current || currentDb || '').trim();
if (!metadataDbName) return;
+ if (isActive) {
+ sharedCurrentDb = metadataDbName;
+ }
const metadataDbNames = collectQueryEditorReferencedDatabaseNames(
getCurrentQuery(),
metadataDbName,
@@ -1097,6 +1537,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
sequencesRef.current = [...allSequences];
packagesRef.current = [...allPackages];
if (isActive) {
+ sharedCurrentDb = metadataDbName;
sharedTablesData = tablesRef.current;
sharedAllColumnsData = allColumnsRef.current;
sharedViewsData = viewsRef.current;
@@ -1585,6 +2026,131 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
}, [addTab]);
+ const openDefinitionObjectEditTab = useCallback(async (
+ navigationTarget: Extract,
+ connectionId: string,
+ targetDbName: string,
+ ) => {
+ const targetSchemaName = String(navigationTarget.schemaName || '').trim();
+ const conn = connectionsRef.current.find((item) => item.id === connectionId);
+ const dialect = conn ? normalizeMetadataDialect(conn) : '';
+ let targetObjectName = '';
+ let objectEditName = '';
+ let objectLabel = '';
+ let definitionTabType: 'view-def' | 'sequence-def' | 'package-def' = 'view-def';
+ let definitionQueries: string[] = [];
+ let collectAllDefinitionRows = false;
+ let latestDefinition = '';
+
+ if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') {
+ targetObjectName = String(navigationTarget.viewName || '').trim();
+ if (!targetObjectName) return;
+ definitionTabType = 'view-def';
+ objectEditName = buildQueryEditorQualifiedObjectName(targetObjectName, targetSchemaName);
+ objectLabel = navigationTarget.type === 'materialized-view'
+ ? translate('definition_viewer.object.materialized_view')
+ : translate('definition_viewer.object.view');
+ definitionQueries = conn
+ ? buildQueryEditorViewDefinitionQueries(
+ dialect,
+ targetObjectName,
+ targetDbName,
+ targetSchemaName,
+ navigationTarget.type === 'materialized-view' ? 'materialized' : 'view',
+ )
+ : [];
+ } else if (navigationTarget.type === 'sequence') {
+ targetObjectName = String(navigationTarget.sequenceName || '').trim();
+ if (!targetObjectName) return;
+ definitionTabType = 'sequence-def';
+ objectEditName = buildQueryEditorQualifiedObjectName(targetObjectName, targetSchemaName);
+ objectLabel = translate('definition_viewer.object.sequence');
+ definitionQueries = conn
+ ? buildQueryEditorSequenceDefinitionQueries(dialect, targetObjectName, targetDbName, targetSchemaName)
+ : [];
+ } else {
+ targetObjectName = String(navigationTarget.packageName || '').trim();
+ if (!targetObjectName) return;
+ definitionTabType = 'package-def';
+ objectEditName = buildQueryEditorQualifiedObjectName(targetObjectName, targetSchemaName);
+ objectLabel = translate('definition_viewer.object.package');
+ collectAllDefinitionRows = true;
+ definitionQueries = conn
+ ? buildQueryEditorPackageDefinitionQueries(dialect, targetObjectName, targetDbName, targetSchemaName)
+ : [];
+ }
+
+ if (conn && definitionQueries.length > 0) {
+ const rows = await runQueryEditorObjectDefinitionCandidates(
+ buildQueryEditorObjectDefinitionConnectionConfig(conn),
+ targetDbName,
+ definitionQueries,
+ collectAllDefinitionRows,
+ );
+ if (definitionTabType === 'view-def') {
+ latestDefinition = extractQueryEditorViewDefinition(dialect, rows);
+ } else if (definitionTabType === 'sequence-def') {
+ latestDefinition = extractQueryEditorSequenceDefinition(rows, targetObjectName, targetSchemaName);
+ } else {
+ latestDefinition = extractQueryEditorPackageDefinition(rows);
+ }
+ }
+
+ addTab({
+ id: `query-edit-object-${connectionId}-${targetDbName}-${objectEditName}-${Date.now()}`,
+ title: translate('definition_viewer.edit.tab_title', {
+ object: objectLabel,
+ name: objectEditName,
+ }),
+ type: 'query',
+ connectionId,
+ dbName: targetDbName,
+ query: buildQueryEditorEditableDefinitionSql(
+ definitionTabType,
+ latestDefinition,
+ objectEditName,
+ objectLabel,
+ ),
+ queryMode: 'object-edit',
+ });
+ }, [addTab]);
+
+ const openTriggerObjectEditTab = useCallback(async (
+ navigationTarget: Extract,
+ connectionId: string,
+ targetDbName: string,
+ ) => {
+ const targetTriggerName = String(navigationTarget.triggerName || '').trim();
+ if (!targetTriggerName) return;
+
+ const conn = connectionsRef.current.find((item) => item.id === connectionId);
+ let latestDefinition = '';
+ if (conn) {
+ const dialect = normalizeMetadataDialect(conn);
+ const rows = await runQueryEditorObjectDefinitionCandidates(
+ buildQueryEditorObjectDefinitionConnectionConfig(conn),
+ targetDbName,
+ buildQueryEditorTriggerDefinitionQueries(
+ dialect,
+ targetTriggerName,
+ targetDbName,
+ navigationTarget.schemaName,
+ ),
+ );
+ latestDefinition = extractQueryEditorTriggerDefinition(dialect, rows);
+ }
+
+ addTab({
+ id: `query-edit-trigger-${connectionId}-${targetDbName}-${targetTriggerName}-${Date.now()}`,
+ title: translate('trigger_viewer.tab.edit_trigger_title', { name: targetTriggerName }),
+ type: 'query',
+ connectionId,
+ dbName: targetDbName,
+ query: buildEditableTriggerSql(targetTriggerName, latestDefinition, { translate }),
+ queryMode: 'object-edit',
+ });
+ }, [addTab]);
+
// Setup Autocomplete and Editor
const handleEditorDidMount: OnMount = (editor, monaco) => {
editorRef.current = editor;
@@ -1666,7 +2232,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (isKeyboardLikeEvent && isImeComposingKeyEvent(keyboardEvent as KeyboardEvent)) {
return;
}
- const nextPressed = !!(keyboardEvent?.ctrlKey || keyboardEvent?.metaKey);
+ const keyboardEventType = isKeyboardLikeEvent ? String((keyboardEvent as KeyboardEvent).type || '').toLowerCase() : '';
+ const keyboardKey = isKeyboardLikeEvent ? String((keyboardEvent as KeyboardEvent).key || '').trim().toLowerCase() : '';
+ const keyboardCode = isKeyboardLikeEvent ? String((keyboardEvent as KeyboardEvent).code || '').trim().toLowerCase() : '';
+ const isModifierKeyDown = isKeyboardLikeEvent
+ && keyboardEventType !== 'keyup'
+ && (
+ keyboardKey === 'control'
+ || keyboardKey === 'ctrl'
+ || keyboardKey === 'meta'
+ || keyboardKey === 'os'
+ || keyboardKey === 'command'
+ || keyboardCode.startsWith('control')
+ || keyboardCode.startsWith('meta')
+ || keyboardCode.startsWith('os')
+ );
+ const eventHasModifierFlag = hasQueryEditorCtrlMetaModifier(keyboardEvent);
+ const nextPressed = isKeyboardLikeEvent
+ ? !!(eventHasModifierFlag || isModifierKeyDown)
+ : !!(eventHasModifierFlag || wasPressed);
ctrlMetaPressedRef.current = nextPressed;
if (!nextPressed && !wasPressed) {
return;
@@ -1678,7 +2262,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
if (!wasPressed || isKeyboardLikeEvent) {
- applyNavigationHoverStateAtPosition(lastHoverTargetPositionRef.current);
+ const keyboardFallbackPosition = isKeyboardLikeEvent
+ ? normalizeEditorPosition(editor.getPosition?.()) || lastEditorCursorPositionRef.current
+ : null;
+ applyNavigationHoverStateAtPosition(lastHoverTargetPositionRef.current || keyboardFallbackPosition);
}
};
const handleWindowBlur = () => {
@@ -1864,7 +2451,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (!isQueryEditorPrimaryMouseButton(browserEvent)) {
return;
}
- if (!browserEvent.ctrlKey && !browserEvent.metaKey) {
+ if (!hasQueryEditorCtrlMetaModifier(browserEvent) && !ctrlMetaPressedRef.current) {
return;
}
@@ -1921,88 +2508,30 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
connectionId,
dbName: targetDbName,
tableName: targetTableName,
+ initialViewMode: 'fields',
+ initialViewModeRequestId: String(Date.now()),
objectType: 'table',
});
return;
}
if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') {
- const targetViewName = String(navigationTarget.viewName || '').trim();
- if (!targetViewName) return;
- const viewTitle = navigationTarget.type === 'materialized-view'
- ? translate('sidebar.tab.materialized_view_definition', { name: targetViewName })
- : translate('sidebar.tab.view_definition', { name: targetViewName });
- const targetSchemaName = String(navigationTarget.schemaName || '').trim();
- const sidebarLocateKey = navigationTarget.type === 'materialized-view'
- ? `${connectionId}-${targetDbName}-materialized-view-${targetViewName}`
- : `${connectionId}-${targetDbName}-view-${targetViewName}`;
- addTab({
- id: `view-def-${connectionId}-${targetDbName}-${targetViewName}`,
- title: viewTitle,
- type: 'view-def',
- connectionId,
- dbName: targetDbName,
- viewName: targetViewName,
- viewKind: navigationTarget.type === 'materialized-view' ? 'materialized' : 'view',
- schemaName: targetSchemaName || undefined,
- sidebarLocateKey,
- });
+ void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName);
return;
}
if (navigationTarget.type === 'trigger') {
- const targetTriggerName = String(navigationTarget.triggerName || '').trim();
- if (!targetTriggerName) return;
- const targetTriggerTableName = String(navigationTarget.tableName || '').trim();
- const targetSchemaName = String(navigationTarget.schemaName || '').trim();
- const sidebarLocateKey = `${connectionId}-${targetDbName}-trigger-${targetTriggerName}-${targetTriggerTableName}`;
- addTab({
- id: `trigger-${connectionId}-${targetDbName}-${targetTriggerName}`,
- title: translate('sidebar.tab.trigger', { name: targetTriggerName }),
- type: 'trigger',
- connectionId,
- dbName: targetDbName,
- triggerName: targetTriggerName,
- triggerTableName: targetTriggerTableName || undefined,
- schemaName: targetSchemaName || undefined,
- sidebarLocateKey,
- });
+ void openTriggerObjectEditTab(navigationTarget, connectionId, targetDbName);
return;
}
if (navigationTarget.type === 'sequence') {
- const targetSequenceName = String(navigationTarget.sequenceName || '').trim();
- if (!targetSequenceName) return;
- const targetSchemaName = String(navigationTarget.schemaName || '').trim();
- const sidebarLocateKey = `sequence-def-${connectionId}-${targetDbName}-${targetSequenceName}`;
- addTab({
- id: `sequence-def-${connectionId}-${targetDbName}-${targetSequenceName}`,
- title: translate('sidebar.tab.sequence_definition', { name: targetSequenceName }),
- type: 'sequence-def',
- connectionId,
- dbName: targetDbName,
- sequenceName: targetSequenceName,
- schemaName: targetSchemaName || undefined,
- sidebarLocateKey,
- });
+ void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName);
return;
}
if (navigationTarget.type === 'package') {
- const targetPackageName = String(navigationTarget.packageName || '').trim();
- if (!targetPackageName) return;
- const targetSchemaName = String(navigationTarget.schemaName || '').trim();
- const sidebarLocateKey = `package-def-${connectionId}-${targetDbName}-${targetPackageName}`;
- addTab({
- id: `package-def-${connectionId}-${targetDbName}-${targetPackageName}`,
- title: translate('sidebar.tab.package_definition', { name: targetPackageName }),
- type: 'package-def',
- connectionId,
- dbName: targetDbName,
- packageName: targetPackageName,
- schemaName: targetSchemaName || undefined,
- sidebarLocateKey,
- });
+ void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName);
return;
}
@@ -2179,6 +2708,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (!raw) return raw;
return shouldQuoteCompletionIdentifiers ? quoteQualifiedIdent(activeDialect, raw) : raw;
};
+ const getActiveCompletionDbName = () => String(sharedCurrentDb || currentDbRef.current || currentDb || tab.dbName || '').trim();
const dialectKeywords = resolveSqlKeywords(activeDialect);
const dialectFunctions = resolveSqlFunctions(activeDialect);
@@ -2205,6 +2735,42 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
dbQualifiedLabel,
};
};
+ const normalizeRoutineType = (routineType: string) => (
+ String(routineType || '').trim().toUpperCase().includes('PROC') ? 'PROCEDURE' : 'FUNCTION'
+ );
+ const getRoutineTypeLabel = (routineType: string) => (
+ normalizeRoutineType(routineType) === 'PROCEDURE'
+ ? translate('sidebar.object.procedure')
+ : translate('sidebar.object.function')
+ );
+ const buildRoutineSuggestionMeta = (routine: CompletionRoutineMeta) => {
+ const rawDbName = String(routine.dbName || '').trim();
+ const rawRoutineName = String(routine.routineName || '').trim();
+ const parsed = splitSchemaAndTable(rawRoutineName);
+ const schemaName = String(routine.schemaName || parsed.schema || '').trim();
+ const objectName = String(parsed.table || rawRoutineName).trim();
+ const schemaMatchesDb = !!schemaName
+ && !!rawDbName
+ && schemaName.toLowerCase() === rawDbName.toLowerCase();
+ const isCurrentDb = rawDbName.toLowerCase() === getActiveCompletionDbName().toLowerCase();
+ const displayName = isCurrentDb && schemaMatchesDb
+ ? objectName
+ : (parsed.schema ? rawRoutineName : objectName);
+ const dbQualifiedLabel = rawDbName && !isCurrentDb
+ ? `${rawDbName}.${displayName}`
+ : displayName;
+ const insertName = rawDbName && !isCurrentDb
+ ? dbQualifiedLabel
+ : displayName;
+ return {
+ displayName,
+ dbQualifiedLabel,
+ insertText: `${quoteCompletionPath(insertName)}($0)`,
+ objectName,
+ schemaName,
+ routineType: normalizeRoutineType(routine.routineType),
+ };
+ };
const buildConnConfig = () => {
const connId = sharedCurrentConnectionId;
@@ -2413,7 +2979,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
sortText: '0' + suggestionMeta.displayName
};
});
- return { suggestions };
+ const routineSuggestions = sharedRoutinesData
+ .filter((routine) => String(routine.dbName || '').toLowerCase() === qualifierLower)
+ .map((routine) => ({ routine, meta: buildRoutineSuggestionMeta(routine) }))
+ .filter(({ routine, meta }) => (
+ !prefix
+ || meta.displayName.toLowerCase().startsWith(prefix)
+ || meta.objectName.toLowerCase().startsWith(prefix)
+ || String(routine.routineName || '').toLowerCase().startsWith(prefix)
+ ))
+ .map(({ routine, meta }) => ({
+ label: meta.displayName,
+ kind: monaco.languages.CompletionItemKind.Function,
+ insertText: meta.insertText,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+ detail: `${getRoutineTypeLabel(routine.routineType)} (${routine.dbName})`,
+ range,
+ sortText: '1' + meta.displayName,
+ }));
+ return { suggestions: [...suggestions, ...routineSuggestions] };
}
// qualifier 是 schema(如 dbo/public)时,仅补全表名,避免输入 dbo. 后再补成 dbo.dbo.table
@@ -2443,7 +3027,23 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
range,
sortText: '0' + t.table
}));
- return { suggestions };
+ const routineSuggestions = sharedRoutinesData
+ .filter((routine) => {
+ const meta = buildRoutineSuggestionMeta(routine);
+ return meta.schemaName.toLowerCase() === qualifierLower;
+ })
+ .map((routine) => ({ routine, meta: buildRoutineSuggestionMeta(routine) }))
+ .filter(({ meta }) => !prefix || meta.objectName.toLowerCase().startsWith(prefix))
+ .map(({ routine, meta }) => ({
+ label: meta.objectName,
+ kind: monaco.languages.CompletionItemKind.Function,
+ insertText: `${quoteCompletionPart(meta.objectName)}($0)`,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+ detail: `${getRoutineTypeLabel(routine.routineType)} (${routine.dbName}${meta.schemaName ? '.' + meta.schemaName : ''})`,
+ range,
+ sortText: '1' + meta.objectName,
+ }));
+ return { suggestions: [...suggestions, ...routineSuggestions] };
}
// 否则检查是否是表别名或表名,提示列
@@ -2488,7 +3088,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
foundTables.add(t.toLowerCase());
}
- const currentDatabase = sharedCurrentDb || '';
+ const currentDatabase = getActiveCompletionDbName();
const wordPrefix = (word.word || '').toLowerCase();
const startsWithPrefix = (candidate: string) => !wordPrefix || candidate.toLowerCase().startsWith(wordPrefix);
const includesWordPrefix = (candidate: string) => !wordPrefix || String(candidate || '').toLowerCase().includes(wordPrefix);
@@ -2502,14 +3102,18 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return '9';
};
const expectsTableName = /\b(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM|TABLE|DESCRIBE|DESC|EXPLAIN)\s+[`"]?[\w.]*$/i.test(linePrefix);
+ const expectsRoutineName = /\bCALL\s+[`"]?[\w.]*$/i.test(linePrefix);
const shouldBoostKeywords = !expectsTableName
+ && !expectsRoutineName
&& wordPrefix.length > 0
&& dialectKeywords.some((keyword) => keyword.toLowerCase().startsWith(wordPrefix));
const sortGroups = shouldBoostKeywords
- ? { keyword: '00', func: '05', columnCurrent: '10', columnOther: '11', tableCurrent: '20', tableOther: '21', db: '30' }
+ ? { keyword: '00', func: '05', routineCurrent: '06', routineOther: '07', columnCurrent: '10', columnOther: '11', tableCurrent: '20', tableOther: '21', db: '30' }
+ : expectsRoutineName
+ ? { keyword: '30', func: '40', routineCurrent: '00', routineOther: '01', columnCurrent: '20', columnOther: '21', tableCurrent: '10', tableOther: '11', db: '35' }
: expectsTableName
- ? { keyword: '20', func: '25', columnCurrent: '10', columnOther: '11', tableCurrent: '00', tableOther: '01', db: '30' }
- : { keyword: '30', func: '25', columnCurrent: '00', columnOther: '01', tableCurrent: '10', tableOther: '11', db: '20' };
+ ? { keyword: '20', func: '25', routineCurrent: '26', routineOther: '27', columnCurrent: '10', columnOther: '11', tableCurrent: '00', tableOther: '01', db: '30' }
+ : { keyword: '30', func: '25', routineCurrent: '26', routineOther: '27', columnCurrent: '00', columnOther: '01', tableCurrent: '10', tableOther: '11', db: '20' };
let completionTables = sharedTablesData;
if (
expectsTableName
@@ -2674,6 +3278,33 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
});
+ const routineSuggestions = sharedRoutinesData
+ .map((routine) => ({ routine, meta: buildRoutineSuggestionMeta(routine) }))
+ .filter(({ routine, meta }) => {
+ if (expectsRoutineName && meta.routineType !== 'PROCEDURE') return false;
+ return includesWordPrefix(meta.dbQualifiedLabel)
+ || includesWordPrefix(meta.displayName)
+ || includesWordPrefix(meta.objectName)
+ || includesWordPrefix(routine.routineName || '');
+ })
+ .map(({ routine, meta }) => {
+ const isCurrentDb = (routine.dbName || '').toLowerCase() === currentDatabase.toLowerCase();
+ const schemaInfo = meta.schemaName && meta.schemaName.toLowerCase() !== String(routine.dbName || '').toLowerCase()
+ ? `.${meta.schemaName}`
+ : '';
+ return {
+ label: meta.dbQualifiedLabel,
+ kind: monaco.languages.CompletionItemKind.Function,
+ insertText: meta.insertText,
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+ detail: `${getRoutineTypeLabel(routine.routineType)} (${routine.dbName}${schemaInfo})`,
+ range,
+ sortText: (isCurrentDb ? sortGroups.routineCurrent : sortGroups.routineOther)
+ + getPrefixMatchRank(meta.dbQualifiedLabel, meta.displayName, meta.objectName, routine.routineName || '')
+ + meta.dbQualifiedLabel,
+ };
+ });
+
// 数据库提示
const dbSuggestions = sharedVisibleDbs
.filter((db) => startsWithPrefix(db))
@@ -2714,6 +3345,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
...relevantColumns, // FROM 表的列最优先
...tableSuggestions, // 表次之
...dbSuggestions, // 数据库
+ ...routineSuggestions, // 存储过程/函数
...funcSuggestions, // 内置函数
...keywordSuggestions // 关键字最后
];
diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
index 9e5f8a44..0c12d703 100644
--- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts
+++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts
@@ -130,6 +130,16 @@ export const isQueryEditorPrimaryMouseButton = (event: any): boolean => {
return false;
};
+export const hasQueryEditorCtrlMetaModifier = (event: any): boolean => {
+ const candidates = [
+ event,
+ event?.browserEvent,
+ event?.nativeEvent,
+ event?.originalEvent,
+ ];
+ return candidates.some((candidate) => !!(candidate?.ctrlKey || candidate?.metaKey));
+};
+
export const readSidebarSqlDropText = (
event: DragEvent,
currentConnectionId = '',
diff --git a/frontend/src/components/useDataGridDdlView.ts b/frontend/src/components/useDataGridDdlView.ts
index 5364ba14..4f1f35e1 100644
--- a/frontend/src/components/useDataGridDdlView.ts
+++ b/frontend/src/components/useDataGridDdlView.ts
@@ -7,6 +7,8 @@ import { formatDdlForDisplay } from '../utils/ddlFormat';
type GridViewMode = 'table' | 'json' | 'text' | 'fields' | 'ddl' | 'er' | 'sqlLog';
type DdlViewLayoutMode = 'bottom' | 'side';
type TranslateParams = Record;
+const GRID_VIEW_MODES: GridViewMode[] = ['table', 'json', 'text', 'fields', 'ddl', 'er', 'sqlLog'];
+const V2_ONLY_GRID_VIEW_MODES = new Set(['fields', 'ddl', 'er', 'sqlLog']);
const DDL_VIEW_LAYOUT_STORAGE_KEY = 'gonavi.dataGrid.ddlViewLayout';
let sharedDdlViewOpen = false;
let sharedDdlViewLayout: DdlViewLayoutMode | null = null;
@@ -72,6 +74,17 @@ const buildDdlContextKey = (currentConnConfig: unknown, dbName?: string, tableNa
].join('\u0001');
};
+const resolveInitialGridViewMode = (
+ initialViewMode: GridViewMode | undefined,
+ isV2Ui: boolean,
+ canViewDdl: boolean,
+): GridViewMode | null => {
+ if (!initialViewMode || !GRID_VIEW_MODES.includes(initialViewMode)) return null;
+ if (!isV2Ui && V2_ONLY_GRID_VIEW_MODES.has(initialViewMode)) return null;
+ if (initialViewMode === 'ddl' && !canViewDdl) return null;
+ return initialViewMode;
+};
+
interface UseDataGridDdlViewParams {
canViewDdl: boolean;
currentConnConfig: unknown;
@@ -90,6 +103,8 @@ interface UseDataGridDdlViewParams {
};
dbType?: string;
translate?: (key: string, params?: TranslateParams) => string;
+ initialViewMode?: GridViewMode;
+ initialViewModeRequestId?: string;
}
export interface UseDataGridDdlViewResult {
@@ -128,10 +143,15 @@ export const useDataGridDdlView = ({
messageApi,
dbType,
translate,
+ initialViewMode,
+ initialViewModeRequestId,
}: UseDataGridDdlViewParams): UseDataGridDdlViewResult => {
const canRestoreSharedDdlView = isV2Ui && canViewDdl && !!currentConnConfig && !!tableName && shouldRestoreSharedDdlView();
const shouldStartWithSharedDdlView = isActive && canRestoreSharedDdlView;
- const [viewMode, setViewMode] = React.useState(() => (shouldStartWithSharedDdlView ? 'ddl' : 'table'));
+ const initialResolvedViewMode = resolveInitialGridViewMode(initialViewMode, isV2Ui, canViewDdl);
+ const [viewMode, setViewMode] = React.useState(() => (
+ shouldStartWithSharedDdlView ? 'ddl' : (initialResolvedViewMode || 'table')
+ ));
const [ddlModalOpen, setDdlModalOpen] = React.useState(false);
const [ddlLoading, setDdlLoading] = React.useState(shouldStartWithSharedDdlView);
const [ddlText, setDdlText] = React.useState('');
@@ -289,6 +309,17 @@ export const useDataGridDdlView = ({
setViewMode(nextMode);
}, [cellEditMode, closeCellEditModeRef, closeDdlView, handleOpenTableDdl, isV2Ui, mergedDisplayDataRef, resolvedViewMode, rowKeyStr, selectedRowKeys, setTextRecordIndex]);
+ const appliedInitialViewModeKeyRef = React.useRef('');
+ React.useEffect(() => {
+ if (!isActive) return;
+ const nextInitialViewMode = resolveInitialGridViewMode(initialViewMode, isV2Ui, canViewDdl);
+ if (!nextInitialViewMode) return;
+ const applyKey = `${ddlContextKey}\u0001${nextInitialViewMode}\u0001${initialViewModeRequestId || ''}`;
+ if (appliedInitialViewModeKeyRef.current === applyKey) return;
+ appliedInitialViewModeKeyRef.current = applyKey;
+ handleViewModeChange(nextInitialViewMode);
+ }, [canViewDdl, ddlContextKey, handleViewModeChange, initialViewMode, initialViewModeRequestId, isActive, isV2Ui]);
+
const handleDdlSidebarResizeStart = React.useCallback((event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
diff --git a/frontend/src/i18n/catalog.test.ts b/frontend/src/i18n/catalog.test.ts
index fbc1d0cb..ab89bd56 100644
--- a/frontend/src/i18n/catalog.test.ts
+++ b/frontend/src/i18n/catalog.test.ts
@@ -189,6 +189,10 @@ describe("i18n catalog", () => {
"app.theme.mode.light.description",
"app.theme.mode.light.label",
"app.theme.mode_title",
+ "app.theme.data_table.table_double_click_action",
+ "app.theme.data_table.table_double_click_action.open_data",
+ "app.theme.data_table.table_double_click_action.open_design",
+ "app.theme.data_table.table_double_click_action_hint",
"app.theme.nav.appearance.description",
"app.theme.nav.appearance.title",
"app.theme.nav.theme.description",
@@ -2216,19 +2220,29 @@ describe("i18n catalog", () => {
it("keeps QueryEditor object navigation tab titles in catalogs instead of source literals", () => {
const objectTabTitleKeys = [
- "sidebar.tab.view_definition",
- "sidebar.tab.materialized_view_definition",
- "sidebar.tab.trigger",
- "sidebar.tab.routine_definition",
+ "definition_viewer.edit.tab_title",
+ "definition_viewer.object.view",
+ "definition_viewer.object.materialized_view",
+ "definition_viewer.object.sequence",
+ "definition_viewer.object.package",
+ "trigger_viewer.tab.edit_trigger_title",
+ "sidebar.tab.edit_routine",
"sidebar.object.procedure",
"sidebar.object.function",
] as const;
const source = readQueryEditorSource();
- const objectNavigationSource = sliceBetween(
- source,
- " if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') {",
- " editor.onDidDispose?.(() => {",
- );
+ const objectNavigationSource = [
+ sliceBetween(
+ source,
+ "const buildQueryEditorEditableDefinitionSql = (",
+ "const buildQueryEditorAiContextPrompt = (",
+ ),
+ sliceBetween(
+ source,
+ " const openRoutineObjectEditTab = useCallback(async (",
+ " // Setup Autocomplete and Editor",
+ ),
+ ].join("\n");
for (const language of SUPPORTED_LANGUAGES) {
for (const key of objectTabTitleKeys) {
@@ -2236,10 +2250,13 @@ describe("i18n catalog", () => {
expect(catalogs[language][key]).toBeTruthy();
}
- expect(getPlaceholders(catalogs[language]["sidebar.tab.view_definition"])).toEqual(["name"]);
- expect(getPlaceholders(catalogs[language]["sidebar.tab.materialized_view_definition"])).toEqual(["name"]);
- expect(getPlaceholders(catalogs[language]["sidebar.tab.trigger"])).toEqual(["name"]);
- expect(getPlaceholders(catalogs[language]["sidebar.tab.routine_definition"])).toEqual(["name", "type"]);
+ expect([...getPlaceholders(catalogs[language]["definition_viewer.edit.tab_title"])].sort()).toEqual(["name", "object"]);
+ expect(getPlaceholders(catalogs[language]["definition_viewer.object.view"])).toEqual([]);
+ expect(getPlaceholders(catalogs[language]["definition_viewer.object.materialized_view"])).toEqual([]);
+ expect(getPlaceholders(catalogs[language]["definition_viewer.object.sequence"])).toEqual([]);
+ expect(getPlaceholders(catalogs[language]["definition_viewer.object.package"])).toEqual([]);
+ expect(getPlaceholders(catalogs[language]["trigger_viewer.tab.edit_trigger_title"])).toEqual(["name"]);
+ expect([...getPlaceholders(catalogs[language]["sidebar.tab.edit_routine"])].sort()).toEqual(["name", "type"]);
expect(getPlaceholders(catalogs[language]["sidebar.object.procedure"])).toEqual([]);
expect(getPlaceholders(catalogs[language]["sidebar.object.function"])).toEqual([]);
}
@@ -2248,7 +2265,11 @@ describe("i18n catalog", () => {
expect(objectNavigationSource).toContain(key);
}
- assertSourceDoesNotInlineCatalogValues(objectNavigationSource, objectTabTitleKeys);
+ assertSourceDoesNotInlineCatalogValues(objectNavigationSource, [
+ "definition_viewer.edit.tab_title",
+ "trigger_viewer.tab.edit_trigger_title",
+ "sidebar.tab.edit_routine",
+ ]);
});
it("guards QueryEditor V2 empty state against inlining any catalog literal into source", () => {
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 660d5aa3..e7d30957 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -470,6 +470,8 @@ export interface TabData {
queryMode?: "standard" | "object-edit";
filePath?: string;
initialTab?: string;
+ initialViewMode?: "table" | "json" | "text" | "fields" | "ddl" | "er" | "sqlLog";
+ initialViewModeRequestId?: string;
readOnly?: boolean;
providerMode?: "jmx" | "endpoint" | "agent";
resourcePath?: string;
diff --git a/frontend/src/utils/sqlDialect.test.ts b/frontend/src/utils/sqlDialect.test.ts
index ebce9c21..eb9101fa 100644
--- a/frontend/src/utils/sqlDialect.test.ts
+++ b/frontend/src/utils/sqlDialect.test.ts
@@ -118,7 +118,7 @@ describe('sqlDialect', () => {
});
it('resolves mysql-family completion keywords and functions with mysql syntax', () => {
- expect(resolveSqlKeywords('mariadb')).toEqual(expect.arrayContaining(['LIMIT', 'CHANGE', 'AUTO_INCREMENT']));
+ expect(resolveSqlKeywords('mariadb')).toEqual(expect.arrayContaining(['LIMIT', 'CHANGE', 'AUTO_INCREMENT', 'CALL']));
expect(names(resolveSqlFunctions('diros'))).toEqual(expect.arrayContaining(['DATE_FORMAT', 'GROUP_CONCAT']));
expect(resolveSqlKeywords('starrocks')).toEqual(expect.arrayContaining(['OLAP', 'DISTRIBUTED BY', 'BUCKETS', 'ADD ROLLUP', 'EXTERNAL CATALOG']));
expect(names(resolveSqlFunctions('starrocks'))).toEqual(expect.arrayContaining(['TO_BITMAP', 'HLL_UNION_AGG']));
diff --git a/frontend/src/utils/sqlDialect.ts b/frontend/src/utils/sqlDialect.ts
index 759f9def..1aef8e5c 100644
--- a/frontend/src/utils/sqlDialect.ts
+++ b/frontend/src/utils/sqlDialect.ts
@@ -581,6 +581,7 @@ const COMMON_KEYWORDS = [
const MYSQL_KEYWORDS = [
'LIMIT', 'OFFSET', 'MODIFY', 'CHANGE', 'AUTO_INCREMENT', 'SHOW', 'DESCRIBE',
'DESC', 'ENGINE', 'CHARSET', 'COLLATE', 'REPLACE', 'DUPLICATE KEY', 'LOCK',
+ 'CALL',
];
const PG_KEYWORDS = [
diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json
index 113e1b24..5c8f7173 100644
--- a/shared/i18n/de-DE.json
+++ b/shared/i18n/de-DE.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "Global folgen",
"app.theme.data_table.font_size": "Schriftgröße der Datentabelle",
"app.theme.data_table.sidebar_tree_font_size": "Schriftgröße des linken Schemasbaums",
+ "app.theme.data_table.table_double_click_action": "Doppelklick-Aktion für Tabellen",
+ "app.theme.data_table.table_double_click_action.open_data": "Daten öffnen",
+ "app.theme.data_table.table_double_click_action.open_design": "Objektdesign öffnen",
+ "app.theme.data_table.table_double_click_action_hint": "Steuert die Standardaktion beim Doppelklick auf Tabellenknoten im linken Schemasbaum. Standardmäßig werden Tabellendaten geöffnet; optional kann direkt das Objektdesign geöffnet werden.",
"app.theme.data_table.title": "Datentabellenanzeige",
"app.theme.data_table.vertical_borders": "Vertikale Trennlinien der Datentabelle anzeigen",
"app.theme.data_table.vertical_borders_hint": "Wirkt nur auf DataGrid auf Datentabellenseiten und nicht auf andere Tabellenkomponenten.",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + klicken, um dieses Paket zu öffnen",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + klicken, um diese gespeicherte Prozedur zu öffnen",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + klicken, um diese Sequenz zu öffnen",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + klicken, um diese Tabelle zu öffnen",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + klicken, um das Tabellen-Objektdesign zu öffnen",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + klicken, um diesen Trigger zu öffnen",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + klicken, um diese Ansicht zu öffnen",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + klicken, um zu dieser Datenbank zu wechseln",
diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json
index dbe8da78..4a00591c 100644
--- a/shared/i18n/en-US.json
+++ b/shared/i18n/en-US.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "Follow Global",
"app.theme.data_table.font_size": "Data Table Font Size",
"app.theme.data_table.sidebar_tree_font_size": "Sidebar Schema Tree Font Size",
+ "app.theme.data_table.table_double_click_action": "Table Double-click Action",
+ "app.theme.data_table.table_double_click_action.open_data": "Open data",
+ "app.theme.data_table.table_double_click_action.open_design": "Open object design",
+ "app.theme.data_table.table_double_click_action_hint": "Controls the default action when double-clicking table nodes in the left schema tree. The default opens table data; switch it to open object design directly.",
"app.theme.data_table.title": "Data Table Display",
"app.theme.data_table.vertical_borders": "Show Data Table Vertical Separators",
"app.theme.data_table.vertical_borders_hint": "Only affects DataGrid on data table pages, not other table components.",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + click to open this package",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + click to open this stored procedure",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + click to open this sequence",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + click to open this table",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + click to open this table object design",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + click to open this trigger",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + click to open this view",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + click to switch to this database",
diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json
index e0d93bc9..59d7a26d 100644
--- a/shared/i18n/ja-JP.json
+++ b/shared/i18n/ja-JP.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "全体設定に従う",
"app.theme.data_table.font_size": "データテーブルのフォントサイズ",
"app.theme.data_table.sidebar_tree_font_size": "左側スキーマツリーのフォントサイズ",
+ "app.theme.data_table.table_double_click_action": "テーブルのダブルクリック動作",
+ "app.theme.data_table.table_double_click_action.open_data": "データを開く",
+ "app.theme.data_table.table_double_click_action.open_design": "オブジェクト設計を開く",
+ "app.theme.data_table.table_double_click_action_hint": "左側スキーマツリーでテーブルノードをダブルクリックしたときの既定動作を制御します。既定ではテーブルデータを開き、直接オブジェクト設計を開くよう切り替えられます。",
"app.theme.data_table.title": "データテーブル表示",
"app.theme.data_table.vertical_borders": "データテーブルの縦区切り線を表示",
"app.theme.data_table.vertical_borders_hint": "データテーブルページの DataGrid のみに作用し、他のテーブルコンポーネントには影響しません。",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + クリックでこのパッケージを開く",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + クリックでこのストアドプロシージャを開く",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + クリックでこのシーケンスを開く",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + クリックでこのテーブルを開く",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + クリックでこのテーブルのオブジェクト設計を開く",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + クリックでこのトリガーを開く",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + クリックでこのビューを開く",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + クリックでこのデータベースに切り替え",
diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json
index 7c608194..c2244ab7 100644
--- a/shared/i18n/ru-RU.json
+++ b/shared/i18n/ru-RU.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "Следовать глобально",
"app.theme.data_table.font_size": "Размер шрифта таблицы данных",
"app.theme.data_table.sidebar_tree_font_size": "Размер шрифта левого дерева схем",
+ "app.theme.data_table.table_double_click_action": "Действие двойного щелчка по таблице",
+ "app.theme.data_table.table_double_click_action.open_data": "Открыть данные",
+ "app.theme.data_table.table_double_click_action.open_design": "Открыть дизайн объекта",
+ "app.theme.data_table.table_double_click_action_hint": "Управляет действием по умолчанию при двойном щелчке по таблице в левом дереве схем. По умолчанию открываются данные таблицы; можно переключить на прямое открытие дизайна объекта.",
"app.theme.data_table.title": "Отображение таблицы данных",
"app.theme.data_table.vertical_borders": "Показывать вертикальные разделители таблицы данных",
"app.theme.data_table.vertical_borders_hint": "Влияет только на DataGrid на страницах таблиц данных и не затрагивает другие табличные компоненты.",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть этот пакет",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту хранимую процедуру",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту последовательность",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту таблицу",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть проектирование объекта таблицы",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть этот триггер",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть это представление",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + щелчок, чтобы переключиться на эту базу данных",
diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json
index 48f1aef6..92f25d10 100644
--- a/shared/i18n/zh-CN.json
+++ b/shared/i18n/zh-CN.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "跟随全局",
"app.theme.data_table.font_size": "数据表字体大小",
"app.theme.data_table.sidebar_tree_font_size": "左侧库表字体大小",
+ "app.theme.data_table.table_double_click_action": "双击表名行为",
+ "app.theme.data_table.table_double_click_action.open_data": "打开表数据",
+ "app.theme.data_table.table_double_click_action.open_design": "打开对象设计",
+ "app.theme.data_table.table_double_click_action_hint": "控制左侧库表树中双击表节点的默认动作。默认打开表数据,可切换为直接打开表对象设计。",
"app.theme.data_table.title": "数据表显示",
"app.theme.data_table.vertical_borders": "显示数据表竖向分隔线",
"app.theme.data_table.vertical_borders_hint": "仅作用于数据表页面 DataGrid,不影响其他表格组件。",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + 点击打开该存储包",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + 点击打开该存储过程",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + 点击打开该序列",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 点击打开该表",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 点击打开该表对象设计",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + 点击打开该触发器",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + 点击打开该视图",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + 点击切换到该数据库",
diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json
index a6d2de10..cd6ad139 100644
--- a/shared/i18n/zh-TW.json
+++ b/shared/i18n/zh-TW.json
@@ -2677,6 +2677,10 @@
"app.theme.data_table.follow_global": "跟隨全域",
"app.theme.data_table.font_size": "資料表字型大小",
"app.theme.data_table.sidebar_tree_font_size": "左側庫表字型大小",
+ "app.theme.data_table.table_double_click_action": "雙擊表名行為",
+ "app.theme.data_table.table_double_click_action.open_data": "開啟表資料",
+ "app.theme.data_table.table_double_click_action.open_design": "開啟物件設計",
+ "app.theme.data_table.table_double_click_action_hint": "控制左側庫表樹中雙擊表節點的預設動作。預設開啟表資料,可切換為直接開啟表物件設計。",
"app.theme.data_table.title": "資料表显示",
"app.theme.data_table.vertical_borders": "显示資料表竖向分隔线",
"app.theme.data_table.vertical_borders_hint": "仅作用于資料表页面 DataGrid,不影响其他表格组件。",
@@ -6112,7 +6116,7 @@
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + 點擊開啟該套件",
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + 點擊開啟該預存程序",
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + 點擊開啟該序列",
- "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 點擊開啟該資料表",
+ "query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 點擊開啟該資料表物件設計",
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + 點擊開啟該觸發器",
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + 點擊開啟該檢視",
"query_editor.hover.switch_database_with_shortcut": "{{shortcut}} + 點擊切換到該資料庫",