@@ -150,15 +150,20 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
150150 organizationId : project . organizationId ,
151151 } ) ;
152152
153+ // Admins and impersonating users can use EXPLAIN
154+ const isAdmin = user . admin || user . isImpersonating ;
155+
153156 return typedjson ( {
154157 defaultQuery,
155158 history,
159+ isAdmin,
156160 } ) ;
157161} ;
158162
159163const ActionSchema = z . object ( {
160164 query : z . string ( ) . min ( 1 , "Query is required" ) ,
161165 scope : z . enum ( [ "environment" , "project" , "organization" ] ) ,
166+ explain : z . enum ( [ "true" , "false" ] ) . nullable ( ) . optional ( ) ,
162167} ) ;
163168
164169export const action = async ( { request, params } : ActionFunctionArgs ) => {
@@ -173,15 +178,31 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
173178 ) ;
174179 if ( ! canAccess ) {
175180 return typedjson (
176- { error : "Unauthorized" , rows : null , columns : null , stats : null , hiddenColumns : null } ,
181+ {
182+ error : "Unauthorized" ,
183+ rows : null ,
184+ columns : null ,
185+ stats : null ,
186+ hiddenColumns : null ,
187+ explainOutput : null ,
188+ generatedSql : null ,
189+ } ,
177190 { status : 403 }
178191 ) ;
179192 }
180193
181194 const project = await findProjectBySlug ( organizationSlug , projectParam , user . id ) ;
182195 if ( ! project ) {
183196 return typedjson (
184- { error : "Project not found" , rows : null , columns : null , stats : null , hiddenColumns : null } ,
197+ {
198+ error : "Project not found" ,
199+ rows : null ,
200+ columns : null ,
201+ stats : null ,
202+ hiddenColumns : null ,
203+ explainOutput : null ,
204+ generatedSql : null ,
205+ } ,
185206 { status : 404 }
186207 ) ;
187208 }
@@ -195,6 +216,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
195216 columns : null ,
196217 stats : null ,
197218 hiddenColumns : null ,
219+ explainOutput : null ,
220+ generatedSql : null ,
198221 } ,
199222 { status : 404 }
200223 ) ;
@@ -204,6 +227,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
204227 const parsed = ActionSchema . safeParse ( {
205228 query : formData . get ( "query" ) ,
206229 scope : formData . get ( "scope" ) ,
230+ explain : formData . get ( "explain" ) ,
207231 } ) ;
208232
209233 if ( ! parsed . success ) {
@@ -214,12 +238,17 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
214238 columns : null ,
215239 stats : null ,
216240 hiddenColumns : null ,
241+ explainOutput : null ,
242+ generatedSql : null ,
217243 } ,
218244 { status : 400 }
219245 ) ;
220246 }
221247
222- const { query, scope } = parsed . data ;
248+ const { query, scope, explain : explainParam } = parsed . data ;
249+ // Only allow explain for admins/impersonating users
250+ const isAdmin = user . admin || user . isImpersonating ;
251+ const explain = explainParam === "true" && isAdmin ;
223252
224253 try {
225254 const [ error , result ] = await executeQuery ( {
@@ -232,6 +261,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
232261 organizationId : project . organizationId ,
233262 projectId : project . id ,
234263 environmentId : environment . id ,
264+ explain,
235265 history : {
236266 source : "DASHBOARD" ,
237267 userId : user . id ,
@@ -240,7 +270,15 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
240270
241271 if ( error ) {
242272 return typedjson (
243- { error : error . message , rows : null , columns : null , stats : null , hiddenColumns : null } ,
273+ {
274+ error : error . message ,
275+ rows : null ,
276+ columns : null ,
277+ stats : null ,
278+ hiddenColumns : null ,
279+ explainOutput : null ,
280+ generatedSql : null ,
281+ } ,
244282 { status : 400 }
245283 ) ;
246284 }
@@ -251,11 +289,21 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
251289 columns : result . columns ,
252290 stats : result . stats ,
253291 hiddenColumns : result . hiddenColumns ?? null ,
292+ explainOutput : result . explainOutput ?? null ,
293+ generatedSql : result . generatedSql ?? null ,
254294 } ) ;
255295 } catch ( err ) {
256296 const errorMessage = err instanceof Error ? err . message : "Unknown error executing query" ;
257297 return typedjson (
258- { error : errorMessage , rows : null , columns : null , stats : null , hiddenColumns : null } ,
298+ {
299+ error : errorMessage ,
300+ rows : null ,
301+ columns : null ,
302+ stats : null ,
303+ hiddenColumns : null ,
304+ explainOutput : null ,
305+ generatedSql : null ,
306+ } ,
259307 { status : 500 }
260308 ) ;
261309 }
@@ -276,8 +324,9 @@ const QueryEditorForm = forwardRef<
276324 defaultScope : QueryScope ;
277325 history : QueryHistoryItem [ ] ;
278326 isLoading : boolean ;
327+ isAdmin : boolean ;
279328 }
280- > ( function QueryEditorForm ( { defaultQuery, defaultScope, history, isLoading } , ref ) {
329+ > ( function QueryEditorForm ( { defaultQuery, defaultScope, history, isLoading, isAdmin } , ref ) {
281330 const [ query , setQuery ] = useState ( defaultQuery ) ;
282331 const [ scope , setScope ] = useState < QueryScope > ( defaultScope ) ;
283332
@@ -332,6 +381,17 @@ const QueryEditorForm = forwardRef<
332381 ) )
333382 }
334383 </ Select >
384+ { isAdmin && (
385+ < Button
386+ type = "submit"
387+ name = "explain"
388+ value = "true"
389+ variant = "tertiary/small"
390+ disabled = { isLoading || ! query . trim ( ) }
391+ >
392+ Explain
393+ </ Button >
394+ ) }
335395 < Button
336396 type = "submit"
337397 variant = "primary/small"
@@ -348,7 +408,7 @@ const QueryEditorForm = forwardRef<
348408} ) ;
349409
350410export default function Page ( ) {
351- const { defaultQuery, history } = useTypedLoaderData < typeof loader > ( ) ;
411+ const { defaultQuery, history, isAdmin } = useTypedLoaderData < typeof loader > ( ) ;
352412 const results = useTypedActionData < typeof action > ( ) ;
353413 const navigation = useNavigation ( ) ;
354414
@@ -406,6 +466,7 @@ export default function Page() {
406466 defaultScope = { initialScope }
407467 history = { history }
408468 isLoading = { isLoading }
469+ isAdmin = { isAdmin }
409470 />
410471 { /* Results */ }
411472 < div className = "grid max-h-full grid-rows-[2rem_1fr] overflow-hidden border-t border-grid-dimmed bg-charcoal-800" >
@@ -472,6 +533,27 @@ export default function Page() {
472533 Try fix error
473534 </ Button >
474535 </ div >
536+ ) : results ?. explainOutput ? (
537+ < div className = "flex h-full flex-col gap-4 overflow-auto p-3" >
538+ { results . generatedSql && (
539+ < div >
540+ < Header3 className = "mb-2" > Generated ClickHouse SQL</ Header3 >
541+ < div className = "overflow-auto rounded border border-grid-dimmed bg-charcoal-900 p-3" >
542+ < pre className = "whitespace-pre font-mono text-xs text-text-bright" >
543+ { results . generatedSql }
544+ </ pre >
545+ </ div >
546+ </ div >
547+ ) }
548+ < div className = "flex min-h-0 flex-1 flex-col" >
549+ < Header3 className = "mb-2" > Query Execution Plan</ Header3 >
550+ < div className = "min-h-0 flex-1 overflow-auto rounded border border-grid-dimmed bg-charcoal-900 p-3" >
551+ < pre className = "whitespace-pre font-mono text-xs text-text-bright" >
552+ { results . explainOutput }
553+ </ pre >
554+ </ div >
555+ </ div >
556+ </ div >
475557 ) : results ?. rows && results ?. columns ? (
476558 < div className = "flex h-full flex-col overflow-hidden" >
477559 { results . hiddenColumns && results . hiddenColumns . length > 0 && (
0 commit comments