mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 08:53:46 +08:00
✨ feat(sidebar): 表新建查询展开全部字段而非 select *
- 新建查询时通过 DBGetColumns 拉取字段并生成 SELECT 列表 - 元数据不可用时回退 SELECT *,消息队列仍保持 LIMIT 预览 - 覆盖 V2/旧版右键菜单与表总览入口,补充模板单测 Refs #710
This commit is contained in:
@@ -12,7 +12,7 @@ import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
|
||||
import { resolveTableSelectQuery } from '../utils/objectQueryTemplates';
|
||||
import {
|
||||
TABLE_OVERVIEW_RENDER_BATCH_SIZE,
|
||||
buildTableOverviewSearchIndex,
|
||||
@@ -522,15 +522,23 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
|
||||
const openQueryForTable = useCallback((tableName: string) => {
|
||||
if (!connection) return;
|
||||
setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' });
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('table_overview.menu.new_query'),
|
||||
type: 'query',
|
||||
connectionId: connection.id,
|
||||
dbName: tab.dbName,
|
||||
query: buildTableSelectQuery(metadataDialect, tableName),
|
||||
});
|
||||
void (async () => {
|
||||
setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' });
|
||||
const queryTemplate = await resolveTableSelectQuery({
|
||||
dbType: metadataDialect,
|
||||
tableName,
|
||||
dbName: String(tab.dbName || ''),
|
||||
connectionConfig: connection.config,
|
||||
});
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('table_overview.menu.new_query'),
|
||||
type: 'query',
|
||||
connectionId: connection.id,
|
||||
dbName: tab.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
})();
|
||||
}, [addTab, connection, metadataDialect, setActiveContext, t, tab.dbName]);
|
||||
|
||||
const openTableInER = useCallback((tableName: string) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ import { t } from '../../i18n';
|
||||
import { useStore } from '../../store';
|
||||
import type { SavedConnection, SavedQuery, SavedQueryGroup } from '../../types';
|
||||
import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
|
||||
import { buildTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
import { resolveTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
import {
|
||||
buildSavedQueryGroupPath,
|
||||
getSavedQueryGroupOwnerIds,
|
||||
@@ -866,14 +866,23 @@ export const buildSidebarLegacyNodeMenuItems = (
|
||||
label: t('sidebar.menu.new_query'),
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: buildTableSelectQuery('starrocks', String(node.dataRef?.tableName || node.dataRef?.viewName || ''))
|
||||
});
|
||||
void (async () => {
|
||||
const tableName = String(node.dataRef?.tableName || node.dataRef?.viewName || '');
|
||||
const queryTemplate = await resolveTableSelectQuery({
|
||||
dbType: 'starrocks',
|
||||
tableName,
|
||||
dbName: String(node.dataRef?.dbName || ''),
|
||||
connectionConfig: node.dataRef?.config,
|
||||
});
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
})();
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -964,16 +973,23 @@ export const buildSidebarLegacyNodeMenuItems = (
|
||||
label: t('sidebar.menu.new_query'),
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: () => {
|
||||
const tableName = String(node.dataRef?.tableName || '').trim();
|
||||
const queryTemplate = buildTableSelectQuery(getMetadataDialect(node.dataRef as SavedConnection), tableName);
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate
|
||||
});
|
||||
void (async () => {
|
||||
const tableName = String(node.dataRef?.tableName || '').trim();
|
||||
const queryTemplate = await resolveTableSelectQuery({
|
||||
dbType: getMetadataDialect(node.dataRef as SavedConnection),
|
||||
tableName,
|
||||
dbName: String(node.dataRef?.dbName || ''),
|
||||
connectionConfig: node.dataRef?.config,
|
||||
});
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
})();
|
||||
}
|
||||
},
|
||||
...(messagePublishTarget ? [{
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ConnectionTag, SavedConnection } from '../../types';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { normalizeConnectionEnvironmentType } from '../../utils/connectionEnvironment';
|
||||
import { buildTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
import { resolveTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
import { DBReleaseConnection } from '../../../wailsjs/go/app/App';
|
||||
import { getDbIcon } from '../DatabaseIcons';
|
||||
import { getMetadataDialect } from './sidebarMetadataLoaders';
|
||||
@@ -172,16 +172,24 @@ export const useSidebarV2ActionHandlers = ({
|
||||
openDesign(node, 'columns', false);
|
||||
return;
|
||||
case 'new-query': {
|
||||
const tableName = String(node.dataRef?.tableName || '').trim();
|
||||
const queryTemplate = buildTableSelectQuery(getMetadataDialect(node.dataRef as SavedConnection), tableName);
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
void (async () => {
|
||||
const tableName = String(node.dataRef?.tableName || '').trim();
|
||||
const dbType = getMetadataDialect(node.dataRef as SavedConnection);
|
||||
const queryTemplate = await resolveTableSelectQuery({
|
||||
dbType,
|
||||
tableName,
|
||||
dbName: String(node.dataRef?.dbName || ''),
|
||||
connectionConfig: node.dataRef?.config,
|
||||
});
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
})();
|
||||
return;
|
||||
}
|
||||
case 'publish-message':
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { buildTableSelectQuery } from './objectQueryTemplates';
|
||||
const { dbGetColumnsMock } = vi.hoisted(() => ({
|
||||
dbGetColumnsMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../wailsjs/go/app/App', () => ({
|
||||
DBGetColumns: dbGetColumnsMock,
|
||||
}));
|
||||
|
||||
import {
|
||||
buildTableSelectQuery,
|
||||
extractTableSelectColumnNames,
|
||||
resolveTableSelectQuery,
|
||||
} from './objectQueryTemplates';
|
||||
|
||||
describe('buildTableSelectQuery', () => {
|
||||
it('quotes uppercase postgres table names in new query templates', () => {
|
||||
expect(buildTableSelectQuery('postgres', 'public.MyTable')).toBe('SELECT * FROM public."MyTable";');
|
||||
});
|
||||
|
||||
it('expands provided columns into a multi-line select list', () => {
|
||||
expect(buildTableSelectQuery('mysql', 'users', ['id', 'name', 'created_at'])).toBe(
|
||||
'SELECT\n `id`,\n `name`,\n `created_at`\nFROM `users`;',
|
||||
);
|
||||
});
|
||||
|
||||
it('quotes reserved and uppercase column names for postgres', () => {
|
||||
expect(buildTableSelectQuery('postgres', 'public.orders', ['user', 'OrderID'])).toBe(
|
||||
'SELECT\n "user",\n "OrderID"\nFROM public.orders;',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a preview limit for RocketMQ topic browsing', () => {
|
||||
expect(buildTableSelectQuery('rocketmq', 'orders.events')).toBe('SELECT * FROM "orders.events" LIMIT 100;');
|
||||
});
|
||||
@@ -23,3 +47,65 @@ describe('buildTableSelectQuery', () => {
|
||||
expect(buildTableSelectQuery('rabbitmq', 'orders.events.v1')).toBe('SELECT * FROM "orders.events.v1" LIMIT 100;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTableSelectColumnNames', () => {
|
||||
it('reads mixed-case column name fields and keeps order without duplicates', () => {
|
||||
expect(extractTableSelectColumnNames([
|
||||
{ Name: 'id' },
|
||||
{ name: 'name' },
|
||||
{ COLUMN_NAME: 'name' },
|
||||
{ field: 'created_at' },
|
||||
{},
|
||||
])).toEqual(['id', 'name', 'created_at']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTableSelectQuery', () => {
|
||||
beforeEach(() => {
|
||||
dbGetColumnsMock.mockReset();
|
||||
});
|
||||
|
||||
it('loads columns and expands them into the select list', async () => {
|
||||
dbGetColumnsMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ name: 'id' },
|
||||
{ name: 'email' },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(resolveTableSelectQuery({
|
||||
dbType: 'mysql',
|
||||
tableName: 'users',
|
||||
dbName: 'app',
|
||||
connectionConfig: { type: 'mysql', host: 'localhost' },
|
||||
})).resolves.toBe('SELECT\n `id`,\n `email`\nFROM `users`;');
|
||||
|
||||
expect(dbGetColumnsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('falls back to select star when column metadata is unavailable', async () => {
|
||||
dbGetColumnsMock.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'unavailable',
|
||||
data: null,
|
||||
});
|
||||
|
||||
await expect(resolveTableSelectQuery({
|
||||
dbType: 'postgres',
|
||||
tableName: 'public.users',
|
||||
dbName: 'app',
|
||||
connectionConfig: { type: 'postgres', host: 'localhost' },
|
||||
})).resolves.toBe('SELECT * FROM public.users;');
|
||||
});
|
||||
|
||||
it('keeps message-queue templates on select star without loading columns', async () => {
|
||||
await expect(resolveTableSelectQuery({
|
||||
dbType: 'kafka',
|
||||
tableName: 'logs.app-1',
|
||||
connectionConfig: { type: 'kafka' },
|
||||
})).resolves.toBe('SELECT * FROM "logs.app-1" LIMIT 100;');
|
||||
|
||||
expect(dbGetColumnsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,95 @@
|
||||
import { quoteQualifiedIdent } from './sql';
|
||||
import { DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import { getColumnDefinitionName } from './columnDefinition';
|
||||
import { buildRpcConnectionConfig } from './connectionRpcConfig';
|
||||
import { quoteIdentPart, quoteQualifiedIdent } from './sql';
|
||||
|
||||
export const buildTableSelectQuery = (dbType: string, tableName: string): string => {
|
||||
const MESSAGE_QUEUE_DB_TYPES = new Set(['rocketmq', 'mqtt', 'kafka', 'rabbitmq']);
|
||||
|
||||
const isMessageQueueDbType = (dbType: string): boolean => (
|
||||
MESSAGE_QUEUE_DB_TYPES.has(String(dbType || '').trim().toLowerCase())
|
||||
);
|
||||
|
||||
export const extractTableSelectColumnNames = (columns: unknown): string[] => {
|
||||
if (!Array.isArray(columns)) return [];
|
||||
const names: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const column of columns) {
|
||||
const name = getColumnDefinitionName(column);
|
||||
if (!name || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
export const buildTableSelectQuery = (
|
||||
dbType: string,
|
||||
tableName: string,
|
||||
columns: string[] = [],
|
||||
): string => {
|
||||
const normalizedTableName = String(tableName || '').trim();
|
||||
if (!normalizedTableName) {
|
||||
return 'SELECT * FROM ';
|
||||
}
|
||||
if (['rocketmq', 'mqtt', 'kafka', 'rabbitmq'].includes(String(dbType || '').trim().toLowerCase())) {
|
||||
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)} LIMIT 100;`;
|
||||
|
||||
const quotedTable = quoteQualifiedIdent(dbType, normalizedTableName);
|
||||
const limitSuffix = isMessageQueueDbType(dbType) ? ' LIMIT 100' : '';
|
||||
const normalizedColumns = columns
|
||||
.map((column) => String(column || '').trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (normalizedColumns.length === 0) {
|
||||
return `SELECT * FROM ${quotedTable}${limitSuffix};`;
|
||||
}
|
||||
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)};`;
|
||||
|
||||
const selectList = normalizedColumns
|
||||
.map((column) => quoteIdentPart(dbType, column))
|
||||
.join(',\n ');
|
||||
return `SELECT\n ${selectList}\nFROM ${quotedTable}${limitSuffix};`;
|
||||
};
|
||||
|
||||
type ResolveTableSelectQueryOptions = {
|
||||
dbType: string;
|
||||
tableName: string;
|
||||
dbName?: string;
|
||||
connectionConfig?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a SELECT template for "new query" from a table/view.
|
||||
* Prefers expanding all column names; falls back to SELECT * when metadata is unavailable.
|
||||
*/
|
||||
export const resolveTableSelectQuery = async ({
|
||||
dbType,
|
||||
tableName,
|
||||
dbName = '',
|
||||
connectionConfig,
|
||||
}: ResolveTableSelectQueryOptions): Promise<string> => {
|
||||
const normalizedTableName = String(tableName || '').trim();
|
||||
if (!normalizedTableName) {
|
||||
return buildTableSelectQuery(dbType, normalizedTableName);
|
||||
}
|
||||
|
||||
// Message-queue "tables" are topics/queues; column expansion is not meaningful.
|
||||
if (isMessageQueueDbType(dbType) || !connectionConfig) {
|
||||
return buildTableSelectQuery(dbType, normalizedTableName);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await DBGetColumns(
|
||||
buildRpcConnectionConfig(connectionConfig as any) as any,
|
||||
String(dbName || ''),
|
||||
normalizedTableName,
|
||||
);
|
||||
if (res?.success && Array.isArray(res.data)) {
|
||||
const columnNames = extractTableSelectColumnNames(res.data);
|
||||
if (columnNames.length > 0) {
|
||||
return buildTableSelectQuery(dbType, normalizedTableName, columnNames);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall back to SELECT * when metadata lookup fails.
|
||||
}
|
||||
|
||||
return buildTableSelectQuery(dbType, normalizedTableName);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user