From e34705d9160b28bb58dbe55afc0de9afacec7f54 Mon Sep 17 00:00:00 2001 From: "Mateusz \"Serafin\" Gajewski" Date: Fri, 26 Jul 2024 18:24:52 +0200 Subject: [PATCH] Add query explain distributed plan to UI --- .../io/trino/server/ui/UiQueryResource.java | 45 ++++++++++++++++++- .../webapp/src/components/QueryHeader.jsx | 6 ++- .../webapp/src/components/QueryList.jsx | 5 +++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/core/trino-main/src/main/java/io/trino/server/ui/UiQueryResource.java b/core/trino-main/src/main/java/io/trino/server/ui/UiQueryResource.java index 68f341f6adbbe..3e639d3c3c4ea 100644 --- a/core/trino-main/src/main/java/io/trino/server/ui/UiQueryResource.java +++ b/core/trino-main/src/main/java/io/trino/server/ui/UiQueryResource.java @@ -15,9 +15,13 @@ import com.google.common.collect.ImmutableList; import com.google.inject.Inject; +import io.trino.client.NodeVersion; import io.trino.dispatcher.DispatchManager; import io.trino.execution.QueryInfo; import io.trino.execution.QueryState; +import io.trino.metadata.FunctionManager; +import io.trino.metadata.Metadata; +import io.trino.metadata.SessionPropertyManager; import io.trino.security.AccessControl; import io.trino.server.BasicQueryInfo; import io.trino.server.DisableHttpCache; @@ -26,6 +30,8 @@ import io.trino.spi.QueryId; import io.trino.spi.TrinoException; import io.trino.spi.security.AccessDeniedException; +import io.trino.sql.planner.planprinter.NoOpAnonymizer; +import io.trino.sql.planner.planprinter.ValuePrinter; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; @@ -49,19 +55,28 @@ import static io.trino.security.AccessControlUtil.checkCanViewQueryOwnedBy; import static io.trino.security.AccessControlUtil.filterQueries; import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI; +import static io.trino.sql.planner.planprinter.PlanPrinter.textDistributedPlan; import static java.util.Objects.requireNonNull; @Path("/ui/api/query") @DisableHttpCache public class UiQueryResource { + private final Metadata metadata; + private final FunctionManager functionManager; + private final NodeVersion nodeVersion; + private final SessionPropertyManager sessionPropertyManager; private final DispatchManager dispatchManager; private final AccessControl accessControl; private final HttpRequestSessionContextFactory sessionContextFactory; @Inject - public UiQueryResource(DispatchManager dispatchManager, AccessControl accessControl, HttpRequestSessionContextFactory sessionContextFactory) + public UiQueryResource(Metadata metadata, FunctionManager functionManager, NodeVersion nodeVersion, SessionPropertyManager sessionPropertyManager, DispatchManager dispatchManager, AccessControl accessControl, HttpRequestSessionContextFactory sessionContextFactory) { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.nodeVersion = requireNonNull(nodeVersion, "nodeVersion is null"); + this.sessionPropertyManager = requireNonNull(sessionPropertyManager, "sessionPropertyManager is null"); this.dispatchManager = requireNonNull(dispatchManager, "dispatchManager is null"); this.accessControl = requireNonNull(accessControl, "accessControl is null"); this.sessionContextFactory = requireNonNull(sessionContextFactory, "sessionContextFactory is null"); @@ -105,6 +120,34 @@ public Response getQueryInfo(@PathParam("queryId") QueryId queryId, @Context Htt return Response.status(Status.GONE).build(); } + @ResourceSecurity(WEB_UI) + @GET + @Path("{queryId}/explain") + public Response explainQuery(@PathParam("queryId") QueryId queryId, @Context HttpServletRequest servletRequest, @Context HttpHeaders httpHeaders) + { + requireNonNull(queryId, "queryId is null"); + + Optional queryInfo = dispatchManager.getFullQueryInfo(queryId); + if (queryInfo.isPresent()) { + try { + checkCanViewQueryOwnedBy(sessionContextFactory.extractAuthorizedIdentity(servletRequest, httpHeaders), queryInfo.get().getSession().toIdentity(), accessControl); + + ValuePrinter valuePrinter = new ValuePrinter(metadata, functionManager, queryInfo.get().getSession().toSession(sessionPropertyManager)); + return Response.ok(textDistributedPlan( + queryInfo.get().getOutputStage().orElseThrow(), + queryInfo.get().getQueryStats(), + valuePrinter, + true, + new NoOpAnonymizer(), + nodeVersion)).type("text/plain;charset=utf-8").build(); + } + catch (AccessDeniedException e) { + throw new ForbiddenException(); + } + } + return Response.status(Status.GONE).build(); + } + @ResourceSecurity(WEB_UI) @PUT @Path("{queryId}/killed") diff --git a/core/trino-main/src/main/resources/webapp/src/components/QueryHeader.jsx b/core/trino-main/src/main/resources/webapp/src/components/QueryHeader.jsx index 5b8fff88e41ee..91451f3fa83b2 100644 --- a/core/trino-main/src/main/resources/webapp/src/components/QueryHeader.jsx +++ b/core/trino-main/src/main/resources/webapp/src/components/QueryHeader.jsx @@ -80,7 +80,7 @@ export class QueryHeader extends React.Component { return (
-
+ -
+
@@ -103,6 +103,8 @@ export class QueryHeader extends React.Component {   JSON   + Explain +   {this.renderTab("references.html", "References")} diff --git a/core/trino-main/src/main/resources/webapp/src/components/QueryList.jsx b/core/trino-main/src/main/resources/webapp/src/components/QueryList.jsx index 217facc38eb98..f26c3e381229f 100644 --- a/core/trino-main/src/main/resources/webapp/src/components/QueryList.jsx +++ b/core/trino-main/src/main/resources/webapp/src/components/QueryList.jsx @@ -151,6 +151,11 @@ export class QueryListItem extends React.Component { title="Query JSON">   + + +