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.
This commit is contained in:
jischeng 2026-06-30 18:42:05 +08:00 committed by GitHub
parent 0777e4aad1
commit 112fce1050
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 1 deletions

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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] ?? ""));
}