fix(informix): resolve metadata owners correctly

This commit is contained in:
hb authored and GitHub committed 2026-08-02 09:04:30 +08:00
1 parent 4ee1f2b946
commit b5206415ae
10 files changed
+530 -79

No files matched your search

@@ -3,6 +3,7 @@ import type { ConnectionConfig } from "@/types/database";
import {
GAUSSDB_M_JDBC_DRIVER_CLASS,
connectionObjectTreeNodeSchema,
connectionObjectTreeQuerySchema,
connectionQueryExecutionSchema,
connectionShouldLoadIdentifierQuote,
connectionUsesDatabaseObjectTreeMode,
@@ -223,4 +224,22 @@ describe("object tree node schema", () => {
it("uses the SQLite database alias to qualify attached tables", () => {
expect(connectionObjectTreeNodeSchema({ db_type: "sqlite" }, "analytics")).toBe("analytics");
});
it("keeps unqualified Informix metadata on the login owner", () => {
expect(connectionObjectTreeQuerySchema({ db_type: "informix" }, "prulife")).toBe("");
expect(connectionObjectTreeNodeSchema({ db_type: "informix" }, "prulife")).toBeUndefined();
});
it.each([
{ db_type: "jdbc" as const, connection_string: "jdbc:informix-sqli://localhost:9088/prulife" },
{ db_type: "gbase" as const, driver_profile: "gbase8s" },
])("keeps compatible Informix metadata on the login owner", (connection) => {
expect(connectionObjectTreeQuerySchema(connection, "prulife")).toBe("");
expect(connectionObjectTreeNodeSchema(connection, "prulife")).toBeUndefined();
});
it("preserves explicit Informix owners", () => {
expect(connectionObjectTreeQuerySchema({ db_type: "informix" }, "prulife", "xtdpcky")).toBe("xtdpcky");
expect(connectionObjectTreeNodeSchema({ db_type: "informix" }, "prulife", "xtdpcky")).toBe("xtdpcky");
});
});
@@ -162,6 +162,7 @@ export function connectionQueryExecutionSchema(connection: JdbcDialectConnection
export function connectionObjectTreeQuerySchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string {
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return "";
if (effectiveDatabaseTypeForConnection(connection) === "informix") return schema || "";
return schema || database;
}
@@ -176,6 +177,7 @@ export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection
if (connectionUsesDatabaseObjectTreeMode(connection)) return undefined;
if (schema) return schema;
const type = effectiveDatabaseTypeForConnection(connection);
if (type === "informix") return undefined;
if (type === "sqlite") return database;
return isSchemaAware(type) ? database : undefined;
}
@@ -46,6 +46,20 @@ describe("TreeNodeLoadRegistry", () => {
).toBe(db);
});
it("does not let a user-cancelled load reclaim ownership", () => {
const registry = new TreeNodeLoadRegistry();
const node: TreeNodeLike = { id: "c1:db:schema", connectionId: "c1", isLoading: false, children: [] };
const load = registry.begin(node);
registry.cancelPrefix("c1:db");
node.isLoading = false;
const reclaimed = load.reclaim(node);
expect(reclaimed).toBe(load);
expect(reclaimed.isCurrent()).toBe(false);
expect(node.isLoading).toBe(false);
});
it("invalidates pruned descendant generations even when root no longer contains them", () => {
const registry = new TreeNodeLoadRegistry();
const db: TreeNodeLike = { id: "c1:db", connectionId: "c1", isLoading: false, children: [] };
@@ -28,12 +28,13 @@ export type TreeNodeLoadEpoch = {
export class TreeNodeLoadRegistry {
private readonly generations = new Map<string, number>();
private readonly cancellationGenerations = new Map<string, number>();
begin(node: TreeNodeLike): TreeNodeLoadHandle {
const generation = (this.generations.get(node.id) ?? 0) + 1;
this.generations.set(node.id, generation);
node.isLoading = true;
return this.createHandle(node.id, generation);
return this.createHandle(node.id, generation, this.cancellationGenerations.get(node.id) ?? 0);
}
/** Snapshot the current generation without claiming ownership (no spinner). */
@@ -61,6 +62,22 @@ export class TreeNodeLoadRegistry {
}
}
/**
* User cancellation is stronger than ordinary invalidation: a load waiting for
* connection recovery must not reclaim ownership after its node was collapsed.
*/
cancelPrefix(nodeId: string): void {
const prefix = `${nodeId}:`;
const affectedIds = new Set<string>([nodeId]);
for (const id of this.generations.keys()) {
if (id.startsWith(prefix)) affectedIds.add(id);
}
for (const id of affectedIds) {
this.cancellationGenerations.set(id, (this.cancellationGenerations.get(id) ?? 0) + 1);
}
this.invalidatePrefix(nodeId);
}
/** Bump recorded generations for descendants only (not the parent load itself). */
invalidateDescendants(parentId: string): void {
const prefix = `${parentId}:`;
@@ -90,7 +107,7 @@ export class TreeNodeLoadRegistry {
return this.generations.get(nodeId) === generation;
}
private createHandle(nodeId: string, generation: number): TreeNodeLoadHandle {
private createHandle(nodeId: string, generation: number, cancellationGeneration: number): TreeNodeLoadHandle {
const registry = this;
return {
nodeId,
@@ -99,6 +116,9 @@ export class TreeNodeLoadRegistry {
return registry.isCurrent(nodeId, generation);
},
reclaim(node: TreeNodeLike) {
if (!registry.isCurrent(nodeId, generation) && (registry.cancellationGenerations.get(nodeId) ?? 0) !== cancellationGeneration) {
return this;
}
if (node.id !== nodeId) {
return registry.begin(node);
}