Skip to content

Commit

Permalink
Properly render plans of failed queries (#127)
Browse files Browse the repository at this point in the history
For query plans of queries which errored-out, we now:
* Show the error message top-level, right under the graph title
* Highlight the failed operator in red
  • Loading branch information
vogelsgesang authored Oct 20, 2024
1 parent 1f90218 commit 845a9e4
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 25 deletions.
40 changes: 25 additions & 15 deletions query-graphs/src/hyper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface ConversionState {
crosslinks: UnresolvedCrosslink[];
edgeWidths: {node: TreeNode; width: number}[];
runtimes: {node: TreeNode; time: number}[];
metadata: Map<string, string>;
}

// Customization points for rendering the various different
Expand Down Expand Up @@ -222,6 +223,12 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS
expandedByDefault: nodeType != "operator" && expandedChildren.length == 0,
} as TreeNode;

// Highlight the node which errored out, in case the query failed
const errored = conversionState.metadata.has("Error") && tryGetPropertyPath(rawNode, ["analyze", "running"]) === true;
if (errored) {
convertedNode.iconColor = "red";
}

// Information on the execution time
const execTime = tryGetPropertyPath(rawNode, ["analyze", "execution-time"]);
if (typeof execTime === "number") {
Expand Down Expand Up @@ -320,29 +327,31 @@ function setEdgeWidths(state: ConversionState) {
}
}

interface LinkedNodes {
root: TreeNode;
crosslinks: Crosslink[];
}

function convertHyperPlan(node: Json): LinkedNodes {
function convertHyperPlan(node: Json): TreeDescription {
const conversionState = {
operatorsById: new Map<string, TreeNode>(),
crosslinks: [],
edgeWidths: [],
runtimes: [],
metadata: new Map<string, string>(),
} as ConversionState;
// Check if the query failed
const errorMsg = tryGetPropertyPath(node, ["analyze", "error", "message", "original"]);
if (errorMsg) {
conversionState.metadata.set("Error", forceToString(errorMsg));
}

const root = convertHyperNode(node, "result", conversionState);
if (Array.isArray(root)) {
throw new Error("Invalid Hyper query plan");
}
colorRelativeExecutionTime(conversionState);
setEdgeWidths(conversionState);
const crosslinks = resolveCrosslinks(conversionState);
return {root, crosslinks};
return {root, crosslinks, metadata: conversionState.metadata};
}

function convertOptimizerSteps(node: Json): LinkedNodes | undefined {
function convertOptimizerSteps(node: Json): TreeDescription | undefined {
// Check if we have a top-level object with a single key "optimizersteps" containing an array
if (typeof node !== "object" || Array.isArray(node) || node === null) return undefined;
if (Object.getOwnPropertyNames(node).length != 1) return undefined;
Expand All @@ -353,6 +362,7 @@ function convertOptimizerSteps(node: Json): LinkedNodes | undefined {
// Transform the optimizer steps
const crosslinks: Crosslink[] = [];
const children: TreeNode[] = [];
const properties = new Map<string, string>();
for (const step of steps) {
// Check that our step has two subproperties: "name" and "plan"
if (typeof step !== "object" || Array.isArray(step) || step === null) return undefined;
Expand All @@ -364,20 +374,20 @@ function convertOptimizerSteps(node: Json): LinkedNodes | undefined {
if (typeof name !== "string") return undefined;

// Add the child
const {root: childRoot, crosslinks: newCrosslinks} = convertHyperPlan(plan);
crosslinks.push(...newCrosslinks);
const {root: childRoot, crosslinks: newCrosslinks, metadata: newProperties} = convertHyperPlan(plan);
crosslinks.push(...(newCrosslinks ?? []));
children.push({name: name, children: [childRoot]});
for (const p of newProperties ?? new Map<string, string>()) {
properties.set(p[0], p[1]);
}
}
const root = {name: "optimizersteps", children: children};
return {root, crosslinks};
return {root, crosslinks, metadata: properties};
}

// Loads a Hyper query plan
export function loadHyperPlan(json: Json): TreeDescription {
// Load the graph with the nodes collapsed in an automatic way
const {root, crosslinks} = convertOptimizerSteps(json) ?? convertHyperPlan(json);
// Adjust the graph so it is collapsed as requested by the user
return {root, crosslinks};
return convertOptimizerSteps(json) ?? convertHyperPlan(json);
}

function tryStripPrefix(str, pre) {
Expand Down
5 changes: 2 additions & 3 deletions query-graphs/src/tree-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ export interface Crosslink {
export interface TreeDescription {
/// The tree root
root: TreeNode;
/// Displayed in the top-level tree label
/// XXX remove
properties?: Map<string, string>;
/// Metadata about the graph; displayed in the top-level tree label
metadata?: Map<string, string>;
/// Additional links between indirectly related nodes
crosslinks?: Crosslink[];
}
Expand Down
10 changes: 6 additions & 4 deletions query-graphs/src/ui/QueryNode.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@

.qg-prop-name {
color: hsl(0, 0%, 50%);
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}

.qg-prop-value {
-webkit-user-select: all;
-moz-user-select: all;
-ms-user-select: all;
user-select: all;
-webkit-user-select: text;
-ms-user-select: text;
user-select: text;
}
2 changes: 1 addition & 1 deletion standalone-app/src/QueryGraphsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function QueryGraphsApp() {
} else {
return (
<QueryGraph treeDescription={tree}>
<TreeLabel title={treeTitle ?? ""} setTitle={setTreeTitle} />
<TreeLabel title={treeTitle ?? ""} setTitle={setTreeTitle} metadata={tree.metadata} />
</QueryGraph>
);
}
Expand Down
9 changes: 8 additions & 1 deletion standalone-app/src/TreeLabel.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.graph-title {
font-size: 1.2em;
background: #fff;
font-size: 1.1em;
font-weight: bold;
field-sizing: content;
min-width: 10em;
Expand All @@ -8,4 +9,10 @@

.graph-title:hover {
background: #ffeded;
}

.graph-metadata {
background: #fff;
max-width: 20em;
word-break: break-all;
}
14 changes: 13 additions & 1 deletion standalone-app/src/TreeLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import {ReactElement} from "react";
import "./TreeLabel.css";

export interface TreeLabelProps {
title: string;
setTitle?: (v: string) => void;
metadata?: Map<string, string>;
}

export function TreeLabel({title, setTitle}: TreeLabelProps) {
export function TreeLabel({title, setTitle, metadata}: TreeLabelProps) {
const metadataChildren = [] as ReactElement[];
for (const [key, value] of (metadata || []).entries()) {
metadataChildren.push(
<div key={key}>
<span className="qg-prop-name">{key}:</span> <span className="qg-prop-value">{value}</span>
</div>,
);
}

return (
<div className="react-flow__panel">
<input
Expand All @@ -15,6 +26,7 @@ export function TreeLabel({title, setTitle}: TreeLabelProps) {
value={title}
onChange={(e) => (setTitle ? setTitle(e.target.value) : undefined)}
/>
<div className="graph-metadata">{metadataChildren}</div>
</div>
);
}

0 comments on commit 845a9e4

Please sign in to comment.