fix(oceanbase): localize explain plan estimated time label

This commit is contained in:
efeire authored and GitHub committed 2026-09-20 23:00:59 +08:00
1 parent ce2d270132
commit 98b7a9ed75
18 files changed
+119 -16

No files matched your search

@@ -37,7 +37,7 @@ describe("OceanBase Oracle explain plan", () => {
expect(plan.raw).toEqual(JSON_PLAN);
expect(nodes.map((node) => node.id)).toEqual(["0", "1", "3", "2"]);
expect(nodes.map((node) => node.title)).toEqual(["HASH JOIN", "SUBPLAN SCAN on T1", "EXPRESSION", "TABLE FULL SCAN on T2"]);
expect(nodes[0]).toMatchObject({ rows: "1", details: ["Estimated time: 5 µs", "output: output([T1.C1], [T2.C1])"] });
expect(nodes[0]).toMatchObject({ rows: "1", estimatedTimeUs: "5", details: ["output: output([T1.C1], [T2.C1])"] });
expect(nodes[1]).toMatchObject({ relation: "T1", rows: "2" });
expect(nodes[3].details).toContain("filter: filter([T2.C1 > 4])");
expect(nodes[0].cost).toBeUndefined();
@@ -50,4 +50,24 @@ describe("OceanBase Oracle explain plan", () => {
nodes: [],
});
});
it("preserves zero estimates and sorts numbered children without exposing structural fields", () => {
const plan = parseExplainResult(
"oceanbase-oracle",
explainResult(
JSON.stringify({
ID: 0,
OPERATOR: "HASH JOIN",
"EST.TIME(us)": 0,
COST: 7,
CHILD_10: { ID: 10, OPERATOR: "TABLE FULL SCAN", "EST.TIME(us)": null },
CHILD_2: { ID: 2, OPERATOR: "TABLE FULL SCAN", filter: "C1 > 4" },
}),
),
);
expect(plan.nodes[0]).toMatchObject({ estimatedTimeUs: "0", cost: "7", details: [] });
expect(plan.nodes[0].children.map((node) => node.id)).toEqual(["2", "10"]);
expect(plan.nodes[0].children[0]).toMatchObject({ estimatedTimeUs: undefined, details: ["filter: C1 > 4"] });
expect(plan.nodes[0].children[1]).toMatchObject({ estimatedTimeUs: undefined, details: [] });
});
});
+19 -9
View File
@@ -12,6 +12,7 @@ export interface ExplainPlanNode {
cost?: string;
rows?: string;
width?: string;
estimatedTimeUs?: string;
details: string[];
children: ExplainPlanNode[];
}
@@ -26,6 +27,11 @@ export interface ParsedExplainPlan {
export type BuildExplainSqlResult = { ok: true; sql: string } | { ok: false; reason: "unsupported" | "empty" | "unsafe" };
export function formatExplainPlanDetails(node: ExplainPlanNode | undefined, estimatedTimeLabel: string): string[] {
if (!node) return [];
return node.estimatedTimeUs === undefined ? node.details : [`${estimatedTimeLabel}: ${node.estimatedTimeUs} µs`, ...node.details];
}
const SUPPORTED_EXPLAIN_TYPES = new Set<DatabaseType>(["mysql", "postgres", "dameng", "questdb", "doris", "oracle", "oceanbase-oracle", "sqlserver"]);
export function supportsExplainPlan(databaseType?: DatabaseType): databaseType is ExplainPlanDatabaseType {
return !!databaseType && supportsDatabaseFeature(databaseType, "sqlExplain") && SUPPORTED_EXPLAIN_TYPES.has(databaseType);
@@ -598,25 +604,29 @@ function parseOceanbaseOracleExplain(result: QueryResult): ParsedExplainPlan {
return { databaseType: "oceanbase-oracle", raw, nodes: root ? [parseOceanbaseOracleNode(root, "0")] : [] };
}
const OCEANBASE_PLAN_KEYS = { id: "ID", operator: "OPERATOR", name: "NAME", rows: "EST.ROWS", time: "EST.TIME(us)", cost: "COST" } as const;
const OCEANBASE_PLAN_FIELDS = new Set<string>(Object.values(OCEANBASE_PLAN_KEYS));
const OCEANBASE_CHILD_KEY = /^CHILD_\d+$/;
function parseOceanbaseOracleNode(plan: Record<string, unknown>, fallbackId: string): ExplainPlanNode {
const nodeType = stringValue(plan.OPERATOR)?.trim() || "Plan";
const relation = stringValue(plan.NAME)?.trim() || undefined;
const nodeType = stringValue(plan[OCEANBASE_PLAN_KEYS.operator])?.trim() || "Plan";
const relation = stringValue(plan[OCEANBASE_PLAN_KEYS.name])?.trim() || undefined;
const children = Object.entries(plan)
.filter(([key, value]) => /^CHILD_\d+$/.test(key) && objectValue(value))
.filter(([key, value]) => OCEANBASE_CHILD_KEY.test(key) && objectValue(value))
.sort(([left], [right]) => Number(left.slice(6)) - Number(right.slice(6)))
.map(([key, value]) => parseOceanbaseOracleNode(objectValue(value)!, `${fallbackId}.${key.slice(6)}`));
const estimatedTime = numberLike(plan["EST.TIME(us)"]);
const estimatedTimeUs = numberLike(plan[OCEANBASE_PLAN_KEYS.time]);
const details = Object.entries(plan)
.filter(([key, value]) => !["ID", "OPERATOR", "NAME", "EST.ROWS", "EST.TIME(us)", "COST"].includes(key) && !/^CHILD_\d+$/.test(key) && value !== null)
.filter(([key, value]) => !OCEANBASE_PLAN_FIELDS.has(key) && !OCEANBASE_CHILD_KEY.test(key) && value !== null)
.map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
if (estimatedTime !== undefined) details.unshift(`Estimated time: ${estimatedTime} µs`);
return {
id: numberLike(plan.ID) || fallbackId,
id: numberLike(plan[OCEANBASE_PLAN_KEYS.id]) || fallbackId,
title: relation && relation !== nodeType ? `${nodeType} on ${relation}` : nodeType,
nodeType,
relation,
cost: numberLike(plan.COST),
rows: numberLike(plan["EST.ROWS"]),
cost: numberLike(plan[OCEANBASE_PLAN_KEYS.cost]),
rows: numberLike(plan[OCEANBASE_PLAN_KEYS.rows]),
estimatedTimeUs,
details,
children,
};