🐛 fix(driver): 修复驱动代理校验与 DuckDB 表预览超时

- 校验可选 driver-agent revision,避免重装后复用旧代理
- DuckDB 表预览默认不再追加兜底 ORDER BY
- 优化 DuckDB 超时中断提示并补充回归测试
This commit is contained in:
Syngnat
2026-05-06 19:32:55 +08:00
parent 3c68325132
commit da9a76715a
6 changed files with 229 additions and 9 deletions

View File

@@ -176,6 +176,46 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
it('does not add fallback ORDER BY for DuckDB table preview when a primary key is available', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-order', dbName: 'main', tableName: 'events', title: 'events' }));
const tableQueries = backendApp.DBQuery.mock.calls
.map((call: any[]) => String(call[2] || ''))
.filter((sql: string) => sql.includes('FROM "events"'));
expect(tableQueries.length).toBeGreaterThan(0);
expect(tableQueries.every((sql: string) => !/\border\s+by\b/i.test(sql))).toBe(true);
expect(tableQueries[tableQueries.length - 1]).toContain('LIMIT 101 OFFSET 0');
renderer.unmount();
});
it('shows an actionable message for DuckDB timeout interruption errors', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: false,
message: 'context deadline exceeded INTERRUPT Error: Interrupted!',
fields: [],
data: [],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-timeout', dbName: 'main', tableName: 'events', title: 'events' }));
expect(messageApi.error).toHaveBeenCalledWith('DuckDB 查询超过连接超时时间,已中断。请调大连接超时时间,或减少排序/筛选范围后重试。');
expect(storeState.addSqlLog.mock.calls.some((call: any[]) => String(call[0]?.message || '').includes('context deadline exceeded'))).toBe(true);
renderer.unmount();
});
it('keeps non-Oracle table preview read-only when no safe locator exists', async () => {
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'main';

View File

@@ -165,6 +165,20 @@ const isDuckDBComplexColumnType = (columnType?: string): boolean => {
return raw.includes('map') || raw.includes('struct') || raw.includes('union') || raw.includes('array') || raw.includes('list');
};
const formatDataViewerQueryError = (dbType: string, messageText: unknown): string => {
const rawMessage = String(messageText || '查询失败').trim() || '查询失败';
const lower = rawMessage.toLowerCase();
const isTimeout = lower.includes('context deadline exceeded') || lower.includes('deadline exceeded') || lower.includes('timeout') || lower.includes('timed out') || lower.includes('超时');
const isDuckDBInterrupted = String(dbType || '').trim().toLowerCase() === 'duckdb' && (lower.includes('interrupt error') || lower.includes('interrupted'));
if (isTimeout || isDuckDBInterrupted) {
if (String(dbType || '').trim().toLowerCase() === 'duckdb') {
return 'DuckDB 查询超过连接超时时间,已中断。请调大连接超时时间,或减少排序/筛选范围后重试。';
}
return '查询超过连接超时时间,已中断。请调大连接超时时间,或减少查询范围后重试。';
}
return rawMessage;
};
const reverseOrderBySQL = (orderBySQL: string): string => {
const raw = String(orderBySQL || '').trim();
if (!raw) return '';
@@ -929,11 +943,11 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
}
}
} else {
message.error(String(resData.message || '查询失败'));
message.error(formatDataViewerQueryError(dbTypeLower, resData.message));
}
} catch (e: any) {
if (fetchSeqRef.current !== seq) return;
message.error("Error fetching data: " + e.message);
message.error(formatDataViewerQueryError(dbTypeLower, e?.message || e));
addSqlLog({
id: `log-${Date.now()}-error`,
timestamp: Date.now(),