🐛 fix(query): 修正新建查询未引用 PostgreSQL 大写表名

- 抽取表查询模板 helper 并统一复用方言标识符引用逻辑
- 修正 Sidebar 与 TableOverview 的表节点新建查询入口
- 补充前端回归测试并更新 issue backlog 记录

Fixes #349
This commit is contained in:
Syngnat
2026-04-17 13:30:07 +08:00
parent d57081ecfb
commit 8a10519f9b
5 changed files with 37 additions and 7 deletions

View File

@@ -37,6 +37,7 @@
| #343 | redis删除hash类型中的key报错 | Fixed | Pending |
| #346 | TDEngine只显示子表不显示超级表 | Fixed | Pending |
| #348 | [Bug] sql查询同名字段结果集不会自动添加别名 | Fixed | Pending |
| #349 | [Bug] postgres对于表名大小写敏感且为大写时通过选中表右键新建查询时生成的sql语句没有自动带上引号"" | Fixed | Pending |
| #351 | 为什么没有截断和清空表的功能呀? | Fixed | Pending |
## Notes
@@ -137,6 +138,12 @@
- 处理:为 `scanRows` 增加稳定列名归一化逻辑。首次出现保留原名,重复列自动追加 `_2``_3` 后缀;空列名回退为 `column_N`。返回的列列表和每行数据统一使用同一套唯一列名,避免覆盖。
- 验证:新增 `internal/db/scan_rows_test.go` 回归测试,覆盖重复列 `id/id/name` 自动归一化为 `id/id_2/name` 且两列值均保留,并执行 `go test ./internal/db -run TestScanRowsRenamesDuplicateColumns -count=1``go test ./internal/db -count=1`
### #349
- 根因:表节点“新建查询”模板在 Sidebar 与 TableOverview 两处都直接拼接 `SELECT * FROM ${tableName};`,没有复用现有的标识符引用逻辑。对 PostgreSQL 这类未加引号会把标识符折叠为小写的数据库,遇到大写表名时生成的 SQL 会直接指向错误对象。
- 处理:抽出统一的 `buildTableSelectQuery` helper内部复用 `quoteQualifiedIdent` 按数据库方言生成表引用;并将 Sidebar、TableOverview 的三个“新建查询”入口统一接到该 helper保证 PostgreSQL/Kingbase 等方言在大写或特殊字符表名场景下自动补双引号。
- 验证:新增 `frontend/src/utils/objectQueryTemplates.test.ts` 回归测试,覆盖 PostgreSQL `public.MyTable` 自动生成 `SELECT * FROM public.\"MyTable\";`,并执行 `frontend``npm exec vitest run src/utils/objectQueryTemplates.test.ts``npm run build`
### #330
- 根因:查询结果表格已经支持拖拽调整列宽,但 resize handle 没有提供双击自适应逻辑,导致用户只能靠手工拖拽慢慢试宽度。

View File

@@ -45,6 +45,7 @@ import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDa
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import FindInDatabaseModal from './FindInDatabaseModal';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
const { Search } = Input;
@@ -3559,7 +3560,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
icon: <ConsoleSqlOutlined />,
onClick: () => {
const tableName = String(node.dataRef?.tableName || '').trim();
const queryTemplate = tableName ? `SELECT * FROM ${tableName};` : 'SELECT * FROM ';
const queryTemplate = buildTableSelectQuery(getMetadataDialect(node.dataRef as SavedConnection), tableName);
addTab({
id: `query-${Date.now()}`,
title: `新建查询`,

View File

@@ -7,6 +7,7 @@ import type { TabData } from '../types';
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
interface TableOverviewProps {
tab: TabData;
@@ -153,6 +154,10 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
const [viewMode, setViewMode] = useState<ViewMode>('list');
const connection = useMemo(() => connections.find(c => c.id === tab.connectionId), [connections, tab.connectionId]);
const metadataDialect = useMemo(
() => getMetadataDialect(connection?.config?.type || '', connection?.config?.driver),
[connection?.config?.driver, connection?.config?.type]
);
const autoFetchVisible = useAutoFetchVisibility();
const loadData = useCallback(async () => {
@@ -167,11 +172,10 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
useSSH: connection.config.useSSH || false,
ssh: connection.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' },
};
const dialect = getMetadataDialect(connection.config.type, connection.config.driver);
const sql = buildTableStatusSQL(dialect, tab.dbName || '', (tab as any).schemaName);
const sql = buildTableStatusSQL(metadataDialect, tab.dbName || '', (tab as any).schemaName);
const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', sql);
if (res.success && Array.isArray(res.data)) {
setTables(parseTableStats(dialect, res.data));
setTables(parseTableStats(metadataDialect, res.data));
} else {
message.error('获取表信息失败: ' + (res.message || '未知错误'));
}
@@ -180,7 +184,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
} finally {
setLoading(false);
}
}, [connection, tab.dbName]);
}, [connection, metadataDialect, tab.dbName]);
useEffect(() => {
if (!autoFetchVisible) {
@@ -471,7 +475,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
type: 'query',
connectionId: tab.connectionId,
dbName: tab.dbName,
query: `SELECT * FROM ${t.name};`,
query: buildTableSelectQuery(metadataDialect, t.name),
});
}},
{ type: 'divider' },
@@ -557,7 +561,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
type: 'query',
connectionId: tab.connectionId,
dbName: tab.dbName,
query: `SELECT * FROM ${t.name};`,
query: buildTableSelectQuery(metadataDialect, t.name),
});
}},
{ type: 'divider' },

View File

@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest';
import { buildTableSelectQuery } from './objectQueryTemplates';
describe('buildTableSelectQuery', () => {
it('quotes uppercase postgres table names in new query templates', () => {
expect(buildTableSelectQuery('postgres', 'public.MyTable')).toBe('SELECT * FROM public."MyTable";');
});
});

View File

@@ -0,0 +1,9 @@
import { quoteQualifiedIdent } from './sql';
export const buildTableSelectQuery = (dbType: string, tableName: string): string => {
const normalizedTableName = String(tableName || '').trim();
if (!normalizedTableName) {
return 'SELECT * FROM ';
}
return `SELECT * FROM ${quoteQualifiedIdent(dbType, normalizedTableName)};`;
};