-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathSQLiteExplainTab.tsx
More file actions
209 lines (191 loc) · 4.9 KB
/
SQLiteExplainTab.tsx
File metadata and controls
209 lines (191 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { cn, Tooltip } from "@cloudflare/kumo";
import { TableIcon } from "@phosphor-icons/react";
import { Fragment } from "react";
import type { StudioResultSet } from "../../../types/studio";
import type { ReactNode } from "react";
interface StudioSQLiteExplainProps {
data: StudioResultSet;
}
interface StudioSQLiteExplainRow {
id: number;
parent: number;
detail: string;
}
interface StudioSQLiteExplainTree extends StudioSQLiteExplainRow {
children: StudioSQLiteExplainTree[];
}
export function StudioSQLiteExplainTab({
data,
}: StudioSQLiteExplainProps): JSX.Element {
const rows = data.rows as unknown as StudioSQLiteExplainRow[];
let tree = rows.map(
(r) =>
({
...r,
children: [],
}) satisfies StudioSQLiteExplainTree
);
const nodeTable = tree.reduce(
(a, b) => ({
...a,
[b.id]: b,
}),
{} as Record<string, StudioSQLiteExplainTree>
);
for (const node of tree) {
if (node.parent) {
nodeTable[node.parent]?.children.push(node);
}
}
tree = tree.filter((node) => node.parent === 0);
return (
<div className="w-full h-full grow p-8 overflow-auto">
<div className="font-mono text-sm">
<ExplainNodes data={tree} />
</div>
</div>
);
}
interface ExplainNodesProps {
data: StudioSQLiteExplainTree[];
}
function ExplainNodes({ data }: ExplainNodesProps): JSX.Element {
return (
<>
{data.map((row) => {
const { label, performance } = describeExplainNode(row.detail);
return (
<Fragment key={row.id}>
<div className="h-8 flex gap-2 items-center">
<div
className={cn("inline-flex border rounded-full", {
"bg-green-500": performance === "fast",
"bg-red-500": performance === "slow",
"bg-yellow-500": performance === "medium",
"bg-gray-500": performance === "neutral",
})}
style={{ width: 10, height: 10, marginLeft: -5 }}
/>
<div>{label}</div>
</div>
<div className="pl-4 border-l">
<ExplainNodes data={row.children} />
</div>
</Fragment>
);
})}
</>
);
}
type ExplainNodePerformance = "slow" | "medium" | "fast" | "neutral";
/**
* Convert an EXPLAIN step detail string into a UI-friendly
* description with performance classification and formatted label.
*
* Performance indicates execution efficiency:
* - slow: likely very costly
* - medium: potentially adds extra work
* - fast: generally efficient
* - neutral: informational only
*
* @param detail - The raw detail text from the EXPLAIN result
*
* @returns Object containing a ReactNode label and performance level
*/
function describeExplainNode(d: string): {
label: ReactNode;
performance: ExplainNodePerformance;
} {
if (d.startsWith("SCAN ")) {
return {
performance: "slow",
label: (
<div className="flex items-center">
<strong>SCAN </strong>
<span className="border border-border p-1 mx-2 rounded flex items-center gap-2">
<TableIcon />
{d.substring("SCAN ".length)}
</span>
</div>
),
};
}
if (d.startsWith("CORRELATED ")) {
return {
performance: "slow",
label: (
<div>
<Tooltip
side="bottom"
content={
<div className="flex flex-col gap-2">
<div>
This subquery depends on values from the outer query, so
it's evaluated once per outer row.{" "}
<strong className="text-red-500">
Can be slow on large inputs
</strong>
.
</div>
<div className="text-green-500">
Mitigate by indexing the correlated columns or rewriting as a
JOIN + aggregate.
</div>
</div>
}
>
<strong className="underline cursor-pointer">CORRELATED</strong>
</Tooltip>
<span>{d.substring("CORRELATED".length)}</span>
</div>
),
};
}
if (d.startsWith("SEARCH ")) {
return {
performance: "fast",
label: (
<div>
<strong>SEARCH </strong>
<span>{d.substring("SEARCH".length)}</span>
</div>
),
};
}
if (
d.startsWith("USE TEMP B-TREE FOR ORDER BY") ||
d.startsWith("USE TEMP B-TREE FOR GROUP BY") ||
d.startsWith("USE TEMP B-TREE FOR DISTINCT")
) {
return {
performance: "medium",
label: (
<Tooltip
side="bottom"
content={
<div className="flex flex-col gap-2">
<div>
SQLite can’t return rows in the requested order/grouping
directly, so it gathers them into a temporary structure and
processes them before returning results.{" "}
<span className="text-red-500">
This adds extra work and grows with result size.
</span>
</div>
<div className="text-green-500">
Add an index that matches the clause (ORDER BY / GROUP BY /
DISTINCT) to avoid the temp structure.
</div>
</div>
}
>
<strong className="underline cursor-pointer">{d}</strong>
</Tooltip>
),
};
}
return {
label: <span>{d}</span>,
performance: "neutral",
};
}