fix(oceanbase): localize explain plan estimated time label
This commit is contained in:
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: [] });
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user