diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f135d0cd..1d3ba3b3 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'; @@ -236,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 { @@ -2728,6 +2730,19 @@ function App() { activeTab: currentTab, validConnectionIds: new Set(connections.map(connection => connection.id)), }); + const connection = connections.find(c => c.id === targetContext.connectionId); + const inheritsTableContext = canInheritNewQueryTableContext({ + activeTab: currentTab, + targetContext, + }); + const tableName = inheritsTableContext ? String(currentTab?.tableName || '').trim() : ''; + 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)}`, @@ -2735,9 +2750,9 @@ function App() { type: 'query', connectionId: targetContext.connectionId, dbName: targetContext.dbName, - 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/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; +}; 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;