fix(informix): resolve metadata owners correctly
This commit is contained in:
10 files changed
+530
-79
No files matched your search
@@ -75,6 +75,19 @@ function genericJdbcConnection(): ConnectionConfig {
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function informixConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "informix-1",
|
||||
name: "Informix",
|
||||
db_type: "informix",
|
||||
host: "127.0.0.1",
|
||||
port: 9088,
|
||||
username: "informix",
|
||||
password: "",
|
||||
database: "prulife",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function procedure(name: string): ObjectInfo {
|
||||
return {
|
||||
name,
|
||||
@@ -2245,6 +2258,151 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(dbNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not re-expand an Informix schema collapsed while its metadata cache is being saved", async () => {
|
||||
let resolveCacheSave!: () => void;
|
||||
const saveSchemaCache = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveCacheSave = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
|
||||
|
||||
const connection = informixConnection();
|
||||
const databaseNode: TreeNode = {
|
||||
id: `${connection.id}:prulife`,
|
||||
label: "prulife",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
const schemaNode: TreeNode = {
|
||||
id: `${connection.id}:prulife:xtdpcky`,
|
||||
label: "xtdpcky",
|
||||
type: "schema",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
schema: "xtdpcky",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
databaseNode.children = [schemaNode];
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [databaseNode],
|
||||
},
|
||||
];
|
||||
|
||||
const loadPromise = store.loadTables(connection.id, "prulife", "xtdpcky", { force: true });
|
||||
await vi.waitFor(() => expect(saveSchemaCache).toHaveBeenCalledTimes(1));
|
||||
expect(saveSchemaCache.mock.calls[0]?.[0]).toBe(`${connection.id}:prulife:xtdpcky:objects-grouped-v7-informix-owner-v2`);
|
||||
expect(schemaNode.isLoading).toBe(true);
|
||||
|
||||
schemaNode.isExpanded = false;
|
||||
store.cancelTreeNodeLoad(schemaNode.id);
|
||||
resolveCacheSave();
|
||||
await loadPromise;
|
||||
|
||||
expect(schemaNode.isLoading).toBe(false);
|
||||
expect(schemaNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let an Informix schema load reclaim ownership after collapse during a health check", async () => {
|
||||
let resolveHealthCheck!: () => void;
|
||||
const checkConnectionHealth = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveHealthCheck = resolve;
|
||||
}),
|
||||
);
|
||||
const saveSchemaCache = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth,
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
|
||||
|
||||
const connection = informixConnection();
|
||||
const schemaNode: TreeNode = {
|
||||
id: `${connection.id}:prulife:xtdpcky`,
|
||||
label: "xtdpcky",
|
||||
type: "schema",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
schema: "xtdpcky",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:prulife`,
|
||||
label: "prulife",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
isExpanded: true,
|
||||
children: [schemaNode],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const loadPromise = store.loadTables(connection.id, "prulife", "xtdpcky", { force: true });
|
||||
await vi.waitFor(() => expect(checkConnectionHealth).toHaveBeenCalledTimes(1));
|
||||
expect(schemaNode.isLoading).toBe(true);
|
||||
|
||||
schemaNode.isExpanded = false;
|
||||
store.cancelTreeNodeLoad(schemaNode.id);
|
||||
resolveHealthCheck();
|
||||
await loadPromise;
|
||||
|
||||
expect(saveSchemaCache).not.toHaveBeenCalled();
|
||||
expect(schemaNode.isLoading).toBe(false);
|
||||
expect(schemaNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not apply load-more results after the parent generation is invalidated", async () => {
|
||||
const firstPage = Array.from({ length: 201 }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
|
||||
@@ -1372,6 +1372,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return parts.map((part) => encodeURIComponent(part)).join(":");
|
||||
}
|
||||
|
||||
function ownerAwareMetadataCacheVersion(config: ConnectionConfig | undefined, version: string): string {
|
||||
return config?.db_type === "informix" ? `${version}-informix-owner-v2` : version;
|
||||
}
|
||||
|
||||
function supportedSidebarObjectTypes(config?: ConnectionConfig): DatabaseObjectTreeKind[] {
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
return sidebarObjectKindsForDatabase(dbType);
|
||||
@@ -1401,7 +1405,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function objectGroupCacheKey(node: TreeNode): string {
|
||||
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||
const cacheVersion = config?.db_type === "oracle" ? "objects-v7" : "objects-v6";
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "oracle" ? "objects-v7" : "objects-v6");
|
||||
return schemaCacheKey(node.connectionId || "", node.database || "", node.schema || "", node.type, cacheVersion);
|
||||
}
|
||||
|
||||
@@ -1917,7 +1921,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (parent.type === "group-tables") return objectGroupCacheKey(parent);
|
||||
if (parent.type !== "database" && parent.type !== "schema" && parent.type !== "linked-server-schema") return null;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
return schemaCacheKey(parent.connectionId, parent.database, parent.schema || "", simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(getConfig(parent.connectionId), simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
return schemaCacheKey(parent.connectionId, parent.database, parent.schema || "", cacheVersion);
|
||||
}
|
||||
|
||||
function sidebarTableSearchIndexCacheKey(parent: TreeNode): string | null {
|
||||
@@ -3659,8 +3664,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const showSystemSchemas = getConfig(connectionId)?.show_system_schemas === true;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, "schemas-v3", showSystemSchemas ? "show-system" : "hide-system");
|
||||
const config = getConfig(connectionId);
|
||||
const showSystemSchemas = config?.show_system_schemas === true;
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, "schemas-v3");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, cacheVersion, showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
@@ -3709,7 +3716,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!targetNode) return;
|
||||
setChildren(targetNode, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
@@ -3748,7 +3756,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!targetNode) return;
|
||||
setChildren(targetNode, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
@@ -4005,7 +4014,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
@@ -4032,7 +4042,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
});
|
||||
if (!options?.force && simpleObjectDisplayForScope && !searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilterForScope) {
|
||||
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "objects-simple-v6");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", ownerAwareMetadataCacheVersion(configForScope, "objects-simple-v6"));
|
||||
if (await hydrateTreeNodeFromCache(findNode(treeNodes.value, nodeId), cacheKey)) {
|
||||
void loadTables(connectionId, database, schema, { ...options, force: true }).catch(() => undefined);
|
||||
return;
|
||||
@@ -4063,9 +4073,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
const config = getConfig(connectionId);
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", cacheVersion);
|
||||
const querySchema = connectionObjectTreeQuerySchema(config, database, schema);
|
||||
const effectiveSchema = connectionObjectTreeNodeSchema(config, database, schema);
|
||||
const tableNameFilter = activeTableNameFilterForScope({
|
||||
@@ -4123,10 +4134,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!searchFilter && !isSidebarTableSearch && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (!currentTargetNode) return;
|
||||
currentTargetNode.isExpanded = true;
|
||||
if (simpleObjectDisplay && !searchFilter && !isSidebarTableSearch && nonTableObjectTypes.length > 0) {
|
||||
void loadSimpleSupplementalObjectChildren({
|
||||
node: targetNode,
|
||||
node: currentTargetNode,
|
||||
nodeId,
|
||||
connectionId,
|
||||
database,
|
||||
@@ -4258,7 +4271,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!searchFilter && !isSidebarTableSearch && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(node.connectionId, e, load);
|
||||
throw e;
|
||||
@@ -4323,8 +4337,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const nextChildren = page.hasMore ? appendTableTreeLoadMoreNode(mergedChildren, buildLoadMoreNode(targetParent, page.nextOffset, loadMore.pageSize), page.loadMoreParent) : mergedChildren;
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", "objects-simple-v6"), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", ownerAwareMetadataCacheVersion(config, "objects-simple-v6")), nextChildren);
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
}
|
||||
const objectTypes = objectTypesForGroupNode(parent.type);
|
||||
@@ -4374,7 +4389,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
}
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
@@ -4382,7 +4398,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(parentConnectionId, e, load);
|
||||
throw e;
|
||||
@@ -6695,8 +6712,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
function cancelTreeNodeLoad(nodeId: string): void {
|
||||
// Supersede any in-flight loader for this node so a collapse issued while
|
||||
// the load is still running (or a loader that never resolves) cannot
|
||||
// re-expand the node via its trailing `targetNode.isExpanded = true`.
|
||||
treeNodeLoads.invalidatePrefix(nodeId);
|
||||
// reclaim ownership after connection recovery or re-expand the node.
|
||||
treeNodeLoads.cancelPrefix(nodeId);
|
||||
const node = findNode(treeNodes.value, nodeId);
|
||||
if (node) node.isLoading = false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user