fix(sidebar): preserve primary keys on first table open

This commit is contained in:
vrustx authored and GitHub committed 2026-07-19 15:38:50 +08:00
1 parent 7b14ef3b48
commit 5289dc819d
11 files changed
+978 -21

No files matched your search

@@ -0,0 +1,76 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ColumnInfo, QueryResult } from "@/types/database";
function sampleResult(): QueryResult {
return { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
}
function column(name: string): ColumnInfo {
return { name, data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null };
}
describe("data tab snapshot restore vs tableMetaPending", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
setActivePinia(createPinia());
});
it("does not roll back real metadata to a placeholder snapshot and re-pends placeholder restores", async () => {
vi.doMock("@/lib/tabs/tabResultCache", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/tabs/tabResultCache")>();
return {
...actual,
// 延迟元数据期间落盘的快照:空列占位身份
readTabResultSnapshot: vi.fn(async () => ({ result: sampleResult(), tableMeta: { tableName: "users", schema: "public", columns: [], primaryKeys: [] } }) as any),
};
});
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
const tab = store.tabs.find((item) => item.id === tabId)!;
// 真实元数据已在快照落盘后到达并清除 pending
tab.tableMeta = { tableName: "users", schema: "public", columns: [column("id")], primaryKeys: ["id"] };
tab.tableMetaPending = false;
tab.resultEvicted = true;
tab.resultCacheKey = "cache-key";
await store.reloadEvictedTab(tabId);
// 快照确已恢复(防止"恢复根本没发生"导致的误通过)
expect(tab.result).toBeDefined();
expect(tab.resultEvicted).toBeUndefined();
// 快照恢复不得用占位身份回滚真实元数据(否则 pending=false + 空主键 = 编辑门控失效)
expect(tab.tableMeta?.columns.length).toBe(1);
expect(tab.tableMeta?.primaryKeys).toEqual(["id"]);
expect(tab.tableMetaPending).toBe(false);
});
it("re-pends the edit gate when a restore leaves only placeholder metadata", async () => {
vi.doMock("@/lib/tabs/tabResultCache", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/tabs/tabResultCache")>();
return {
...actual,
readTabResultSnapshot: vi.fn(async () => ({ result: sampleResult(), tableMeta: { tableName: "users", schema: "public", columns: [], primaryKeys: [] } }) as any),
};
});
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
const tab = store.tabs.find((item) => item.id === tabId)!;
// 当前也只有占位身份(真实元数据从未落地)
tab.tableMeta = { tableName: "users", schema: "public", columns: [], primaryKeys: [] };
tab.tableMetaPending = false;
tab.resultEvicted = true;
tab.resultCacheKey = "cache-key";
await store.reloadEvictedTab(tabId);
expect(tab.result).toBeDefined();
// 恢复后行标识仍未知:编辑门控必须重新挂起
expect(tab.tableMetaPending).toBe(true);
});
});
+19 -1
View File
@@ -2240,6 +2240,11 @@ export const useQueryStore = defineStore("query", () => {
if (tab) {
tab.tableMeta = meta;
tab.tableMetaUpdatedAt = Date.now();
// 只有真实元数据(columns 非空)落地才结束行标识等待;多处调用方会先写
// columns/primaryKeys 为空的占位身份(如 useNavigationTargets),不得
// 借此提前解除编辑门控。失败/中止路径不清除——标签页保持只读是安全
// 兜底,刷新或重开表会重新加载元数据恢复
if (meta.columns.length > 0) tab.tableMetaPending = false;
}
}
@@ -3827,6 +3832,9 @@ export const useQueryStore = defineStore("query", () => {
const executionId = tab.executionId;
if (!executionId) return false;
tab.isCancelling = true;
// 单调递增、不随取消结果回退:导航流程据此判断"执行期间用户请求过停止"
// (isCancelling 在取消失败或查询先完成时会被清掉,无法承担这个语义)
tab.cancelRequestCount = (tab.cancelRequestCount ?? 0) + 1;
const cancellationStartedAt = performance.now();
try {
const canceled = await withCancelQueryTimeout(api.cancelQuery(executionId));
@@ -4010,7 +4018,17 @@ export const useQueryStore = defineStore("query", () => {
tab.querySourceColumns = snapshot.querySourceColumns;
tab.queryEditabilityReason = snapshot.queryEditabilityReason;
tab.mongoEditTarget = snapshot.mongoEditTarget;
tab.tableMeta = snapshot.tableMeta;
// Data tab 快照可能是延迟元数据期间落盘的空列占位身份:不得用它回滚
// 之后已落地的真实元数据(否则 pending 已清除而行标识又变未知,编辑
// 门控失效);若恢复后确实没有真实列,重新挂起编辑门控
if (tab.tableMeta?.columns.length && !snapshot.tableMeta?.columns.length) {
// 保留当前真实元数据,忽略快照中的占位身份
} else {
tab.tableMeta = snapshot.tableMeta;
}
if (tab.mode === "data" && !tab.tableMeta?.columns.length) {
tab.tableMetaPending = true;
}
tab.resultPageSql = snapshot.resultPageSql;
tab.resultPageLimit = snapshot.resultPageLimit;
tab.resultPageOffset = snapshot.resultPageOffset;