fix(sidebar): preserve primary keys on first table open
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user