mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-15 07:16:52 +08:00
fix(navigation): retry table opens without the row locator on rejection
Navigation-target table opens eagerly inject the PostgreSQL tuple locator (tableoid/ctid/xmin) for keyless base tables, but unlike the sidebar open and data-tab refresh flows they had no rejection fallback. Engines without system-column support (Hologres, #12647) failed every such open outright while views kept working. Reuse the same degradation contract: retry once without the locator and remember the connection.
This commit is contained in:
@@ -354,4 +354,36 @@ describe("useNavigationTargets openTableTarget", () => {
|
||||
// 无取消请求:元数据落地后按既有行为执行第二次查询
|
||||
expect(mocks.executeTabSql).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries without the synthetic row-id locator when the engine rejects system columns (Hologres, #12647)", async () => {
|
||||
const { resetRowIdLocatorStateForTests, rowIdLocatorUnsupported } = await import("@/lib/table/tableRowIdSupport");
|
||||
resetRowIdLocatorStateForTests();
|
||||
expect(rowIdLocatorUnsupported("connection-1")).toBe(false);
|
||||
// Hologres-like: keyless PostgreSQL base table; the engine rejects every
|
||||
// locator-bearing SELECT (tableoid/ctid/xmin do not exist there).
|
||||
mocks.databaseType = "postgres";
|
||||
mocks.getColumns.mockResolvedValue([{ ...column("payload"), is_primary_key: false }]);
|
||||
mocks.listIndexes.mockResolvedValue([]);
|
||||
mocks.buildTableSelectSql.mockImplementation(async ({ tableName, includeRowId }: { tableName: string; includeRowId?: boolean }) => (includeRowId ? `SELECT tableoid::text || ':' || ctid::text || ':' || xmin::text AS "__DBX_ROWID", * FROM ${tableName}` : `SELECT * FROM ${tableName}`));
|
||||
mocks.executeTabSql.mockImplementation(async (id: string, sql?: string) => {
|
||||
const tab = mocks.tabs.find((item) => item.id === id);
|
||||
if (!tab || typeof sql !== "string") return;
|
||||
if (sql.includes("__DBX_ROWID")) {
|
||||
tab.result = { columns: ["db error"], rows: [['ERROR: column "tableoid" does not exist']], affected_rows: 0, execution_time_ms: 1, execution_error: true } as QueryTab["result"];
|
||||
} else {
|
||||
tab.result = { columns: ["payload"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 } as QueryTab["result"];
|
||||
}
|
||||
});
|
||||
|
||||
await useNavigationTargets(dialogs).openTableTarget({ connectionId: "connection-1", database: "app", tableName: "users", tableType: "TABLE" });
|
||||
|
||||
const executedSql = mocks.executeTabSql.mock.calls.map((call) => String(call[1]));
|
||||
expect(executedSql.some((sql) => sql.includes("__DBX_ROWID"))).toBe(true);
|
||||
// Same degradation contract as the sidebar open path: replay once without
|
||||
// the locator so the table still opens (read-only), then remember the
|
||||
// connection so later opens skip the locator up front.
|
||||
expect(executedSql.some((sql) => !sql.includes("__DBX_ROWID") && sql.includes("FROM users"))).toBe(true);
|
||||
expect(rowIdLocatorUnsupported("connection-1")).toBe(true);
|
||||
resetRowIdLocatorStateForTests();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { buildTableSelectSql, requiresEagerTableMetadataForDataOpen } from "@/li
|
||||
import { resolveTableDefaultSort, applyTableDefaultSortResult } from "@/lib/table/tableDefaultSort";
|
||||
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
|
||||
import { editableRowIdentifierColumns, physicalTablePrimaryKeys, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
|
||||
import { rowIdLocatorUnsupported } from "@/lib/table/tableRowIdSupport";
|
||||
import { markRowIdLocatorUnsupported, rowIdLocatorUnsupported, shouldKeepRowIdLocatorDisabled, shouldRetryWithoutRowIdLocator } from "@/lib/table/tableRowIdSupport";
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { beginDataTabNavigation, endDataTabNavigation, isCurrentDataTabNavigation } from "@/lib/tabs/dataTabNavigationGeneration";
|
||||
@@ -220,7 +220,7 @@ async function openTableTarget(target: NavigationTarget, options: NavigationOpen
|
||||
const eagerPrimaryKeys = eagerMetadata?.metadata.primaryKeys ?? [];
|
||||
const eagerIncludeRowId = Boolean(eagerMetadata) && shouldIncludeSyntheticRowId(effectiveDbType, eagerPrimaryKeys, targetTableType) && !rowIdLocatorUnsupported(target.connectionId);
|
||||
const defaultSort = resolveTableDefaultSort(settingsStore.editorSettings, effectiveDbType, physicalTablePrimaryKeys(eagerColumns, eagerMetadata?.metadata.indexes, effectiveDbType), identifierQuote);
|
||||
const sql = await buildTableSelectSql({
|
||||
const tableSelectSqlOptions = {
|
||||
databaseType: effectiveDbType,
|
||||
driverProfile: config.driver_profile,
|
||||
serverVersion: connectionStore.getConfig(target.connectionId)?.database_info?.productVersion,
|
||||
@@ -238,7 +238,8 @@ async function openTableTarget(target: NavigationTarget, options: NavigationOpen
|
||||
...tableDataLargeValuePreviewOptions(effectiveDbType, eagerColumns, eagerPrimaryKeys, pageLimit, config.db_type === "jdbc"),
|
||||
whereInput: target.whereInput,
|
||||
limit: pageLimit,
|
||||
});
|
||||
};
|
||||
const sql = await buildTableSelectSql(tableSelectSqlOptions);
|
||||
if (!isPreparationCurrent() || !isCurrentGeneration()) return;
|
||||
queryStore.updateSql(tabId, sql);
|
||||
queryStore.setTableMeta(tabId, {
|
||||
@@ -265,6 +266,19 @@ async function openTableTarget(target: NavigationTarget, options: NavigationOpen
|
||||
const cancelRequestedDuringExecute = (tabAfterFirstExecute?.cancelRequestCount ?? 0) > cancelCountBeforeExecute;
|
||||
const firstQueryFailed = cancelRequestedDuringExecute || tabAfterFirstExecute?.isCancelling === true || (firstResult !== undefined && isQueryExecutionErrorResult(firstResult));
|
||||
if (!firstQueryFailed && targetTab) applyTableDefaultSortResult(targetTab, defaultSort, queryStore.sortTabResultLocally);
|
||||
// 引擎拒绝合成行定位器(Hologres 等无系统列的 PG 兼容引擎拒 tableoid/
|
||||
// ctid/xmin,#12647):首查 SQL 带 __DBX_ROWID 而失败时,去掉定位器重试
|
||||
// 一次,表仍可打开(只读);成功后记住该连接,后续打开直接跳过定位器。
|
||||
// 与侧边栏打开/数据页签刷新路径同一降级契约;用户主动停止不重试。
|
||||
if (!cancelRequestedDuringExecute && tabAfterFirstExecute?.isCancelling !== true && shouldRetryWithoutRowIdLocator(sql, firstResult)) {
|
||||
const fallbackSql = await buildTableSelectSql({ ...tableSelectSqlOptions, includeRowId: false });
|
||||
if (!isCurrentTarget() || !isCurrentGeneration()) return;
|
||||
queryStore.updateSql(tabId, fallbackSql);
|
||||
await queryStore.executeTabSql(tabId, fallbackSql, { pagination: { limit: pageLimit, offset: 0 } });
|
||||
if (!isCurrentTarget()) return;
|
||||
const retryTab = queryStore.tabs.find((tab) => tab.id === tabId);
|
||||
if (shouldKeepRowIdLocatorDisabled(retryTab?.result)) markRowIdLocatorUnsupported(target.connectionId);
|
||||
}
|
||||
// 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 +
|
||||
|
||||
Reference in New Issue
Block a user