From e1bc2839c5caccbff91a918ec9140dd4310ef80e Mon Sep 17 00:00:00 2001 From: kunghim Date: Fri, 7 Aug 2026 16:45:31 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat(query):=20=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E6=9F=A5=E8=AF=A2=E7=BB=A7=E6=89=BF=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E8=A1=A8=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 19 ++++++++-- .../src/utils/objectQueryTemplates.test.ts | 35 +++++++++++++++++++ frontend/src/utils/objectQueryTemplates.ts | 28 +++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c0e7e8c..3f3b6232 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -102,6 +102,8 @@ import { import { downloadBrowserTextFile } from './utils/browserFileTransfer'; import { buildDataSyncWorkbenchTab } from './utils/dataSyncTab'; import { buildSqlAuditWorkbenchTab } from './utils/sqlAuditTab'; +import { resolveDataSourceType } from './utils/dataSourceCapabilities'; +import { buildContextualNewQueryTemplate } from './utils/objectQueryTemplates'; import { extractCustomThemeAntTokens, } from './utils/customTheme'; @@ -2723,6 +2725,7 @@ function App() { const handleNewQuery = useCallback(() => { let connId = ''; let db = ''; + let tableName = ''; // Priority: Active Tab Context (if connection still valid) > Sidebar Selection (activeContext) if (activeTabId) { @@ -2730,6 +2733,9 @@ function App() { if (currentTab && currentTab.connectionId && connections.some(c => c.id === currentTab.connectionId)) { connId = currentTab.connectionId; db = currentTab.dbName || ''; + if (currentTab.type === 'table' || currentTab.type === 'design') { + tableName = String(currentTab.tableName || '').trim(); + } } } @@ -2739,15 +2745,24 @@ function App() { db = activeContext.dbName || ''; } + const connection = connections.find(c => c.id === connId); + const contextualQuery = tableName && connection + ? buildContextualNewQueryTemplate({ + dbType: resolveDataSourceType(connection.config), + tableName, + customTemplate: appearance.newQuerySqlTemplate, + }) + : null; + addTab({ id: `query-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, title: t('query.new'), type: 'query', connectionId: connId, dbName: db, - query: '' + query: contextualQuery ?? '' }); - }, [activeTabId, tabs, connections, activeContext, addTab, t]); + }, [activeTabId, tabs, connections, activeContext, addTab, appearance.newQuerySqlTemplate, t]); const switchActiveTabByOffset = useCallback((offset: 1 | -1) => { if (tabs.length < 2) return; diff --git a/frontend/src/utils/objectQueryTemplates.test.ts b/frontend/src/utils/objectQueryTemplates.test.ts index b20dc4e0..75d1b0b8 100644 --- a/frontend/src/utils/objectQueryTemplates.test.ts +++ b/frontend/src/utils/objectQueryTemplates.test.ts @@ -9,12 +9,47 @@ vi.mock('../../wailsjs/go/app/App', () => ({ })); import { + buildContextualNewQueryTemplate, buildTableSelectQuery, extractTableSelectColumnNames, isElasticsearchDbType, resolveTableSelectQuery, } from './objectQueryTemplates'; +describe('buildContextualNewQueryTemplate', () => { + it('builds a dialect-aware select for the active table with the default template', () => { + expect(buildContextualNewQueryTemplate({ + dbType: 'mysql', + tableName: 'Order Items', + customTemplate: null, + })).toBe('SELECT * FROM `Order Items`;'); + }); + + it('appends the active table when a custom template ends with FROM', () => { + expect(buildContextualNewQueryTemplate({ + dbType: 'postgres', + tableName: 'public.OrderItems', + customTemplate: 'SELECT id, total FROM ', + })).toBe('SELECT id, total FROM public."OrderItems"'); + }); + + it('preserves custom templates that do not expose a table insertion point', () => { + expect(buildContextualNewQueryTemplate({ + dbType: 'mysql', + tableName: 'users', + customTemplate: 'SELECT CURRENT_TIMESTAMP;', + })).toBeNull(); + }); + + it('builds the native query format for an active Elasticsearch index', () => { + expect(buildContextualNewQueryTemplate({ + dbType: 'elasticsearch', + tableName: 'orders-v1', + customTemplate: null, + })).toBe('GET /orders-v1/_search\n{\n "query": {\n "match_all": {}\n }\n}\n'); + }); +}); + describe('buildTableSelectQuery', () => { it('quotes uppercase postgres table names in new query templates', () => { expect(buildTableSelectQuery('postgres', 'public.MyTable')).toBe('SELECT * FROM public."MyTable";'); diff --git a/frontend/src/utils/objectQueryTemplates.ts b/frontend/src/utils/objectQueryTemplates.ts index 892ee4ce..7aad49e3 100644 --- a/frontend/src/utils/objectQueryTemplates.ts +++ b/frontend/src/utils/objectQueryTemplates.ts @@ -59,6 +59,34 @@ export const buildTableSelectQuery = ( return `SELECT\n ${selectList}\nFROM ${quotedTable}${limitSuffix};`; }; +type BuildContextualNewQueryTemplateOptions = { + dbType: string; + tableName: string; + customTemplate?: string | null; +}; + +/** + * Build the initial query for a new tab opened while a table-like data tab is active. + * A custom template is only augmented when it explicitly ends at a FROM insertion point. + */ +export const buildContextualNewQueryTemplate = ({ + dbType, + tableName, + customTemplate, +}: BuildContextualNewQueryTemplateOptions): string | null => { + const normalizedTableName = String(tableName || '').trim(); + if (!normalizedTableName) return null; + if (isElasticsearchDbType(dbType) || customTemplate === null || customTemplate === undefined) { + return buildTableSelectQuery(dbType, normalizedTableName); + } + + const template = String(customTemplate) + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + if (!/\bfrom\s*$/i.test(template)) return null; + return `${template}${quoteQualifiedIdent(dbType, normalizedTableName)}`; +}; + type ResolveTableSelectQueryOptions = { dbType: string; tableName: string; From 4acf16893f4ae542b1d02f9209349df6cc693f14 Mon Sep 17 00:00:00 2001 From: mango <1711456624@qq.com> Date: Fri, 7 Aug 2026 21:58:53 +0800 Subject: [PATCH 2/2] fix(query): preserve selected new query context --- frontend/src/App.tsx | 12 ++++++------ frontend/src/utils/newQueryContext.test.ts | 18 +++++++++++++++++- frontend/src/utils/newQueryContext.ts | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9a234300..1d3ba3b3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -238,7 +238,7 @@ import { import { useAppUpdateManager } from './hooks/useAppUpdateManager'; import { useAppLogPanelResize } from './hooks/useAppLogPanelResize'; import { useAppSidebarResize } from './hooks/useAppSidebarResize'; -import { resolveNewQueryContext } from './utils/newQueryContext'; +import { canInheritNewQueryTableContext, resolveNewQueryContext } from './utils/newQueryContext'; import { useAppUtilityStyles } from './hooks/useAppUtilityStyles'; import { useWorkbenchTabs } from './hooks/useWorkbenchTabs'; import { @@ -2731,11 +2731,11 @@ function App() { validConnectionIds: new Set(connections.map(connection => connection.id)), }); const connection = connections.find(c => c.id === targetContext.connectionId); - const inheritsTableContext = currentTab - && (currentTab.type === 'table' || currentTab.type === 'design') - && String(currentTab.connectionId || '').trim() === targetContext.connectionId - && String(currentTab.dbName || '').trim() === targetContext.dbName; - const tableName = inheritsTableContext ? String(currentTab.tableName || '').trim() : ''; + const inheritsTableContext = canInheritNewQueryTableContext({ + activeTab: currentTab, + targetContext, + }); + const tableName = inheritsTableContext ? String(currentTab?.tableName || '').trim() : ''; const contextualQuery = tableName && connection ? buildContextualNewQueryTemplate({ dbType: resolveDataSourceType(connection.config), diff --git a/frontend/src/utils/newQueryContext.test.ts b/frontend/src/utils/newQueryContext.test.ts index a1a188bd..bb0c8d34 100644 --- a/frontend/src/utils/newQueryContext.test.ts +++ b/frontend/src/utils/newQueryContext.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { resolveNewQueryContext } from './newQueryContext'; +import { canInheritNewQueryTableContext, resolveNewQueryContext } from './newQueryContext'; describe('resolveNewQueryContext', () => { const validConnectionIds = new Set(['conn-a', 'conn-b']); @@ -44,4 +44,20 @@ describe('resolveNewQueryContext', () => { validConnectionIds, })).toEqual({ connectionId: '', dbName: '' }); }); + + it('only inherits a table tab when it belongs to the resolved target context', () => { + const tableTab = { type: 'table', connectionId: 'conn-a', dbName: 'database_a', tableName: 'users' }; + expect(canInheritNewQueryTableContext({ + activeTab: tableTab, + targetContext: { connectionId: 'conn-a', dbName: 'database_a' }, + })).toBe(true); + expect(canInheritNewQueryTableContext({ + activeTab: tableTab, + targetContext: { connectionId: 'conn-b', dbName: 'database_b' }, + })).toBe(false); + expect(canInheritNewQueryTableContext({ + activeTab: { ...tableTab, type: 'query' }, + targetContext: { connectionId: 'conn-a', dbName: 'database_a' }, + })).toBe(false); + }); }); diff --git a/frontend/src/utils/newQueryContext.ts b/frontend/src/utils/newQueryContext.ts index 7fc7c553..05c3bf66 100644 --- a/frontend/src/utils/newQueryContext.ts +++ b/frontend/src/utils/newQueryContext.ts @@ -35,3 +35,20 @@ export const resolveNewQueryContext = ({ || normalizeValidContext(activeTab, validConnectionIds) || { connectionId: '', dbName: '' } ); +export interface NewQueryTableContextLike extends NewQueryContextLike { + type?: unknown; + tableName?: unknown; +} + +export const canInheritNewQueryTableContext = ({ + activeTab, + targetContext, +}: { + activeTab?: NewQueryTableContextLike | null; + targetContext: NewQueryContext; +}): boolean => { + const tabType = String(activeTab?.type || ''); + return (tabType === 'table' || tabType === 'design') + && String(activeTab?.connectionId || '').trim() === targetContext.connectionId + && String(activeTab?.dbName || '').trim() === targetContext.dbName; +};