diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index 743820e80..df9e8aed7 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -1,5 +1,6 @@ import * as api from "@/lib/api"; import { effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/jdbcDialect"; +import { isNoSnapshotErrorResult } from "@/lib/queryResultError"; import { buildTableSelectSql } from "@/lib/tableSelectSql"; import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/tableEditing"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -92,6 +93,24 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab primaryKeys: [], }); await queryStore.executeTabSql(tabId, sql); + // executeTabSql surfaces query failures as an "Error" result instead of throwing. + // A snapshot-less lake table fails the data preview above but its metadata still + // reads fine — retry with LIMIT 0 so the user sees the table structure (columns + + // empty grid) rather than a cryptic server error. The flag also skips the + // synthetic-row-id re-query below, which is another data read that would fail + // the same way on a snapshot-less table. + const fellBackToLimitZero = isNoSnapshotErrorResult(queryStore.tabs.find((tab) => tab.id === tabId)?.result); + if (fellBackToLimitZero) { + const emptySql = await buildTableSelectSql({ + databaseType: effectiveDbType, + schema: target.schema, + tableName: target.tableName, + whereInput: target.whereInput, + limit: 0, + }); + queryStore.updateSql(tabId, emptySql); + await queryStore.executeTabSql(tabId, emptySql); + } try { const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName).catch(() => []); @@ -104,7 +123,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab columns, primaryKeys, }); - if (useRowId || config.db_type === "tdengine") { + if (!fellBackToLimitZero && (useRowId || config.db_type === "tdengine")) { const newSql = await buildTableSelectSql({ databaseType: effectiveDbType, schema: target.schema, diff --git a/apps/desktop/src/lib/__tests__/queryResultError.spec.ts b/apps/desktop/src/lib/__tests__/queryResultError.spec.ts new file mode 100644 index 000000000..43263f479 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/queryResultError.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import type { QueryResult } from "@/types/database"; + +import { isNoSnapshotErrorResult } from "@/lib/queryResultError"; + +function errorResult(message: string): QueryResult { + return { columns: ["Error"], rows: [[message]], affected_rows: 0, execution_time_ms: 0 }; +} + +function dataResult(columns: string[]): QueryResult { + return { columns, rows: [], affected_rows: 0, execution_time_ms: 0 }; +} + +describe("isNoSnapshotErrorResult", () => { + it("matches the StarRocks Paimon no-snapshot error surfaced via executeTabSql", () => { + const result = errorResult("Server error: `ERROR HY000 (1064): There is currently no snapshot.`"); + expect(isNoSnapshotErrorResult(result)).toBe(true); + }); + + it("matches case-insensitively", () => { + const result = errorResult("there IS currently NO snapshot"); + expect(isNoSnapshotErrorResult(result)).toBe(true); + }); + + it("does not match unrelated query errors", () => { + expect(isNoSnapshotErrorResult(errorResult("Unknown table 'tag_test.record_tag_t'"))).toBe(false); + expect(isNoSnapshotErrorResult(errorResult("ERROR 1142: SELECT command denied"))).toBe(false); + }); + + it("does not match successful data results", () => { + expect(isNoSnapshotErrorResult(dataResult(["id", "name"]))).toBe(false); + expect(isNoSnapshotErrorResult(dataResult(["Error"]))).toBe(false); // data column literally named Error, no rows + }); + + it("returns false for missing or empty results", () => { + expect(isNoSnapshotErrorResult(undefined)).toBe(false); + expect(isNoSnapshotErrorResult(null)).toBe(false); + expect(isNoSnapshotErrorResult({ ...errorResult("There is currently no snapshot."), rows: [] })).toBe(false); + }); +}); diff --git a/apps/desktop/src/lib/queryResultError.ts b/apps/desktop/src/lib/queryResultError.ts new file mode 100644 index 000000000..e15d0b999 --- /dev/null +++ b/apps/desktop/src/lib/queryResultError.ts @@ -0,0 +1,13 @@ +import type { QueryResult } from "@/types/database"; + +// Lake/external tables (e.g. Paimon in StarRocks) return this error on a data +// read when no snapshot exists yet, while metadata reads (DESC/SHOW CREATE) +// still succeed. executeTabSql surfaces query failures as an "Error" result, so +// callers can detect this case and fall back to a structure-only (LIMIT 0) +// preview instead of showing a cryptic server error. +const NO_SNAPSHOT_ERROR_PATTERN = /there is currently no snapshot/i; + +export function isNoSnapshotErrorResult(result: QueryResult | undefined | null): boolean { + if (!result || !result.columns.includes("Error") || result.rows.length === 0) return false; + return NO_SNAPSHOT_ERROR_PATTERN.test(String(result.rows[0]?.[0] ?? "")); +}