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;