From 112fce1050b68add6ed8d878f7d0d4380ea0a303 Mon Sep 17 00:00:00 2001 From: jischeng <49861575+jischeng@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:42:05 +0800 Subject: [PATCH] fix(data-tab): fall back to LIMIT 0 preview for snapshot-less lake tables (#2197) Lake/external tables (e.g. Paimon in StarRocks) return 'There is currently no snapshot' on data reads when no snapshot exists yet, while metadata reads (DESC/SHOW CREATE) still succeed. Opening such a table surfaced the raw server error because executeTabSql stores the failure as an 'Error' result and never reached the column fetch. Detect that error result and retry the preview with LIMIT 0, which returns the table structure (columns + empty grid). Skip the synthetic-row-id re-query on this path since it is another data read that would fail the same way. --- .../src/composables/useNavigationTargets.ts | 21 +++++++++- .../lib/__tests__/queryResultError.spec.ts | 41 +++++++++++++++++++ apps/desktop/src/lib/queryResultError.ts | 13 ++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/lib/__tests__/queryResultError.spec.ts create mode 100644 apps/desktop/src/lib/queryResultError.ts 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] ?? "")); +}