From a1299ecf457217d186b373c45047d5e9a39dadba Mon Sep 17 00:00:00 2001 From: Lauren Nathan Date: Tue, 24 Sep 2024 10:48:56 -0700 Subject: [PATCH 01/15] exposed backend call for plan --- src/controllers/queryRunner.ts | 1 + src/models/contracts/queryExecute.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/controllers/queryRunner.ts b/src/controllers/queryRunner.ts index 59b6c7f37..1a943b70d 100644 --- a/src/controllers/queryRunner.ts +++ b/src/controllers/queryRunner.ts @@ -203,6 +203,7 @@ export default class QueryRunner { // Put together the request let queryDetails: QueryExecuteParams = { ownerUri: this._ownerUri, + executionPlanOptions: {includeActualExecutionPlanXml: false, includeEstimatedExecutionPlanXml: true}, querySelection: selection, }; diff --git a/src/models/contracts/queryExecute.ts b/src/models/contracts/queryExecute.ts index b65df5f1b..5a3923267 100644 --- a/src/models/contracts/queryExecute.ts +++ b/src/models/contracts/queryExecute.ts @@ -104,6 +104,7 @@ export namespace QueryExecuteStatementRequest { export class QueryExecuteParams { ownerUri: string; + executionPlanOptions?: ExecutionPlanOptions; querySelection: ISelectionData; } @@ -115,6 +116,11 @@ export class QueryExecuteStatementParams { export class QueryExecuteResult {} +export class ExecutionPlanOptions { + includeActualExecutionPlanXml: boolean; + includeEstimatedExecutionPlanXml: boolean; +} + // ------------------------------- < Query Results Request > ------------------------------------ export namespace QueryExecuteSubsetRequest { export const type = new RequestType< From c1769c3bec30c80be5013e982dbcbfd651851e22 Mon Sep 17 00:00:00 2001 From: Lauren Nathan Date: Tue, 24 Sep 2024 11:40:53 -0700 Subject: [PATCH 02/15] execution plan from query --- package.json | 14 ++++++++++++++ package.nls.json | 1 + src/constants/constants.ts | 1 + src/controllers/mainController.ts | 12 +++++++++++- src/controllers/queryRunner.ts | 4 +++- src/models/contracts/queryExecute.ts | 4 ++-- src/models/sqlOutputContentProvider.ts | 5 +++-- 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 99d352aab..e5ac3941c 100644 --- a/package.json +++ b/package.json @@ -305,6 +305,11 @@ "command": "mssql.changeDatabase", "when": "editorLangId == sql", "group": "navigation@4" + }, + { + "command": "mssql.showExecutionPlanInResults", + "when": "editorLangId == sql", + "group": "navigation@4" } ], "editor/context": [ @@ -780,6 +785,15 @@ "command": "mssql.clearAzureAccountTokenCache", "title": "%mssql.clearAzureAccountTokenCache%", "category": "MS SQL" + }, + { + "command": "mssql.showExecutionPlanInResults", + "title": "%mssql.showExecutionPlanInResults%", + "category": "MS SQL", + "icon": { + "dark": "media/executionPlan_dark.svg", + "light": "media/executionPlan_light.svg" + } } ], "keybindings": [ diff --git a/package.nls.json b/package.nls.json index a815fea34..bdecbaa60 100644 --- a/package.nls.json +++ b/package.nls.json @@ -36,6 +36,7 @@ "mssql.addAadAccount":"Add Microsoft Entra Account", "mssql.removeAadAccount":"Remove Microsoft Entra Account", "mssql.clearAzureAccountTokenCache":"Clear Microsoft Entra account token cache", +"mssql.showExecutionPlanInResults":"Estimated Plan", "mssql.rebuildIntelliSenseCache":"Refresh IntelliSense Cache", "mssql.logDebugInfo":"[Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools)", "mssql.maxRecentConnections":"The maximum number of recently used connections to store in the connection list.", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 816a28da0..d1beb3c8c 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -78,6 +78,7 @@ export const cmdAzureSignInToCloud = "azure-account.loginToCloud"; export const cmdAadRemoveAccount = "mssql.removeAadAccount"; export const cmdAadAddAccount = "mssql.addAadAccount"; export const cmdClearAzureTokenCache = "mssql.clearAzureAccountTokenCache"; +export const cmdShowExecutionPlanInResults = "mssql.showExecutionPlanInResults"; export const cmdNewTable = "mssql.newTable"; export const cmdEditTable = "mssql.editTable"; export const cmdEditConnection = "mssql.editConnection"; diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 61579d9d2..91bb38026 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -56,6 +56,7 @@ import { ExecutionPlanService } from "../services/executionPlanService"; import { ExecutionPlanWebviewController } from "./executionPlanWebviewController"; import { QueryResultWebviewController } from "../queryResult/queryResultWebViewController"; import { MssqlProtocolHandler } from "../mssqlProtocolHandler"; +import { ExecutionPlanOptions } from "../models/contracts/queryExecute"; /** * The main controller class that initializes the extension @@ -247,6 +248,12 @@ export default class MainController implements vscode.Disposable { this._event.on(Constants.cmdClearAzureTokenCache, () => this.onClearAzureTokenCache(), ); + this.registerCommand(Constants.cmdShowExecutionPlanInResults); + this._event.on(Constants.cmdShowExecutionPlanInResults, async () => { + this.onRunQuery(undefined, { + includeEstimatedExecutionPlanXml: true + }) + }); this.initializeObjectExplorer(); this.registerCommandWithArgs( @@ -430,6 +437,7 @@ export default class MainController implements vscode.Disposable { uri, undefined, title, + {}, queryPromise, ); await queryPromise; @@ -1453,7 +1461,7 @@ export default class MainController implements vscode.Disposable { /** * get the T-SQL query from the editor, run it and show output */ - public async onRunQuery(callbackThis?: MainController): Promise { + public async onRunQuery(callbackThis?: MainController, executionPlanOptions?: ExecutionPlanOptions): Promise { // the 'this' context is lost in retry callback, so capture it here let self: MainController = callbackThis ? callbackThis : this; try { @@ -1510,11 +1518,13 @@ export default class MainController implements vscode.Disposable { if (editor.document.getText(selectionToTrim).trim().length === 0) { return; } + await self._outputContentProvider.runQuery( self._statusview, uri, querySelection, title, + executionPlanOptions ); } catch (err) { console.warn(`Unexpected error running query : ${err}`); diff --git a/src/controllers/queryRunner.ts b/src/controllers/queryRunner.ts index 1a943b70d..1b04706a4 100644 --- a/src/controllers/queryRunner.ts +++ b/src/controllers/queryRunner.ts @@ -27,6 +27,7 @@ import { QueryExecutionOptionsParams, QueryExecutionOptions, DbCellValue, + ExecutionPlanOptions, } from "../models/contracts/queryExecute"; import { QueryDisposeParams, @@ -197,13 +198,14 @@ export default class QueryRunner { // Pulls the query text from the current document/selection and initiates the query public async runQuery( selection: ISelectionData, + executionPlanOptions?: ExecutionPlanOptions, promise?: Deferred, ): Promise { await this.doRunQuery(selection, async (onSuccess, onError) => { // Put together the request let queryDetails: QueryExecuteParams = { ownerUri: this._ownerUri, - executionPlanOptions: {includeActualExecutionPlanXml: false, includeEstimatedExecutionPlanXml: true}, + executionPlanOptions: executionPlanOptions, querySelection: selection, }; diff --git a/src/models/contracts/queryExecute.ts b/src/models/contracts/queryExecute.ts index 5a3923267..47c8ed30f 100644 --- a/src/models/contracts/queryExecute.ts +++ b/src/models/contracts/queryExecute.ts @@ -117,8 +117,8 @@ export class QueryExecuteStatementParams { export class QueryExecuteResult {} export class ExecutionPlanOptions { - includeActualExecutionPlanXml: boolean; - includeEstimatedExecutionPlanXml: boolean; + includeActualExecutionPlanXml?: boolean; + includeEstimatedExecutionPlanXml?: boolean; } // ------------------------------- < Query Results Request > ------------------------------------ diff --git a/src/models/sqlOutputContentProvider.ts b/src/models/sqlOutputContentProvider.ts index 23eae078e..6aef99e03 100644 --- a/src/models/sqlOutputContentProvider.ts +++ b/src/models/sqlOutputContentProvider.ts @@ -14,7 +14,7 @@ import VscodeWrapper from "./../controllers/vscodeWrapper"; import { ISelectionData, ISlickRange } from "./interfaces"; import { WebviewPanelController } from "../controllers/webviewController"; import { IServerProxy, Deferred } from "../protocol"; -import { ResultSetSubset, ResultSetSummary } from "./contracts/queryExecute"; +import { ExecutionPlanOptions, ResultSetSubset, ResultSetSummary } from "./contracts/queryExecute"; import { sendActionEvent } from "../telemetry/telemetry"; import { QueryResultWebviewController } from "../queryResult/queryResultWebViewController"; import { QueryResultPaneTabs } from "../sharedInterfaces/queryResult"; @@ -163,6 +163,7 @@ export class SqlOutputContentProvider { uri: string, selection: ISelectionData, title: string, + executionPlanOptions?: any, promise?: Deferred, ): Promise { // execute the query with a query runner @@ -178,7 +179,7 @@ export class SqlOutputContentProvider { this._panels.get(uri).revealToForeground(uri); } } - await queryRunner.runQuery(selection, promise); + await queryRunner.runQuery(selection, executionPlanOptions as ExecutionPlanOptions, promise); } }, ); From 71e41ff2c66e3b71f31670a3d59008486e6baedc Mon Sep 17 00:00:00 2001 From: Lauren Nathan Date: Thu, 26 Sep 2024 11:36:47 -0700 Subject: [PATCH 03/15] extract query text to use for execution plan --- src/controllers/mainController.ts | 14 ++++++++----- src/models/sqlOutputContentProvider.ts | 4 +++- .../queryResultWebViewController.ts | 4 +++- .../pages/QueryResult/queryResultPane.tsx | 21 ++++++++++++++++++- src/sharedInterfaces/queryResult.ts | 2 ++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 91bb38026..c35cb0dc9 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -80,6 +80,10 @@ export default class MainController implements vscode.Disposable { private _queryHistoryProvider: QueryHistoryProvider; private _scriptingService: ScriptingService; private _queryHistoryRegistered: boolean = false; + private _executionPlanOptions: ExecutionPlanOptions = { + includeEstimatedExecutionPlanXml: false, + includeActualExecutionPlanXml: false + }; public sqlTasksService: SqlTasksService; public dacFxService: DacFxService; public schemaCompareService: SchemaCompareService; @@ -183,6 +187,7 @@ export default class MainController implements vscode.Disposable { }); this.registerCommand(Constants.cmdRunQuery); this._event.on(Constants.cmdRunQuery, () => { + this._executionPlanOptions.includeEstimatedExecutionPlanXml = false; this.onRunQuery(); }); this.registerCommand(Constants.cmdManageConnectionProfiles); @@ -250,9 +255,8 @@ export default class MainController implements vscode.Disposable { ); this.registerCommand(Constants.cmdShowExecutionPlanInResults); this._event.on(Constants.cmdShowExecutionPlanInResults, async () => { - this.onRunQuery(undefined, { - includeEstimatedExecutionPlanXml: true - }) + this._executionPlanOptions.includeEstimatedExecutionPlanXml = true; + this.onRunQuery() }); this.initializeObjectExplorer(); @@ -1461,7 +1465,7 @@ export default class MainController implements vscode.Disposable { /** * get the T-SQL query from the editor, run it and show output */ - public async onRunQuery(callbackThis?: MainController, executionPlanOptions?: ExecutionPlanOptions): Promise { + public async onRunQuery(callbackThis?: MainController): Promise { // the 'this' context is lost in retry callback, so capture it here let self: MainController = callbackThis ? callbackThis : this; try { @@ -1524,7 +1528,7 @@ export default class MainController implements vscode.Disposable { uri, querySelection, title, - executionPlanOptions + self._executionPlanOptions ); } catch (err) { console.warn(`Unexpected error running query : ${err}`); diff --git a/src/models/sqlOutputContentProvider.ts b/src/models/sqlOutputContentProvider.ts index 6aef99e03..2f3478a8a 100644 --- a/src/models/sqlOutputContentProvider.ts +++ b/src/models/sqlOutputContentProvider.ts @@ -182,6 +182,7 @@ export class SqlOutputContentProvider { await queryRunner.runQuery(selection, executionPlanOptions as ExecutionPlanOptions, promise); } }, + executionPlanOptions, ); } @@ -220,6 +221,7 @@ export class SqlOutputContentProvider { uri: string, title: string, queryCallback: any, + executionPlanOptions?: any, ): Promise { let queryRunner = await this.createQueryRunner( statusView ? statusView : this._statusView, @@ -240,7 +242,7 @@ export class SqlOutputContentProvider { await this.createWebviewController(uri, title, queryRunner); } } else { - this._queryResultWebviewController.addQueryResultState(uri); + this._queryResultWebviewController.addQueryResultState(uri, executionPlanOptions?.includeEstimatedExecutionPlanXml ?? false); } if (queryRunner) { queryCallback(queryRunner); diff --git a/src/queryResult/queryResultWebViewController.ts b/src/queryResult/queryResultWebViewController.ts index 1553423ea..a78d754e0 100644 --- a/src/queryResult/queryResultWebViewController.ts +++ b/src/queryResult/queryResultWebViewController.ts @@ -53,7 +53,7 @@ export class QueryResultWebviewController extends ReactWebviewViewController< }); } - public addQueryResultState(uri: string): void { + public addQueryResultState(uri: string, isExecutionPlan?: boolean): void { this._queryResultStateMap.set(uri, { value: "", messages: [], @@ -61,6 +61,7 @@ export class QueryResultWebviewController extends ReactWebviewViewController< resultPaneTab: qr.QueryResultPaneTabs.Messages, }, uri: uri, + isExecutionPlan: isExecutionPlan ?? false }); } @@ -78,4 +79,5 @@ export class QueryResultWebviewController extends ReactWebviewViewController< ): void { this._sqlOutputContentProvider = provider; } + } diff --git a/src/reactviews/pages/QueryResult/queryResultPane.tsx b/src/reactviews/pages/QueryResult/queryResultPane.tsx index b218d2047..fb33cd19e 100644 --- a/src/reactviews/pages/QueryResult/queryResultPane.tsx +++ b/src/reactviews/pages/QueryResult/queryResultPane.tsx @@ -136,6 +136,7 @@ export const QueryResultPane = () => { const [columns] = useState[]>(columnsDef); const items = metadata?.messages ?? []; + const [xmlPlanText, setXmlPlanText] = useState(""); const sizingOptions: TableColumnSizingOptions = { time: { @@ -198,6 +199,14 @@ export const QueryResultPane = () => { > {MESSAGES} + {metadata.resultSetSummary && metadata.isExecutionPlan && ( + + {"Query Plan"} + + )} {metadata.tabStates!.resultPaneTab == qr.QueryResultPaneTabs.Results && ( @@ -208,7 +217,6 @@ export const QueryResultPane = () => { }} /> )} - {